[Java] Decorator Pattern

2025. 12. 29. 14:37·JVM/Java

요구사항

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
'JVM/Java' 카테고리의 다른 글
  • [Java] JUnit Inner class 를 정의하는 팁
  • [Java] Primitive 타입보다 VO 를 의존하라
  • [Java] Clean Code - index, array 대신 Iterator
  • [Java] Closeable vs AutoCloseable
M_Falcon
M_Falcon
  • M_Falcon
    Falcon
    M_Falcon
  • 전체
    오늘
    어제
    • 분류 전체보기 (432)
      • Web (16)
        • Nodejs (14)
        • Javascript (23)
        • FrontEnd (4)
      • DataBase (39)
        • Fundamental (1)
        • Redis (4)
        • PostgreSQL (10)
        • NoSQL (4)
        • MySQL (9)
        • MSSQL (3)
        • Error (4)
      • Algorithm (79)
        • Algorithm (문제풀이) (56)
        • Algorithm (이론) (23)
      • JVM (65)
        • Spring (13)
        • JPA (5)
        • Kotlin (13)
        • Java (24)
        • Error (7)
      • 기타 (70)
        • Kafka (3)
        • Kubernetes (3)
        • Docker (13)
        • git (19)
        • 잡동사니 (27)
      • 재테크 (11)
        • 세무 (4)
        • 투자 (3)
        • 보험 (0)
      • BlockChain (2)
        • BitCoin (0)
      • C (32)
        • C (10)
        • C++ (17)
        • Error (3)
      • Low Level (8)
        • OS (3)
        • 시스템 보안 (5)
      • 네트워크 (3)
      • LINUX (30)
        • Linux (26)
        • Error (4)
      • 저작권과 스마트폰의 이해 (0)
      • 생각 뭉치 (6)
      • 궁금증 (2)
      • Private (4)
        • 이직 경험 (0)
        • 꿈을 찾아서 (1)
      • Android (21)
        • OS (4)
  • 블로그 메뉴

    • 홈
    • WEB
    • 알고리즘
    • DataBase
    • Linux
    • Mobile
    • C
    • 방명록
  • 링크

    • github
  • 공지사항

  • 인기 글

  • 태그

    android
    알고리즘
    Git
    linux
    javascript
    Bitcoin
    Kotlin
    프로그래머스
    docker
    Spring
    JPA
    Programmers
    PostgreSQL
    kafka
    백준
    java
    database
    ubuntu
    C++
    algorithm
  • hELLO· Designed By정상우.v4.10.3
M_Falcon
[Java] Decorator Pattern
상단으로

티스토리툴바