728x90
3편. 이미지 서비스 흐름과 테스트 전략
도입
이전 글에서는 이미지 엔티티 설계와 상태 기반 흐름을 소개했다.
이번 글에서는 ImageService 중심으로 실제 업로드 → 확정 → 삭제까지의 비즈니스 흐름과 이를 검증하기 위한 단위 테스트 전략을 상세히 다룬다.
설계 목표
- 서비스 로직은 컨트롤러, 컴포넌트, 리포지토리 사이에서 얇게 유지
- 상태 전이, 유저 검증, URL 생성 등은 전담 클래스로 분리
- 테스트 가능한 구조를 기반으로 예외 상황도 명확히 제어
구현 설명
업로드 흐름
public ImageUploadResponse upload(MultipartFile file, ...) {
// 1. WebP 변환
byte[] webpBytes = ImageUtil.toWebp(file, 0.8f);
// 2. 저장소에 저장 (e.g. Local, S3)
imageStorage.store(webpBytes, storedPath);
// 3. DB 저장 (상태: TEMP)
Image image = Image.builder()
.status(ImageStatus.TEMP)
...
.build();
imageRepository.save(image);
return new ImageUploadResponse(image.getId());
}
→ 업로드만으론 사용되지 않음. 반드시 confirmImages(...) 호출을 통해 확정 처리되어야만 실제 콘텐츠와 연결됨.
이미지 확정
@Transactional
public void confirmImages(ImageConfirmRequest request, String currentUserId) {
List<Image> images = imageRepository.findAllById(request.getImageIds());
for (Image image : images) {
validator.validateUser(image, currentUserId);
validator.validateTempStatus(image);
validator.validateCategory(image, request.getCategory());
image.setReferenceId(request.getReferenceId());
statusChanger.changeStatus(image, ImageStatus.TEMP, ImageStatus.CONFIRMED, IMAGE_NOT_TEMP);
}
imageRepository.saveAll(images);
}
- 검증 책임은
ImageValidator로 분리 - 상태 전이는
ImageStatusChanger에서 수행 - 이미지 객체에 대한 조작 외의 행위는 없음 → 도메인 중심의 흐름 유지
삭제 요청 (Soft Delete)
public void deleteImage(Long imageId) {
Image image = imageRepository.findById(imageId).orElseThrow(...);
statusChanger.changeStatus(image, null, ImageStatus.PENDING_DELETE, IMAGE_ALREADY_PENDING_DELETE);
}
→ 실제 삭제는 안됨. PENDING_DELETE 상태로 바꾸고, 주기적으로 스케줄러가 삭제한다.
정기 스케줄러 삭제
@Scheduled(cron = "0 0 * * * *")
public void deleteExpiredImages() {
List<Image> expired = ...
for (Image image : expired) {
imageStorage.delete(image.getStoredPath());
imageRepository.delete(image);
}
}
→ TEMP 1시간 초과 / PENDING_DELETE 1시간 초과 / REJECTED 등은 이 스케줄러가 완전히 삭제함
테스트 전략
단위 테스트의 방향성
- 유저가 잘못된 ID로 confirm 요청 시 →
UNAUTHORIZED_ACCESS - TEMP가 아닌 이미지를 confirm 요청 시 →
IMAGE_NOT_TEMP - CONFIRMED 이미지를 삭제 요청 시 →
PENDING_DELETE상태로 전이
예시: ImageServiceTest
@DisplayName("이미지 확정 시 검증 후 상태 업데이트 및 refId 설정")
@Test
void confirmImages_shouldValidateAndUpdate() {
// given
Image image = ...
ImageConfirmRequest req = ...
when(repository.findAllById(...)).thenReturn(List.of(image));
// when
imageService.confirmImages(req, "user1");
// then
verify(validator).validateUser(...);
verify(statusChanger).changeStatus(image, ImageStatus.TEMP, ImageStatus.CONFIRMED, ...);
}
→ Mock 기반으로 Validator, StatusChanger의 호출을 검증함
→ 상태 기반 설계라 이런 흐름이 명확하게 Mock 확인으로 드러남
예시: ImageStatusChangerTest
@Test
@DisplayName("상태 전이 조건 불일치 시 예외 발생")
void changeStatus_shouldThrow_whenStatusMismatch() {
Image image = Image.builder().status(CONFIRMED).build();
assertThrows(ImageException.class, () ->
changer.changeStatus(image, TEMP, PENDING_DELETE, ...));
}
→ 도메인 전이 조건을 명시적으로 테스트 가능
회고 및 확장 가능성
- 서비스 흐름을
Validator,StatusChanger,ImageStorage등의 컴포넌트로 쪼갬으로써 각 책임을 명확히 분리 - 테스트는
Service단과Component단위로 구분하여 커버리지 확보 - 추후 Kafka 이벤트를 붙일 경우
StatusChanger에서 도메인 이벤트 발행만 추가하면 끝
다음 글 예고
4편. 이미지 처리 확장과 Kafka 이벤트 흐름
CONFIRMED상태 전이 시 Kafka 발행- 이미지 삭제 시 비동기 이벤트로 처리 결과 알림
- NSFW 필터링 실패 시
REJECTED처리 - 확장 가능한
ImageEvent기반 설계 전략
4편은 카프카 이벤트 구상이 끝나면 올려야 할거같다. 게이트 웨이도 webflex 만 쓰려다가... 클라우드랑 게이트웨이 스프링 프레임워크로 개발하려고 뭐.. 이것저것 변할게 많다ㅣ.. ㅠㅠ
728x90
'BindProject' 카테고리의 다른 글
| [Backend] 유저 활동 로그 수집 모듈 개발기 2편 – 메모리 버퍼 기반 로그 수집기 설계와 구현 (0) | 2025.06.21 |
|---|---|
| [Backend] 유저 활동 로그 수집 모듈 개발기 1편 유저 활동 로그 수집기 구상하기 (0) | 2025.06.21 |
| [Backend] Image모듈 개발기 2편: 이미지도 생명주기가 있다면 – 도메인 모델과 상태 설계 (0) | 2025.06.21 |
| [Backend] Image 모듈 개발기 1편: 왜 우리는 직접 이미지 업로드 모듈을 만들었는가? (0) | 2025.06.21 |
| [초안] 합주실 탐색 기능의 설계: 정렬에서 표기로, 현실적인 MVP를 위한 선택 (3) | 2025.06.20 |