연관관계의 주인과 mappedBy

작성일

양방향 연관관계

@Entity
public class Member {
    ...
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

@Entity
public class Team {
    ...
    @OneToMany(mappedBy = "team")
    // 초기화 시켜놓으면 add 할때 오류가 안나기 때문에 관례로 이렇게 쓴다
    private List<Member> members = new ArrayList<>();
}
// member가 연관관계의 주인
// 조회
Team findTeam = em.find(Team.class, team.getId()); 
// 역방향 조회
int memberSize = findTeam.getMembers().size();

연관관계의 주인과 mappedBy

mappedBy를 이해하기 위해선 객체와 테이블간에 연관관계를 맺는 차이를 이해해야 한다.

1

객체의 양방향 연관관계

객체의 양방향 관계는 서로 다른 단방향 관계 2개이다. 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.

Member와 Team으로 예를 들면, 객체 연관관계는 회원에서 팀을 참조하는 연관관계(단방향)와 팀에서 회원을 참조하는 연관관계(단방향) 총 2개의 객체 연관관계가 있다.

테이블의 양방향 연관관계

테이블은 외래키 하나로 두 테이블의 연관관계를 관리한다.

Member와 Team으로 예를 들면, 테이블의 연관관계는 회원, 팀의 연관관계가 1개이다. MEMBER.TEAM_ID 외래키 하나로 양방향 연관관계를 가진다.(양쪽으로 조인이 가능하다.)

여기서, 연관관계의 주인이 필요한 이유가 나온다.

객체의 양방향 연관관계는 참조값이 2개이다. Member에 있는 Team 값이 업데이트 되었을 때와 Team에 있는 Member값이 업데이트 되었을 때 어떤 외래키 값을 업데이트 해야할지 딜레마가 온다.

연관관계의 주인(Owner)

양방향 매핑 규칙

객체의 두 관계 중 하나를 연관관계의 주인으로 지정해야 한다. 연관관계의 주인만이 외래키를 관리(등록, 수정) 할 수 있고 주인이 아닌 쪽은 읽기만 가능하다. 주인이 아닌 쪽에 mappedBy 속성을 넣어준다.

그렇다면, 누구를 주인으로 해야할까?

외래키가 있는 곳을 주인으로 한다. 예시에서는 Member.team이 연관관계의 주인이다. DB 입장에서 보면 외래키가 있는 곳이 N, 외래키가 없는 곳이 1 이다. 즉, N 이 연관관계의 주인이 되면 된다.

양방향 매핑 시 많이 하는 실수

양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고 주인이 아닌 곳에만 값을 입력하는 것이다.

Member member = new Member();
member.setName("member1");
em.persist(member);

Team team = new Team();
team.setName("TeamA");

// 주인이 아닌 곳만 연관관계 설정
team.getMembers().add(member);

em.persist(team);

위의 코드처럼 주인이 아닌 곳에만 값을 저장 후 Member 테이블을 조회하면 외래키인 TEAM_ID 값이 null로 저장된다. team은 연관관계의 주인이 아니기 때문에 member값을 넣어줘도 JPA에서 실행하지 않는다. 다시 한 번 강조하지만 연관관계의 주인만이 외래 키 값을 변경할 수 있다.

team.getMembers().add(member);
// 연관관계의 주인에 값을 설정!
member.setTeam(team)

그렇다면, 연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에는 값을 저장하지 않아도 될까?

순수한 객체까지 고려한 양방향 연관관계

결론부터 얘기하자면 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자. JPA입장에서 보면 member.setTeam(team)만 해주는게 맞지만, 객체지향의 입장에서 보면 양쪽에 값을 다 줘야한다.

양쪽을 다 설정해주다보면 사람이기 때문에 실수를 할 수 있다. 따라서, 연관관계 편의 메소드를 이용한다.

public class Member {
    ...
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

		// 연관관계 편의 메소드
    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}
Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setUsername("member1");
member.changeTeam(team); //연관관계 편의 메소드
em.persist(member);

em.flush();
em.clear();

그 밖의 양방향 매핑 시에 주의할 점은 무한 루프를 조심해야 한다.

예를 들면, toString(), lombok, JSON 생성 라이브러리가 있는데 우선, lombok의 toString()은 가급적 사용하지 말자. 또한, API를 만들때 엔티티는 반드시 DTO로 변경해서 반환해야 한다. (실제 실무에서는 가급적 controller에서 엔티티를 사용하면 안된다.)

양방향 매핑 정리

단방향 매핑만으로도 이미 연관관계 매핑은 완료된 것이고 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다. (JPQL에서 역방향으로 탐색할 일이 많다.)

단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 된다. (테이블에 영향을 주지 않는다.) 즉, 설계 시 단방향으로 설계 후 양방향은 개발하면서 필요할 때 추가한다. 연관관계의 주인은 외래키의 위치를 기준으로 정한다.

카테고리: