JPA

JPA 연관관계 매핑과 연관관계 주인

배털 2022. 3. 25. 09:47

이 글은 2021년 5월에 작성되었으며 블로그를 이전하며 옮기게 되었습니다.

시작하기 앞서 저의 글에 대한 피드백이나 지적은 언제나 환영입니다 😊

연관관계 매핑

용어를 먼저 이해해보자

  • 방향(Direction) : 단방향, 양방향
  • 다중성(Multiplicity) : 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 이해
  • 연관관계의 주인(Owner) : 객체 양방향 연관관계는 관리 주인이 필요하다.

연관관계는 왜 필요해?

"객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다."
-조영호(객체지향의 사실과 오해)

: 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력관계를 만들 수 없다.

  • 테이블은 외래키로 조인을 사용하여 연관된 테이블을 찾는다.
  • 객체는 참조를 사용해서 연관된 객체를 찾는다.
  • 테이블과 객체 사이에는 이런 큰 차이가 있다.
    즉, 제대로된 협력 공동체를 만들기 위하여 연관관계가 필요하다

연관관계 매핑 시 고려사항 3가지

  • 다중성
  • 단방향, 양방향
  • 연관관계의 주인

다중성

  • 다대일(N:1) : @ManyToOne
  • 일대다(1:N) : @OneToMany
  • 일대일(1:1) : @OneToOne
  • 다대다(N:M) : @ManyToMany

모두 대칭성이 있다.

테이블과 객체의 연관관계 차이

테이블

  • 외래키 하나로 양쪽 조인 가능
  • 사실 방향이라는 개념이 없다 객체
  • 참조용 필드가 있는 쪽으로만 참조 가능
  • 한쪽만 참조하면 단방향
  • 양쪽이 서로 참조하면 양방향 (사실 단방향이 2개)

단방향 연관관계

  • @ManyToOne
    ex) 하나의 팀이 여러 명의 멤버를 가질 수 있음
  • @JoinColumn(name = "TEAM_ID)
    ex) Join 하는 컬럼은 TEAM_ID 즉 Team을 TEAM_ID로 Join

양방향 연관관계

단방향 -> Member에서 Team으로 접근 가능하지만 Team에서 Member로 접근 불가하다
하지만 참조(reference)를 넣어두면 양쪽 다 참조해서 접근할 수 있게 된다.
양방향 연관관계를 만들어도 단방향과 비교해봐도 테이블에 변화가 없다 (테이블은 결국 다 Join으로 조회할 수 있기 때문이다) 테이블의 연관관계는 외래 키 하나로 양방향이 다 있는 것이다.

이때 객체에선 문제가 일어난다 Member에선 Team으로 갈 수 있지만 Team에선 Member에 접근할 수 있는 방법이 없었다.(객체 참조와 테이블의 외래 키의 가장 큰 차이이다)

그래서 Team에 List형식의 memebers를 넣어주면 양쪽으로 다 접근할 수 있게 된다.

  • JPA를 설계할 때 양방향 매핑은 하지 말고 단방향 매핑만 해놓자 (처음엔 단방향 매핑으로 설계를 끝내야 한다)
  • JPQL에서 역방향으로 탐색할 일이 더 많다
  • 단방향 매핑을 잘해놓고 양방향은 필요할 때 추가해도 됨 (테이블에 영향을 주지 않는다)

연관관계의 주인

  • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정한다.
  • 연관관계의 주인만이 외래 키를 관리한다. (등록, 수정)
  • 연관관계의 주인이 아닌 쪽(주인의 반대편)은 읽기(단순 조회)만 가능하다. (외래 키에 영향을 주지 않음)
  • 주인은 mappedBy속성을 사용하지 않는다.
  • 주인이 아니면 mappedBy속성으로 주인을 지정해준다
    ex) List members가 Team team을 mappedBy = "team"으로 team을 주인으로 지정하고 주인인 team이 외래 키를 관리한다.

그래서 누구를 주인으로 해야 하는데?


Member.team이 연관관계의 주인 (진짜 매핑)이다.
Team.members는 주인의 반대편(가짜 매핑)

외래 키가 있는 곳을 주인으로 정하자

그렇지 않으면 단점이 있다

  1. 일단 헷갈린다
  2. Team.members의 값을 바꿨다고 가정해보자 그럴 시 다른 테이블에 update쿼리문이 나가게 된다
    (외래 키 관련으로 (Team에서 insert쿼리, Member에서 update쿼리 등이 나가고 복잡해져 버린다.))
  3. 위와 같은 이유들로 성능 이슈 또한 날 수 있다.

외래 키가 있는곳을 주인으로 하는 규칙으로 하자!

이렇게 되면 설계가 깔끔해지며 외래키가 있는 테이블에서 관리가 된다는 장점이 있다.

  • DB입장에서 보면 외래 키가 있는곳이 무조건 '다'( * , N )이다.
  • 외래키가 없는 곳이 무조건 ( 1 )이 되는 것

즉 DB의 N 쪽이 무조건 연관관계의 주인이 된다
ex)@ManyToOne

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

  • 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안 됨
  • 연관관계의 주인은 외래 키의 위치를 기준으로 정해야 함

예를 들자면 자동차와 자동차의 바퀴가 있다.
비즈니스적으로 보면 자동차가 훨씬 중요하지만 연관관계의 주인을 바퀴로 잡는 격인 것이다.

정말 간단하게 말해서 연관관계의 주인은 그냥 N 쪽 '다'인 쪽을 연관관계의 주인으로 설정하면 된다.

양방향 연관관계 매핑 시 가장 많이 하는 실수

  • 연관관계 주인에 값을 입력하지 않는 것!
  • 실무에서 조회만 가능한 즉, 주인이 아닌 쪽(가짜 매핑)에 값을 넣고 DB에 값이 안 들어가는 사고가 난 사례가 있음*
    ex) members 즉, 연관관계의 주인이 아닌 곳 가짜 매핑에 입력하고 있었던 것!
    그래서 Member에 TEAM_ID가 null값이 들어가는 것이다.
  • 연관관계 주인에서 값을 지정해주자*
  • 양방향 매핑 시 무한루프를 조심하자
    ex) toString(), lombok, JSON생성 라이브러리
    toString(), lombok : toString()을 양쪽에서 출력하여 스택오버플로우 오류가 일어난다.
    JSON생성 라이브러리 : 쭉 다 뽑아버리기 때문에 엔티티를 JSON으로 바꾸는 순간 무한루프 -> 스택오버플로우(장애) 컨트롤러에서 엔티티를 바로 반환할 때 엔티티를 json으로 변환하며 연관관계가 양방향으로 걸려있다면 그때 계속 서로 파고들면서 장애가 남

오류 해결

  • toString : 써도 매핑된 것들을 빼고 써라
  • json생성 라이브러리 : 컨트롤러에서 절대 엔티티를 반환하지 마라 (DTO 사용하기)

사실 양방향 연관관계 매핑에는 정답이 있다.
바로 순수한 객체 관계를 고려하여 항상 양쪽 다 값을 넣어주는 것이다.

  • 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자
  • 연관관계 편의 메서드를 이용하자
    : 연관관계 편의 메서드를 사용 시 어느 쪽에 값을 넣어줄지는 개발자 마음대로이다.

연관관계 편의 메서드 예시

//연관관계 주인인쪽(Member)에서 작성
public void changeTeam(Team team) {
    this.team = team;
    team.getMembers().add(this);
}

//연관관계 주인이 아닌쪽(Team)에서 작성
public void addMembers(Member member) {
    member.setTeam(this);
    members.add(member);
}

글을 마치며

JPA에서 중요한 다른 한 가지 연관관계 매핑에 대해 정리해보았습니다.
JPA설계 등 에서 매우 중요하다고 생각되었으며
다음 글은 다중성을 이용한 다양한 연관관계 매핑입니다!
긴 글 읽어주셔서 감사합니다.😊