김영한님이 지음, [자바 ORM 표준 JPA 프로그래밍] 책을 읽고 정리한 필기입니다.📢

양방향 연관관계의 주의점

데이터베이스에 외래 키 값이 정상적으로 저장되지 않다면 주인이 아닌 곳에만 값을 입력했는지 확인해라.(제발!)

주인이 아닌 곳에만 값을 설정할 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void testSaveNonOwner(EntityManager em){
    //회원1 저장
    Member member1 = new Member("member1", "회원1");
    em.persist(member1);

    //회원2 저장
    Member member2 = new Member("member2", "회원2");
    em.persist(member2);

    Team team1 = new Team("team1", "팀1");
    //주인이 아닌 곳만 연관관계 설정
    team1.getMembers().add(member1);
    team1.getMembers().add(member2);

    em.persist(team1);
}

MEMBER 테이블 조회 결과

MEMBER_ID USERNAME TEAM_ID
member1 회원1 null
member2 회원2 null

위 코드는 연관관계의 주인인 Member.team에 아무 값도 입력하지 않았기 때문에 외래 키 TEAM_IDteam1이 아닌 null 값이 입력 됨

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

연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에는 저장을 안해도 되는가?

No. 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 바람직함

양쪽 방향 모두 값을 입력하지 않으면 객체를 사용할 때 문제 발생

1
2
3
4
5
6
7
8
9
10
11
12
13
public void test순수한객체_양방향(){
  //팀1
  Team team1 = new Team("team1", "팀1");
  Member member1 = new Member("member1", "회원1");
  Mbmber member2 = new Member("member2", "회원2");
  
  member1.setTeam(team1); //연관관계 설정 member1 -> team1
  member2.setTeam(team1); //연관관계 설정 member2 -> team1
  
  List<Member> members = team1.getMembers();
  System.out.println("members.size = " + members.size());
}
//결과 : members.size = 0

때문에, 객체까지 고려하면 양쪽 다 관계를 맺어야 함

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void testSave(EntityManager em){
    //팀1 저장
    Team team1 = new Team("team1", "팀1");
    em.persist(team1);

    //회원1 저장
    Member member1 = new Member("member1", "회원1");

    //양방향 연관관계 설정
    member1.setTeam(team1);          //연관관계 설정 member1 -> team1
    team1.getMembers().add(member1); //연관관계 설정 team1 -> member1
    em.persist(member1);

    //회원2 저장
    Member member2 = new Member("member2", "회원2");
    member2.setTeam(team1);          //연관관계 설정 member2 -> team1
    team1.getMembers().add(member2); //연관관계 설정 team1 -> member2
    em.persist(member2);

    em.flush();

    Team findTeam = em.find(Team.class, "team1");
    System.out.println("members.size = " + findTeam.getMembers().size());
}
//결과 : member.size = 2

team.getMembers().add(member) 를 하지 않을 경우 결과는 member.size = 0 이 출력 됨

연관관계 편의 메소드

양방향 연관관계는 결국 양쪽 다 신경 써야 하는데 각 방향마다 신경 쓰다보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있음

양방향 관계에서 두 코드는 하나인 것처럼 사용하는 것이 안전

1
2
3
member.setTeam(team);
team.getMembers().add(member);
//실수 여지가 있음
1
2
3
4
5
6
7
8
public class Member {
    ...
    public void setTeam(Team team){
        this.team = team;
        this.team.getMembers().add(this);
    }
    ...
} //실수를 없앨 수 있음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void testSave(EntityManager em){
    //팀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);

    em.flush();

    Team findTeam = em.find(Team.class, "team1");
    System.out.println("members.size = " + findTeam.getMembers().size());
}
//결과 : member.size = 2

연관관계 평의 메소드 : 한 번에 양방향 관계를 설정하는 메소드

연관관계 편의 메소드 작성 시 주의사항

아래와 같은 문제가 생길 수 있음

1
2
3
member1.setTeam(teamA);  //1
member1.setTeam(teamB);  //2
Member findMember = teamA.getMember(); //팀이 B로 변경 되었음에도 A팀에 member1이 여전히 조회 됨

img-name
삭제되지 않은 관계

연관관계를 변경할 때는 기존 객체와의 관계를 제거해줘야 한다.

1
2
3
4
5
6
7
8
public void setTeam(Team team){
    //기존 팀과 관계를 제거
    if(this.team != null){
    		this.team.getMembers().remove(this);
    }
    this.team = team;
    team.getMembers().add(this);
}

양방향 관계는 서로 다른 단방향 연관관계 2개를 양방향인 것처럼 보이게 하려고 비용이 많이 듬, 양방향을 사용하려거든 로직을 견고하게 작성해야 함

💡 Tip.

삭제되지 않은 관계에서 teamAmember 관계가 제거되지 않아도 데이터베이스 외래 키를 변경하는 데는 문제가 없다. `Team.members`는 연관관계의 주인이 아니기 때문이다. 문제는 영속성 컨텍스트가 아직 유지되는 상태에서 teamAgetMembers()를 호출하면 member1이 반환된다는 점이다. 따라서 변경된 연관관계는 기존 관계를 제거하는 것이 안전하다.

정리

단방향 매핑과 비교해 양방향 매핑은 복잡함, 주인 설정해야 하고, 로직 관리 해야 함

연관관계가 하나이자 언제나 연관관계의 주인인 단방향 관계에 주인이 아닌 연관관계를 추가했을 뿐

→ 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것뿐이다 !

3줄 요약

  • 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료 됨
  • 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가 됨
  • 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 함

💡 Tip. 연관관계의 주인을 정하는 기준

단방향은 항상 외래 키가 있는 곳을 기준으로 매핑하면 된다. 하지만 양방향은 연관관계의 주인(Owner)이라는 이름으로 인해 오해가 있을 수 있다. 비즈니스 로직상 더 중요하다고 연관관계의 주인으로 선택하면 안된다. 비즈니스 중요도를 배제하고 단순히 외래 키 관리자 정도의 의미만 부여해야 한다.

예를 들어 자동차의 차체와 바퀴를 생각해보면 바퀴가 외래 키가 있는 다 쪽이다. 따라서 바퀴가 연관관계의 주인이 된다. 차체가 더 중요한 것 같아 보이지만 연관관계의 주인은 단순히 외래 키를 매핑한 바퀴를 선택하면 된다. 따라서 연관관계의 주인은 외래 키의 위치와 관련해서 정해야지 비즈니스 중요도로 접근하면 안된다.

⚠️ 양방향 매핑 시에는 무한 루프에 빠지지 않게 조심해야 함. Member.toString() 에서 getTeam() 호출하고 Team.String()에서 getMember() 호출 시 발생 함. Lombok 사용시 특히 주의해야 하고 JSON 변환할 때 자주 발생하니 무한루프에 빠지지 않도록 방어해주는 어노테이션을 사용하자.

JPA 카테고리 내 다른 글 보러가기

댓글남기기