요구사항
FileRepository 인터페이스는 파일 스토리지로부터 파일키를 입력받아 파일 InputStream을 반환한다.
/**
* Get the file contents from a file storage (e.g. LocalFile System, S3, GCS, Azure Blob Storage, etc.)
*/
public interface FileRepository {
InputStream getFile(FileKey fileKey) throws IOException;
}
- LocalFile 은 LocalFileRepository,
- AWS S3 File 은 S3FileRepository 클래스로 파일을 가져와 InputStream 을 반환한다.
기존 기능에 다음과 같은 스팩이 추가되어야 한다.
".zip, .gz, .zst 같은 압축 파일은 압축 해제를 지원해야한다"
어떻게 구현할 것인가?
LocalFileRepository
public class LocalFileRepository implements FileRepository {
@Override
public InputStream getFile(FileKey fileKey) throws IOException {
Path path = Paths.get(URI.create(fileKey.get()));
return Files.newInputStream(path, READ);
}
}
S3FileRepository
@RequiredArgsConstructor
public class S3FileRepository implements FileRepository {
private final S3Client s3Client;
@Override
public InputStream getFile(FileKey fileKey) {
S3Location s3Location = S3Location.from(fileKey);
GetObjectRequest request = GetObjectRequest.builder()
.bucket(s3Location.bucket())
.key(s3Location.key())
.build();
return s3Client.getObject(request);
}
}
Solution 1 각 클래스에 압축해제 메소드 추가
FileRepository 에 압축해제 라는 책임을 추가한다.
public interface FileRepository {
InputStream getFile(FileKey fileKey) throws IOException;
InpusStream decompress(FileKey key, InputStream in) throws IOException;
}
벌써 구린 냄새가난다.
FileRepository 는 원래 '파일을 가져오기'만 했는데 책임이 커지고있다.
압축해제 뿐만 아니라 Encoding / Decoding 도 추가된다면? 인터페이스가 점점 비대해질 것이다.
코드 중복도 심해진다 예를들면
decompress 메소드를 LocalFileRepository, S3FileRepository 양쪽 모두 중복 구현해야한다.
일단 가져온 파일에 대한 파일 압축 해제 로직은 같기 때문에, DRY(Don't Repeat Yourself) 원칙을 어기게된다.
@Override
public InputStream decompress(FileKey key, InputStream in) throws IOException {
String extension = FilenameUtils.getExtension(key.get())
switch (extension) {
case "zip":
ZipInputStream zipInputStream = new ZipInputStream(in);
ZipEntry zipEntry = zipInputStream.getNextEntry();
if (zipEntry == null) {
throw new IOException("Empty zip file");
}
return zipInputStream;
}
case "zst":
return new ZstdInputStream(in);
case "gz":
return new GZIPInputStream(in);
default:
return in;
}
기존 코드를 그대로 두고, 행위만 추가한다.
앞선 코드의 문제는 ISP, DRY 원칙을 어긴 것이었다.
기존 동작을 그대로 두고 '압축 해제' 라는 추가 행위만 부여할 수 없을까?
이럴 때 사용하는 것이 데코레이터 패턴이다.
Decorator 사전 정의를 보면
a person whose job is to paint the inside or outside of buildings and to do other related work
뭔가를 꾸며주는 사람이다.
디자인 패턴에서의 데코레이터는 "어떤 추가적인 행위를 더해주는 역할" 이다.
다음과같이 구현할 수 있다.
(2) Solution II. Decroator Pattern
DecompressingFileRepository
(1) 원본 파일을 가져온다. (기존 동작)
(2) 압축 해제 객체에 의해 압축을 해제하여 반환한다 (추가된 동작)
@RequiredArgsConstructor
public class DecompressingFileRepository implements FileRepository {
private final FileRepository delegate;
@Override
public InputStream getFile(FileKey fileKey) throws IOException {
InputStream inputStream = delegate.getFile(fileKey); // (1) 원본 파일을 기존 행위 그대로 가져온다.
Decompressor decompressor = DecompressorSelector.select(fileKey);
return decompressor.decompress(inputStream); // (2) 압축을 해제하여 반환한다.
}
}
@RequiredArgsConstructor
public enum DecompressorSelector {
GZIP(new GzipDecompressor()),
ZIP(new ZipDecompressor()),
ZSTD(new ZstdDecompressor()),
NONE(new NoneDecompressor());
private final Decompressor decompressor;
public static Decompressor select(FileKey fileKey) {
for (DecompressorSelector selector : values()) {
if (selector.decompressor.supports(fileKey)) {
return selector.decompressor;
}
}
throw new IllegalArgumentException("No supported decompressor found for file: " + fileKey.get());
}
}
Class Diagram

'JVM > Java' 카테고리의 다른 글
| [Java] JUnit Inner class 를 정의하는 팁 (0) | 2025.12.22 |
|---|---|
| [Java] Primitive 타입보다 VO 를 의존하라 (0) | 2025.11.20 |
| [Java] Clean Code - index, array 대신 Iterator (0) | 2025.11.19 |
| [Java] Closeable vs AutoCloseable (0) | 2025.10.17 |
| [Java] 제네릭과 동적 타입 캐스팅 (0) | 2025.10.16 |