프로젝트를 하다보면 자연스럽게 다대일 양방향 매핑을 사용하여 연관관계 매핑을 했을 것이다. 나는 일대다 단방향 매핑으로 설정을 하다가도 불편함을 느껴 다른 코드를 참조하여 다대일 양방향 매핑으로 변경했었는데 책을 통해 해당 내용을 정리해보려한다.
일대다 단방향 [1:N] 매핑과 단점
일대다 관계의 예로 팀과 회원을 볼 수 있다. 하나의 팀은 여러 회원을 참조할 수 있고 (일대다) 팀은 회원들을 참조하지만 반대로 회원은 팀을 참조하지않는다. (단방향)
이를 UML 로 보면 아래와 같은데 일대다 관계에서 외래키는 항상 다(MEMBER)쪽에 있음을 확인할 수 있다.

그러면, 이를 코드로 나타내보자.
@Entity @Table @Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id @Column(name = "MEMBER_ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String passwd;
// builder ..
}
@Entity @Table @Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>(); // ★
// builder..
}

그런데 코드를 통해 구현했을 때에는 Member 엔티티에 외래키를 매핑할 수 있는 참조 필드가 없음을 알 수 있다.
이것은 일대다 단방향 매핑의 단점을 보여준다.
본인 테이블에 외래키가 있으면 엔티티의 저장과 연관관계 처리를 INSERT 쿼리 한 번으로 끝낼 수 있지만, 다른 테이블에 외래키가 있으면 UPDATE 쿼리가 추가로 실행된다.
성능 문제도 있지만 엔티티를 매핑한 테이블이 아닌 다른 테이블이 외래키를 관리해야해서 관리가 힘들어진다는 단점도 있다.
1:N 단방향 테스트
@Test
@DisplayName("일대다 단방향 테스트")
void oneToManySimplexTest(){
Member member = new Member("limnj@test.com", "1234", "limnj");
Team team = new Team("IT");
team.getMembers().add(member);
memberRepository.save(member);
teamRepository.save(team);
}
- 실행 결과

다대일 양방향[1:N, N:1] 매핑
왼 쪽이 연관관계의 주인이 되므로 일대다 양방향이 아닌 다대일 양방향 매핑을 사용한다.
더 정확히는 양방향 매핑에서 @OneToMany 는 연관관계의 주인이 될 수 없다. 왜냐하면 관계형 데이터베이스의 특성상 일대다, 다대일 관계는 항상 다 쪽에 외래키가 있기 때문이다.
그래서 @ManyToOne 에는 mappedBy 속성이 없다.
코드를 통해 확인해보자.
..
public class Member {
@Id @Column(name = "MEMBER_ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String passwd;
@ManyToOne
@JoinColumn(name = "TEAM_ID") // 연관관계 주인이므로 mappedBy 사용 X
private Team team;
public void setTeam(Team team){
this.team = team;
if(!team.getMembers().contains(this)){ // 무한 루프에 빠지지 않도록 체크
team.getMembers().add(this);
}
}
// builder ..
}
..
public class Team {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
public void addMember(Member member) {
this.members.add(member);
if (member.getTeam() != this) { // 무한 루프에 빠지지 않도록 체크
member.setTeam(this);
}
}
// builder ..
}

이제는 주인인 Member 엔티티에 외래키를 매핑할 수 있는 참조 필드를 확인할 수 있다.
또한, 무의미한 Update 쿼리를 막을 수 있게 되었다.
여기서 주의할 점은 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 입력하는 것이다.
그렇다면 연관관계의 주인에만 값을 저장하면 되는가? 라고 하면 그것도 아니다.
객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다. 양쪽 방향 모두 값을 입력하지 않으면 JPA 를 사용하지 않는 순수한 객체 상태에서 문제를 야기할 수 있게 된다.
결론은, 양 쪽 다 값을 저장하라는 것이다.
N:1 양방향 테스트
@Test
@DisplayName("다대일 양방향 테스트")
void manyToOneInteractiveTest(){
Team team = new Team("IT");
teamRepository.save(team);
Member member = new Member(1L,"limnj@test.com", "1234", "limnj");
member.setTeam(team); // 연관관계 설정 member -> team
team.addMember(member); // 연관관계 설정 team -> member
memberRepository.save(member);
Member findMember = memberRepository.findById(1L).get();
assertEquals(findMember.getTeam().getId(),team.getId());
int size = teamRepository.findAll().size();
assertEquals(size,1);
}
- 실행 결과

참고
김영한. 자바 ORM 표준 프로그래밍, 에이콘출판, 2015
'Web' 카테고리의 다른 글
| [Spring] Transaction 에 대해서 (3) | 2023.10.22 |
|---|---|
| [JPA] 지연로딩과 즉시로딩 (0) | 2023.10.08 |
| [Spring] CORS Configuration with Spring security (0) | 2023.09.17 |
| [Spring] 동시성 이슈와 synchronized 에 관하여 (0) | 2023.09.10 |
| [JPA] Dirty Checking 동작 방식 및 성능 개선 (2) | 2023.09.03 |