JPA 에서 가장 중요한 개념 중의 하나라고 할 수 있는 연관관계에 대해 알아보자.
JPA 에서 Entity 는 데이터베이스(RDB) 의 Table 과 매핑되는 개념이다. 즉 데이터베이스의 테이블을 객체로 매핑한 것이 Entity 라고 볼 수 있다.
그러나 DB 테이블과 객체 사이에는 좁혀지지 않는 간극이 있는데, 테이블과 테이블 사이에 관계를 맺는 경우가 바로 그것이다. 테이블과 테이블이 관계를 맺을 경우, 하나의 테이블에서 다른 테이블의 PK 를 FK 로 가지고 있음으로써 두 테이블은 관계를 맺을 수 있다. 이 경우 조인을 통해 다른 테이블의 정보에 접근할 수 있다. 즉 아래와 같은 방식이다.
객체(Entity)의 경우는 조금 다르다. 객체가 다른 객체와 관계를 맺으려면, 객체 내부에 다른 객체를 프로퍼티로 가지고 있으면서 참조의 형태로 접근하게 된다. 즉 아래와 같다.
여기까지만 보면 테이블과 객체 사이의 관계 맺기에 대해 큰 차이는 보이지 않는 것 같다. 이제 관계의 방향성을 살펴보자.
단방향, 양방향 매핑
위 예시에서 Team 과 Member 의 연관 관계를 생각해 보자. Member 객체에서는 Team 객체를 참조할 수 있다. 그러나 Team 객체에서는 Member 객체를 참조할 수 없다. Member 객체를 모르기 때문에 접근할 수 없는 것이다. 같은 관계에서 DB 테이블의 경우에는, 조인을 통해 Team 에서도 Member를, Member 에서도 Team을 알 수 있다. 이것이 객체와 DB 테이블 사이의 패러다임의 차이이다.
DB 테이블에서처럼 객체에서도 양쪽이 서로를 참조할 수 있도록, Member 에서 Team 객체를, Team 에서 Member 객체를 가지고 있도록 할 수 있다. 이것을 JPA 에서 양방향 매핑이라고 한다. Member 에서는 Team 객체를 가지고 있고, Team 에서는 Member 객체를 List 형태로 갖고 있다. 그림으로는 아래와 같다.
위 그림처럼 양쪽에서 참조하지 않고, 한쪽만 다른 한쪽을 알고 있는 관계를 단방향 매핑이라고 한다. 처음의 예시처럼 Member 만 Team 을 알고 있을 수도 있고, Team 에서만 Member List를 가지고 있을 수도 있다. DB 테이블 관점에서는 선택권이 없이 N 측이 1 측의 PK 를 FK 로 갖고 있는 형태로 관계를 정의할 수 있지만, JPA 에서는 이처럼 단방향으로 매핑할것이냐 양방향으로 매핑할 것이냐를 선택해야 하고 또 어떤 객체에서 다른 하나를 관리할 지 등을 선택해야 한다. 그렇다면 매핑 방법에 따라 어떤 장단점이 있는지, 어떤 기준에 따라 선택해야 할 지 등을 이제부터 알아보자.
단방향 매핑
가장 기본적인 매핑 방법인 단방향 매핑이다. Team 과 Member 가 1:N 의 관계라고 할 때, 1 측인 Team 객체에서만 Member 를 관리하는 방법을 @OneToMany (One 인 Team 에서 Many 인 Member 를 관리하므로) 라고 하고, N측인 Member 객체에서 Team 을 관리하는 방법을 @ManyToOne (Many 인 Member 에서 One 인 Team 을 관리하므로) 이라고 한다.
단방향 @OneToMany 는 지양하라는 말이 있다. 그 이유는 테이블과 객체의 매핑이 다르기 때문에 그 불일치에서 오는 여러 문제가 있기 때문이다. 어떤 관계든 테이블에서는 N측에서 1측의 PK 를 FK 로 가지고 있다. 이는 객체 매핑에서 @ManyToOne 과 동일하다. (N측에서 1측을 관리한다는 점에서) 그러나 @OneToMany 는 1 측에서 N 측을 관리하기 때문에 테이블에서 실제 매핑되는 것과는 차이가 있다. 실제 테이블 상에서 외래키의 위치와 객체 상에서 참조되는 객체의 위치가 다르기 때문에, INSERT 쿼리 요청 시 불필요하게 UPDATE 쿼리가 생성된다. 또 @JoinColumn 을 사용하지 않으면 불필요한 조인 테이블이 생성되어 의도하지 않은 쿼리가 나갈 수 있다.
이와 같은 이유들 때문에 @OneToMany 관계는 지양하라는 말이 있지만, 그렇다고 반드시 배제해야 하는 것은 아니다. 비즈니스 요구사항을 충족시키는 데 특정 형태의 관계가 필요한 경우는 이를 고려하여 관계를 결정하는 것이 좋다. 가령 '주문 요청은 주문상품을 생성한다' 라는 요구사항이 있을 경우, Order Entity 와 OrderProduct Entity 는 @OneToMany 관계를 맺어야 할 것이다.(주문에서 주문 상품을 알아야 하므로) 이와 같이 객체 사이의 관계는 요구사항과 기술적 장단점을 고려하여 유동적으로 결정해야 하는 부분이고, 정답은 없다.
양방향 매핑
객체가 양쪽에서 서로를 참조하는 양방향 매핑이다. DB 테이블 상에서는 단방향 매핑에서와 변함없이 하나의 외래키로 양 쪽 테이블에서 조인하여 접근할 수 있다. 객체는 양방향이라는 용어를 사용하지만, 사실 @OneToMany + @ManyToOne 의 두 개의 단방향 매핑과 같다. 양방향 매핑에서는 연관관계의 주인이라는 것을 정해야 하는데, 이를 자세히 알아보자.
※ 연관관계의 주인(mappedBy="~")
객체가 양방향으로 연관관계를 맺고 있다고 하자. Team 에서도 Member 를 List 로 갖고 있고, Member 에서도 Team 을 갖고 있다. Team 에 새로운 멤버를 추가해야 한다면, Team 엔티티의 Member 를 수정함에 따라 DB에서 FK 를 업데이트 할 것인지, Member 엔티티의 Team 을 수정함에 따라 DB에서 FK 를 업데이트 할 것인지 혼란스럽다. 따라서 JPA 에서는 양방향 매핑 시 '연관관계의 주인' 이라는 것을 정하고, 연관관계의 주인 측에서 값이 업데이트 됐을 경우만 DB 테이블에 반영한다. 연관관계의 주인 측이 아닌 객체에서 값을 변경시키는 것은 DB에 반영되지 않는다. (따라서, 양방향 연관관계에서는 연관관계의 주인 측에서만 수정, 삭제 등이 가능하고 반대측에서는 조회만 가능하다.)
그렇기 때문에, 연관관계의 주인을 정하고 그 반대 측(연관관계의 주인이 아닌 측)에 (mappedBy="~~") 를 걸어 "나는 ~~ 테이블에 의해 매핑된 테이블이다" 라는 것을 명시해 주어야 한다.
- 그럼 연관관계의 주인은 어떤 기준으로 정하나?
→ 대부분의 레퍼런스에서 입을 모아 이야기하는, 정답이 있는 문제다. 연관관계의 주인은 FK 를 가진 쪽을 기준으로 할 것.(이유는 단방향에서 @OneToMany 를 지양해야 하는 이유와 동일) 그러나 비즈니스 요구사항에 따라 FK 가 없는 측이 연관관계의 주인이 되도록 설계할 수도 있다. 특별한 요구사항이 없다면 되도록 위 지침을 따르자. - 연관관계의 주인 객체에서만 값을 업데이트 해주면 될까?
→ 그렇지 않다. 양방향 매핑에서 값 초기화 시 양쪽에서 모두 초기화 해 주는게 필요하다. 연관관계의 주인 측의 객체에서만 초기화를 시켜 줘도 DB에서 정상적으로 저장이 되고 양쪽 객체에서 접근이 가능하지만, 연관관계 주인이 아닌 객체에서도 명시적으로 초기화 해 주는 것이 좋다.(이를 위해 양쪽에서 한번에 값을 초기화해주는 편의 메서드를 고려해 볼 수 있다.) 이유는 다음과 같다.
① JPA는 값 업데이트 시 영속성 컨텍스트가 종료되는 시점에 DB 상에 데이터가 저장되기 때문에, 영속성 컨텍스트 내에서(영속성 컨텍스트가 종료되기 전) 객체에서 갱신된 값에 대한 참조를 할 경우 잘못된 값을 참조하게 될 수 있다. (영속성 컨텍스트 내에서는 1차 캐시에서만 조회하게 되므로)
② 테스트 환경에서는 JPA 에 종속되지 않은 순수 java 객체간 서로를 참조하게 되는데, 이 경우 양측 객체에서 초기화된 값이 없으면 잘못된 값을 참조하게 될 수 있다.
양방향 매핑은 위에서 @OneToMany 를 지양해야 하는 이유와 비슷한 맥락(→ 테이블과 객체의 매핑 불일치로 인한 혼란)으로 대부분 사용하지 않는 것을 권장한다. 양방향 매핑을 지양해야 하는 이유는 크게 두 가지로 볼 수 있는데, 하나는 위에서 언급한 @OneToMany 의 단점과 같은 맥락으로 객체 매핑과 DB 테이블 상 매핑이 일치하지 않음(→ 에서 발생하는 여러 참조 문제), 또 하나는 엔티티 간 순환참조 문제(toString, JSON 변환 라이브러리 등 ) 이다. 그 밖에 양방향 매핑을 남용하면 엔티티의 복잡성이 증가하는 등 다양한 문제가 발생할 수 있다. 위 이유들로 흔히들 양방향 매핑은 지양하라고 하는데, 그렇다면 양방향 매핑은 사용하지 말아야 할까? 양방향과 단방향 매핑 중 어떤 것을 사용할지에 대한 정답은 없지만 개인적인 가이드를 정리해 봤다.
양방향 vs 단방향 결론
양방향, 단방향 중 어느 방법으로 매핑하는 것이 정답이냐? 라고 하면 정확한 답은 비즈니스 요구사항에 따라 정해지겠지만, 결국 양방향 연관관계라는 것은 연관관계의 주인이 아닌 측의 조회를 더 쉽게 하기 위함이다. (반대 방향에서도 조회할 수 있다! 만 추가된 것. 반대 방향 객체에서는 수정, 삭제 등을 할 수 없다.) 또한 단방향 → 양방향 으로의 확장은 DB 상에서 변경이 없고 유연한 확장이므로, '되도록이면 초기에는 단방향으로 설계하되 필요에 따라 양방향으로 확장시키는 것이 낫다' 고 결론지을 수 있을 것 같다. 정리하면 다음과 같다.
- 양방향 매핑은 아주 치명적인 문제를 일으키는 것은 아니다. 그렇지만 객체의 복잡성을 증가시키고, DB와의 매핑 차이로 인한 혼란으로부터 다양한 문제를 야기할 수 있다(예기치 않은 쿼리 발생이나 null 참초, 순환참조 등)
- 단방향 매핑만으로도 이미 연관관계 매핑은 완료된 상태이다.
- 단방향 → 양방향으로의 확장은 DB 테이블에서의 변화가 없을 뿐더러 유연하고 간단한 확장이다.
- 따라서 요구사항에 반하는 내용이 아니라면, 초기 설계는 단방향으로 시작하되 이후 필요에 따라 양방향으로 확장시키는 방법을 권장한다.
'Study' 카테고리의 다른 글
OSI 7 계층 (0) | 2024.07.24 |
---|---|
레디스 락 vs DB 락 (0) | 2024.07.10 |
[Spring/JPA] N+1 문제와 해결 방법(fetch Join) (0) | 2024.06.03 |
[Java]가비지 컬렉션(GC) (0) | 2024.05.22 |
[Spring]ResponseEntity 사용법과 사용하는 이유 (0) | 2024.05.20 |