파일 입출력을 다뤄봅시다. 사실, 예외나, 인터페이스를 하지도 않았는데 뜬금없이 이것을 다루는 이유는, 네. 맞습니다. 같이 다루기 위해서입니다. 여기에서는 파일을 읽는 것에 대해서만 중점적으로 다루고, 중간 중간에 예외나, 인터페이스에 대해 다루겠습니다. 그리고, 인코딩 디코딩도 중요하기 때문에 다시 한 번 짚고 넘어가겠습니다.

 


 먼저, 1.txt에 저장된 내용을 보겠습니다.

 

 Hello Java, ... 이런 내용들이 들어 있습니다. 이 파일을 읽어서 콘솔에 그대로 출력하고자 합니다. 물론, src 아래에 1.txt가 있습니다.

 

 

 파일 객체를 하나 생성합니다. src\\1.txt는 윈도우에서, 프로젝트의 루트로부터의 경로 /src/1.txt를 의미합니다. 파일객체 하나를 얻어옵니다. 그리고, try resource문 안에, FileInputStream is를 하나 생성합니다. 파일과, 스트림을 연결합니다. 그리고 7번째 줄에서부터 10번째 줄까지는, 그냥, 스트림에서 파일의 끝을 만날 때 까지 계속 읽어들입니다.

 

 

 결과는 위와 같이 나옵니다. 스트림과 파일을 연관시킨 다음에, 스트림에서 내용을 읽은 것 뿐입니다. 이제 하나 하나 간단하게 보겠습니다.

 


 만약에 없는 파일을 read 하려고 하면 어떨까요? 파일이 없다는 익셉션이 떠야 하지 않을까요? 그런데, 이것을 따로 catch를 하지 않으면, 에러가 난다는 것을 볼 수 있는데, 코드를 조금만 바꾸어서, 아예 없는 파일과 스트림을 연관시켜서, 예외가 어떤 식으로 가는지 보겠습니다.

 

 코드는 위와 같습니다. 당연하게도, src 디렉토리 안에 1.txt는 있지만, 2.txt는 없는 상황입니다.

 

 

 디버그 트레이스를 해 보면, FileNotFoundException이 떡하니 보입니다.

 

 

 이 부분입니다. 이것은, open0이 호출하였습니다. 거꾸로 타고 내려가 보겠습니다.

 

 

  open0을 보니, native. 그리고 FileNotFoundException을 넘겨버리네요.

 

 

 open0을 호출한 open에서도 딱히, catch 하는 부분이 없습니다. 이것을 호출한 친구는 FileInputStream의 생성자입니다.

 

 

 여기도 찾아보니, 딱히 없습니다. 그러면, 어디로 가야 할까요? FileInputStream의 생성자를 호출한 Main 클래스로 가면 됩니다.

 

 

 11번째 줄을 보면, catch 절이 있습니다. IOException이면, IO error라고 출력하게끔 합니다. 여기서 문제는 파일을 찾을 수 없다는 것이 IOException이냐인데, 해당 예외 클래스를 보겠습니다.

 

 

 파일을 찾을 수 없는 예외가, IOException을 extends 한다고 되어 있습니다. 즉, 파일을 찾을 수 없는 것은, IO 예외인 것입니다.

 

 

 그리고 IO 예외는 예외이고요. 즉, FileNotFoundException보다는 IOException이, IOException보다는 Exception이 상위 예외임을 알 수 있습니다.

 

 

 상위 예외를 먼저 잡아버리면 이미 잡혀버렸기 때문에, 다음에 하위 예외가 있다고 하더라도 이미 잡혔다고 합니다. 당연하게도, 예외가 먼저 잡혔다면, IO exception 또한 잡혔을 것이기 때문입니다. 그러면, 여기까지 한 이야기를 그림으로 정리해 보겠습니다.

 

 

  파일 경로를 가지고 open을 하는 과정에서 예외가 떴다. 계속 누가 호출했는지 따라 나오다가, IOException을 catch하는 로직을 만났다. 그래서 catch 절에 들어가서 IO Error를 출력하였다. 당연하게도, read의 경우에도, 무언가를 throw 하니 봐두시는 것도 좋습니다.

 

 read의 경우에는, EOF를 만났을 때에는 -1을, 그렇지 않다면, 읽을 수 있거나, 예외가 발생할 때 까지 block이 됩니다. 읽은 바이트 수를 리턴하고요. 즉, read의 리턴값이 양수일 때 까지 계속 읽어들이면 됩니다.

 


 read 메서드는, byte 배열을 리턴하니, 이것을 String으로 변환을 해야 합니다. toString을 쓰면 어떨까요? 그냥 읽어만 보면, 스트링으로 변환하는 것 같지만, 사실은 byte 배열에서 toString을 호출하면 Object의 toString을 부릅니다.

 

 

 그러면, 이것은 class의 이름하고, 객체의 해시코드 값을 출력합니다. 그러면 어떻게 해야 할까요?

 

 

 해결책은 간단합니다. 단지, 새롭게 String을 생성할 때, byte 배열을 인자로 넘겨주면 됩니다.

 

 

 그러면, Charaset의 defaultCharset으로 디코딩을 하게 됩니다. 제 컴퓨터의 경우에는 UTF-8이였습니다. 그러면 만약에 src/1r.txt가 UTF-16LE로 인코딩이 되어 있다면 어떨까요?

 

 

 밑에 있는 UTF-16 LE는 인코딩입니다.

 

 

 그런데, 글자가 깨져서 나옵니다. 이것은 UTF-16 LE로 인코딩이 된 것을 UTF-16 LE가 아닌 다른 것으로 디코딩을 했기 때문입니다. 쌍이 맞지 않는 셈입니다.

 

 

 쌍이 맞지 않는다면, 디코딩을 잘못 할 수 있을 겁니다. 그러면 어떻게 해야 할까요? 디코딩 할 charset을 추가로 생성자에 명시하면 됩니다.

 

 

 9번째 줄의 String 생성자를 호출하는 것을 보면, byte 배열은 그대로입니다만, charsetName을 UTF-16LE로 명시해 줬음을 알 수 있습니다. 이 정도만 짚고 넘어가도 될 듯 싶네요.