[JAVA] 예외 처리3 -실습

2025. 7. 11. 17:42·자바

지난 예외 처리 1장에서 봤듯, 예외를 문자열로 반환하고 이를 비교하여 처리하는 방식은 예외 처리 코드가 핵심 구현보다 더 복잡해지는 구조적 한계를 드러낸다. 이 장에서는 자바의 예외 처리 메커니즘을 통해 이 문제를 점진적으로 리팩토링하며, 각 단계마다 어떤 문제가 있었고 어떻게 해결되었는지 구체적으로 살펴본다.

 

V1. 문자열 기반 예외 처리

package exception.ex1;

public class NetworkClientV1 {
    public boolean connectError;
    public boolean sendError;
    private final String address;

    public NetworkClientV1(String address) {
        this.address = address;
    }
    public String connect(){
        if(connectError){
            System.out.println(address+" 서버 연결 실패");
            return "connectError";
        }
        System.out.println(address+" 서버 연결 성공");
        return "success";
    }
    public String send(String data){
        if(sendError){
            System.out.println(address+" 서버에 데이터 전송 실패");
            return "sendError";
        }
        //전송 성공
        System.out.println(address+" 서버의 데이터 전송 "+data);
        return "success";
    }

    public void disconnect(){
        System.out.println(address+" 서버 연결 헤제");
    }

    public void initError(String data){
        if(data.contains("error1")){
            connectError=true;
        }
        if(data.contains("error2")){
            sendError=true;
        }

    }
}
package exception.ex1;


public class NetworkService1_3 {
    public void sendMessage(String data){
        String address="http://example.com";
        NetworkClientV1 client = new NetworkClientV1(address);
        client.initError(data);


        String connectResult= client.connect();
        if(isError(connectResult)){
            System.out.println("[네트워크 오류 발생] 오류 코드: "+ connectResult);
        }
        else{
            String sendResult=client.send(data);
            if(isError(sendResult)){
                System.out.println("[네트워크 오류 발생] 오류 코드: "+sendResult);
            }
        }

        client.disconnect();

    }

    private static boolean isError(String connectResult) {
        return !connectResult.equals("success");
    }
}

 

 

문제

  • "connectError", "sendError" 같은 문자열로 오류를 반환한다.
  • 호출자는 이를 문자열로 비교하며 처리해야 하므로 중복 코드와 실수 가능성이 높다.
  • 정상 흐름과 예외 흐름이 섞여 있어 가독성이 떨어진다

따라서 자바에서 제공하는 에외를 통해 이를 리펙토링 해볼 것이다.

 

 

V2.1. 커스텀 예외 도입 + throws 활용

문제

  • V1에서는 문자열 비교로 오류를 처리하므로, 의미 없는 "문자열 프로토콜"을 호출자가 해석해야 한다.
  • 실질적인 오류는 예외(Exception)로 다뤄져야 한다.

개선 방안

  • 예외 클래스를 만들어 오류의 종류와 메시지를 담는다.
  • connect()와 send()에서 직접 예외를 던지고, 호출자에게 throws로 위임한다
package exception.ex2;

public class NetworkClientExceptionV2 extends Exception {
    private final String errorCode;

    public NetworkClientExceptionV2(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public String getErrorCode() {
        return errorCode;
    }
}

 

package exception.ex2;

public class NetworkClientV2 {
    public boolean connectError;
    public boolean sendError;
    private final String address;

    public NetworkClientV2(String address) {
        this.address = address;
    }

    public void connect() throws NetworkClientExceptionV2 {
        if (connectError) {
            throw new NetworkClientExceptionV2("connectError", address + " 서버 연결 실패");
        }
        System.out.println(address + " 서버 연결 성공");
    }

    public void send(String data) throws NetworkClientExceptionV2 {
        if (sendError) {
            throw new NetworkClientExceptionV2("sendError", address + " 서버에 데이터 전송 실패: " + data);
        }
        System.out.println(address + " 서버에 데이터 전송: " + data);
    }

    public void disconnect() {
        System.out.println(address + " 서버 연결 해제");
    }

    public void initError(String data) {
        if (data.contains("error1")) connectError = true;
        if (data.contains("error2")) sendError = true;
    }
}

 

package exception.ex2;

public class NetworkServiceV2_1 {
    public void sendMessage(String data) throws NetworkClientExceptionV2 {
        String address = "http://example.com";
        NetworkClientV2 client = new NetworkClientV2(address);
        client.initError(data);

        client.connect();
        client.send(data);
        client.disconnect();
    }
}

 

개선 효과

  • 오류 발생 시 명시적인 throw로 전달 → 호출자는 정상 흐름만 구현하여 가독성이 좋아짐
  • 문자열 비교 제거 → 타입 안전성과 명확성 향상
  • 예외는 호출자에게 위임 → 역할 분리

 

V2.2. try-catch를 통한 예외 직접 처리

 

문제

V2.1에서는 예외를 throws로 위임만 했기 때문에 호출하는 쪽에서 별도의 처리를 하지 않으면 프로그램이 비정상 종료될 수 있다. 실서비스에서는 예외를 잡고 복구하거나 사용자에게 메시지를 출력한 뒤 프로그램을 계속 수행하는 방식이 요구된다.

 

개선 방안

  • try-catch를 사용하여 예외를 직접 잡고 처리한다.
  • connect() 또는 send() 중 하나라도 실패하면 해당 메시지를 출력하고 로직을 종료한다.
package exception.ex2;

public class NetworkServiceV2_2 {
    public void sendMessage(String data) {
        String address = "http://example.com";
        NetworkClientV2 client = new NetworkClientV2(address);
        client.initError(data);

        try {
            client.connect();
        } catch (NetworkClientExceptionV2 e) {
            System.out.println("[오류] 코드: " + e.getErrorCode() + ", 메시지: " + e.getMessage());
            return; // 연결 실패 시 전송 시도하지 않음
        }

        try {
            client.send(data);
        } catch (NetworkClientExceptionV2 e) {
            System.out.println("[오류] 코드: " + e.getErrorCode() + ", 메시지: " + e.getMessage());
            return; // 전송 실패 시 종료
        }

        client.disconnect(); // 정상 수행 후 연결 해제
    }
}

 

개선 효과

  • 예외 발생 시 정상 흐름 종료 및 사용자에게 오류 메시지 전달
  • 프로그램 강제 종료 없이 복구 가능
  • 단점: try-catch 블록이 두 번 나뉘어져 정상 흐름이 분리되어 있음

남은 문제

  • connect()와 send()를 각각 별도로 감싸다 보니 정상 흐름이 끊기고, 흐름을 파악하기 어려워진다.
  • 만약 disconnect() 전에 예외가 발생하면, 연결이 닫히지 않는 위험도 존재한다.

 

 

V2.3. try 내부에 정상 흐름을, catch에 예외 흐름을

문제

V2.2에서는 connect()와 send() 각각을 개별 try-catch 블록으로 감쌌기 때문에 정상 흐름이 분리되어 코드 가독성이 떨어지고, 예외 처리 코드가 중복되는 단점이 있다.

 

개선 방안

  • 하나의 try 블록 안에서 정상 흐름을 모두 처리하고,
  • catch 블록에서 예외 흐름을 한 번에 처리함으로써,
  • 구현 흐름과 예외 흐름을 명확히 분리한다.
package exception.ex2;

public class NetworkServiceV2_3 {
    public void sendMessage(String data) {
        String address = "http://example.com";
        NetworkClientV2 client = new NetworkClientV2(address);
        client.initError(data);

        try {
            client.connect();
            client.send(data);
            client.disconnect(); // 정상 흐름에서 자원 해제
        } catch (NetworkClientExceptionV2 e) {
            System.out.println("[오류] 코드: " + e.getErrorCode() + ", 메시지: " + e.getMessage());
        }
    }
}

 

개선 효과

  • try 블록에는 정상 흐름만 존재 → 코드 가독성 향상
  • catch 블록에는 예외 처리만 존재 → 책임 분리 명확
  • 예외가 발생해도 프로그램은 중단되지 않음

남은 문제

  • 예외가 발생하면 disconnect()가 호출되지 않는다.
  • 즉, 외부 자원(네트워크 연결)이 해제되지 않을 수 있는 위험이 있다

 

V2.4. 예외가 발생하면 disconnect()가 호출되지 않는 구조

문제

V2.3에서는 disconnect() 호출을 try 블록 안에 두었기 때문에, 만약 connect()나 send() 중간에 예외가 발생하면 disconnect()까지 도달하지 못하고 연결이 닫히지 않는다. 이는 실제 서비스 환경에서는 자원 누수(메모리, 파일, DB, 소켓 등)로 이어질 수 있는 치명적인 문제다.

 

개선 방안

  • 예외가 발생해도 반드시 실행되는 disconnect()는 try 밖으로 분리하여,
  • 예외 발생 여부와 관계없이 항상 실행되도록 한다.
package exception.ex2;

public class NetworkServiceV2_4 {
    public void sendMessage(String data) {
        String address = "http://example.com";
        NetworkClientV2 client = new NetworkClientV2(address);
        client.initError(data);

        try {
            client.connect();
            client.send(data);
        } catch (NetworkClientExceptionV2 e) {
            System.out.println("[오류] 코드: " + e.getErrorCode() + ", 메시지: " + e.getMessage());
        }

        client.disconnect(); // 문제: 예외 발생 시 실행되지 않을 수 있음
    }
}

 

문제 상세 분석

  • NetworkClientExceptionV2 예외가 아닌 다른 예외가 발생할 경우 disconnect() 가 호출되지 않는다.
  • 즉 프로그램은 복구되더라도 네트워크 연결은 그대로 남아있는 상태가 된다.

 

V2.5. finally를 활용한 자원 해제 보장

문제

앞선 V2.4에서는 생성한  NetworkClientExceptionV2가 아닌 예외 발생 시 disconnect()가 호출되지 않는 구조였다. 이는 외부 자원을 사용하는 프로그램에서 매우 심각한 문제다. 자바는 이런 경우를 위해 finally 블록을 제공하여, 예외 발생 여부와 상관없이 반드시 실행되는 코드를 작성할 수 있게 한다.

 

개선 방안

  • client.disconnect()를 finally 블록 안으로 이동시켜,
  • connect() 또는 send()에서 예외가 발생하더라도 항상 네트워크 연결을 해제한다.
package exception.ex2;

public class NetworkServiceV2_5 {
    public void sendMessage(String data) {
        String address="http://example.com";
        NetworkClientV2 client = new NetworkClientV2(address);
        client.initError(data);

        try {
            client.connect();
            client.send(data);
        } catch (NetworkClientExceptionV2 e) {
            System.out.println("[오류] 코드: "+e.getErrorCode()+", 메시지: "+e.getMessage());
        }
        finally {
            client.disconnect();
        }

    }
}

 

개선 효과

  • connect(), send() 중 어디서 예외가 발생하더라도 disconnect()는 반드시 호출된다.
  • 정상 흐름 → 예외 흐름 → 자원 정리 흐름이 각각 try, catch, finally로 명확하게 분리된다.
  • 자원 누수 없이 안정적인 예외 처리 구조를 갖추게 된다.

 

리팩토링 결과

리펙토링 전:

package exception.ex1;


public class NetworkService1_3 {
    public void sendMessage(String data){
        String address="http://example.com";
        NetworkClientV1 client = new NetworkClientV1(address);
        client.initError(data);


        String connectResult= client.connect();
        if(isError(connectResult)){
            System.out.println("[네트워크 오류 발생] 오류 코드: "+ connectResult);
        }
        else{
            String sendResult=client.send(data);
            if(isError(sendResult)){
                System.out.println("[네트워크 오류 발생] 오류 코드: "+sendResult);
            }
        }

        client.disconnect();

    }

    private static boolean isError(String connectResult) {
        return !connectResult.equals("success");
    }
}

 

리펙토링 후:

package exception.ex2;

public class NetworkServiceV2_5 {
    public void sendMessage(String data) {
        String address="http://example.com";
        NetworkClientV2 client = new NetworkClientV2(address);
        client.initError(data);

        try {
            client.connect();
            client.send(data);
        } catch (NetworkClientExceptionV2 e) {
            System.out.println("[오류] 코드: "+e.getErrorCode()+", 메시지: "+e.getMessage());
        }
        finally {
            client.disconnect();
        }

    }
}

 

자바의 예외 처리 기능을 통해 기존 V1에서 발생했던 예외 처리 코드가 구현 로직보다 더 많아지는 문제를 해결할 수 있었다. V1에서는 한 로직 안에 예외 판단과 메시지 출력, 분기 처리가 뒤섞여 있어 가독성과 유지보수성이 모두 떨어졌다.

이번 리팩토링을 통해 다음과 같은 구조로 개선되었다:

  • 정상 로직은 try 블록에, 예외 처리는 catch 블록에 분리
  • 자원 정리와 같은 반드시 호출되어야 하는 로직은 finally에 배치
  • 구현 흐름과 예외 흐름이 명확하게 분리되어 가독성 향상
  • 코드 중복 제거, 실수 가능성 감소, 안정성 확보

결국 자바 예외 처리 구조를 제대로 활용하면, 핵심 로직에 집중하면서도 예외에 안전한 프로그램을 만들 수 있다는 것을 확인할 수 있었다.

 

마무리하며

우리가 자바에서 예외 처리를 사용하는 이유는 간단하다.
그냥 if문으로 오류를 처리하다 보면, 해야 할 일보다 오류 처리 코드가 더 많아지고,
코드도 지저분해져서 읽기 어렵고 유지보수하기 힘들어지기 때문이다.

자바는 이런 문제를 해결하기 위해 try, catch, finally, throw 같은 예외 처리 문법뿐만 아니라,
Exception, RuntimeException 같은 예외 객체(클래스)들도 함께 제공한다.

 

오류가 발생하면 예외를 객체로 만들어 던질 수 있고,
필요하다면 try-catch를 통해 예외를 잡아 처리하면서,
구현 로직과 오류 처리 로직을 명확하게 분리할 수 있다.

 

또한, 파일이나 네트워크처럼 꼭 정리해야 할 자원은 finally를 활용해
오류가 나든 안 나든 무조건 정리되도록 처리할 수 있다.

 

우리가 직접 예외 클래스를 만들어 쓰면,
오류의 원인을 더 명확하게 전달할 수 있고,
어디서 어떤 문제가 발생했는지 추적하기도 쉬워진다.

 

결국 자바의 예외 처리는 단순히 "문제 발생 시 멈추는 장치"가 아니라,
코드를 더 명확하고, 안정적이며, 유지보수하기 쉽게 만들어주는 중요한 도구라는 걸 확인할 수 있었다.

 

감사합니다.

'자바' 카테고리의 다른 글

[JAVA] 제네릭이 필요한 이유  (1) 2025.07.14
[JAVA] 예외 처리4 -실무  (4) 2025.07.12
[JAVA] 예외 처리 2 -이론  (5) 2025.07.10
[JAVA] 예외 처리1 -이론  (0) 2025.07.10
[JAVA] 중첩 클래스. 내부 클래스3  (2) 2025.07.09
'자바' 카테고리의 다른 글
  • [JAVA] 제네릭이 필요한 이유
  • [JAVA] 예외 처리4 -실무
  • [JAVA] 예외 처리 2 -이론
  • [JAVA] 예외 처리1 -이론
0kingki_
0kingki_
자바 + 스프링 웹 개발
  • 0kingki_
    0kingki_
    0kingki_
  • 전체
    오늘
    어제
    • 분류 전체보기 (134)
      • 코딩 테스트 (54)
      • 자바 (21)
      • 스프링 (27)
      • 타임리프 (16)
      • 스프링 데이터 JPA (8)
      • 최적화 (2)
      • QueryDSL (4)
      • AWS (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    스프링 데이터 JPA
    다형성
    spring
    SOLID
    객체지향
    예외 처리
    thymeleaf
    LocalDateTime
    자바
    예외처리
    최적화
    스프링 컨테이너
    재귀
    QueryDSL
    코딩 테스트
    불변객체
    JPA
    dfs
    Java
    컬렉션
    mvc
    BFS
    쿼리
    쿼리dsl
    백준
    스프링
    SpringDataJpa
    타임리프
    fetch join
    코딩테스트
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
0kingki_
[JAVA] 예외 처리3 -실습
상단으로

티스토리툴바