참고
자바 예외 사용법
예외의 잘못된 사용
NOTE
예외처리는 예상치 못한 상황이나 프로그램 실행 중 발생한 오류를 처리하기 위해 설계되었습니다. 즉 예상치 못한 상황이나 실행 중 발생한 오류에 대처하기 위함이지, 제어 흐름의 일부로 사용하면 안됩니다.
try { // 성능이 2배 정도 느리다
int i = 0;
while(true)
range[i++].climb();
} catch (ArrayIndexOutOfBoundsException e) {}
Java
복사
제어 흐름으로 써버린 경우 - 따라하지 말 것!
try {
int value = Integer.parseInt("someString");
} catch (NumberFormatException e) {
// NumberFormatException을 잡아서 정상적인 흐름 제어에 사용
}
Java
복사
예외 대신 유효한 정수인지 판단하는 방법을 사용하자.
잘 설계된 API는 클라이언트가 예외를 사용할 필요가 없는 정상적인 제어 흐름을 가져야 합니다. 특정 상태에서 호출할 수 있는 상태 의존적 메서드를 제공하는 클래스는 상태 검사 메서드를 함께 제공함으로써 예외 사용을 피할 수 있습니다.
// 두 메서드 덕분에 표준 for 관용구를 사용할 수 있음
for (Iterator<Foo> i = collections.iterator(); i.hasNext(); ) {
Foo foo = i.next();
}
Java
복사
상태 검사 메서드 제공
try {
Iterator<Foo> i = collection.iterator();
while(true) {
Foo foo = i.next();
...
// 예외를 쓰게되면서 코드가 읽기 힘들어진다.
} catch (NoSuchElementException e) {
}
Java
복사
상태 검사 메서드 미제공
표준 예외의 활용
NOTE
자바 표준 라이브러리에서 제공하는 표준 예외를 사용하면 코드의 일관성을 유지하고, API 사용자에게 더 친숙하고 이해하기 쉬운 API를 제공할 수 있습니다.
다음은 자주 사용되는 표준 예외입니다:
•
IllegalArgumentException: 메서드에 부적절한 인수가 전달될 때.
•
IllegalStateException: 객체가 메서드 호출을 처리할 준비가 되어 있지 않은 상태일 때.
◦
ex) 이미 종료된 상태의 객체에 대한 작업 요청, 메서드 호출 순서가 잘못된 경우
•
NullPointerException: 프로그램이 null 객체 참조를 잘못 사용할 때.
•
IndexOutOfBoundsException: 인덱스가 배열, 리스트 또는 다른 데이터 구조의 범위를 벗어날 때.
•
UnsupportedOperationException: 객체가 요청받은 작업을 지원하지 않을 때.
실패 원자성의 중요성
NOTE
실패 원자성은 연산 도중 예외가 발생하더라도, 해당 연산 이전의 상태를 보존함으로써 프로그램의 일관성을 유지하는 특성을 가리킵니다. 즉 “전부 아니면 전무”의 원칙을 따르며, 오류 발생시 복구가 용이하고 추가적인 오류 처리 로직을 최소화할 수 있습니다.
실패 원자적을 달성하는 방법은 대표적으로 4가지가 있습니다.
1.
불변객체 사용: 불견 객체는 상태가 변하지 않으므로 자연스럽게 실패 원자적입니다. 객체의 상태를 수정할 수 없기에, 연산 중 예외가 발생해도 객체의 상태가 변할 일이 없습니다.
2.
매개변수의 유효성을 검사: 매개변수의 모든 예외 상황을 체크한 후, 마지막에 상태를 변경하면 예외가 발생해도 객체의 상태가 변하지 않습니다.
3.
방어적 복사본 작업 수행: 객체의 상태를 변경하기 전에 임시 복사본에서 모든 작업을 수행하고, 작업이 성공적으로 완료된 후에만 상태를 업데이트 합니다. 예시로 객체의 정렬에서 원본데이터를 배열로 복사하고 정렬을 진행합니다.
4.
롤백 매커니즘 구현: 연산을 수행하는 동안 예외가 발생하면, 이전 상태로 돌아가는 로직을 구현합니다.
// 요소를 제거하고 반환. 이 연산은 실패 원자적이다.
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 사용하지 않는 참조 해제
return result;
}
Java
복사
2번의 매개변수 예제
고려사항
실패 원자성을 구현할 때는 몇 가지 고려해야 할 점이 있습니다. 방어적 복사본이나 롤백 매커니즘을 사용하면 추가적인 자원이 필요하고 성능이 저하될 수 있으며 코드의 복잡성 또한 증가할 수 있습니다.
실패 원자성은 모든 상황에서 가능하거나 필요한 것은 아니지만 프로그램의 안전성과 신뢰성을 높이는데 크게 기여할 수 있으므로 실패 원자성을 염두에 두면서 개발하는 것이 좋습니다.
try-with-resources
NOTE
제대로 닫힘을 보장하는 수단으로 try-with-resource 문법을 사용하자!
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
} finally {
out.close();
} finally {
in.close();
}
}
Java
복사
try-finall구조 - 각각의 try-final을 작성해야해서 코드가 더럽고 위험하다.
static void copy(String src, String det) throws IOException {
try (InputStream in = new FileInputStream(src); OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n == in.read(buf)) > = 0)
out.write(buf, 0, n);
}
Java
복사
try-with-resource - 훨씬 읽기 좋고, 문제진단에 편하다.
•
try-with-resource문법은 자바7부터 등장했다.
•
이러한 문법을 사용하기 위해 AutoCloseable기능을 구현해야 하는데 대부분의 API는 이를 지원한다.
예외 계층
NOTE
자바에서 예외는 기본적으로 잡아서 처리하거나(catch), 처리할 수 없으면 밖으로 던져야 합니다.
자바의 예외 처리 시스템은 Throwable 클래스에서 시작합니다. Throwable 클래스는 모든 예외와 에러의 최상위 클래스로, 이 클래스에서 2개의 하위 클래스인 Error와 Exception으로 나뉩니다.
자바 예외 계층
•
Error는 시스템 레벨에서 발생하는 심각한 문제를 나타내며, 이러한 에러는 개발자가 처리할 수 없거나 처리해서는 안되는 경우가 대부분입니다. 대표적인 Error는 OutofMemoryError, StackOverflowError가 있습니다.
•
Exception은 개발자가 처리할 수 있는 예외적인 상황을 나타냅니다. 이는 다시 체크 예외와 언체크 예외로 구분됩니다.
자바의 예외는 try-catch로 잡거나 혹은 throw로 던질 수 있습니다.
catch로 예외를 service에서 잡음
만약 예외를 모두 던진다면? → 예외를 계속해서 throw함 (프로그램 종료)
모든 예외를 끝까지 던지면 다음과 같은 결과가 발생합니다.
•
자바: main()에서 예외 로그를 출력하고 시스템이 종료됩니다.
•
웹 애플리케이션: 하나의 예외로 인해 종료되는 것은 피해야 하므로, 해당 예외를 처리하고 오류 페이지를 보여줍니다.
Exception - 체크 예외
NOTE
체크 예외는 컴파일러가 예외 처리를 강제하는 예외입니다. try-cath 혹은 thorw로 무조건 예외에 대한 처리를 해줘야합니다.
public static void main(String[] args) {
// try-catch 구조가 필수다!
try {
String line = readFirstLineFromFile("/path/to/file.txt");
System.out.println(line);
} catch (IOException e) {
System.out.println("An error occurred while reading the file.");
e.printStackTrace();
}
}
public static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
Java
복사
IOException은 대표적인 체크 예외다.
•
체크 예외는 일반적으로 호출하는 쪽에서 복구하리라 여겨지는 경우에 사용합니다.
•
컴파일러에서 개발자가 예외를 누락하지 않게 할 수 있지만 무조건적으로 처리해야 하고 의존관계에 따른 단점도 존재합니다.
◦
ex) Repository에서 발생한 SQL Exception이 Service에 올라올 수 있습니다.
Runtime Exceptions - 언체크 예외
NOTE
언체크 예외의 경우 프로그램 실행 중 발생할 수 있는 오류를 나타내며, 대부분의 프로그래밍 오류를 나타냅니다. 언체크 예외는 예외처리가 컴파일 시점에 강제되지 않습니다.
int[] numbers = {1, 2, 3};
// 인덱스가 배열 범위를 벗어남 - ArrayIndexOutOfBoundsException
System.out.println(numbers[3]);
Java
복사
catch로 잡지 않으면 자동으로 throw해준다.
•
언체크 예외는 catch-try가 강제되지 않으며 예외 발생시 자동으로 throws처리 해줍니다.
•
실제 웹개발에서는 대부분 RuntimeException을 통해서 던지며, 대부분 프로그래밍 단계에서 해당 예외를 사용한다.
필요없는 언체크 예외 사용은 피하자
NOTE
체크 예외는 메서드 사용자가 예외를 처리하도록 강제하는 예외 유형으로, API 설계 시 잘못 사용되었을 때 사용성을 크게 저하시킬 수 있습니다.
체크 예외는 이론상으로는 예외 상황에 대처하고 복구할 수 있도록 해주지만, 실제로 대부분의 예외 상황은 개발자가 복구가 불가능하므로 오히려 예외 처리 로직이 과도하게 복잡해질 수 있습니다.
스프링 프레임워크에서는 일반적으로 런타임 예외를 사용하여 예외 처리를 단순화합니다. 특히 ControllerAdvice를 활용해 애플리케이션 전반의 런타임 예외를 일괄적으로 처리할 수 있게 해줍니다.
SQLException을 해결이 불가능한 위치인 Controller, Service에서 의존하게되며 연쇄적으로 던지게된다.
만약 기술변경으로 다른 예외가 터지는경우 전부 수정해줘야 한다.
public class CheckedAppTest {
@Test
void checked(){
Controller controller = new Controller();
Assertions.assertThatThrownBy(() -> controller.request())
.isInstanceOf(Exception.class);
}
// 컨트롤러
static class Controller {
Service service = new Service();
public void request() throws SQLException, ConnectException {
service.logic();
}
}
// 서비스 (DI객체의 예외가 전파된다.)
static class Service{
Repository repository = new Repository();
NetworkClient networkClient = new NetworkClient();
public void logic() throws SQLException, ConnectException {
repository.call();
networkClient.call();
}
}
// 네트워크
static class NetworkClient {
public void call() throws ConnectException {
throw new ConnectException("연결 실패");
}
}
// 레포지토리
static class Repository{
public void call() throws SQLException {
throw new SQLException("ex");
}
}
}
Java
복사
예외의 연쇄
언체크 예외(RuntimeException) 활용
NOTE
체크 예외가 발생한경우 언체크 예외로 변경해서 던져주게된다면 예외의 연쇄가 발생하지 않게됩니다.
체크 예외에 있던 예외연쇄 문제를 해결할 수 있다!
public class UnCheckedAppTest {
@Test
void unchecked(){
Controller controller = new Controller();
Assertions.assertThatThrownBy(() -> controller.request())
.isInstanceOf(Exception.class);
}
// 컨트롤러
static class Controller {
Service service = new Service();
public void request(){
service.logic();
}
}
// 서비스 => thorw 코드가 없어졌다!
static class Service{
Repository repository = new Repository();
NetworkClient networkClient = new NetworkClient();
public void logic(){
repository.call();
networkClient.call();
}
}
// 네트워크
static class NetworkClient {
public void call(){
throw new RuntimeConnectException("연결 실패");
}
}
// 레포지토리
static class Repository{
public void call() {
try{
runSQL();
}catch (SQLException e){
throw new RuntimeSQLException(e);
}
}
public void runSQL() throws SQLException {
throw new SQLException("ex");
}
}
// ConnectionException 체크 예외 => 언체크 예외
static class RuntimeConnectException extends RuntimeException{
public RuntimeConnectException(String message){
super(message);
}
}
// SQLException 체크 예외 => 언체크 예외
static class RuntimeSQLException extends RuntimeException{
public RuntimeSQLException(Throwable cause){
super(cause);
}
}
}
Java
복사
예제 코드
예외 전환 시, 기존 예외
NOTE
체크 예외 ⇒ 언체크 예외로 수정을 한다면 반드시 기존 예외를 포함시켜야 합니다. 기존 예외를 포함하지 않으면 Stack Trace에서 정확한 예외원인을 파악할 수 없습니다.
static class Repository{
public void call() {
try {
runSQL();
}catch (SQLException e){
// 예외를 포함한다 → StackTrace 정상 출력됨.
throw new RuntimeSQLException(e);
}
}
// 예외 발생 메소드
private void runSQL() throws SQLException {
throw new SQLException("ex");
}
}
// SQLException 체크 예외 => 언체크 예외
static class RuntimeSQLException extends RuntimeException{
public RuntimeSQLException(Throwable cause){
super(cause);
}
}
Java
복사
Repository (체크예외 → 런타임예외로 변경)
@Test
void printEx() {
Controller controller = new Controller();
try {
controller.request();
} catch (Exception e) {
// 예외 테스트를 위해 잡는다.
log.info("ex",e);
}
}
Java
복사
테스트 코드
만약 예외변환시 체크 예외를 제대로 포함해서 던져주지 않으면 StackTrace에서 최초의 원인이 되는 Exception을 찾을 수 없게됩니다.
체크 예외를 포함하지 않은 경우
체크 예외를 제대로 포함해서 던져준 경우 SQLException 발견