Skip to content
This repository was archived by the owner on Dec 30, 2021. It is now read-only.

Chapter 07. Error Handling

YoungMinKim edited this page Sep 11, 2021 · 25 revisions

2021.09.04 (SAT) 10:20-12:00 (100mins)
🚀 Lead by. 'YoungMin'

서론

오류 처리는 프로그램에 반드시 필요한 요소 중 하나:
  - 상당수 코드 기반은 전적으로 "오류 처리 코드"에 "좌우"되기 때문에 "깨끗한 코드"와 연관이 있다.
  - 여기저기 흩어진 오류 처리 코드 때문에 실제 코드가 하는 일을 파악하기 힘들어진다.
  - 오류 처리 코드로 인해 프로그램 논리를 이해하기 어려워진다면 깨끗한 코드라 부르기 어렵다.

오류(Error)와 예외(Exception)

들어가기에 앞서 오류와 예외에 대해 간략히 알아보자:
  - 오류 그리고 예외란 무엇인가?
  - 오류와 예외는 어떤 차이가 있는가? 

오류(Error)

  • 오류시스템비정상적인 상황이 생겨 해당 시스템(Process)이 종료되어야 할 수준의 수습할 수 없는 심각한 문제.
  • 시스템 레벨에서 발생하기에 개발자가 미리 예측할 수 없는 심각한 오류.

예외(Exception)

  • 예외개발자가 구현한 로직에서 발생한 실수 혹은 사용자의 행동에 의해 발생.
  • 오류와 달리 개발자가 미리 예측하여 방지할 수 있기에 상황에 맞는 예외처리(Exception Handle)를 해야한다.

오류 코드보다 예외를 사용하라

왜 오류 코드보다 예외 사용을 지향해야 하는가?:
  - 얼마 전까지만 해도 예외를 지원하지 않는 프로그래밍 언어가 많았다.
  - 예외를 지원하지 않는 언어는 "오류를 처리하고 보고하는 방법이 제한적이다."
  - "오류 플래그" 설정
  - 호출자에게 "오류 코드" 반환

Step I - 논리와 오류 처리 코드가 뒤섞임

// Bad
public class DeviceController {
  ...

    public void sendShutDown() {
        DeviceHandle handle = getHandle(DEV1); // Check Device Status 

        if (handle != DeviceHand.INVALID) {
            retrieveDeviceRecord(handle); // Save device status to record field

            if (record.getStatus() != DEVICE_SUSPENDED) { // Shut down if device is not paused
                pauseDevice(handle);
                clearDeviceWorkQueue(handle);
                closeDevice(handle);
            } else {
                logger.log("Device suspended. Unable to shut down");
            }
        } else {
            logger.log("Invalid handle for: " + DEV1.toString());
        }
    }
}
  • 위와 같은 방법은 함수를 호출한 즉시 오류를 확인해야 하기에, 호출자 코드가 복잡해진다.
  • 오류가 발생하면 예외를 던지는 편이 낫다.
    • 호출자 코드가 깔끔해진다.
    • 코드의 논리 영역오류 처리 코드를 분리할 수 있다.

Step II - 논리와 오류 처리 코드를 분리

// Good
public class DeviceController {
  ...

    public void sendShutDown() {
        try {
            tryToShutDown(); // Simplified call statement
        } catch (DeviceShutDownError e) {
            logger.log(e);
        }
    }

    private void tryToShutDown() throws DeviceShutDownError {
        DeviceHandle handle = getHandle(DEV1); // Check Device Status
        DeviceRecord record = retrieveDeviceRecord(handle);

        pauseDevice(handle);
        clearDeviceWorkQueue(handle);
        closeDevice(handle);
    }

    private DeviceHandle getHandle(DeviceId id) {
        ...
        throw new DeviceShutDownError("Invalid handle for: " + id.toString());
        ...
    }
}
  • 위 코드를 보면 앞서 뒤 섞였던 개념을 분리 하였다.
    • 디바이스의 상태를 파악하여 저장 후 종료하는 알고리즘.
    • 오류를 처리하는 알고리즘.
  • 각 개념을 독립적으로 살펴보고 이해할 수 있다.
  • 코드의 품질가독성이 높아졌다.

throw vs throws

throw와 throws의 차이?:
  - throws와 throw는 둘 다 예외(Exception)을 발생 시킨다는 것에는 차이가 없다.
  - 하지만 두 키워드 사이에는 차이점이 존재한다.

throw

class CheckedException extends Exception {

    public CheckedException(String message) {
        super(message);
    }
}

class UnCheckedException extends RuntimeException {

    public UnCheckedException(String message) {
        super(message);
    }
}
class Student {
    ...

    public void callStudent() {
        try {
            if (null == this.name || null == this.age) {
                throw new CheckedException("An exception has occurred in callStudent method");
            }
            System.out.println("이름 = " + this.name + ", 나이 = " + this.age);
        } catch (CheckedException e) {
            e.printStackTrace();
        }
    }
}

public class HandleException {

    public static void main(String[] args) {
        Student st = new Student("kym", "29");
        st.callStudent();

        st = new Student(null, "23");
        st.callStudent();
    }
}
  • throw는 메서드 내에서 예외 처리 하기 위해, 개발자가 예외(Exception)를 강제로 발생시키는 것.
  • 애플리케이션이 예외를 적절히 처리하지 못하면 프로그램이 죽거나 오작동하게 된다.

throws

public class ThrowsTest {

    public static void main(String[] args) {
        System.out.println("====Throws TEST====");
        method1();
    }

    public static void method1() {
        try {
            method2();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    public static void method2() throws ClassNotFoundException {
        // ok : java.lang.String
        Class clazz = Class.forName("java.lang.StringError"); // error!
    }
}
  • 현재 메서드를 기준으로 자신을 호출한 상위 메서드로 Exception을 발생 시킨다.
  • throws 키워드를 사용하는 메서드를 호출한 상위 메서드에게 에러 처리에 대한 책임을 전가한다.

Try-Catch-Finally 문부터 작성하라

try~catch 이미지
Try-Catch-Finally?:
  - 예외가 발생할 것 같은 코드를 짤 때는 try-catch-finally 문으로 시작하는 편이 낫다.
  - try 블록에 들어간 코드 실행 시, "어느 시점에서든 실행 중단 후 catch 블록으로 넘어갈 수 있다".
  - try 블록에서 무슨 일이 생기든 catch 블록은 프로그램 상태를 "일관성 있게 유지"해야 한다.
  • 즉, try-catch-finally 구문을 선언함으로써 무슨 일이 생기든 호출자가 기대하는 상태 정의가 가능해진다.

TDD(Test-Driven-Development) 방식으로 구현

Step I - 단위 테스트 생성

// 단위 테스트를 위해 @Test 어노테이션 사용
@Test(expected = StorageException.class)
public void retrieveSectionShouldThrowOnInvalidFileName() {
    sectionStore.retrieveSection("invalid - file");
}
public List<RecordedGrip> retrieveSection(String sectionName) {
    // This Line : 예외 발생을 위해 해당 라인에서의 어떠한 제스처가 필요함
    return new ArrayList<RecordedGrip>();
}
  • 위 코드는 파일이 없으면 예외를 던지는지 알아보는 단위 테스트를 구현한 코드다.
  • 예외를 발생시켜야 하는데, 예외가 발생되지 않아 단위 테스트에 실패한다.

Step II - 파일에 직접 접근

public List<RecordedGrip> retrieveSection(String sectionName) {
    try {
        FileInputStream fis = new FileInputStream(sectionName);
    } catch (Exception e) {
        throw new StorageException("retrieval error = ", e);
    }
    
    return new ArrayList<RecordedGrip>();
}
  • 파일을 직접 읽어들이는 코드를 작성해 보았다.
  • 파일을 읽는 과정에서 예외가 발생하게 되면 해당 예외를 호출자에게 반환하게 되고 단위 테스트 성공하게 된다.

Step III - 예외 처리 영역 리팩토리

public List<RecordedGrip> retrieveSection(String sectionName) {
    try {
        FileInputStream fis=new FileInputStream(sectionName);

        // 비즈니스 로직 영역
        // ..
        // ..    
        // end

        fis.close();
    } catch (FileNotFoundException e) {
        throw new StorageException("retrieval error = ",e);
    }

    return new ArrayList<RecordedGrip>();
}
  • catch 블록에서의 기존 예외 유형(Exception e)을 좁혀 리팩토리하는 과정을 거친다.

미확인 예외를 사용하라

확인된 예외를 왜 사용해야 하는가?:
  - 여러 해 동안 프로그래머들은 "확인된 예외"의 장단점을 놓고 논쟁을 벌여왔다.
  - 자바 첫 버전이 확인된 예외를 선보였던 당시는 확인된 예외가 상당히 "멋진 아이디어"로 여겨졌다.
  - 하지만 안정적인 소프트웨어를 제작하는 요소로 확인된 예외는 반드시 필요하지 않다는 사실이 분명해졌다.

미확인 예외 vs 확인된 예외

ckException
Checked Exception Unchecked Exception
처리 여부 반드시 예외 처리를 해야함 명시적인 처리를 강제하지 않음
확인 시점 컴파일 단계(Compile) 실행 단계(Runtime)
예외 발생시 트랜잭션 처리 Roll-back 안됨 Roll-back 함
대표 Exception IOException, SQLException NullPointerException, IllegalArgumentException

Checked Exception

  • 위에서 설명하였지만 Checked Exception은 확인된 예외로 컴파일 시점에 확인되는 예외를 말한다.
  • 확인된 예외OCP 원칙 을 위반한다.
    • 메서드에서 확인된 예외를 던졌는데 catch 블록이 세 단계 위에 있다면, 그 사이 메서드 모두가 선언부에 해당 예외를 정의해야 한다.
    • 즉, 하위 단계에서 코드를 변경하면 상위 단계 메서드 선언부를 전부 고쳐야 한다는 말이다.
  • 모듈과 관련된 코드가 바뀌지 않아도, 선언부가 변경되었기에 다시 빌드 후 배포 해야한다.

Step I - OCP 위반?

class HandleCheckedException {

    public void catchFileInputException() {
        try {
            method1();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void method1() throws IOException {
        method2();
    }

    public void method2() throws IOException {
        method3();
    }

    public void method3() throws IOException {
        FileInputStream fst = new FileInputStream(new File("/Users/youngminkim/Desktop/Images/naver2.png"));
        FileOutputStream fos = null;
        int cnt = 0;

        while ((cnt = fst.read()) != -1) {
            System.out.println("cnt = " + cnt);
            break;
        }

        fst.close();
    }
}

public class CheckException {

    public static void main(String[] args) {
        HandleCheckedException h = new HandleCheckedException();
        h.catchFileInputException(); // call method
    }
}
  • 위 코드를 보면 main 메서드 내에서 객체를 생성한 후 파일을 읽어들이는 로직을 작성하였다.
  • main -> catchFileInputException -> method1() -> method2() -> method3()
    • method3() throws IOException 은 method2() 메서드가 받게 된다.
    • method2() 메서드는 해당 예외를 try~catch로 처리하거나, 상위 메서드(자신을 호출한 메서드)에 넘겨야한다.
  • 이러한 특징으로 인해 최하단(마지막 호출된 메서드)부터 최상단(최초로 다른 메서드를 호출한 메서드)까지 연쇄 작용이 일어난다.

예외에 의미를 제공하라

예외에 의미를 제공하여 의미를 더욱 더 확실히 한다:
  - 예외를 던질 때는 "전후 상황"을 덧붙인다. 그러면 오류가 발생한 "원인"과 "위치"를 찾기가 쉽다.
  • 오류 메시지정보를 담아 예외와 함께 던진다.
  • 실패한 연산 이름실패 유형도 언급한다.
  • 애플리케이션이 로깅 기능을 사용 한다면 catch 블록에서 오류를 기록하도록 충분한 정보를 넘겨준다.

Step I - 예외 발생 시 로그 작성

// Bad
try {
    process(url);
} catch(IOException e) {
    // 공란
}

// Bad
try {
    process(url);
} catch(IOException e) {
    // 단순히 기본 에러 메시지 출력
    e.printStackTrace();
}
// Good
try {
    process(url);
} catch(IOException e) {
    log.debug("Fail to call API = " + url, e);
}
  • process(url) 호출 실패 시, 실패 메시지실패 정보를 남긴다

호출자를 고려해 예외 클래스를 정의하라

오류를 분류하는 방법?:
  - 오류를 분류하는 방법은 수 없이 많고, 오류가 발생한 위치를 통해 분류가 가능하다.
  - 즉, 오류가 발생한 "컴포넌트", "유형"으로 오류를 분류 할 수 있다.
  - 애플리케이션에서 오류를 정의할 때 "가장 중요한 관심사는 오류를 잡아내는 방법"이다.   

예외 클래스(Exception Class)?

  • 예외 클래스(Exception Class)를 만들 때 가장 중요한것은 어떤 방식으로 예외를 잡을까이다.
  • 써드파티 라이브러리(Third Party Library)를 사용하는 경우 그것들을 Wrapping 함으로써 라이브러리 교체 등의 변경이 있는 경우 대응이 쉬워진다.
  • 호출부에서는 하나의 Exception 만 처리하고 예외 클래스를 만들어 throws 하는 형태로 사용해보자.
// Bad
public class ConnectACMEPortAPI {

    public static void main(String[] args) {
        ACMEPort port = new ACMEPort(12);

        try {
            port.open();
        } catch (DeviceResponseException e) {
            reportPortError(e);
            logger.log("Device response exception = ", e);
        } catch (ATM1212UnlockedException e) {
            reportPortError(e);
            logger.log("Unlock exception = ", e);
        } catch (GMXError e) {
            reportPortError(e);
            logger.log("Device response exception = ", e);
        } finally {
            ...
        }
    }
}
  • 위 코드를 보면 외부 라이브러리(API)가 던지는 예외를 다 처리하는 구문이다.
  • 위 같이 예외 처리를 하게 되면 외부 라이브러리의 API에 종속적인 상태가 되고 코드의 중복이 반복된다.
  • 또한 에러마다 기능을 추가해야 한다면 코드가 더욱 더 길어질 것이다.
// Good
// Wrapper Class(Exception Class)로 감싸서 처리
class LocalPort {

    private ACMEPort innerPort; // obj

    public LocalPort(int portNumber) {
        this.innerPort = portNumber;
    }

    public void open() {
        try {
            innerPort.open();
        } catch (DeviceResponseException e) {
            throw new PortDeviceFailure(e);
        } catch (ATM1212UnlockedException e) {
            throw new PortDeviceFailure(e);
        } catch (GMXError e) {
            throw new PortDeviceFailure(e);
        }
    }
  ...
}

public class ConnectACMEPortAPI {

    public static void main(String[] args) {
        LocalPort port = new LocalPort(12);

        try {
            port.open();
        } catch (PortDeviceFailedException e) {
            reportPortError(e);
            logger.log(e.getMessage(), e);
        } finally {
            ...
        }
    }
}
  • 실제로 외부 API를 사용할 때는 감싸기 기법이 최선이다.
  • 외부 API를 클래스로 감싸면 아래와 같은 장점이 생긴다.
    • 에러 처리 로직이 간결해진다.
    • 외부 라이브러리프로그램 사이의 의존성이 크게 줄어든다.
    • 추후 다른 라이브러리로 갈아타도 비용이 적다.
    • Wrapper Class에서 외부 API를 호출하는 대신, 테스트 코드를 넣어주는 방법으로 테스트가 용이해진다.

정리

  1. 혼히 예외 클래스가 하나만 있어도 충분한 코드가 많다.
  2. 예외 클래스에 포함된 정보로 오류를 구분해도 괜찮은 경우가 그렇다.
  3. 위 같은 이유로, 한 예외는 잡아내고 다른 예외는 무시해도 괜찮은 경우 여러 예외 클래스 사용을 지향하자.

정상 흐름을 정의하라

앞 절에서 충고한 지침을 통해 비즈니스 논리와 오류 처리가 잘 분리된 코드가 나온다:
  - 오류 감지가 프로그램 언저리로 밀려난다.
  - 때로는 이러한 중단이 적합하지 않을때가 있다.
  - 객체를 조작해 특수사례를 처리하거나, 특수 사례 패턴으로 클래스를 만든다.
  - 클라이언트 코드가 예외적인 상황을 처리할 필요가 없어진다.

Step I - 비용 청구 총계 계산

// Bad
public String getTotalExpenses(Employee employee) {
    try {
        MealExpenses expenses = expensesReportDAO.getMeals(employee.getID());
        m_total += expenses.getTotal();
    } catch(MealExpensesNotFound e){
        m_total += getMealPerDiem();
    }
}
// Good
public String getTotalExpenses(Employee employee) {
    MealExpenses expenses = expensesReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();
}
public class PerDiemMealExpenses implements MealExpenses {

    public int getTotal() {
        // 기본값으로 일일 기본 식비를 반환한다.
    }
}
  • 두 번째 코드를 특수 사례 패턴(Special Case Pattern)이라고 부른다.
    • 클래스객체를 조작하여 특수 사례를 처리하는 방식.
    • 클래스나 객체가 예외적인 상황을 캡슐화하여 처리하기 때문에, 클라이언트 코드가 예외적인 상황을 처리할 필요가 없어진다.

null을 반환하지 마라

무의식적으로 할 수 있는 null 반환:
  - 오류 처리를 논하는 장이기에, 우리가 흔히 저지르는 바람에 오류를 유발하는 "행위"도 언급해야 한다 생각한다.
  - 애플리케이션 구축 과정에서 우리가 무의식적으로 null을 반환하는 시스템을 구현하는 것을 의미.  

null을 반환하는 습관

  • 한줄 건너 하나씩 null을 확인하는 코드로 가득한 애플리케이션을 지금까지 수도없이 봤다.
// Bad
public void regsiterItem(Item item) {
  if(item != null) {
      ItemRegistry registry = peristentStore.getItemRegistry(); // null or data
        
      if(registry != null) {
          Item existing = registry.getItem(item.getId()); // null or data
  
          if(existing.getBillingPeriod().hasReatailOwner()) {
              existing.register(item);
          }
      }
  }
}
  • 이런 코드를 기반으로 코드를 짜왔다면 무엇이 문제인지 무엇이 나쁜지 모를수도 있다.
  • 일단 위 코드는 나쁜 코드다!
  • 왜 나쁜 코드인가?
    • null을 반환하는 코드는 일거리를 늘릴뿐만 아니라 호출자에게 문제를 떠넘긴다. 좋지 못한 코딩 습관.
    • 누구 하나라도 null 확인을 빼먹는다면 애플리케이션이 통제 불능에 빠질지 모른다.
    • null을 리턴하고 싶은 생각이 들면 특수 사례 객체를 리턴하라.
    • ex) Collections.emptyList()

문제점

  • null 확인이 누락된 문제이지만, 더 큰 문제는 null 확인이 너무 많다는게 더욱 더 문제다.
  • 둘째 행에 null 확인이 빠진 부분.
  • 만약 peristentStore.getItemRegistry()가 null을 반환하는 경우.
  • 위쪽 어디선가 NullpointException을 잡을지도 모르지만, 어느쪽이든 좋지 않다.

해결책

  • 예외를 던진다
  • 특수 사례 객체를 반환한다

Step I - 특수 사례 객체 예제

// Bad
public void addTotalPay() {
    int totalPay = 0;
    List<Employee> employees = getEmployees();
  
    if(employees != null) {
        for(Employee employee : employees) {
            totalPay += employee.getPay();
        }
    }    
}
// Good
public void addTotalPay() {
    int totalPay = 0;
    List<Employee> employees = getEmployees();

    for(Employee employee : employees) {
        totalPay += employee.getPay();
    }
}
public List<Employee> getEmployees() {
    List<Employee> employeeList = employeeDAO.getEmployees(); // Get Data from DB
    return (employeeList.size() > 0) ? employeeList : Collection.emptyList();
}
  • getEmployees()null도 반환 한다. 하지만 굳이 null을 반환할 필요가 있는가?
  • getEmployees()를 조금 수정하여 특수 사례 객체(Collections.emptyList())를 반환한다.

null을 전달하지 마라

인수로 null을 전달하지 마라:
  - 메서드에서 null을 반환하는 방식도 나쁘지만, 메서드로 null을 전달하는 방식은 더 나쁘다.
  - 즉, 정상적인 인수로 null을 기대하는 API가 아니라면 메서드로 null을 전달하는 코드는 최대한 피한다.

Step I - 두 지점 사이의 거리 계산 : 기본

// Basic
public class MetricsCalculator {

    public double xProjection(Point p1, Point p2) {
        return (p2.x - p1.x) * 1.5;
    }
    ...
}
public static void main(String[] args) {
    MetricsCalculator calculator = new MetricsCalculator(); // Create Object
    calculator.xProjection(null, new Point(12,13)); // Call function xProjection
}
  • 위 코드처럼 첫 번째 인수 값에 null을 전달한다면 어떻게 될까?
    • NullPointerException 발생!

Step II - 두 지점 사이의 거리 계산 : 예외 유형 던지기

// Handle Exception
public class MetricsCalculator {

    public double xProjection(Point p1, Point p2) {

        if (p1 == null || p2 == null) {
            throw InvalidArgumentException("Invalid argument for MetricsCalculator.xProjection"); // Throw Current Exception
        }

        return (p2.x - p1.x) * 1.5;
    }
    ...
}
  • 매개변수값 p1, p2 값에 대한 유효성 처리 후 예외가 발생하면 InvalidArgumentException을 예외로 던진다.
  • 예외를 상위 호출부에게 던져서 해당 예외를 처리하도록 구현 하였다.

Step III - 두 지점 사이의 거리 계산 : assert문 사용

// Use Assert 
public class MetricsCalculator {

    public double xProjection(Point p1, Point p2) {
        assert p1 != null : "p1 should not be null";
        assert p2 != null : "p2 should not be null";
        return (p2.x - p1.x) * 1.5;
    }
...
}
  • assertJDK 1.4 부터 지원이되는 객체가 아닌 예약어

Step IV - 간단한 계산 예제

public class AssertTest {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int num1 = sc.nextInt();
        int num2 = sc.nextInt();

        System.out.println("result = " + calculator(num1, num2)); // 결과 출력
    }

    public static int calculator(int num1, int num2) {
        assert num1 > 0 : "a is less than 0";
        assert num2 > 0 : "b is less than 0";
        return num1 + num2;
    }
}
  • 자바 assert문 사용.
  • 예외가 발생하면 Exception을 발생 시킨다.

스프링 전역 예외 처리

Spring Framework 3.2부터 @ControllerAdvice가 나옴:
  - 각각의 컨트롤러마다 예외 처리 어노테이션을 사용하여 처리.
  - 특정 컨트롤러에 종속되지 않은 전역 예외 처리 로직 작성 가능.

단일 예외 처리

@Slf4j
@RestController
@RequestMapping("/api")
public class BoardAPIController {
    
    @Autowired
    private BoardService boardService;

    @ExceptionHandler(CustomException.class)
    @GetMapping("/board/getBoardList")
    public Page<BoardDTO> getBoardList(
            @PageableDefault(size = 10, sort = "createdDate") Pageable pageable,
            @RequestParam(required = false) String title,
            @RequestParam(required = false) String content) {
        
        Page<BoardDTO> boardDTO = 
                StringUtils.isBlank(title) || StringUtils.isBlank(content) ?
                      boardService.getBoardList(pageable) :
                      boardService.getBoardList(pageable, title, content);
        
        log.debug("boardDTO = {} ", boardDTO.getSize());
        return boardDTO;
    }
    ....
}
  • 각각의 컨트롤러마다 @ExceptionHandler(CustomException.class)를 사용하여 예외 처리를 수행한다.
@Slf4j
@RestController
@RequestMapping("/api")
public class ApprovalController {

    @Autowired
    private ApprovalService approvalService;

    /**
     * 유저 경비 출력
     * @return
     */
    @ExceptionHandler(CustomException.class)
    @GetMapping("/approval/getUserPayment")
    public String getUserPayments(...) {
        ...
    }

    /**
     * 유저 경비 저장
     * @return
     */
    @ExceptionHandler(RuntimeException.class)
    @PutMapping("/approval/savePayment")
    public String savePayment(...) {
          ...
    }
    ... 
}
  • 만약 컨트롤러가 1개가 아닐 경우?
  • 각각의 컨트롤러 안에 존재하는 메서드에 일일이 @ExceptionHandler(에러.class)를 작성한다?
    • ApprovalController
    • BoardController
    • UserController
    • AdminController
    • PaymentController

공통 예외 처리

ErrorResponse 생성

@Getter
@ToString
public class ErrorResponse {
    private final LocalDateTime timestamp = LocalDateTime.now();
    private final String errorCode;
    private final String errorMessage;

    @Builder
    public ErrorResponse(String errorCode, String errorMessage) {
        this.errorCode = errorCode;
        this.errorMessage = errorMessage;
    }
}

ExceptionEnum 생성

@Getter
@ToString
//@AllArgsConstructor
public enum ErrorCode {
    RUNTIME_EXCEPTION(HttpStatus.BAD_REQUEST, "E001"),

    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E002"),

    ;

    private final HttpStatus status;
    private final String code;
    private String message;

    ErrorCode(HttpStatus status, String code) {
        this.status = status;
        this.code = code;
    }

    ErrorCode(HttpStatus status, String code, String message) {
        this.status = status;
        this.code = code;
        this.message = message;
    }
}

Custom Exception 생성

@Getter
public class CustomException extends RuntimeException {
    private ErrorCode error;

    public CustomException(ErrorCode error) {
        this.error = error;
    }
}

ExceptionAdvice 생성

@Slf4j
@RestControllerAdvice
// public class ApiExceptionAdvice extends ResponseEntityExceptionHandler {
public class ApiExceptionAdvice {

    /**
     *  Handle CommonException
     */
    @ExceptionHandler( value = { CustomException.class } )
    public ResponseEntity<ErrorResponse> exceptionHandler(HttpServletRequest request, final CustomException e) {
        log.debug("🚀 CustomException e.getError().getStatus() = {} ", e.getError().getStatus());
        log.debug("🚀 CustomException e.getError().getCode() = {} ", e.getError().getCode());
        log.debug("🚀 CustomException e.getError().getMessage() = {} ", e.getError().getMessage());

        return ResponseEntity
                .status(e.getError().getStatus())
                .body(ErrorResponse.builder()
                        .errorCode(e.getError().getCode())
                        .errorMessage(e.getError().getMessage())
                        .build());
    }

    /**
     *  Handle RuntimeException
     */
    @ExceptionHandler( value = { RuntimeException.class } )
    public ResponseEntity<ErrorResponse> exceptionHandler(HttpServletRequest request, final RuntimeException e) {
        log.debug("🚀 RuntimeException e.getMessage() = {} ", e.getMessage());
        log.debug("ExceptionEnum.RUNTIME_EXCEPTION.getStatus() = {} ", ErrorCode.RUNTIME_EXCEPTION.getStatus());
        log.debug("ExceptionEnum.RUNTIME_EXCEPTION.getCode() = {} ", ErrorCode.RUNTIME_EXCEPTION.getCode());

        //e.printStackTrace();
        return ResponseEntity
                .status(ErrorCode.RUNTIME_EXCEPTION.getStatus())
                .body(ErrorResponse.builder()
                        .errorCode(ErrorCode.RUNTIME_EXCEPTION.getCode())
                        .errorMessage(e.getMessage())
                        .build());
    }

    /**
     *  Handle Exception
     */
    @ExceptionHandler( value = { Exception.class } )
    public ResponseEntity<ErrorResponse> exceptionHandler(HttpServletRequest request, final Exception e) {
        log.debug("🚀 Exception e.getMessage() = {} ", e.getMessage());
        log.debug("ExceptionEnum.INTERNAL_SERVER_ERROR.getStatus() = {} ", ErrorCode.INTERNAL_SERVER_ERROR.getStatus());
        log.debug("ExceptionEnum.INTERNAL_SERVER_ERROR.getCode() = {} ", ErrorCode.INTERNAL_SERVER_ERROR.getCode());

        //e.printStackTrace();
        return ResponseEntity
                .status(ErrorCode.INTERNAL_SERVER_ERROR.getStatus())
                .body(ErrorResponse.builder()
                        .errorCode(ErrorCode.INTERNAL_SERVER_ERROR.getCode())
                        .errorMessage(e.getMessage())
                        .build());
    }
}
  • 스프링 공통 예외 처리 코드.
  • 하나의 컨트롤러(@Controller)에서 예외처리 한 부분을 공통 모듈을 생성하여 처리한다.

결론

  • 깨끗한 코드는 가독성이 높아야하며 안정성 역시 높아야 한다.
  • 오류 처리를 프로그램 논리와 분리. 🌟
    • 독자적인 사안으로 고려하면 안정성이 높고 가독성이 좋은 코드 작성이 가능하다.
    • 독립적인 추론이 가능해지며 코드 유지보수성이 크게 높아진다.
  • 올바른 오류 처리를 통해 코드의 가독성과 애플리케이션의 안정성을 높이기 위해 심여를 기울이자.

참고 자료

Clone this wiki locally