Project/배달 주문 관리 플랫폼

[Spring Boot] 배달 플랫폼 배송지 도메인 설계: Entity와 Repository (Soft Delete 적용기)

Kr1 2026. 5. 4. 00:29

  • 배달 플랫폼에서 배송지(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() 처럼 의미 있는 비즈니스 메서드를 엔티티 내부에 구현하여 도메인 모델의 응집도를 높였습니다.