Post

Springboot Jpa - 연관관계 매핑 기초

연관관계의 매핑하는 방법을 알아보자.

Springboot Jpa - 연관관계 매핑 기초

관계형 DB와 객체의 패러다임 차이에서 오는 어려움이 있지만, 기초부터 찬찬히 학습해보자.

단방향 연관관계

객체에서 참조가 한 방향으로만 되어있는 관계를 단방향 연관관계라고 한다.

연관관계가 필요한 이유

관계형 DB와 객체의 차이

  • 테이블: 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
  • 객체: 참조를 사용해서 연관된 객체를 찾는다.

이러한 차이를 극복하기 위해서는 연관관계가 필요하다.

단방향 연관관계

객체를 테이블에 맞춰 모델링 객체를 테이블에 맞춰 모델링

객체 지향 모델링 객체 지향 모델링


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
public class Member {
  @Id @GeneratedValue
  private Long id;
  @Column(name = "USERNAME")
  private String name;
  @Column(name = "TEAM_ID")
  private Long teamId;
  // 생략
}
@Entity
public class Team {
  @Id @GeneratedValue
  private Long id;
  private String name;
  // 생략
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity
public class Member {
  @Id @GeneratedValue
  private Long id;
  @Column(name = "USERNAME")
  private String name;
  @ManyToOne
  @JoinColumn(name = "TEAM_ID")
  private Team team;
  // 생략
}
@Entity
public class Team {
  @Id @GeneratedValue
  private Long id;
  private String name;
  // 생략
}

객체를 테이블에 맞춰 모델링시 team의 id를 저장하며, Member에서 Team의 정보는 team_id로 한번 더 Team의 정보를 조회해야한다. 이러한 방식은 객체 지향적이지 않은 방식이다.

객체 지향 모델링을 한다면 team을 저장하며, MemberTeam의 정보를 가지고 있어, team_id로 한번 더 조회하지 않아도 된다.


양방향 연관관계

관계형 DB에서는 연관관계의 방향이 따로 없다. MEMBER 테이블을 중심으로, TEAM의 정보를 조회하는 것과 TEAM 테이블을 중심으로, MEMBER 정보를 조회하는 것은 MEMBER테이블의 외래키 TEAM_ID로 다 가능하기 떄문이다.

그런데 객체 지향에서는 다르다. 아까 예제에서, Team에서 getMembers()를 한다면 팀원 정보가 나올 수 없다. 팀원 정보를 보여주기 위해서는 양방향 매핑을 해주어야한다.

이것이 객체 참조와 테이블의 외래키의 가장 큰 차이점이다.

양방향 매핑

위의 예제를 양방향 매핑으로 바꿔보자.

양방향 매핑 양방향 매핑

1
2
3
4
5
6
7
8
9
10
11
@Entity
public class Team {
  @Id @GeneratedValue
  private Long id;
  private String name;
  
  @OneToMany(mappedBy = "team")
  List<Member> members = new ArrayList<Member>();
  //생략
}

Member는 유지하고, Team에만 추가한다.

  • 6~7 TeamMember에 대한 연관관계를 추가했다.

Team -> Member 방향의 객체 그래프 탐색과 Member -> Team 방향의 객체 그래프 탐색이 모두 가능하다.

객체와 테이블의 관계를 맺는 차이

이제 테이블의 연관관계와 같을까? 이렇게 변경한다고 해도 차이가 존재한다.

양방향 매핑 양방향 매핑

객체 연관관계
2개
회원 -> 팀 연관관계 1개 = 참조값이 Member에서 Team참조 ,단방향
팀 -> 회원 연관관계 1개 = 참조값이 Team에서 Member참조 ,단방향
객체의 양방향 관계는 사실 양방향 관계가 아닌, 다른 단방향 2개를 의미한다.
테이블 연관관계
1개
회원 <-> 팀의 연관관계 1개 = 참조값이 MEMBER테이블의 TEAM_ID의 참조로 양방향 조회, 양방향

연관관계의 주인

딜레마 발생

방금 양방향 매핑을 한 객체의 연관관계에서, 멤버가 팀을 변경한 상황이라고 생각해보자.

MEMBER 테이블의 TEAM_ID 업데이트는 어느 경우에 되어야할까?

  • Member에서 team을 변경할 때
  • Teammembers가 변경되었을 때

사실 둘 다 수정을 해야하는 경우가 맞고, 사실 DB에서는 TEAM_ID만 변경되면 된다.

이 경우를 대비해 연관관계의 주인을 정하는 규칙이 생겼다. MemberTeam 중 하나만 외래키를 관리하는 주인으로 지정하는 것이다.

연관관계의 주인

양방향 매핑 규칙
객체의 두 관계중 하나를 연관관계의 주인으로 지정한다.
등록, 수정이 있을 때, 연관관계의 주인만이 외래 키를 관리한다.
주인이 아니면 mappedBy 속성으로 주인 지정하고, 주인은 @JoinColumn을 사용해 외래키를 매핑한다.
주인이 아닌쪽의 참조데이터(=Teammemebers)는 읽기만 가능하다.
연관관계의 주인을 정하는 가이드
외래키가 있는 곳을 주인으로 지정한다.

외래키를 포함하는 곳을 주인으로 지정한 이유

  • Team을 변경시 MEMBER테이블에 대한 query가 생성되는 것보다, Member를 변경시 MEMBER테이블에 대한 query가 생성되는 것이 더 직관적이다.
  • 반대의 경우를 지정하면 성능 이슈(이후 내용에서 설명)

연관관계의 주인을 설정한 매핑

가장 많이하는 실수

연관관계의 주인에 값을 미입력

1
2
3
4
5
6
7
8
9
10
Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");

team.getMembers().add(member);

em.persist(member);
  • 1~3 팀 생성 및 저장
  • 5~6 멤버 생성
  • 8 실수하는 부분! 역방향(주인이 아닌 방향, team)만 연관관계 설정
    역방향인 team에 값을 수정해도 읽기 전용이라 아무런 일도 발생하지 않는다.
    • 10 멤버 저장, 결과 = 멤버는 저장이되지만, 해당멤버의 팀 정보는 누락된다.
연관관계 편의 메서드 생성

양방향 매핑에 있는 양쪽(주인과 주인이 아닌 방향) 모두에 값을 넣어주자.

  • 값을 연관관계의 주인에게 입력하는 것을 누락하는 실수를 방지하기 위해
  • 순수한 객체 관계를 고려하면 항상 양쪽다 값을 입력해야 해서
  • 또 다른 실수를 방지하기 위해
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    Team team = new Team();
    team.setName("TeamA");
    em.persist(team);
    
    Member member = new Member();
    member.setName("member1");
    team.getMembers().add(member);
    
    member.setTeam(team);//연관관계의 주인에 값 설정
    em.persist(member);
    
    : 한쪽에만 값을 넣어준다면, team.getMembers() 호출시 제대로 값이 나오지 않아 문제가 발생할 가능성이 생긴다.
    commit()이나 flush(), clear(), team 새로 조회 를 하기 전까지는 team의 정보는 1차 캐시에서 조회되기 때문에 값이 조회되지 않는다.
    테스트 케이스 작성시에는 JPA없이 자바 코드 상태로 작성하는데, 이때에도 문제가 된다.

가이드

항상 양쪽에 값을 넣어주면 되는데, 모든 코드에 대해서 하기가 번거롭기도 하고 그러려고 했지만 누락되는 경우도 있다. 이런 경우를 방지해 편의 설정을 위한 메서드를 생성하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
public class Member {
  // 생략  
  
  @ManyToOne
  @JoinColumn(name="TEAM_ID")
  private Team team;
  
  public void changeTeam(Team team) {
    this.team = team;
    team.getMembers().add(this);
  }
  // 생략
}
  • 11 team이 변경시 사용되는 Member.changeTeam()을 정의하고, 내용으로 team변경과 team.members에도 같이 추가될 수 있도록 작성한다.
    조금 더 세세히 작성하자면, 기존에 있던 팀의 members에서 자신을 제거 후 새로운 곳에 추가해야한다.
    반대로 Team에서 member를 저장하도록 하는 방법도 있는데 이건 개인적으로 선택해 사용하면 된다.

무한 루프

가능성

  • 연관관계 편의 메서드를 양방향에서 다 작성
    member의 편의 메서드가 team의 편의메서도 호출 -> team의 편의 메서드가 member의 편의 메서드 호출 … 무한루프
  • toString() 정의시 IDE를 이용한 방식이나 lombok을 이용한 방식으로 작성하는 것
    member.toString()에서 team.toString() 호출 -> team.members.toString()에서 각 멤버에 대한 member.toString() 호출 … 무한루프
  • JSON 라이브러리 -> Controller에서 직접 Entity 리턴시, 응답을 하기 위해 Entity를 json으로 변환하려 한다.
    JSON 변환시에도 위와 비슷한 방식으로, 전부 각 필드의 값을 모두 변환하기 위해 계속 호출해 무한루프가 생성된다.

가이드

  • 연관관계 편의 메서드를 양방향에서 다 작성 -> 한 쪽을 정해서 한쪽만 작성
  • toString() -> lombok의 @toString 사용 지양
  • JSON 라이브러리 -> Controller에서 Entity 반환 금지, DTO 사용

Controller에서 Entity 직접 반환 금지를 권고하는 또 다른 이유 JSON의 문제 외에도 EntityController에서 반환하게 된다면, Entity 변경시 마다 API 스펙이 변경되기 때문에 DTO 사용을 권장한다.

정리

지금까지 단방향과 양방향의 연관관계가 무엇인지, 어떻게 설정하는지, 또 양방향 연관관계 설정시 많이하는 실수에 대해 알아보았다.

추가로 실제 JPA 설계시에는 단방향 매핑만으로도 이미 연관관계 매핑이 되도록 설계하고, 이후 반대 매핑이 필요한 부분만 양방향으로 설계하는것이 좋다. 양방향을 나중에 고려하는 이유는 양방향에 대한 연관관계는 jpa 코드만 추가 작성되면 되기 때문에 언제든 수정할 수 있기 때문이다. 만약 단방향을 추가하는 경우라면, DB에 영향이 가게된다.

This post is licensed under CC BY 4.0 by the author.