본문 바로가기
Back-end/java spring

[JPA] 다양한 연관관계 매핑

by kkkdh 2023. 2. 1.
728x90

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

  • 다중성 (연관관계 차수)
  • 단방향 or 양방향
  • 연관관계 주인 설정

다중성

  • 다대일: @ManyToOne
  • 일대다: @OneToMany
  • 일대일: @OneToOne
  • 다대다: @ManyToMany

다중성과 관련되어 헷갈리는 경우 반대 엔티티에서의 연관관계 차수를 생각해보자! (다중성은 대칭성을 띄기 때문이다.)

 

참고: 다대다 관계는 실무에서 사용하면 안 된다.
왜 그런지에 대해서는 뒤에서 공부하자.

단방향과 양방향

  • 테이블
    • 외래 키 하나로 양쪽 조인이 모두 가능
    • 사실상 방향이라는 개념이 없다.
  • 객체
    • 참조용 필드가 있는 쪽으로만 참조가 가능하다.
    • 한쪽만 참조하면 단방향
    • 양쪽에 필드를 만들어 서로 참조해야 양방향
    • 사실 양방향이란 개념은 없다. (참조의 입장에서 사실은 두 개의 단방향이다)

연관관계의 주인

연관관계의 주인은 객체 지향 설계의 패러다임과 DB 패러다임의 차이점으로 인해 생긴 개념이다.

 

테이블의 관점에서는 하나의 외래 키(FK)에 의해 양방향으로 연관관계에 의한 조회가 가능하지만, 객체는 양쪽의 entity 객체가 서로 참조를 위한 필드를 보유하고 있어야 한다.

 

따라서 앞서 정리한 내용처럼 연관관계의 주인으로써 테이블과의 연동을 위해 업데이트시 반영을 위한 기준이 되는 객체의 필드를 설정해야 할 필요가 있었다.

 

연관관계의 주인이 되는 필드는 외래 키를 관리하는 참조용 필드이고, 연관관계 주인의 반대편 필드는 DB의 외래 키에 영향을 주지 않고, 오직 단순 조회(read-only) 목적으로만 사용한다.


다대일 (N : 1)

가장 많이 사용하는 다대일 연관관계 관련 개념부터 정리해 보자.

위 다이어그램과 같이 다대일 관계에서는 '다' 쪽에서 항상 FK를 갖는다. 그렇기 때문에, '다'쪽의 entity 객체에서 연관관계의 주인이 되는 필드를 갖는다고 생각하면 된다.

(이는 FK를 보유한 엔티티 객체의 필드가 연관관계의 주인이 되기 때문이었다)

 

다대일 단방향 정리

  • 가장 많이 사용하는 연관관계이며
  • 다대일의 반대는 일대다!

 

다대일 양방향

양방향 연관관계가 된다고, 테이블에 전혀 영향을 주지 않는다.

참고: '일대다', '다대일' 이런 용어에서 앞의 글자가 연관관계 주인에 해당한다!

여기서도 '다'인 Member 쪽이 연관관계 주인인 필드를 보유하고 있다.

 

위 다이어그램에서 볼 수 있듯이, 다대일 연관관계를 양방향으로 전환하기 위해서는 객체의 설계만을 변경하면 되고, 다음과 같은 코드를 예시로 들 수 있을 것 같다.

 

public class Team{
    ...
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
    
    ...
}

mappedBy 속성을 연관관계 주인의 반대편에서 꼭 작성해줘야 한다.

 

이는 연관관계 주인이 해당 객체를 어떻게 매핑하고 있는지를 명시해 주는 작업이다.


일대다 (1 : N)

여기서는 '일' 쪽이 연관관계의 주인이다.

 

우선 일대다 단방향 연관관계부터 보자.

 

일대다 단방향

일단, DB 설계 제한상 '다'쪽에 무조건 FK가 들어가야 한다. (칼럼의 값이 배열의 형태가 될 수 없기 때문)

 

아주 이상하게도 Team 객체가 연관관계의 주인 입장이 되기 때문에, members list 필드를 변경해야 JPA에 의해 DB에 반영이 되고, 변경되는 테이블은 TEAM이 아닌 MEMBER가 될 것이다.

 

일반적으로 객체 변경시 객체와 매핑된 테이블이 변경되는 것과는 다른 구조이다.

조금 어색하지만, 이렇게 하면 동작한다고 함

try{
    Member member = new Member();
    member.setName("Kang");

    Team team = new Team();
    team.setName("Team A");

    team.getMembers().add(member);
    em.persist(team);

    transaction.commit(); // commit 해도 udpate query 날라가지 않음
}

member와 team을 하나씩 만들어 영속성 컨텍스트를 거쳐 DB에 반영되도록 하는 예시 코드를 하나 실행해 보면

update query가 하나 더 날라간다

위와 같이 insert 쿼리 이후에 update 쿼리가 추가적으로 DB로 전달됨을 확인할 수 있다.

 

이는 분명히 손해 (반대로 연관관계 주인을 설정했으면, 벌어지지 않았을 문제였다)

더 큰 문제는 설계 상에서 보이기에는 TEAM 엔티티를 건드렸음에도 MEMBER 테이블이 변경되는 내부적인 동작에 대한 이해가 부족한 경우 이해하기 힘든 동작이 진행된다는 것이다.

 

그래서 설계상으로 Member에서 Team을 참조할 일이 없음에도 불구하고, 약간의 손해를 감수하고 Member 엔티티에서 Team 엔티티를 참조하는 필드를 추가해서 양방향으로 사용하는 것이 다대일과 유사하면서도 그나마 동작 방식이 이해 가능한 설계가 된다고 함

 

일대다 단방향 정리

  • 일대다 단방향은 일대다(1 : N)에서 일(1)이 연관관계의 주인이 된다.
  • 테이블 일대다 관계는 항상 다(N)쪽에 외래 키(FK)가 있다.
  • 객체와 테이블의 차이 때문에, 반대편 테이블의 외래 키를 관리하는 특이한 구조
  • @JoinColumn을 꼭 사용해야 한다. 그렇지 않으면, 조인 테이블 방식을 사용하게 된다. (중간에 테이블을 하나 추가해서 사용하는 방식)

이렇게 생긴 join table이 만들어지므로 JoinColumn을 꼭 사용하자.

  • 일대다 단방향 매핑 단점
    • 엔티티가 관리하는 외래 키가 다른 테이블에 있다. (어마어마한 단점이다)
    • 연관관계 관리를 위해 추가로 UPDATE SQL이 실행된다. (다른 테이블을 변경해야 하기 때문이다)
  • 그래서 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하는 것이 좋다고 한다. (손해를 보더라도)

 

일대다 양방향

약간 억지스럽지만, 양방향도 있다.

  • 하지만, 이런 매핑은 공식적으로는 존재 X
  • @JoinColumn(name="TEAM_ID", insertable=false, updatable=false)
  • 읽기 전용 필드를 사용해서 양방향처럼 사용하는 방법이다.
  • 그냥 다대일 양방향을 사용하자!! (괜히 복잡하게 하지 말자)

일대일 (1 : 1)

  • 일대일 관계는 반대도 일대일
  • 주 테이블이나 대상 테이블 중에 FK 선택 가능하다.
    • 주 테이블, 대상 테이블 어디 넣어도 상관없다.
  • 외래 키에 데이터베이스 유니크(UNI) 제약 조건이 추가되어야 한다. (유일하게 연결되어야 하기 때문)
주 테이블과 대상 테이블을 구분하는 기준은 조회를 더 많이 하는 쪽을 주 테이블로 정의 한다고 한다.
주 테이블을 선정하기 위해서는 실제 개발 과정을 더 알아야 할 것 같다.

사물함과 사용자의 관계를 예로 들어보자.

회원마다 하나의 라커만 사용 가능

Member table과 Locker table 둘 중 어디에 외래 키를 넣어도 상관없다.

 

어노테이션만 다를뿐 다대일(@ManyToOne) 단방향 매핑과 유사함

Locker entity의 클래스 파일을 생성하고, Member entity 선언부에 위와 같이 locker 필드를 추가하면 된다.

TEAM_ID 칼럼 추가 (FK)

양방향 매핑도 다대일에서 했던 것과 같이 진행하면 된다. 

mappedBy로 지정하면 끝

다대일 관계에서는 list 컬랙션을 활용했던 것과 다르게 단일 객체를 사용하는 것만 다를 뿐이다.

 

일대일: 주 테이블에 외래 키 양방향 정리

  • 다대일 양방향 매핑과 같이 외래 키가 있는 곳이 연관관계의 주인
  • 반대편은 mappedBy를 적용

 

대상 테이블에 외래 키 단방향 관계는 JPA에서 지원하지 않는다. (연관관계의 주인은 주 테이블에 매핑된 객체인 상황)

하지만, 양방향 관계는 지원한다 → 이것도 사실 일대일 주 테이블에 외래 키 양방향과 매핑 방법이 동일하다.

(반대로 뒤집으면 주 테이블에서 연관관계 주인 필드를 갖는 것과 동일하다는 말)

 

 

<< 그렇다면, 외래 키를 Member에 넣는 것이 좋을까 Locker에 넣는 것이 좋을까..?? >>

→ 정답은 없다. 하지만

  • 예를 들어 시간이 지나서 비지니스 로직이 변경되어 한 명의 회원이 여러 라커를 가질 수 있게 되는 상황이 된다고 가정해보자.
  • 그런 경우에 Member에서 FK를 갖도록 하면, LOCKER table의 MEMBER_ID 칼럼이 UNI 제약 조건만 없애면 다대일 관계로 깔끔하게 변경할 수 있다.
  • 하지만, MEMBER 테이블에 FK를 넣으면??
    • 이건 변경해야 되는 포인트가 많아진다.. (테이블의 칼럼에는 배열 값을 갖도록 설계할 수 없는 제약이 있기 때문이다.)
  • 하지만, 개발자의 입장에서는 Member 객체가 locker 객체를 갖고 있는 것이 성능상 유리하다고 한다. (라커를 갖는 회원을 대상으로 로직을 짠다면??)
  • 따라서 이 부분은 선호하는 방식에 따라 가는 것이 맞는 것 같다..

 

일대일 정리

  • 주 테이블에 외래 키
    • 주 객체가 대상 객체의 참조를 갖는 것 처럼 주 테이블에 외래 키를 두고, 대상 테이블을 찾는다.
    • 객체지향 개발자가 선호하는 방식
    • JPA 매핑이 편리
    • 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
    • 단점: 값이 없으면, 외래 키에 null 허용
  • 대상 테이블에 외래 키
    • 대상 테이블에 외래 키가 존재한다.
    • 전통적인 데이터베이스 개발자가 선호하는 방식
    • 장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조를 유지한다.
    • 단점: 프록시 기능의 한계로 지연 로딩을 설정해도 항상 즉시 로딩됨 (이 부분은 추후에 더 공부하자)
  • 사실 강의를 들으며, 주 테이블과 대상 테이블 구분 기준과, 연관관계 주인이 주 테이블에 고정인 상황이 맞는지 헷갈렸는데, 구글링을 통해 주 테이블과 대상 테이블 선정 기준을 알 수 있었다. (아마 강의를 듣다가 놓친 것 같다)
  • 따라서 이번 챕터에서는 주 테이블에 해당하는 entity 객체를 연관관계 주인으로 고정한 상황에서의 개념 설명이라고 이해하면 될 것 같다.

다대다 (N : M)

실무에서는 사용하면 안되는 관계 유형이라고 한다. (조금 가볍게 들어도 될지도?)

 

  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
  • 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야만 한다.

요렇게

하지만, 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계를 표현할 수 있다. (이게 문제 쌍방향으로 List를 가지면 된다.)

 

또 패러다임이 다른 점에서 문제가 되는 것이다. 

 

  • @ManyToMany 어노테이션을 사용하면 되며
  • @JoinTable로 연결 테이블을 지정할 수 있다.
  • 다대다 매핑: 단방향, 양방향 모두 가능

이렇게 연결 테이블을 JoinTable 어노테이션으로 지정해줘야

편리하지만, 실무에서는 사용하지 않는다고 한다.

 

연결 테이블에서는 단순 연결을 넘어 다양한 데이터를 넣어 관리할 수도 있어야 한다. (수량, 주문 시간 같은 정보들)

 

다대다 한계 극복

  • 연결 테이블용 엔티티를 추가로 만든다. (매핑 테이블을 엔티티로 승격시킨다)
  • 다대다 관계 → 다대일 + 일대다

연결 테이블을 entity로 승격시킨다.
각각의 객체에서 ManyToMany를 다음과 같이 변경

이렇게 다대다의 한계를 일대다 + 다대일로 풀어서 해결할 수 있다. → 이제 연결 테이블에 더 많은 정보를 저장할 수도 있다.

 

참고: 연결 테이블의 PK로 보통 각 테이블의 PK를 FK로 가져와서 두 개를 합쳐 사용하곤 하는데, GeneratedValue를 따로 만들어 사용하는 것이 연결 테이블 entity를 조금 더 유연하게 쓸 수 있게 만들어준다고 한다. (김영한님 경험상 그렇다고 한다. 서비스를 계속해서 운영을 하다 보면 PK가 다른 곳에 종속되는 것에 제약이 많다고 한다.)

JoinColumn은 @Column 어노테이션과 유사하며, 다만 FK를 매핑하기 위한 목적으로 쓰인다는 점에서 차이가 있다.

 

referencedColumnName 속성을 통해 외래 키가 참조하는 대상 테이블의 기본 키가 아닌 다른 칼럼을 FK로 설정할 수 있다. (복잡한 경우에 사용한다)

 

참고로 @ManyToOne 어노테이션의 속성에는 mappedBy가 존재하지 않는데, 이를 통해서 다대일 관계에서 "다"쪽은 항상 연관관계의 주인이 되어야 함을 알 수 있다. (일대다 양방향이 공식적으로 존재하지 않는다는 것도 이 때문이다.)

728x90

'Back-end > java spring' 카테고리의 다른 글

[JPA] 프록시  (0) 2023.02.15
[JPA] 고급 매핑  (0) 2023.02.06
[JPA] 연관관계 매핑 기초  (0) 2023.01.30
[JPA] 엔티티 매핑  (0) 2023.01.27
[JPA] 영속성 컨텍스트에 대한 정리  (0) 2023.01.21

댓글