S3 서비스 기능에 관한 고찰

박화랑·2025년 5월 7일
1

Spring_6기

목록 보기
20/32

고민하게 된 계기

Spring 프로젝트에서 파일 업로드 기능을 만들다 보면, 흔히 로컬 파일 시스템에 저장하거나 클라우드 서비스인 AWS S3를 사용하게 된다.
이번 과제에서는 S3에 이미지를 업로드하는 기능을 구현하는 것이 목표였다.

S3는 Amazon Web Services에서 제공하는 객체 저장소(Object Storage)로,
정적 파일(이미지, 영상, HTML 등)을 저장하고, 언제든 접근 가능하게 해준다.

초기에는 이렇게 단순히 생각했다:

MultipartFile 하나 받아서 amazonS3.putObject() 호출하면 끝 아닌가?”

하지만 실무나 강의 예제를 보면 S3 관련 코드를 단일 클래스에 몰아넣지 않고,
S3Service, S3Uploader, S3InfraService 등으로 명확하게 나누는 패턴을 자주 쓴다.

그래서 다음과 같은 의문이 생겼다:

  • "단순 업로드 기능인데 왜 구조를 나눌까?"
  • "실제로 유지보수나 테스트에서 차이가 있나?"
  • "그냥 하나의 서비스로 만들면 안 되나?"

이 의문에서 출발해 구현하며 겪은 고민과 해결 과정을 정리해보았다.


2. 왜 구조를 나눠야 할까?

핵심 포인트

S3Service비즈니스 로직을,
S3InfraService외부 시스템 연동을 책임지도록 분리한다.

이렇게 하면 얻는 이점이 명확하다:

구분설명
관심사 분리S3 호출 자체는 인프라 역할, 도메인 로직과 분리
유지보수 용이인프라 로직 수정 시 서비스 영향 ↓
확장성 확보S3 외에 다른 저장소 연동 시 교체 쉬움
테스트 용이외부 시스템 없이도 단위 테스트 가능

3. 패키지 및 클래스 구조

└── s3
    ├── S3Service.java       # 비즈니스 서비스 (컨트롤러 호출 대상)
    └── S3InfraService.java  # AWS SDK 직접 호출

4. 클래스 예시

S3Service – 비즈니스 로직

@RequiredArgsConstructor
@Service
public class S3Service {
    private final S3InfraService s3InfraService;

    public String upload(MultipartFile file) {
        return s3InfraService.uploadFile(file, "images/");
    }
}

S3InfraService – S3 통신

@RequiredArgsConstructor
@Component
public class S3InfraService {
    private final AmazonS3 amazonS3;
    private final String bucketName = "your-bucket-name";

    public String uploadFile(MultipartFile file, String dirName) {
        String fileName = dirName + UUID.randomUUID();
        amazonS3.putObject(new PutObjectRequest(bucketName, fileName, file.getInputStream(), null)
                           .withCannedAcl(CannedAccessControlList.PublicRead));
        return amazonS3.getUrl(bucketName, fileName).toString();
    }
}

5. 테스트에서도 분리의 이점

항목설명
S3Service 테스트S3InfraService를 Mock 처리 가능
테스트 속도외부 호출 없이 빠르고 안정적
비용/위험실제 S3 접근 안 하므로 안전 (요금 없음)

예시

@ExtendWith(MockitoExtension.class)
class S3ServiceTest {
    @InjectMocks
    private S3Service s3Service;

    @Mock
    private S3InfraService s3InfraService;

    @Test
    void uploadFile_returnsUrl() {
        given(s3InfraService.uploadFile(any(), anyString()))
            .willReturn("https://s3.amazonaws.com/bucket/file.png");

        String result = s3Service.upload(mockFile);
        assertThat(result).contains("https://s3.amazonaws.com");
    }
}

결론

  • 다시 한 번 개발의 방향성을 생각하게 된 계기였다. 그러나 용도에 따라 이 기능을 분리하여 사용하지 않고 하나로만 사용하는 경우가 생기기도 한다. 지금은 단일책임원칙을 지켜서 만들려고 만든 것이지만, 이게 꼭 필요한가에 대해서는 추후에 개발하는 방향성과 목표에 따라 좀 더 고민해보는게 좋다고 생각한다.
profile
개발자 희망생

0개의 댓글