📂 목차
📚 본문
Java IO
자바는 입출력을 세 가지 원칙에 따라 만든다
- 유연성
- 확장성
- 재사용성
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
로 만들어주는 하위 데코레이터들이 다음 그림에 나타나있다.
여기서 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 안에 자원 여러개 넣을 수 있음