데코레이터 패턴을 통한 비동기 처리의 안정적 도입

2025. 4. 14. 22:58·프로젝트 일기/한편의 수학 학원

🙈 1. 문제 파악

현재 진행 중인 프로젝트에서는 이미지 업로드 API의 응답 시간을 단축하고, 처리량(Throughput)을 향상시키기 위해 비동기 처리를 도입하고자 했습니다.

스프링에서는 AOP(Aspect-Oriented Programming)로 @Async 어노테이션을 통해 비동기 처리를 지원하고 있습니다. 그러나 기존의 MediaStorage 구현체에 @Async를 직접 적용하는 상황에서, 몇 가지 문제가 발생했습니다.

문제점

1. UploadFile 구현에 따른 의존성 문제

@Async를 MediaStorage의 구현체에 직접 추가하면, 기존 MediaStorage를 의존하는 모든 서비스들에 대해 비동기 처리가 적용됩니다. 하지만 UploadFile 자체가 비동기를 지원하지 않으면 (예: MultipartFile 스트림 처리), 비동기 작업이 제대로 처리되지 않아 데이터가 제대로 저장되지 않는 문제가 발생할 수 있습니다.

2. 동기적 작업과 비동기적 작업의 분리 어려움

@Async를 전체 MediaStorage 구현체에 적용할 경우, 비동기 처리가 필요 없는 동기적인 작업까지 비동기화되어버리는 문제가 생깁니다. 이로 인해 동기적, 비동기적 작업을 명확하게 구분하기 어렵게 됩니다.

public interface MediaStorage {
    void store(UploadFile uploadFile);
    void remove(String fileName);
    MediaDto loadFile(String fileName);
    Set<String> loadAllFileNames();
}

💡 2. 해결 과정

해결 전략

이 문제를 해결하기 위해, 두 가지 주요 전략을 도입했습니다.

2-1. 기존의 UploadFile을 그대로 두고, 이를 상속한 AsyncUploadFile을 정의
UploadFile을 그대로 유지하고, 비동기 처리가 필요한 경우 이를 상속한 AsyncUploadFile을 정의하여 비동기 처리를 안전하게 지원하도록 합니다.

public interface AsyncUploadFile extends UploadFile {
}

2-2. StorageAsyncDecorator를 사용하여 비동기 처리를 분리
@Async를 StorageAsyncDecorator에 적용하여, 기존의 MediaStorage 구현체에 영향을 주지 않고 비동기 처리를 분리하여 적용합니다.

/**
 * {@link MediaStorage}에 {@link Async} 기능을 추가하기 위해 도입된 {@code Decorator} 클래스입니다.
 */
@Component
@RequiredArgsConstructor
public class StorageAsyncDecorator {
    private final MediaStorage mediaStorage;

    /**
     * {@link AsyncUploadFile} 구현체를 매개변수로 받아 비동기적으로 파일을 저장합니다.
     * @param asyncUploadFile 비동기 업로드 파일
     */
    @Async
    public void store(final AsyncUploadFile asyncUploadFile) {
        mediaStorage.store(asyncUploadFile);
    }
}

위 전략을 토대로 아래와 같이 실제 구현체를 만들었습니다.

AsyncUploadFile을 구현하는 클래스를 작성합니다. AsyncImageUploadFile을 구현하여 비동기적으로 이미지를 업로드할 수 있도록 했습니다.

public class ImageUploadFile implements UploadFile {
    private final MultipartFile multipartFile;
    private final String newFileName;
    private final String extension;

    public ImageUploadFile(MultipartFile multipartFile) {
        this.multipartFile = multipartFile;
        this.newFileName = getNewFileName(multipartFile.getOriginalFilename());
        this.extension = parseExtension(multipartFile.getOriginalFilename());
    }

    private String getNewFileName(String fileName) {
        return UUID.randomUUID() + parseExtension(fileName);
    }

    private String parseExtension(String fileName) {
        final int extensionIdx = fileName.lastIndexOf(".");
        if (extensionIdx == -1) {
            throw new InvalidUploadFileException(ErrorCode.INVALID_UPLOAD_FILE);
        }
        return fileName.substring(extensionIdx);
    }

    @Override
    public String getUniqueFileName() {
        return this.newFileName;
    }

    @Override
    public InputStream getInputStream() {
        try {
            return multipartFile.getInputStream();
        } catch (IOException e) {
            throw new StorageException(ErrorCode.NO_SUCH_MEDIA);
        }
    }

    @Override
    public String getExtension() {
        return this.extension;
    }
}

이제 AsyncUploadFile을 구현하는 AsyncImageUploadFile을 작성합니다.

public class AsyncImageUploadFile extends ImageUploadFile implements AsyncUploadFile {
    private final byte[] data;

    public AsyncImageUploadFile(final MultipartFile multipartFile) {
        super(multipartFile);
        try {
            data = multipartFile.getBytes();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public InputStream getInputStream() {
        return new ByteArrayInputStream(data);
    }
}

@Async 어노테이션을 사용하여 store 메서드를 비동기적으로 처리하도록 했습니다. 이를 통해 기존의 MediaStorage를 참조하는 클래스들의 동작을 보장하면서도 비동기 처리를 적용할 수 있었습니다.

⭐️ 3. 깨달은 점

이번 문제 해결 과정에서 디자인 패턴의 중요성을 다시 한 번 깨달았습니다. 특히 Decorator 패턴을 활용하여 기존의 클래스를 변경하지 않고 새로운 기능을 확장할 수 있다는 점에서 객체지향 설계의 유연성과 강력함을 실감할 수 있었습니다.

디자인 패턴을 활용한 코드 확장성 및 재사용성의 중요성을 다시 한 번 상기하게 되었고, 앞으로도 이러한 패턴을 적극적으로 활용하여 안정적이고 확장 가능한 시스템을 설계해야겠다고 다짐했습니다.

긴 글 읽어주셔서 감사합니다.

'프로젝트 일기 > 한편의 수학 학원' 카테고리의 다른 글

질문 도메인 최적화 과정  (1) 2025.06.05
API 테스트 하는데... 인증까지 매번..?  (0) 2025.04.22
이미지 업로드 API에서의 트랜잭션 병목과 비동기 처리 전략  (0) 2025.04.13
[Refactor] Bean Validation Duplicated  (0) 2025.04.09
벌크연산을 통한 쿼리 최적화  (0) 2025.04.04
'프로젝트 일기/한편의 수학 학원' 카테고리의 다른 글
  • 질문 도메인 최적화 과정
  • API 테스트 하는데... 인증까지 매번..?
  • 이미지 업로드 API에서의 트랜잭션 병목과 비동기 처리 전략
  • [Refactor] Bean Validation Duplicated
윤희종
윤희종
호기심을 잃지 말자 지적, 질문은 언제나 환영합니다 ;)
  • 윤희종
    서버견문록
    윤희종
  • 전체
    오늘
    어제
    • 분류 전체보기 (36)
      • 데일리 플랜 (1)
      • 이것저것 (4)
      • Java (6)
      • Spring (12)
        • SpringBoot (10)
        • Spring MVC (0)
      • Computer Science (4)
        • Network (1)
        • Operating System (0)
        • Data Structure (0)
        • Algorithm (2)
        • Database (0)
      • IOS (0)
      • 프로그래머스 문제풀이 (2)
      • 프로젝트 일기 (7)
        • 한편의 수학 학원 (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    비동기 처리 유의점
    cache write back
    SecurityFilterChain 구성
    스프링 부트 인증 우회
    read through
    스프링 시큐리티 구성
    SecurityFilterChain
    Spring
    제네릭
    인증 우회 테스트
    스프링
    알고리즘
    mysql 쿼리 최적화
    스프링 시큐리티 사용법
    성능 개선
    springboot
    스프링 부트
    캐시
    인증 테스트
    servlet
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
윤희종
데코레이터 패턴을 통한 비동기 처리의 안정적 도입
상단으로

티스토리툴바