- 배달 플랫폼에서 배송지(Address)는 유저가 음식을 받을 매우 중요한 정보입니다. 제가 담당했던 배송지 도메인의 가장 밑바탕이 되는 데이터베이스 계층(Entity와 Repository)을 어떻게 설계하고 구현했는지 회고해보려 합니다.
💻 실제 코드 (AddressEntity.java & AddressRepository.java)
AddressEntity.java
@Entity
@Table(name = "p_address")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AddressEntity extends BaseAuditEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "address_id", nullable = false, updatable = false)
private UUID id;
@Column(name = "user_id", nullable = false)
private UUID userId;
@Column(name = "alias", length = 50)
private String alias;
@Column(name = "address", nullable = false, length = 255)
private String address;
@Column(name = "detail", length = 255)
private String detail;
@Column(name = "zip_code", length = 255)
private String zipCode;
@Column(name = "is_default", nullable = false)
private Boolean isDefault = false;
@Builder
public AddressEntity(UUID userId, String alias, String address, String detail, String zipCode, Boolean isDefault) {
this.userId = userId;
// ... (생략)
this.isDefault = isDefault != null ? isDefault : false;
}
// 도메인 주도 설계(DDD) 관점의 비즈니스 메서드
public void updateAddress(String alias, String address, String detail, String zipCode, Boolean isDefault) {
this.alias = alias;
this.address = address;
// ... (생략)
if (isDefault != null) {
this.isDefault = isDefault;
}
}
public void setDefault(Boolean isDefault) {
this.isDefault = isDefault;
}
}
AddressRepository.java
public interface AddressRepository extends JpaRepository<AddressEntity, UUID> {
Page<AddressEntity> findAllByUserIdAndAliasContainingAndIsDeletedFalse(@Param("userId") UUID userId, @Param("alias") String alias, Pageable pageable);
Page<AddressEntity> findAllByUserIdAndIsDeletedFalse(@Param("userId") UUID userId, Pageable pageable);
Optional<AddressEntity> findByIdAndIsDeletedFalse(@Param("id") UUID id);
Optional<AddressEntity> findByUserIdAndIsDefaultTrueAndIsDeletedFalse(@Param("userId") UUID userId);
}
🤔 왜 이렇게 짰는가? (설계 의도와 이유)
왜 @Setter를 안 쓰고 updateAddress 메서드를 따로 만들었을까?
- 엔티티 클래스 상단을 보면 @Getter만 있고 @Setter는 없습니다. 만약 @Setter를 열어두면 서비스 로직 어디서든 address.setAlias("집") 처럼 데이터를 마음대로 바꿀 수 있어, 데이터가 어디서 변경되었는지 추적하기가 매우 힘들어진다고 생각했습니다.
- 그래서 수정이 필요한 필드들만 모아서 한 번에 업데이트하는 updateAddress()라는 비즈니스 메서드를 만들어, 객체 지향적인 원칙(객체가 스스로 상태를 관리함)을 지키려 노력했습니다.
왜 모든 쿼리 메서드 끝에 AndIsDeletedFalse가 붙어있을까?
- 우리 프로젝트의 핵심 설계 중 하나는 'Soft Delete(논리적 삭제)'입니다.
- AddressEntity는 BaseAuditEntity를 상속받고 있는데, 여기에는 isDeleted라는 필드가 있습니다. 만약 유저가 배송지를 삭제했을 때 DB에서 하드 딜리트(Hard Delete)를 해버리면, 과거 주문 내역(Order)에서 이 배송지를 참조하고 있을 때 데이터 정합성이 깨지게 됩니다. (과거 주문의 배송지 정보가 NULL이 됨)
- 그래서 실제 삭제 대신 isDeleted = true로 상태만 바꾸고, Repository에서 데이터를 조회할 때는 항상 AndIsDeletedFalse를 붙여서 "삭제되지 않은 활성 배송지"만 가져오도록 강제했습니다.
💡정리
- 데이터 무결성을 고려한 설계: 주문(Order) 도메인과의 연관관계를 고려하여 배송지 삭제 시 물리적 삭제 대신 Soft Delete를 채택했고, Repository 조회 쿼리에서 이를 철저히 필터링하여 데이터 무결성을 확보했습니다.
- 객체 지향적 엔티티 설계 (DDD): 무분별한 Setter 사용을 지양하고, updateAddress(), setDefault() 처럼 의미 있는 비즈니스 메서드를 엔티티 내부에 구현하여 도메인 모델의 응집도를 높였습니다.