Spring boot로 프로젝트를 할때 비즈니스에 따른 객체와 관계형 데이터베이스의 연관관계 설계는 까다롭고 매우 중요하다는것을 다들 아실겁니다.
오늘은 그 연관관계에 대해서 이론적으로 정리해 보고자 합니다.
- 연관관계의 정의
- 연관관계의 방향 : 단방향, 양방향
- 연관관계의 주체 : 양방향일때 관리 주체
- 연관관계의 종류 : N+1, 1+N, 1:1, N+M
연관관계 방향
데이터베이스의 테이블은 Foreign key로 테이블의 조인이 가능합니다.
그러므로 테이블은 방향성을 논할 필요가 없겠죠
그에 반해 객체는 참조할 수 있는 필드에 따라서 다른 객체를 참조할 수 있습니다.
그말의 즉슨 한쪽에만 참조용 필드가 있으면 단방향, 양쪽 모두에 참조용 필드가 있으면 양방향 관계가 됩니다.
말이 양방향이지 서로 방향을 각각 가지는 겁니다. A-> <-B 뭐 이런식인 거죠.
객체 연관관계를 양방향으로 할지 말지는 비지니스 로직에 따라서 결정됩니다.
- 예) 판매자와 상점의 연관관계를 만든다고 했을때Shop.seller : 단방향 (N:1)
- 위처럼 참조가 되었다면 양방향 연관관계를 맺게 되겠습니다.
- Seller.shop : 단방향 (1:N)
질문 : 단방향으로 만들지 말고 전부 양방향으로 잡으면 되지 않나요?
불필요한 양방향 매핑은 객체 관계도를 복잡하게 만들수 있습니다.
객체 참조가 꼭 필요한 경우에 양방향 연관관계를 맺는것을 추천 드립니다.
연관 관계의 주인
- 전에 인강을 들으면서 강사분이 했던 말이 떠오르는데 쉽게 생각해서 데이터베이스 외래키가 있는 쪽을 주인으로 잡으시면 됩니다.
- 주인이 아닌쪽은 mappedBy 속성으로 주인을 지정해 주시면 되겠습니다.
질문 : 연관관계의 주인을 지정하는 이유는 뭐죠?
1. 패러다임 불일치
수정시 발생하는 JPA혼란을 방지하기 위해서 정한다고 보시면 되겠습니다.
패러다임의 불일치 때문이라고들합니다.
위에서 예로든 Seller와 Shop이 양방향 연관관계를 맺고 있을때 Shop쪽에 외래키를 설정해 놓았습니다.
만약 위에서 외래키가 있는 Shop을 수정하려면 연관 관계의 주인만 변경하면 됩니다.
예를 들어서 다시 설명을 드리자면
Shop을 다른 Seller로 수정하려고 할때 Shop 객체에서 setSeller() 수정을 할지, Seller객체에서
getShopList()로 List로 되어있는 Shop List객체를 수정해야 하는지에 대한 혼란이 생깁니다.
객체입장에서만 보면 문제가 없지만 데이터베이스 입장으로 적용하려고 하면 JPA입장에서 혼란스러운 상황이 되겠죠?
이러한 이유로 연관관계의 주인을 지정하게 되는 것입니다.
2. 영속성 컨텍스트 변경감지 기준
예로 Shop.setSeller(), seller.getShopList().addShop 둘다 허용하면 영속성 컨텍스트가 모호하게 됩니다.
양쪽모두를 수정하게 되면 업데이트 쿼리가 두번 발생할 수 있습니다..
또 연관관계 모순되는 경우 처리할 수 없는 케이스도 발생할 수 있습니다.
Seller.shopList에 A라는 상점이 있는데 shop.setSeller(null)이라고 한다면 어떤 기준이 없게 되겠습니다.
연관관계의 방향성
- N+1 (다대일) 단방향
@ManyToOne, @JoinColumn 어노테이션 사용합니다.
Shop 테이블에 외래키가 존재합니다.
@Entity
class Seller (
@Id @GeneartedValue
private Long id;
)
@Entity
class Shop (
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "seller_id")
var seller: Seller
)
- N+1 (다대일) 양방향
반대쪽 Entity에 @OneToMany(mappedBy="Entity명") 어노테이션을 통해서 구현합니다.
@Entity
class Seller (
@Id @GeneartedValue
private Long id;
@OneToMany(mappedBy = "seller")
val shops: MutableList<Shop>,
)
@Entity
class Shop (
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "seller_id")
var seller: Seller
)
- 1+N (일대다) 단방향
1쪽의 Entity만 반대편 객체를 참조하는 구조입니다.
1쪽의 참조가 변경되면 데이터베이스의 N쪽 테이블의 키가 변경됩니다.
이구조에서는 1쪽이 연관관계의 주인이 됩니다.
일반적으로 아니 무조건 연관관계의 주인은 데이터베이스의 외래키가 있는쪽을 잡는데 이경우 반대편 테이블이 외래키를 관리하는 특이한 구조가 됩니다.
그래서 일대다 단방향 매핑보다는 다대일 양방향 매핑하는 것을 권해드립니다.
@Entity
class Seller (
@Id @GeneartedValue
private Long id;
@OneToMany(mappedBy = "seller")
val shops: MutableList<Shop>,
)
- 1+N (일대다) 양방향
JPA 공식 스펙으로 지원하는 매핑은 아닙니다.
구현은 @JoinColumn에 insertable=false, updatable=false 속성을 추가해 읽기전용으로 구현하게 됩니다.
공식지원하지 않은 구조이다보니 사용하지 않길 권장드리며 N+1 양방향으로 변경하여 사용하길 권해드립니다.
@Entity
class Shop (
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "seller_id", insertable=false, updatable=false)
var seller: Seller
)
- 1+1 (일대일) 단방향
데이터베이스의 어느쪽에 외래키를 두어도 무방합니다.
외래키로 설정된 테이블에는 유니크 제약 조건을 추가합니다.
JoinColumn, @OneToOne에서 mappedBy로 매핑할 컬럼을 지정해 줄수 있습니다.
외래키를 어디에 두는냐에 따른 차이
어느 쪽에서 참조하냐에 따라 외래키 위치가 정해지기 때문에 설계시 비즈니스 로직에 맞게 고려하시는게 좋을듯 싶습니다.
예를 들어 Shop은 Seller를 가지지만 Seller는 Shop이 없을 수도 있을 겁니다.
이 경우 객체 지향 관점에서 보면 Seller가 Shop 필드를 가지는게 좋을것입니다.
그럼 Seller에 shop_id 외래키 칼럼을 가지게 만들 수 있는데 이렇게 되면 shop을 소요하지 않은 Seller에 해당 컬럼 값이 Null이 되게 됩니다.
데이터베이스 관점에서 값이 Null은 별로 바람직하지 않기 때문에 Shop테이블에 seller_id 컬럼을 만드는 게 더 나은 방법입니다.
또한 더 나아가 shop을 여러사람이 공동소유하거나, 한 Seller가 소유할 수 있는 shop이 많아지게 되면 1+1연관계를 N+1관계로 확장될 수도 있습니다.
확실한 정답은 없기때문에 확장성을 고려해서 테이블 및 엔티티 설계를 하시는 게 좋을듯 싶습니다.
- 1+1 (일대일) 양방향
- 양쪽에 @OneToOne으로 모두 참조값을 넣어주고 mappedBy로 설정하지 않은 쪽을 연관관계의 주인을 설정해 주시면 됩니다.데이터베이스 관점 또는 추후 확정성을 고려해 연관관계 주인을 설정해 주시는 게 좋겠습니다.
- Seller쪽에 mappedBy를 설정해주면 Shop쪽이 연관관계의 주인이 되어 외래키를 관리하게 됩니다.
- 양쪽 엔티티에 참조를 걸어주기에 객체관점은 염려하실게 없으시고
- N+M (다대다)
관계형 데이터베이스에서는 두 테이블로 다대다 관계를 표현할 수 없습니다.
그래서 중간에 연결 테이블을 추가해서 일대다 다대일 관계로 구현하실 수는 있습니다.
그에 반해 객체는 컬렉션(List)을 사용하여 두 객체를 다대다 관계를 설계가 가능합니다.
다대다관계도 단방향 양방향 구현이 가능하지만 실무에서는 다대다 대신에 중간에 연결 테이블 객체를 만들어 다대일 - 일대다의 형식으로 만드시는 것이 좋습니다.
정리
오늘은 JPA 객체 연관관계에 대해서 이론적으로 알아 보았습니다. 정리를 한다고 위 글을 써놓고도 헷갈리는 것 같습니다.
비즈니스의 로직에 따라서 객체와 데이터베이스의 설계를 잘 해두는것은 프로젝트를 할때 가장 중요한 부분이니 정확하게 이해하는 것이 필요하고 적절하게 잘 이용하는게 중요한 것 같습니다.
'IT > JPA' 카테고리의 다른 글
JPQL – JPA 및 Hibernate에서 쿼리를 정의하는 방법 (0) | 2023.04.03 |
---|---|
JPA @Embedded 및 @Embeddable에 대해 알아보자 (0) | 2023.04.01 |
JPA Entity 애노테이션 알아보기 (0) | 2022.11.09 |
JPA Entity를 만들어 보자 (0) | 2022.11.09 |
[ JPA ] 프록시와 연관관계 정리 (0) | 2022.11.01 |