[JPA 활용1] [2] 도메인 분석 설계
인프런 김영한 님의 강의를 듣고 작성한 글입니다.
[1] 요구사항 분석
■ 동작화면
■ 기능목록
- 회원기능
- 회원등록
- 회원조회
- 상품기능
- 상품등록
- 상품수정
- 상품조회
- 주문기능
- 상품주문
- 주문내역조회
- 주문취소
- 기타 요구사항
- 상품은 재고 관리가 필요하다.
- 상품의 종류는 도서, 음반, 영화가 있다.
- 상품을 카테고리로 구분할 수 있다.
- 상품주문 시 배송 정보를 입력할 수 있다.
[2] 도메인 모델과 테이블 설계
■ 도메인 모델과 테이블 설계
- 주문과 상품 : 다대다 관계 => 일대다, 다대일 관계
다대다 관계는 실무에선 거의 사용하지 않는다.
따라서 그림처럼 주문상품(OrderItem)이라는 엔티티를 추가해 다대다 관계를 일대다·다대일 관계로 풀어냈다. - 카테고리와 상품 : 다대다 관계(@ManyToMany)
다대다 관계는 좋지 않지만 다양한 예제를 위해 사용.
연결 테이블이 필요하다.
■ 회원 엔티티 분석
- 회원과 주문
회원이 주문을 하기 때문에, 회원이 주문리스트를 가지는 것은 얼핏 보면 잘 설계한 것 같지만, 사실 객체 세상과 실제 세계는 다르다.
회원과 주문은 동급으로 두고 생각해야 한다. 실무에선 주문을 생성할 때 회원이 필요하다고 해석해야 한다.
위 내용에 따르면 주문이 회원을 참조하는 것으로 충분(단방향)하나, 다양한 예제를 위해 양방향 관계를 사용했다.
■ 회원 테이블 분석
- ITEM 테이블
앨범, 도서, 영화 타입을 통합해서 하나의 테이블로 만듦
단일 테이블이 성능면에서 좋다.
DTYPE 컬럼으로 타입을 구분한다. - ORDERS 테이블
테이블명이 ORDER 가 아니라 ORDERS 인 것은 데이터베이스가 order by 때문에 예약어로 잡고 있는 경우가 많기 때문. 그래서 관례상 ORDERS를 많이 사용한다. - CATEGORY, ITEM 테이블
회원 엔티티 분석에서 다대다 관계였던 두 테이블을 CATECORY_ITEM이라는 매핑 테이블을 두어 일대다, 다대일 관계로 풀어냈다.
참고] 테이블명·컬럼명
데이터베이스 테이블명, 컬럼명에 대한 관례는 회사마다 다르다.
보통은 대문자 + (언더스코어)나 소문자 + (언더스코어) 방식 중에 하나를 지정해서 일관성 있게 사용한다
■연관관계 매핑 분석
외래 키가 있는 곳을 연관관계의 주인으로 정해라.
- 회원과 주문
일대다, 다대일의 양방향 관계
연관관계의 주인을 정해야 하는데, 외래 키가 있는 주문을 연관관계의 주인으로 정하는 것이 좋다.
그러므로 Order.member를 ORDERS.MEMBER_ID 외래 키와 매핑한다. - 주문상품과 주문
다대일 양방향 관계
외래 키가 주문상품에 있으므로 주문상품이 연관관계의 주인이다.
그러므로 OrderItem.order를 ORDER_ITEM.ORDER_ID 외래 키와 매핑한다. - 주문상품과 상품
다대일 단방향 관계
OrderItem.item을 ORDER_ITEM.ITEM_ID 외래 키와 매핑한다. - 주문과 배송
일대일 양방향 관계
Order.delivery를 ORDERS.DELIVERY_ID 외래 키와 매핑한다. - 카테고리와 상품
다대다 관계
@ManyToMany를 사용해서 매핑한다.
(실무에서 @ManyToMany는 사용하지 말자)
참고 ] 외래 키가 있는 곳을 연관관계 주인으로 정해라
연관관계의 주인은 단순히 외래 키를 누가 관리하냐의 문제이지 비즈니스상 우위에 있다고 주인으로 정하면 안 된다.
예를 들어서 자동차와 바퀴가 있으면, 일대다 관계에서 항상 다쪽에 외래 키가 있으므로 외래 키가 있는 바퀴를 연관관계의 주인으로 정하면 된다.
물론 자동차를 연관관계의 주인으로 정하는 것이 불가능한 것은 아니지만, 자동차를 연관관계의 주인으로 정하면 자동차가 관리하지 않는 바퀴 테이블의 외래 키 값이 업데이트되므로 관리와 유지보수가 어렵고, 추가적으로 별도의 업데이트 쿼리가 발생하는 성능 문제도 있다.
[3] 엔티티 클래스 개발 1·2
Member <> Order [다대일 관계]
- 외래 키 값을 가진 Order가 연관관계의 주인
- 참고] @ManyToOne, @OneToOne은 기본이 즉시 로딩 → 전부 FetchType.LAZY로 설정해야 한다.
- 연관관계의 주인이 바뀔 때만 값이 수정된다. 반대편은 조회만 된다.
Order <> Delivery [일대일 관계]
- @OneToOne일 땐 조회할 일이 많은 쪽을 연관관계의 주인으로 선택하는 게 좋다. 여기선 Order를 선택
- Enum 타입으로 주문상태, 배달 상태 구분.
- DB엔 Enum타입이 없어서 @Enumerated(EnumType.STRING)을 명시해줘야 한다.
- 순서를 DB에 저장하는 EnumType.ORDINAL은 사용하지 않는다.
자세한 내역은 [JPA] [4] 엔티티 매핑(상편) 확인
Address - 값 타입
- @Embeddable로 값 타입을 정의한 Address에 표시
- @Embedded 값 타입 사용하는 곳인 Delivery에 표시
- 임베디드 타입은 기본 생성자가 필수다.
- JPA 스펙상 엔티티나 임베디드타입( @Embeddable )은 자바 기본생성자(default constructor)를 public 또는
protected로 설정해야 한다. public으로 두는 것보다는 protected로 설정하는 것이 그나마 더 안전하다.
- JPA 스펙상 엔티티나 임베디드타입( @Embeddable )은 자바 기본생성자(default constructor)를 public 또는
Order <> OrderItem <> Item (다대다 분리)
- 다대다 관계를 풀기 위해 OrderItem 테이블 만듦(연결 테이블을 엔티티로 승격)
Category <> Item (다대다)
- 실무에선 쓰이지 않는 다대다 방식
- @JoinTable로 연결 테이블 생성
- joinColumns : 현재 PK로 사용할 컬럼 지정
- inverseJoinColums : 반대편에서 PK로 사용할 컬럼 지정
- Category가 계층구조라 자기 자신과 다대일 양방향 매핑
- @Inheritance : 상속전략 지정 (여기선 싱글 테이블 선택)
- @DiscrimatorColum으로 하위 클래스 구분하기 위한 컬럼명 지정(디폴트는 DTYPE)
- 자세한 내역은 [JPA] [7] 고급 매핑 확인
[4] 엔티티 설계 시 주의사항
① 엔티티에는 가급적 Setter를 사용하지 말자
- Setter가 모두 열려있으면, 변경포인트가 너무 많아 유지보수가 어렵다.
② 모든 연관관계는 지연로딩으로 설정해야 한다.
- 즉시로딩( EAGER )은 예측이 어렵고, 어떤 SQL이 실행될지 추적하기 어렵다.
- 특히 JPQL을 실행할 때 N+1 문제가 자주 발생한다.
- @XToOne(OneToOne, ManyToOne) 관계는 기본이 즉시 로딩이므로 직접 지연 로딩으로 설정해야 한
다. (ctrl + shift + f로 @XToOne을 찾고 바꾸면 좀 더 간편하다) - 연관된 엔티티를 함께 DB에서 조회해야 하면, fetch join 또는 엔티티 그래프 기능을 사용한다.
③ 컬렉션은 필드에서 초기화하자
- null 문제에서 안전하다.
- 하이버네이트는 엔티티를 영속화할 때, 컬렉션을 감싸서 하이버네이트가 제공하는 내장컬렉션으로 변경한
다. - 만약 getOrders()처럼 임의의 메서드에서 컬렉션을 잘못 생성하면 하이버네이트 내부 메커니즘에 문
제가 발생할 수 있다. 따라서 필드 레벨에서 생성하는 것이 가장 안전하고, 코드도 간결하다.
④ 테이블·컬럼명 생성 전략
- 하이버네이트 기존 구현 : 엔티티 필드명을 그대로 테이블의 컬럼명으로 사용
- 스프링부트 신규 설정(엔티티(필드) → 테이블(컬럼))
- 카멜 케이스 → 언더스코어(memberPoint → member_point)로 변경
- .(점) → _(언더스코어)로 변경
- 대문자 → 소문자로 변경
- 적용 2 단계
- 논리명 생성
- 명시적(@Table, @Column)으로 컬럼, 테이블명을 설정할 수 있다.
- 명시적 설정을 하지 않으면 ImplicitNamingStrategy(암시적 명칭 전략) 사용
- 물리명 적용
- 기본적으로 하이버네이트에선 물리적 이름은 논리적 이름을 그대로 사용
- PhysicalNamingStrategy 인터페이스를 구현하여 논리적 이름을 내부 명명 규칙을 따르는 물리적 이름에 매핑할 수 있다.(username → usernm 등으로 회사 룰에 맞춰 바꿀 수 있음)
- 논리명 생성
추가내용]
■ Cascade
//Cascade 사용 전
persist(orderItemA)
persist(orderItemB)
persist(orderItemC)
persiste(order)
//Cascade 사용 후
persiste(order)
Order - 일부 발췌
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
- CascadeType.ALL은 order를 persist()할 때 연관된 컬렉션을 같이 영속화하거나, 삭제해 주는 기능을 한다.
■ 연관관계 편의 메서드
Order
//==연관관계 메서드 ==//
public void setMember(Member member){
this.member = member;
member.getOrders().add(this);
}
public void addOrderItem(OrderItem orderItem){
orderItems.add(orderItem);
orderItem.setOrder(this);
}
public void setDelivery(Delivery delivery){
this.delivery = delivery;
delivery.setOrder(this);
}
- 양방향 연관관계를 구현했을 때 한 곳에 데이터를 생성하면 반대편도 넣어주어야 한다. 개발 도중 해당 부분을 깜빡하기 쉬우므로 연관관계 편의 메서드를 구현하면 헷갈리지 않게 설정할 수 있다.
- [JPA] [5] 연관관계 매핑 기초 Tip 연관관계 편의 메소드 부분 참고
Category
@Entity
@Getter
@Setter
public class Category {
@Id
@GeneratedValue
@Column(name = "category_id")
private Long id;
private String name;
@ManyToMany
@JoinTable(name = "category_item",
joinColumns = @JoinColumn(name = "category_id"),
inverseJoinColumns = @JoinColumn(name = "item_id")
)
private List<Item> items = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Category parent;
@OneToMany(mappedBy = "parent")
private List<Category> child = new ArrayList<>();
//==연관관계 메서드==//
public void addChildCategory(Category child){
this.child.add(child);
child.setParent(this);
}
}
- Category는 자기 자신을 양방향 연관관계로 지정.