BindProject

[Backend] Image모듈 개발기 3편. 이미지 서비스 흐름과 테스트 전략

dding-shark 2025. 6. 21. 15:15
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