응애개발자
article thumbnail
Published 2023. 7. 13. 02:33
연관관계매핑 기초 Spring/JPA
728x90

1. 단방향 연관관계

ex) 다대일 단방향 관계 이해

  • 회원과 팀이 있다.
  • 회원은 하나의 팀에만 소속될 수 있다.
  • 회원과 팀은 다대일 관계다.

다대일 연관관계

 

- 객체 연관관계

  • 회원 객체는 Member.team 필드로 팀 객체와 연관관계를 맺는다.
  • 회원과 팀은 단방향 관계이다. 회원은 Member.team을 통해 팀을 알 수 있지만 반대로 팀은 회원을 알 수 없다.

- 테이블 연관관계

  • 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺는다.
  • 회원과 팀은 양방향 관계이다. MEMBER테이블의 TEAM_ID 외래 키 하나로 MEMBER JOIN TEAM과 TEAM JOIN MEMBER 둘 다 가능하다.

회원과 팀을 조인하는 SQL
팀과 회원을 조인하는 SQL

 

- 객체 연관관계와 테이블 연관관계의 가장 큰 차이

  • 객체 간의 연관관계를 양방향으로 만들고 싶으면 반대편에서도 연관관계를 하나 더 만들어야 한다. 결국 이것은 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.
  • 반면 테이블은 외래 키 하나로 양방향으로 조인할 수 있다.

 

- 객체 연관관계 vs 테이블 연관관계

  • 객체는 참조(주소)로 연관관계를 맺는다. (a.getB().getC())
  • 테이블은 외래 키로 연관관계를 맺는다. (JOIN)
  • 참조를 사용하는 객체의 연관관계는 단방향이다.
  • 외래 키를 사용하는 테이블의 연관관계는 양방향이다.
  • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.

 

순수한 객체 연관관계

밑은 JPA를 사용하지 않은 순수한 회원과 팀 클래스의 코드다.

회원과 팀 클래스

 

그 다음 회원1과 회원 2를 팀에 넣어보자.

동작 코드
클래스 관계, 순수한 객체 단방향, 다대일(N:1) 클래스
단방향, 다대일 인스턴스

이러면 다음 코드로 회원1이 속한 팀1을 조회할 수 있다.

Team findTeam = member1.getTeam();

이처럼 객체는 참조를 사용하여 연관관계를 탐색할 수 있는데 이것을 객체 그래프 탐색이라 한다.

 

테이블 연관관계

이번에는 DB 테이블의 회원과 팀의 관계를 살펴보자

테이블 DDL
테이블에 값 넣기
SQL로 회원 1 소속된 팀 조회

이처럼 DB는 외래 키를 사용해서 연관관계를 탐색할 수있는데 이것을 조인이라 한다.

 

객체 관계 매핑

이제 JPA를 사용해 매핑하자.

매핑한 회원 엔티티
매핑한 팀 엔티티

 

@ManyToOne : 말 그대로 다대일 관계라는 매핑 정보다. 연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해야 한다.

 

@JoinColumn(name="TEAM_ID") : 조인 컬럼은 외래 키를 매핑할 때 사용한다. 이 어노테이션은 생략 할 수 있다.

 

@JoinColumn

이 어노테이션은 외래 키를 매핑할 때 사용한다.

 

속성 기능 기본값
name 매핑할 외래 키 이름 필드명 + _ + 참조하는 테이블 기본 키 컬럼명
referencedColumnName 외래 키가 참조하는 대상 테이블의 컬럼명 참조하는 테이블의 기본 키 컬럼명
foreignKey(DDL) 외래 키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용한다.  
unique, nullable, insertable, updatable, columnDefinition, table @Column의 속성과 같다.  

*@JoinColumn 생략

@JoinColumn을 생략하면 외래 키를 찾을 때 기본 전략을 사용한다.

 

ex) @ManyToOne
private Team team;

  • @JoinColumn을 사용하지 않을 경우 필드명 + ‘_’ + 참조하는 테이블의 기본키 컬럼명으로 외래키가 자동 생성 된다.

@ManyToOne

이 어노테이션은 다대일 관계에서 사용한다.

속성기능기본값

속성 기능 기본값
optional false로 설정하면 연관된 엔티티가 항상 있어야 한다. true
fetch 글로벌 패치 전략을 설정 @ManyToOne=FetchType.EAGER
@OneToMany=FetchType.LAZY
cascade 영속성 전이 기능을 사용한다. {}
targetEntity 연관된 엔티티의 타입 정보를 설정 void.class

 

2. 연관관계 사용

저장

  • JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.
  • 영속상태가 아닌 엔티티를 참조해서 저장하려고 하는 경우 에러가 발생한다.

회원과 팀을 저장하는 코드
이때 실행된 SQL
DB 확인하기 위한 SQL
결과

조회

엔티티를 조회하는 방법

  • 객체 그래프 탐색
  • 객체지향 쿼리 사용

- 객체 그래프 탐색

member.getTeam()을 사용해서 member와 관련된 team 엔티티를 조회할 수 있다.

 

- 객체지향 쿼리 사용

JPQL 조인 검색

* : teamName 과 같이 : 로 시작하는 것은 파라미터를 바인딩받는 문법이다.

 

수정

팀1 소속이던 회원을 팀2에 소속하도록 수정

연관관계를 수정하는 코드
수정 SQL

 

제거

연관관계를 삭제하는 코드
삭제 SQL

 

연관된 엔티티 삭제

연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다. 그렇지 않으면 외래 키 제약조건으로 인해, DB 오류가 발생한다. 팀 1에는 회원 1,2가 소속되어 있다. 여기서 팀 1을 삭제하려면 연관관계를 먼저 끊어야 한다.

 

3. 양방향 연관관계

팀에서 회원으로 접근하는 관계 추가

양방향 객체 연관관계

회원과 팀은 N : 1 관계 , 반대로 팀과 회원은 1 : N 관계이다. 일대다 관계는 ㅇ여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야 한다. Team.members를 List 컬렉션으로 추가했다.

 

회원 -> 팀(Member.team)

팀 -> 회원(Team.members)

 

양방향 연관관계 매핑

매핑한 회원 엔티티
매핑한 팀 엔티티

mappedBy 속성은 양방향 매핑일때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 된다.

 

일대다 컬렉션 조회

일대다 방향으로 객체 그래프 탐색

 

4. 연관관계의 주인

mappedBy는 왜 필요할까?

엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다. 따라서 둘 사이에 차이가 발생한다. 이런 차이로 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인Owner이라 한다.

 

양방향 매핑의 규칙 : 연관관계의 주인

두 연관관계 중 하나를 연관관계의 주인으로 정해야 한다. 연관관계의 주인만이 DB 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.

둘 중 하나를 연관관계의 주인으로 선택해야 한다.

Member.team, Team.members 둘 중 어떤 것으로 선택해야할까 ?

연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다. 여기서는 회원 테이블에 있는 TEAM_ID 외래 키를 관리할 관리자를 선택해야 한다.만약 회원 엔티티에 있는 Member.team을 주인으로 선택하면 자기 테이블에 있는 외래 키를 관리하면 된다. 하지만 팀 엔티티에 있는 Team.members를 주인으로 선택하면 물리적으로 전혀 다른 테이블의 외래 키를 관리해야 한다. 

 

연관관계의 주인은 외래 키가 있는 곳

말 그대로 테이블에 외래 키가 있는 곳으로 정해야 한다. 여기서는 회원 테이블이 외래 키를 가지고 있으므로 Member.team이 주인이 된다. 주인이 아닌 Team.members 에는 mappedBy="team" 속성을 사용하여 주인이 아님을 설정한다. team은 연관관계의 주인인 Member 엔티티의 team 필드를 말한다.

연관관계의 주인과 반대편

주인만 연관관계와 매핑되고 관리할 수 있고, 반대편은 읽기만 가능하고 외래 키를 변경하지는 못한다.

 

 

5. 양방향 연관관계 저장

양방향 연관관계 저장

DB에서 회원 테이블을 조회해보자.

TEAM_ID 외래 키에 팀의 기본 키 값이 저장되어 있다.

양방향 연관관계는 연관관계의 주인이 외래 키를 관리한다. 따라서 주인이 아닌 방향은 값을 설정하지 않아도 DB에 외래 키 값이 정상 입력된다.

따라서 이 코드는 DB에 저장할 때 무시된다.

Member.team은 연관관계의 주인이다. 엔티티 매니저는 이곳에 입력된 값을 사용하여 외래 키를 관리한다.

 

6. 양방향 연관관계 주의점

가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다. DB에 외래 키 값이 정상적으로 저장되지 않으면 이것부터 의심해보자.

DB에서 회원 테이블을 조회해보자.

외래 키 TEAM_ID에 team1이 아닌null값이 입력되어 있는데, 연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문이다. 연관관계의 주인만이 외래 키의 값을 변경할 수 있다.Member.team에 아무 값도 입력하지 않았고, 따라서 TEAM_ID 외래 키의 값도 null 이 저장된다.

 

순수한 객체까지 고려한 양방향 연관관계

사실 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다. 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.

순수한 객체 연관관계

 

회원에서 팀은 설정했지만 팀에서 회원을 설정하지 않아서 size는 0이 나왔다.

이렇게 양쪽 모두 관계를 설정해야 기대했던 결과가 출력된다.

 

이제 JPA를 사용하여 만들어보자.

이렇게 양쪽에 연관관계를 설정하였다. 따라서 순수한 객체 상태에서도 동작하며, 테이블의 외래 키도 정상 입력된다. 

  • Member.team : 연관관계의 주인, 이 값으로 외래 키를 관리한다.
  • Team.members : 연관관계의 주인이 아니다. 따라서 저장 시에 사용되지 않는다.

결론 : 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주자.

 

연관관계 편의 메서드

양방향 관계에서는

두 코드는 하나인 것처럼 사용하는 것이 안전하다. (가끔 하나만 호출할수도 있기 때문에)

setTeam() 메서드 하나로 양방향 관계를 모두 설정하도록 변경했다. 연관관계를 설정하는 부분을 수정하자.

양방향 리팩토링

연관관계 편의 메서드 작성 시 주의사항

member1.setTeam(teamA)를 호출한 직후

다음으로 member1.setTeam(teamB)를 호출한 직후

teamB로 변경할 때 teamA -> member1 관계를 제거하지 않았다. 연관관계를 변경할 때는 기존 팀이 있으면 연관관계를 삭제하는 코드를 추가해야 한다.

기존 관계 제거

 

결국 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것 뿐이다.

'Spring > JPA' 카테고리의 다른 글

고급 매핑  (0) 2023.07.14
다양한 연관관계 매핑  (0) 2023.07.13
엔티티 매핑  (0) 2023.07.11
영속성 관리  (0) 2023.07.10
JPA 시작  (0) 2023.07.09
profile

응애개발자

@Eungae-D

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!