[TIL] 공통 정책(deliveryInfo, refundPolicy)을 설정으로 분리한 이유

김재진·2026년 4월 15일

내일배움캠프

목록 보기
64/70

문제 상황

상품 등록 API를 구현하면서 모든 상품에 공통으로 들어가는 텍스트가 있었다.

  • 배송 안내 (deliveryInfo)
  • 환불 정책 (refundPolicy)

처음에는 서비스 코드나 엔티티 생성 로직에 문자열을 직접 넣는 방식도 고려했다. 하지만 정책 문구는 운영 중 바뀔 가능성이 높다. 하드코딩하면 정책이 바뀔 때마다 코드 수정 + 재배포가 필요해진다.


선택한 방식 — @ConfigurationProperties

공통 정책을 애플리케이션 설정값으로 분리했다.

설정 클래스

/**
 * 상품 공통 정책 설정값을 바인딩한다.
 */
@Getter
@Component
@ConfigurationProperties(prefix = "product.policy")
public class ProductPolicyProperties {

  private String deliveryInfo = "Common delivery policy";
  private String refundPolicy = "Common refund policy";
}

환경 설정

# application-local.yml
product:
  policy:
    delivery-info: "기본 배송 정책: 평일 1-2일 배송"
    refund-policy: "기본 환불 정책: 수령 후 7일 이내 미개봉 상태에서만 가능"

서비스에서 사용

/**
 * 상품 생성 관련 도메인 서비스를 제공한다.
 */
@Service
@RequiredArgsConstructor
public class ProductService {

    private static final int MAX_IMAGE_COUNT = 5;

    private final ProductRepository productRepository;
    private final ProductPolicyProperties policyProperties; // 주입받음

    @Transactional
    public ProductCreateResponse createSellerProduct(
            Long sellerId,
            boolean sellerApproved,
            boolean sellerVerified,
            ProductCreateRequest request
    ) {
        validateSellerState(sellerApproved, sellerVerified);
        validateBusinessRules(request);

        // 핵심 아이디어:
        // ProductService는 정책 문구를 직접 알지 않는다.
        // 정책은 ProductPolicyProperties에서 주입받아 엔티티 생성 시 전달한다.
        Product product = Product.builder()
                .sellerId(sellerId)
                .name(request.getName())
                .price(request.getPrice())
                .discountRate(request.getDiscountRate())
                .stock(request.getStock())
                .category(request.getCategory())
                .description(request.getDescription())
                .specification(request.getSpecification())
                .deliveryInfo(policyProperties.getDeliveryInfo())   // 설정에서 주입
                .refundPolicy(policyProperties.getRefundPolicy())   // 설정에서 주입
                .thumbnailUrl(extractThumbnailUrl(request))
                .build();

        request.getImages().stream()
                .sorted(Comparator.comparingInt(ProductCreateRequest.ImageRequest::getSortOrder))
                .forEach(image -> product.addImage(
                        ProductImage.builder()
                                .product(product)
                                .imageUrl(image.getImageUrl())
                                .sortOrder(image.getSortOrder())
                                .isThumbnail(image.getIsThumbnail())
                                .build()
                ));

        Product saved = productRepository.save(product);
        return ProductCreateResponse.from(saved);
    }
}

장점

변경 비용 감소
정책 문구 수정 시 코드를 건드리지 않고 yml 설정만 수정하면 된다.

책임 분리
비즈니스 로직(ProductService)과 운영 정책(설정)이 분리되어 각자의 역할이 명확해진다.

환경별 유연성
application-local.yml, application-dev.yml, application-prod.yml마다 정책 문구를 다르게 운영할 수 있다.

테스트 용이성
테스트 환경에서 별도 yml을 주입하면 된다.

// 테스트에서 정책값 주입 예시
@TestPropertySource(properties = {
    "product.policy.delivery-info=테스트 배송 안내",
    "product.policy.refund-policy=테스트 환불 정책"
})

단점

설정 누락 시 런타임 오류
프로퍼티가 누락된 채로 실행하면 문제가 발생할 수 있다. @Validated@NotBlank를 함께 적용해 애플리케이션 시작 시점에 검증하는 것이 좋다.

@Getter
@Setter
@Validated  // 빈값이면 애플리케이션 시작 실패 → 런타임 오류보다 안전
@Component
@ConfigurationProperties(prefix = "product.policy")
public class ProductPolicyProperties {

    @NotBlank
    private String deliveryInfo;

    @NotBlank
    private String refundPolicy;
}

"코드 수정 없이 설정만 바꾸면 된다"는 말의 한계
yml을 수정해도 결국 재배포는 필요하다. 하드코딩 대비 코드 수정이 없다는 장점은 있지만, 무중단으로 실시간 변경이 되는 건 아니다. 진정한 실시간 변경이 필요하다면 Spring Cloud Config나 DB 정책 테이블 방식을 써야 한다.

문구 변경 이력 관리 한계
yml은 Git으로 이력 추적은 가능하지만 별도 승인 워크플로우나 변경 감사(Audit) 기능은 없다.


다른 방법과 비교

A. 하드코딩

Product product = Product.builder()
        .deliveryInfo("기본 배송 정책: 평일 1-2일 배송")
        .refundPolicy("기본 환불 정책: 수령 후 7일 이내 미개봉 상태에서만 가능")
        .build();
내용
장점구현이 가장 빠름
단점정책 변경마다 코드 수정 + 재배포 필요
판단초기 프로토타입에만 적합, 유지보수성 낮음

B. 설정 파일 (@ConfigurationProperties) ← 현재 선택

내용
장점코드와 정책 분리, 환경별 다른 정책 적용 가능, 구현 단순
단점변경 시 재배포 필요, 비개발자 운영 편의 제한
판단현재 개발 속도와 구조 안정성을 모두 고려했을 때 가장 적합한 균형점

C. DB 정책 테이블

CREATE TABLE policy (
    id         BIGINT PRIMARY KEY,
    type       VARCHAR(50),  -- 'DELIVERY_INFO', 'REFUND_POLICY'
    content    TEXT,
    updated_at DATETIME
);
내용
장점실시간 변경 가능, 이력 관리, 관리자 UI 확장 가능
단점캐시 전략, 동기화, 관리 화면까지 설계해야 해서 복잡도 증가
판단정책 변경 빈도가 높아지거나 비개발자가 직접 운영해야 할 때 도입

왜 지금 이 방법이 맞았는가

현재 단계에서는 기능 개발 속도와 구조 안정성이 더 중요했다. 설정 기반 분리는 하드코딩의 단점을 빠르게 해소하면서도, 나중에 DB 정책 테이블로 확장할 수 있는 중간 지점이다.

"지금 필요한 유연성은 확보하고, 과도한 설계는 피하는 선택"

이는 소프트웨어 설계 원칙 중 OCP(Open-Closed Principle) 와도 맞닿아 있다. 정책이라는 변경 가능성이 높은 부분을 코드 외부로 분리함으로써, 정책이 바뀌어도 ProductService 코드는 닫혀 있고(수정 불필요) 설정만 열려 있는(변경 가능) 구조를 만들었다.


다음 개선 아이디어

  • 정책 변경 이력이 필요해지면 DB 테이블로 승격
  • 관리자 화면에서 정책 편집이 필요해지면 API + 캐시(@Cacheable) 구조 도입

참고

profile
개발공부 처음해보는 사람

0개의 댓글