
연관 관계 매핑 종류와 방향
One To One (1대1)관계
하나의 객체에 대해서 반드시 하나의 연관 객체만 매핑되는 구조
1 대 1 단방향 관계
예를 들면, 상품과 그 상품에 대한 상세정보는 1대 1 매핑일 것이다.

코드상으로는 Product Detail Entity 클래스에서 Product Entity를 가지도록 해야 한다.
@OneToOne 어노테이션과 @JoinColumn(name="product_number")를 통해서 1대 1 관계를 매핑하게 된다.
JoinColumn에서는 여러 가지 속성 값을 지정해 줄 수도 있는데,
- name : 매핑항 외래키의 이름을 설정합니다.
- rerferencedColumnName : 외래키가 참조할 상대 테이블의 칼럼명을 지정합니다.
- foreignKey : 외래키를 생성하면서, 지정할 제약 조건을 설정합니다. (unique, nullable, insertable, updateable 등..)
만약, Product Detail Entity에서 1대 1 연관관계 매핑을 했다면, JPA를 통해서 쿼리를 조회할 때, 내부에서 Join Column으로 설정한
참조 값을 토대로 해당 객체와 join 후 데이터를 추출합니다.

또한, 비즈니스 로직에서 Product Detail을 통해 Product를 찾고자 한다면, @Getter 어노테이션을 적용하여, getProduct()를 통해 해당 세부정보를 통해서 제품을 조회하는 식의 로직을 구성할 수 있습니다.
만약, 해당 관계가 nullable이 true인 경우, optional = true를 통해서 null인 값을 허용하지 않게 할 수 있습니다. 실행 시점에서 해당 컬럼은 not null로 전환되게 됩니다.
1 대 1 양방향
1 대 1 양방향 관계란 각 객체를 통해서 매핑되어 있는 객체를 조회할 수 있는 것을 의미합니다. 이전의 단방향의 경우, @OneToOne 어노테이션을 적용한 객체에서만 연관된 객체를 조회할 수 있었는데, 이번 절에서는 각자가 매핑된 객체의 정보를 조회할 수 있습니다.

하지만 이렇게 두 객체 모두 OneToOne을 사용하게 된다면, 양쪽에서 외래키를 가지고 left outer join이 2번이나 수행되는 경우는 효율성이 떨어지게 됩니다. 실제 데이터베이스에서도 테이블 간 연관관계를 맺으면, 한쪽 테이블이 외래키를 가지는 구조로 이뤄집니다. 이전에 언급한 master객체, '주인'에 대한 개념입니다.
따라서 주인 객체로 삼고 싶은 엔티티에서 @OneToOne에 속성 값을 지정해야 합니다. mappedBy는 어떤 객체가 주인인지 표시하는 속성이라고 볼 수 있습니다.

mappedBy에 들어가는 값은 견관관계를 갖고 있는 상대 엔티티에 있는 연관관계 필드의 이름이 됩니다.
이렇게 설정을 마치면, ProductDetail 엔티티는 Product 엔티티의 주인이 되는 것입니다.
여기서 주의해야 할 점은 @ToString 어노테이션을 사용할 때, @ToString.Exclude를 사용해야 한다는 점입니다.
양방향으로 객체의 관계를 정의하게 되면, 순환 참조로 인해서 StackOverFlowError가 발생하게 됩니다.
따라서 자식 객체의 값은 표현하지 않도록 Exclude를 ToString에 추가해줘야 합니다.
Many To One (다 대 1), One To Many (1 대 다) 관계
ManyToOne과 OneToMany는 객체의 연관 관계 매핑을 어느 객체의 관점에서 해석하느냐에 따라서 달라지게 됩니다.
단방향

일반적으로 외래키를 갖는 쪽이 주인의 역할을 수행하기 때문에, 이 경우 상품 엔티티가 공급업체 엔티티의 주인입니다.
만약 product 테이블에 정보를 삽입하게 된다면, 다음과 같은 쿼리가 생성될 것입니다.

해당 쿼리를 보면, product에 값을 삽입할 때, provider_id 값만 들어가는 것을 볼 수 있습니다. 이렇게 product 테이블은 JoinColumn에 설정한 이름을 기반으로 자동으로 값을 선정해서 추가하게 됩니다.

만약 위의 코드와 같이, product 객체를 통해서 provider 객체의 정보를 조회하고자 한다면, 다음과 같은 쿼리가 생성될 것입니다.

양방향
이전에는 상품 엔티티와 공급업체 엔티티 사이에 다대일 단방향 연관관계를 설정했습니다. 이제 반대로, 공급 업체를 통해서 등록된 상품을 조회하기 위한 일대다 연관관계를 설정해 보겠습니다.

일대다 연관관계의 경우, 여러 상품 엔티티가 포함될 수 있어, Collection 형식으로 필드를 생성합니다.
여기서 중요한 점은 OneToMany 연관관계 어노테이션을 적용할 때, fetch = FetchType.EAGER로 설정한 점입니다.
@OneToMany의 기본 Fetch 전략이 Lazy이기 때문에, 즉시 로딩으로 조정한 것입니다.
일대다 관계에서 fetch = FetchType.EAGER로 설정하는 이유는 관련된 엔티티들을 즉시 로딩하여 성능 최적화를 도모하기 위함입니다. 구체적으로 다음과 같은 이유들이 있습니다:
- 즉시 사용 가능: EAGER 패치 방식은 관련된 엔티티들이 즉시 로드되므로, 엔티티가 로드될 때 관련 데이터도 함께 메모리에 로드되어, 이후 접근 시 별도의 데이터베이스 쿼리가 필요하지 않습니다.
- N+1 문제 방지: EAGER 패치 방식은 한 번의 조인 쿼리로 모든 관련 데이터를 가져오기 때문에, 레이지 로딩 시 발생할 수 있는 N+1 쿼리 문제를 방지할 수 있습니다. 이는 다수의 하위 엔티티를 가져와야 할 때 쿼리 횟수가 급증하는 문제를 해결합니다.
- 일관성 보장: 특정 상황에서는 모든 관련 엔티티가 반드시 필요할 때가 있습니다. 이때 EAGER 패치 방식을 사용하면 모든 관련 엔티티들이 확실히 로드되어 데이터 일관성을 보장할 수 있습니다.
- 성능 최적화: 애플리케이션의 요구 사항에 따라 특정 연산에서 모든 관련 엔티티가 필요할 경우, EAGER 패치가 성능을 최적화할 수 있습니다. 이는 반복적인 데이터베이스 접근을 줄여줍니다.
하지만, EAGER 패치는 항상 모든 경우에 적합하지 않을 수 있습니다. 불필요한 데이터 로딩으로 인해 메모리 사용량이 증가할 수 있으며, 초기 로딩 시 시간이 더 걸릴 수 있습니다. 따라서, 상황에 맞게 EAGER와 LAZY 패치 방식을 적절히 선택하는 것이 중요합니다.
Many To Many (다 대 다) 관계
다대다(M:N) 연관관계는 실무에서는 거의 사용되지 않는 구조입니다. 다대다 연관관계를 상품과 생산업체의 예로 들자면, 한 종류의 상품이 여러 생산 업체를 통해 생산될 수 있고, 생산업체 한 곳이 여러 상품을 생산 할 수도 있습니다.

다대다 연관관계에서는 각 엔티티에서 서로를 리스트로 가지는 구조가 만들어집니다. 이런 경우에는 교차 엔티티라고 부르는 중간 테이블을 생성해서 다대다 관계를 일대다 혹은 다대일 관계로 해소해서 합니다.
실무에서 다대다 관계를 사용하지 않는 이유는 여러 가지 이유가 있습니다.
- 복잡한 데이터 관리: 다대다 관계는 데이터베이스의 구조를 복잡하게 만듭니다. 연결 테이블이 추가되어야 하며, 이를 통해 두 테이블 간의 관계를 관리해야 합니다. 이는 데이터 삽입, 삭제, 갱신 시 더 많은 신경을 써야 하며, 쿼리 작성 시에도 복잡성이 증가합니다.
- 성능 문제: 다대다 관계는 조인 연산이 빈번하게 발생하므로, 대량의 데이터 처리 시 성능 문제가 발생할 수 있습니다. 특히, 연결 테이블을 통해 여러 테이블을 조인해야 하는 경우 쿼리 성능이 저하될 수 있습니다.
- 비즈니스 로직의 복잡성: 실무에서는 단순한 다대다 관계보다는 더 복잡한 비즈니스 로직이 필요한 경우가 많습니다. 예를 들어, 두 테이블 간의 관계가 추가적인 속성을 가져야 하거나, 상태나 유효 기간 등의 추가적인 정보가 필요할 수 있습니다. 이 경우 단순한 다대다 관계보다는 일대다 또는 일대일 관계로 풀어서 관리하는 것이 더 적합합니다.
- 데이터 무결성: 다대다 관계는 데이터 무결성을 유지하기 어렵게 만들 수 있습니다. 예를 들어, 연결 테이블에서 중복 데이터가 발생하거나, 한쪽 테이블에서 데이터가 삭제될 때 관련된 모든 연결이 적절히 처리되지 않으면 데이터 무결성에 문제가 생길 수 있습니다.
- 유연성 부족: 다대다 관계는 많은 경우에서 비즈니스 요구사항을 충분히 반영하기 어렵습니다. 예를 들어, 관계 자체에 대한 추가 속성이 필요하거나, 특정 관계에 대한 조건이 필요할 때, 단순한 다대다 관계로는 이를 처리하기 어렵습니다. 이 경우 연결 테이블을 독립적인 엔티티로 관리하여 더 유연하게 처리하는 것이 일반적입니다.
영속성 전이 (Cascade)
영속성 전이란 특정 엔티티의 연속성 상태를 변경할 때, 그 엔티티와 연관된 엔티티의 연속성에도 영향을 미쳐 영속성 상태를 변경하는 것을 의미합니다.

위의 표에서 알 수 있듯이, 영속성 전이에 사용되는 타입은 엔티티 생명 주기와 연관이 있습니다. 한 엔티티가 주어진 cascade 요소의 값으로 영속 상태의 변경이 일어나면, 매핑으로 연관된 엔티티에도 동일한 동작이 일어나도록 전이를 발생시키는 것입니다.
고아객체(Orphan)
JPA에서 Orphan(고아)란 부모 엔티티와 연관관계가 끊어진 엔티티를 의미합니다. JPA에는 이러한 고아 객체를 자동으로 제거하는 기능이 있습니다. 물론 자식 엔티티가 다른 엔티티와 연관관계를 가지고 있다면, 이러한 기능을 사용하지 않는 것이 좋습니다.
사용하기 위해서는 @OneToMany와 같은 연관 관계 어노테이션에 orphanRemoval = true라는 값을 지정해 주면 됩니다.

이번 장에서는 최근에 진행하고 있는 프로젝트에 큰 도움이 될 것같은 장이었다.
특히 다대다 관계를 처리하는 작업이 많았는데, 기존에도 중간에 테이블을 두고 다대일과 일대다 관계로 해소해서 진행하고 있었는데, 잘 진행하고 있다는 생각이 들었다.
'책' 카테고리의 다른 글
[Book] 스프링 부트 핵심 가이드 Chap.11 (0) | 2024.08.05 |
---|---|
[Book] 스프링 부트 핵심 가이드 Chap.10 - 유효성 검사와 예외 처리 (0) | 2024.07.28 |
[Book] 스프링 부트 핵심 가이드 Chap.8 (0) | 2024.07.14 |
[Book] 스프링 부트 핵심 가이드 Chap.6 (0) | 2024.07.02 |
[Book] 스프링 부트 핵심 가이드 Chap.5 (1) | 2024.07.02 |

연관 관계 매핑 종류와 방향
One To One (1대1)관계
하나의 객체에 대해서 반드시 하나의 연관 객체만 매핑되는 구조
1 대 1 단방향 관계
예를 들면, 상품과 그 상품에 대한 상세정보는 1대 1 매핑일 것이다.

코드상으로는 Product Detail Entity 클래스에서 Product Entity를 가지도록 해야 한다.
@OneToOne 어노테이션과 @JoinColumn(name="product_number")를 통해서 1대 1 관계를 매핑하게 된다.
JoinColumn에서는 여러 가지 속성 값을 지정해 줄 수도 있는데,
- name : 매핑항 외래키의 이름을 설정합니다.
- rerferencedColumnName : 외래키가 참조할 상대 테이블의 칼럼명을 지정합니다.
- foreignKey : 외래키를 생성하면서, 지정할 제약 조건을 설정합니다. (unique, nullable, insertable, updateable 등..)
만약, Product Detail Entity에서 1대 1 연관관계 매핑을 했다면, JPA를 통해서 쿼리를 조회할 때, 내부에서 Join Column으로 설정한
참조 값을 토대로 해당 객체와 join 후 데이터를 추출합니다.

또한, 비즈니스 로직에서 Product Detail을 통해 Product를 찾고자 한다면, @Getter 어노테이션을 적용하여, getProduct()를 통해 해당 세부정보를 통해서 제품을 조회하는 식의 로직을 구성할 수 있습니다.
만약, 해당 관계가 nullable이 true인 경우, optional = true를 통해서 null인 값을 허용하지 않게 할 수 있습니다. 실행 시점에서 해당 컬럼은 not null로 전환되게 됩니다.
1 대 1 양방향
1 대 1 양방향 관계란 각 객체를 통해서 매핑되어 있는 객체를 조회할 수 있는 것을 의미합니다. 이전의 단방향의 경우, @OneToOne 어노테이션을 적용한 객체에서만 연관된 객체를 조회할 수 있었는데, 이번 절에서는 각자가 매핑된 객체의 정보를 조회할 수 있습니다.

하지만 이렇게 두 객체 모두 OneToOne을 사용하게 된다면, 양쪽에서 외래키를 가지고 left outer join이 2번이나 수행되는 경우는 효율성이 떨어지게 됩니다. 실제 데이터베이스에서도 테이블 간 연관관계를 맺으면, 한쪽 테이블이 외래키를 가지는 구조로 이뤄집니다. 이전에 언급한 master객체, '주인'에 대한 개념입니다.
따라서 주인 객체로 삼고 싶은 엔티티에서 @OneToOne에 속성 값을 지정해야 합니다. mappedBy는 어떤 객체가 주인인지 표시하는 속성이라고 볼 수 있습니다.

mappedBy에 들어가는 값은 견관관계를 갖고 있는 상대 엔티티에 있는 연관관계 필드의 이름이 됩니다.
이렇게 설정을 마치면, ProductDetail 엔티티는 Product 엔티티의 주인이 되는 것입니다.
여기서 주의해야 할 점은 @ToString 어노테이션을 사용할 때, @ToString.Exclude를 사용해야 한다는 점입니다.
양방향으로 객체의 관계를 정의하게 되면, 순환 참조로 인해서 StackOverFlowError가 발생하게 됩니다.
따라서 자식 객체의 값은 표현하지 않도록 Exclude를 ToString에 추가해줘야 합니다.
Many To One (다 대 1), One To Many (1 대 다) 관계
ManyToOne과 OneToMany는 객체의 연관 관계 매핑을 어느 객체의 관점에서 해석하느냐에 따라서 달라지게 됩니다.
단방향

일반적으로 외래키를 갖는 쪽이 주인의 역할을 수행하기 때문에, 이 경우 상품 엔티티가 공급업체 엔티티의 주인입니다.
만약 product 테이블에 정보를 삽입하게 된다면, 다음과 같은 쿼리가 생성될 것입니다.

해당 쿼리를 보면, product에 값을 삽입할 때, provider_id 값만 들어가는 것을 볼 수 있습니다. 이렇게 product 테이블은 JoinColumn에 설정한 이름을 기반으로 자동으로 값을 선정해서 추가하게 됩니다.

만약 위의 코드와 같이, product 객체를 통해서 provider 객체의 정보를 조회하고자 한다면, 다음과 같은 쿼리가 생성될 것입니다.

양방향
이전에는 상품 엔티티와 공급업체 엔티티 사이에 다대일 단방향 연관관계를 설정했습니다. 이제 반대로, 공급 업체를 통해서 등록된 상품을 조회하기 위한 일대다 연관관계를 설정해 보겠습니다.

일대다 연관관계의 경우, 여러 상품 엔티티가 포함될 수 있어, Collection 형식으로 필드를 생성합니다.
여기서 중요한 점은 OneToMany 연관관계 어노테이션을 적용할 때, fetch = FetchType.EAGER로 설정한 점입니다.
@OneToMany의 기본 Fetch 전략이 Lazy이기 때문에, 즉시 로딩으로 조정한 것입니다.
일대다 관계에서 fetch = FetchType.EAGER로 설정하는 이유는 관련된 엔티티들을 즉시 로딩하여 성능 최적화를 도모하기 위함입니다. 구체적으로 다음과 같은 이유들이 있습니다:
- 즉시 사용 가능: EAGER 패치 방식은 관련된 엔티티들이 즉시 로드되므로, 엔티티가 로드될 때 관련 데이터도 함께 메모리에 로드되어, 이후 접근 시 별도의 데이터베이스 쿼리가 필요하지 않습니다.
- N+1 문제 방지: EAGER 패치 방식은 한 번의 조인 쿼리로 모든 관련 데이터를 가져오기 때문에, 레이지 로딩 시 발생할 수 있는 N+1 쿼리 문제를 방지할 수 있습니다. 이는 다수의 하위 엔티티를 가져와야 할 때 쿼리 횟수가 급증하는 문제를 해결합니다.
- 일관성 보장: 특정 상황에서는 모든 관련 엔티티가 반드시 필요할 때가 있습니다. 이때 EAGER 패치 방식을 사용하면 모든 관련 엔티티들이 확실히 로드되어 데이터 일관성을 보장할 수 있습니다.
- 성능 최적화: 애플리케이션의 요구 사항에 따라 특정 연산에서 모든 관련 엔티티가 필요할 경우, EAGER 패치가 성능을 최적화할 수 있습니다. 이는 반복적인 데이터베이스 접근을 줄여줍니다.
하지만, EAGER 패치는 항상 모든 경우에 적합하지 않을 수 있습니다. 불필요한 데이터 로딩으로 인해 메모리 사용량이 증가할 수 있으며, 초기 로딩 시 시간이 더 걸릴 수 있습니다. 따라서, 상황에 맞게 EAGER와 LAZY 패치 방식을 적절히 선택하는 것이 중요합니다.
Many To Many (다 대 다) 관계
다대다(M:N) 연관관계는 실무에서는 거의 사용되지 않는 구조입니다. 다대다 연관관계를 상품과 생산업체의 예로 들자면, 한 종류의 상품이 여러 생산 업체를 통해 생산될 수 있고, 생산업체 한 곳이 여러 상품을 생산 할 수도 있습니다.

다대다 연관관계에서는 각 엔티티에서 서로를 리스트로 가지는 구조가 만들어집니다. 이런 경우에는 교차 엔티티라고 부르는 중간 테이블을 생성해서 다대다 관계를 일대다 혹은 다대일 관계로 해소해서 합니다.
실무에서 다대다 관계를 사용하지 않는 이유는 여러 가지 이유가 있습니다.
- 복잡한 데이터 관리: 다대다 관계는 데이터베이스의 구조를 복잡하게 만듭니다. 연결 테이블이 추가되어야 하며, 이를 통해 두 테이블 간의 관계를 관리해야 합니다. 이는 데이터 삽입, 삭제, 갱신 시 더 많은 신경을 써야 하며, 쿼리 작성 시에도 복잡성이 증가합니다.
- 성능 문제: 다대다 관계는 조인 연산이 빈번하게 발생하므로, 대량의 데이터 처리 시 성능 문제가 발생할 수 있습니다. 특히, 연결 테이블을 통해 여러 테이블을 조인해야 하는 경우 쿼리 성능이 저하될 수 있습니다.
- 비즈니스 로직의 복잡성: 실무에서는 단순한 다대다 관계보다는 더 복잡한 비즈니스 로직이 필요한 경우가 많습니다. 예를 들어, 두 테이블 간의 관계가 추가적인 속성을 가져야 하거나, 상태나 유효 기간 등의 추가적인 정보가 필요할 수 있습니다. 이 경우 단순한 다대다 관계보다는 일대다 또는 일대일 관계로 풀어서 관리하는 것이 더 적합합니다.
- 데이터 무결성: 다대다 관계는 데이터 무결성을 유지하기 어렵게 만들 수 있습니다. 예를 들어, 연결 테이블에서 중복 데이터가 발생하거나, 한쪽 테이블에서 데이터가 삭제될 때 관련된 모든 연결이 적절히 처리되지 않으면 데이터 무결성에 문제가 생길 수 있습니다.
- 유연성 부족: 다대다 관계는 많은 경우에서 비즈니스 요구사항을 충분히 반영하기 어렵습니다. 예를 들어, 관계 자체에 대한 추가 속성이 필요하거나, 특정 관계에 대한 조건이 필요할 때, 단순한 다대다 관계로는 이를 처리하기 어렵습니다. 이 경우 연결 테이블을 독립적인 엔티티로 관리하여 더 유연하게 처리하는 것이 일반적입니다.
영속성 전이 (Cascade)
영속성 전이란 특정 엔티티의 연속성 상태를 변경할 때, 그 엔티티와 연관된 엔티티의 연속성에도 영향을 미쳐 영속성 상태를 변경하는 것을 의미합니다.

위의 표에서 알 수 있듯이, 영속성 전이에 사용되는 타입은 엔티티 생명 주기와 연관이 있습니다. 한 엔티티가 주어진 cascade 요소의 값으로 영속 상태의 변경이 일어나면, 매핑으로 연관된 엔티티에도 동일한 동작이 일어나도록 전이를 발생시키는 것입니다.
고아객체(Orphan)
JPA에서 Orphan(고아)란 부모 엔티티와 연관관계가 끊어진 엔티티를 의미합니다. JPA에는 이러한 고아 객체를 자동으로 제거하는 기능이 있습니다. 물론 자식 엔티티가 다른 엔티티와 연관관계를 가지고 있다면, 이러한 기능을 사용하지 않는 것이 좋습니다.
사용하기 위해서는 @OneToMany와 같은 연관 관계 어노테이션에 orphanRemoval = true라는 값을 지정해 주면 됩니다.

이번 장에서는 최근에 진행하고 있는 프로젝트에 큰 도움이 될 것같은 장이었다.
특히 다대다 관계를 처리하는 작업이 많았는데, 기존에도 중간에 테이블을 두고 다대일과 일대다 관계로 해소해서 진행하고 있었는데, 잘 진행하고 있다는 생각이 들었다.
'책' 카테고리의 다른 글
[Book] 스프링 부트 핵심 가이드 Chap.11 (0) | 2024.08.05 |
---|---|
[Book] 스프링 부트 핵심 가이드 Chap.10 - 유효성 검사와 예외 처리 (0) | 2024.07.28 |
[Book] 스프링 부트 핵심 가이드 Chap.8 (0) | 2024.07.14 |
[Book] 스프링 부트 핵심 가이드 Chap.6 (0) | 2024.07.02 |
[Book] 스프링 부트 핵심 가이드 Chap.5 (1) | 2024.07.02 |