프로젝트 코드 작성 중 EmbeddidId와 관련한 고려사항을 정리해보았다.
구현 목표
매일 자정을 기준으로 랜덤하게 갱신되는 Shop에서, 특정 수량을 구매하는 기능 구현
설계
Shop : 매일 랜덤한 아이템 리스트와 수량이 업데이트된다. (자정 기준) 구매하지 않더라도 시간이 지나면 목록에서 사라짐(ex. 로스트아크 떠돌이 상인)
User : Shop에 갱신되는 목록 중 일부 아이템, 일부 수량을 구매할 수 있다(최대 수량 이하).
Purchase : User의 Shop 구매 목록(중간테이블 역할)
작업내용
이전부터 Entity를 설계할 땐, 관계 설정 시 항상 비식별 관계로 참조되는 형태로 구성했다.
@Entity
public class Purchase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long purchaseId;
@ManyToOne
@JoinColumn(name = "shop_id")
private Shop shop;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
@Column
private Long count;
@Column
private LocalDateTime purchaseTime;
이런 느낌으로 entity마다 GeneratedValue를 사용해 Id를 만들고, shop 과 user 를 비식별 Column으로 사용했었다.
웹 개발의 입문(?)이라고 할 수 있는 게시판 만들기에서 사용되는 게시판, 댓글, 유저 등의 경우 보통 저런 id를 기반으로도 여러 작업을 진행할 수 있기 때문에 위와 같은 Id방식을 사용하는 게 매우 편리하고 좋았다.
이렇게 할 경우 식별자인 purchaseId 자체에는 shop이나 user에 관한 어떠한 정보도 포함되어 있지 않기 때문에, shop 정보나 user 정보를 기준으로 조회할 시 더 번거로울 수 있다.
테이블 성격 상 구매 내역을 저장하기만 하는 해당 Entity의 경우에는 위 방식의 구매 내역 키만 가지고 뭔가 작업하는 일은 거의 없다.
유저 별 구매 갯수 초과 여부를 판단하거나,
shop별 구매내역이나 특정 user의 구매 내역과 같이 통계에 사용되는 경우가 대부분일텐데, 이런 상황에서 purchaseId라는 값은 거의 쓸모가 없었다.
그래서 EmbeddidId를 활용해 복합 키를 만들어보기로 했다.
코드의 경우 구조를 설명하기 위해 필요한 부분만 남겨놓았다.
@Entity
public class Shop {
@EmbeddedId
private ShopPk shopId;
@Column
private Long count;
@ManyToOne
@MapsId("itemId")
@JoinColumn(name = "item_id")
private Item item;
@Embeddable
public class ShopPk implements Serializable {
private LocalDateTime startTime;
private Long itemId;
}
}
우선 Shop의 경우
1. 매일 자정을 기준으로 item 리스트가 갱신된다.
2. item 목록은 랜덤으로 갱신되며, 같은 날짜에는 중복되지 않는다.
두 가지 조건을 만족하는 식별 키로
등록일자와 item_id를 복합 키로 하는 entity로 설계하였다.
item의 경우 다른 날짜에는 반복적으로 등록될 수 있기 때문에,
Shop에서 ManyToOne 관계로 설정했다.
@Entity
@NoArgsConstructor
@Getter
public class Purchase {
@EmbeddedId
private PurchasePk purchaseId;
@Column
private Long count;
@Column
private LocalDateTime purchaseTime;
@ManyToOne
@MapsId("shopId")
@JoinColumn(name = "shop_id")
private Shop shop;
@ManyToOne
@MapsId("userId")
@JoinColumn(name = "user_id")
private User user;
@Embeddable
public class PurchasePk implements Serializable {
private ShopPk shopId;
private String userId;
}
}
구매 내역의 경우 shopId에 해당하는 shopPk와, 구매한 유저의 id를 PK로 설정했다.
이렇게 설정하고 보니 한 유저가 동일 날짜의 같은 품목(동일 shop) 개수를 나눠서 구매할 경우를 row로는 표현할 수 없어서, 이미 구매 내역이 있을 경우 count를 update하는 방식으로 service단의 로직을 추가해야했다.
고려사항
사실 이런 복합키를 구성해보려고 시도한 이유는, 최근 SQL공부를 많이 하다보니 DB의 관점에서 테이블을 바라보는 게 더 익숙해졌었기 때문이다.
어떻게보면 시대를 거슬러가는 학습을 하고 있다고 봐도 될 것 같다.
https://techblog.woowahan.com/2595/
Legacy DB의 JPA Entity Mapping (복합키 매핑 편) | 우아한형제들 기술블로그
{{item.name}} 안녕하세요. 우아한형제들에서 배달의민족 서비스의 광고시스템을 개발하고 있습니다. 시스템을 점진적으로 Spring Boot / JPA 기반으로 이관하면서 경험했던 내용을 공유하고자 합니다.
techblog.woowahan.com
해당 글을 보면 5년 전 글에서도 DB 중심 설계는 이미 Legacy라고 불렸다는 걸 알 수 있다. (Legacy가 나쁘다는 뜻은 아니다.)
또 이미 15년 전 부터 테이블에 인조 키를 생성하는 것에 대한 논의가 있어왔다는 것도 찾을 수 있었다.
https://stackoverflow.com/questions/337503/whats-the-best-practice-for-primary-keys-in-tables
What's the best practice for primary keys in tables?
When designing tables, I've developed a habit of having one column that is unique and that I make the primary key. This is achieved in three ways depending on requirements: Identity integer colum...
stackoverflow.com
적용을 결심한 초기에는 당연히 기본 키 Index를 잘 만들어서 잘 타는 게 성능에 훨씬 유리하지 않을까? 싶은 생각이었다.
그런데 적용을 하면서 관련된 자료를 찾다 보니, JPA에서 복합 키를 구성할 땐 Index가 알파벳순으로 정해진다는 것과 같이 성능에 영향을 미칠 수 있는 불편함이 또 존재했다.
당장에 Shop의 경우 날짜별로 상점 목록이 조회되기 때문에 날짜를 복합index 맨 앞에 두고싶었는데, 알파벳 순서에 밀려 인덱스 순서가 바뀌는 것이다.
그래서 그냥 인조식별자를 생성해서 쓰고, 조회시에는 날짜 column에 index를 추가해서 날짜로 조회하는 게 더 낫지 않을까 싶은 생각도 들었다. 하루에 갱신되는 아이템 수가 많지 않은 상황이라면, 날짜만 index를 타도 충분한 성능이 나올 것 같았다. 애초에 데이터 수가 폭발적으로 증가하는 테이블이 아니기 때문에 굳이 고려사항이 아닐지도 모른다.
적용하기를 결심하고 자료를 이것저것 찾아보면서 느낀 점은,
프로그램적으로 더 좋은 코드도 물론 중요하지만 그냥 요구사항 분석과 설계부터가 제일 중요하다는 것이다.
현재는 SQL을 공부하고 있더라도 너무 DB 관점에만 매몰되지 말고, JPA를 사용할 때에 더 좋은 설계는 무엇일지에 대해서도 많이 고민해보아야겠다.
'프로젝트 > HI-SERVER' 카테고리의 다른 글
JPA - findById (0) | 2024.03.27 |
---|---|
아이디 중복 검사 (0) | 2023.12.16 |
개인 프로젝트) HI-SERVER 시작 (0) | 2023.12.15 |