DB/JPA

[5장] 연관관계 매핑 기초

꾸준함. 2025. 2. 24. 23:59

단방향 연관관계

아래 그림의 다대일 단방향 관계는 다음과 같습니다.

  • 회원과 팀 객체가 있음
  • 회원은 하나의 팀에만 소속될 수 있음
  • 회원과 팀은 다대일 관계
  • 객체 연관관계에서는 Member.team 필드를 통해서 팀을 알 수 있지만 반대로 팀은 회원을 알 수 없기 때문에 단방향 관계
  • 반면, 테이블 연관관계에서는 회원 테이블의 TEAM_ID 외래 키를 통해 회원과 팀을 조인할 수 있고 반대로 팀과 회원도 조인할 수 있으므로 양방향 관계

 

https://oth3410.tistory.com/170

 

부연 설명

  • 참조를 통한 연관관계는 언제나 단방향
    • 객체 간에 연관관계를 양방향으로 만들고 싶은면 반대쪽에도 필드를 추가해서 참조를 보완해야 함
    • 양쪽에서 서로 참조하는 것을 양방향 연관관계라고 부르지만 정확하게는 양방향 관계가 아니라 서로 다른 단방향 관계 2개

 

  • 객체는 참조로 연관관계를 맺는 반면 테이블은 외래 키로 연관관계를 맺어 JOIN을 사용

 

1. 순수한 객체 연관관계

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

 

https://taegyunwoo.github.io/jpa/JPA_Relation_Basic

 

 

부연 설명

  • 회원1과 회원2는 팀1에 소속
  • 마지막 라인에 있는 코드를 통해 회원1이 속한 팀1을 조회 가능
  • 이처럼 객체가 참조를 사용해서 연관관계를 탐색하는 것을 객체 그래프 탐색이라고 부름

 

2. 테이블 연관관계

이번에는 데이터베이스 테이블의 회원과 팀의 관계입니다.

 

 

 

회원1이 소속된 팀을 조회할 경우 데이터베이스는 다음과 같이 외래 키를 사용해서 연관관계를 탐색하며 이것을 JOIN이라고 합니다.

 

 

3. 객체 관계 매핑

JPA를 사용해서 회원과 팀을 매핑하면 다음과 같습니다.

 

 

부연 설명

  • 앞서 객체 연관관계에서는 회원 객체의 Member.team 필드를 사용했고 테이블 연관관계에서는 회원 테이블의 MEMBER.TEAM_ID 외래 키 컬럼을 사용했음
  • 회원 엔티티에 있는 연관관계 매핑 부분을 보면 연관관계를 매핑하기 위한 새로운 어노테이션들이 있음
    • @ManyToOne: 이름 그대로 다대일 관계라는 매핑 정보로 연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해야 함
    • @JoinColumn(name = "TEAM_ID"): 조인 칼럼은 외래 키를 매핑할 때 사용하며 name 속성에는 매핑할 외래 키 이름을 지정, 회원과 팀 테이블은 TEAM_ID 외래 키로 연관관계를 맺으므로 해당 값을 지정

 

4. @JoinColumn

  • @JoinColumn은 외래 키를 매핑할 때 사용하며 속성은 다음과 같음

 

속성 기능 기본값
name 매핑할 외래 키 이름 {필드명}_ {참조하는 테이블의 기본 키 컬럼명}
referenceColumnName 외래 키가 참조하는 대상 테이블의 컬럼명 참조하는 테이블의 기본 키 컬럼명
foreignKey
(DDL)
외래 키 제약조건을 직접 지정

해당 속성은 테이블을 생성할 때만 사용
 
unique
nullable
insertable
updatable
columnnDefinition
table
@Column의 속성과 같음  

 

5. @ManyToOne

  • @ManyToOne 어노테이션은 다대일 관계에서 사용하며 주요 속성은 다음과 같음

 

속성 기능 기본값
optional false로 설정하면 연관된 엔티티가 항상 있어야 함 true
fetch 글로벌 패치 전략 설정 @ManyToOne = FetchType.EAGER
@OneToMany = FetchType.LAZY
cascade 영속성 전이 기능 사용  

 

연관관계 사용

 

1. 저장

 

 

부연 설명

  • JPA는 참조한 팀의 식별자 (Team.id)를 외래 키로 사용해서 적절한 INSERT 쿼리를 생성하며 생성된 쿼리는 다음과 같음
    • INSERT INTO TEAM (TEAM_ID, NAME) VALUE ('team1', '팀1')
    • INSERT INTO MEMBER (MEMBER_ID, NAME, TEAM_ID) VALUES ('member1', '회원1', 'team1')
    • INSERT INTO MEMBER (MEMBER_ID, NAME, TEAM_ID) VALUES ('member2', '회원2', team1')

 

2. 조회

  • 연관관계가 있는 엔티티를 조회하는 방법은 다음과 같이 크게 두 가지
    • 객체 그래프 탐색 (객체 연관관계를 사용한 조회)
    • 객체지향 쿼리 (JPQL) 사용

 

  • 앞서 저장한 대로 회원1, 회원2가 팀1에 소속해 있다고 가정할 경우
    • member.getTeam()을 사용해서 member와 연관된 team 엔티티를 조회할 수 있으며 이처럼 객체를 통해 연관된 엔티티를 조회하는 것을 객체 그래프 탐색이라고 함
    • 팀1에 소속된 회원만 조회하려면 회원과 연관된 팀 엔티티를 검색 조건으로 사용해야 하며 JPQL은 연관된 테이블을 조인해서 검색 조건을 사용

 

 

부연 설명

  • JPQL의 FROM Member m JOIN m.team t 부분을 보면 회원이 팀과 관계를 가지고 있는 필드 (m.team)를 통해서 Member와 Team을 조인
  • 그리고 WHERE 절을 보면 조인한 t.name을 검색 조건으로 사용해서 팀1에 속한 회원만 검색
  • 실행되는 SQL은 다음과 같음

 

 

3. 수정

 

 

부연 설명

  • em.update() 같은 메서드는 없으며 단순히 불러온 엔티티의 값만 변경해 두면 트랜잭션을 커밋할 때 flush가 일어나면서 변경 감지 기능이 작동함
    • 이후 변경사항을 데이터베이스에 자동으로 반영
    • 연관관계를 수정할 때도 참조하는 대상만 변경하면 나머지를 JPA가 자동으로 처리

 

4. 연관관계 제거

  • 연관관계를 null로 설정한 뒤 트랜잭션을 커밋하면 연관관계가 제거됨

 

 

이때 실행되는 연관관계 제거 SQL은 다음과 같습니다.

 

 

5. 연관된 엔티티 삭제

  • 연관된 엔티티를 삭제하기 위해서는 기존에 있던 연관관계를 먼저 제거하고 삭제해야 함
    • 그렇지 않으면 외래 키 제약조건으로 인해 데이터베이스에서 오류 발생
    • 앞서 에제에서 팀1에는 회원1과 회원2가 소속되어 있는데 이때 팀1을 삭제하려면 연관관계를 먼저 끊어야 함

 

 

양방향 연관관계

  • 다음 그림과 같이 회원과 팀은 다대일 관계이며 반대로 팀에서 회원은 일대다 관계
    • 일대다 관계는 여러 건과 연관관계를 맺을 수 있으므로 Team.members를 List 컬렉션으로 추가

 

https://taegyunwoo.github.io/jpa/JPA_Relation_Basic

 

 

  • 앞서 말했다시피 데이터베이스 테이블은 외래 키 하나로 양방향으로 조회 가능
    • 따라서 데이터베이스에 별도로 추가해야 할 내용은 없음
    • TEAM_ID 외래 키를 사용해서 MEMBER JOIN TEAM이 가능하고 반대로 TEAM JOIN MEMBER도 가능

 

1. 양방향 연관관계 매핑

 

 

부연 설명

  • 팀과 회원은 일대다 관계이므로 팀 엔티티에 컬렉션인 List<Member> members 추가
  • 일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보 사용
  • mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 됨
    • 반대쪽 매핑이 Member.team이므로 team을 값으로 부여

 

2. 일대다 컬렉션 조회

  • 회원 컬렉션으로 객체 그래프 탐색을 사용해서 조회한 회원들을 출력

 

 

연관관계의 주인

  • 엄밀히 이야기하면 객체에는 양방향 연관관계라는 것이 없으며 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 한 것
  • 반면 데이터베이스 테이블은 앞서 설명한 것처럼 외래 키 하나로 양쪽이 서로 조인할 수 있으므로 테이블은 외래 키 하나만으로 양방향 연관관계를 맺음
  • 엔티티를 단방향으로 매핑하면 참조를 하나만 사용하므로 해당 참조로 외래 키를 관리하면 됨
  • 그런데 엔티티를 양방향으로 매핑할 경우 회원 -> 팀, 팀 -> 회원 두 곳에서 서로를 참조
    • 따라서 객체의 연관관계를 관리하는 포인트는 두 곳으로 늘어남
    • 이처럼 엔티티를 양방향 연관관계로 설정할 경우 객체의 참조는 둘인데 외래 키는 하나가 되면서 둘 사이에 차이가 발생
    • 이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해야 하는데 이것을 연관관계의 주인이라고 함

 

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

  • 양방향 연관관게 매핑 시 지켜야 할 규칙이 있는데 두 연관관계 중 하나를 연관관계의 주인으로 정해야 한다는
    • 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있음
    • 반면 주인이 아닌 쪽은 읽기만 할 수 있음

 

  • 어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하면 됨
    • 연관관계 주인은 mappedBy 속성을 사용하지 않음
    • 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 함

 

  • 연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것
    • 앞서 에제에서는 회원 테이블에 있는 TEAM_ID 외래 키를 관리할 관리자를 선택해야 함
    • 만약 회원 엔티티에 있는 Member.team을 주인으로 선택하면 자기 테이블에 있는 외래 키를 관리하면 됨
    • 하지만 팀 엔티티에 있는 Team.members를 주인으로 선택하면 물리적으로 전혀 다른 테이블의 외래 키를 관리해야 함
      • Team.members가 있는 Team 엔티티는 TEAM 테이블에 매핑되어 있는데 관리해야 할 외래 키는 MEMBER 테이블에 있기 때문

 

https://velog.io/@conatuseus/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-%EA%B8%B0%EC%B4%88-2-%EC%96%91%EB%B0%A9%ED%96%A5-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EC%99%80-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EC%9D%98-%EC%A3%BC%EC%9D%B8

 

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

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

 

  • 정리하면 연관관계의 주인만 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있으며 주인이 아닌 반대편은 읽기만 가능하고 외래 키를 변경하지 못함
  • 데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가짐
    • 따라서 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없음

 

https://velog.io/@conatuseus/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-%EA%B8%B0%EC%B4%88-2-%EC%96%91%EB%B0%A9%ED%96%A5-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EC%99%80-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EC%9D%98-%EC%A3%BC%EC%9D%B8

 

양방향 연관관계 저장

 

 

부연 설명

  • 위 코드는 단방향 연관관계에서 살펴본 회원과 팀을 저장하는 코드와 완전히 같음
    • `team1.getMembers().add(member1);`과 같은 코드가 추가로 있어야 할 것 같지만 Team.members는 연관관계의 주인이 아니기 때문에 코드는 데이터베이스에 저장할 때 무시됨

 

  • 양방향 연관관계는 연관관계의 주인이 외래 키를 관리하기 때문에 주인이 아닌 방향은 값을 설정하지 않아도 데이터베이스에 외래 키 값이 정상 입력됨
  • 다시 말하지만 Member.team은 연관관계의 주인이며 EntityManager는 이곳에 입력된 값을 사용해서 외래 키를 관리

 

양방향 연관관계의 주의점

  • 데이터베이스에 외래 키 값이 정상적으로 저장되지 않을 경우 다음 실수를 의심해 보자
    • 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력했을 때 외래 키 값이 정상적으로 저장되지 않음

 

  • 다시 한번 강조하지만 연관관계의 주인만이 외래 키의 값을 변경할 수 있음
    • 따라서 member의 team을 setter를 통해 세팅해 줘야지만 외래 키의 값이 변경됨
    • 반대 방향인 team.getMembers()에 member들을 추가하는 코드만 있을 경우 반영이 안 됨

 

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

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

 

JPA를 사용하지 않고 엔티티에 대한 테스트 코드를 작성한다고 가정했을 때 다음 코드는 문제를 야기합니다.


 

문제가 발생하는 이유

  • 위 코드는 JPA를 사용하지 않는 순수한 객체인데 코드를 보면 Member.team에만 연관관계를 설정하고 반대 방향은 연관관계를 설정하지 않았기 때문에 마지막 줄에 로그에 2가 아닌 0이 출력됨
  • 순수한 객체 상태에서는 양방향일 경우 양쪽 다 관계를 설정해야 함
    • 회원 -> 팀을 설정하면 다음 코드처럼 반대 방향인 팀 -> 회원도 설정해야 함
    • team1.getMembers().add(member1); // 팀 -> 회원

 

양쪽 모두 관계를 설정하여 예상한 대로 동작하는 코드는 다음과 같습니다.


 

JPA를 사용해서 완성한 코드는 다음과 같습니다.


 

부연 설명

  • 위 코드는 양쪽에 연관관계를 설정했기 때문에 순수한 객체 상태에서도 동작하며, 테이블의 외래 키도 정상적으로 입력됨
  • 물론 외래 키의 값은 연관관계의 주인인 Member.team 값을 사용
  • 위 코드처럼 객체까지 고려해서 주인이 아닌 곳에도 값을 입력하여 양쪽 모두 관계를 맺어주는 것을 권장함

 

2. 연관관계 편의 메서드

  •  양방향 연관관계일 경우 객체까지 고려하면 양쪽 다 신경을 써 줘야 하는데 실수로 인해 둘 중 하나만 호출해서 양방향이 깨질 위험이 있음
    • 이 때문에 양방향 관계에서 두 코드는 하나인 것처럼 사용하는 것이 안전함
    • 다음 코드처럼 한 번에 양방향 관계를 설정하는 연관관계 편의 메서드를 정의하면 실수도 줄어들고 좀 더 그럴듯하게 양방향 연관관계를 설정할 수 있음


 

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

  • 사실 앞서 작성한 코드에서 setTeam(0 메서드에는 버그가 있음


https://velog.io/@conatuseus/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-%EA%B8%B0%EC%B4%88-2-%EC%96%91%EB%B0%A9%ED%96%A5-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EC%99%80-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84%EC%9D%98-%EC%A3%BC%EC%9D%B8

 

 

부연 설명

  • teamB로 변경할 때 teamA -> member1 관계를 제거하지 않았기 때문에 `삭제되지 않은 관계` 문제 발생
  • 연관관계를 변경할 때는 아래 코드처럼 기존 팀이 있을 경우 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 함
  • 정리하면 관계형 데이터베이스는 외래 키 하나로 문제를 단순하게 해결하는 반면 객체에서는 서로 다른 단방향 연관관계 2개를 양방향인 것처럼 보이게 하기 위해 많은 고민과 수고가 필요함


 

정리

  • 단방향 매핑으로 테이블과 객체의 연관관계 매핑은 이미 완료되어 있음
  • 단방향을 양방향으로 만들 경우 반대 방향으로 객체 그래프 탐색 기능이 추가됨
  • 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 함
    • 복잡하므로 우선 단방향 매핑을 사용하고 반대 방향으로 객체 그래프 탐색 기능이 필요할 때 양방향을 사용하도록 코드를 추가하는 것을 권장

 

  • 연관관계의 주인은 외래 키의 위치와 관련해서 정해야지 비즈니스 중요도로 접근하면 안 됨
    • 다대일 관계에서 무조건 다 쪽이 연관관계의 주인

 

참고

자바 ORM 표준 JPA 프로그래밍 - 김영한 저

 

반응형

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

[JPA] Hibernate MultipleBagFetchException  (0) 2023.06.28
[JPA] 준영속(Detached) 상태 엔티티 수정하는 방법  (0) 2023.05.07
[JPA] JPQL 추가 정리  (0) 2021.10.18
[JPA] JPQL 간단 정리  (0) 2021.10.16
[JPA] 값 타입 정리  (0) 2021.09.29