Post

연관 관계 기초





참고



JPA 연관 관계 매핑

  • JPA에서의 테이블 연관 관계 매핑에 대해 알아본다.

  • 고려할 것은 세 가지가 있다.

    • 방향
      • 단방향, 양방향
    • 연관 관계 주인
      • 양방향 연관 관계에서 관리 주체
    • 다중성
      • 일대일(1:1), 일대다(1:N), 다대일(N:1), 다대다(N:M)

단방향, 양방향

(이미지)

  • DB 테이블은 방향의 개념이 없다.
    • 항상 JPA 엔티티의 양방향과 같다.
    • 하나의 외래키로 양쪽 테이블에서 조인이 가능하다.
  • JPA는 연관 관계를 엔티티 객체끼리의 참조로 구현하기 때문에 단방향, 양방향의 개념이 있다.
    • 하나의 엔티티 객체만이 다른 엔티티 객체를 참조하는 경우가 단방향
    • 양쪽 엔티티 객체가 서로 참조하는 경우가 양방향이다.



단방향

  • 단방향은 단순하게 하나의 엔티티에서 다른 엔티티를 멤버 필드로 참조하는 경우이다.
    • @OneToOne, @OneToMany, @ManyToOne, @ManyToMany 어노테이션을 사용한다.
  • 당연하게도 참조하는 테이블에서 외래키를 관리한다.
    • 참조하는 쪽의 테이블에서 참조되는 테이블의 id 값을 외래키로 갖는다.
  • 역시 당연하게 참조하는 객체만이 참조되는 객체를 조회할 수 있다.



  • 책 정보를 갖는 Book 엔티티와 책장 정보를 갖는 Bookshelf 엔티티가 있다고 할 때,
1
2
3
4
5
6
7
@Entity
class Book {
    // id 등 생략

    @ManyToOne
    private Bookshelf bookshelf;
}

이처럼 단방향 연관 관계 매핑을 할 수 있다.



양방향

  • 양방향은 양쪽 엔티티 객체가 서로를 참조하는 경우이다.
    • 단방향과 달리 양쪽 객체가 서로를 조회할 수 있다.
  • 그럼 무조건 양방향으로 하면 편한 거 아님?
    • 당장은 편하겠지만, 나중에 프로젝트가 커질 수록 복잡도가 점점 증가하게 된다.
    • 불필요한 연관 관계를 늘려 복잡도를 올리는 것 보다는 당장 필요하다고 느끼는 단방향 연관 관계를 맺어 두고,
    • 개발 중에 양방향의 필요성을 느끼면 그때 양방향으로 매핑하는 것이 좋다고 한다.



  • 책 정보를 갖는 Book 엔티티와 책장 정보를 갖는 Bookshelf 엔티티가 있다고 할 때,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Entity
class Book {
    // id 등 생략

    @ManyToOne
    private Bookshelf bookshelf;
}

@Entity
class Bookshelf {
    // id 등 생략

    @OneToMany
    private List<Book> bookList;
}
  • 위의 경우는 양방향 매핑이 아니라 단방향 매핑이 두 개라고 해야한다.
  • 양방향 연관 관계 매핑을 위해서는 둘 중 하나에게 연관 관계의 주인을 지정해줘야 한다.





연관 관계 주인

  • 연관 관계의 주인이라는 것은
    • 외래키를 관리하며,
    • 삽입, 수정, 삭제 등의 제어 권한을 갖는다.
    • 반대로 연관 관계의 주인이 아닌 애는 조회만 가능하다.
  • 연관 관계 주인은 어떻게 설정하나
    • 연관 관계 어노테이션에 mappedBy 속성을 통해 설정할 수 있다.
    • mappedBy가 붙지 않은 쪽이 연관 관계의 주인이다.
    • mappedBy가 붙은 애는 연관 관계의 주인인 테이블이 본인을 어떤 변수명으로 나타내는지 적어줘야 한다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Entity
class Book {
    // id 등 생략

    @ManyToOne
    private Bookshelf bookshelf;
}

@Entity
class Bookshelf {
    // id 등 생략

    @OneToMany(mappedBy = "bookshelf")
    private List<Book> bookList;
}
  • 위의 경우에 Book이 연관 관계의 주인이라고 할 수 있다.
    • 연관 관계의 주인인 Book은 Bookshelf를 bookshelf라는 변수명으로 두었다.
      • Bookshelf는 mappedBy 속성으로 “bookshelf”를 줘야한다.



그럼 누구를 연관 관계의 주인으로 설정해야 하는가?

  • 이것은 DB에서 외래키를 관리하는 쪽이 누구인가를 생각하면 된다.
  • 보통 다대일 관계에서 ‘다’쪽이 외래키를 관리한다.
  • JPA에서도 같은 맥락으로 생각하여 연관 관계 주인을 정해주면 가장 자연스럽다.



주의점

  • DB에서는 실제로 양방향 연결이기 때문에 한 쪽에서 수정이 이뤄지면 된다.
  • 하지만 JPA의 엔티티는 객체이기 때문에 문제가 발생할 수 있다.

  • Book-Bookshelf 예제를 계속 사용해보면
    • book.setBookshelf(bookshelf);
      • Book이 연관 관계의 주인이기 때문에 테이블에 외래키로 bookshelf의 아이디가 잘 들어간다.
      • 그러나 bookshelf에는 book이 add되지 않았으므로 코드 레벨에서 데이터 동기화가 발생한다.
    • bookshelf.getBookList().add(book);
      • bookshelf는 연관 관계의 주인이 아니기 때문에 db에 연관 관계가 저장되지 안흔ㄴ다.
      • book에는 bookshelf가 set되지 않으므로 코드 레벨에서 데이터 동기화가 발생한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entity
class Book {
    // id 등 생략

    public void setBookshelf(Bookshelf bookshelf) {
        bookshelf.add(this);
        this.bookshelf = bookshelf;
    }
}

@Entity
class Bookshelf {
    // id 등 생략

    public void addBook(Book book) {
        this.bookList.add(book);
        book.setBookshelf(this);
    }
}
  • 위와 같이 메서드를 정의해서 사용한다면 문제 생길 일이 없다.
    • 이런 편의 메서드도 한 쪽에만 만들어두는게 좋다고 한다.
  • 또 양방향 매핑 시에 무한 루프를 조심해야 한다.
    • toString(), lombok, JSON 생성 라이브러리
    • 예를 들어,
      • 양방향 연관 관계를 가지는 book과 bookshelf에서 lombok의 @toString으로 toString()을 자동 생성하고,
      • book.toString()을 호출하면 bookshelf.toString()이 호출되고
      • bookshelf.toString()을 호출하면 book.toString()이 호출되기 때문에
      • 조심해야 한다. 이거 생각보다 자주 있음.





다중성

  • 엔티티의 다중성을 결정할 때 헷갈리면 반대쪽을 생각하여 결정하면 된다.
    • 서로 대비되기 때문에
    • 다대일 <-> 일대다
    • 일대일 <-> 일대일
    • 다대다 <-> 다대다



다대일

  • 다대일 단방향 매핑
1
2
3
4
5
6
7
8
9
10
11
12
13
class Book {
    // 생략

    @ManyToOne
    @JoinColumn(name = "bookshelf_id")
    private Bookshelf bookshelf;
}

class Bookshelf {
    // 생략
}

  • 가장 많이 쓰이는 연관 관계 매핑이다.
  • DB에서 외래키 관리하는 방식과 유사하다.
    • DB는 ‘다’쪽 테이블이 외래키를 갖고 관리한다.
  • 위의 예시에서는 Book이 Bookshelf의 아이디를 갖고 관리한다.



  • 다대일 양방향 매핑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Book {
    // 생략

    @ManyToOne
    @JoinColumn(name = "bookshelf_id")
    private Bookshelf bookshelf;
}

class Bookshelf {
    // 생략

    @OneToMany(mappedBy = "bookshelf")
    private List<Book> bookList;
}

  • bookshelf에도 연관 관계 매핑을 해주고
  • mappedBy로 연관 관계의 주인이 Book 엔티티임을 나타내준다.
    • 이걸 하지 않으면 양방향이라고 할 수 없다.



일대다

  • 일대다 단방향 매핑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Book {
  // 생략

    private Bookshelf bookshelf;
}

class Bookshelf {
  // 생략

    @OneToMany
    @JoinColumn(name = "bookshelf_id")
    private List<Book> bookList;
}

  • ‘일’쪽에서 연관 관계를 관리한다.
    • DB에는 ‘다’쪽에 외래키가 존재한다.
    • 자연스러운 흐름과 반대되는 방식이다.
  • @JoinColumn을 넣어주지 않으면 중간 테이블이 추가된다.
    • Book_Bookshelf 이런 이름으로 연관 관계가 맺어진 row의 아이디만 가지는 테이블이다.
    • @JoinTable로 이 테이블을 설정해줄 수도 있다.
  • 추천하지 않는 방식이다.
    • 먼저 Book이 연관 관계를 관리하는 다대일 단방향 매핑을 예로 들면,
      1. 새로 bookshelf를 만들었다.
      • Bookshelf 테이블에 insert
        1. 새로 book을 만들었고, book은 bookshelf를 참조한다.
      • Book 테이블에 book이 insert
      • book은 외래키로 bookshelf의 아이디를 가진다.
    • 다음 Bookshelf가 연관 관계를 관리하는 일대다 단방향을 예로 들면,
      1. 새로 book을 만들었다.
      • Book 테이블에 insert
        1. 새로 bookshelf를 만들었고, bookshelf는 book의 참조를 가진다.
      • Bookshelf 테이블에 bookshelf를 insert
      • Book 테이블에 book의 row에 bookshelf 아이디를 추가하는 update
    • 이렇게 쿼리가 하나 더 나간다.
    • 이런 성능적인 부분 외에도 자연스러운 흐름을 거스르기 때문에 헷갈릴 수 있어서 비추다.



  • 일대다 양방향 매핑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Book {
  // 생략

    @ManyToOne
    @JoinColumn(name = "bookshelf_id", insertable = false, updatable = false)
    private Bookshelf bookshelf;
}

class Bookshelf {
  // 생략

    @OneToMany
    @JoinColumn(name = "bookshelf_id")
    private List<Book> bookList;
}

  • JPA 스펙 상으론 일대다 양방향 매핑은 없다고 한다.
  • 위의 코드는 그저 살짝 야매로 만든 것이다.
    • 연관 관계 주인이 ‘일’이고
    • 주인이 아닌 ‘다’는 조회만 가능하다.
  • 실무에선 사용하지 않는 것이 좋다고 한다.
    • 자연스러운 흐름과 반대되기 때문에 헷갈리고
    • 다대일 매핑이라는 좋은 대체재가 있기 때문



일대일

  • 일대일의 반대는 당연히 일대일이다.
  • 두 테이블 중에 하나 외래키를 주면 된다.
    • 두 테이블 중에 주 테이블이라고 여겨지는 곳
    • 두 테이블 중에 대상 테이블이라고 여겨지는 곳
  • 외래키에 유니크 제약 조건이 추가해야 된다.



  • 일대일 단방향 매핑
1
2
3
4
5
6
7
8
9
10
11
12
class Account {
    // ...

    @OneToOne
    private Player player;
}

class Player {
    // ...
}

  • Account 테이블에 외래키를 준 모양이다.
  • 반대로 작성할 수도 있다.



  • 일대일 양방향 매핑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Account {
    // ...

    @OneToOne
    private Player player;
}

class Player {
    // ...

    @OneToOne(mappedBy = "player")
    private Account account;
}

  • 마찬가지로 mappedBy로 연관 관계 주인을 정해줘야한다.
  • 어느 쪽을 연관 관계의 주인으로 설정하느냐에 따라 외래키 위치가 달라진다.



다대다

  • 관계형 DB에서는 정규화된 테이블 두 개로 다대다 관계를 표현할 수 없다.
    • 중간에 연결 테이블을 추가해서 일대다 : 다대일 관계로 풀어야 한다.
  • 객체는 두 개로 다대다 관계를 표현할 수 있다.
    • 각각 컬렉션을 가지면 된다.
  • 실무에서 사용되지 않는다고 한다.
    • 연결 테이블이 단순 연결 역할만으로 끝나지 않는다.
    • 생성 시간 등의 부가적인 정보의 컬럼이 계속 추가될 수 있다.
    • 예상치 못한 쿼리가 날아갈 수 있다.
  • 다대다 관계는 중간 엔티티를 만들어 일대다 다대일로 풀어내야 한다.
    • 여러 리그에 참여할 수 있는 Team과 여러 팀을 가질 수 있는 League가 있을 때
      • Team과 League 사이에 TeamLeague 같은 엔티티를 만든다.
      • 그리고 두 엔티티가 각각 일대다로 연관 관계 매핑을 한다.





This post is licensed under CC BY 4.0 by the author.