본문 바로가기
Java, Kotlin, Spring/JPA

JPA - 연관관계 매핑(N:1, 1:N, 1:1, N:N)

by Wordbe 2021. 9. 21.
728x90

JPA 005 - 다양한 연관관계 매핑

다대일, 일대다, 일대일, 다대다 매핑을 알아보자. (앞에 있는 객체가 외래키를 가지는 연관관계 주인이라고 가정한다.)

1. 다대일 (N:1)

저번시간에 JPA - 단방향, 양방향 연관관계 매핑 글에서

Comment (N) - Post (1) 관계에서 연관관계 주인을 Comment로 선택하여 외래키를 Comment가 관리하게 하는 예제를 보았다.

다대일 관계는 이를 말한다.

될 수 있으면 단방향 관계만 설정하고, 반대편 (Post) 에서도 조회하고 싶다면 양방향 관계를 추가하고 양방향 매핑을 주의해서 관리해주면 된다.

2. 일대다 (1:N)

일대다 단방향 매핑

일대다 관계는 자바 컬렉션(Collection, List, Set, Map) 중 하나를 사용해서 참조할 객체를 매핑하면 된다.

아래 예제를 보자. @OneToMany 로 연관관계를 설정했지만, 실제 외래키는 Comment 테이블에 생긴다.

@Entity
public class Post {
    @Id
    @GeneratedValue
    private Long id;
    private String title;
    private String content;

    @OneToMany
    @JoinColumn(name = "post_id") // Comment 테이블의 post_id 를 말한다. (FK)
    private List<Comment> comments = new ArrayList<>();
}
@Entity
public class Comment {
    @Id @GeneratedValue
    private Long id;
    private String content;
}

일대다(일 → 다 객체 잠조) 단방향 매핑은 권장하지 않는데 이유는 아래와 같다.

  • 복잡성 증가 : 테이블에서는 항상 외래키를 다 쪽에서 관리한다. 따라서 Comment 에 외래키 컬럼이 생성되지만, 일대다 단방향에서는 Post가 외래키를 관리한다.
  • 쿼리 성능 악화 : Post 를 저장하면 외래키를 관리하기 위해 Comment 의 UPDATE SQL 이 실행된다. 기존에는 Comment. Post 를 저장하는 쿼리만 실행되었다면, 이 때는 Comment 의 UPDATE 쿼리도 실행된다.

주의사항

  • @JoinColumn 를 사용해야 한다. 그렇지 않으면 의도치 않게 조인 테이블을 추가하여 사용하게 된다.

일대다 양방향

@OneToMany 를 사용한 객체의 필드는 연관관계의 주인이 될 수 없다. 관계형 데이터베이스에서 외래키는 항상 '다' 쪽에 있기 때문이다. 연관관계 주인은 항상 @ManyToOne 에 있으며, @OneToManymappedBy 가 없는 이유가 이것 때문이다.

@Entity
public class Comment {
    @Id @GeneratedValue
    private Long id;
    private String content;

      @ManyToOne
      @JoinColumn(name = "post_id", insertable = false, updatable = false)
      private Post post;
}

외래키는 현재 Post 의 comments 가 관리하기로 했는데, Comment 의 post 도 관리하면 문제가 발생할 수 있으므로 insertable = false, updatable = false 옵션을 사용해서 양방향 매핑을 할 수 있다.

하지만 일대다 단방향의 단점을 그대로 이어받기도 하고, 관리의 복잡성이 증가해서 단점이 크다.

3. 일대일 (1:1)

두 테이블 관계를 보면 주 테이블이 있고 대상이 되는 테이블이 있다.

대상 테이블을 주인으로 만들어 외래키를 매핑하는 것은 전통적인 관계형 데이터베이스 논리에 잘 맞는다.

하지만 JPA 를 사용하는 개발자에게는 주 테이블을 주인으로 하여 외래키를 매핑하는 것이 더 자연스럽고 사용하기 편리할 수 있다. 이 점을 잘 조절하는 것이 바람직 하다.

주 테이블에 외래키

Husband 를 주 테이블, Wife 를 부 테이블로 가정해보자.

단방향

@Entity
public class Husband {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToOne
    @JoinColumn(name = "wife_id")
    private Wife wife;
}
@Entity
public class Wife {
    @Id @GeneratedValue
    private Long id;
    private String name;
}
  • 남편(Husband) 이 자기 아내(Wife)에 해당하는 외래키를 가지고 있다. 그러므로 남편은 아내를 참조하여 조회할 수 있다.
  • 1에서 말한 다대일(@ManyToOne) 매핑과 똑같다.
  • 외래키가 주 테이블에 들어 있다.

양방향

@Entity
public class Husband {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToOne
    @JoinColumn(name = "wife_id")
    private Wife wife;
}
@Entity
public class Wife {
    @Id @GeneratedValue
    private Long id;
    private String name;

      @OneToOne(mappedBy = "wife")
        private Husband husband;
}
  • 아내도 남편을 참조하려면 Wife → Husband 참조를 만들어 양방향 매핑을 하면된다.
  • 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인이 가능하다.

대상 테이블에 외래키

단방향

이번엔 @OneToMany 의 상황이랑 비슷한데, JPA 는 대상 테이블이 외래키를 갖게 하는 일대일 단방향 매핑을 지원하지 않는다. 하지만 대상 테이블에 외래키를 갖게하는 일대다 단방향은 지원한다. (그래도 권장하지 않는다. 이유는 JPA - 단방향, 양방향 연관관계 매핑 에서 참고)

양방향

@Entity
public class Husband {
    @Id @GeneratedValue
    private Long id;
    private String name;

      @OneToOne(mappedBy = "husband")
    private Wife wife;
}
@Entity
public class Wife {
    @Id @GeneratedValue
    private Long id;
    private String name;

      @OneToOne
      @JoinColumn(name = "husband_id")
      private Husband husband;
}
  • 이번에는 대상 테이블 (Wife) 에 외래키가 존재한다.
  • 관계형 데이터베이스에서는 이 구성이 뭔가 자연스럽다.
  • 단점 : 프록시 기능의 한계로 Wife.husband 는 지연로딩이 가능한 반면, Husband.wife 는 지연로딩으로 설정하더라도 즉시 로딩된다.

4. 다대다 (N:N)

다대다 연관관계를 매핑하면, 두 테이블 사이에 조인 테이블이 생성된다. 관계형 데이터베이스에서는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없기 때문이다.

하지만 이 테이블은 두 테이블의 id 를 매핑하는 이외의 필드를 추가하지 못한다는 단점이 있다. 실제로는 조인 테이블 안에 추가 정보를 넣을 경우가 많으므로 이 조인 테이블을 실제 엔티티로 승격시켜 만드는 방법을 사용하는 것이 좋다.

@Entity
public class Book {

    @Id @GeneratedValue
    private Long id;

    private String title;

    @OneToMany(mappedBy = "book")
    private List<BookStore> bookStores = new ArrayList<>();
}

조인 테이블

@Entity
public class BookStore {

    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "book_id")
    private Book book;

    @ManyToOne
    @JoinColumn(name = "store_id")
    private Store store;
}
@Getter @Setter
@Entity
public class Store {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "store")
    private List<BookStore> bookStores = new ArrayList<>();
}

1번에서 다루었던 다대일 양방향 관계를 각각 조인 테이블에 매핑해주면 된다.

728x90

댓글