🙈 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 패턴을 활용하여 기존의 클래스를 변경하지 않고 새로운 기능을 확장할 수 있다는 점에서 객체지향 설계의 유연성과 강력함을 실감할 수 있었습니다.
디자인 패턴을 활용한 코드 확장성 및 재사용성의 중요성을 다시 한 번 상기하게 되었고, 앞으로도 이러한 패턴을 적극적으로 활용하여 안정적이고 확장 가능한 시스템을 설계해야겠다고 다짐했습니다.
긴 글 읽어주셔서 감사합니다.
'프로젝트 일기 > 한편의 수학 학원' 카테고리의 다른 글
이미지 업로드 API에서의 트랜잭션 병목과 비동기 처리 전략 (0) | 2025.04.13 |
---|---|
[Refactor] Bean Validation Duplicated (0) | 2025.04.09 |
벌크연산을 통한 쿼리 최적화 (0) | 2025.04.04 |
로그인 시도 횟수 제한 기능 구현하기 (0) | 2025.04.03 |