본문 바로가기
기타

Spring에서 예외 처리하기

코동이 2022. 6. 11.

안녕하세요, oooo 백성규선임입니다. 

스프링을 사용하면서 예외처리는 무엇이고 어떻게하는 것인지 공부하면서 조금 더 효율적으로 관리하는 방법에 대해 공유하고자 합니다. 이번 시간에 자바 예외처리의 구조와 주의사항을 확인하겠습니다.

 

목차

     1. 자바 예외 클래스 구조 소개

     2. 예외처리 방법

     3. UnChecked Exception vs Checked Exception

     4. Checked Exception 특징

 

 

 

1. 자바 예외 클래스의 구조

 

자바 예외 클래스의 최상의 클래스는 Object이고 Throwable이 상속받고 있습니다. 

또한 모든 예외 클래스는 Throwable 클래스를 상속받는데, 여기서 크게 Error와 Exception으로 분리됩니다. 

 

 Error는 OutOfMemoryError(메모리 문제), StackOverFlowError(스택 크기 문제)와 같은 문제로 발생 시 복구가 불가능하며 어플리케이션이 종료됩니다. (공식문서에서는 Error의 경우 복구 시도 자체를 하지 말라고 적혀있습니다.)

 

 Exception은 이름에서 나와있다시피 모든 예외 클래스들이 상속하는 클래스입니다. 그중에 RuntimeException에 주목해야 합다.  RuntimeException을 상속하는 모든 클래스는 UnChecked Exception이고 그 이외에 모든 예외는 Checked Exception입니다.  이 구조를 잘 파악하고 있어야 내가 사용하는 예외가 어떤 종류인지, 나아가 어떻게 사용해야 하는지 효율적인 전략을 세울 수 있습니다.

 

 

2. 예외 처리 방법

 예외발생 시, 처리방법은 총 3가지가 있습니다. 재시도, 책임회피, 예외 전환에 대해 알아보겠습니다.

 

2-1. 재시도

 

 

 일정시간 대기하고 로직을 다시 실행하는 방법입니다. 최대로 시도할 횟수를 정하고, 최대 횟수를 채울때까지는 다시 반복합니다. try-catch의 catch에서 예외를 처리하지 않고, 일정시간 대기하는 것이 특징이며, 개발자에 따라 다양한 방식으로 구현할 수 있습니다. 만약 최대 횟수를 넘기면 그때서야 예외를 발생시킵니다. 예외 발생을 대비해 새로운 흐름으로 전환시킬 수 있습니다.

 

2-2. 책임 회피

 

 

 현재 발생한 예외를 내가 처리하지 않고 나를 호출한 쪽으로 책임을 회피하는 방법입니다. 나를 호출한 쪽이라는 뜻은, 내 메서드를 실행시킨 (상위의) 메서드를 의미합니다. 이 때, 명시적으로 메서드명 옆에 throws를 붙입니다. 책임회피는 다른 곳에서 나의 예외를 처리할 수 있는 장치를 마련하고서 사용해야 안전합니다.

 

2-3. 예외 전환

 

 

 예외가 발생했을 때, 다른 예외를 던지는 것입니다. 책임 회피와 차이점은, 책임 회피는 내 예외를 그대로 다른 쪽으로 떠넘기지만, 예외 전환은 내가 의도한 다른 예외를 다시 발생시킵니다. 의도된 예외 전환입니다. CheckedException에서 UnCheckedException을 발생시키면, 예외 전파를 막을 수 있습니다. 또한 스프링에서 로그인 5회 이상 실패, 잘못된 이미지 파일 확장자 검사 등 예외상황에 Unchecked Exception을 통해 처리하는 것이 일반적입니다.

 

 이상, 3가지 예외처리를 알아보았습니다. 예외가 발생 할 것을 대비해 시스템은 안전하게, 유지보수는 쉽게 가져가기 위해서 고민이 필요합니다. throws를 신중히 던져야합니다. 무분별한 사용은 예외의 범위를 너무 광범위하게 키웁니다. 반대로 무분별한 try-catch도 가독성을 해치기 쉽습니다.

 

 

3. UnCheckedException vs CheckedException

 

 실제 개발자가 예외를 처리하는데 있어서 차이점을 주의해야할 2가지 Exception을 알아보겠습니다.

 

  UnChecked Exception Checked Exception
클래스 RuntimeException 클래스와 자손 클래스 Excpetion 클래스 자손 중 
RuntimeException 제외한 모든 클래스
확인시점 실행 단계 컴파일 단계
강제 처리여부 처리를 강제하지 않음 반드시 예외처리를 해야 함
예외발생시 트랜잭션 처리 rollback 함 rollback하지 않음
대표예외 - NullPointException
- IllegalArgumentException
- IOException
- SQlException

 

 2가지 Exception를 가르는 가장 큰 기준은 "강제 처리해야하는가/아닌가"입니다. Checekd Exception의 경우, 무조건 강제 처리를 해야하지만 UnChecked Exception은 자유롭게 처리할 수 있습니다. Checked Exception의 경우, throws를 던지거나, try-catch 로 꼭 처리해야합니다. 반면, UnChecked Exception은 보통 개발자의 부주의한 코딩으로 발생하는 예외가 대부분이며, 부주의가 예외까지는 아니기 때문에 강제로 처리하지 않아도 됩니다.

 

 또한 확인 시점으로 구분할 수 있습니다. Checked Exception은 컴파일 단계에서 알 수 있으며, 개발자가 입출력 관련 코드를 작성할 때 많이 볼 수 있습니다. 따라서 해당 예외를 처리해두지 않으면 실행조차 되지 않습니다. 반면, UnChecked Exception은 실행하고 나서 발견되기 때문에, 미리 알 수 없어서 UnChecked라고 불립니다.

 

마지막으로 롤백 여부입니다. Chechked Exception은 예외 발생 시, rollback하지 않습니다. 따라서, 복구가 가능하도록 설계가 되어있으며, 복구 범위를 잘 확인하고 처리해야만 예상치 못한 코드의 실행을 예방할 수 있습니다. 다른 트랜잭션이 이미 커밋 될 수 있기 때문에 범위를 잘 살펴보아야 합니다. 반면, UnChecked Exception 기본적으로 트랜잭션을 rollback한다는 점에서 차이가 있습니다.

 

 

* Checked Exception 특징

 

먼저 강제 처리여부를 살펴보겠습니다. 개발을 하다가 예외처리를 하라는 친절한 IDE의 도움말을 여러번 경험하셨을 것입니다. 해당 경우들이 Checked Exception이며 강제처리를 해야하고, IDE에서 자체적으로 컴파일단계에서 알려주기 때문에 알기 쉽습니다.

 

 

먼저, ObjectMapper는 객체와 json의 문자열을 상호 변환해주는 유용한 기능의 객체입니다. 

해당 객체의 메서드인 writeValueAsString을 사용할 때, JsonProcessingException을 처리하라는 경고메세지가 나옵니다. 

대부분 예외메세지 이름만 잘 읽어도 그 내용을 파악할 수 있습니다. JsonProcessingException을 위한 2가지 처리방법이 있습니다.

 

 

1. Throws 던지기

 

Throws던지기() 메서드를 호출한 쪽으로 예외처리를 '위임'합니다. 

 

장점은 해당 메서드에서 명시적으로 처리하지 않아도 되고 코드에 예외처리가 없어 가독성이 좋습니다.(책임회피) 

 

단점은, 만약 연속적으로 메서드가 호출되어 책임회피가 늘어날수록, 상위에 있는 메서드의 throws 코드가 지저분해지고 예외의 위치를 찾는게 쉽지 않습니다.

 

 

(Controller가 예외까지 알 필요가 있을까? 또 저 예외는 어디에서 발생했을까?)

 

 

2. Try-Catch로 감싸기

 

 

Try_Catch감싸기는 예외가 발생하면 해당 부분을 try-catch로 감싸고 해당 메서드 내에서 예외를 처리하는 것입니다. 

 

장점은 예외처리를 책임지고 처리하므로 예외가 전파되지 않습니다. 

 

단점은, 만약 모든 예외 하나마다 try-catch를 사용한다면 오히려  코드가 많이 늘어나 가독성을 해치게 됩니다.

 

 

(ftp 연결을 종료하기위해 finally에 무려 15줄)

 

개인적으로 저는 예외가 발생했을 때, 가능하면 해당 로직에서 바로 예외를 처리합니다.

대부분, 서비스나 유틸 패키지의 예외를 다른 서비스로직이나 컨트롤러한테까지 위임할 필요는 없다는 의견입니다.

 

여기서 핵심은 Checked Exception은 개발자가 명시적으로 예외를 처리해야 한다는 것입니다.

 

지난 글에 이어 이번에는 RuntimeException을 이용해서 어떻게 처리하는지 알아보도록 하겠습니다.

 

목차

    1. 실제 요구사항

     2. 기존 예외처리 방식의 위험성

     3. RuntimException 사용하기

 

 

1. 실제 요구사항

 

 실제 현대 큐밍 홈페이지에서 요청받아 개발을 진행했던 "상담신청에 추천인 인증 추가"을 상황에 맞게 예시로 들겠습니다. 요구사항은 제품 상담신청 시, 추천인 인증을 위해 현대 소속 임직원의 명함사진을 같이 업로드할 수 있도록 이미지 첨부와 소속회사 추가였습니다.

 

추천인 인증을 완료한 상담신청은 서버에서 다음의 3가지 작업을 합니다.

 

1. 상담신청 내용 DB 저장 

2. 추천인 명함 이미지이름 및 경로 DB저장 

3. 추천인 명함 이미지 FTP 서버에 저장 

 

당연히, 3가지는 한번에 저장이 성공하거나, 실패해야 합니다. 

 

1번이 실패하면 애초에 2번, 3번까지 갈 일이 없기 때문에 문제가 없습니다.

 

하지만, 1,2번이 저장되었다고 해도 이미지가 사이즈가 너무 커서 3번이 실패하면 1,2번도 취소되어야 합니다. 즉 rollback 되어야 합니다. 이미지가 FTP 서버에 저장 되지 않았는데, 이미지 경로만 저장되거나, 이미지가 있다고 기록된 상담신청 정보가 저장되면 안됩니다.

 

 

 

2. 기존 예외처리 방식의 위험성

 

 

기존에 수행사가 리뉴얼한 코드 방식에 따라 성공/실패 여부 & 메세지를 Map형식으로 리턴하도록 해보았습니다. 

리턴된 Map은 ajax 호출결과에서 fail로 처리될 예정입니다. Checked Exception에서 Map로 결과를 리턴하는 것도 좋은 전략이지만, 여러 트랜잭션이 있는 상황에서 이렇게 예외를 처리할 때 위험의 소지가 있습니다.

 

 

 

왜냐하면, Checked Exception는 트랜잭션 rollback을 하지 않기 때문입니다.

따라서 FTP 연결 실패(3번)을 Checked Eexception으로만 처리했다면, 3번 실패 시, 1, 2번 트랜잭션은 rollback되지 않고 

DB에 저장이 그대로 유지됩니다. 해결책은, 이 예외를 다시 한번 Unchecked Exception인 RuntimeException으로 던지면 됩니다.

 

 

3. RuntimeExeption 사용하기

 

 RuntimeException은 예외발생 시, 그동안의 트랜잭션을 rollback하기 때문에 3번이 실패해도 1번과 2번 역시 취소되어 정합성을 유지합니다. 따라서 주문신청으로 커밋된 모든 트랜잭션이 rollback되고 종료됩니다. Checked Exception을 Runtime Exception으로 다시 던지면 코드의 가독성, rollback처리의 이점을 살려 안전한 예외처리를 한꺼번에 할 수 있습니다. (예외 전환)

 

 

스프링에서 RuntimeException을 사용하여 예외를 처리할 수 있는  전략이 있습니다.  @ControllerAdvice, @ExceptionHandler입니다.  이 어노테이션을 이용하여 애플리케이션의 모든 RuntimeException 예외를 한 곳에서 처리할 수 있습니다.

 

 

 

RuntimeException을 상속받아서 새로운 예외 클래스를 하나 만듭니다. Ftp연결이 실패하는 경우이므로

FtpConnectionFailedException이라고 이름 지었습니다. 생성자에 super(message)를 넣어 내가 실제 사용시 적은 메세지를 보관합니다.

 

 

내가 정의한 여러 예외 클래스들을 처리할 전역 예외처리 클래스를 만듭니다.

@ControllerAdvice를 선언하면 해당 클래스는 Bean으로 등록되고 컨트롤러의 동작을 감지합니다.  

@ExceptionHandler 에 등록한 예외가 발생하였다면 이를 감지하고 해당 메서드로 가져와 처리합니다.(복수 등록 가능)

@ResponseStatus로 응답 상태를 설정합니다.

리턴은 Map을 많이 사용하지만 좀 더 뚜렷한 의미전달을 위해 ErrorResponse 이름의 클래스로 전달합니다.

 

 

 

lombok인 @Builder로 생성자를 안전하게 만들어 객체를 전달하도록 합니다.

 

 

이전에는 Map으로 처리했지만 내가 만든 FtpConnectionFailedException으로 예외가 전환됩니다. 

@ControllerAdvice에서 @ExceptionHandler가 예외를 감지하고 ErrorResponse 형태로 ajax에 전달됩니다.

해당 예외는 RuntimeException 이므로 이 로직에서 실행된 모든 트랜잭션은 rollback처리가 됩니다.

 

 

일부러 예외를 발생시키고 결과 리턴값을 콘솔에 찍어보았습니다. 우리가 정의한 message와 status가 정상적으로 리턴됩니다. 

 

 try-catch에서 예외전환 뿐 아니라, 일반 애플리케이션 로직에서도 RuntimException을 활용하여 통일성 있는 예외처리가 가능합니다. 로그인 시도 시 아이디 혹은 비밀번호가 틀리는 경우, 회원가입에서 필수값을 입력하지 않은 경우, 세션이 없거나 권한이 없는 경우 등등에 사용할 수 있으며, 다른 로직에서도 재활용이 가능하다는 장점이 있습니다.

 

 이상으로 자바의 예외처리에 대해 알아보았습니다. 글을 쓰면서 조사하지 않았으면, 저도 놓치고 지나쳤을 부분들이 있었습니다. 이 글을 보시는 모든 분들도 각자 관리하시는 프로젝트와 비교해보며 발전하는 시간이 되셨기를 바랍니다. 내용 중 이해가 안되거나 잘못된 부분 알려주시면 열심히 답변드리고 피드백하도록 하겠습니다. 감사합니다

반응형

'기타' 카테고리의 다른 글

ip주소 확인하는 방법  (0) 2023.10.31
네이버 EP 작업기록  (2) 2022.06.12
OCP(Open Closed Principle) 원칙  (0) 2022.06.11
현업들과 협력/의사소통하기  (0) 2022.06.10
lombok local에서 실행하기  (0) 2022.05.24