자바 프로그래밍 랭귀지를 처음 시작하는 프로그래머들은 흔히 예외 (exceptions)를 무시하거나 잊어버리는 경우가 많다. 예외처리의 이점은 런타임시에 프로그래머가 작성한 코드로 인해 발생할 수 있는 문제점들을 미리 생각해 보게 한다는 데에 있다. 이번 테크 팁은 예외처리를 다루면서 예외를 해당 소스에 가깝게 두어야 하는 이유와 예외의 설명이 명확하면 명확할수록 좋은 이유를 설명한다. 예외처리를 하는 목적은 사용자가 소프트웨어를 좀 더 순조롭게 이용할 수 있도록 돕는 것이다.
사용자가 파일로부터 무언가를 읽고자 할 때 발생하는 일들을 생각해보자. 사용자가 커맨드 라인 파라미터로 파일의 이름을 지정하면, 이의 응답으로 지정한 파일이름의 File 객체가 생성된다. 그리고 나서 File의 컨텐츠를 읽기 위해 FileReader 객체가 생성되고, File의 컨텐츠를 읽기 쉽도록 BufferedReader가 연쇄(chain)된다.
예외처리를 하지 않으면 프로그램이 잘못 작동할 수 있다. 예를 들어 사용자가 프로그램을 실행할 때 파일 이름을 명시하지 않거나, 파일 이름을 명시하지만 그 이름에 대응하는 파일이 없을 수 있다. 이와 같이 잘못될 가능성이 있는 것들에 대해 고려하는 것이 바로 예외처리이다.
특정한 예외처리 액션을 하지 않았을 때 발생하는 현상을 보면 예외처리의 중요성을 알 수 있다. 다음 BadFileAccess프로그램을 시작해 보자.
import java.io.File;
import java.io.FileReader;
import java.io.BufferedReader;
public class BadFileAccess {
// do NOT code like this
public static void main(String[] args) {
String fileName = args[0];
File aFile = new File(fileName);
FileReader fileReader = new FileReader(aFile);
BufferedReader buffReader =
new BufferedReader(fileReader);
while (true) {
System.out.println(buffReader.readLine());
}
}
}
BadFileAccess는 컴파일이 되지 않는 대신에 java.io.FileNotFoundException 과 java.io.IOException, 2개의 예외를 처리하지 않았다고 나타낸다. FileNotFoundException는 java.io.Exception을 상속하는 IOException를 상속한다. 예외처리를 위해서는 특별한 액션을 취하는 대신 일반적으로 예외 계층구조의 이점을 이용한다. 다시 말하자면, 2개 예외를 모두 다음과 같은 클래스의 Exception 인스턴스로 간주해 버릴 수 있다는 것이다.
import java.io.File;
import java.io.FileReader;
import java.io.BufferedReader;
public class BadFileAccess {
// do NOT code like this
public static void main(String[] args) {
String fileName = args[0];
File aFile = new File(fileName);
try {
FileReader fileReader = new FileReader(aFile);
BufferedReader buffReader =
new BufferedReader(fileReader);
while (true) {
System.out.println(buffReader.readLine());
}
} catch (Exception e) {
// especially avoid coding like this
}
}
}
코드를 컴파일 하자. 이번에는 코드가 문제없이 컴파일되는 것을 볼 수 있다. 하지만 이 코드는 많은 런타임 에러를 포함하고 있다. 이러한 에러중의 하나를 확인해 보기 위해 다음과 같이 매개변수를 지정하지 않고 코드를 실행해보자.
java BadFileAccess
이 때 ArrayIndexOutOfBoundsException가 발생하는 것을 볼 수 있다. 이 예외는 프로그램의 args[0]에 대한 레퍼런스에 의해 발생되며 프로그램이 종료되게 만든다. 이와 같은 현상이 바로 사용자가 파일이름을 지정하지 않고 프로그램을 실행했을 때 일어나야 하는 바람직한 모습이다. 하지만 ArrayIndexOutOfBoundsException 이 발생함에도 불구하고 이것이 사용자에게 적절한 파일명을 입력해야 한다는 사실을 알려주는 역할을 하는 것을 기대하기는 어렵다.
예외처리의 방법을 살펴보자. 한가지 적절한 접근방법은 2001/01/09 Handling Uncaught Exceptions에서 설명한 기법을 이용하는 것이다. 그 기법을 사용하려면 다음 라인을 try block에 포함시켜야 한다.
String fileName = args[0];
위의 코드를 포함한 BadFileAccess프로그램을 보자.
import java.io.File;
import java.io.FileReader;
import java.io.BufferedReader;
public class BadFileAccess {
// do NOT code like this
// this is the WORST version
public static void main(String[] args) {
try{
String fileName = args[0];
File aFile = new File(fileName);
FileReader fileReader = new FileReader(aFile);
BufferedReader buffReader =
new BufferedReader(fileReader);
while (true) {
System.out.println(buffReader.readLine());
}
} catch (Exception e) {
// especially avoid coding like this
}
}
}
코드를 컴파일하고 실행하자. 프로그램은 컴파일되지만 이를 실행시켰을 때 ArrayIndexOutOfBoundsException는 보이지 않는다. 때문에 이 기법은 사용자에게 어떤 문제가 있는지를 알리는 데에는 적당치 않다. 여기서 필요한 것은 일반적인 Exception의 캐치 블록 전에 ArrayIndexOutOfBoundsException을 캐치하고 사용자가 문제의 원인을 파악할 수 있는 메시지를 제공하는 것이다. 이와 같은 메시지를 포함하고 있는 BadFileAccess 프로그램을 살펴보자.
import java.io.File;
import java.io.FileReader;
import java.io.BufferedReader;
public class BadFileAccess {
// do NOT code like this
public static void main(String[] args) {
try {
String fileName = args[0];
File aFile = new File(fileName);
FileReader fileReader = new FileReader(aFile);
BufferedReader buffReader =
new BufferedReader(fileReader);
while (true) {
System.out.println(buffReader.readLine());
}
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("correct usage: " +
"java BadFileAccess <pathToFile>");
} catch (Exception e) {
// especially avoid coding like this
}
}
}
업데이트된 프로그램을 컴파일하고 실행해보자. 이것을 실행하면 다음메시지를 보게 된다.
correct usage: java BadFileAccess <pathToFile>
서로 다른 여러 개의 Arrays가 액세스되는 프로그램의 main() 메소드를 상상해보자. 사용자는 모든 ArrayIndexOutOfBoundsException들에 동일한 메시지가 나타나는 것을 원치는 않을 것이다. 일반적으로 예외처리를 할 때는 예외처리 구문을 예외가 일어나는 원인이 되는 소스에 가깝게 두는 것이 좋다. 이를 위해서는 다음의 라인을 이동시켜야 한다.
String fileName = args[0];
그리고 다음과 같이 try 블록의 외부에 변수를 선언하고 초기화한다.
import java.io.File;
import java.io.FileReader;
import java.io.BufferedReader;
public class BadFileAccess {
// do NOT code like this
public static void main(String[] args) {
String fileName = null;
try {
fileName = args[0];
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("correct usage: " + "" +
"java BadFileAccess <pathToFile>");
}
try {
File aFile = new File(fileName);
FileReader fileReader = new FileReader(aFile);
BufferedReader buffReader =
new BufferedReader(fileReader);
while (true) {
System.out.println(buffReader.readLine());
}
} catch (Exception e) {
// especially avoid coding like this
}
}
}
이번에는 사용자가 유효하지 않은 파일 이름을 지정하는 경우를 생각해보자. 이 예제에서 지명된 noFile이라는 파일이 존재하지 않는다고 가정하고 다음 커맨드를 실행시키자.
java BadFileAccess noFile.txt
이 커맨드를 실행시켰을 때 어떠한 에러 메시지도 발생하지 않기 때문에 자칫 프로그램이 정상적으로 여겨질 수 있다. 하지만 실은 바로 이것이 문제이다. 사용자는 noFile.txt라는 이름의 파일이 존재하지 않는다는 메세지를 받아야 한다. FileReader생성자는 FileNotFoundException를 발생시킬 수 있다는 것을 기억하자. 유효하지 않은 파일의 예외를 처리하기 위해서는 또 다른 캐치 블록을 추가해야 한다.
import java.io.File;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
public class BadFileAccess {
// do NOT code like this
public static void main(String[] args) {
String fileName = null;
try {
fileName = args[0];
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("correct usage: " +
"java BadFileAccess <pathToFile>");
}
try {
File aFile = new File(fileName);
FileReader fileReader = new FileReader(aFile);
BufferedReader buffReader =
new BufferedReader(fileReader);
while (true) {
System.out.println(buffReader.readLine());
}
}catch (FileNotFoundException e){
System.out.println(" There is no file at " +
fileName);
} catch (Exception e) {
// especially avoid coding like this
}
}
}
파일 이름으로 noFile.txt을 지정하고 프로그램을 컴파일, 실행하자. 이제 다음과 같은 메시지를 보게 된다.
There is no file at noFile.txt
여기에서, 우리는 2가지 예외 사항을 처리하는 코드를 추가했다. 파일이름을 지정하지 않고 프로그램을 실행했을 때와 유효하지 않은 파일이름을 지정하여 프로그램을 실행시키는 경우가 바로 그것이다. 하지만 애플리케이션의 사용을 돕기 위한 방법에는 몇 가지가 더 있다. 가령, 파일이름을 지정하지 않거나 유효하지 않은 파일이름을 지정하면 파일의 위치를 찾아낼 때 문제가 생긴다는 것을 지적할 수 있다. 뿐만 아니라, 각각의 상황에 맞는 고유한 예외를 생성해 낼 수도 있다. 가령, 파일명을 지정하지 않았을 경우에 적절한 에러 메시지와 함께 FileNotFoundException 을 생성하면 더 이상 ArrayIndexOutOfBoundsException을 캐치할 필요가 없다. 한 개의 파라미터가 존재하는지를 확인하고 만약 존재한다면 그 위치에 있는 파일을 열고, 존재하지 않으면 새로운 FileNotFoundException 을 발생시킨다. 이때 FileNotFoundException 는 이 상황에 맞는 적절한 에러 메시지를 포함하고 있어야 한다.
import java.io.File;
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
public class BadFileAccess {
// do NOT code like this
private static String fileName;
public static void main(String[] args) {
try {
FileReader fileReader = new FileReader(
getFile(args));
BufferedReader buffReader =
new BufferedReader(fileReader);
while (true) {
System.out.println(buffReader.readLine());
}
}catch (FileNotFoundException e){
System.out.println(
"Could not locate a file: " +
e.getMessage());
} catch (Exception e) {
// especially avoid coding like this
}
}
private static File getFile(String[] args)
throws FileNotFoundException {
if(args.length == 1){
fileName = args[0];
} else {
throw new FileNotFoundException(
"correct usage: " +
"java BadFileAccess <pathToFile>");
}
File aFile = new File(fileName);
if (aFile.exists()) {
return aFile;
} else {
throw new FileNotFoundException(
"There is no file at " + fileName);
}
}
}
파일이름을 지정하지 않고 프로그램을 컴파일하고 실행해보자. 프로그램은 FileNotFoundException을 발생시키고 다음과 같은 메시지를 출력한다.
Could not locate a file: correct usage: java BadFileAccess <pathToFile>
유효하지 않은 파일명을 지정한 후 프로그램을 실행해 보자.
java BadFileAccess noFile.txt
프로그램은 FileNotFoundException을 발생시키고 다음과 같은 메시지를 출력한다.
Could not locate a file: There is no file at noFile.txt
2003/4/22 reusing exceptions을 참고하면 FileNotFoundException의 다중 인스턴스 생성을 피할 수 있다. 이것은 동일한 예외의 각각의 인스턴스들에 대한 속성을 설정하는 Singleton의 사용법을 알려준다. 혹은 대안으로 예외를 재발생 시키는 대신에 getFile() 메소드내에서 예외처리를 할 수도 있다. 하지만 어떤 경우든, 실제 텍스트 파일을 읽어들이면 다음 블록은 사용자가 아직 해결해 놓지 않은 문제를 발생시키게 된다.
while (true) {
System.out.println(buffReader.readLine());
}
"This is a sample document." 문장을 기입한 realFile.txt라는 간단한 텍스트 파일을 생성하고 애플리케이션을 실행하자.
java BadFileAccess realFile.txt
이를 실행하면 "This is a sample document." 문장을 포함하는 출력값을 보게 되는데 계속되는 라인에 "null"이라는 단어가 이를 뒤따른다. "null" 이라는 단어로 이루어진 문장은 사용자가 애플리케이션의 실행을 중지시킬 때까지 출력값을 채우기를 계속한다. 이러한 연속적인 결과는 파일로부터 읽기를 멈추는 명령이 프로그램내에 없기 때문에 파일의 끝에 다다를 때까지 계속되기 때문이다.
여기서 사용자를 도울 수 있는 방법은 무엇이 있을까? BufferedReader 클래스의 readLine() 메소드는 IOException 을 발생시킨다. 하지만 사용자가 IOException이 발생하는 것을 보더라도 문제의 원인을 파악할 수는 없다. 그렇지만 여기서 적어도 사용자에게 어떤 문제가 발생한 것인지를 알려주는 메시지를 포함할 수는 있다. 예를 들면, 다음 FileAccess프로그램은 IOException 이 발생하면 이에 대한 설명을 해주는 메시지를 디스플레이한다.
import java.io.FileReader;
import java.io.FileNotFoundException;
import java.io.File;
import java.io.BufferedReader;
import java.io.IOException;
public class FileAccess {
private static String fileName;
public static void main(String[] args) {
FileReader fileReader = null;
try {
fileReader = new FileReader(getFile(args));
readFile(fileReader);
} catch (FileNotFoundException e) {
System.out.println("Could not locate a file: " +
e.getMessage());
}
}
private static File getFile(String[] args)
throws FileNotFoundException {
if(args.length == 1){
fileName = args[0];
}
else {
throw new FileNotFoundException("correct usage: "
+ "java FileAccess <pathToFile>");
}
File aFile = new File(fileName);
if (aFile.exists()) {
return aFile;
} else {
throw new FileNotFoundException(
"There is no file at " + fileName);
}
}
private static void readFile(FileReader fileReader) {
BufferedReader buffReader =
new BufferedReader(fileReader);
String temp = "";
try {
System.out.println("== Beginning of the fil" +
fileName + " ==");
while ((temp = buffReader.readLine()) != null) {
System.out.println(temp);
}
System.out.println("== End of the file: " +
fileName + " ==");
} catch (IOException e) {
System.out.println("There was a problem reading:"
+ fileName);
}
}
}
다음과 같이 애플리케이션을 실행하면
java FileAccess realFile.txt
이하의 출력사항을 보게 된다.
== Beginning of the file: realFile.txt == This a sample document. == End of the file: realFile.txt ==
예외처리에 관한 자세한 정보는 자바 튜토리얼의 Catching and Handling Exceptions을 참고한다.
"Java SE" 카테고리의 다른 글
- JSSE 이용한 안전한 커뮤니케이션 (댓글 2개 / 트랙백 0개) 2004/08/31
- 3D 화면(scene)에 빛 효과 주기 (댓글 1개 / 트랙백 0개) 2004/07/30
- Java SE & Java SE for Business 지원 로드맵 (댓글 0개 / 트랙백 0개) 2009/09/11
- 클래스에서 enhanced For-Loop 사용 (댓글 0개 / 트랙백 1개) 2007/10/09
- VARIABLE CONTENT로 메세지 포맷하기 (댓글 7개 / 트랙백 2개) 2003/08/19
- CONTENTPANE 작업의 변화 (댓글 1개 / 트랙백 0개) 2004/11/11
- 새로운 포매터로 출력물 포맷하기 (댓글 1개 / 트랙백 0개) 2004/10/27
- 스윙 유저 인터페이스에서 컴포넌트의 방향성 (댓글 2개 / 트랙백 0개) 2003/09/26
- 쓰레드의 상태정보를 저장할 때 사용되는 THREADLOCAL 변수들 (댓글 4개 / 트랙백 0개) 2003/12/12
- 2개의 스트링이 같은 경우는? (댓글 2개 / 트랙백 0개) 2004/05/27
댓글을 달아 주세요
예외 처리할 때 도움되는 글이네요..
2007/09/04 15:54예외 처리할때 무조건 Exception 만 쓰고 있는데.
앞으로는 가려서 써야 겠네요..
굉장히 유용했습니다^^
2007/09/06 10:23짧지만 어떻게 구조적으로 예외처리를 이끌어 줘야하는지를 배운것 같네요..
요글부터 차근차근 공부해보겟습니다^^
sdnkorea.com엔 유용한 정보가 많은것 같습니다.^^
2007/09/08 08:30처음보는게 많네요~
파일관련된 exception 처리에 대해 체계적으로 정리 잘되었네요
2007/09/14 20:59오호 이런 방법이 있었네요. 역시~
2007/09/19 02:32좋은 정보 감사해요~
2007/09/19 05:54