@Embedded
JPA 에서는 임베디드 타입(embedded type)이란 것이 존재하며, 새로운 값 타입을 직접 정의해서 사용할 수 있다.
직접 정의한 임베디드 타입도 int, string 처럼 값 타입이다.
평범한 회원 엔티티에서
"회원 엔티티는 이름, 주소 도시, 주소 번지, 주소 우편 번호, 주문상품들 을 가진다." 보다는
"회원 엔티티는 이름, 주소, 주문상품들을 가진다." 라고 말 할 때 더 응집력이 높은 명확한 코드를 작성할 수 있다.
임베디드 타입을 사용하는 방법은
@Embeddable
값 타입을 정의하는 곳에 표시한다.
@Embedded
값 타입을 사용하는 곳에 표시한다.
- 임베디드 타입은 기본 생성자가 필수이다. public한 생성자를 사용하도록 유도하기 위해 기본 생성자는 JPA가 제공해주는 또 다른 접근지정자인 protected를 사용하여 만들어주었다.
- @Setter 를 제거하고, 생성자에서 값을 모두 초기화해서 변경 불가능한 클래스로 만들어주었다.
Entity 클래스를 개발한다.
이론적으로 Getter, Setter 모두 제공하지 않고, 꼭 필요한 별도의 메서드를 제공하는 것이 가장 이상적이다.
그러나 실무에서 엔티티의 데이터를 조회 할 일이 많기 때문에, Getter의 경우 모두 열어두는 것이 편리하다.
Getter는 아무리 호출해도 어떤 일이 발생하지는 않으므로 괜찮지만, Setter는 호출하면 데이터가 변한다.
Setter를 막 열어두면 가까운 미래에 엔티티가 도대체 왜 변경되는지 추적이 어려우므로,
엔티티를 변경할 때는 Setter 대신에 변경 지점이 명확하도록 변경을 위한 비지니스 메서드를 별도로 제공해야 한다.
Delivery 엔티티의 경우에 Order 엔티티의 일대일 매핑된다. 일대일의 경우 연관 관계의 주인이 어느 쪽이 되어도 상관 없다. 중심이 되는 쪽의 엔티티가 가져가는 것이 좋으므로 order 엔티티가 가져간다.
status 의 경우 DeliveryStatus 타입으로 직접 만든 enum 클래스의 타입이다.
@Enumerated
Java의 enum 타입을 엔티티 클래스의 속성으로 사용할 수 있다.
@Enumerated 애노테이션에는 두 가지 EnumType이 존재한다.
- EnumType.ORDINAL : enum 순서 값을 DB에 저장 (default)
- EnumType.STRING : enum 이름을 DB에 저장
ORDINAL(숫자형) 의 경우에는 Enum 상수 값들 사이에 하나가 추가 될 경우, 전체 값이 변경되어버리는 위험한 일이 발생되기 때문에
String을 사용했다.
주문 엔티티 클래스이다.
Order 입장에서 Member는 다대일 관계이기 때문에 @ManyToOne으로 연관관계 매핑을 해주었고, 다 쪽이 연관관계 주인으로 두기에 @JoinColumn(name="member_id") 로 외래키(FK)를 매핑해주었다.
Order(주문)와 Item(상품)는 다대다 매핑이기 때문에 중간에 OrderItem(주문상품) 엔티티를 추가하여 일대다, 다대일로 풀어주었다.
하나의 order에 여러 orderItem이 있을 수 있고,
모든 orderItem을 persist해줄 필요 없이 cascade = CascadeType.ALL 을 통해 한 order에 있는 모든 주문상품을 영속화시킬 수 있다.
JPA의 N+1 문제란?
연관 관계에서 발생하는 이슈로 연관 관계가 설정된 엔티티를 조회할 경우에 조회된 데이터 개수(n)만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오게 된다.
예를 들어,
고양이와 고양이 집사의 관계를 표현해보겠다.
- 고양이 집사는 여러 마리의 고양이를 키우고 있다.
- 고양이는 한 명의 집사에 종속되어 있다.
고양이 집사를 조회해보자
- 고양이를 10마리 생성하였다.
- 고양이 집사를 10명 생성하였다.
- 고양이 집사는 10마리씩 고양이를 키우고 있다.
- 고양이 집사를 조회해보자.
Hibernate SQL log를 활성화하여 실제 호출된 쿼리를 확인해보았을 때
- 고양이 집사 조회하는 쿼리를 호출하였다.
- 고양이를 조회하는 쿼리가 고양이 집사를 조회한 row 만큼 쿼리가 호출한 것이 확인 되었다.
default인 FetchType.EAGER에서 FetchType.LAZY 로 설정을 변경했을 때,
쿼리가 하나밖에 호출되지 않았다.
FetchType을 LAZY로 설정했다는 것은 연관관계 데이터를 Proxy 객체로 바인딩한다는 것이다.
하지만 실제로는 연관관계 엔티티를 프록시만으로는 사용하지 않는다. 연관관계 엔티티의 멤버 변수를 사용하거나 가공하는 일은 코드를 구현하는 경우가 많기 때문에 다른 해결방안이 필요하다.
FetchType을 변경하는 것은 단지 N+1 발생 시점을 연관관계 데이터를 사용하는 시점으로 미룰지, 아니면 초기 데이터 로드 시점에 가져오느냐에 차이만 있는 것이다.
N+1은 왜 발생하는 것일까?
jpaRepository에 정의한 인터페이스 메서드를 실행하면 JPA는 메서드 이름을 분석해서 JPQL을 생성하여 실행하게 된다.
JPQL은 엔티티 객체와 필드 이름을 가지고 쿼리를 실행하기 떄문에
JPQL은 findAll() 이란 메서드를 수행하였을 때 해당 엔티티를 조회하는 select * from Owner 쿼리만 실행하게 되는 것이다.
JPQL 입장에서는 연관관계 데이터를 무시하고 해당 엔티티 기준으로 해당 엔티티 기준으로 쿼리를 조회하기 때문이다.
그렇기 때문에 연관된 엔티티 데이터가 필요한 경우, FetchType으로 지정한 시점에 조회를 별도로 호출하게 된다.
해결방안으로는 Fetch join 이 있다.
사실 우리가 원하는 쿼리는
"select o from Owner o join fetch o.cats" 일 것이다.
Fetch Join을 사용하면 최적화된 쿼리를 우리가 직접 사용할 수 있다. 그러나 이는 jpaRepository가 지원해주는 것이 아닌 JPQL로 작성해야 한다.
단점으로는,
Fetch Join을 사용하게 되면 데이터 호출 시점에 모든 연관 관계의 데이터를 가져오기 때문에 FetchType을 LAZY로 해놓는게 의미가 없다.
주문 상품 엔티티이다.
주문 엔티티와 상품 엔티티의 다대다 관계를 풀어주기 위해 넣은 엔티티이다.
Item과 Order에 대해 전부 다대일 매핑을 해주었다.
빈 생성자가 하나 필요하기 때문에 @NoArgsConstructor 애노테이션으로 만들어주었고 접근지정자를 protected로 해주었다.
생성 메서드를 엔티티에 작성해주었다.
핵심 비지니스 로직을 엔티티 파일에 작성해줌으로써 응집력을 높여주었다. (상품 취소, 상품 조회)
상속 관계 매핑
관계형 데이터베이스에는 객체지향에서의 상속이라는 개념이 없다.
대신 슈퍼타입 서브타입 관계 라는 모델링 기법이 가장 유사하다.
ORM에서 슈퍼타입 서브타입 논리모델을 실제 물리 모델인 테이블로 구현할 때는 3가지 방법이 있다.
1. 각각의 테이블로 변환 (조인 전략)
각각을 모두 테이블로 만들고 조회할 때 조인을 사용한다.
2. 통합 테이블로 변환 (단일 테이블 전략)
테이블을 하나만 사용해서 통합한다.
3. 서브타입 테이블로 변환 (구현 클래스마다 테이블 전략)
서브 타입마다 하나의 테이블을 만든다.
단일 테이블 전략을 사용했다. Item 엔티티에는 앨범, 도서, 영화 엔티티가 상속(extends) 한다.
이 세 타입을 통합해서 하나의 테이블로 만들고, DTYPE 컬럼으로 타입을 구분한다.
@DiscriminatorValue() 를 지정해서 구분 컬럼을 사용해주었다.
지정해주지 않으면 기본으로 엔티티 이름이 사용된다.
Reference
- https://techblog.woowahan.com/2527/
- https://incheol-jung.gitbook.io/docs/q-and-a/spring/n+1
- https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-%ED%99%9C%EC%9A%A9-1
- https://www.nowwatersblog.com/jpa/ch7/7-1
'Server' 카테고리의 다른 글
[Spring] Spring boot와 JPA 활용 - 웹 계층 개발 (0) | 2022.07.05 |
---|---|
[Spring] Spring boot와 JPA 활용 - 주문 도메인 개발 (0) | 2022.06.24 |
[Spring] Spring boot와 JPA 활용 - 상품 도메인 개발 (0) | 2022.06.23 |
[Spring] Spring boot와 JPA 활용 - 애플리케이션 구현 준비, 회원 도메인 개발 (0) | 2022.06.19 |
[Spring] Spring boot와 JPA 활용 - 프로젝트 환경설정 (0) | 2022.06.05 |