JPA

[JPA] 양방향 연관관계

제리 . 2020. 11. 21. 16:16

객체의 참조는 단방향이다. 반면 테이블에서의 join은 어느쪽이나 가능한다. 

 

member와 team의 관계는 다대일이다.  하나의 team에는 여러 member가 소속될 수 있다. 그렇기에 team의 객체에서는 컬렉션을 사용한다.

 

테이블의 연관관계는 team의 외래 키 하나로 양방향으로 조회할 수 있다. 양방향 객체의 연관관계를 만든다고해서 테이블에 새로 추가할 것은 없다.

양방향 연관관계 매핑

@Entity
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private String id;
    
    private String username;
    
    @ManyToOne
    @JoinColumn(name="TEAM_ID");
    private Team team;
    
    //연관관계 설정
    public void setTeam(Team team) {
    	this.team = team;
    }
    
    ...
    
}
@Entity
public class Team {

    @Id
    @Column(name = "TEAM_ID")
    private String id;
    
    private String name;
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();

	}
    
    ...
    
}

양방향 연관관계에서 객체에 추가되는 것은 Team에 @OneToMany부분이다. 그런데 mappedBy속성은 무엇일까?

 

mappedBy속성은 연관관계의 주인을 나타낸다. 엄밀히 말하면 객체는 양방향 관계라는 것이 없고 서로 다른 단방향 관계를 애플리케이션 로직으로 묶어 양방향인 것 처럼 보이게한다. 반면 데이터베이스 테이블은 양방향이 존재하고 외래 키하나로 조인을 할 수 있다. 엔티티에 단방향으로 매핑하면 참조를 사용한다. 그런데 양방향으로 매핑을 하면 두 군데에서 서로를 참조한다. 객체의 참조는 둘인데 외래키는 하나인 상황이 발생하는 것 이다. 이런 차이로 테이블의 외래 키를 관리할 주인을 설정해줘야한다.

 

연관관계의 주인은 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있다. 반면 주인이 아닌 쪽은 읽기만 가능하다. 주인은 mappedBy속성을 사용하지 않는다. 주인이 아닌 경우 mappedBy를 사용해서 주인을 지정한다. 

 

연관관계의 주인을 설정한다는 것은 외래 키 관리자를 선택하는 것이다. member엔티티의 team을 주인으로 선택하면 자기 테이블에 있는 외래키를 관리하면 된다. 만약 team엔티티의 members주인으로 선택하면 물리적으로 전혀 다른 테이블의 외래 키를 관리해야 한다. members가 있는 team엔티티는 TEAM테이블에 있는데 관리해야할 외래키는 MEMBER테이블에 있기 때문이다. 연관관계의 주인은 외래키가 있는 곳 이다.

 

정리하자면 연관관계의 주인은 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있다. 주인이 아니라면 읽기만 가능하고 외래 키를 변경할 수 없다.

 

*다대일, 일대다 관계에서는 항상 다 쪽이 외래키를 가진다. 

 

양방향 연관관계 저장

public void testSave() {
    //팀1 저장
    Team team1 = new Team("team1", "팀1");
    em.persist(team1);
    
    //회원1저장
    Member member1 = new Member("member1", "회원1");
    member1.setTeam(team1); //연관관계 설정
    em.persist(member1);
    
    //회원2저장
    Member member2 = new Member("member2", "회원2");
    member2.setTeam(team1); //연관관계 설정
    em.persist(member2);
}

양방향 연관관계에서 외래키를 관리하는 주인이 연관관계를 설정하면 된다.

 

양방향 연관관계의 주의점

public void testSave() {
    //회원1저장
    Member member1 = new Member("member1", "회원1");
    em.persist(member1);
    
    //회원2저장
    Member member2 = new Member("member2", "회원2");
    em.persist(member2);
    
    //팀1 저장
    Team team1 = new Team("team1", "팀1");
    em.persist(team1);
    team1.getMembers().add(member1);
    team1.getMembers().add(member2);
    
    em.persist(team1)
}

위의 코드는 연관관계가 저장되지 않는다. 주인이 아닌 team엔티티에 값을 저장했기 때문이다.

 

그렇다면 정말 주인에게만 값을 저장하는게 맞을까? 사실 양쪽 엔티티에 값을 모두 입력하는게 안전하다. 양쪽다 입력하지 않으면 JPA를 사용하지 않는 순수한 객체의 경우 문제가 발생할 수 있다.

public void test순수한객체_양방향() {

    //팀1
    Team team1 = new Team("team1", "팀1");
    Member member1 = new Member("member1", "회원1");
    Member member2 = new Member("member2", "회원2");

    member1.setTeam(team1); //연관관계 설정
    member2.setTeam(team1); //연관관계 설정
    
    List<Member> members = team1.getMembers();
    System.out.println(members.size()); // 결과 0
}

JPA를 사용하지 않은 순수한 객체이다 . members.size()의 결과가 0이 나온다. 한쪽에만 연관관계를 설정했기 때문이다. 그렇기때문에 team1의 members에도 객체를 추가해줘야한다.

public void test순수한객체_양방향() {

    //팀1
    Team team1 = new Team("team1", "팀1");
    Member member1 = new Member("member1", "회원1");
    Member member2 = new Member("member2", "회원2");

    member1.setTeam(team1); //연관관계 설정
    team1.getMembers().add(member1);
    member2.setTeam(team1); //연관관계 설정
    team1.getMembers().add(member2);
    
    List<Member> members = team1.getMembers();
    System.out.println(members.size()); // 결과 2
}

결과는 2가 나온다.

 

public void test순수한객체_양방향() {

    //팀1
    Team team1 = new Team("team1", "팀1");
    em.persist(team1);
    
    Member member1 = new Member("member1", "회원1");
    member1.setTeam(team1); //연관관계 설정
    team1.getMembers().add(member1);
    em.persist(member1);
    
    Member member2 = new Member("member2", "회원2");
    member2.setTeam(team1); //연관관계 설정
    team1.getMembers().add(member2);
    em.persist(member2);
    
}

JPA를 사용해서 양쪽다 관계를 맺으면 위의 코드가 된다. 순수한 객체상태에서도 동작하고 테이블의 외래 키도 정상 입력된다. 양쪽다 추가하는 것을 잊게되는 것을 방지하기 위해 setTeam()메서드에 team의 members의 값을 채워넣는 것도 좋은 방법이다. 단, 기존의 참조관계를 변경할 때 이전 참조를 삭제하는걸 잊지말자. team의 참조관계가 변경되어도 주인이 아니라 데이터베이스 외래 키를 변경하는데 문제는 없다. 하지만 team의 영속성 컨텍스트가 아직 살아있는 상태에서 getMembers()를 호출하면 이전 참조관계가 제거되지 않고 반환된다.

 

 

내용을 정리하자면

- 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.

- 양방향이 필요한 이유는 반대 방향으로 객체그래프를 탐색하기 위해서이다.

- 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야한다.