-
일대다 연관관계를 지양하자개발/JPA 2023. 7. 18. 12:31
다대일 연관관계
웬만하면 다대일 연관관계를 가져가는 게 맞다. 다음 테이블 관계도를 보자.
다(N)인 쪽에 외래키를 생성하는 것이 당연하다. 그리고 JPA는 당연히 그렇게 테이블을 만든다. Team에 Member 외래키가 생긴다면 같은 팀이 계속 추가되니까 정규화를 잘못한 거다. 외래키를 가지고 있는 Member에서 단방향으로 연관관계를 설정하고 team 객체를 주인으로 설정하는 게 자연스럽다. 단방향이니까 Team에는 아무런 설정이 필요 없다. 지연 로딩은 기본 정책으로 가져가자.
@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; @Column(name = "USERNAME") private String name; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "TEAM_ID") private Team team; // getter, setter 생략 }
이렇게 하면 Member를 통해 Team을 세팅하며 insert query가 나갈 때 Member 외래키가 정해지니까 추가적인 query 발생이 없다.
Team team = new Team(); team.setName("teamA"); em.persist(team); Member member = new Member(); member.setName("member1"); member.setTeam(team); em.persist(member1);
일대다 연관관계
일을 하다 보면 일대다 연관관계가 생길 수도 있다. 위 예제를 활용해서 확인해보자. 테이블 관계도는 동일하다. 앞서 말했듯 다(N)인 쪽에 외래키가 생성되기 때문이다. 객체 연관관계를 맺을 때 주인의 위치가 바뀌면서(Team에서 members를 참조하는 것으로 바뀜), 테이블이 다대일에서 일대다로만 바뀌었지 Member가 TEAM_ID 외래키를 가지는 기본 구조는 동일하다.
코드는 다음과 같다. 다대일하고 약간 달라서 어색하게 느껴질 수 있다. 일대다에서 일이 Team이 되므로 Team에서 설정한다.
@Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; @OneToMany @JoinColumn(name = "TEAM_ID") // 이걸 안 넣으면 자동으로 조인테이블이 생성됨 private List<AssociativeMemberV2> members = new ArrayList<>(); // getter, setter 생략 }
외래키를 지정하지 않으면 Member의 PK, Team의 PK를 이어주는 조인 테이블이 생성되는데 외래키를 지정하지 않았을 때의 기본 동작이다. 외래키를 꼭 지정해주자. Team의 members가 연관관계의 주인이 되므로 Team을 통해서 member를 추가해야 한다.
Member member = new Member(); member.setName("member1"); em.persist(member); // Team 저장이 되고 나서 // Member 테이블의 fk를 업데이트 한다. 왜냐하면 테이블 일대다 관계는 항상 다(N)쪽에 외래키가 있기 때문에 // Team 엔티티를 건드렸는데 Member 테이블이 업데이트되는 상황은 // 추적을 어렵게 만든다 Team team = new Team(); team.setName("team"); team.getMembers().add(member); em.persist(team);
멤버의 변경은 Member 테이블에서 이뤄져야 하는데 Team에 연관관계의 주인이 있는 관계로 Team을 통해서 Member를 변경해야 한다. member가 insert 될 때는 외래키카 설정될 수 없다. 그러므로 Team까지 insert 되고 나서 member update 되는 query가 발생하게 된다.
Team 객체를 건드렸는데 Member 테이블에서 query가 발생되는 상황은 유지 보수나 버그 수정 때 추적을 어렵게 만든다. 예제에서는 테이블이 두 개니까 와닿지 않을 수 있는데 실무에서는 프로젝트 크기에 따라 수십 개의 테이블이 복잡하게 얽혀 있으므로, 건드릴 수 있는 객체도 한둘이 아니다. 그리고 update query가 발생이 성능을 크게 떨어뜨리진 않겠지만 비효율은 여전히 비효율이다.
만약 일대다 구조에서 다(N)에도 양방향 관계를 설정하고 싶다면, JPA 공식 스펙으로 지원하는 건 아니지만 약간의 꼼수를 부릴 수 있다. 우리 예제에서 Member가 다(N)인데 기본적으로 @ManyToOne은 mappedBy을 지원하지 않아서 읽기 전용으로 바꿔줄 옵션을 @JoinColumn에서 넣어준다.
@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; @Column(name = "USERNAME") private String name; // 일(Team)대다에서 다소 억지로 양방향 관계 만들기 // 읽기 전용 맵핑 // 공식 스펙이 아님 @ManyToOne @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) private Team team; // getter, setter 생략 }
'개발 > JPA' 카테고리의 다른 글
페치 조인의 한계과 극복(요약 ver.) (0) 2023.07.25 MVCC=TRUE (0) 2023.07.18 영속성 전이와 고아 객체 (0) 2023.07.13 양방향 관계 시 주의사항 (0) 2023.07.11 시퀀스 최적화 (0) 2023.07.10