학습 목표 : 자바의 Input과 Ontput에 대해 학습하세요.
구분 | IO | NIO |
---|---|---|
입출력 방식 | 스트림 | 채널 |
버퍼 | 넌버퍼(non-buffer) | 버퍼(buffer) |
비동기 방식 | 지원 X | 지원 O |
블로킹/넌블로킹 | 블로킹 | 둘 다 지원 |
스트림이란 데이터를 운반하는데 사용되는 연결통로
버퍼(Buffer: 메모리 저장소) 읽고 쓰기가 가능한 메모리 배열
양방향으로 입력과 출력이 가능
스트림은 바이트단위로 데이터를 전송하며 입출력 대상에 따라 다음과 같은 입출력스트림이 있다.
입력스트림 | 출력스트림 | 입출력 대상의 종류 |
---|---|---|
FileInputStream | FileOutputStream | 파일 |
ByteArrayInputStream | ByteArrayOutputStream | 메모리(byte배열) |
PipedInputStream | PipedOutputStream | 프로세스(프로세스간의 통신) |
AudioInputStream | AudioOutputStream | 오디오장치 |
예를 들어, 어떤 파일의 내용을 읽고 싶으면 FileInputStream을 사용하면 된다.
모두 InputStream과 OutputStream의 자손들이며, 각각 읽고 쓰는데 필요한 추상메소드를 자신에 맞게 구현했다.
InputStream | OutputStream |
---|---|
abstract int read() | abstract void write(int b) |
int read(byte[] b) | void write(byte[] b) |
int read(byte[] b, int off, int len) | void write(byte[] b, int off, int len) |
read()와 write(int b)는 입출력의 대상에 따라 읽고 쓰는 방법이 다를 것이므로 각 상황에 맞게 구현할 수
있도록 추상메소드로 정의되어있다.
나머지 메소드들은 추상메소드인 read()와 write(int b)를 이용해서 구현한 것이므로 read()와 write(int b)
가 구현되어 있지 않으면 아무런 의미가 없다.
메모리 즉, 바이트배열에 데이터를 입출력 하는데 사용되는 스트림
다음은 ByteArrayInputStream/ByteArrayOutputStream을 이용해서 바이트배열 inSrc의 데이터를 outSrc로 복사하는 예제이다.
read() 와 write()를 사용하는 가장 기본적인 방법을 보여준다.
public class IOEx1 {
public static void main(String[] args) {
byte[] inSrc = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
byte[] outSrc = null;
ByteArrayInputStream input = null;
ByteArrayOutputStream output = null;
input = new ByteArrayInputStream(inSrc);
output = new ByteArrayOutputStream();
int data = 0;
while((data = input.read()) != -1) {
output.write(data);
}
outSrc = output.toByteArray(); // 스트림의 내용을 byte 배열로 변환
System.out.println("Input Source : " + Arrays.toString(inSrc));
System.out.println("Output Source :" + Arrays.toString(outSrc));
}
}
// 결과
Input Source : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Output Source :[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
while문의 조건식은 다음의 순서로 처리된다.
(data = input.read()) != -1
1. data = input.read() // read()를 호출한 반환값을 변수 data에 저장
2. data != -1 // data에 저장된 값이 -1 아닌지 비교
바이트배열은 사용하는 자원이 메모리 밖에 없으므로 가비지컬렉터에 의해 자동적으로 자원을 반환하므로 close()를
이용해서 스트림을 닫지 않아도 된다.
한 번에 1 byte만 읽고 쓰므로 작업 효율이 좋지 않다.
다음 예제는 배열을 사용해서 입출력 작업이 더 효율적이게 만들었다.
public class IOEx2 {
public static void main(String[] args) {
byte[] inSrc = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
byte[] outSrc = null;
byte[] temp = new byte[10];
ByteArrayInputStream input = null;
ByteArrayOutputStream output = null;
input = new ByteArrayInputStream(inSrc);
output = new ByteArrayOutputStream();
input.read(temp, 0, temp.length);
output.write(temp, 5, 5);
outSrc = output.toByteArray();
System.out.println("Input Source : " + Arrays.toString(inSrc));
System.out.println("temp : " + Arrays.toString(temp));
System.out.println("Output Source : " + Arrays.toString(outSrc));
}
}
// 결과
Input Source : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
temp : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Output Source : [5, 6, 7, 8, 9]
int read(byte[] b, int off, int len)와 void write(byte[] b, int off, int len)을 사용해서 입출력하는 방법이다.
이전 예제와 달리 byte배열을 사용해서 한 번에 배열의 크기만크 읽고 쓸 수 있다.
다음은 보조스트림의 종류이다.
입력 | 출력 | 설명 |
---|---|---|
FilterInputStream | FilterOutputStream | 필터를 이용한 입출력 처리 |
BufferedInputStream | BufferedOutputStream | 버퍼를 이용한 입출력 성능향상 |
DataInputStream | DataOutputStream | int, float와 같은 기본형 단위(primitive type)로 데이터를 처리하는 기능 |
SequenceInputStream | 없음 | 두 개의 스트림을 하나로 연결 |
LineNumberInputStream | 없음 | 읽어 온 데이터의 라인 번호를 카운트 (JDK 1.1 부터 LineNumberReader로 대체) |
ObjectInputStream | ObjectOutputStream | 데이터를 객체단위로 읽고 쓰는데 사용. 주로 파일을 이용하며 객체 직렬화와 관련 |
없음 | PrintStream | 버퍼를 이용하며, 추가적인 print관련 기능(print, printf, println 메소드) |
PushbackInputStream | 없음 | 버퍼를 이용해서 읽어 온 데이터를 다시 되돌리는 기능(unread, push back to buffer) |
FilterInputStream/FilterOutputStream은 InputStream/OutputStream의 자손이면서 모든 보조스트림의 조상이다.
FilterInputStream/FilterOutputStream을 상속받아서 기반스트림에 보조기능을 추가한 보조스트림 클래스는 다음과 같다.
FilterInputStream의 자손 BufferedInputStream, DataInputStream, PushbackInputStream 등
FilterOutputStream의 자손 BufferedOutputStream, DataOutputStream, PrintStream 등
한 바이트씩 입출력하는 것 보다는 버퍼(바이트배열)를 이용해서 한 번에 여러 바이트를 입출력하는 것이 빠르기 때문에
대부분의 입출력 작업에 사용된다.
생성자 | 설명 |
---|---|
BufferedInputStream(InputStream in, int size) | 주어진 InputStream 인스턴스를 입력소스(input source)로 하며 지정된 크기(byte단위)의 버퍼를 갖는 BufferedInputStream 인스턴스를 생성한다. |
BufferedInputStream(InputStream in) | 주어진 InputStream 인스턴스를 입력소스(input source)로 하며 버퍼의 크기를 지정해주지 않으므로 기본적으로 8192byte 크기의 버퍼를 갖게된다. |
BufferedInputStream의 버퍼크기는 입력소스로부터 한 번에 가져올 수 있는 데이터의 크기로 지정하면 좋다.
버퍼에 저장된 모든 데이터를 다 읽고 그 다음 데이터를 읽기위해 read 메소드가 호출되면 BufferedInputStream은
입력소스로부터 다시 버퍼크기 만큼의 데이터를 읽어 버퍼에 저장한다. 이와 같은 작업이 계속해서 반복된다.
메소드/생성자 | 설명 |
---|---|
BufferedOutputStream(OutputStream out, int size) | 주어진 OutputStream인스턴스를 출력소스(output source)로하며 지정된 크기(byte단위)의 버퍼를 갖는 BufferedOutputStream의 인스턴스를 생성한다. |
BufferedOutputStream(OutputStream out) | 주어진 OutputStream 인스턴스를 출력소스(output source)로 하며 버퍼의 크기를 지정해주지 않으므로 기본적으로 8192byte 크기의 버퍼를 갖게된다. |
flush() | 버퍼의 모든 내용을 출력소스에 출력한 다음, 버퍼를 비운다. |
close() | flush()를 호출해서 버퍼의 모든 내용을 출력소스에 출력하고, BufferedOutputStream 인스턴스가 사용하던 모든 자원을 반환한다. |
💡 버퍼가 가득 찼을 때만 출력소스에 출력을 하기 때문에, 마지막 출력부분이 출력소스에 쓰이지 못하고
BufferedOutputStream의 버퍼에 남아있는 채로 프로그램이 종료될 수 있음을 주의
public class BufferedOutputStreamEx1 {
public static void main(String[] args) {
try {
FileOutputStream fos = new FileOutputStream("123.txt");
// BufferedOutputStream의 버퍼 크기를 5로 한다.
BufferedOutputStream bos = new BufferedOutputStream(fos, 5);
// 파일 123.txt에 1 부터 9까지 출력한다.
for (int i = '1'; i <= '9'; i++) {
bos.write(i);
}
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 결과
12345
크기가 5인 BufferdOutputStream을 이용해서 파일 123.txt에 1~9 까지 출력하는 예제이다.
그런데, 123.txt 파일을 열어보면 5까지만 출력되어있다.
이유는 아래 그림과 같이 버퍼에 남아있는 데이터가 출력되지 못한 상태로 프로그램이 종료되었기 때문이다.
이 문제를 해결하기 위한 방법은 fos.close();가 아닌 bos.close(); 를 해주어야 한다.
BufferedOutputStream의 close()를 호출해 주어야 버퍼에 남아있던 모든 내용이 출력된다.
BufferedOutputStream의 close()는 기반 스트림인 FileOutputStream의 close()를 호출하기 때문에
FileOutputStream의 close()는 따로 호출해주지 않아도 된다.
즉, 보조스트림을 사용할 경우 기반스트림의 close()나 flush()를 호출할 필요가 없다.
데이터를 읽고 쓸 때 byte 단위가 아닌, 8가지 기본 자료형의 단위로 읽고 쓸 수 있는 장점이 있다.
public class DataOutputStreamEx2 {
public static void main(String[] args) {
ByteArrayOutputStream bos;
DataOutputStream dos;
byte[] result;
try {
bos = new ByteArrayOutputStream();
dos = new DataOutputStream(bos);
dos.writeInt(10);
dos.writeFloat(20.0f);
dos.writeBoolean(true);
result = bos.toByteArray();
String[] hex = new String[result.length];
for (int i = 0; i < result.length; i++) {
if (result[i] < 0) {
hex[i] = String.format("%02x", result[i] + 256);
} else {
hex[i] = String.format("%02x", result[i]);
}
}
System.out.println("10 진수 : " + Arrays.toString(result));
System.out.println("16 진수 : " + Arrays.toString(hex));
dos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 결과
10 진수 : [0, 0, 0, 10, 65, -96, 0, 0, 1]
16 진수 : [00, 00, 00, 0a, 41, a0, 00, 00, 01]
public class DataInputStreamEx1 {
public static void main(String[] args) {
try {
FileOutputStream fos = new FileOutputStream("sample.dat");
DataOutputStream dos = new DataOutputStream(fos);
dos.writeInt(10);
dos.writeFloat(20.0f);
dos.writeBoolean(true);
dos.close();
FileInputStream fis = new FileInputStream("sample.dat");
DataInputStream dis = new DataInputStream(fis);
System.out.println(dis.readInt());
System.out.println(dis.readFloat());
System.out.println(dis.readBoolean());
dis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 결과
10
20.0
true
sample.dat 파일을 생성 후 출력하는 예제이다.
sample.dat 파일로부터 데이터를 읽어 올 때, 아무런 변환이나 자릿수를 셀 필요없이 단순히 readInt()와 같이
읽어 올 데이터 타입에 맞는 메소드를 사용하면된다.
여러 개의 입력스트림을 연속적으로 연결해서 하나의 스트림으로부터 데이터를 읽는 것과 같이 처리
public class SequenceInputStreamEx {
public static void main(String[] args) {
byte[] arr1 = {0, 1, 2};
byte[] arr2 = {3, 4, 5};
byte[] arr3 = {6, 7, 8};
byte[] outSrc = null;
Vector v = new Vector();
v.add(new ByteArrayInputStream(arr1));
v.add(new ByteArrayInputStream(arr2));
v.add(new ByteArrayInputStream(arr3));
SequenceInputStream input = new SequenceInputStream(v.elements());
ByteArrayOutputStream output = new ByteArrayOutputStream();
int data;
try {
while((data = input.read()) != -1) {
output.write(data);
}
} catch (IOException e) {
e.printStackTrace();
}
outSrc = output.toByteArray();
System.out.println("Input Source1 : " + Arrays.toString(arr1));
System.out.println("Input Source2 : " + Arrays.toString(arr2));
System.out.println("Input Source3 : " + Arrays.toString(arr3));
System.out.println("Output Source : " + Arrays.toString(outSrc));
}
}
데이터를 기반스트림에 다양한 형태로 출력할 수 있는 print, println, printf와 같은 메소드를 오버로딩하여 제공
public class PrintStreamEx1 {
public static void main(String[] args) {
int i = 65;
float f = 1234.56789f;
Date d = new Date();
System.out.printf("문자 %c의 코드는 %d%n", i, i);
System.out.printf("%d는 8진수로 %o, 16진수로 %x%n", i, i, i);
System.out.printf("%3d%3d%3d%n", 100, 90, 80);
System.out.println();
System.out.printf("%s%-5s%5s%n", "123", "123", "123");
System.out.println();
System.out.printf("%-8.1f%8.1f %e%n", f, f, f);
System.out.println();
System.out.printf("오늘은 %tY년 %tm월 %td일 입니다. %n", d, d, d);
System.out.printf("지금은 %tH시 %tM분 %tS초 입니다. %n", d, d, d);
System.out.printf("지금은 %1$tH시 %1$tM분 %1$tS초 입니다. %n", d);
}
}
printf()를 사용한 예제이다.
Java에서는 한 문자를 의미하는 char형이 2 byte여서 바이트기반의 스트림으로 2 byte인 문자를 처리하는데 어렵다.
문자데이터를 입출력할 때는 바이트기반 스트림 대신 문자기반 스트림을 사용하자.
InputStream --> Reader
OutputStream --> Writer
바이트기반 스트림 | 문자기반 스트림 |
---|---|
FileInputStream FileOutputStream | FileReader FileWriter |
ByteArrayInputStream ByteArrayOutputStream | CharArrayReader CharArrayWriter |
PipedInputStream PipedOutputStream | PipedReader PipedWriter |
StringBufferInputStream StringBufferOutputStream | StringReader StringWriter |
💡 StringBufferInputStream, StringBufferOutputStream은 StringReader와 StringWriter로 대체되어 더이상 사용되지 않음.
다음은 바이트기반 스트림과 문자기반 스트름의 읽기, 쓰기 메소드를 비교한 것이다.
InputStream | Reader |
---|---|
abstract int read() | int read() |
int read(byte[] b) | int read(char[] cbuf) |
int read(byte[] b, int off, int len) | abstract int read(char[], int off, int len) |
InputStream | Reader |
---|---|
abstract void write(int b) | void write(int c) |
void write(byte[] b) | void write(char[] cbuf) |
void write(byte[] b, int off, int len) | abstract void write(char[] cbuf, int off, int len) void write(String str) void write(String str, int off, int len) |
바이트기반 보조스트림 | 문자기반 보조스트림 |
---|---|
BufferedInputStream BufferedOutputStream | BufferedReader BufferedWriter |
FilterInputStream FilterOutputStream | FilterReader FilterWriter |
LineNumberInputStream(deprecated) | LineNumberReader |
PrintStream | PrintWriter |
PushbackInputStream | PushbackReader |
보조스트림 역시 다음과 같은 문자기반 보조스트림이 존재하며 사용목적과 방식은 바이트기반 보조스트림과 다르지않다.
쓰레드 간에 데이터를 주고받을 때 사용된다.
다른 스트림과 달리 입력과 출력 스트림을 하나의 스트림으로 연결해서 데이터를 주고받는다.
public class PipedReaderWriter {
public static void main(String[] args) {
InputThread inputThread = new InputThread("InputThread");
OutputThread outputThread = new OutputThread("OutputThread");
// PipedReader와 PipedWriter 연결
inputThread.connect(outputThread.getOutput());
inputThread.start();
outputThread.start();
}
}
class InputThread extends Thread {
PipedReader input = new PipedReader();
StringWriter sw = new StringWriter();
InputThread (String name) {
super(name); // Thread(String name);
}
@Override
public void run() {
try {
int data;
while((data = input.read()) != -1) {
sw.write(data);
}
System.out.println(getName() + " received : " + sw.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
public PipedReader getInput() {
return input;
}
public void connect (PipedWriter output) {
try {
input.connect(output);
} catch (IOException e) {
e.printStackTrace();
}
}
}
class OutputThread extends Thread {
PipedWriter output = new PipedWriter();
OutputThread(String name){
super(name);
}
@Override
public void run() {
try{
String msg = "Hello";
System.out.println(getName() + " sent : " + msg);
output.write(msg);
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public PipedWriter getOutput() {
return output;
}
public void connect(PipedReader input) {
try {
output.connect(input);
} catch (IOException e) {
e.printStackTrace();
}
}
}
두 쓰레드가 PipedReader/PipedWriter를 이용해서 서로 메시지를 주고받는 예제이다.
쓰레드 시작전에 두 스트림을 연결해야하는 것을 주의하자!
CharArrayReader/CharArrayWriter와 같이 입출력 대상이 메모리인 스트림이다.
StringBuffer getBuffer()
StringWriter에 출력한 데이터가 저장된 StringBuffer를 반환 String toString()
StringWriter에 출력된 문자열을 반환 public class StringReaderWriterEx {
public static void main(String[] args) {
String inputData = "ABCD";
StringReader input = new StringReader(inputData);
StringWriter output = new StringWriter();
int data;
try {
while((data = input.read()) != -1) {
output.write(data);
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("Input Data : " + inputData);
System.out.println("Output Data : " + output.toString());
}
}
// 결과
Input Data : ABCD
Output Data : ABCD
버퍼를 이용해서 입출력의 효율을 높일 수 있도록 해주는 역할을 한다.
public class BufferedReaderEx1 {
public static void main(String[] args) {
try(FileReader fr = new FileReader("src/main/java/com/jihan/javastudycode/week13/BufferedReaderEx1.java");
BufferedReader br = new BufferedReader(fr)) {
String line = "";
for(int i = 1; (line = br.readLine()) != null; i++) {
// ";"을 포함한 라인을 출력한다.
if(line.indexOf(";") != -1)
System.out.println(i + ": " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 결과
1: package com.jihan.javastudycode.week13;
3: import java.io.*;
7: try(FileReader fr = new FileReader("src/main/java/com/jihan/javastudycode/week13/BufferedReaderEx1.java");
10: String line = "";
12: for(int i = 1; (line = br.readLine()) != null; i++) {
13: // ";"을 포함한 라인을 출력한다.
14: if(line.indexOf(";") != -1)
15: System.out.println(i + ": " + line);
19: e.printStackTrace();
BufferedReader의 readLine()을 이용해서 파일을 라인단위로 읽은 다음 indexOf()를 이용해서
';'을 포함하고 있는 라인만 출력하는 예제이다.
- 바이트기반 스트림을 문자기반 스트림으로 연결시켜주는 역할을 한다.
- 바이트기반 스트림의 데이터를 지정된 인코딩 문자데이터로 변환하는 작업을 한다.
public class InputStreamReaderEx {
public static void main(String[] args) {
String line = "";
try(InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(isr)) {
System.out.println("사용중인 OS의 인코딩 : " + isr.getEncoding());
do {
System.out.print("문장을 이력하세요. 마치려면 q를 입력하세요.>");
line = br.readLine();
System.out.println("입력하신 문장 : " + line);
} while (!line.equalsIgnoreCase("q"));
System.out.println("프로그램을 종료합니다.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 결과
사용중인 OS의 인코딩 : UTF8
문장을 이력하세요. 마치려면 q를 입력하세요.> hello my name is jihan
입력하신 문장 : hello my name is jihan
문장을 이력하세요. 마치려면 q를 입력하세요.>jihan world~~
입력하신 문장 : jihan world~~
문장을 이력하세요. 마치려면 q를 입력하세요.>good bye
입력하신 문장 : good bye
문장을 이력하세요. 마치려면 q를 입력하세요.>q
입력하신 문장 : q
프로그램을 종료합니다.
표준입출력은 콘솔(console)을 통한 데이터 입력과 콘솔로의 데이터 출력을 의미한다.
자바에서는 표준 입출력을 위해 3가지 입출력 스트림을 자바 애플리케이션 실행과 동시에 사용할 수 있게 자동으로
생성해준다. 따라서, 개발자가 별도의 스트림 생성없이 사용할 수 있다.
System.in 콘솔로부터 데이터를 입력받는데 사용
System.out 콘솔로 데이터를 출력하는데 사용
System.err 콘솔로 데이터를 출력하는데 사용
public class StandardIOEx1 {
public static void main(String[] args) {
try {
int input = 0;
while((input=System.in.read()) != -1) {
System.out.println("input : " + input + ", (char) input : " + (char)input );
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 결과
hello!!!
input : 104, (char) input : h
input : 101, (char) input : e
input : 108, (char) input : l
input : 108, (char) input : l
input : 111, (char) input : o
input : 33, (char) input : !
input : 33, (char) input : !
input : 33, (char) input : !
input : 10, (char) input :
System.in.read()를 이용하여 사용자의 입력을 받아 데이터를 읽어들이는 예제이다.
public class StandardIOEx2 {
public static void main(String[] args) {
System.out.println("out: Hello World!");
System.err.println("err: Hello World!");
}
}
// 결과
out: Hello World!
err: Hello World!
메소드 | 설명 |
---|---|
static void setOut(PrintStream out) | System.out의 출력을 지정된 PrintStream으로 변경 |
static void setErr(PrintStream err) | System.err의 출력을 지정된 PrintStream으로 변경 |
static void setIn(PrintStream in) | System.in의 출력을 지정된 PrintStream으로 변경 |
그러나 JDK 1.5부터 등장한 Scanner 클래스로 인해 System.in으로부터 데이터를 입력받아 작업하는 것이 편리해짐
public class StandardIOEx3 {
public static void main(String[] args) {
PrintStream ps = null;
FileOutputStream fos = null;
try {
fos = new FileOutputStream("test.txt");
ps = new PrintStream(fos);
System.setOut(ps); // System.out의 출력대상을 test.txt 파일로 변경
} catch (FileNotFoundException e) {
e.printStackTrace();
}
System.out.println("Hello by System.out");
System.err.println("Hello by System.err");
}
}
// 결과
// console
Hello by System.err
// text.txt
Hello by System.out
파일에 입출력을 하기 위한 스트림
생성자 | 설명 |
---|---|
FileInputStream(String name) | 지정된 파일명(name)을 가진 실제 파일과 연결된 FileInputStream을 생성 |
FileInputStream(File file) | File 인스턴스로 지정해주어야 하는점을 제외하고 위와 같다. |
FileInputStream(FileDescriptor fdObj) | 파일 디스크립터(fdObj)로 FileInputStream을 생성 |
FileOutputStream(String name) | 지정된 파일명(name)을 가진 실제 파일과 연결된 FileOutputStream을 생성 |
FileOutputStream(String name, boolean append) | 두번째 인자인 append를 true로 하면, 출력시 기존의 파일내용의 마지막에 덧붙인다. false면, 기존의 파일내용을 덮어쓴다. |
FileOutputStream(File file) | File 인스턴스로 지정해주어야 하는점을 제외하고 FileOutputStream(String name)과 같다. |
FileOutputStream(File file, boolean append) | File 인스턴스로 지정해주어야 하는점을 제외하고 FileOutputStream(String name, boolean append)과 같다. |
FileOutputStream(FileDescriptor fdObj) | 파일 디스크립터(fdObj)로 FileOutputStream을 생성 |
첫 번째 예제를 작성하자.
public class FileViewer {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream(args[0]);
int data = 0;
while((data=fis.read()) != -1) {
char c = (char)data;
System.out.print(c);
}
}
}
커맨드 라인으로부터 입력받은 파일의 내용을 그대로 읽어서 출력하는 예제이다.
read()의 반환값이 int형(4 byte)이긴 하지만, 더 이상 입력값이 없음을 알리는 -1을 제외하고는 0~255(1 byte)범위의
정수값이기 때문에, char형(2 byte)로 변환해도 손실되는 값은 없다.
$ java com.jihan.javastudycode.week13.FileViewer com/jihan/javastudycode/week13/FileViewer.java
package com.jihan.javastudycode.week13;
import java.io.FileInputStream;
import java.io.IOException;
public class FileViewer {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream(args[0]);
int data = 0;
while((data=fis.read()) != -1) {
char c = (char)data;
System.out.print(c);
}
}
}
커맨드 라인에서 입력한 결과이다. 파일의 내용을 그대로 출력해준다.
또 다른 예제를 봐보자. 이번에는 파일의 내용을 복사하여 다른 파일에 옮기는 예제이다.
public class FileCopy {
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream(args[0]);
FileOutputStream fos = new FileOutputStream(args[1]);
int data = 0;
while ((data = fis.read()) != -1) {
fos.write(data);
}
fis.close();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
$ java com.jihan.javastudycode.week13.FileCopy com/jihan/javastudycode/week13/FileCopy.java backUp.txt
$ cat backUp.txt
package com.jihan.javastudycode.week13;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileCopy {
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream(args[0]);
FileOutputStream fos = new FileOutputStream(args[1]);
int data = 0;
while ((data = fis.read()) != -1) {
fos.write(data);
}
fis.close();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
FileCopy.java의 내용을 backUp.txt 파일에 그대로 옮겼다. 텍스트 파일을 다루는 경우에는
FileInputStream/FileOutputStream 보다 문자기반 스트림인 FileReader/FileWriter를 사용하는 것이 더 좋다.
스트림의 기능을 보완하기 위한 보조스트림이 제공된다.
데이터 입출력의 기능은 없지만, 스트림의 기능을 향상시키거나 새로운 기능을 추가할 수 있다.
예를 들면, hello.txt 파일을 읽기위해 FileInputStream을 사용할 때, 입력 성능을 향상시키기 위해
BufferedInputStream을 사용하는 코드는 다음과 같다.
// 먼저 스트림을 생성한다.
FileInputStream fis = new FileInputStream("hello.txt");
// 스트림을 이용해서 보조스트림을 생성한다.
BufferedInputStream bis = new BufferedInputStream(fis);
bis.read(); // 보조스트림으로부터 데이터를 읽는다.
파일로부터 텍스트 데이터를 읽고, 파일에 쓰는데 사용
public class FileReaderEx1 {
public static void main(String[] args) {
String fileName = "test.txt";
try(FileInputStream fis = new FileInputStream(fileName);
FileReader fr = new FileReader(fileName)) {
int data;
while((data = fis.read()) != -1) {
System.out.print((char)data);
}
System.out.println();
while((data=fr.read()) != -1) {
System.out.print((char)data);
}
System.out.println();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 결과
// FileInputStream
ìë
! ìë
~ Hello$$
// FileReader
안녕! 안녕~ Hello$$
바이트 기반 스트림인 FileInputStream과 문자기반 FileReader를 사용하여 파일의 내용을 읽어 화면에 출력하는 예제이다.
결과를 보면 FileInputStream을 사용했을 경우 한글이 깨져서 출력된 것을 볼 수 있다.
public class FileConversion {
public static void main(String[] args) {
String fileName = "test.txt";
try(FileReader fr = new FileReader("test.txt");
FileWriter fw = new FileWriter("convert.txt")) {
int data;
while((data=fr.read()) != -1) {
if (data != '\t' && data != '\n' && data != ' ' && data != '\r'){
fw.write(data);
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
위에서 만든 "test.txt" 파일을 읽어서 "convert.txt" 파일로 출력하는 예제이다.
코드를 보면 공백을 제거해서 새로운 convert.txt 파일을 출력한다.
// 결과
// convert.txt
안녕!안녕~Hello$$
RandomAccessFile은 하나의 클래스로 파일에 대한 입출력을 모두 할 수 있다.
getFilePointer()
를 사용 seek(long pos)나 skipBytes(int n)
을 사용public class RandomAccessFileEx1 {
public static void main(String[] args) {
try {
RandomAccessFile raf = new RandomAccessFile("test.dat", "rw");
System.out.println("파일 포인터의 위치: " + raf.getFilePointer());
raf.writeInt(100);
System.out.println("파일 포인터의 위치: " + raf.getFilePointer());
raf.writeLong(100L);
System.out.println("파일 포인터의 위치: " + raf.getFilePointer());
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 결과
파일 포인터의 위치: 0
파일 포인터의 위치: 4
파일 포인터의 위치: 12
파일에 출력작업이 수행됐을 때 파일 포인터의 위치 변화를 출력하는 예제이다.
int 일 때 4 byte long 일 때 8 byte 이동하여 총, 12 byte 만큼 이동한 것을 알 수 있다.
💡 인스턴스 모드
r 파일로 부터 읽기만 수행
rw 파일에 읽기 쓰기 수행
rws, rwd rw와 같으나 출력내용이 파일에 지연 없이 바로 쓰이게 한다. rwd는 파일의 내용만, rws는 파일의 메타정보도 포함
public class RandomAccessFileEx2 {
public static void main(String[] args) {
int[] score = {100, 85, 90, 60};
try {
RandomAccessFile raf = new RandomAccessFile("score2.dat", "rw");
for (int i = 0; i < score.length; i++) {
raf.writeInt(score[i]);
}
raf.seek(0);
while (true) {
System.out.println(raf.readInt());
}
} catch (EOFException eof) {
// readInt()를 호출했을 때 더 이상 읽을 내용이 없으면 EOFException 발생
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 결과
100
85
90
60
File 클래스를 통해서 파일과 디렉토리를 다룰 수 있다.
File 인스턴스는 파일 일 수도 있고 디렉토리 일 수도 있다.
파일의 경로와 디렉토리나 파일의 이름을 구분하는데 사용되는 구분자가 OS 마다 다르기 때문에 OS 독립적으로
프로그램을 작성하기 위해서는 반드시 특정 멤버변수들을 이용해야한다.
public class FileEx1 {
public static void main(String[] args) throws IOException {
File f = new File("src/main/java/com/jihan/javastudy/FileEx1.java");
String fileName = f.getName();
int pos = fileName.lastIndexOf(".");
System.out.println("경로를 제외한 파일이름 - " + f.getName());
System.out.println("확장자를 제외한 파일이름 - " + fileName.substring(0, pos));
System.out.println("확장자 - " + fileName.substring(pos + 1));
System.out.println();
System.out.println("경로를 포함한 파일이름 - " + f.getPath());
System.out.println("파일의 절대 경로 - " + f.getAbsolutePath());
System.out.println("파일의 정규경로 - " + f.getCanonicalPath());
System.out.println("파일이 속해 있는 디렉토리 - " + f.getParent());
System.out.println();
System.out.println("File.separator - " + File.separator);
System.out.println("File.separatorChar - " + File.separatorChar);
System.out.println("File.pathSeparator - " + File.pathSeparator);
System.out.println("File.pathSeparatorChar - " + File.pathSeparatorChar);
System.out.println();
System.out.println("user.dir = " + System.getProperty("user.dir"));
}
}
경로를 제외한 파일이름 - FileEx1.java
확장자를 제외한 파일이름 - FileEx1
확장자 - java
경로를 포함한 파일이름 - src/main/java/com/jihan/javastudy/FileEx1.java
파일의 절대 경로 - /Users/jihan/janjanee/workspace/java/java-study-code/src/main/java/com/jihan/javastudy/FileEx1.java
파일의 정규경로 - /Users/jihan/janjanee/workspace/java/java-study-code/src/main/java/com/jihan/javastudy/FileEx1.java
파일이 속해 있는 디렉토리 - src/main/java/com/jihan/javastudy
File.separator - /
File.separatorChar - /
File.pathSeparator - :
File.pathSeparatorChar - :
user.dir = /Users/jihan/janjanee/workspace/java/java-study-code
public class FileEx4 {
public static void main(String[] args) {
File dir = new File(args[0]);
File[] files = dir.listFiles();
for (int i = 0; i < files.length; i++) {
File f = files[i];
String name = f.getName();
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mma");
String attribute = "";
String size = "";
if(files[i].isDirectory()) {
attribute = "DIR";
} else {
size = f.length() + "";
attribute = f.canRead() ? "R" : " ";
attribute += f.canWrite() ? "W" : " ";
attribute += f.isHidden() ? "H" : " ";
}
System.out.printf("%s %3s %6s %s\n", df.format(new Date(f.lastModified())), attribute, size, name);
}
}
}
$ java com.jihan.javastudycode.week13.FileEx4 경로
2021-01-14 23:58오후 RW 45003 dog.jpg
2021-02-17 15:23오후 RWH 6148 .DS_Store
2021-01-14 23:02오후 RW 3801 cup.jpeg
2021-02-17 15:23오후 DIR example
2021-01-14 23:57오후 RW 889 mobilenet.html
2021-01-14 23:57오후 RW 1006 bodyfix.html
2021-01-14 23:17오후 RW 111443 person.jpg
2021-01-14 23:49오후 RW 389534 cat.jpg
2021-01-14 23:03오후 RW 61371 latte.jpg
public class FileSplit {
public static void main(String[] args) {
final int VOLUME = Integer.parseInt(args[1]);
String filename = args[0];
try (FileInputStream fis = new FileInputStream(filename);
BufferedInputStream bis = new BufferedInputStream(fis);
){
FileOutputStream fos = null;
BufferedOutputStream bos = null;
int data = 0;
int i = 0;
int number = 0;
while((data = bis.read()) != -1) {
if (i % VOLUME == 0) {
if (i != 0) {
bos.close();
}
fos = new FileOutputStream(filename + "_." + ++number);
bos = new BufferedOutputStream(fos);
}
bos.write(data);
i++;
}
bos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 결과
-rw-r--r-- 1287 2 17 20:51 test.txt
-rw-r--r-- 200 2 17 20:51 test.txt_.1
-rw-r--r-- 200 2 17 20:51 test.txt_.2
-rw-r--r-- 200 2 17 20:51 test.txt_.3
-rw-r--r-- 200 2 17 20:51 test.txt_.4
-rw-r--r-- 200 2 17 20:51 test.txt_.5
-rw-r--r-- 200 2 17 20:51 test.txt_.6
-rw-r--r-- 87 2 17 20:51 test.txt_.7
지정한 파일을 지정한 크기로 잘라서 여러개의 파일로 만드는 예제이다.
다음예제는 바로 위에서 쪼개진 파일을 하나로 합치는 예제이다.
public class FileMerge {
public static void main(String[] args) {
String mergeFilename = args[0];
try {
File tempFile = File.createTempFile("~mergetemp", ".tmp");
tempFile.deleteOnExit();
FileOutputStream fos = new FileOutputStream(tempFile);
BufferedOutputStream bos = new BufferedOutputStream(fos);
BufferedInputStream bis = null;
int number = 1;
File f = new File(mergeFilename + "_." + number);
while (f.exists()) {
f.setReadOnly();
bis = new BufferedInputStream(new FileInputStream(f));
int data = 0;
while((data = bis.read()) != -1) {
bos.write(data);
}
bis.close();
f = new File(mergeFilename + "_." + ++number);
}
bos.close();
File oldFile = new File(mergeFilename);
if (oldFile.exists())
oldFile.delete();
tempFile.renameTo(oldFile);
} catch (IOException e) {
e.printStackTrace();
}
}
}
위에서 여러개로 쪼개진 파일들을 하나의 파일로 합친다. 이 때 임시파일을 새로 만들고 프로그램 종료시
자동으로 삭제되도록 한다.
이유는 프로그램 실행중 사용자에 의해 중단되거나, 합쳐지는 도중 불완전한 파일이 생기는 것을 방지하기 위함이다.
작업이 완료되면 기존 파일을 삭제하고 완성된 임시파일의 이름을 기존파일의 이름으로 변경하면 된다.
객체를 컴퓨터에 저장했다가 다음에 다시 꺼내쓸 수 없을까?
네트워크를 통해 컴퓨터 간에 서로 객체를 주고받을 수 없을까?
있다! 직렬화(Serialization)이 가능하게 해준다.
직렬화? 객체를 데이터 스트림으로 만드는 것
역직렬화(deserialization)
라고 한다. static, transient
가 붙은 경우 직렬화 되지 않는다. 직렬화(스트림에 객체를 출력) -> ObjectOutputStream
역직렬화(스트림으로부터 객체를 입력) -> ObjectInputStream
ObjectInputStream(InputStream in)
ObjectOutputStream(OutputStream out)
FileOutputStream fos = new FileOutputStream("objectfile.ser");
ObjectOutputStream out = new ObjectOutputStream(fos);
out.writeObject(new UserInfo());
FileInputStream fis = new FileInputStream("objectfile.ser");
ObjectInputStream in = new ObjectInputStream(fis);
UserInfo info = (UserInfo)in.readObject();
직렬화가 가능한 클래스를 만드는 방법은 직렬화하고자 하는 클래스가
java.io.Serializable 인터페이스를 구현하도록 하면 된다.
public class UserInfo implements Serializable {
...
}
클래스를 직렬화 가능하도록 하려면 위와같이 Serializable 인터페이스를 구현하면 된다.
public interface Serializable {}
Serializable 인터페이스를 확인해보면 아무런 내용이 없는 빈 인터페이스인데 직렬화를 고려하여 작성한 클래스인지를 판단하는 기준이 된다.
public class SuperUserInfo implements Serializable {
String name;
String password;
}
public class UserInfo extends SuperUserInfo {
int age;
}
Serializable을 구현한 클래스를 상속받으면, Serializable을 구현하지 않아도 된다.
위의 예제에서는 UserInfo 는 SuperUserInfo를 상속받았으므로 UserInfo도 직렬화가 가능하다.
public class UserInfo implements Serializable {
String name;
String password;
int age;
Object obj = new Object(); // Object는 직렬화 할 수 없다!
}
위의 클래스를 직렬화하면 java.io.NotSerializableException
이 발생한다.
왜? 그 이유는 직렬화 할 수 없는 Object 클래스를 인스턴스변수로 참조하고 있기 때문이다.
public class UserInfo implements Serializable {
String name;
String password;
int age;
Object obj = new String("hello"); // String은 직렬화될 수 있다.
}
위의 클래스를 직렬화하면 이번에는 성공한다. 인스턴스변수 obj의 타입이 직렬화가 안되는 Object 이더라도
실제로 저장된 객체는 직렬화가 가능한 String 인스턴스이기 때문에 가능한것이다.
💡 인스턴스변수의 타입이 아닌 실제로 연결된 객체의 종류에 의해서 결정된다는 것!
public class UserInfo implements Serializable {
String name;
transient String password; // 직렬화 대상에서 제외
int age;
transient Object obj = new Object(); // 직렬화 대상에서 제외
}
직렬화하려는 객체의 클래스에 제어자 transient
를 붙여서 직렬화 대상에서 제외시킬 수 있다.
그리고 transient가 붙은 인스턴스변수의 값은 그 타입의 기본값으로 직렬화된다.
-> UserInfo 객체를 역직렬화하면 참조변수인 obj와 password의 값은 null 이 된다.
이제 예제를 통해 직렬화를 해보자.
public class UserInfo implements Serializable {
String name;
String password;
int age;
public UserInfo() {
this("Unknown", "1111", 0);
}
public UserInfo(String name, String password, int age) {
this.name = name;
this.password = password;
this.age = age;
}
@Override
public String toString() {
return "UserInfo{" +
"name='" + name + '\'' +
", password='" + password + '\'' +
", age=" + age +
'}';
}
}
직렬화 대상 테스트 클래스인 UserInfo를 만든다.
public class SerialEx1 {
public static void main(String[] args) {
String fileName = "UserInfo.ser";
try(FileOutputStream fos = new FileOutputStream(fileName);
BufferedOutputStream bos = new BufferedOutputStream(fos);
ObjectOutputStream out = new ObjectOutputStream(bos)) {
UserInfo u1 = new UserInfo("Kim", "12345", 30);
UserInfo u2 = new UserInfo("Lee", "3333", 20);
ArrayList<UserInfo> list = new ArrayList<>();
list.add(u1);
list.add(u2);
out.writeObject(u1);
out.writeObject(u2);
out.writeObject(list);
System.out.println("직렬화 끝.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
위에서 만든 UserInfo 객체를 직렬화하여 파일(UserInfo.ser)에 저장하는 예제이다.
FileOutputStream을 기반으로 한 ObjectOutputStream을 생성 후, writeObject()를 이용해서
객체를 출력하면 UserInfo.ser 파일에 객체가 직렬화되어 저장된다.
public class SerialEx2 {
public static void main(String[] args) {
String fileName = "UserInfo.ser";
try(FileInputStream fis = new FileInputStream(fileName);
BufferedInputStream bis = new BufferedInputStream(fis);
ObjectInputStream in = new ObjectInputStream(bis)) {
UserInfo u1 = (UserInfo) in.readObject();
UserInfo u2 = (UserInfo) in.readObject();
ArrayList<UserInfo> list = (ArrayList<UserInfo>) in.readObject();
System.out.println(u1);
System.out.println(u2);
System.out.println(list);
} catch (Exception e) {
e.printStackTrace();
}
}
}
앞의 예제인 직렬화된 객체를 역직렬화하는 예제이다.
부모클래스가 Serializable 인터페이스를 구현하면 자식 클래스도 직렬화가 가능하다고 했다.
그런데 부모 클래스는 Serializable을 구현하지 않고 자식 클래스만 구현했다면?
자식 클래스의 필드만 직렬화가된다.
만약, 이런 상황에서 부모 클래스의 필드도 직렬화하고 싶다면 어떻게 해야할까?
두 방법이 있는데, 첫번째가 좋겠지만 그럴 수 없는 상황이라면 두 번째 방법을 사용해야한다.
private void writeObject(ObjectOutputStream out) throws IOEXception {
// 부모 객체의 필드값을 출력
out.writeXXX(부모필드);
...
out.defaultWriteObject(); // 자식 객체의 필드값을 직렬화
}
private void readObject(ObjectInputStream in) throws IOEXception, ClassNotFoundException {
// 부모 객체의 필드값을 입력
부모필드 = in.readXXX();
...
out.defaultWriteObject(); // 자식 객체의 필드값을 역직렬화
}
두 메소드의 선언 방법이다.
주의할 점은 접근 제한자가 private
가 아니면 자동호출이 되지 않으므로 반드시 private
으로 해야한다.
아래는 예제 코드이다.
public class Parent {
String field1;
}
public class Child extends Parent implements Serializable {
String filed2;
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeUTF(field1);
out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
field1 = in.readUTF();
in.defaultReadObject();
}
}
위에서 만든 UserInfo 클래스에 인스턴스 변수를 하나 추가해보자.
public class UserInfo implements Serializable {
double weight;
...
}
몸무게 weight 변수를 추가하였다.
위의 SerialEx2 예제인 역직렬화를 다시 실행시켜보자.
java.io.InvalidClassException: com.jihan.javastudycode.week13.UserInfo;
local class incompatible: stream classdesc serialVersionUID = 6546280052364076434, local class serialVersionUID = -3670788073303903862
...
직렬화 할 때와 역직렬화 할 때의 클래스의 버전이 다르다는 에러가 발생한다!
객체가 직렬화될 때 클래스에 정의된 멤버들의 정보를 이용해서 serialVersionUID
라는 클래스의 버전을
자동생성해서 직렬화 내용에 포함된다.
그래서 역직렬화 할 때 클래스의 버전을 비교하고 직렬화할 때의 클래스의 버전과 일치하는지 비교할 수 있었고 에러가 발생한 것이다.
public UserInfo implements Serializable {
private static final long serialVersionUID = 1L;
...
}
이렇게 클래스 내에 serialVersionUID를 정의해주면, 클래스의 내용이 바뀌어도 클래스의 버전이 자동생성된 값으로 변경되지 않는다.
컴파일 후 다시 직렬화 -> 인스턴스변수 추가 -> 역직렬화를 진행하여도 에러없이 정상적으로 동작한다.
자바 I/O에 대해 공부하다가 I/O 패키지의 많은 부분들이 데코레이터 패턴을 이용하여 만들어졌다는 것을 알았다.
그래서 데코레이터 패턴이 뭐지? 하는 궁금증에 데코레이터 패턴에 대해서도 조금 공부해봤다.
데코레이터 패턴
객체에 추가적인 요건을 동적으로 첨가한다. 데코레이터는 서브클래스를 만드는 것을 통해서 기능을
유연하게 확장할 수 있는 방법을 제공한다.
당연히 정의만 봐서 무슨 소리인지 이해를 못하겠다.
예제를 보며 차분히 이해해보자.
스타버즈라는 카페가 있다. 스타버즈는 엄청난 급속도로 성장해서 다양한 음료들을 포괄하는 주문시스템을
이제서야 겨우 갖추려고 준비중이다.
처음 사업 시작 시 클래스들은 다음과 같이 구성되어 있었다.
커피를 주문할 때 스팀 우유, 두유, 모카(초코), 휘핑과 같은 토핑을 변경할 수 있는데 이런 경우
기존 구성을 어떻게 변경해야 할까?
처음 스타버즈는 이렇게 해보기로 했다.
Beverage라는 기본 클래스의 각 음료에 우유, 두유, 모카, 휘핑이 들어가는지 여부를 나타내는 인스턴스 변수를 추가
cost()를 추상클래스로 하지 않고, 구현해 놓기로 한다. 각 음료 인스턴스마다 추가 토핑에 해당하는
추가 가격까지 포함시킬 수 있도록 말이다.
와 뭔가 잘 될 것 같다! 라고 생각할 수 있지만 이 구조에는 몇 가지의 문제점이 있을 수 있다.
이런 문제점으로 인해 바로 위의 구조인 상속을 써서 음료 가격과 토핑 가격을 합한 총 가격을 계산한 방법은
그리 좋은 방법이 아니다.
스타버즈는 다음 대안으로 다음과 같이 생각해본다. 우선 특정 음료에서 시작해서, 토핑으로 그 음료를 장식(decorate)
할 것이다. 예를 들어 손님이 모카하고 휘핑을 추가한 에스프레소를 주문한다면 다음과 같다.
- Espresso 객체를 가져온다.
- Mocha 객체로 장식한다.
- Whip 객체로 장식한다.
- cost() 메소드를 호출한다. 이 때 토핑 가격을 계산하는 일은 해당 객체들에게 위임된다.
그러면 객체를 어떻게 "장식" 할 수 있을까?
1️⃣ Espresso 객체에서 시작한다.
2️⃣ 모카 토핑을 주문했으니 Mocha 객체를 만들고 그 객체로 Espresso를 감싼다.
3️⃣ 휘핑 크림도 같이 주문했기 때문에 Whip 데코레이터를 만들고 그 객체로 Mocha를 감싼다.
Whip도 데코레이터기 때문에 Espresso의 형식을 반영하고, 따라서 cost() 메소드를 가진다.
Mocha와 Whip으로 싸여 있는 Espresso는 여전히 Beverage 객체이기 때문에 cost() 메소드 호출을 비롯한
그냥 Espresso일 때와 같이 모든 행동을 할 수 있다.
4️⃣ 마지막으로 가격을 구한다. 가격을 구할 때는 가장 바깥쪽에 있는 데코레이터인 Whip의 cost()를 호출로 시작한다.
이제 실제 코드를 구현하기 전해 조금 더 이해하기 편하도록 클래스 다이어그램을 살펴보자.
Beverage beverage
이제 실제 코드를 작성하며 앞의 내용들을 더 명확하게 알아보자.
🥤 Beverage 클래스 🥤
public abstract class Beverage {
private String description = "제목없음";
public void setDescription(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
public abstract int cost();
}
🥛 ToppingDecorator 클래스 🥛
public abstract class ToppingDecorator extends Beverage {
public abstract String getDescription();
}
☕️ Espresso 클래스 (음료 클래스 구현) ☕️
public class Espresso extends Beverage {
public Espresso () {
setDescription("에스프레소");
}
@Override
public int cost() {
return 4000;
}
}
🍫 Mocha 클래스(토핑 데코레이터 클래스) 🍫
추상 구성요소 (Beverage), 구상 구성요소 (Esppreso), 추상 데코레이터(ToppingDecorator) 까지 만들었으니
마지막으로 구상 데코레이터를 구현하자.
public class Mocha extends ToppingDecorator {
Beverage beverage;
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
@Override
public int cost() {
return 1000 + beverage.cost();
}
@Override
public String getDescription() {
return beverage.getDescription() + ", 모카";
}
}
Mocha는 데코레이터 이므로 추상 데ㄹ코레이터 ToppingDecorator를 상속받는다.
Mocha 인스턴스에는 Beverage에 대한 레퍼런스가 들어있다. 이래야 감싸고자 하는 음료를 저장할 수 있다.
위에서 getDescription()을 추상메소드로 만든이유는 여기있다. "에스프레소" 만 들어있으면 어떤 첨가물이
들어있는지 알 수 없으니 ", 모카"를 덧붙여준다.
cost()는 장식하고있는 객체의 가격을 구한 뒤 그 가격에 모카를 추가한 가격을 리턴한다.
Soy, SteamMilk, Whip 클래스도 위와 동일하게 작성한다.
이제 준비가 다 됐으니 커피를 주문해보자.
🛎 실행 🛎
public class StarbuzzCoffee {
public static void main(String[] args) {
Beverage beverage = new Espresso();
System.out.println(beverage.getDescription() + " " + beverage.cost() +"원");
Beverage beverage2 =new DarkRoast();
beverage2 = new Mocha(beverage2);
beverage2 = new Mocha(beverage2);
beverage2 = new Whip(beverage2);
System.out.println(beverage2.getDescription() + " " + beverage2.cost() + "원");
Beverage beverage3 = new HouseBlend();
beverage3 = new Soy(beverage3);
beverage3 = new Mocha(beverage3);
beverage3 = new Whip(beverage3);
System.out.println(beverage3.getDescription() + " " + beverage3.cost() + "원");
}
}
// 결과
에스프레소 4000원
다크 로스트, 모카, 모카, 휘핑 7000원
하우스 블렌드, 두유, 모카, 휘핑 7200원
첫 번째 에스프레소는 아무것도 들어가지 않는 에스프레소를 주문하고,
두 번째, 세 번째 커피는 각각 토핑을 추가하여 토핑 데코레이터로 감싸서 최종 주문을 할 수 있다.
이 쯤 되니 조금 눈치를 채보면 토핑 데코레이터로 감싸는 부분을 어디서 많이 본 것도 같다?!?!
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(tempFile));
위에 공부한 자바 I/O에서 많이 본 코드이다. 몰랐지만 열심히 데코레이터를 쓰고있었다!
데코레이터 패턴도 알았으니 직접 I/O 입력 데코레이터를 만들 수 있다.
👉 입력 스트림에 있는 대문자를 전부 소문자로 바꿔주는 데코레이터를 만들자!
public class LowerCaseInputStream extends FilterInputStream {
protected LowerCaseInputStream(InputStream in) {
super(in);
}
public int read() throws IOException {
int c = super.read();
return (c == -1 ? c : Character.toLowerCase((char)c));
}
public int read(byte[] b, int offset, int len) throws IOException {
int result = super.read(b, offset, len);
for (int i = offset; i < offset+result; i++) {
b[i] = (byte)Character.toLowerCase((char)b[i]);
}
return result;
}
}
추상 데코레이터인 FilterInputStream을 상속받는다.
두 개의 read() 메소드를 구현한다. 각각 byte 값 하나, byte[] 배열을 읽고 각 byte를 검사하여
대문자이면 소문자로 변환한다.
public class InputTest {
public static void main(String[] args) {
int c;
try (LowerCaseInputStream in =
new LowerCaseInputStream(
new BufferedInputStream(
new FileInputStream("test.txt")))) {
while((c = in.read()) != -1) {
System.out.print((char)c);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
I love pizza, because I just like Pizza.
test.txt 파일의 내용이 아래와 같이 대문자 -> 소문자로 변경된 것을 확인할 수 있다.
i love pizza, because i just like pizza.
데코레이터 패턴을 이용하면 일반적인 상속관계보다 유연하게 기능을 확장할 수 있지만 너무 많은 클래스가
생긴다거나 감싼 구조가 많아지다 보면 구조 때문에 디버깅이 어려워질 수 도 있다.
필요한 부분에 적절하게 사용해야하는데 항상 적절하게? 라는 말은 어렵다🥲
I/O를 학습하다가 어쩌다보니 데코레이터 패턴도 공부하게 됐는데 패턴 공부는 처음해봐서 재밌고 신기했다.