JPA에서 중요한 것은 "객채와 관계형 데이터베이스 테이블이 어떻게 매핑되는지를 이해하는 것"이라고 할 수 있다.
JPA의 목적인 "객체 지향 프로그래밍과 데이터베이스 사이의 패러다임 불일치를 해결"이라는 것과 가장 직접적으로 연관되어 있기 때문이다.
JPA에서는 두 엔티티 사이의 연관 관계를 정의할 때 기본적으로 단방향으로 정의한다. 이로 인해 DB 테이블에서 외래키를 기준으로 하여 조인한다. 두 테이블 간의 연관된 데이터를 조회할 수 있는 것과 달리 JPA에서 단방향 연관 관계가 정의된 상태에서는 한쪽 엔티티 객체는 연관 관계인 엔티티 객체를 조회할 수 있으나, 반대쪽 엔티티 객체는 어떤 엔티티 객체와 연관 관계를 갖는지 알 수 없다.
이로 인해 JPA에서는 비즈니스 로직, 비즈니스 요구사항 등 필요에 따라 양방향 연관 관계를 정의하게 되고, 이번 글에서는 이 주제에 관하여 정리해 보겠다.
연관 관계 정의 규칙
연관 관계를 매핑할 때, 생각해야 할 것은 크게 3가지가 있다.
- 방향 : 단방향, 양방향 (객체 참조)
- 연관 관계의 주인 : 양방향일 때, 연관 관계에서 관리 주체
- 다중성 : 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)
하나씩 알아보자.
단방향, 양방향
데이터베이스 테이블은 외래 키 하나로 양 쪽 테이블 조인이 가능하다. 따라서 데이터베이스는 단방향이니 양방향이니 나눌 필요가 없다.
그러나 객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능하다. 그렇기 때문에도 객체 사이에 하나의 객체만 참조용 필드를 갖고 참조하면 단방향 관계, 두 객체 모두가 각각 참조용 필드를 갖고 참조하면 양방향 관계라고 한다.
엄밀하게는 양방향 관계 <->는 없고 두 객체가 단방향 참조를 각각 가져서 양방향 관계처럼 사용하고 말하는 것이다.
JPA를 사용하여 데이터베이스와 패러다임을 맞추기 위해서 객체는 단방향 연관 관계를 가질지, 양방향 연관 관계를 가질지 선택해야 한다.
선택은 비즈니스 로직에서 두 객체가 참조가 필요한지 여부를 고민해 보면 된다.
Board.getPost() // 이처럼 참조가 필요하면 Board -> Post 단방향 참조
// 만약 참조가 굳이 필요없으면 참조를 안하면 됨
post.getBoard() // 이처럼 참조가 필요하면 Post -> Board 단방향 참조
// 만약 참조가 굳이 필요없으면 참조를 안하면 됨
이렇게 비즈니스 로직에 맞게 선택했는데 두 객체가 서로 단방향 참조를 했다면 양방향 연관 관계가 되는 것이다.
단방향 연관 관계와 양방향 연관 관계를 구분하는 방법은 이렇게 이해하면 된다.
무조건 양방향 관계를 하면 쉽지 않나? 🤔
객체 입장에서 양방향 매핑을 했을 때 오히려 복잡해질 수 있다.
예를 들어 일반적인 비즈니스 애플리케이션에서 사용자(User) 엔티티는 굉장히 많은 엔티티와 연관 관계를 갖는다.
이런 경우에 모든 엔티티를 양방향 관계로 설정하게 되면 사용자(User) 엔티티는 엄청나게 많은 테이블과 연관 관계를 맺게 되고 User 클래스를 보면 엄청나게 복잡해진 것을 확인할 수 있다.
그리고 다른 엔티티들도 불필요한 연관관계 매핑으로 인해 복잡성이 증가할 수 있다. 그렇기 때문에 양방향으로 할지, 단방향으로 할지 필히 구분해줘야 한다.
구분하기 좋은 기준은 기본적으로 단방향 매핑으로 하고 나중에 역방향으로 객체 탐색이 꼭 필요하다고 느낄 때 추가하는 것으로 잡으면 좋을 것 같다. 그냥 참조만 추가한다고 되는 건 아니고 자세한 것은 아래에서 설명하겠다.
연관 관계의 주인
두 객체(A, B)가 양방향 관계, 다시 말해 단방향 관계 2개(A -> B, B -> A)를 맺을 때, 연관 관계의 주인을 지정해 줘야 한다.
연관 관계의 주인을 지정하는 것은 두 단방향 관계(A -> B, B -> A) 중, 제어의 권한(외래 키를 비롯한 테이블 레코드를 저장, 수정, 삭제, 처리)을 갖는 실질적인 관계가 어떤 것인지 JPA에게 알려준다고 생각하면 된다.
연관 관계의 주인은 연관 관계를 갖는 두 객체 사이에서 조회, 저장, 수정, 삭제를 할 수 있지만, 연관 관계의 주인이 아니면 조회만 가능하다.
연관 관계의 주인이 아닌 객체에서 mappedBy 속성을 사용해서 주인을 지정해줘야 한다.
✅ 외래키가 있는 곳을 연관 관계의 주인으로 정하면 된다.
왜 연관 관계의 주인을 지정해야 하는가?
두 객체(Board, Post)가 있고 양방향 연관 관계를 갖는다고 가정해 보자.
그 상황에서 게시글(Post)의 게시판을 다른 게시판(Board)으로 수정하려고 할 때, Post 객체에서 setBoard(...) 같은 메서드를 이용해서 수정하는 게 맞는지, Board 객체에서 getPosts(...) 같은 메서드를 이용해서 List의 게시글을 수정하는 게 맞는지 헷갈릴 수 있다.🤔
두 객체 입장에서는 두 방법 모두 맞는 방법이긴 하다.
그러나 이렇게 객체에서 양방향 연관 관계 관리 포인트가 두 개일 때에는 테이블과 매핑을 담당하는 JPA입장에서 혼란을 주게 된다.
즉, Post에서 Board를 수정할 때 FK(Foreign key)를 수정할지, Board에서 Post를 수정할 때 FK(Foreign Key)를 수정할지를 결정하기 어려운 것이다. 그렇기 때문에 "두 객체 사이의 연관 관계의 주인을 정해서 명확하게 Post에서 Board를 수정할 때만 FK를 수정하겠다!"라고 정하는 것이다.
연관 관계의 주인만 제어하면 되나?
데이터베이스에 외래 키가 있는 테이블을 수정하려면 연관 관계의 주인만 변경하는 것이 맞는가? 에 대한 답은 "맞다"이다.
맞긴 하지만, 그것은 데이터베이스만 생각했을 때고, 객체를 생각해 보면 사실 둘 다 변경해 주는 것이 좋다.(연관 관계의 주인이 아닌 곳에서도 마찬가지로 변경을 해준다.)
왜냐하면 두 참조를 사용하는 순수한 두 객체는 데이터 동기화를 해줘야 하기 때문이다.
다중성
데이터베이스를 기준으로 다중성을 결정한다.
JPA는 JPQL도 그렇고 보통 객체를 기준으로 하는 것이 일반적인데 다중성을 정하는 기준은 데이터베이스 기준인 게 좀 특이하다.
연관 관계는 대칭성을 갖는다
- 1:N <-> N:1
- 1:1 <-> 1:1
- N:M <-> M:N
다대일 (N:1)
게시판(Board)과 게시글(Post)의 관계로 예를 들어보자.
요구사항
- 하나의 게시판(1)에는 여러 게시글(N)을 작성할 수 있다.
- 하나의 게시글은 하나의 게시판에만 작성할 수 있다.
- 게시글과 게시판은 N:1 관계를 갖는다.
데이터베이스를 기준으로 다중성(게시글 N : 게시판 1)을 결정했다. 즉, 외래 키를 게시글(N)이 관리하는 형태가 일반적인 형태이다. (참고로 데이터베이스는 무조건 다(N) 쪽이 외래키를 갖는다.)
다대일 (N:1) 단방향
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@ManyToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
//... getter, setter
}
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
//... getter, setter
}
다대일 단방향에서는 다 쪽인 Post에서 @ManyToOne 만 추가해 준 것을 확인할 수 있다. 반대로 Board에서는 참조하지 않는다.(단방향이기 때문이다.)
다대일(N:1) 양방향
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@ManyToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
//... getter, setter
}
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
@OneToMany(mappedBy = "board")
List<Post> posts = new ArrayList<>();
//... getter, setter
}
다대일 양방향으로 만들려면 일(1) 쪽에 @OneToMany를 추가하고 양방향 매핑을 사용했으니 연관 관계의 주인을 mappedBy로 지정해 준다.
mappedBy로 지정할 때 값은 대상이 되는 변수명을 따라 지정하면 된다. 여기서는 Post 객체(대상)의 board라는 이름의 변수이기 때문에 board로 지정했다.
일대다 (1:N)
일대다는 다대일에서 반대 입장인데 정리할 필요가 있나?라고 생각할 수 있지만 앞서 다대일의 기준은 연관 관계의 주인 다(N) 쪽에 둔 것이고 이번에 언급할 일대다의 기준은 연관 관계의 주인을 일(1) 쪽에 둔 것이다.
❌ 참고로 실무에서는 일대다(1:N) 단방향은 거의 쓰지 않도록 한다.
일대다(1:N) 단방향
데이터베이스 이장에서는 무조건 다(N) 쪽에서 외래키를 관리한다. 그렇지만 일(1) 쪽에서 다(N) 쪽 객체를 조작(생성, 수정, 삭제)하는 방법이다.
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
//... getter, setter
}
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
@OneToMany
@JoinColumn(name = "POST_ID") //일대다 단방향을 @JoinColumn필수
List<Post> posts = new ArrayList<>();
//... getter, setter
}
양방향이 아니기 때문에 @OneToMany 에 mappedBy 가 사라진다. 대신 @JoinColumn 을 이용해서 조인한다.
실제 사용은 아래와 같이 한다.
//...
Post post = new Post();
post.setTitle("가입인사");
entityManager.persist(post); // post 저장
Board board = new Board();
board.setTitle("자유게시판");
board.getPosts().add(post);
entityManager.persist(board); // board 저장
//...
위와 같은 시나리오로 동작을 살펴보면, post를 저장할 때 정상적으로 insert 쿼리가 나간다. 하지만 이다음이 문제다.
board를 저장할 때는 Board를 insert 하는 쿼리가 나간 후에 post를 update 하는 쿼리가 나간다. 왜냐하면 board.getPosts().add(post); 부분 때문이다.
Board 엔티티는 Board 테이블에 매핑되기 때문에 Board 테이블에 직접 지정할 수 있으나, Post 테이블의 FK(Board_ID)를 저장할 방법이 없기 때문에 조인 및 업데이트 쿼리를 날려야 하는 문제가 있다.
여기서 치명적인 단점이 발생한다.
- 일(1) 만 수정한 것 같은데 다른 수정이 생겨 쿼리가 발생한다.
- Board를 저장했는데 왜 Post가 수정이 되는지에 대한 의문점이 생긴다.
- 업데이트 쿼리 때문에 성능상 이슈는 그렇게 크지는 않다.
그렇기 때문에 일대다(1:N) 단방향 연관 관계 매핑이 필요한 경우는 그냥 다대일(N:1) 양방향 연관 관계를 매핑하는 게 추후에 유지보수에 훨씬 수월하기 때문에 이 방식을 채택하는 것이 좋다.
일대다(1:N) 양방향 - 실무 사용 ❌
일대다 양방향은 공식적으로 존재하는 것은 아니기 때문에 생략하겠다.
@JoinColumn(updatable = false, insertable = false) 이런 식으로 작성할 수 있지만, 일대다 양방향을 사용해야 할 때는 다대일 양방향을 사용하는 것이 더 좋다.
결과적으로 "일대다(1:N) 단방향, 양방향은 쓰지 말고 차라리 다대일(N:1) 양방향으로 쓰는 것이 맞다"라고 단순화하여 결론을 내리면 될 것 같다.
일대일 (1:1)
주 테이블에 외래키를 넣을 수도 있고, 대상 테이블에 외래키를 넣을 수도 있다.
✅ 일대일(1:1)이기 때문에 테이블 A, B가 있을 때, A가 주 테이블이면 B가 대상 테이블이고, B가 주 테이블이면 A가 대상 테이블이다.
일대일(1:1) 단방향
외래 키를 주 테이블이 갖고 있다는 의미로 해석하겠다.(Post 테이블(주 테이블)에서 외래키(FK)인 Attach 테이블(대상 테이블)의 PK를 갖고 있도록)
게시글(Post)에 첨부파일(Attach)을 반드시 1개만 첨부할 수 있다고 가정해 보자.
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@OneToOne
@JoinColumn(name = "ATTACH_ID")
private Attach attach;
//... getter,setter
}
@Entity
public class Attach {
@Id @GeneratedValue
@Column(name = "ATTACH_ID")
private Long id;
private String name;
//... getter, setter
}
딱히 특별한 것은 없다.
하지만 Post 테이블(주 테이블)이 아닌 Attach 테이블(대상 테이블)에 외래 키(FK)를 갖고 있을 때는 어떻게 될까?
이것은 JPA에서 애초에 지원 자체를 하지 않는다.
이러한 경우에는 아래의 일대일(1:1) 양방향을 선택할 수 있다. 이 방법을 선택할 때는 외래 키를 Post에서 관리하는 게 좋을지, Attach에서 관리하는 게 좋을지에 대해서는 고민을 해봐야 한다.
일대일(1:1) 양방향
단순하게 똑같이 @OneToOne 설정하고 mappedBy 설정만 해서 읽기 전용으로 만들어주면 양방향도 간단하게 된다.
@Entity
public class Attach {
@Id @GeneratedValue
@Column(name = "ATTACH_ID")
private Long id;
private String name;
@OneToOne(mappedBy = "attach")
private Post post;
//... getter, setter
}
다대다 (N:N)
실무 사용 금지 ❌
- 중간 테이블이 숨겨져 있기 때문에 자기도 모르는 복잡한 조인의 쿼리(Query)가 발생하는 경우가 생길 수 있기 때문이다.
- 다대다로 자동 생성된 중간 테이블은 두 객체의 테이블의 외래 키만 저장되기 때문에 문제가 될 확률이 높다. JPA를 해보면 중간 테이블에 외래 키 외에 다른 정보가 들어가는 경우가 많기 때문에 다대다를 일대다, 다대일로 풀어서 만드는 것(중간 테이블을 Entity로 만드는 것)이 추후 변경에도 유연하게 대처할 수 있다.
결론
연관 관계 매핑에 있어서는 위의 내용(방향성, 다중성, 연관 관계의 주인)을 숙지하고 나름 신중히 사용을 해야 한다.
출처 / 참고
https://colabear754.tistory.com/142
[JPA] 양방향 연관 관계에서 연관 관계의 주인 설정과 주의 사항
목차 개요 JPA에서는 두 엔티티 사이의 연관 관계를 정의할 때 기본적으로 단방향으로 정의한다. 이로 인해 DB 테이블에서 외래키를 기준으로 하여 조인하는 것으로 두 테이블 간의 연관된 데이
colabear754.tistory.com
https://jeong-pro.tistory.com/231
JPA 연관 관계 한방에 정리 (단방향/양방향, 연관 관계의 주인, 일대일, 다대일, 일대다, 다대다)
JPA에서 가장 중요한 것 JPA에서 가장 중요한 것을 뽑자면, "객체와 관계형 데이터베이스 테이블이 어떻게 매핑되는지를 이해하는 것"이라고 생각합니다. 🏅 왜냐하면 JPA의 목적인 "객체 지향 프
jeong-pro.tistory.com
'JPA' 카테고리의 다른 글
[JPA] 기본키 생성 전략 - @Id, @GeneratedValue (0) | 2024.09.04 |
---|---|
API 개발 고급 - 지연 로딩과 조회 성능 최적화 (0) | 2023.05.29 |
API 개발 고급 - 준비 (0) | 2023.05.29 |
API 개발 기본 (0) | 2023.05.29 |
웹 계층 개발 (0) | 2023.05.21 |