Developer.

[멋사 백엔드 19기] TIL 14일차

📂 목차


📚 본문

Java IO

자바는 입출력을 세 가지 원칙에 따라 만든다

  1. 유연성
  2. 확장성
  3. 재사용성

Decorator Pattern

객체에 추가적인 기능을 동적으로 부여하면서도 기존 코드를 수정하지 않고 확장 가능하도록 하는 디자인 패턴이다.

  • 상속 대신 위임(Composition)
  • 객체를 감싸는 래퍼 객체를 만들어 새로운 기능 추가
  • 원래 객체의 인터페이스를 유지하면서 부가 기능을 점진적으로 붙여나감

구성 요소

  • Component: 기본 기능을 정의하는 인터페이스나 추상 클래스
  • ConcreteComponent: 실제로 구현되고 동작하는 구현체
  • Decorator: Component 를 구현하면서 내부에 Component 를 포함하는(has-a 관계)
  • ConcreteDecorator: 구체적인 데코레이터이며, 기존 기능에 추가 기능을 덧붙인다.

예시를 보자.

Component

public interface Printer {
    void print(String message);
}

Concrete Component

public class BasicPrinter implements Printer {
    @Override
    public void print(String message) {
        System.out.println(message);
    }
}
public abstract class PrinterDecorator implements Printer {
    protected Printer printer; // 위임

    public PrinterDecorator(Printer printer) {
        this.printer = printer;
    }

    @Override
    public void print(String message) {
        printer.print(message); // 기본 동작은 그대로 유지
    }
}
public class BracketPrinter extends PrinterDecorator {
    public BracketPrinter(Printer printer) {
        super(printer);
    }

    @Override
    public void print(String message) {
        super.print("[" + message + "]");
    }
}

public class UpperCasePrinter extends PrinterDecorator {
    public UpperCasePrinter(Printer printer) {
        super(printer);
    }

    @Override
    public void print(String message) {
        super.print(message.toUpperCase());
    }
}

위를 보면 한눈에 이해 될 것이다. 여기서 PrintDecorator 가 짜여지는 코드를 기억해야 한다. 위임받은 Printer 를 has-a 로 가져가고 있고, protected 로 선언 하여 하위 ConcreteDecorator 들이 쓸 수 있도록 하며, 이를 상속받은 다양한 프린터들이 확장을 가능하게 한다.

이런 Decorator 패턴은 implements Printer 로 그치지 않고 Printer 외의 다수의 계약을 넣어서 조합하여 사용할 수 있는 활용도 할 수 있을 것이다.

이를 이해하고 Java IO를 보자.

Java IO Decorators

  • InputStream
  • OutputStream
  • Reader
  • Writer

전부 추상 클래스들이다. 이는 Decorator 에 해당하는 것일 터이다. 그럼 이를 Concrete 로 만들어주는 하위 데코레이터들이 다음 그림에 나타나있다.

java-io-hierarchical-structure

여기서 Stream 을 먼저 보자.

Java IO 바이트 단위 입출력 Stream

Stream 은 흐름인데, 기본적으로 컴퓨터가 주고 받는 흐름의 주된 개체는 데이터이다. 이 데이터는 바이트 단위로 주고 받으며, 그래서 단위도 바이트를 자주 쓰게 되고, 이런 바이트의 흐름을 관리하는 기능을 가진게 Stream 이 된다.

우리가 쓰고 읽는 모든 것들은 사실상 바이트로 되어 있기에, Stream 을 쓴다면 모든 종류의 데이터들을 처리할 수 있는 도구를 얻는 것이다.

Stream 은 보통 보는 관점에 따라 카테고리를 나눌 수 있는데, 입력이냐 출력이냐에 따라

  • Input
  • Output

으로 나뉘고, 데이터 단위에 따라

  • Byte Stream: 1 byte
  • Character Stream: 2 bytes(UTF-16)

으로 나눌 수 있겠다.

여기서 이제 흐름(stream) 을 들고와서 데이터 소스에 꽂기만 하면 그 흐름을 타고 알아서 우리 프로그램으로 데이터를 받아오게 된다.

다음은 예제이다:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class ByteStreamExample {
    public static void main(String[] args) {
        // try-with-resources를 사용한 자동 리소스 관리
        try (FileInputStream in = new FileInputStream("input.jpg");
             FileOutputStream out = new FileOutputStream("output.jpg")) {

            int byteData;
            // 파일 끝(-1)까지 한 바이트씩 읽기
            while ((byteData = in.read()) != -1) {
                out.write(byteData);
            }
            System.out.println("파일 복사 완료!");

        } catch (IOException e) {
            System.err.println("파일 처리 중 오류: " + e.getMessage());
        }
    }
}

Decorator 패턴을 쓰기에 OutputStream 이라는 추상 클래스를 통해 하위 ConcreteDecorator 들로 FileOutputStream, ByteArrayOutputStream 등등 많은 변종이 나옴을 사진에서 볼 수 있다.

Java IO 문자 단위 입출력 Reader, Writer

문자 스트림은 텍스트 데이터 처리에 최적화되어 있는데,

  • 바이트 스트림과 달리 2byte 단위로 문자 데이터를 처리
  • 한글이나 유니코드 문자를 다루는데 적합

의 특징이 있다. 입출력을 할 때마다 그때그때 맞는 ConcreteDecorator 를 쓰면 되겠다.

예시

try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
     BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {

    String line;
    while ((line = reader.readLine()) != null) {
        writer.write(line);
        writer.newLine(); // 줄바꿈
    }

    System.out.println("파일 복사 완료!");

} catch (IOException e) {
    e.printStackTrace();
}

try 안에 자원 여러개 넣을 수 있음