Spring 프로젝트에서 파일 업로드 기능을 만들다 보면, 흔히 로컬 파일 시스템에 저장하거나 클라우드 서비스인 AWS S3를 사용하게 된다.
이번 과제에서는 S3에 이미지를 업로드하는 기능을 구현하는 것이 목표였다.
S3는 Amazon Web Services에서 제공하는 객체 저장소(Object Storage)로,
정적 파일(이미지, 영상, HTML 등)을 저장하고, 언제든 접근 가능하게 해준다.
초기에는 이렇게 단순히 생각했다:
“
MultipartFile
하나 받아서amazonS3.putObject()
호출하면 끝 아닌가?”
하지만 실무나 강의 예제를 보면 S3 관련 코드를 단일 클래스에 몰아넣지 않고,
S3Service
, S3Uploader
, S3InfraService
등으로 명확하게 나누는 패턴을 자주 쓴다.
그래서 다음과 같은 의문이 생겼다:
이 의문에서 출발해 구현하며 겪은 고민과 해결 과정을 정리해보았다.
S3Service는 비즈니스 로직을,
S3InfraService는 외부 시스템 연동을 책임지도록 분리한다.
이렇게 하면 얻는 이점이 명확하다:
구분 | 설명 |
---|---|
관심사 분리 | S3 호출 자체는 인프라 역할, 도메인 로직과 분리 |
유지보수 용이 | 인프라 로직 수정 시 서비스 영향 ↓ |
확장성 확보 | S3 외에 다른 저장소 연동 시 교체 쉬움 |
테스트 용이 | 외부 시스템 없이도 단위 테스트 가능 |
└── s3
├── S3Service.java # 비즈니스 서비스 (컨트롤러 호출 대상)
└── S3InfraService.java # AWS SDK 직접 호출
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();
}
}
항목 | 설명 |
---|---|
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");
}
}