-
양방향 관계 시 주의사항개발/JPA 2023. 7. 11. 16:34
연관관계를 설정할 때 단방향을 기본원칙으로 하는 게 좋다. 설계 단계에서는 단방향으로 설정한다. 개발하면서 편의상 필요할 때 추가해야 사이드 이펙트를 줄일 수 있다. 양방향 관계에서는 고려할 사항이 늘어나기 마련인데, 양방향 관계에서 흔하게 일어날 수 있는 실수를 짚어본다. 다음 코드에서 Member(클래스, 테이블)이 team_id를 외래키로 가지고 있으므로 team을 연관관계의 주인으로 설정했다. 다음 코드는 동작할까? 편의상 여기에서 보이는 코드에서는 트랜잭션 부분은 생략한다.
JPA 관점
@Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; @OneToMany(mappedBy = "team") private List<MemberV3_2> members = new ArrayList<>(); } ... @Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; @Column(name = "USERNAME") private String name; @ManyToOne @JoinColumn(name = "TEAM_ID") private Team team;
TeamV2 team = new Team(); team.setName("TeamA"); manager.persist(team); Member member = new Member(); member.setName("member1"); team.getMembers().add(member); manager.persist(member); manager.flush(); manager.clear(); Member findMember = manager.find(Member.class, member.getId()); List<Member> members = findMember.getTeam().getMembers(); for (Member m : members) { System.out.println("m.getName() = " + m.getName()); }
밑에서 다섯 번째 줄인 findMember.getTeam()에서 NullPointerException이 발생한다. 왜냐하면 연관관계 설정 때 주인이 아닌 쪽은 조회만 가능하기 때문에 값을 넣어도(team.getMembers().add(member)) 영향이 없다. H2 Database 콘솔에서 확인해보면 MEMBER 테이블에 team_id에 null이 들어가 있는 걸 확인할 수 있다. 다만 이 코드에서는 예외 때문에 DB가 롤백되므로 해당 부분을 주석처리해서 DB를 확인한다.
JPA 관점에서 올바르게 동작하게 하기 위해서는 연관관계의 주인에 값을 할당해야 한다.
TeamV2 team = new Team(); team.setName("TeamA"); manager.persist(team); Member member = new Member(); member.setName("member1"); member.setTeam(team); // 추가 // team.getMembers().add(member); // 없어도 됨 manager.persist(member); manager.flush(); manager.clear(); Member findMember = manager.find(Member.class, member.getId()); List<Member> members = findMember.getTeam().getMembers(); for (Member m : members) { System.out.println("m.getName() = " + m.getName()); }
위 코드는 flush() 함수를 통해 영속성 컨텍스트를 DB와 동기화하고, clear() 함수를 통해 영속성 컨텍스트를 초기화 하고 나서 지연 로딩에 의해 manager.find에서 Member를 가져오는 select query가 실행되고, findMember.getTeam().getMembers()에서 select query가 실행되기 때문에 List<Members> members에 값이 할당될 수 있다. 주석처리한 부분인 team.getMembers().add(member)를 하지 않았는데도 members가 초기화될 수 있는 이유다. 그럼 flush(), clear()가 없다면 어떻게 될까.
객체지향 관점
JPA 관점에서는 위 코드가 아무 문제가 없겠지만 객체 지향 관점에서는 꺼림칙한 부분이 있다. 객체에 값을 설정하지 않았는데도 해당 값을 사용할 수 있기 때문이다. 영속성 관리는 메모리에서 이루지므로 flush(), clear()를 하지 않는다면 (JPA가 DB에서 데이터를 가져오도록 하지 않는다며) members에는 빈 배열이 들어온다. 영속성 컨텍스트에 해당 데이터가 없기 때문이다. select query가 나가지 않는다. member 컬렉션이 없은 team이 메모리에서 관리되고 있기 때문에 member가 없다.
이를 해결하기 위해서 양방향 연관관계에서는 주인과 주인이 아닌 쪽 모두에 값을 설정하자.
team.getMembers().add(member);
순수 객체 상태를 고려해 양쪽에 값을 설정해야 하지만 매번 양쪽에 수동으로 값을 세팅하는 건 버그를 일으킬 위험이 있으므로 편의 메서드를 작성하면 좋다. Member 쪽에 만들어보면 다음과 같다.
public void changeTeam(Team team) { this.team = team; team.getMembers().add(this); }
이 함수 하나만 호출하면 원자적으로 양방향에 값 설정이 된다. 실무에서는 비즈니스 로직이 더 복잡해질 수 있지만 기본 개념은 알아두자. 참고로 편의 메서드는 Team쪽에도 만들어도 된다. 상황에 따라 해답이 다를 테니, 어느 한쪽에만 만들어서 사용해야 한다.
'개발 > JPA' 카테고리의 다른 글
페치 조인의 한계과 극복(요약 ver.) (0) 2023.07.25 MVCC=TRUE (0) 2023.07.18 일대다 연관관계를 지양하자 (0) 2023.07.18 영속성 전이와 고아 객체 (0) 2023.07.13 시퀀스 최적화 (0) 2023.07.10