예외 처리(Exception Handling) Part 2: [throws, try-catch]

Frog Lemon·2025년 10월 14일
0

Java

목록 보기
5/6
post-thumbnail

이전에 썼던💡예외의 개념과 종류 에서는 예외란 무엇인지, Checked 예외와 Unchecked 예외에 대해서 알아보았다. 이번에는 본격적인 예외 처리 방법에 대해 다루고자 한다. 특히 throwstry-catch 구문을 중심으로 각각의 역할과 사용 시점을 설명할 것이다.


1. Throws

Java에서 메서드가 호출 중에 예외를 발생시킬 가능성이 있는 경우 그 예외를 메서드 선언부에 명시하는 것이 바로 throws 키워드다.

특히 Checked 예외는 컴파일러가 예외 처리를 강제하기 때문에 메서드 내부에서 직접 처리하지 않고 상위 호출자에게 전달하려면 반드시 throws를 사용해야 한다.

public void readFile(String path) throws IOException {
    FileReader reader = new FileReader(path);
    // 파일 읽기 로직
}

위 코드에서 readFile 메서드는 IOException을 처리하지 않고 호출자에게 전달한다.
따라서 호출하는 쪽에서 try-catch 로 예외를 처리하거나 다시 throws로 던져야 한다.

💡개발자는 throws로 던져진 예외를 어느 계층에서 처리할지 명확히 결정해야 한다.
예를 들어 Controller–Service–Repository 구조에서는, Repository에서 발생한 예외를 어디까지 전파할지가 중요한 설계 포인트다.
이에 대한 구체적인 논의는 다음 글에서 다루겠다.


2. try-catch

예외가 발생하면 기본적으로 try-catch를 사용하여 예외를 처리할 수 있다.

try{
	//예외가 발생할 가능성이 있는 로직
}catch(Excetpion e){
	//예외가 발생시 실행할 로직
}

기본적인 형태는 위와 같이 try 구간과 catch 구간으로 나뉘어져 있다.

2-1. try

  • 예외가 발생할 가능성이 있는 로직이 위치한다.

2-2. catch

  • 예외가 발생하면 catch 블록이 실행된다.
  • catch()의 파라미터에는 처리할 예외 클래스를 지정한다.
  • 예를 들어 catch(IOException e)는 IO 관련 예외만 처리한다.

주의점

  • catch(Exception e)는 모든 예외를 포괄하기 때문에 예외별 맞춤 처리 불가능하다.
  • 일반적으로 구체적 예외 → 상위 예외 순서로 catch를 작성하는 것이 좋다.

예제

try {
    FileReader reader = new FileReader("data.txt");
} catch (FileNotFoundException e) {
    System.out.println("파일이 존재하지 않습니다: " + e.getMessage());
} catch (IOException e) {
    System.out.println("입출력 오류 발생: " + e.getMessage());
} catch(Exception e){
    System.out.println("Exception 예외까지 도달: " +  e.getMessage());
}

위의 코드를 보자.
1. 'data.txt' 파일이 없으면 → FileNotFoundException 발생
2. 파일 읽는 도중 다른 IO 문제가 생기면 → IOException 발생
3. 위 두 경우에 해당하지 않는 기타 문제 발생 시 → Exception 발생


3. multi-catch

만약 하나의 catch 문에 복수의 예외를 잡고 싶다면 아래와 같이 multi-catch로 구현할 수 있다.(Java 7 이상에서 사용 가능)

  • 처리 로직이 동일한 경우만 사용 가능

  • multi-catch 내부에서 e 변수는 final 취급 → 재할당 불가

try {
    // 파일 읽기 또는 DB 처리
} catch (IOException | SQLException e) {
    e.printStackTrace(); // 두 예외를 동일하게 처리
}

4. finally

만약 try-catch로 감싼 로직이 정상 동작하든, 예외가 발생하든 반드시 실행시키고 싶은 로직이 있다면 finally 를 이용하면 된다.
finally가 중요하게 쓰이는 대표적인 경우는 파일, DB, 소켓 등과 같은 자원 관리이다.
예외 발생 시 자원을 닫지 않으면 메모리 누수 / 리소스 누수가 발생할 가능성이 있기 때문이다.

public void readFileWithoutFinally() {
    FileReader reader = null;
    try {
        reader = new FileReader("data.txt");
        int data = reader.read();
        // 파일 읽기 로직 수행
    } catch (IOException e) {
        e.printStackTrace();
    }
    // reader.close()가 예외 발생 시 호출되지 않아 자원 누수 가능
}

위 코드는 예외가 발생하면 reader.close()가 호출되지 않아 자원 누수 위험이 존재한다. 이를 보완한 코드를 살펴보자.
예외 발생 여부와 관계없이 finally 블록 내부 코드는 항상 실행되기 때문에 자원 해제를 강제하여 메모리/리소스 누수를 방지한다.


<finally로 보완한 안전한 코드>

public void readFileWithFinally() {
    FileReader reader = null;
    try {
        reader = new FileReader("data.txt");
        int data = reader.read();
        // 파일 읽기 로직 수행
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (reader != null) {
                reader.close(); // 예외 여부와 관계없이 항상 호출
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

그렇다면 개발자는 파일, DB, 소켓등을 사용할 때 매번 직접 finally를 붙여야 할까? 물론 메모리 / 리소스 누수를 방지하기 위해서 반드시 작성해야할 것이다.
하지만 문제점이 있다. 그것은 개발자가 finally를 실수로 안붙이는 휴먼에러의 가능성이 존재한다.
이를 보완해줄 방법이 존재한다.


5. try-with-resources

Java 7 이상에서는 파일, DB, 소켓 등 자원을 안전하게 자동으로 해제하기 위해 try-with-resources 구문을 사용할 수 있다.
이 구문을 사용하면 finally 블록을 직접 작성하지 않아도 자원을 자동으로 닫을 수 있어 코드가 간결해지고 예외 안전성이 높아진다.

특징

  1. 자동 자원 관리

    • try 괄호 안에서 선언한 자원(AutoCloseable 구현 클래스)은 try 블록 종료 시 자동으로 close() 호출
  2. 예외 발생 여부와 상관없이 실행

    • Checked/Unchecked 예외 모두 처리 가능
  3. 코드가 간결해짐

    • finally 블록에서 자원을 닫는 반복적인 코드를 제거

기본 문법

try (자원 선언) {
    // 자원을 사용하는 로직
} catch (예외 e) {
    // 예외 처리
}

try(자원 선언) 의 괄호 안에는 FileReader, BufferedReader, Connection, PreparedStatement 과 같은 AutoCloseable 인터페이스를 구현한 객체를 선언할 수 있다.

또한 여러 자원도 ;로 구분하여 동시에 선언 가능하다.

예제: JDBC 사용

아래 코드는 필자가 우테코 미션에서 구현한 JdbcTemplate 예제이다.
try-with-resources를 사용하여 Connection, PreparedStatement, ResultSet 객체를 자동으로 close하고 있다.

    public <T>List<T> query(String sql, RowMapper<T> rowMapper) {
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql);
             ResultSet rs = pstmt.executeQuery()) {

            log.debug("query : {}", sql);

            List<T> results = new ArrayList<>();
            while (rs.next()) {
                results.add(rowMapper.mapRow(rs));
            }
            return results;
        } catch (SQLException e) {
            log.error(e.getMessage(), e);
            throw new DataAccessException(e);
        }
    }

마무리

예외 처리는 코드의 품질을 결정짓는 중요한 요소다.
throws로 예외를 넘길지, try-catch로 직접 처리할지, 혹은 try-with-resources로 안전성을 확보할지는 모두 의도적인 설계 선택이다.

결국 예외 처리의 목표는 “모든 예외를 잡는 것”이 아니라, 올바른 위치에서 책임 있게 다루는 것이다.

profile
도전하며 굴러가는 돌멩이, 인생 마라톤 중 😎

0개의 댓글