이번주 이커머스를 구현하면서 당면했던 내용을 정리해본다.
JPA를 사용하면서 알아야 하는 것들
도메인 중심으로 개발하고 각 도메인이 연결되었을 때 역할, 책임, 경계에 대해 고민하면서 구현하는 한 주였다. 하지만 JPA라는 도구를 사용하면서 JPA엔티티를 도메인 엔티티와 같게 두고 사용하기에 도메인을 DB에 저장할 때 어떤 형태로 어떤 방법으로 할 지에 대한 것도 매뉴얼 적으로 알 필요가 있었다.
1. VO를 JPA/QueryDSL에서 사용할 때 @Convert vs @Embedded
AttributeConverter는 VO ↔ DB 컬럼 값 간 변환이 필요할 때 사용하는 것으로, 크게 @Convert, @Embedded 방법이 있다.
두 방식의 큰 차이는 Convert는 컬럼 1개로 저장하고, Embedded는 여러 속성을 각각의 DB 컬럼에 저장한다.
컬럼 1개로 저장하면 되서 Convert를 사용했는데, 상품 목록 개발할 때 QueryDSL을 사용하면서 Convert는 경로를 인식하지 못해서
Vo.getValue() 같은 것을 QueryDSL에서 사용하지 못했다. 그래서 Projection DTO에 기본 타입을 갖도록 했다가 VO 자체를 갖도록 변경했다. Embedded로 변경할 수도 있었으나 Convert 만들어둔 것이 아까워서 그렇게 하긴 했다. 이 부분도 어떤 것을 선택하는 것이 좋을지 생각해보면 좋을 것 같다.
참고로 Embedded를 VO에 사용할 경우는 JPA가 VO 내부 필드를 자동 매핑해서 QueryDSL에서 필드 경로 탐색이 가능하고 정렬, 조건 검색 등에서 유리하다고 한다. 그러나 하나의 VO 클래스 (@Embeddable)를 여러 필드에서 사용할 때, 필드 내부에 있는 속성명(예: amount) 들이 DB 컬럼명 충돌을 일으킬 수 있다. 그때는 @AttributeOverrides로 컬럼명을 재정의해줘야 한다.
@Entity
public class Product {
@Id
@GeneratedValue
private Long id;
@Embedded
private Money price;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "amount", column = @Column(name = "discount_amount"))
})
private Money discountPrice;
}
2. 연관관계 매핑 사용 여부
연관관계 매핑을 해야하나 말아야 하는 고민도 있었다.
연관관계는 양방향으로 할 수도 있고, 하나의 방향에만 해둘 수 있다. 또 하나의 방향만 했을 때 어디에 연관관계 매핑을 해두냐도 있다. Order에 @OneToMany 설정 ,OrderLine에 @ManyToOne만 설정 시 두 방식의 차이는 아래와 같다.
내용 | Order에 @OneToMany 설정 | OrderLine에 @ManyToOne만 설정 |
양방향 매핑 | 가능 (Order ↔ OrderLine) | 불가능 (OrderLine → Order) |
주문에서 아이템 탐색 | order.getOrderLines() 사용 가능 | 직접 쿼리해야 함 |
성능 문제 가능성 | 아이템이 수천 개면 성능 저하 우려 | 성능 부담 없음 |
집합 내 consistency 유지 | 더 자연스러움 (root가 라인을 관리) | 가능은 하지만 코드로 명시해야 함 |
연관관계 매핑 시, 조인 방식(fetch join vs EntityGraph)의 차이점
항목 | fetch join | @EntityGraph |
로딩 방식 | JPQL 기반 즉시 로딩 | 선언형 즉시 로딩 |
쿼리 제어력 | 매우 높음 (조건 지정 가능) | 낮음 (JPA 위임) |
재사용성 | 낮음 | 높음 |
쿼리 명확성 | 명확 (직접 작성) | 불투명 (JPA가 생성) |
페이징 | 불가 (@OneToMany) | 가능 (@ManyToOne 등) |
복잡 조인 | 가능 | 어려움 |
- 단순한 fetch + 재사용 가능한 경우: @EntityGraph
- 복잡한 join, 조건 있는 join이 필요한 경우: fetch join
- 페이징이 필요한 경우는 특히 주의 → 컬렉션을 fetch join 하면 안 됨
롬복 관련
Lombok의 @Builder와 @AllArgsConstructor를 함께 사용하는 경우
Lombok은 @Builder가 붙으면 내부적으로 모든 필드를 초기화하는 생성자를 만들고,
필드 초기화 구문(= new ArrayList<>())을 무시한다.
즉, 아래와 같은 경우에는 orderLines가 null이 될 수 있다
@Builder
@AllArgsConstructor
public class Order {
@Builder.Default //해결 방법
private List<OrderLine> orderLines = new ArrayList<>(); // 이 초기화는 무시됨
}
당면했던 문제들
1. 좋아요 등록 멱등성 : Unique 제약조건으로 안한 이유
save() 호출 시 예외가 발생하여 이를 처리하려고 했는데, 해당 메서드에 @Transactional이 적용되어 있어 트랜잭션을 분리해야 했다. REQUIRES_NEW 속성을 사용하려 했지만 같은 클래스 내에서는 트랜잭션이 새로 생성되지 않는다는 한계가 있다고 알고 있다. 또는 특정 Exception 대해 noRollback을 명시하는 옵션도 있지만 여러 케이스 확인과 가장 좋은 방법을 찾는 데 시간이 부족했다. 그래서 예외 처리 대신 soft delete 방식으로 개발 방향을 변경했다.
그러나 soft delete의 경우 구현 시에 deletedAt 컬럼을 함께 조회 조건에 포함시켜야 하는 번거로움이 존재했다.
고민한 주제들
1. 목록 조회 시 정렬조건과 각종 도메인 조건은 페이징 전에 이뤄져야 한다.
그러므로, 도메인에서 도메인 조건들을 처리하는 것이 아니라 DB에서 도메인 조건과 정렬 조건들이 한꺼번에 처리 되어야 한다.
멘토링 시간등에서 이야기 했던 MV, 읽기와 쓰기 도메인이 다른 것등이 함께 나올 수 있는 주제이다. 그리고 설계 시에 도메인 연관관계를 고민했던 내용들이 있었는데 이렇게 DB에서 처리할 수 밖에 없는 것은 도메인 연관관계로 처리하지 않는 것이 될 수 있다.
2. 페이지 처리
1. 커서 기반과 offset기반
커서 기반은 페이지가 뒤로 갈수록 성능적 이점이 있다. 근데 쇼핑을 하다보면 스크롤 방식보다는 각 페이지가 명시되어 있는 것이 고객 입장에서도 편할 때가 있다. 이 때 offset 기반을 선택할 수 밖에 없을까? 커서 기반을 이용한다고 해도 total count는 따로 조회해야 하는데 이 부분을 없앨 수는 없을까
'Essay > WIL' 카테고리의 다른 글
[WIL] 부트캠프 4주차 (3) | 2025.08.10 |
---|---|
[WIL] 부트캠프 1주차 (0) | 2025.07.20 |
자바 2주차 WIL (0) | 2022.02.27 |
자바 1주차 WIL 키워드 (0) | 2022.02.20 |