[Design Pattern] 상속 대신 Decorator 패턴 적용하기

입출력 기능 확장에 상속 대신 Decorator 패턴을 적용하기

0. Decorator 패턴이란?

: 객체 지향 프로그래밍에서 사용되는 디자인 패턴 중 하나

(1) 목적: 객체에 동적으로 새로운 책임(기능)을 추가할 수 있게 한다.

(2) 구조:

  • Component: 기본 기능을 정의하는 인터페이스 또는 추상 클래스
  • ConcreteComponent: 기본 기능을 구현한 클래스
  • Decorator: Component를 참조하는 추상 클래스
  • ConcreteDecorator: Decorator를 상속받아 추가 기능을 구현한 클래스

(3) 작동 방식:

  • 데코레이터 객체가 원본 객체를 감싸고 있다.
  • 데코레이터는 원본 객체와 동일한 인터페이스를 구현한다.
  • 클라이언트의 요청을 데코레이터가 중간에서 처리하고, 필요시 원본 객체에 전달한다.

(4) 장점:

  • 기존 코드를 수정하지 않고 기능을 확장할 수 있다(개방-폐쇄 원칙).
  • 런타임에 동적으로 기능을 추가하거나 제거할 수 있다.
  • 여러 데코레이터를 조합하여 복잡한 기능을 구현할 수 있다.

(5) 단점:

  • 작은 객체들이 많이 생성되어 코드가 복잡해질 수 있다.
  • 데코레이터를 너무 많이 사용하면 디버깅이 어려워질 수 있다.

1. 상속 vs. Decorator 패턴(GoF)

  • 상속: 기능 확장 용이
  • decorator: 기능 확장 및 제거 용이
    • BufferedDataInputStream 분해: BufferedInputStream, DataInputStream, FileInputStream
    • BufferedDataOutputStream 분해: BufferedOutputStream, DataOutputStream, FileOutputStream

=> java에서 제공하는 기능: Java I/O Stream API를 사용해 실습을 해보려고 한다.


2. 상속을 이용한 기능 확장 방식의 문제점

  • FileInputStream ↪️ DataInputStream - byte 데이터: byte, short, int, long, float, double, boolean, String ↪ BufferedDataInputStream - 버퍼 기능 추가 ↪️ BufferedFileInputStream - 버퍼 기능 추가

    => 서브 클래스 간의 기능이 중복됨!

    (1) 상속을 이용해 다양한 기능을 추가시키다 보면 다양한 조합의 서브 클래스들이 대량으로 생성되는 문제 발생 (2) 이 과정 속에서 서브 클래스끼리 기능이 중복되는 문제 발생


3. 위임과 포함을 이용한 기능 확장

설명: [FileInputStream (Java SE 17 & JDK 17)] (https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/FileInputStream.html)

스크린샷 2024-07-31 오후 5 08 05


4. 예시

(1) 파일 읽기 기본 기능 —-사용—-> FileInputStream

FileInputStream in = new FileInputStream("-");
in.read();

(2) 파일 읽기 + Buffer FileInputStream –사용–> ↪️ BufferedInputStream

FileInputStream in = new FileInputStream("-");
BufferedInputStream in2 = new BufferdInputStream(in);
in2.read();

(3) 파일 읽기 + 버퍼 + 데이터 가공

FileInputStream —4.읽기—> 파일 3. read( ) ↪️ BufferedInputStream 2. read( ) ↪️ DataInputStream <—1.사용—


5. 실습

void saveAssignment() {
    try (FileOutputStream out0 = new FileOutputStream("assignment.data");
        DataOutputStream out = new DataOutputStream(out0)) {
        ...
      }
}

위 코드를 아래처럼 더 간결하게 줄일 수 있다.

void saveAssignment() {
    try (DataOutputStream out0 = new DataOutputStream( new FileOutputStream("assignment.data"))) {
        ...
    }
}

** 이 코드는 아래의 복잡하고 긴 코드를 간결하게 줄인 것이다.

try (FileInputStream in0 = new FileInputStream("assignment.data");
      BufferedInputStream in1 = new BufferedInputStream(in0);
      DataInputStream in = new DataInputStream(in1)) {
      ...
      }

BufferedInputStream도 중간에 넣어 연결시킨다.

 void loadAssignment() {
    try (DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("assignment.data")))) {
      ...
    }
 }
try(FileOutputStream out0 = new FileOutputStream("assignment.data");

6. save/load Assignment 전체 코드

// DataInputStream을 사용하여 "assignment.data" 파일에서 데이터를 읽어온다.
 void loadAssignment() {
    try (DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("assignment.data")))) {

      long start = System.currentTimeMillis();
      int size = in.readInt();

      for (int i = 0; i < size; i++) {
        Assignment assignment = new Assignment();
        assignment.setTitle(in.readUTF());
        assignment.setContent(in.readUTF());
        assignment.setDeadline(Date.valueOf(in.readUTF()));
        assignmentRepository.add(assignment);
      }
     long end = System.currentTimeMillis();
      System.out.printf("걸린 시간: %d\n", end - start);

    } catch (Exception e) {
      System.out.println("과제 데이터 로딩 중 오류 발생!");
      e.printStackTrace();
    }
  }

// DataOutputStream을 사용하여 "assignment.data" 파일에 데이터를 저장
  void saveAssignment() {
    try (DataOutputStream out = new DataOutputStream(new BufferedOutputStream( new FileOutputStream("assignment.data")))) {
      long start = System.currentTimeMillis(); // 성능 측정
      out.writeInt(assignmentRepository.size());

      for (Assignment assignment : assignmentRepository) {
        out.writeUTF(assignment.getTitle());
        out.writeUTF(assignment.getContent());
        out.writeUTF(assignment.getDeadline().toString());
      }

      long end = System.currentTimeMillis();
      System.out.printf("걸린 시간: %d\n", end - start);

    } catch (Exception e) {
      System.out.println("과제 데이터 저장 중 오류 발생!");
      e.printStackTrace();
    }
  }