개요
java의 모든 입출력(IO)은 Byte와 Char 둘 중 하나로 이루어진다.
입출력 기능을 사용하려면 당연히 메소드가 있어야하고, 메소드가 있으려면 당연히 클래스가 있어야 하는 법...
그렇기에 java.io 패키지에서는 데코레이터(Decorator) 패턴을 사용한 여러 입출력 클래스를 제공한다.
데코레이터 패턴이 뭔데
어댑터 패턴의 일부임.. 근데 이러면 어댑터 패턴이 뭐냐고 하겠지..? 그냥 그런 게 있다고 쳐요 우리...
어렵게 설명하자면 이런거임
- A 기능을 수행하는 0 클래스(또는 인터페이스)가 있다.
- 0 클래스를 상속받는 자식 클래스 1, 2, 3이 있고, 나는 그 1, 2, 3 클래스가 오버라이드한 A 기능을 전부, 혹은 몇 개만 골라서 쓰고 싶다.
- 그래서 '1+2', '2+3', '1+3', '1+2+3' 클래스를 만들자니 답이 없고, if문으로 분류하자니 해당 코드를 쓸 곳이 너무 많다.
- 음... 그렇다면 1, 2, 3 클래스로 만든 객체를 어딘가에 넣어놓은 다음 A 기능을 실행시킬 때 넣어둔 것이 실행되게 만들면 되는 거 아닐까?
- 그래서 나는 0 클래스를 상속받는 4 클래스를 만들고, 이 클래스가 다른 0 클래스 객체들을 담아둘 수 있게 하고, A 기능이 실행됐을 때 담아둔 객체들의 A 기능을 전부 수행하도록 구성한다. (리스트에 추가하는 기능도 있어야겠지)
- 만들어놓은 1, 2, 3 클래스 객체를 원하는대로 4 클래스 객체에 넣고, 4 클래스 객체의 A 기능을 실행시킨다
- 뭔가 똑똑해진 것 같은 기분을 느낀다.
<단순예>
// 추상 클래스나 인터페이스를 만든다
interface 배관자재
{
메소드 물_흐르게_하기();
}
// 그걸 상속받는 클래스들을 여러개 제작
// 이것들은 스스로도 동작할 수 있지만, 어딘가 나사가 빠진 느낌이라 어딘가에 붙여서 쓰는 것이 좋다
class 필터 implements 배관자재
{ 물_흐르게_하기() 물을 깨끗하게 해주는 기능 }
class 보일러 implements 배관자재
{ 물_흐르게_하기() 물을 데워주는 기능 }
class 샤워기 implements 배관자재
{ 물_흐르게_하기() 물을 흩뿌려주는 기능 }
// 상속받는 클래스들을 붙여서 쓸 수 있는 클래스를 제작
// 이것은 스스로도 동작할 수 있지만, 뭔가를 붙여서 쓰는 기능이 있다
class 수도꼭지 implements 배관자재
{
List<배관자재> 배관용품들
물_흐르게_하기()
{
for (배관용품 : 배관용품들)
배관용품.물_흐르게_하기();
}
배관자재_추가(배관자재 _자재)
{
배관용품들.add(_자재);
}
}
이후 배관자재라는 인터페이스를 상속받는 클래스를 모두 만들고, 수도꼭지에 필터, 보일러, 샤워기를 추가하면...
수도꼭지로 물을 흐르게 했을 때 다른 클래스들의 기능이 같이 발현된다!
이런 식으로 설계하면 클래스의 기능을 상황에 맞춰 축소/확장하기 편리해진다.
이 때 필터/보일러/샤워기 같은 클래스를 '장식 대상 클래스', 수도꼭지 같은 클래스를 '장식하는 클래스'라고 함.
장식 대상 클래스가 장식 대상 클래스가 될수도 있고, 반대가 될수도 있고... 붙이고 또 붙일 수도 있음! 그렇다고 모든 클래스를 장식할수도, 장식될수도 있게 만들면 디자인 패턴의 이점이 퇴색되므로 구분해서 쓰자.
Byte
모든 바이트 입력 클래스는 ~InputStream 라는 추상 클래스를 가진다.
[장식 대상 클래스]
FileInputStream
ByteArrayInputStream
[장식하는 클래스]
DataInputStream
모든 바이트 출력 클래스는 ~OutputStream 라는 이름을 가진다.
[장식 대상 클래스]
OutputStream
ByteArrayOutputStream
[장식하는 클래스]
DataOutputStream
사용법
1바이트씩 읽을 때와 512바이트씩 읽을 때 드는 시간이 다르다. 당연한거지만...
<기초>
public static void main(String[] args)
{
long start = System.currentTimeMillis();
FileInputStream fis = null; // 바이트단위 파일 읽기맨
FileOutputStream fos = null; // 바이트단위 파일 쓰기맨
try
{
fis = new FileInputStream("src/test_proj_2/Main.java"); // 어떤 걸 읽을건지 좌표를 정해서 객체를 만든다
fos = new FileOutputStream("byte.txt"); // 어디에 쓸건지 좌표를 정해서 객체를 만든다
int readCount = -1; // 얼마나 읽었나를 알려주는 지표
// fis.read(...)는 더 읽을 게 없으면 -1을 반환한다. 이걸 이용하면 아래와 같이 코드 작성이 가능함
/*
while( (readCount = fis.read()) != -1 )
{
// 1 바이트씩 읽어서 '써 줘'
fos.write(buffer);
}
*/
byte[] buffer = new byte[512];
while( (readCount = fis.read(buffer)) != -1 )
{
// (win OS 에서의 파일 읽기 기본 속도)
// 512 바이트씩 읽어서 '써 줘'
fos.write(buffer, 0, readCount);
}
}
catch (Exception e) // 입출력은 예외가 많아서 걍 Exception으로 퉁쳤음
{
e.printStackTrace();
}
finally
{
try
{
// 파일을 안 닫아주면 예기치 못한 문제가 생길 수 있으므로 꼭 닫아주기
fos.close();
fis.close();
}
catch (Exception e)
{
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
<활용>
데코레이터 패턴이 사용된 DataOutputStream을 활용하면 값형 자료들(int, boolean, float 등)을 저장할 수 있다.
public static void main(String[] args)
{
// try ( 변수 ) 에서, 변수는 try문이 끝나면 자동으로 해제됨
// 확장자는 그냥 마음대로 설정
// 데코레이터 패턴이 사용된 쓰기맨 생성
try (DataOutputStream out = new DataOutputStream(new FileOutputStream("data.dat")))
{
// 데이터 쓰기
out.writeInt(123456789);
out.writeBoolean(true);
out.writeDouble(10.5D);
}
catch (Exception e) // 입출력은 예외가 많아서 걍 Exception으로 퉁쳤음
{
e.printStackTrace();
}
// 읽기맨 생성
try (DataInputStream in = new DataInputStream(new FileInputStream("data.dat")))
{
// 데이터 읽기 (순서 맞춰서 읽을 것)
int i = in.readInt(); // 현재 읽고 있는 부분에서 4byte 만큼 읽는다. 그 후 읽는 부분이 4byte만큼 뒤로 간다.
boolean b = in.readBoolean(); // 1byte
double d = in.readDouble(); // 8byte
// 출력해보기
System.out.println(i);
System.out.println(b);
System.out.println(d);
}
catch (Exception e) // 입출력은 예외가 많아서 걍 Exception으로 퉁쳤음
{
e.printStackTrace();
}
}
Char
모든 문자 입력 클래스는 ~Reader 라는 이름을 가진다.
[장식 대상 클래스]
FileReader
CharReader
[장식하는 클래스]
BufferedReader
모든 문자 출력 클래스는 ~Writer 라는 이름을 가진다.
[장식 대상 클래스]
FileWriter
CharWriter
[장식하는 클래스]
PrintWriter
사용법
<키보드 입력 받기>
public static void main(String[] args)
{
// System.in은 C언어의 scanf 같은 녀석
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String str = null;
try
{
// 한 줄 읽는다. 이 시점에서 System.in이 한 줄을 쓸 때까지 기다려준다.
str = br.readLine();
}
catch (Exception e)
{
e.printStackTrace();
}
System.out.println(str);
}
<파일에 쓰기>
public static void main(String[] args)
{
BufferedReader br = null;
PrintWriter pw = null;
try
{
// 읽어들일 파일의 위치를 지정해주기
br = new BufferedReader(new FileReader("src/test_proj_2/Main5.java"));
// 굳이 FileWriter를 쓰지 않고 바로 File 인자값을 넣어도 OK
pw = new PrintWriter(new FileWriter("test.txt"));
String line = null;
// 한 줄씩 차례대로 읽는다. readLine()은 읽을 게 없으면 null을 반환한다. String은 참조형이라구!
while ((line = br.readLine()) != null)
{
pw.println(line);
}
// printwriter를 닫아주지 않으면 세이브가 안되므로 주의!
pw.close();
}
catch (Exception e)
{
e.printStackTrace();
}
}'프로그래밍 > Java' 카테고리의 다른 글
| 컬렉션 프레임워크 (0) | 2024.02.12 |
|---|---|
| 메모용 - Java 8~14 버전까지 (0) | 2024.02.12 |
| 스레드 ( Thread ) (0) | 2022.02.11 |
| 어노테이션 ( Annotation ) (0) | 2022.02.10 |
| java.time (0) | 2022.02.10 |