
가장 먼저
입출력이 무엇인지부터 정확하게 잡아야 한다.
그래야 뒤에서 나오는 파일 읽기, 파일 쓰기,URL읽기를 따로 외우지 않고 하나의 흐름으로 이해할 수 있다.
처음 배우면InputStream,OutputStream,Reader,Writer처럼 이름이 많아서 어렵게 느껴진다.
하지만 이 단계에서 가장 먼저 봐야 하는 것은 클래스 이름이 아니다.
데이터가 어디서 들어오고, 어디로 나가는지를 먼저 이해해야 한다.
예를 들어 키보드에서 값을 입력받으면 바깥의 데이터가 프로그램 안으로 들어온다.
이것은입력이다.
반대로 프로그램이 화면에 결과를 보여 주면 프로그램 안의 데이터가 바깥으로 나간다.
이것은출력이다.
즉 이 단원은 새로운 문법을 많이 외우는 단원이 아니다.
데이터 이동 방향을 기준으로 클래스를 읽는 단원이라고 보면 된다.
입출력이란
입출력은 프로그램이 바깥과 데이터를 주고받는 일이다.
여기서 바깥이라는 말은 아주 넓게 보면 된다.
사용자, 키보드, 화면, 파일, 웹 주소 같은 대상이 전부 바깥에 해당한다.
예를 들어 사용자가 키보드로A를 입력하면 프로그램은 그 값을 받아들인다.
이것은 바깥에서 안으로 들어오는 흐름이므로입력이다.
반대로 프로그램이"결과는 10입니다"라는 문장을 화면에 보여 주면, 이것은 안에서 바깥으로 나가는 흐름이므로출력이다.
파일도 같은 기준으로 보면 된다.
파일 내용을 프로그램이 읽으면입력이다.
프로그램이 만든 내용을 파일에 저장하면출력이다.
URL도 마찬가지다.
웹 주소에 있는 데이터를 읽으면 그것도 결국 바깥 데이터를 읽어 오는 것이므로입력이다.
여기서 중요한 점은 겉모습은 달라도 본질은 같다는 것이다.
키보드 입력, 파일 읽기, 웹 데이터 읽기는 전부 바깥 데이터를 프로그램 안으로 가져오는 일이다.
반대로 화면 출력, 파일 저장은 전부 프로그램 안의 데이터를 바깥으로 내보내는 일이다.
입출력은 대상이 달라질 뿐, 데이터가 움직이는 방향으로 보면 하나의 개념으로 묶인다.
먼저 아주 짧게 정리하면
- 키보드에서 값 받기 →
입력- 파일 내용 읽기 →
입력- 화면에 결과 보여 주기 →
출력- 파일에 내용 저장하기 →
출력즉 이 단원은 키보드 예제, 파일 예제,
URL예제를 따로 외우기보다, 데이터가 들어오는지 나가는지로 먼저 생각하는 습관이 중요하다.
스트림이란
스트림은 데이터를 옮겨 주는 통로다.
입출력에서는 데이터가 갑자기 순간이동하듯 나타나거나 사라지지 않는다.
항상 어떤 길을 따라 들어오거나 나간다.
그 길을스트림이라고 부른다.
초보자는 여기서스트림이라는 단어가 어렵게 느껴질 수 있다.
하지만 이 단계에서는 데이터가 지나가는 길, 또는 데이터가 흐르는 통로라고 이해하면 충분하다.
예를 들어 파일에서 글자를 읽는다고 해 보자.
파일 속 글자가 바로 프로그램 변수 안으로 들어오는 것이 아니다.
파일과 프로그램 사이에 데이터를 전달하는 연결이 필요하다.
그 연결이 바로스트림이다.
여기서 꼭 기억할 점
스트림은 기본적으로 한 방향으로 흐른다.- 읽는 길과 쓰는 길은 따로 생각해야 한다.
- 하나의 입력용
스트림으로 읽기와 쓰기를 동시에 처리한다고 이해하면 안 된다.이 부분은 이름을 보면 더 잘 보인다.
InputStream은 들어오는 쪽이다.
OutputStream은 나가는 쪽이다.
Reader는 읽는 쪽이다.
Writer는 쓰는 쪽이다.
즉 이름 안에 이미 방향이 들어 있다.
이 단계에서는 이렇게 이해하면 된다
스트림= 데이터가 흐르는 통로입력 스트림= 프로그램 안으로 들어오는 통로출력 스트림= 프로그램 밖으로 나가는 통로이 기준이 잡혀 있어야 뒤에 나오는
InputStream,FileReader,BufferedReader,FileWriter도 자연스럽게 읽힌다.
입력스트림과 출력스트림
바깥에서 프로그램 안으로 데이터가 들어오면
입력이다.
프로그램 안에서 바깥으로 데이터가 나가면출력이다.
이 차이는 아주 단순해 보이지만, 입출력 전체를 이해하는 기준이 된다.
예를 들어 키보드에서 글자를 눌러 프로그램이 읽는다면입력이다.
반대로 프로그램이 화면에 글자를 보여 주면출력이다.
파일도 마찬가지다.
파일 내용을 읽으면입력이고, 파일에 내용을 저장하면출력이다.
입력 스트림은 데이터를 프로그램 안으로 가져오고,출력 스트림은 데이터를 프로그램 바깥으로 내보낸다.
그림을 볼 때 체크할 포인트
- 화살표가 프로그램 안쪽으로 향하면
입력- 화살표가 프로그램 바깥쪽으로 향하면
출력- 결국 중요한 것은 데이터의 이동 방향
이 방향을 한 번 정확하게 잡고 나면 뒤에 나오는
InputStream,OutputStream,Reader,Writer도 이름이 훨씬 쉽게 읽힌다.
이름 안에 이미 역할이 들어 있기 때문이다.
즉 이 단계에서는 클래스를 외우기보다 먼저 이렇게 정리하면 된다.
들어오면입력, 나가면출력이다.
이 기준이 흔들리지 않으면 이후 예제도 훨씬 수월하게 읽힌다.
표준 입출력과 System.in
자바는 자주 쓰는 기본 입출력 통로를 미리 준비해 둔다.
그 대표가System.in,System.out,System.err다.
System.in은 표준 입력이다.
보통 키보드 입력과 연결해서 생각하면 된다.
System.out은 표준 출력이다.
보통 화면에 결과를 보여 줄 때 사용한다.
System.err는 오류 메시지를 보여 줄 때 많이 쓰는 출력 통로다.
System.in은 프로그램이 입력을 받는 기본 통로이고,System.out,System.err는 프로그램이 결과나 오류를 내보내는 기본 통로다.
이름을 읽는 방법도 같이 익혀 두면 좋다
in→ 들어오는 쪽out→ 나가는 쪽err→ 오류를 보여 주는 출력 쪽즉 키보드 입력과 콘솔 출력도 특별한 별도 개념이 아니다.
입출력의 가장 기본적인 형태라고 보면 된다.
뒤에서 파일을 읽고 쓸 때도 결국 같은 흐름을 다른 대상에 적용하는 것뿐이다.
System.in은 무엇인가
예제에 들어가기 전에
System.in부터 먼저 정확하게 잡고 가야 한다.
이 객체가 바로 뒤 예제의 중심이기 때문이다.
System.in은 자바가 기본으로 제공하는 표준 입력 통로다.
쉽게 말하면 프로그램이 바깥에서 값을 받아올 때 가장 먼저 만나는 기본 입력 창구라고 생각하면 된다.
보통은 키보드 입력과 연결해서 이해하면 가장 쉽다.
즉 사용자가 키보드로 무언가를 누르면, 프로그램은 그 값을System.in이라는 통로를 통해 읽어 온다.
그래서System.in.read()라는 코드는 결국 기본 입력 통로에서 값을 하나 꺼내 오는 코드라고 이해하면 된다.
다만 여기서 정확히 구분해야 할 점이 있다.
System.in은InputStream계열이므로,System.in.read()는 문자 하나를 완벽하게 읽는다기보다 바이트 값 하나를 읽는 가장 기본적인 형태에 가깝다.
영문A처럼 한 바이트로 표현되는 문자는(char)로 바꿔 확인하기 쉽다.
하지만 한글처럼 여러 바이트로 표현되는 문자는 이 방식만으로 한 글자를 자연스럽게 읽기 어렵다.
그래서 뒤 예제처럼InputStreamReader를 붙여 문자 기준으로 읽는 흐름이 필요하다.
System.in을 볼 때 먼저 떠올리면 좋은 것
- 자바가 기본으로 준비해 둔 입력 통로다.
- 보통 키보드 입력과 연결해서 이해하면 된다.
System.in.read()는 바이트 값을 하나 읽는 기본 형태다.- 문자 기준으로 읽고 싶으면
InputStreamReader를 붙이는 흐름으로 이어진다.즉 예제 1은 새로운 입력 통로를 만드는 예제가 아니라, 이미 준비된 기본 입력 통로를 가장 직접적으로 사용하는 예제다.
InputStreamReader는 왜 필요한가
이제
InputStreamReader를 봐야 한다.
이 객체는 예제 2의 중심이다.
System.in은 기본적으로 바이트 입력 스트림이다.
그런데 우리가 실제로 보고 싶은 것은 글자인 경우가 많다.
특히 한글처럼 문자 단위로 다루고 싶은 경우에는 바이트 그대로 보는 것보다, 문자로 해석해서 읽는 것이 훨씬 자연스럽다.
이때 사용하는 것이InputStreamReader다.
이 클래스는 바이트 입력을 문자 입력으로 바꿔 주는 연결 도구다.
쉽게 말하면, 기계가 읽는 쪽에 가까운 입력을 사람이 읽는 글자 쪽으로 조금 더 가까이 옮겨 주는 다리라고 생각하면 된다.
그래서 예제 2에서는System.in을 바로 읽지 않고,InputStreamReader(System.in)처럼 한 번 감싸서 사용한다.
이렇게 하면 입력을 문자 기준으로 더 읽기 쉬운 형태로 바꿔서 볼 수 있다.
InputStreamReader를 붙이는 이유를 짧게 정리하면
System.in은 기본 입력 통로다.- 그런데 그대로 보면 바이트 입력에 더 가깝다.
- 우리는 글자를 읽고 싶은 경우가 많다.
- 그래서
InputStreamReader로 문자 기준 해석 단계를 하나 더 붙인다.즉
InputStreamReader는 예제 1의 구조를 버리는 것이 아니라, 예제 1의 입력 구조 위에 문자 해석 단계를 하나 더 올리는 역할을 한다.
read()라는 이름을 볼 때 주의할 점
입출력 예제에서는
read()라는 이름이 계속 나온다.
하지만 이름이 같다고 해서 항상 같은 단위로 읽는 것은 아니다.
어떤 스트림에서 호출하느냐에 따라 읽는 기준이 달라진다.
InputStream.read()는 바이트 하나를 읽는다.
대표적으로System.in.read()나URL이미지 읽기에서 사용한다.
반대로Reader.read()는 문자 하나를 읽는다.
대표적으로FileReader.read()나InputStreamReader.read()에서 사용한다.
둘 다 반환값은int다.
그 이유는 실제 값뿐 아니라 끝을 의미하는-1도 함께 표현해야 하기 때문이다.
char나byte만으로는 “더 이상 읽을 데이터가 없다”는 특별한 상태까지 함께 표현하기 어렵다.
짧게 비교하면
InputStream.read()→ 바이트 하나를 읽는다.Reader.read()→ 문자 하나를 읽는다.- 둘 다 끝에 도달하면
-1을 반환한다.- 둘 다 반환 타입은
int다.이 구분을 잡아 두면
System.in.read(),FileReader.read(),URL이미지 읽기 코드가 한꺼번에 나와도 덜 헷갈린다.
예제 1. 표준 입력의 가장 기본 구조
먼저 가장 단순한 입력 예제부터 본다.
이 예제는 키보드 입력도 결국스트림으로 처리된다는 점을 보여 준다.
코드
// StandardInputTest.java package day10; public class StandardInputTest { public static void main(String[] args) throws Exception { System.out.println("입력: "); // 입력 안내 int input = System.in.read(); // 기본 입력 통로에서 바이트 값 하나 읽기 System.out.println("입력된 문자: " + (char) input); // 문자로 변환해서 출력 } }출력 결과
이 코드에서 가장 먼저 봐야 할 부분은
System.in.read()다.
이 메서드는 키보드에서 들어온 값을 하나 읽는다.
즉 키보드 입력도 결국 어떤 통로를 통해 들어온다는 뜻이다.
그 통로가 바로System.in이다.
그런데 여기서 초보자가 자주 멈추는 부분이 있다.
문자 하나를 읽는 것처럼 보이는데, 왜 결과를char가 아니라int에 저장하느냐는 점이다.
이유는read()가 실제 값뿐 아니라, 더 이상 읽을 값이 없을 때를 나타내는 특별한 값도 함께 표현해야 하기 때문이다.
그 값이-1이다.
char는 이런 값을 표현하기 어렵기 때문에int를 반환한다.
즉read()는 문자처럼 보이는 것을 읽더라도, 내부에서는 먼저 숫자 값으로 돌려준다고 이해하면 된다.
우리는 그 숫자를(char)로 바꿔서 사람이 읽는 문자처럼 확인하는 것이다.
이 예제에서 꼭 잡아야 할 핵심
System.in은 입력 통로다.System.in.read()는 가장 기본적으로 바이트 값 하나를 읽는다.- 반환값은
char가 아니라int다.- 출력할 때는
(char)로 바꿔서 확인할 수 있다.이 단계에서는
read()는 문자처럼 보여도 실제로는 숫자 값을 반환하고, 필요하면 우리가 문자로 바꿔서 본다고 이해하면 된다.
또 이 예제는 키보드 입력도 결국 특별한 별도 문법이 아니라, 기본 입력 통로에서 값을 하나 읽어 오는 동작이라는 점을 보여 준다.
즉 표준 입력의 가장 원초적인 모습이라고 보면 된다.
예제 2. 바이트 입력을 문자 입력으로 바꾸기
이번에는
InputStreamReader를 붙인 예제를 본다.
코드
// StandardInputTest2.java package day10; import java.io.InputStreamReader; public class StandardInputTest2 { public static void main(String[] args) throws Exception { System.out.println("입력: "); // 입력 안내 int input = new InputStreamReader(System.in).read(); // 문자 기준으로 읽기 System.out.println("입력된 문자: " + (char) input); // 문자 출력 } }출력 결과
이 예제는
System.in에InputStreamReader를 붙인 구조다.
즉 기본 입력 통로는 그대로 두고, 그 입력을 문자 기준으로 읽기 쉬운 형태로 한 번 더 바꾼 것이다.
앞에서 본 것처럼System.in은 기본적으로 바이트 입력 스트림이다.
그런데 우리가 실제로 보고 싶은 것은 글자인 경우가 많다.
특히 한글처럼 문자 단위로 다루고 싶은 경우에는 바이트 그대로 보는 것보다 문자로 해석해서 보는 것이 자연스럽다.
이때 중간에서 연결 역할을 하는 것이InputStreamReader다.
이 클래스는 바이트 입력을 문자 입력으로 바꿔 주는 다리라고 생각하면 된다.
즉 첫 번째 예제보다 한 단계 더 사람이 읽기 쉬운 방향으로 간 것이다.
코드 흐름을 순서대로 보면
System.in으로 입력을 받는다.InputStreamReader가 그 입력을 문자 기준으로 바꿔 준다.read()로 문자 하나를 읽는다.- 읽은 값을 출력해서 확인한다.
첫 번째 예제는 입력도 결국
스트림이라는 점을 보여 준다.
두 번째 예제는 그 스트림을 문자 기준으로 더 읽기 쉽게 바꿀 수도 있다는 점을 보여 준다.
즉 두 예제를 이어서 보면 이런 흐름이 보인다.
System.in으로 입력을 받는다.
필요하면InputStreamReader를 붙여서 문자처럼 읽는다.
이것이 뒤에서 파일과URL을 읽을 때도 계속 반복되는 기본 생각이다.
예제 1과 예제 2를 비교하면
- 예제 1은 기본 입력 통로에서 바이트 값을 바로 읽는 구조다.
- 예제 2는 그 입력을 문자 기준으로 더 읽기 쉽게 바꾼 구조다.
- 즉 두 번째 예제는 첫 번째 예제를 버리는 것이 아니라, 첫 번째 구조 위에 해석 단계를 하나 더 올린 것이다.
여기까지 이해하면 첫 번째 큰 그림이 잡힌다.
입력은 어떤 통로를 통해 들어오고, 필요하면 더 읽기 쉬운 형태로 바꿔서 사용한다.
앞에서는
입출력이 무엇인지, 그리고 데이터가 들어오고 나가는 방향이 왜 중요한지부터 먼저 봤다.
이제부터는 그 데이터를 실제로 어떤 종류의 통로로 다루는지 구분해야 한다.
입출력 예제를 보다 보면InputStream,OutputStream,Reader,Writer,BufferedReader,PrintWriter처럼 이름이 많이 나온다.
초보자가 여기서 가장 자주 헷갈리는 부분은, 이 클래스들이 전부 비슷해 보인다는 점이다.
하지만 실제로는 다루는 데이터 단위도 다르고, 맡는 역할도 다르다.
즉 이 단계에서는 클래스 이름이 낯설다고만 보기보다, 무엇을 어떤 단위로 다루는지, 실제 대상과 직접 연결되는지, 기존 통로를 도와주는 역할인지를 나눠서 보는 것이 중요하다.
이 기준이 잡혀 있어야 뒤에서FileReader,FileWriter,BufferedReader,InputStreamReader,PrintWriter예제가 훨씬 쉽게 읽힌다.
바이트 스트림과 문자 스트림
스트림은 크게
바이트 스트림과문자 스트림으로 나눌 수 있다.
이 둘은 비슷해 보이지만, 데이터를 다루는 기준이 다르다.
바이트 스트림은 데이터를 바이트 단위로 다룬다.
쉽게 말하면 원본 데이터에 더 가까운 방식이다.
이미지, 동영상, 실행 파일처럼 글자가 아니라 원래 데이터 모양 자체가 중요한 경우에 잘 맞는다.
문자 스트림은 데이터를 문자 단위로 다룬다.
즉 사람이 읽는 글자를 기준으로 데이터를 다루는 방식이다.
텍스트 파일처럼 문장이나 글자가 중요한 경우에는 이쪽이 더 자연스럽다.
먼저 느낌으로 구분하면
- 글자를 읽고 쓰는 일이 중요하다 →
문자 스트림- 원본 데이터 자체를 그대로 다뤄야 한다 →
바이트 스트림예를 들어 메모장 파일 안에 들어 있는 문장을 읽고 싶다면, 우리는 글자를 보고 싶은 것이다.
이럴 때는Reader,Writer계열이 더 잘 맞는다.
반대로 이미지 파일을 복사하거나, 웹에서 그림 파일을 받아 저장하려면 글자가 아니라 원본 데이터가 중요하다.
이럴 때는InputStream,OutputStream계열이 더 잘 맞는다.
파일과 연결된
입력 스트림은 데이터를 읽어 오고,출력 스트림은 데이터를 파일로 내보낸다.
이름으로 읽는 법도 같이 보면 좋다
InputStream,OutputStream→바이트 스트림Reader,Writer→문자 스트림여기서 중요한 것은 무조건 어느 클래스가 더 좋다는 것이 아니다.
어떤 데이터를 다루는지에 따라 맞는 스트림 종류가 달라진다.
즉 텍스트는 보통문자 스트림, 원본 데이터는 보통바이트 스트림으로 이해하면 가장 쉽다.
기반 스트림과 보조 스트림
여기서부터는 구분 기준이 바뀐다.
앞의바이트 스트림과문자 스트림은 무엇을 어떤 단위로 다루는가에 대한 구분이었다.
반면 지금부터 보는기반 스트림과보조 스트림은 어떤 역할을 맡는가에 대한 구분이다.
즉바이트/문자와기반/보조는 같은 기준으로 나눈 것이 아니다.
서로 다른 기준으로 나눈 것이다.
이 점을 먼저 잡고 가야 헷갈리지 않는다.
실제 대상과 직접 연결되는 스트림을기반 스트림이라고 한다.
쉽게 말하면 진짜로 파일이나 입력 대상과 바로 연결되는 기본 통로다.
예를 들어FileInputStream,FileOutputStream,FileReader,FileWriter가 여기에 해당한다.
반대로 기존 스트림을 감싸서 기능을 더해 주는 스트림을보조 스트림이라고 한다.
쉽게 말하면 이미 있는 통로를 더 편하게 쓰도록 도와주는 보조 도구다.
예를 들어BufferedReader,InputStreamReader가 여기에 해당한다.
PrintWriter도 기존Writer위에 붙여 출력 기능을 편하게 만들 수 있지만, 파일 경로를 직접 받는 생성자도 있어서 코드에서는 단독처럼 보일 수 있다.
이 차이를 아주 짧게 정리하면
기반 스트림→ 실제 대상과 직접 연결되는 기본 통로보조 스트림→ 기존 통로를 감싸서 기능을 더하는 도구여기서 꼭 분명하게 알아야 할 점이 있다.
대부분의 보조 스트림은 먼저 만들어 둔 기존 스트림 위에 붙어서 기능을 더한다.
다만PrintWriter처럼 파일 경로를 직접 받는 생성자를 제공해 내부적으로 출력 통로를 준비해 주는 클래스도 있다.
즉 보조 스트림을 볼 때는 단순히 생성자 모양만 보지 말고, 기존 입출력 흐름을 더 편하게 만드는 역할을 한다는 점을 중심으로 이해해야 한다.
그래서 코드에서도new BufferedReader(reader)처럼, 먼저 만든 스트림을 괄호 안에 넣는 구조가 자주 나온다.
이 코드는BufferedReader가 스스로 파일과 연결되는 것이 아니라, 이미 연결되어 있는reader위에 올라타는 모습이라고 보면 된다.
보조 스트림은 직접 파일과 연결되기보다, 기존 스트림을 감싸서 기능을 더한다.
예를 들어BufferedReader는 한 줄씩 읽는readLine()같은 편한 기능을 제공한다.
하지만 혼자서 파일과 직접 연결되는 것이 아니라, 먼저 만들어 둔Reader를 감싸서 사용한다.
InputStreamReader도 비슷하다.
기본 입력 통로나 바이트 입력 스트림 위에 붙어서, 그 입력을 문자 기준으로 읽을 수 있게 도와준다.
즉 이 부분에서 꼭 잡아야 할 것은 이것이다.
기반 스트림은 출발점이고, 보조 스트림은 그 출발점 위에 기능을 덧붙이는 구조다.
어떤 스트림을 언제 써야 하는가
여기까지 보면 이제 기준을 하나 세울 수 있다.
스트림을 볼 때는 무조건 이름을 외우려고 하지 말고, 아래 순서대로 생각하면 된다.
먼저 1단계
- 지금 다루는 대상이 글자인가
- 아니면 원본 데이터 자체인가
글자가 중요하면
Reader,Writer쪽으로 간다.
원본 데이터 자체가 중요하면InputStream,OutputStream쪽으로 간다.
다음 2단계
- 실제 대상과 직접 연결해야 하는가
- 아니면 기존 스트림 위에 기능을 더해야 하는가
직접 연결이 필요하면
기반 스트림이다.
기능을 더하고 싶으면보조 스트림을 붙인다.
실제로는 이렇게 생각하면 편하다
- 텍스트 파일 읽기 →
FileReader- 텍스트 파일 한 줄씩 읽기 →
FileReader+BufferedReader- 키보드 입력을 문자처럼 읽기 →
System.in+InputStreamReader- 텍스트 파일에 편하게 출력하기 →
FileWriter+PrintWriter- 이미지 파일 저장하기 →
InputStream+OutputStream즉 흐름은 보통 이렇다.
먼저 실제 대상과 연결되는기반 스트림을 만든다.
그다음 그대로 쓰기 불편하면보조 스트림을 붙인다.
이 기준만 잡혀 있어도 뒤 예제들이 훨씬 쉬워진다.
FileReader는 왜Reader인지,BufferedReader는 왜 따로 붙는지,PrintWriter는 왜FileWriter와 함께 쓰는지가 자연스럽게 보이기 때문이다.
이 파트의 핵심만 다시 정리하면
바이트 스트림과문자 스트림은 데이터를 어떤 단위로 다루는지에 대한 구분이다.기반 스트림과보조 스트림은 어떤 역할을 맡는지에 대한 구분이다.- 텍스트를 다루면 보통
문자 스트림을 먼저 떠올린다.- 원본 데이터를 다루면 보통
바이트 스트림을 먼저 떠올린다.- 실제 대상과 직접 연결되는 것은
기반 스트림이다.- 기존 통로를 감싸서 기능을 더하는 것은
보조 스트림이다.이 기준이 잡히면, 다음 파일 읽기 예제에서 왜
FileReader가 나오고, 왜 다시BufferedReader를 붙이는지도 훨씬 쉽게 이해할 수 있다.
이제 입력 대상을 키보드에서 파일로 옮겨서 본다.
앞에서는System.in으로 바깥의 값을 읽었다면, 여기서는 파일 안에 들어 있는 글자를 읽는다고 생각하면 된다.
이 파트에서 가장 먼저 잡아야 하는 것은FileReader와BufferedReader다.
FileReader가 파일과 연결되는 기본 통로라면,BufferedReader는 그 통로를 더 편하게 쓰도록 도와주는 도구다.
이 둘의 역할이 먼저 머리에 들어와야 뒤 예제들이 자연스럽게 읽힌다.
그리고 여기서는 같은 파일 읽기라도 읽는 단위가 어떻게 달라지는지, 어떤 도구가 왜 추가되는지, 마지막에 자원 정리 방식이 어떻게 더 안전해지는지를 같이 봐야 한다.
처음에는 문자 하나씩 읽고, 그다음에는 한 줄씩 읽고, 마지막에는 닫는 방식까지 더 깔끔해지는 흐름으로 이해하면 된다.
FileReader는 무엇을 하는가
FileReader는 텍스트 파일을 문자 단위로 읽는 클래스다.
이름을 그대로 읽으면File은 파일이고,Reader는 읽는 쪽이다.
즉 파일과 연결해서 글자를 읽는 통로라고 보면 된다.
여기서 중요한 점은 파일을 그냥 연다가 아니라, 파일과 프로그램 사이에 문자 입력 통로를 만든다는 점이다.
프로그램은 그 통로를 통해 파일 안의 글자를 하나씩 받아온다.
핵심만 정리하면
FileReader는 텍스트 파일을 읽기 위한 클래스다.- 글자를 읽는 쪽이므로
Reader계열이다.- 파일과 프로그램 사이에 문자 입력 통로를 만든다.
즉 텍스트 파일을 읽을 때는 보통
FileReader가 가장 기본 출발점이 된다.
read()와 끝 표시 -1
파일을 가장 기본적으로 읽는 방식은
read()를 반복하는 것이다.
FileReader.read()는 문자 하나씩 읽는 흐름으로 보면 된다.
그런데 여기서 꼭 기억해야 할 것이 있다.
파일 끝까지 다 읽으면read()는 더 이상 읽을 것이 없다는 뜻으로-1을 반환한다.
즉 무한히 읽는 것이 아니라, 끝을 만나면 멈춰야 한다.
기본 흐름은 단순하다
FileReader를 만든다.read()로 값을 하나 읽는다.-1이면 끝난다.- 끝나면 닫는다.
이 흐름이 파일 읽기의 가장 기본 뼈대다.
뒤에서 한 줄씩 읽는 예제가 나와도, 결국은 이 기본 흐름 위에서 더 편하게 바뀌는 것이라고 보면 된다.
BufferedReader는 무엇인가
이제
BufferedReader를 봐야 한다.
이 클래스는 뒤 예제에서 계속 나오는데, 예제 전에 역할을 먼저 알고 가는 것이 훨씬 이해하기 쉽다.
BufferedReader는 문자 입력 스트림을 더 편하게 읽도록 도와주는보조 스트림이다.
즉 파일과 직접 연결되는 출발점이라기보다, 이미 만들어 둔Reader위에 붙어서 기능을 더해 주는 쪽이라고 보면 된다.
BufferedReader의Buffered는 임시 저장 공간을 사용한다는 뜻으로 이해하면 된다.
파일에서 글자를 하나 읽을 때마다 매번 파일에 직접 접근하면 비효율적일 수 있다.
그래서 어느 정도 모아 두고 읽으면 더 효율적으로 처리할 수 있다.
또BufferedReader는readLine()을 제공해서 파일을 한 줄씩 읽는 작업도 편하게 만들어 준다.
여기서 꼭 잡아야 할 것
BufferedReader는보조 스트림이다.- 혼자서 바로 파일과 연결되지 않는다.
- 먼저 만들어 둔
Reader위에 붙는다.- 임시 저장 공간을 사용해 읽기 효율을 높인다.
readLine()으로 한 줄씩 읽을 수 있게 해 준다.즉
FileReader가 파일과 연결되는 기본 통로라면,BufferedReader는 그 통로를 더 편하게 쓰게 도와주는 도구라고 이해하면 된다.
왜 BufferedReader를 붙이는가
문자 하나씩 읽는 방식은 구조를 이해하기에는 좋다.
하지만 실제 텍스트 파일을 다룰 때는 한 줄씩 읽는 경우가 훨씬 많다.
문장도 줄로 보이고, 설정 파일도 줄로 끊기고, 데이터도 줄 기준으로 보는 경우가 많기 때문이다.
이때BufferedReader를 붙이면readLine()을 사용할 수 있다.
즉 파일을 문자 하나씩 처리하는 단계에서, 한 줄 전체를 한 번에 다루는 단계로 올라갈 수 있다.
여기서 바뀌는 점
- 문자 하나씩 읽기 →
read()- 한 줄씩 읽기 →
readLine()- 끝 확인 방식도 달라짐 →
-1이 아니라null즉 이 부분에서 바뀌는 것은 메서드 이름만이 아니다.
읽는 단위도 바뀌고, 끝을 확인하는 방식도 함께 바뀐다.
예제 3. 문자 하나씩 읽는 가장 기본 구조
먼저 가장 기본적인 파일 읽기 예제를 본다.
이 예제는 파일을 문자 하나씩 읽는 구조가 어떻게 생겼는지를 보여 준다.
코드
// FileReaderTest.java package day10; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; public class FileReaderTest { public static void main(String args[]) { FileReader reader = null; // 파일 읽기 통로 try { reader = new FileReader("./iotest/output.txt"); // 파일과 연결 while (true) { int data = reader.read(); // 문자 하나 읽기 if (data == -1) // 끝이면 종료 break; char ch = (char) data; // 문자로 변환 System.out.print(ch); // 출력 } } catch (FileNotFoundException fnfe) { System.out.println("파일이 존재하지 않습니다."); } catch (IOException ioe) { System.out.println("파일을 읽을 수 없습니다."); } finally { try { if (reader != null) reader.close(); // 사용 후 닫기 } catch (Exception e) { e.printStackTrace(); } } } }출력 결과
여기서 꼭 봐야 할 핵심
new FileReader("./iotest/output.txt")→ 파일과 연결한다.reader.read()→ 문자 하나를 읽는다.if (data == -1)→ 파일 끝인지 검사한다.reader.close()→ 사용 후 닫는다.이 예제는 빠르거나 편한 예제가 아니라, 파일 읽기의 기본 뼈대를 가장 직접적으로 보여 주는 예제라고 보면 된다.
보충 예제 4. FileReader 위에 BufferedReader가 붙는 구조
FileReaderTest2_1은FileReader위에BufferedReader가 붙는 모습을 먼저 보여 주는 연결 예제로 보면 된다.
즉 이 예제는 새로운 개념을 더 배우는 예제라기보다, 기반 스트림 위에 보조 스트림이 어떻게 올라가는지를 눈에 익히는 용도에 가깝다.
코드
// FileReaderTest2_1.java package day10; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; public class FileReaderTest2_1 { public static void main(String args[]) { try (FileReader reader = new FileReader("./iotest/output.txt"); BufferedReader br = new BufferedReader(reader)) { String data; // 읽은 줄 저장 while (true) { data = br.readLine(); // 한 줄 읽기 if (data == null) // 끝이면 종료 break; System.out.println(data); // 읽은 한 줄 출력 } } catch (IOException e) { System.out.println("파일을 읽을 수 없습니다."); } } }이 예제에서 먼저 볼 부분
FileReader가 먼저 파일과 연결된다.- 그 위에
BufferedReader가 붙는다.BufferedReader가 붙었기 때문에readLine()으로 한 줄씩 읽을 수 있다.이 예제에서 가장 먼저 볼 것은 세부 코드보다 연결 구조다.
즉BufferedReader는 따로 떨어진 독립 통로가 아니라, 기존Reader를 감싸서 기능을 더하는 보조 스트림이라는 점을 확인하면 된다.
흐름으로 쓰면 아래와 같다.
파일→FileReader→BufferedReader→readLine()
예제 5. 한 줄씩 읽는 기본 구조
이제 실제로
BufferedReader를 붙여서 한 줄씩 읽는 예제를 본다.
여기서는 앞의 문자 하나 읽기보다 훨씬 실전적인 구조가 나온다.
코드
// FileReaderTest2.java package day10; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; public class FileReaderTest2 { public static void main(String args[]) { FileReader reader = null; // 기반 스트림 BufferedReader br = null; // 보조 스트림 try { reader = new FileReader("./iotest/output.txt"); // 파일과 연결 br = new BufferedReader(reader); // 보조 스트림 연결 while (true) { String data = br.readLine(); // 한 줄 읽기 if (data == null) // 끝이면 종료 break; System.out.println(data); // 한 줄 출력 } } catch (FileNotFoundException fnfe) { System.out.println("파일이 존재하지 않습니다."); } catch (IOException ioe) { System.out.println("파일을 읽을 수 없습니다."); } finally { try { if (br != null) { // BufferedReader를 닫으면 내부의 FileReader도 함께 닫힘 br.close(); } else if (reader != null) { // BufferedReader가 만들어지기 전에 오류가 났을 때만 FileReader를 직접 닫음 reader.close(); } } catch (Exception e) { e.printStackTrace(); } } } }출력 결과
여기서 꼭 구분해야 할 것
read()의 끝 →-1readLine()의 끝 →nullBufferedReader를 붙여야 한 줄 읽기가 가능해진다.같은 파일 읽기라도, 어떤 메서드를 쓰는지에 따라 끝을 검사하는 방식이 달라진다.
이 부분은 꼭 따로 기억해야 한다.
예제 6. 자동으로 닫히는 구조까지 보기
이번에는 읽는 방식은 비슷하지만, 자원을 정리하는 방식이 더 안전하게 바뀐 예제를 본다.
즉 파일을 읽는 핵심 흐름은 유지하면서, 마지막 정리 방법이 좋아진 경우다.
코드
// FileReaderTest4.java package day10; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; public class FileReaderTest4 { public static void main(String args[]) { try (FileReader reader = new FileReader("./iotest/output.txt"); BufferedReader br = new BufferedReader(reader)) { String data; // 읽은 줄 저장 while (true) { data = br.readLine(); // 한 줄 읽기 if (data == null) // 끝이면 종료 break; System.out.println(data); // 출력 } } catch (FileNotFoundException fnfe) { System.out.println("파일이 존재하지 않습니다."); } catch (IOException ioe) { System.out.println("파일을 읽을 수 없습니다."); } } }출력 결과
이 예제에서 봐야 할 핵심
- 읽는 방식은
FileReaderTest2와 비슷하다.- 다른 점은
try-with-resources를 사용했다는 것이다.- 자원을 직접 닫지 않아도 자동으로 정리된다.
즉 이 예제는 새로운 읽기 개념을 보여 주는 예제라기보다, 같은 파일 읽기 구조를 더 안전하게 정리하는 방법을 보여 주는 예제라고 이해하면 된다.
이 파트의 흐름을 한 번에 정리하면
지금까지 본 예제는 전부 같은 파일 읽기 흐름 안에 있다.
다만 읽는 단위와 자원 정리 방식이 조금씩 달라졌다.
순서대로 보면
FileReaderTest→ 문자 하나씩 읽는 가장 기본 구조FileReaderTest2_1→FileReader위에BufferedReader가 붙는 연결 구조FileReaderTest2→ 한 줄씩 읽는 실전 구조FileReaderTest4→ 한 줄씩 읽되, 자원 정리까지 더 안전하게 바꾼 구조즉 파일 읽기 예제는 따로따로 외우는 것이 아니다.
기본 구조에서 시작해서, 더 편하게 읽고, 더 안전하게 닫는 방향으로 발전하는 흐름으로 이해하면 된다.
텍스트 파일은 단순히 글자를 읽는 것으로 끝나지 않는다.
파일 안에는 결국 바이트가 저장되어 있고, 프로그램은 그 바이트를 어떤 문자로 해석할지 정해서 읽는다.
그래서 텍스트 파일을 다룰 때는 무엇을 읽는가만 중요한 것이 아니라, 어떤 기준으로 해석하거나 저장하는가도 같이 중요하다.
이 기준이 맞으면 글자가 정상적으로 보이고, 기준이 다르면 한글이 깨져서 보일 수 있다.
이 파트에서는 같은 파일을 읽더라도FileReader로 읽을 때와,InputStreamReader로 인코딩을 지정해서 읽을 때 결과가 왜 달라지는지 이해하면 된다.
그리고 마지막에는 쓰기에서도 인코딩을 지정할 수 있다는 점까지 연결해서 본다.
바이트를 문자로 바꾼다는 뜻
텍스트 파일은 겉으로 보면 글자만 들어 있는 것처럼 보인다.
하지만 실제 저장은 결국 바이트 단위로 된다.
즉 파일 안에는 눈에 보이는 글자가 그대로 들어 있는 것이 아니라, 어떤 규칙에 따라 바이트 값이 저장되어 있는 것이다.
프로그램이 파일을 읽을 때는 그 바이트를 문자로 바꿔서 보여 준다.
이 과정을 아주 쉽게 말하면 이 바이트들을 어떤 글자로 읽을지 해석하는 과정이라고 보면 된다.
예를 들어 같은 바이트라도 어떤 규칙으로 읽느냐에 따라 다른 글자로 보일 수 있다.
그래서 텍스트 파일 읽기에서는 파일 저장 방식과 읽는 쪽의 해석 기준이 서로 맞아야 한다.
맞으면 정상적으로 글자가 보인다.
맞지 않으면 글자가 깨져서 이상한 문자처럼 보인다.
여기서 꼭 잡아야 할 핵심
- 파일 안에는 결국 바이트가 저장된다.
- 프로그램은 그 바이트를 문자로 해석해서 보여 준다.
- 해석 기준이 맞으면 정상적으로 읽힌다.
- 해석 기준이 다르면 글자가 깨질 수 있다.
즉 인코딩 문제는 글자가 왜 이상하게 보이지 수준의 문제가 아니라, 저장된 바이트를 어떤 문자로 해석하느냐의 문제라고 이해하면 된다.
기본 인코딩으로 읽기와 인코딩을 지정해서 읽기
파일을 읽을 때는 크게 두 가지 방식이 있다.
하나는 별도 설정 없이 기본 방식으로 읽는 것이다.
다른 하나는 이 파일은 이 기준으로 읽으라고 직접 정해서 읽는 것이다.
기본 방식으로 읽으면 현재 실행 환경의 기본 인코딩 기준을 따라가게 된다.
이때 파일 저장 방식과 실행 환경의 기준이 우연히 맞으면 정상적으로 읽힌다.
하지만 기준이 다르면 글자가 깨질 수 있다.
반대로 인코딩을 직접 지정하면, 프로그램이 파일을 어떤 문자 체계로 해석할지 우리가 분명하게 정할 수 있다.
즉 이 바이트들은 이 기준으로 읽어야 한다고 직접 알려 주는 것이다.
이 둘의 차이를 아주 짧게 정리하면
- 기본 방식으로 읽기 → 현재 환경 기준에 많이 의존한다.
- 인코딩 지정해서 읽기 → 우리가 해석 기준을 직접 정한다.
그래서 한글처럼 인코딩 차이에 민감한 파일은, 파일이 어떤 방식으로 저장되었는지 알고 있다면 인코딩을 직접 지정하는 쪽이 더 안전하다.
InputStreamReader는 여기서 왜 중요한가
앞 파트에서는
InputStreamReader를 바이트 입력을 문자 입력으로 바꿔 주는 다리라고 봤다.
이 파트에서는 거기에 한 가지가 더 붙는다.
바로 문자로 바꿀 때 어떤 인코딩을 쓸지 정할 수 있다는 점이다.
즉InputStreamReader는 단순히 바이트를 문자로 바꿔 주기만 하는 것이 아니다.
그 바이트를 어떤 문자 기준으로 읽을지도 함께 정할 수 있다.
그래서 인코딩이 중요한 파일을 읽을 때 자주 쓰인다.
흐름으로 보면 이렇게 된다
FileInputStream→ 파일에서 바이트를 읽는다.InputStreamReader→ 그 바이트를 문자로 바꾼다.- 이때 필요하면 인코딩도 직접 지정한다.
즉 이 파트에서
InputStreamReader는 단순 변환 도구가 아니라, 문자 해석 기준까지 조절할 수 있는 연결 도구라고 이해하면 된다.
예제 7. 기본 인코딩으로 읽고 현재 인코딩 확인하기
먼저 기본 방식으로 파일을 읽는 예제를 본다.
이 예제는 별도로 인코딩을 지정하지 않고, 현재 환경 기준으로 파일을 읽는다.
코드
// FileReaderTest3.java package day10; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; public class FileReaderTest3 { public static void main(String args[]) { try (FileReader reader = new FileReader("src/day10/output_ansi.txt")) { int data; // 읽은 문자 저장 System.out.println(reader.getEncoding()); // 현재 인코딩 확인 while (true) { data = reader.read(); // 문자 하나 읽기 if (data == -1) // 끝이면 종료 break; System.out.print((char) data); // 문자 출력 } } catch (FileNotFoundException fnfe) { System.out.println("파일이 존재하지 않습니다."); } catch (IOException ioe) { System.out.println("파일을 읽을 수 없습니다."); } } }출력 결과
이 예제에서 가장 먼저 봐야 할 부분은
reader.getEncoding()이다.
이 코드는 지금 어떤 인코딩 기준으로 파일을 읽고 있는지 확인하게 해 준다.
실행 결과를 보면 현재 환경에서는UTF8로 읽고 있다는 것을 확인할 수 있다.
그런데 뒤에 출력된 내용은 한글이 정상적으로 보이지 않는다.
이 말은 지금 읽는 기준과 파일이 저장된 기준이 서로 맞지 않는다는 뜻이다.
즉 파일은 텍스트 파일이지만, 현재 해석 기준으로 읽었더니 글자가 깨진 것이다.
이 예제에서 꼭 봐야 할 핵심
- 별도로 인코딩을 지정하지 않았다.
- 현재 환경 기준으로 파일을 읽는다.
getEncoding()으로 현재 읽는 기준을 확인할 수 있다.- 기준이 맞지 않으면 한글이 깨질 수 있다.
즉 이 예제는 단순히 읽기 실패 예제가 아니다.
기본 인코딩으로 읽을 때 어떤 문제가 생길 수 있는지 보여 주는 예제라고 보면 된다.
예제 8. 인코딩을 직접 지정해서 읽기
이번에는 같은 파일을 읽되, 인코딩을 직접 지정하는 예제를 본다.
이 예제는 앞의 예제와 비교해서 봐야 의미가 분명해진다.
코드
// FileReaderTest3WithANSI.java package day10; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; public class FileReaderTest3WithANSI { public static void main(String args[]) { try (FileInputStream fis = new FileInputStream("src/day10/output_ansi.txt"); InputStreamReader isr = new InputStreamReader(fis, "EUC-KR")) { int data; // 읽은 문자 저장 System.out.println(isr.getEncoding()); // 현재 인코딩 확인 while (true) { data = isr.read(); // 문자 하나 읽기 if (data == -1) // 끝이면 종료 break; System.out.print((char) data); // 문자 출력 } } catch (FileNotFoundException fnfe) { System.out.println("파일이 존재하지 않습니다."); } catch (IOException ioe) { System.out.println("파일을 읽을 수 없습니다."); } } }출력 결과
이 예제의 핵심은
new InputStreamReader(fis, "EUC-KR")이다.
즉 이 파일은EUC-KR기준으로 읽으라고 직접 지정한 것이다.
실행 결과를 보면 앞 예제와 달리 한글이 정상적으로 보인다.
이것은 파일이 저장된 방식과, 지금 읽을 때 지정한 기준이 서로 맞았다는 뜻이다.
즉 앞 예제에서는 기준이 맞지 않아 깨졌고, 이번 예제에서는 기준을 맞춰 줬기 때문에 정상적으로 읽힌 것이다.
이 예제에서 꼭 봐야 할 핵심
FileInputStream으로 파일의 바이트를 읽는다.InputStreamReader로 그 바이트를 문자로 바꾼다.- 이때
"EUC-KR"을 직접 지정한다.- 기준이 맞으면 한글이 정상적으로 보인다.
즉 이 예제는 인코딩을 직접 지정하면 파일을 원하는 기준으로 정확하게 해석할 수 있다는 점을 보여 주는 예제다.
OutputStreamWriter로 저장 인코딩 지정하기
인코딩은 읽을 때만 중요한 것이 아니다.
파일에 글자를 저장할 때도 어떤 기준으로 바이트를 만들지 정할 수 있다.
InputStreamReader가 바이트 입력을 문자 입력으로 바꾸면서 인코딩을 지정할 수 있다면,OutputStreamWriter는 문자 출력을 바이트 출력으로 바꾸면서 인코딩을 지정할 수 있다.
즉 읽을 때는 “이 바이트를 어떤 문자로 해석할까”가 중요하고, 쓸 때는 “이 문자를 어떤 바이트 기준으로 저장할까”가 중요하다.
예제: UTF-8 기준으로 파일 저장하기
// OutputStreamWriterExample.java package day10; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStreamWriter; public class OutputStreamWriterExample { public static void main(String[] args) throws Exception { File dir = new File("./iotest"); // 저장 폴더 객체 if (!dir.exists()) { dir.mkdirs(); // 필요한 폴더 생성 } try (FileOutputStream fos = new FileOutputStream("./iotest/output_utf8.txt"); OutputStreamWriter writer = new OutputStreamWriter(fos, "UTF-8")) { // UTF-8 기준으로 파일에 문자열 저장 writer.write("안녕하세요. UTF-8로 저장합니다."); System.out.println("UTF-8 저장 완료"); } } }// 출력결과 // UTF-8 저장 완료// 파일 내용 // 안녕하세요. UTF-8로 저장합니다.이 예제는 프로그램 안의 문자열을
UTF-8기준의 바이트로 바꿔 파일에 저장한다.
즉InputStreamReader가 읽기 쪽 인코딩을 다룬다면,OutputStreamWriter는 쓰기 쪽 인코딩을 다룬다고 보면 된다.
두 읽기 예제를 비교하면
두 예제는 같은 파일을 읽는다.
하지만 읽는 기준이 다르다.
그래서 결과도 달라진다.
차이를 한 번에 보면
FileReaderTest3→ 기본 인코딩 기준으로 읽는다.FileReaderTest3WithANSI→EUC-KR을 직접 지정해서 읽는다.- 앞 예제는 한글이 깨졌고, 뒤 예제는 한글이 정상적으로 읽혔다.
즉 인코딩 문제는 파일 내용이 이상해서 생기는 것이 아니다.
같은 파일이라도 어떤 기준으로 읽느냐에 따라 결과가 달라지는 문제다.
인코딩 문제는 글자 모양의 문제가 아니라, 저장된 바이트를 어떤 문자로 해석하느냐의 문제다.
이 파트의 흐름을 한 번에 정리하면
이번 파트에서는 파일을 읽을 때 단순히 읽는 것으로 끝나지 않는다는 점을 봤다.
텍스트 파일을 읽는다면, 결국 바이트를 문자로 바꾸는 해석 과정까지 같이 생각해야 한다.
그리고 파일에 쓸 때도 문자를 어떤 바이트 기준으로 저장할지 생각할 수 있다.
순서대로 보면
- 파일 안에는 결국 바이트가 저장된다.
- 프로그램은 그 바이트를 문자로 해석해서 보여 준다.
- 기본 인코딩으로 읽으면 현재 환경 기준을 따른다.
- 기준이 맞지 않으면 글자가 깨질 수 있다.
InputStreamReader로 인코딩을 직접 지정하면 원하는 기준으로 읽을 수 있다.OutputStreamWriter로 인코딩을 직접 지정하면 원하는 기준으로 저장할 수 있다.즉 텍스트 파일 입출력에서는 파일 경로만 맞춘다고 끝나는 것이 아니다.
어떤 기준으로 읽고 저장하는지까지 같이 생각해야 비로소 정상적으로 이해할 수 있다.
앞에서는 파일 안에 들어 있는 글자를 읽는 흐름을 봤다.
이제는 반대로 프로그램 안에 있는 내용을 파일 바깥으로 내보내는 흐름을 봐야 한다.
읽기가 바깥에서 안으로 들어오는 흐름이었다면, 쓰기는 안에서 바깥으로 나가는 흐름이다.
즉 방향만 반대일 뿐, 이것도 결국입출력이다.
이 파트에서는FileWriter가 왜 필요한지,write()가 어떻게 동작하는지,write(int)가 왜 헷갈리기 쉬운지,flush()와close()는 어떻게 다른지까지 함께 이해하면 된다.
처음에는 기본적인 파일 쓰기 구조를 보고, 그다음에는try-with-resources와write(int),flush()까지 이어서 보면 된다.
파일에 쓴다는 것은 무엇인가
파일에 쓴다는 것은 프로그램 안에 있는 데이터를 파일에 저장하는 것이다.
즉 프로그램이 만든 글자나 문장을 파일 쪽으로 보내는 흐름이라고 보면 된다.
앞에서 파일을 읽을 때는 파일 안의 내용을 프로그램이 받아 왔다.
이번에는 반대로 프로그램이 만든 내용을 파일에 보낸다.
그래서 읽기에서는입력이었고, 쓰기에서는출력이 된다.
예를 들어"안녕하세요"라는 문자열을 파일에 저장했다면, 그 문자열은 프로그램 안에서 만들어져서 파일로 나간 것이다.
이것이 바로 파일 출력이다.
여기서 먼저 잡아야 할 핵심
- 파일 읽기 → 파일에서 프로그램으로 들어오는 흐름
- 파일 쓰기 → 프로그램에서 파일로 나가는 흐름
- 즉 읽기와 쓰기는 방향이 반대다
이 기준이 분명해야
Reader와Writer이름도 흔들리지 않는다.
읽는 쪽은Reader, 쓰는 쪽은Writer라는 흐름으로 이어진다.
FileWriter는 무엇을 하는가
FileWriter는 텍스트 파일에 글자를 쓰는 클래스다.
이름을 그대로 읽으면File은 파일이고,Writer는 쓰는 쪽이다.
즉 파일과 연결해서 글자를 내보내는 통로라고 보면 된다.
앞에서FileReader가 파일에서 글자를 읽어 오는 통로였다면,FileWriter는 반대로 프로그램 안의 글자를 파일로 보내는 통로다.
즉 파일과 프로그램 사이에 문자 출력 통로를 만든다고 생각하면 된다.
그리고 여기서 같이 알아두면 좋은 점이 하나 있다.
FileWriter를 기본 방식으로 생성하면, 보통 기존 내용을 새로 쓰는 흐름으로 이해하면 된다.
즉 같은 파일에 다시 쓰면 앞의 내용 뒤에 자동으로 붙는 것이 아니라, 새로 기록하는 쪽이 기본이다.
나중에FileWriter(..., true)처럼 별도 옵션을 줄 때만 이어쓰기 방식이 된다.
FileWriter를 볼 때 핵심은 이렇다
- 텍스트 파일에 글자를 저장할 때 사용한다.
- 글자를 쓰는 쪽이므로
Writer계열이다.- 파일과 프로그램 사이에 문자 출력 통로를 만든다.
- 기본 방식은 보통 기존 내용을 새로 쓰는 흐름으로 이해하면 된다.
즉 텍스트 파일에 내용을 저장할 때는
FileWriter가 가장 기본 출발점이 된다.
write()는 무엇을 하는가
FileWriter에서 가장 자주 보는 메서드는write()다.
이 메서드는 파일에 내용을 기록하는 역할을 한다.
그런데write()는 한 가지 방식만 있는 것이 아니다.
문자 하나를 쓸 수도 있고, 문자 배열을 쓸 수도 있고, 문자열을 쓸 수도 있다.
즉 같은write()라도 무엇을 넘기느냐에 따라 기록하는 대상이 달라진다.
이 부분은 이렇게 보면 쉽다
- 문자 하나 쓰기 → 한 글자 저장
- 문자 배열 쓰기 → 여러 글자 한 번에 저장
- 문자열 쓰기 → 문장처럼 저장
즉
write()는 파일에 기록한다는 큰 역할은 같고, 어떤 형태의 데이터를 넘기느냐에 따라 조금씩 다르게 동작한다고 보면 된다.
파일 쓰기에서도 왜 닫아야 하는가
파일 읽기에서 스트림을 닫아야 했던 것처럼, 파일 쓰기에서도 사용이 끝나면 스트림을 닫아야 한다.
이 부분은 읽기와 쓰기 모두에서 중요하다.
특히 쓰기에서는 아직 남아 있는 내용이 완전히 기록되기 전에 프로그램이 끝날 수 있다.
그래서 작업이 끝났다면close()를 해서 파일 기록을 마무리하고 자원을 정리해야 한다.
여기서도 핵심은 같다
- 스트림은 사용이 끝나면 닫아야 한다.
- 그래야 자원이 정리된다.
- 쓰기의 경우 기록이 제대로 마무리되도록 하는 의미도 있다.
즉 파일 쓰기에서도 다 썼으니 끝이 아니라, 마지막 정리까지 해야 완성이라고 보면 된다.
flush()와 close()는 무엇이 다른가
파일 쓰기에서는
close()와 함께flush()도 알아두면 좋다.
둘 다 출력 흐름과 관련 있지만 역할은 다르다.
flush()는 아직 버퍼에 남아 있는 내용을 실제 파일이나 출력 대상으로 밀어내는 메서드다.
반대로close()는 스트림을 닫으면서 자원을 정리한다.
대부분의 출력 스트림은close()할 때 내부적으로flush()도 함께 수행한다.
그래서 작업 중간에 내용을 확실히 내보내고 계속 쓰고 싶으면flush()를 생각하고, 작업이 완전히 끝났으면close()를 생각하면 된다.
예제: flush()로 중간 기록 밀어내기
// FlushExample.java package day10; import java.io.File; import java.io.FileWriter; public class FlushExample { public static void main(String[] args) throws Exception { File dir = new File("./iotest"); // 저장 폴더 객체 if (!dir.exists()) { dir.mkdirs(); // 필요한 폴더 생성 } try (FileWriter writer = new FileWriter("./iotest/flush.txt")) { writer.write("파일에 먼저 보낼 내용"); // 버퍼에 남은 내용을 실제 파일로 밀어냄 writer.flush(); writer.write("\n이후에 이어서 쓸 내용"); System.out.println("flush 예제 저장 완료"); } } }// 출력결과 // flush 예제 저장 완료// 파일 내용 // 파일에 먼저 보낼 내용 // 이후에 이어서 쓸 내용이 예제에서
flush()는 중간에 한 번 파일로 내용을 밀어내는 역할을 한다.
하지만 스트림 사용이 완전히 끝났을 때는 여전히close()가 필요하다.
try-with-resources를 쓰면 블록이 끝날 때 자동으로close()가 호출된다.
예제 9. 기본적인 파일 쓰기
먼저 가장 기본적인 파일 출력 예제를 본다.
이 예제는 파일을 저장할 폴더를 준비하고, 문자 하나, 문자 배열, 문자열을 차례대로 파일에 쓰는 흐름을 보여 준다.
코드
// FileWriterTest.java package day10; import java.io.File; import java.io.FileWriter; import java.io.IOException; public class FileWriterTest { public static void main(String args[]) { FileWriter writer = null; // 출력 통로 String path = "./iotest"; // 폴더 경로 File isDir = new File(path); // 폴더 객체 if (!isDir.exists()) { isDir.mkdirs(); // 필요한 폴더 생성 } try { writer = new FileWriter("./iotest/output.txt"); // 파일과 연결 char arr[] = { '객', '체', '지', '향', '언', '어', ' ', 'J', 'a', 'v', 'a' }; for (int cnt = 0; cnt < arr.length; cnt++) writer.write(arr[cnt]); // 문자 하나씩 쓰기 writer.write("\n"); // 줄바꿈 writer.write(arr); // 배열 쓰기 writer.write("\n"); writer.write("OCJP 시험 대비"); // 문자열 쓰기 writer.write("\n"); writer.write("우리는 한팀!! 마지막까지 서로 도우며 열심히 합시다!!"); System.out.println("파일에 출력 완료!!"); // 완료 메시지 } catch (IOException ioe) { ioe.printStackTrace(); System.out.println("파일로 출력할 수 없습니다."); } finally { try { if (writer != null) writer.close(); // 사용 후 닫기 } catch (Exception e) { System.out.println("파일을 닫는 동안 오류 발생!!"); } } } }출력 결과
콘솔에는 파일 출력 완료 메시지가 출력되고,
iotest폴더 안에는output.txt파일이 생성된다.
파일을 열어 보면FileWriter로 기록한 문자 배열과 문자열이 실제 파일 내용으로 저장되어 있다.
이 예제에서는 파일 쓰기 흐름이 한 번에 보인다.
먼저./iotest폴더가 있는지 확인하고, 없으면 만든다.
그다음FileWriter로output.txt와 연결해서 실제 내용을 파일에 기록한다.
그리고write()가 여러 방식으로 쓰이는 것도 한 번에 확인할 수 있다.
문자 하나를 쓸 수도 있고, 배열을 쓸 수도 있고, 문자열을 쓸 수도 있다.
즉 같은write()라도 넘기는 값에 따라 기록 방식이 달라진다.
또 이 예제의FileWriter는 기본 생성 방식이다.
그래서 실행할 때는 기존 내용을 계속 뒤에 붙이는 흐름이 아니라, 현재 내용을 새로 기록하는 쪽으로 이해하면 된다.
이 점은 뒤에서 이어쓰기 예제를 볼 때 차이를 비교하는 기준이 된다.
이 예제에서 꼭 봐야 할 핵심
new File("./iotest")→ 저장할 폴더를 준비한다.new FileWriter("./iotest/output.txt")→ 파일과 연결한다.writer.write(...)→ 실제 내용을 파일에 기록한다.writer.close()→ 쓰기 작업을 마무리한다.- 기본 방식은 보통 기존 내용을 새로 쓰는 흐름이다.
즉 이 예제는 파일 쓰기의 기본 구조 전체를 보여 주는 출발점이라고 보면 된다.
write(int)가 왜 헷갈리는가
여기서 초보자가 자주 헷갈리는 부분이 있다.
write(100)을 보면"100"이라는 문자열이 파일에 저장될 것처럼 보이기 쉽다.
하지만 실제로는 그렇지 않다.
write(int)는 숫자 세 자리를 문자열처럼 기록하는 메서드가 아니다.
정수 하나를 받아서, 그 값에 해당하는 문자 코드 하나를 기록하는 방식이라고 이해해야 한다.
즉"100"을 저장하는 것과write(100)은 전혀 다른 동작이다.
꼭 구분해야 할 차이
"100"→ 문자1,0,0세 개를 저장write(100)→ 문자 코드100에 해당하는 문자 하나를 저장이 부분은 실제 결과를 확인해 보면 더 잘 보인다.
그래서 바로 다음 예제에서 이 차이를 확인하게 된다.
예제 10. write(int)와 try-with-resources
이번 예제는 기본 파일 쓰기 구조를 바탕으로,
write(int)가 실제로 어떻게 동작하는지와try-with-resources를 함께 보여 준다.
즉 쓰기 동작과 자원 정리를 같이 확인하는 예제다.
코드
// FileWriterTest2.java package day10; import java.io.File; import java.io.FileWriter; import java.io.IOException; public class FileWriterTest2 { public static void main(String args[]) { File dir = new File("./iotest"); // 저장 폴더 객체 if (!dir.exists()) { dir.mkdirs(); // 필요한 폴더 생성 } try (FileWriter writer = new FileWriter("./iotest/output2.txt")) { char arr[] = { '객', '체', '지', '향', '언', '어', 'J', 'a', 'v', 'a' }; for (int cnt = 0; cnt < arr.length; cnt++) writer.write(arr[cnt]); // 문자 하나씩 쓰기 writer.write('\n'); // 줄바꿈 writer.write(arr); // 배열 쓰기 writer.write('\n'); writer.write("OCJP 시험 대비!!"); // 문자열 쓰기 writer.write('\n'); writer.write(100); // 문자 코드 100 하나 쓰기 writer.write('\n'); System.out.println("파일에 출력 완료!!"); } catch (IOException ioe) { ioe.printStackTrace(); System.out.println("파일로 출력할 수 없습니다."); } } }출력 결과
이 예제에서는 두 가지를 같이 봐야 한다.
하나는write(int)가 생각한 것과 다르게 동작할 수 있다는 점이다.
다른 하나는try-with-resources를 쓰면 스트림을 자동으로 닫을 수 있다는 점이다.
실제 결과를 보면write(100)이"100"을 그대로 저장한 것이 아니라, 문자 코드100에 해당하는 문자 하나를 기록한 것을 확인할 수 있다.
그래서 결과 파일에서는 숫자100이 아니라 보통d한 글자가 보이게 된다.
즉 코드만 보면 헷갈릴 수 있는 부분을 결과물에서 바로 확인하게 된다.
또 앞 예제에서는finally에서 직접close()를 호출했다.
이번 예제에서는try(...)안에서FileWriter를 만들었기 때문에, 블록이 끝나면 자동으로 닫힌다.
이 예제에서 꼭 봐야 할 핵심
write(int)는 문자열 저장이 아니다.- 문자 코드 하나를 기록하는 동작이다.
- 그래서
write(100)결과는"100"이 아니라 문자 하나로 남는다.try-with-resources를 쓰면 자동으로 닫힌다.- 즉 쓰기 동작과 자원 정리 방식이 함께 개선된 예제다.
이 예제는 새로운 파일 쓰기 개념을 완전히 새로 보여 주는 예제가 아니라, 기본 파일 쓰기 구조 위에 헷갈리기 쉬운
write(int)와 더 안전한 자원 정리 방식을 추가한 예제라고 이해하면 된다.
이 파트의 흐름을 한 번에 정리하면
지금까지 본 내용은 전부 텍스트 파일을 파일에 저장하는 흐름이다.
다만 기본 쓰기 구조를 먼저 보고, 그다음에 헷갈리기 쉬운write(int), 더 안전한 자원 정리 방식,flush()와close()차이까지 이어서 본 것이다.
순서대로 보면
FileWriter→ 텍스트 파일에 글자를 쓰는 기본 통로write()→ 문자, 배열, 문자열을 파일에 기록하는 메서드flush()→ 버퍼에 남은 내용을 출력 대상으로 밀어내는 메서드close()→ 스트림을 닫고 자원을 정리하는 메서드FileWriterTest→ 파일 쓰기의 기본 구조FileWriterTest2→write(int)의 실제 동작과try-with-resources즉 텍스트 파일 쓰기 예제도 따로따로 외우는 것이 아니다.
기본적으로 파일에 글자를 쓰고, 그다음 더 정확하게 기록 방식을 이해하고, 마지막에는 자원 정리까지 더 안전하게 만드는 흐름으로 이해하면 된다.
FileWriter만으로도 파일에 글자를 저장할 수 있다.
하지만 실제로 파일에 내용을 정리해서 쓰다 보면 줄바꿈, 값 여러 개 출력, 형식 맞추기 같은 작업이 점점 번거로워진다.
이때 사용하는 것이PrintWriter다.
이 클래스는 파일 출력도 콘솔 출력처럼 더 편하게 다룰 수 있게 도와준다.
즉 기본 출력 통로 위에 붙어서 출력 작업을 더 읽기 쉽게 만들어 주는 도구라고 보면 된다.
이 파트에서는PrintWriter가 왜 필요한지,print(),println(),printf()가 어떻게 다른지, 기본 쓰기와 이어쓰기 차이, 그리고PrintWriter의 오류 확인 방식까지 함께 보면 된다.
PrintWriter가 왜 필요한가
앞 파트에서 본
FileWriter는 텍스트 파일에 글자를 쓰는 가장 기본적인 통로였다.
즉 파일에 내용을 저장하는 출발점으로는 좋다.
하지만 실제로 문장 여러 줄을 정리해서 쓰거나, 숫자와 문자와 날짜를 함께 출력하려고 하면 조금 불편해질 수 있다.
예를 들어FileWriter만 사용하면 줄바꿈도 직접 넣어야 하고, 여러 값을 한 문장처럼 정리할 때도 손이 더 많이 간다.
반면PrintWriter를 붙이면print(),println(),printf()처럼 콘솔에서 자주 보던 방식을 파일에서도 거의 그대로 사용할 수 있다.
즉PrintWriter의 핵심은 단순하다.
파일에 출력하는데도 콘솔에 출력하듯 자연스럽게 다룰 수 있게 해 준다는 점이다.
그리고 앞에서 본기반 스트림과보조 스트림기준으로 보면,PrintWriter는 보통 기존 출력 통로를 더 편하게 쓰게 해 주는 쪽으로 이해하면 된다.
즉 출발점 자체라기보다, 이미 있는 출력 흐름을 더 다루기 좋게 만들어 주는 역할에 가깝다.
PrintWriter를 붙이면 좋아지는 점
print()로 값만 바로 출력할 수 있다.println()으로 줄바꿈까지 함께 처리할 수 있다.printf()로 형식을 맞춰 출력할 수 있다.- 여러 종류의 값을 한결 자연스럽게 파일에 기록할 수 있다.
즉
PrintWriter는 파일 출력 자체를 새로 만드는 클래스라기보다, 기존 출력 통로를 더 편하게 쓰게 해 주는 도구라고 이해하면 된다.
print(), println(), printf() 차이
PrintWriter를 이해하려면 이 세 메서드 차이를 먼저 알아야 한다.
이 셋은 비슷해 보이지만 출력 방식이 조금씩 다르다.
print()는 값을 출력하지만 줄바꿈은 하지 않는다.
그래서 다음 출력이 같은 줄 뒤에 이어진다.
println()은 값을 출력한 뒤 줄바꿈까지 같이 한다.
즉 한 줄을 끝내고 다음 줄로 넘기고 싶을 때 자연스럽다.
printf()는 형식을 맞춰서 출력한다.
예를 들어 숫자에 쉼표를 넣거나, 소수점 자리 수를 맞추거나, 여러 값을 정해진 모양으로 출력하고 싶을 때 사용한다.
차이를 짧게 정리하면
print()→ 출력만 한다.println()→ 출력하고 줄바꿈한다.printf()→ 형식을 맞춰 출력한다.즉 파일에 단순히 글자만 저장하는 수준을 넘어, 읽기 좋은 결과를 만들고 싶을 때
PrintWriter가 더 유리하다.
PrintWriter는 어떻게 연결되는가
PrintWriter는 두 가지 식으로 생각하면 이해가 쉽다.
하나는 파일 경로를 직접 넘겨서 바로 출력하는 방식이다.
다른 하나는FileWriter같은 기존 출력 통로 위에 붙여서 사용하는 방식이다.
즉PrintWriter는 상황에 따라 단독으로 보일 수도 있고, 기존 출력 스트림 위에 붙어 있는 모습으로도 보일 수 있다.
중요한 것은 겉모양이 아니라, 출력을 더 편하게 만들기 위한 도구라는 점이다.
흐름으로 보면 이렇게 이해하면 된다
- 파일 경로를 직접 받아서 출력할 수도 있다.
FileWriter위에 붙여서 출력할 수도 있다.- 핵심 역할은 언제나 출력 작업을 더 편하게 만드는 것이다.
그래서 예제 11과 예제 12도 겉모양은 조금 다르지만, 둘 다
PrintWriter가 출력 편의성을 높여 준다는 공통점을 가진다.
예제 11처럼 파일 경로를 직접 넘기면 내부적으로 파일 출력 통로를 준비해 주는 형태로 보면 되고, 예제 12처럼FileWriter를 직접 만들어 넘기면 기존 출력 통로 위에PrintWriter를 붙이는 형태로 보면 된다.
PrintWriter의 오류 확인
PrintWriter는 출력 문법이 편하지만, 출력 중 오류를 일반적인IOException처럼 바로 던지지 않는 경우가 있다.
대신 내부에 오류 상태를 기록할 수 있다.
정말 오류 여부를 확인해야 하는 상황이라면checkError()를 사용할 수 있다.
지금 단계에서는PrintWriter가 편한 출력 도구라는 점을 먼저 잡으면 된다.
그리고 오류 확인이 필요할 때checkError()라는 메서드가 있다는 정도만 알아두면 충분하다.
예제 11. PrintWriter로 더 편하게 출력하기
이제
PrintWriter를 실제로 사용하는 예제를 본다.
이 예제는 문자, 문자열, 숫자, 실수,boolean값을 파일에 자연스럽게 출력하는 흐름을 보여 준다.
또 이 예제는PrintWriter가 파일 경로를 직접 받아 출력할 수 있다는 점도 같이 보여 준다.
즉 꼭FileWriter를 먼저 만들지 않아도,PrintWriter("./iotest/output3.txt")처럼 바로 시작할 수도 있다는 뜻이다.
코드
// PrintWriterTest.java package day10; import java.io.File; import java.io.IOException; import java.io.PrintWriter; public class PrintWriterTest { public static void main(String args[]) { File dir = new File("./iotest"); // 저장 폴더 객체 if (!dir.exists()) { dir.mkdirs(); // 필요한 폴더 생성 } try (PrintWriter pw = new PrintWriter("./iotest/output3.txt")) { char arr[] = { '객', '체', '지', '향', '언', '어', ' ', 'J', 'a', 'v', 'a' }; for (int cnt = 0; cnt < arr.length; cnt++) pw.print(arr[cnt]); // 문자 하나씩 출력 pw.println(); // 줄바꿈 pw.println(arr); // 배열 출력 pw.println("OCJP 시험 대비!!"); // 문자열 출력 pw.println(100); // 숫자 출력 pw.println(3.14); // 실수 출력 pw.println(true); // boolean 출력 pw.printf("%,d원%n", 1000000); // 형식화 출력 pw.printf("%.2f%n", 123.5678); System.out.println("파일에 출력 완료!!"); if (pw.checkError()) { System.out.println("출력 중 오류가 발생했습니다."); } } catch (IOException ioe) { System.out.println("파일로 출력할 수 없습니다."); } } }출력 결과
이 예제에서 가장 먼저 보이는 장점은 출력 문장이 훨씬 자연스럽다는 점이다.
앞 파트의FileWriter는write()를 여러 번 호출하면서 직접 줄바꿈을 넣어야 했다.
하지만 여기서는println()과printf()를 사용해서 훨씬 읽기 쉬운 방식으로 파일 내용을 만들 수 있다.
또 숫자, 실수, 문자열,boolean값을 같은 흐름으로 자연스럽게 출력할 수 있다는 것도 중요하다.
즉PrintWriter는 단순히 글자를 저장하는 수준을 넘어, 사람이 읽기 좋은 결과를 파일에 정리해서 남기기 좋은 도구라고 보면 된다.
이 예제에서 꼭 봐야 할 핵심
PrintWriter는 파일 출력도 콘솔 출력처럼 다룰 수 있게 해 준다.- 파일 경로를 직접 받아서 시작할 수도 있다.
println()덕분에 줄바꿈 처리가 편하다.printf()덕분에 형식을 맞춘 출력이 가능하다.- 여러 종류의 값을 자연스럽게 파일에 기록할 수 있다.
- 오류 확인이 필요하면
checkError()를 사용할 수 있다.즉 이 예제는
PrintWriter가 왜 편한지 가장 직접적으로 보여 주는 예제라고 보면 된다.
이어쓰기와 형식 있는 문장 저장
앞 예제에서는
PrintWriter로 파일에 다양한 값을 편하게 출력하는 흐름을 봤다.
이번에는 거기에 한 가지가 더 붙는다.
바로 기존 파일 뒤에 내용을 이어서 저장하는 방식이다.
앞 파트에서 본 기본FileWriter생성 방식은 보통 기존 내용을 덮어쓰는 흐름으로 이해하면 된다.
반면FileWriter(..., true)처럼true를 주면 기존 내용 뒤에 이어서 기록한다.
즉 덮어쓰기가 아니라 추가 저장 방식으로 바뀐다.
이 차이는 실제 파일을 계속 기록해 나가야 할 때 중요하다.
하루 기록, 로그, 날짜별 메모처럼 기존 내용을 지우면 안 되는 경우에는 이어쓰기 방식이 더 잘 맞는다.
여기서 핵심은 이 차이다
- 기본
FileWriter→ 보통 기존 내용을 덮어쓰는 흐름FileWriter(..., true)→ 기존 내용 뒤에 이어서 쓰는 흐름즉 이번 예제는
PrintWriter의 편한 출력에, 이어쓰기 개념까지 함께 붙는다고 보면 된다.
예제 12. 이어쓰기와 형식 있는 문장 저장
이 예제는
FileWriter와PrintWriter를 함께 사용해서, 날짜와 요일 정보를 읽기 좋은 문장 형태로 파일에 저장한다.
그리고 기존 내용 뒤에 이어서 기록하는 구조도 같이 보여 준다.
코드
// FileWriterTest3.java package day10; import java.io.File; import java.io.FileWriter; import java.io.PrintWriter; import java.time.DayOfWeek; import java.time.LocalDate; import java.time.format.TextStyle; import java.util.Locale; public class FileWriterTest3 { public static void main(String[] args) { String path = "./iotest"; // 폴더 경로 File dir = new File(path); // 폴더 객체 if (!dir.exists()) { dir.mkdirs(); // 필요한 폴더 생성 } try (FileWriter writer = new FileWriter("./iotest/today.txt", true); PrintWriter out = new PrintWriter(writer)) { LocalDate ld = LocalDate.now(); // 오늘 날짜 int yearNum = ld.getYear(); int monthNum = ld.getMonthValue(); int dateNum = ld.getDayOfMonth(); DayOfWeek day = ld.getDayOfWeek(); String korday = day.getDisplayName(TextStyle.FULL, Locale.KOREAN); out.printf("오늘은 %d년 %d월 %d일입니다.%n", yearNum, monthNum, dateNum); out.printf("오늘은 %s입니다.%n", korday); LocalDate myld = LocalDate.of(2022, 7, 17); // 다른 날짜 DayOfWeek myday = myld.getDayOfWeek(); String mykorday = myday.getDisplayName(TextStyle.FULL, Locale.KOREAN); out.printf("%s은 %s에 태어났습니다.%n", "XXX", mykorday); System.out.println("저장이 완료되었습니다."); } catch (Exception e) { System.out.println("파일에 저장하는 동안 오류가 발생했습니다."); } } }출력 결과
이 예제에서는 두 가지가 함께 보인다.
하나는PrintWriter를 이용해 문장 형태의 결과를 깔끔하게 만드는 점이다.
다른 하나는FileWriter("./iotest/today.txt", true)를 사용해서 기존 파일 뒤에 내용을 이어서 저장한다는 점이다.
즉 이 예제는 단순히 값 하나를 파일에 쓰는 수준이 아니라, 형식 있는 문장을 계속 누적해서 저장하는 구조를 보여 준다.
그래서 앞 예제보다 실제 활용 모습에 더 가깝다.
또 이 예제를 보면PrintWriter가 왜 편한지도 더 분명해진다.
날짜, 숫자, 문자열을 한 문장 안에 자연스럽게 섞어서 출력할 수 있기 때문이다.
이런 경우write()만으로 처리하려면 훨씬 번거로워진다.
이 예제에서 꼭 봐야 할 핵심
FileWriter(..., true)는 이어쓰기다.PrintWriter로 문장 형태 출력을 더 자연스럽게 만들 수 있다.printf()를 사용하면 날짜와 값을 읽기 좋은 모양으로 정리할 수 있다.- 즉 출력 편의성과 이어쓰기 개념이 함께 들어간 예제다.
이 예제는
PrintWriter를 단순 출력 도구로만 보는 것이 아니라, 실제로 파일 내용을 계속 쌓아 가며 정리하는 도구로 볼 수 있게 해 준다.
이 파트의 흐름을 한 번에 정리하면
지금까지 본 내용은 전부 텍스트 파일에 내용을 더 편하게 저장하는 흐름이다.
앞 파트의FileWriter가 기본 통로였다면, 이번 파트의PrintWriter는 그 통로를 더 다루기 쉽게 만들어 주는 도구다.
순서대로 보면
PrintWriter→ 파일 출력도 콘솔 출력처럼 편하게 다룰 수 있게 해 준다.print(),println(),printf()→ 출력 방식과 줄바꿈, 형식 지정을 나눠서 처리한다.PrintWriterTest→ 여러 값을 편하게 출력하는 기본 예제FileWriterTest3→ 이어쓰기와 형식 있는 문장 저장까지 보여 주는 예제즉 이 파트는 단순히 파일에 쓴다에서 한 단계 더 나아간 것이다.
파일에 내용을 더 읽기 좋게 쓰고, 필요하면 계속 이어서 저장하는 흐름까지 이해하면 된다.
앞에서는
FileReader,BufferedReader,FileWriter,PrintWriter처럼 파일 내용을 읽고 쓰는 흐름을 봤다.
이번에는 파일을 조금 다른 방식으로 읽는 방법과, 파일 자체를 관리하는 방법을 함께 정리해야 한다.
여기서 중요한 것은 역할을 섞지 않는 것이다.
Scanner는 파일 내용을 읽는 데 사용할 수 있는 도구이고,File은 파일 내용 자체를 읽고 쓰는 클래스가 아니라 파일이나 폴더를 다루는 클래스다.
즉 이 파트에서는 파일 내용을 읽는 도구와 파일 자체를 관리하는 도구를 나눠서 이해해야 한다.
이 기준이 잡혀 있어야ScannerFromFile,FileTest1,FileTest2,FileTest3가 따로 노는 예제가 아니라 하나의 흐름으로 보인다.
Scanner는 파일도 읽을 수 있다
Scanner는 보통 키보드 입력 받을 때 먼저 많이 본다.
그래서 초보자는Scanner를 입력창 전용 도구처럼 생각하기 쉽다.
하지만Scanner는 키보드뿐 아니라 파일과도 연결할 수 있다.
즉Scanner는 어디에서 읽을 것인가에 따라 키보드와도 연결되고, 파일과도 연결될 수 있다.
여기서는 그중에서 파일과 연결해서 읽는 경우를 보는 것이다.
Scanner를 파일과 연결하면 좋은 점
- 비교적 간단한 코드로 파일을 읽을 수 있다.
- 한 줄씩 읽을 수도 있다.
- 공백 기준으로 잘라서 읽을 수도 있다.
즉
Scanner는 파일도 읽을 수 있고, 줄 기준과 공백 기준을 쉽게 바꿔 가며 확인할 수 있는 도구라고 이해하면 된다.
nextLine()과 next()는 무엇이 다른가
Scanner를 파일과 연결할 때 가장 먼저 알아야 하는 것은nextLine()과next()차이다.
이 둘은 비슷해 보이지만 읽는 기준이 다르다.
nextLine()은 한 줄 전체를 읽는다.
즉 줄바꿈 전까지를 하나로 보고 가져온다.
그래서 파일 내용을 줄 기준으로 보고 싶을 때 잘 맞는다.
반면next()는 공백을 기준으로 잘라서 읽는다.
즉 띄어쓰기 전까지를 하나로 보고 가져온다.
그래서 문장 전체가 아니라 단어처럼 끊어서 보게 된다.
차이를 짧게 정리하면
nextLine()→ 한 줄 전체를 읽는다.next()→ 공백 기준으로 한 덩어리씩 읽는다.즉 같은 파일을 읽더라도 어떤 메서드를 쓰느냐에 따라 결과가 달라진다.
여기서는 이 차이를 눈으로 확인하는 것이 중요하다.
예제 13. Scanner로 파일 읽기
이 예제는
Scanner가 파일과 연결될 수 있다는 점과,nextLine()과next()가 어떻게 다르게 동작하는지를 함께 보여 준다.
실행할 때마다 행 단위 또는 워드 단위 중 하나가 선택되도록 되어 있다.
다만 이 예제는Random으로 실행할 때마다 행 단위 읽기 또는 워드 단위 읽기 중 하나가 선택된다.
그래서 실행할 때마다 결과가 다르게 나올 수 있다.
복습할 때는 두 흐름을 각각 따로 확인한다고 생각하면 된다.
코드
// ScannerFromFile.java package day10; import java.io.File; import java.io.FileNotFoundException; import java.util.Random; import java.util.Scanner; public class ScannerFromFile { public static void main(String args[]) { File f = new File("./iotest/output.txt"); // 읽을 파일 try (Scanner scan = new Scanner(f)) { if (new Random().nextBoolean()) { System.out.println("[ 행 단위 ]"); while (scan.hasNextLine()) System.out.println(scan.nextLine()); // 줄 단위 읽기 } else { System.out.println("[ 워드 단위 ]"); while (scan.hasNext()) System.out.println(scan.next()); // 단어 단위 읽기 } } catch (FileNotFoundException e) { System.out.println("파일이 존재하지 않네요...ㅜ"); } } }출력 결과
이 예제에서 핵심은
Scanner자체보다 읽는 기준이 바뀌면 결과가 달라진다는 점이다.
행 단위로 읽으면 파일 내용이 줄 기준으로 보인다.
반대로 워드 단위로 읽으면 같은 파일도 단어처럼 잘려서 보인다.
즉Scanner는 파일을 읽을 수 있다는 점도 중요하지만, 그보다 더 중요한 것은 같은 파일도 어떤 방식으로 읽을지 선택할 수 있다는 점이다.
이 예제에서 꼭 봐야 할 핵심
Scanner는 파일과도 연결할 수 있다.nextLine()은 한 줄씩 읽는다.next()는 공백 기준으로 읽는다.- 같은 파일이라도 읽는 기준에 따라 결과가 달라진다.
Random때문에 실행할 때마다 두 흐름 중 하나가 선택될 수 있다.즉 이 예제는
Scanner가 파일 내용을 읽을 수 있다는 점과, 읽는 단위 차이를 함께 보여 주는 예제라고 보면 된다.
File 클래스의 역할
이제
File클래스를 봐야 한다.
여기서 초보자가 가장 많이 헷갈리는 부분이 있다.
이름 때문에File이 파일 내용을 읽고 쓰는 클래스처럼 느껴질 수 있다는 점이다.
하지만 실제 역할은 다르다.
File은 파일 내용 자체를 읽고 쓰는 클래스가 아니다.
대신 파일이나 폴더의 경로, 존재 여부, 종류, 생성, 정보 확인 같은 것을 다룬다.
쉽게 말하면 파일 안의 내용보다, 파일이라는 대상 자체를 관리하는 클래스라고 보면 된다.
이 부분은 꼭 구분해야 한다
FileReader,FileWriter→ 파일 내용을 읽고 쓰는 클래스File→ 파일과 폴더 자체를 다루는 클래스즉
File은 내용 입출력용 클래스가 아니라, 파일과 폴더를 관리하는 클래스다.
이 기준이 흔들리면 뒤 예제들이 전부 헷갈리기 쉬워진다.
왜 File도 입출력 파트에서 같이 보는가
File은 내용을 직접 읽고 쓰지는 않는다.
그런데도 입출력 파트에서 같이 보는 이유가 있다.
실제 파일 입출력을 하려면 파일이 어디 있는지, 폴더가 있는지, 파일이 존재하는지, 새 파일을 만들 수 있는지 같은 정보도 같이 알아야 하기 때문이다.
즉File은 직접 내용을 옮기는 통로는 아니지만, 파일 입출력이 이루어지는 대상과 위치를 관리하는 데 꼭 필요한 도구다.
그래서Reader,Writer예제와 함께 보는 것이 자연스럽다.
여기서는 이렇게 이해하면 된다
Reader,Writer→ 내용 이동File→ 대상과 위치 관리즉 역할은 다르지만, 실제 파일 작업에서는 서로 이어져서 쓰인다.
mkdir()와 mkdirs() 차이
디렉토리를 만들 때는
mkdir()와mkdirs()를 볼 수 있다.
이 둘은 이름이 비슷하지만 약간 다르다.
mkdir()은 마지막 디렉토리 하나만 만들 때 사용한다.
중간 경로가 이미 있어야 성공하기 쉽다.
반대로mkdirs()는 중간에 필요한 부모 디렉토리까지 함께 만들 수 있다.
그래서 여러 단계의 경로를 한 번에 준비해야 할 때 더 편하다.
예를 들어a/b/c를 만들고 싶은데a와b가 아직 없다면,mkdir()보다mkdirs()가 더 자연스럽다.
예제 14. 디렉토리 존재 여부 확인과 생성
이 예제는 사용자가 입력한 이름으로 디렉토리를 확인하고, 없으면 새로 만드는 흐름을 보여 준다.
여기서는 파일 내용을 읽거나 쓰지 않는다.
대신 폴더가 있는지, 없으면 만들 수 있는지를 확인한다.
코드
// FileTest1.java package day10; import java.io.File; import java.util.Scanner; public class FileTest1 { public static void main(String[] args) { Scanner scan = new Scanner(System.in); // 입력 도구 System.out.println("생성하려는 디렉토리명을 작성하세요 : "); String dirName = scan.nextLine(); // 디렉토리명 입력 File f = new File(dirName); // 디렉토리 객체 if (f.exists()) { System.out.println("'" + dirName + "' 디렉토리가 존재합니다."); } else { if (f.mkdirs()) { System.out.println("'" + dirName + "' 디렉토리가 생성되었습니다."); } else { System.out.println("'" + dirName + "' 디렉토리 생성에 실패했습니다."); } } scan.close(); // 입력 닫기 } }출력 결과
이 예제는
File객체를 이용해 사용자가 입력한 경로가 이미 존재하는지 먼저 확인한다.
그리고 없으면mkdirs()로 디렉토리를 만든다.
즉 이 예제는 파일 내용을 다루는 일이 아니라, 폴더가 이미 있는지 확인하고, 없으면 새로 만드는 일에 집중하고 있다.
이 예제에서 꼭 봐야 할 핵심
new File(dirName)→ 입력한 경로로File객체를 만든다.exists()→ 이미 있는지 확인한다.mkdirs()→ 없으면 디렉토리를 만든다.mkdirs()는 필요한 부모 디렉토리까지 함께 만들 수 있다.즉 이 예제는
File이 폴더 생성과 존재 여부 확인에 쓰인다는 점을 보여 주는 예제라고 보면 된다.
예제 15. 파일 정보 확인
이 예제는 특정 파일이 실제 파일인지 확인하고, 그 파일의 여러 정보를 출력하는 흐름을 보여 준다.
즉 파일 내용을 읽는 것이 아니라, 파일 자체의 정보를 확인하는 예제다.
코드
// FileTest2.java package day10; import java.io.File; import java.util.Scanner; public class FileTest2 { public static void main(String[] args) { Scanner scan = new Scanner(System.in); // 입력 도구 System.out.println("점검하려는 파일명을 작성하세요 : "); String fileName = scan.nextLine(); // 파일명 입력 File fObj = new File(fileName); // 파일 객체 if (fObj.isFile()) { System.out.println("'" + fileName + "' 파일이 존재합니다."); System.out.println("절대패스 : " + fObj.getAbsolutePath()); System.out.println("파일명 : " + fObj.getName()); System.out.println("부모 폴더 : " + fObj.getParent()); System.out.println("크기 : " + fObj.length()); System.out.println("URI 기반의 패스 : " + fObj.toURI()); } else { System.out.println(fileName + " : 파일이 아닙니다!"); } scan.close(); // 입력 닫기 } }출력 결과
이 예제에서는
isFile()로 정말 파일인지 먼저 확인한다.
그다음 절대 경로, 파일명, 부모 폴더, 파일 크기,URI같은 정보를 차례대로 출력한다.
즉 이 예제는 파일 내용을 보는 것이 아니라, 파일이 어디에 있고 어떤 정보를 가지고 있는지 확인하는 예제라고 이해하면 된다.
이 예제에서 꼭 봐야 할 핵심
isFile()→ 실제 파일인지 확인한다.getAbsolutePath()→ 절대 경로를 확인한다.getName()→ 파일명을 확인한다.getParent()→ 부모 폴더를 확인한다.length()→ 파일 크기를 확인한다.즉 이 예제는
File이 파일의 메타 정보를 확인하는 데 쓰인다는 점을 보여 준다.
예제 16. 실제 파일 생성
이 예제는 지정한 디렉토리 안에 새로운 파일을 직접 만드는 흐름을 보여 준다.
즉 이번에는 존재 여부를 확인하는 수준을 넘어서, 실제로 새 파일을 생성하는 작업까지 간다.
이 예제는 실행할 때 디렉토리 경로를 프로그램 인자로 전달해야 한다.
만약 아무 값도 전달하지 않으면args[0]이 존재하지 않아서 오류가 날 수 있다.
그래서 먼저args.length를 확인하는 흐름을 넣으면 더 안전하다.
코드
// FileTest3.java package day10; import java.io.File; import java.util.Date; public class FileTest3 { public static void main(String[] args) throws Exception { // 프로그램 인자가 없으면 안내 후 종료 if (args.length == 0) { System.out.println("디렉토리 경로를 프로그램 인자로 전달하세요."); return; } File dir = new File(args[0]); // 전달받은 디렉토리 if (!dir.exists() || !dir.isDirectory()) { System.out.println("유효하지 않은 디렉토리입니다."); return; } long time = new Date().getTime(); // 현재 시간 String newFileName = "temp" + time + ".tmp"; // 새 파일명 File file = new File(dir, newFileName); // 파일 객체 생성 if (file.createNewFile()) System.out.println(newFileName + " 파일이 생성되었습니다."); else System.out.println(newFileName + " 파일이 이미 존재합니다."); } }출력 결과
이 예제는 앞의 두 예제보다 한 단계 더 나간다.
폴더가 있는지 확인하는 데서 끝나는 것이 아니라, 그 폴더 안에 새로운 파일을 실제로 만든다.
여기서는args[0]으로 디렉토리 경로를 받는다.
즉 실행할 때 프로그램 인자로 대상 폴더를 넘겨줘야 한다.
그다음 현재 시간을 붙여서temp...tmp같은 새 파일명을 만들고,createNewFile()로 실제 파일을 생성한다.
즉 이 예제는 지정한 폴더를 확인한 뒤, 그 안에 새 파일을 만들어 넣는 흐름을 보여 준다.
이 예제에서 꼭 봐야 할 핵심
args.length로 프로그램 인자가 있는지 먼저 확인한다.args[0]으로 디렉토리 경로를 받는다.- 전달받은 경로가 유효한 디렉토리인지 먼저 확인한다.
- 현재 시간을 이용해 새 파일명을 만든다.
createNewFile()로 실제 파일을 생성한다.즉 이 예제는
File이 단순 정보 확인을 넘어서, 실제 파일 생성에도 쓰인다는 점을 보여 주는 예제라고 보면 된다.
이 파트의 흐름을 한 번에 정리하면
이번 파트에서는 파일을 읽는 방법이 꼭
FileReader만 있는 것이 아니라는 점과, 파일 내용을 직접 읽고 쓰지 않아도 파일 자체를 다루는 작업이 따로 있다는 점을 함께 봤다.
순서대로 보면
ScannerFromFile→ 파일 내용을 다른 방식으로 읽는다.FileTest1→ 디렉토리 존재 여부를 확인하고 생성한다.FileTest2→ 파일 정보를 확인한다.FileTest3→ 실제 파일을 새로 만든다.즉 이 파트는 파일 내용을 다루는 예제와, 파일 대상 자체를 관리하는 예제를 함께 묶어서 보는 파트라고 이해하면 된다.
앞에서는 키보드 입력, 파일 읽기, 파일 쓰기를 차례대로 봤다.
이제는 입력 대상이 파일이 아니라 웹 주소인 경우를 보면 된다.
처음 보면URL은 입출력과 조금 다른 주제처럼 느껴질 수 있다.
하지만 실제로는 그렇지 않다.
웹 주소에 있는 데이터를 프로그램이 읽어 오는 것도 결국 바깥 데이터를 프로그램 안으로 가져오는 입력 흐름이다.
즉 이 파트에서는URL은 완전히 다른 규칙으로 동작한다고 보기보다, 웹 주소도 결국 입력 통로를 열어서 데이터를 읽는 대상이라고 이해하면 된다.
URL에서 읽는다는 것은 무엇인가
URL에서 읽는다는 것은 웹 주소와 연결된 입력 통로를 연다는 뜻이다.
파일을 읽을 때는 파일과 프로그램 사이에 통로를 만들었다.
여기서는 웹 주소와 프로그램 사이에 통로를 만든다고 생각하면 된다.
그 중심에 있는 것이openStream()이다.
이 메서드를 호출하면 해당URL과 연결된 입력 스트림을 얻을 수 있다.
즉 웹에 있는 데이터를 읽기 위한 출발점이 생기는 것이다.
여기서 먼저 잡아야 할 핵심
URL은 바깥 데이터가 있는 위치다.openStream()을 호출하면 입력 통로를 연다.- 그다음부터는 데이터 성격에 따라 읽는 방식이 달라진다.
즉
URL이라고 해서 완전히 새로운 규칙이 생기는 것이 아니다.
먼저 입력 통로를 열고, 그다음은 우리가 앞에서 배운 입출력 흐름으로 이어진다고 보면 된다.
텍스트와 바이너리에 따라 읽는 방식이 달라진다
URL에서 가져오는 데이터는 전부 같은 모양이 아니다.
어떤 것은 글자 데이터고, 어떤 것은 이미지처럼 원본 데이터 자체가 중요하다.
그래서 읽는 방식도 달라진다.
웹 페이지 소스처럼 글자 데이터가 중요하면 문자처럼 읽어야 한다.
이럴 때는InputStreamReader,BufferedReader,Scanner같은 도구가 자연스럽게 붙는다.
반대로 이미지 파일처럼 원본 바이트 자체가 중요하면 바이트 그대로 읽어야 한다.
이럴 때는InputStream으로 읽고,FileOutputStream으로 바로 파일에 저장하는 흐름이 자연스럽다.
여기서는 이렇게 나누면 된다
- 글자 데이터 → 문자처럼 읽는다.
- 이미지 같은 원본 데이터 → 바이트 그대로 읽는다.
즉
URL에서 읽는다고 해서 무조건 한 가지 방식만 쓰는 것이 아니다.
무엇을 읽고 있는지에 따라 맞는 도구가 달라진다.
예제 17. URL의 텍스트를 줄 단위로 읽기
먼저 웹 주소에 있는 글자 데이터를 줄 단위로 읽는 예제를 본다.
이 예제는URL에서도 결국 텍스트를 읽을 때는 문자 스트림 흐름으로 간다는 점을 보여 준다.
코드
// URLTest1.java package day14; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URI; import java.net.URL; public class URLTest1 { public static void main(String[] args) { try { URL req = new URI("https://www.etnews.com/").toURL(); try (InputStream is = req.openStream(); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr)) { String data; while (true) { data = br.readLine(); if (data == null) break; System.out.println(data); } } } catch (Exception e) { System.out.println("오류: " + e.getMessage()); } } }출력 결과
이 예제에서는 먼저
openStream()으로 입력 스트림을 얻는다.
그다음InputStreamReader로 문자 기준 읽기 흐름으로 바꾸고, 다시BufferedReader를 붙여서readLine()으로 한 줄씩 읽는다.
즉 이 코드는URL에서도 결국 텍스트를 읽을 때는 문자 스트림 쪽 흐름으로 간다는 점을 보여 준다.
파일 읽기에서 봤던InputStreamReader+BufferedReader패턴이 여기서도 그대로 이어진다고 보면 된다.
다만 실제 웹 페이지는 문자 인코딩에 따라 한글이 깨질 수 있으므로, 필요하면InputStreamReader(is, "UTF-8")처럼 읽는 기준을 직접 지정할 수 있다.
그리고 이 예제에서도 사용이 끝난 스트림은 닫아야 한다.
이번 코드에서는try-with-resources를 사용했기 때문에, 블록이 끝나면 연결된 입력 자원이 자동으로 정리된다.
즉 대상이 파일이 아니라 웹 주소로 바뀌었을 뿐, 입력 통로를 열었다면 마지막 정리도 필요하다는 점은 같다.
이 예제에서 꼭 봐야 할 핵심
openStream()으로 입력 통로를 연다.InputStreamReader로 문자 기준 읽기로 바꾼다.BufferedReader를 붙여서 한 줄씩 읽는다.- 사용이 끝난 입력 자원은 정리해야 한다.
- 즉
URL도 텍스트를 읽을 때는 문자 스트림 흐름으로 간다.이 예제는
URL은 특별한 별도 세계가 아니라는 점을 가장 잘 보여 준다.
읽는 대상만 웹 주소로 바뀌었을 뿐, 흐름 자체는 우리가 이미 본 문자 입력 구조와 연결된다.
예제 18. URL의 바이너리 데이터를 파일로 저장하기
이번에는
URL에서 글자가 아니라 이미지처럼 원본 데이터 자체가 중요한 경우를 본다.
이 예제는 웹에서 받아 온 데이터를 파일로 그대로 저장하는 흐름을 보여 준다.
코드
// URLTest2.java package day14; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.net.URI; import java.net.URL; public class URLTest2 { public static void main(String[] args) { try { URL req = new URI("https://www.adobe.com/kr/products/firefly/features/media_156f60527ad304e5d5cc92b1c934b381d6ff0625c.jpg").toURL(); File dir = new File("./iotest"); // 저장 폴더 객체 if (!dir.exists()) { dir.mkdirs(); // 필요한 폴더 생성 } try (InputStream is = req.openStream(); FileOutputStream fos = new FileOutputStream("./iotest/test.jpg")) { int data; while (true) { data = is.read(); // 바이트 하나 읽기 if (data == -1) break; fos.write(data); // 바이트 하나 쓰기 } System.out.println("파일에 저장완료"); } } catch (Exception e) { System.out.println("오류: " + e.getMessage()); } } }출력 결과
이 예제는 앞의 예제와 성격이 다르다.
여기서는 웹 페이지 글자를 읽는 것이 아니라, 이미지 파일을 그대로 받아서 저장한다.
그래서 문자 스트림이 아니라 바이트 스트림 흐름으로 가야 한다.
즉InputStream으로URL에서 바이트를 읽고,FileOutputStream으로 그 바이트를 파일에 그대로 쓴다.
이 구조는 앞에서 봤던 읽고 → 쓰고 → 끝이면 멈추고 → 닫는다 흐름이 그대로 확장된 것이라고 보면 된다.
이 코드는 바이트를 하나씩 옮기기 때문에 구조를 이해하기에는 좋다.
다만 큰 파일을 처리할 때는 여러 바이트를 배열에 담아 한 번에 읽고 쓰는 방식이 더 효율적이다.
바로 다음 보충 예제에서 그 방식을 확인한다.
이 예제에서 꼭 봐야 할 핵심
- 글자가 아니라 이미지 데이터를 읽고 있다.
- 그래서 문자 스트림이 아니라 바이트 스트림으로 간다.
InputStream으로 읽고FileOutputStream으로 저장한다.- 저장 경로는
./iotest/test.jpg로 맞춰서 본다.- 이 코드는 바이트 하나씩 옮기는 기본 구조를 보여 준다.
이 예제는
URL이 항상 텍스트만 다루는 것이 아니라는 점과, 데이터가 달라지면 스트림 선택도 달라진다는 점을 보여 준다.
보충 예제. 바이트 배열로 더 효율적으로 저장하기
기본 예제는 바이트를 하나씩 옮기는 구조라서 흐름을 이해하기 좋다.
하지만 실제 파일 복사에서는 보통byte[]배열을 사용해 여러 바이트를 한 번에 읽고 쓴다.
이렇게 하면 큰 파일을 처리할 때 훨씬 효율적이다.
코드
// URLBinaryCopyBufferedExample.java package day14; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.net.URI; import java.net.URL; public class URLBinaryCopyBufferedExample { public static void main(String[] args) { try { URL req = new URI("https://www.adobe.com/kr/products/firefly/features/media_156f60527ad304e5d5cc92b1c934b381d6ff0625c.jpg").toURL(); File dir = new File("./iotest"); // 저장 폴더 객체 if (!dir.exists()) { dir.mkdirs(); // 필요한 폴더 생성 } try (InputStream is = req.openStream(); FileOutputStream fos = new FileOutputStream("./iotest/test.jpg")) { byte[] buffer = new byte[8192]; // 여러 바이트를 담을 배열 while (true) { int count = is.read(buffer); // 읽은 바이트 개수 if (count == -1) break; fos.write(buffer, 0, count); // 읽은 만큼만 파일에 저장 } System.out.println("파일에 저장완료"); } } catch (Exception e) { System.out.println("오류: " + e.getMessage()); } } }// 출력결과 // 파일에 저장완료이 예제에서는
byte[] buffer에 여러 바이트를 한 번에 담는다.
is.read(buffer)는 읽은 바이트 개수를 반환한다.
그 개수만큼fos.write(buffer, 0, count)로 파일에 저장한다.
즉 바이트 하나씩 옮기는 기본 흐름은 같지만, 실제 작업에서는 이렇게 묶어서 처리하는 방식이 더 자주 쓰인다.
예제 19. Scanner로 URL 읽기
이번에는 다시 글자 데이터를 읽는 예제다.
다만 앞의URLTest1과는 다르게BufferedReader대신Scanner를 사용한다.
즉 같은URL입력 스트림도 다른 도구와 연결해서 읽을 수 있다는 점을 보여 준다.
코드
// URLTest3.java package day14; import java.io.InputStream; import java.net.URI; import java.net.URL; import java.util.Scanner; public class URLTest3 { public static void main(String[] args) { try { URL req = new URI("https://www.etnews.com/").toURL(); try (InputStream is = req.openStream(); Scanner scan = new Scanner(is)) { while (scan.hasNextLine()) { System.out.println(scan.nextLine()); } } } catch (Exception e) { System.out.println("오류: " + e.getMessage()); } } }출력 결과
이 예제의 목적은
URLTest1과 비슷하다.
둘 다URL에서 텍스트를 읽는다.
차이는 사용하는 도구다.
앞 예제는BufferedReader를 썼고, 이번 예제는Scanner를 쓴다.
여기서 중요한 점은nextLine()으로 한 줄씩 읽고 있으므로, 확인도hasNextLine()으로 맞춰야 한다는 것이다.
next()로 읽을 때는hasNext()를 쓰고,nextLine()으로 읽을 때는hasNextLine()을 쓰면 기준이 맞다.
즉openStream()으로 입력 통로를 연 다음, 그 통로를 어떤 도구와 연결할지는 상황에 따라 달라질 수 있다는 뜻이다.
이 예제는 같은 입력 스트림도Scanner와 연결해서 읽을 수 있다는 점을 보여 준다.
그리고 여기서도 자원 정리는 똑같이 중요하다.
이번 코드에서는try-with-resources를 사용했기 때문에Scanner와 입력 통로가 자동으로 정리된다.
이 예제에서 꼭 봐야 할 핵심
URL에서 입력 스트림을 연다.- 그 입력 스트림을
Scanner와 연결한다.nextLine()을 쓸 때는hasNextLine()으로 확인한다.Scanner로도URL의 텍스트를 읽을 수 있다.- 사용이 끝난 자원은 닫아야 한다.
이 예제는
BufferedReader와Scanner가 서로 완전히 다른 세계가 아니라, 같은 입력 통로를 다루는 다른 방법이라는 점까지 보여 준다.
URLTest1과 URLTest3를 비교하면
이 두 예제는 모두 텍스트를 읽는 예제다.
차이는 읽는 도구다.
차이를 한 번에 보면
URLTest1→BufferedReader중심URLTest3→Scanner중심- 둘 다 결국
URL에서 입력 스트림을 열고 텍스트를 읽는다.즉 중요한 것은
URL자체보다, 열어 둔 입력 통로를 어떤 도구로 다룰 것인가다.
이 기준이 잡혀 있으면 예제들이 훨씬 쉽게 정리된다.
이 파트의 흐름을 한 번에 정리하면
이번 파트에서는 웹 주소도 결국 입력 대상이라는 점을 봤다.
그리고 같은URL이라도 무엇을 읽는지에 따라 읽는 방식이 달라진다는 점도 함께 확인했다.
순서대로 보면
openStream()으로URL과 연결된 입력 통로를 연다.- 텍스트를 읽을 때는 문자 스트림 쪽 흐름으로 간다.
- 이미지처럼 원본 데이터가 중요하면 바이트 스트림으로 간다.
- 같은 텍스트라도
BufferedReader나Scanner처럼 다른 도구로 읽을 수 있다.- 그리고 사용이 끝난 자원은 마지막에 정리해야 한다.
즉 이 파트의 핵심은 이것이다.
URL도 결국 바깥 데이터를 읽어 오는 입력 대상이다.
대상이 파일에서 웹 주소로 바뀌었을 뿐, 입력 통로를 열고 알맞은 도구를 붙여 읽는 흐름은 그대로 이어진다.
여기까지 오면
입출력에서 나오는 클래스 이름이 많아서 다시 헷갈릴 수 있다.
그래서 마지막에는 예제를 하나씩 다시 보는 것이 아니라, 지금까지 나온 내용을 자주 헷갈리는 기준끼리 묶어서 정리하는 것이 중요하다.
이 파트는 새로운 내용을 배우는 구간이 아니다.
앞에서 본 흐름을 한 번 더 정리해서, 실제로 어떤 상황에서 어떤 클래스를 떠올려야 하는지 머릿속 기준을 정리하는 구간이라고 보면 된다.
자원 정리는 왜 꼭 해야 하는가
입출력에서 사용하는 스트림은 단순한 변수 하나가 아니다.
파일, 키보드 입력, 웹 주소 같은 바깥 대상과 연결된 통로이기 때문에, 사용이 끝나면 정리해 주어야 한다.
쉽게 말하면 프로그램 안에서만 잠깐 쓰고 끝나는 값이 아니라, 바깥 대상과 연결된 작업 통로라고 보면 된다.
그래서 다 쓰고 난 뒤에도 그대로 두면 불필요하게 자원을 계속 잡고 있게 된다.
읽기에서도 닫아야 하고, 쓰기에서도 닫아야 한다.
특히 쓰기에서는 아직 남아 있는 내용이 완전히 기록되기 전에 프로그램이 끝날 수 있어서, 마지막 정리가 더 중요하다.
여기서 꼭 기억할 핵심
- 스트림은 바깥 대상과 연결된 통로다.
- 사용이 끝나면 닫아야 한다.
- 읽기와 쓰기 모두 마지막 정리가 필요하다.
- 쓰기에서는 기록 마무리라는 의미도 함께 있다.
즉
close()는 단순한 마무리 문장이 아니라, 입출력 작업을 제대로 끝내는 마지막 단계라고 이해하면 된다.
finally와 try-with-resources는 무엇이 다른가
앞 예제들에서 자원 정리 방식이 두 가지로 나왔다.
하나는finally에서 직접 닫는 방식이고, 다른 하나는try-with-resources를 쓰는 방식이다.
finally방식은try와catch가 끝난 뒤 마지막에 직접close()를 호출하는 구조다.
즉 개발자가 이 스트림 닫기, 저 스트림 닫기를 하나씩 써 줘야 한다.
반면try-with-resources는try(...)괄호 안에 자원을 넣어 두면, 블록이 끝날 때 자동으로 닫히는 구조다.
그래서 코드가 더 간단하고, 직접 닫는 코드를 덜 써도 된다.
차이를 짧게 정리하면
finally→ 마지막에 직접 닫는다.try-with-resources→ 블록이 끝나면 자동으로 닫힌다.즉 둘 다 목적은 같고, 자원을 더 안전하고 편하게 정리하는 쪽이
try-with-resources라고 보면 된다.
read()의 끝 -1과 readLine()의 끝 null
이 부분은 입출력에서 정말 자주 헷갈린다.
같은 파일 읽기인데 끝을 나타내는 값이 다르기 때문이다.
read()는 값을 하나씩 읽는다.
InputStream.read()는 바이트 하나씩 읽고,Reader.read()는 문자 하나씩 읽는다.
이 방식에서는 더 이상 읽을 값이 없을 때-1이 나온다.
반면readLine()은 한 줄 전체를 읽는다.
그래서 더 이상 읽을 줄이 없을 때는null이 나온다.
즉 메서드가 돌려주는 값의 형태가 다르기 때문에, 끝을 나타내는 값도 다르다.
이 차이는 꼭 분리해서 기억해야 한다
read()→ 하나씩 읽고, 끝은-1readLine()→ 한 줄씩 읽고, 끝은null즉 같은 파일 읽기라도 무엇을 읽는 메서드인가에 따라 끝을 검사하는 방식이 달라진다.
File과 FileReader는 무엇이 다른가
이 둘도 이름이 비슷해서 많이 헷갈린다.
하지만 역할은 완전히 다르다.
FileReader는 파일 내용을 읽는 클래스다.
즉 파일 안에 들어 있는 글자를 프로그램이 읽어 오는 데 사용한다.
반면File은 파일 내용을 읽는 클래스가 아니다.
파일이나 폴더의 경로, 존재 여부, 종류, 생성, 정보 확인 같은 것을 다룬다.
즉 파일 안의 내용보다, 파일이라는 대상 자체를 관리하는 클래스다.
차이를 짧게 정리하면
FileReader→ 파일 내용 읽기FileWriter→ 파일 내용 쓰기File→ 파일과 폴더 자체 관리즉
File은 입출력 대상 자체를 다루고,FileReader와FileWriter는 그 대상의 내용을 다룬다고 이해하면 된다.
바이트 스트림과 문자 스트림은 무엇이 다른가
이 둘은 데이터를 어떤 단위로 다루는가에 대한 구분이다.
즉 역할 구분이 아니라, 다루는 데이터 기준의 차이라고 보면 된다.
바이트 스트림은 데이터를 바이트 단위로 다룬다.
이미지, 동영상, 실행 파일처럼 원본 데이터 자체가 중요할 때 잘 맞는다.
문자 스트림은 데이터를 문자 단위로 다룬다.
텍스트 파일처럼 사람이 읽는 글자가 중요할 때 잘 맞는다.
이렇게 구분하면 가장 쉽다
- 글자가 중요하다 →
문자 스트림- 원본 데이터 자체가 중요하다 →
바이트 스트림즉 텍스트를 다룰 때는 보통
Reader,Writer를 먼저 떠올리고, 이미지 같은 데이터를 다룰 때는InputStream,OutputStream을 먼저 떠올리면 된다.
기반 스트림과 보조 스트림은 무엇이 다른가
이 둘은 데이터를 어떤 단위로 다루는지가 아니라, 어떤 역할을 맡는가에 대한 구분이다.
즉바이트/문자와는 다른 기준이다.
기반 스트림은 실제 대상과 직접 연결되는 기본 통로다.
예를 들어FileReader,FileWriter,FileInputStream,FileOutputStream이 여기에 해당한다.
보조 스트림은 기존 스트림 위에 붙어서 기능을 더해 주는 도구다.
예를 들어BufferedReader,InputStreamReader가 여기에 해당한다.
PrintWriter도 기존Writer위에 붙여 출력 기능을 편하게 만들 수 있지만, 파일 경로를 직접 받는 생성자도 있어서 코드에서는 단독처럼 보일 수 있다.
여기서 특히 중요한 점은 하나다.
보조 스트림은 기존 통로를 감싸서 기능을 더하는 역할로 이해해야 한다.
대부분은 먼저 만들어 둔 기존 스트림 위에 붙지만,PrintWriter처럼 파일 경로를 직접 받는 생성자를 제공해 내부적으로 연결 통로를 준비하는 경우도 있다.
차이를 짧게 정리하면
기반 스트림→ 실제 대상과 직접 연결되는 기본 통로보조 스트림→ 기존 통로를 감싸서 기능을 더하는 도구즉
FileReader가 출발점이라면,BufferedReader는 그 위에 붙어서 더 편하게 읽게 해 주는 도구라고 이해하면 된다.
write(int)와 문자열 저장은 무엇이 다른가
이 부분도 초보자가 자주 헷갈린다.
write(100)을 보면"100"이라는 글자가 파일에 들어갈 것처럼 느껴지기 쉽기 때문이다.
하지만 실제로는 다르다.
"100"은 문자1,0,0세 개를 저장하는 것이다.
반면write(100)은 숫자 세 자리를 저장하는 것이 아니라, 문자 코드100에 해당하는 문자 하나를 기록하는 것이다.
그래서 결과 파일을 보면"100"이 그대로 보이는 것이 아니라, 보통 문자 하나만 남게 된다.
앞 예제에서는 그 결과를 실제 파일 내용으로 확인했다.
이 차이는 꼭 분리해서 기억해야 한다
"100"→ 문자 세 개 저장write(100)→ 문자 코드 하나 저장즉 숫자처럼 보여도,
write(int)는 문자열 저장과 전혀 다른 동작이라고 보면 된다.
flush()와 close()는 무엇이 다른가
출력 작업에서는
flush()와close()도 구분해야 한다.
flush()는 버퍼에 남아 있는 내용을 실제 출력 대상으로 밀어내는 동작이다.
close()는 스트림을 닫고 자원을 정리하는 동작이다.
대부분의 경우 작업이 완전히 끝났다면close()만으로 충분하다.
하지만 중간에 내용을 확실히 내보내고 작업을 계속해야 할 때는flush()를 사용할 수 있다.
차이를 짧게 정리하면
flush()→ 아직 남은 출력 내용을 밀어낸다.close()→ 출력 내용을 마무리하고 자원을 닫는다.즉
flush()는 중간 전송,close()는 최종 마무리로 이해하면 쉽다.
지금까지의 흐름을 가장 짧게 정리하면
이번 단원은 클래스 이름을 따로 외우는 단원이 아니다.
데이터가 어디서 들어오고 어디로 나가는지, 그리고 어떤 대상에 어떤 통로를 연결하는지 흐름으로 이해하는 단원이다.
정말 마지막 핵심만 다시 정리하면
- 들어오면
입력, 나가면출력이다.- 텍스트를 다루면 보통
문자 스트림, 원본 데이터를 다루면 보통바이트 스트림이다.- 실제 대상과 직접 연결되면
기반 스트림, 그 위에 기능을 더하면보조 스트림이다.- 파일 내용을 다루는 것과 파일 자체를 관리하는 것은 다르다.
- 읽기와 쓰기 모두 마지막 자원 정리가 필요하다.
read()의 끝은-1,readLine()의 끝은null이다.- 출력 작업에서는
flush()와close()차이도 함께 보면 좋다.즉
입출력은 클래스 이름을 외우는 것이 아니라, 대상, 방향, 데이터 종류, 연결 방식을 기준으로 정리하면 훨씬 쉽게 이해할 수 있다.