반응형

가독성 높이는 습관 - 익셉션 핸들링 (1)

 

 

프로그래밍에서 에러

Compile Error

  • 컴파일 타임에 발생하는 에러
  • syntax (잘못된 문법)
  • type (잘못된 타입)

 

Runtime Error

  • 프로그램 실행 중 발생하는 에러

 

 

오류(error)와 예외(exception)

Throwable throwable = new Throwable();
  • Object → Throwable → Error(unchecked) or Exception(checked)

 

오류

  • 대부분의 시스템에서 비정상적인 상황을 마주했을 때 발생
  • OutOfMemoryError : JVM에 할당된 메모리 부족
  • StackOverFlowError : 재귀와 같은 특정 상황에 빠져 콜 스택이 꽉찬 경우
  • 이러한 에러들은 어플리케이션에서 캐치를 하더라도 처리할 방법이 없음

 

예외

  • 작성된 코드 내에서 발생하므로 예외가 발생했을 때 처리방법을 잘 알고 있어야함
  • Exception(checked) → RunTimeException(unchecked) or Other Exceptions(checked)
  • RunTimeException : 예외처리를 강제하지 않으며, 런타임에 체크
  • ex. NullPointerExcption, IllegalArgumentException, ArithmeticException, IndexOutBoundsException
  • Other Exceptions : 호출하는 대상에게 예외처리를 강제하고, 컴파일 타임에 체크 (명시적인 처리 필요)
  • ex. ClassNotFoundException, IOException, FileNotFoundException, SQLException, NoSuchMethodException

 

 

잘못된 핸들링 - return 값을 통한 에러 핸들링 (1)

// 절대 따라하지 말 것
public void foo() {
    int result = otherService.call();
    if (result == 0) {
        log.info("정상");
        // 정상일 때 return 값 작성
    } else {
        log.info("에러");
        // 에러일 때 return 값 작성
    }
}
// 절대 따라하지 말 것
public void foo() {
    String result = otherService.call();
    if (result.equalsIgnoreCase("SUCCESS")) {
        log.info("정상");
        // 정상일 때 return 값 작성
    } else {
        log.info("에러");
        // 에러일 때 return 값 작성
    }
}
// 절대 따라하지 말 것
public void foo() {
    ResultCode result = otherService.call();
    if (result == ResultCode.SUCCESS) {
        log.info("정상");
        // 정상일 때 return 값 작성
    } else {
        log.info("에러");
        // 에러일 때 return 값 작성
    }
}

 

 

잘못된 핸들링 - return 값을 통한 에러 핸들링 (2)

// 절대 따라하지 말 것
public void foo(Parameter parameter) {
    Map<String, Object> response = new HashMap<>();
    otherService.call(parameter, response);
    if (response.get("ERROR") == null) {
        log.info("정상");
        // 정상일 때 return 값 작성
    } else {
        log.info("에러");
        // 에러일 때 return 값 작성
    }
}
// 절대 따라하지 말 것
public void foo() {
    Map<String, Object> result =  otherService.call();
    if (result.get("ERROR") == null) {
        log.info("정상");
        // 정상일 때 return 값 작성
    } else {
        log.info("에러");
        // 에러일 때 return 값 작성
    }
}
// 절대 따라하지 말 것
public void foo() {
    SuperDto result =  otherService.call();
    if (result.getError() == null) {
        log.info("정상");
        // 정상일 때 return 값 작성
    } else {
        log.info("에러");
        // 에러일 때 return 값 작성
    }
}

 

return을 통한 핸들링의 문제점

  • 함수를 호출한 모든 곳이 복잡해짐
  • 에러를 처리하는 로직과 기존의 비지니스 로직이 뒤섞여서 하나의 함수가 여러가지 일을 수행
  • 유사한 에러로직이 어플리케이션 전반에 중복되게 됨
  • 새로 추가된 로직에 대해서 누락하기 쉬움

 

 

올바른 핸들링 - throw 활용하기

try {
   otherService.call();
   // 정상 케이스
} catch (NullPointException | NoSuchElementException e) {
    log.error(e.getMessage());
} catch (IndexOutOfBoundsException e) {
    log.error(e.getMessage());
} catch (RuntimeException e) {
    log.error(e.getMessage());
}
// try-catch 사이에 발생한
// 모든 RuntimeException을 받을 수 있음
void call() {
    throw new RuntimeException("어떤 에러가 발생함");
}

 

 

올바른 핸들링 - catch 절에서 잡은 익셉션은 반드시 처리하기

try {
    otherService.call();
} catch (RuntimeException e) {
    e.printStackTrace();
    // doNoting
}
  • 서버에서 직접 로그를 확인하지 않는 이상 해당 메서드가 정상 동작을 하는지 알 수 없음
  • 이러한 코드는 에러 상황을 정상 상황으로 속였기 때문에 에러 추적이 굉장히 어려움
  • 특히 스프링 트랜잭션과 뒤섞여 있다면 어플리케이션은 정상적으로 진행했다고 클라이언트에 응답하겠지만,
  • 내부적으로는 DB에서 rollback 하는 기이한 현상이 발생하기도 함

 

 

올바른 핸들링 - 익셉션은 정말 예외 상황일 때만 사용하기

public String decrypt(String encryptedText) {
    try {
        return cryptoService.decryptRsa(encryptedText);
    } catch (Exception e) {
        try {
            return cryptoService.decryptAes(encryptedText);
        } catch (Exception exception) {
            return cryptoService.decryptDes(encryptedText);
        }
    }
}

 

 

올바른 핸들링 - 최상위 Exception 하나로만 잡지 않기

try {
    otherService.call();
} catch (Exception e) {
    if (e instanceof RuntimeException) {
        // doSomething
    } else if (e instanceof NoSuchElementException) {
        // doSomething
    }
}

 

 

올바른 핸들링 - call stack 잃어버리지 않기

try {
    otehrService.call();
} catch (RuntimeException e) {
    throw new ZeroBaseException();
}

 

 

올바른 핸들링 - 너무 많은 CustionException 만들지 않기

(생략)

 

 

올바른 핸들링 - Checked Exception

public String decrypt(String encryptedText) {
    try {
        return cryptoService.decryptRsa(encryptedText);
    } catch (NoSuchPaddingException | NoSuchAlgorithmException e) {
        return ""; // 복구해야하는데 어떻게 하지...
    }
}

public User foo(User user) {
    try {
        return jdbcService.executeQuery("select * from user where user_id = 1");
    } catch (SQLException e) {
        return null; // 복구해야하는데 어떻게 하지...
    }
}
public String decrypt(String encryptedText) throws NoSuchPaddingException, NoSuchAlgorithmException {
    return cryptoService.decryptRsa(encryptedText);
}

public User foo(User user) throws SQLException {
    return jdbcService.executeQuery("select * from user where user_id = 1");
}
public String decrypt(String encryptedText) {
    try {
        return cryptoService.decryptRsa(encryptedText);
    } catch (NoSuchPaddingException | NoSuchAlgorithmException e) {
        throw new ZeroBaseException(e);
    }
}

public User foo(User user) {
    try {
        return jdbcService.executeQuery("select * from user where user_id = 1");
    } catch (SQLException e) {
        throw new ZeroBaseException(e);
    }
}

 

 

 

정리

- Compile Eroor와 Runtime Error 가 존재

- 오류와 예외가 존재

- Checked exception과 Unchecked exception

- 올바른 핸들링

  • 1. throw 하기
  • 2. 잡은 익셉션은 반드시 처리하기
  • 3. 예외 상황일 때만 쓰기
  • 4. 최상위에서 exception 한 개만 잡지 않기
  • 5. call stack 잃어버리지 않기
  • 6. 너무 많은 custom exception은 지양하기

 

 


반응형

+ Recent posts