연관 관계 기초
참고
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”를 줘야한다.
- 연관 관계의 주인인 Book은 Bookshelf를 bookshelf라는 변수명으로 두었다.
그럼 누구를 연관 관계의 주인으로 설정해야 하는가?
- 이것은 DB에서 외래키를 관리하는 쪽이 누구인가를 생각하면 된다.
- 보통 다대일 관계에서 ‘다’쪽이 외래키를 관리한다.
- JPA에서도 같은 맥락으로 생각하여 연관 관계 주인을 정해주면 가장 자연스럽다.
주의점
- DB에서는 실제로 양방향 연결이기 때문에 한 쪽에서 수정이 이뤄지면 된다.
하지만 JPA의 엔티티는 객체이기 때문에 문제가 발생할 수 있다.
- Book-Bookshelf 예제를 계속 사용해보면
- book.setBookshelf(bookshelf);
- Book이 연관 관계의 주인이기 때문에 테이블에 외래키로 bookshelf의 아이디가 잘 들어간다.
- 그러나 bookshelf에는 book이 add되지 않았으므로 코드 레벨에서 데이터 동기화가 발생한다.
- bookshelf.getBookList().add(book);
- bookshelf는 연관 관계의 주인이 아니기 때문에 db에 연관 관계가 저장되지 안흔ㄴ다.
- book에는 bookshelf가 set되지 않으므로 코드 레벨에서 데이터 동기화가 발생한다.
- book.setBookshelf(bookshelf);
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이 연관 관계를 관리하는 다대일 단방향 매핑을 예로 들면,
- 새로 bookshelf를 만들었다.
- Bookshelf 테이블에 insert
- 새로 book을 만들었고, book은 bookshelf를 참조한다.
- Book 테이블에 book이 insert
- book은 외래키로 bookshelf의 아이디를 가진다.
- 다음 Bookshelf가 연관 관계를 관리하는 일대다 단방향을 예로 들면,
- 새로 book을 만들었다.
- Book 테이블에 insert
- 새로 bookshelf를 만들었고, bookshelf는 book의 참조를 가진다.
- Bookshelf 테이블에 bookshelf를 insert
- Book 테이블에 book의 row에 bookshelf 아이디를 추가하는 update
- 이렇게 쿼리가 하나 더 나간다.
- 이런 성능적인 부분 외에도 자연스러운 흐름을 거스르기 때문에 헷갈릴 수 있어서 비추다.
- 먼저 Book이 연관 관계를 관리하는 다대일 단방향 매핑을 예로 들면,
- 일대다 양방향 매핑
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 같은 엔티티를 만든다.
- 그리고 두 엔티티가 각각 일대다로 연관 관계 매핑을 한다.
- 여러 리그에 참여할 수 있는 Team과 여러 팀을 가질 수 있는 League가 있을 때
This post is licensed under CC BY 4.0 by the author.