Tech/Clean Code

정책 추가에 따라 분기문 생길 때, 전략 패턴 적용하기

행복한 시지프 2026. 5. 15. 22:30

들어가며

백엔드 개발을 하다 보면 정책이 까다로운 코드가 생긴다. 처음에는 if, switch로 분기해도 충분해 보인다.

 

문제는 목적이 늘어날 때 생긴다. 이번 사이드 프로젝트에서 S3 presigned-url API를 구현하면서 업로드 목적별 정책이 달라졌다. 프로필 이미지는 5MB까지만 허용하고, 활동 기록 이미지는 10MB까지 허용한다. OCR 오류 리포트 이미지는 저장 경로도 별도로 가져가야 했다.

처음 구현은 목적별 정책이 여러 파일에 흩어져 있었다. 새로운 업로드 목적을 추가하면서 한 곳을 고치고 다른 한 곳을 빼먹었다. 이때 전략 패턴을 적용해 정책을 업로드 목적 단위로 모았다.

전략 패턴이 무엇인가?

전략 패턴은 바뀔 수 있는 규칙이나 알고리즘을 인터페이스로 분리하고, 실행 시점에 알맞은 구현체를 골라 쓰는 디자인 패턴이다.

핵심은 "분기문을 없앤다"가 아니다. 핵심은 "변경되는 이유가 같은 코드를 한 곳에 모은다"이다.

 

전략 패턴을 적용하기 전에는 호출하는 쪽이 여러 조건을 알고 있었다.

if (purpose == PROFILE_IMAGE) {
    // 프로필 이미지 정책
}

if (purpose == ACTIVITY_RECORD_IMAGE) {
    // 활동 기록 이미지 정책
}

전략 패턴을 적용하면 호출하는 쪽은 공통 인터페이스만 알고, 구체적인 정책은 각 전략 객체가 맡는다.

UploadPurposePolicy policy = uploadPurposePolicy(request.uploadPurpose());
policy.validateFileSize(request.fileSize());
String objectKey = policy.createObjectKey(userId, request.fileName());

비즈니스 로직

S3 presigned-url API는 업로드 목적별로 다른 정책을 가진다.

목적 허용 content type 최대 파일 크기 object key prefix

PROFILE_IMAGE 공통 이미지 타입 5MB profile-images/{userId}/
ACTIVITY_RECORD_IMAGE 공통 이미지 타입 10MB activity-records/{userId}/{yyyy}/{MM}/
SCREEN_TIME_OCR_REPORT_IMAGE 공통 이미지 타입 10MB screen-time-ocr-reports/{userId}/{yyyy}/{MM}/

여기서 모든 정책이 목적별로 다른 것은 아니다.

  • content type 검증은 공통이다.
  • 파일 크기 제한은 목적별로 다르다.
  • object key 경로도 목적별로 다르다.

따라서 모든 코드를 전략으로 밀어 넣으면 안 된다. 공통 정책은 공통 정책으로 남기고, 목적에 따라 달라지는 정책만 전략으로 묶어야 한다.

기존 구현의 문제

원래는 업로드 목적이 두 개였다. 이때는 목적별 분기가 크게 문제처럼 보이지 않았다. 하지만 새 목적을 추가하자 변경 지점이 흩어져 있다는 문제가 드러났다.

 

새로운 업로드 목적을 추가하려면 최소한 다음 파일들을 같이 봐야 했다.

  • UploadFileSizePolicy
  • UploadObjectKeyFactory

실제로 UploadFileSizePolicy에 분기문을 추가하는 것을 잊었다.

이건 단순 실수가 아니다. 코드 구조가 실수를 유도한 것이다. 업로드 목적 하나를 추가하는 변경인데, 파일 크기 정책과 object key 생성 정책이 서로 다른 모듈에 흩어져 있었다. 변경 이유는 하나인데 수정해야 할 모듈은 여러 개였다.

 

응집도가 낮다는 것은 이런 상태다. 같이 바뀌어야 하는 코드가 같이 있지 않다. 그래서 유지보수할 때 여러 파일을 돌아다녀야 하고, 그중 하나를 놓치기 쉽다.

개선 방향

이 문제의 축은 "업로드 목적"이다.

업로드 목적이 바뀌면 함께 바뀌는 규칙이 있다.

  • 이 목적은 어떤 UploadPurpose에 대응하는가?
  • 최대 파일 크기는 얼마인가?
  • 사용자별 object key prefix는 무엇인가?
  • 실제 object key directory는 어떻게 구성하는가?

그래서 이 규칙들을 UploadPurposePolicy라는 하나의 인터페이스로 모았다.

public interface UploadPurposePolicy {

    UploadPurpose purpose();

    long maxFileSize();

    String objectKeyPrefixForUser(Long userId);

    String objectKeyDirectory(Long userId);

    default void validateFileSize(Long fileSize) {
        // 공통 파일 크기 검증 흐름
    }

    default String createObjectKey(Long userId, String fileName) {
        // 공통 object key 생성 흐름
    }
}

각 업로드 목적은 이 인터페이스를 구현한다.

  • ProfileImageUploadPurposePolicy
  • ActivityRecordImageUploadPurposePolicy
  • ScreenTimeOcrReportImageUploadPurposePolicy

이제 파일 크기와 object key 경로는 목적별 정책 객체 안에 같이 있다.

개선된 구조

 

S3UploadService는 더 이상 목적별 세부 규칙을 직접 알 필요가 없다. 요청의 업로드 목적에 맞는 정책을 찾고, 그 정책에게 검증과 object key 생성을 맡긴다.

String contentType = uploadContentTypePolicy.validate(request.contentType());
UploadPurposePolicy uploadPurposePolicy = uploadPurposePolicy(request.uploadPurpose());
uploadPurposePolicy.validateFileSize(request.fileSize());

String objectKey = uploadPurposePolicy.createObjectKey(userId, request.fileName());

여기서 UploadContentTypePolicy는 그대로 공통 정책으로 남긴다. 업로드 목적과 무관하게 이미지 타입을 검증하는 규칙이기 때문이다.

 

반면 maxFileSize, objectKeyPrefixForUser, objectKeyDirectory는 목적별로 달라진다. 그래서 ProfileImageUploadPurposePolicy, ActivityRecordImageUploadPurposePolicy, ScreenTimeOcrReportImageUploadPurposePolicy 안으로 들어간다.

 

결과적으로 새 업로드 목적이 추가될 때 기존 정책 클래스들을 수정하지 않아도 된다. 새 목적에 맞는 UploadPurposePolicy 구현체를 추가하면 된다. 전략 패턴이 줄여주는 것은 기존 정책 분기 수정이다.

무엇이 좋아졌는가?

1. 변경 지점이 줄었다. 새 업로드 목적을 추가할 때 기존 UploadFileSizePolicy, UploadObjectKeyFactory의 분기문을 다시 열지 않는다. 목적별 정책 클래스 하나를 추가한다.

 

2. 응집도가 높아졌다. SCREEN_TIME_OCR_REPORT_IMAGE의 파일 크기 제한과 저장 경로가 같은 클래스에 있다. 이 목적을 이해하려면 여러 정책 파일을 찾아다니지 않아도 된다.

 

3. 테스트가 목적 단위로 쉬워졌다. ScreenTimeOcrReportImageUploadPurposePolicy를 테스트하면 OCR 리포트 업로드 목적의 파일 크기와 경로 규칙을 함께 확인할 수 있다.

 

4. S3UploadService의 역할이 단순해졌다. 이 서비스는 presigned-url 발급 흐름을 조립한다. 목적별 정책 판단은 정책 객체가 담당한다.

전략 패턴을 쓸 때의 기준

전략 패턴은 다음 조건이 있을 때 잘 맞는다.

  • 규칙의 종류가 앞으로 늘어날 가능성이 있다.
  • 새 규칙을 추가할 때 기존 분기문을 여러 곳에서 수정해야 한다.
  • 호출하는 쪽이 세부 규칙을 몰라도 된다.

마치며

전략 패턴은 흩어진 변경 지점을 모으는 일이다. 이번 리팩터링의 핵심은 업로드 목적별로 달라지는 정책을 UploadPurposePolicy로 묶은 것이다. 공통 정책은 공통 정책으로 두고, 목적별로 바뀌는 파일 크기와 object key 생성 규칙만 전략으로 분리했다. 그 결과 새 목적을 추가할 때 기존 분기문을 수정하는 대신 새 전략을 추가하는 구조가 됐다. 유지보수하기 훨씬 좋아졌다.