| 용어
종류 | 내용 | |
오류(error) | OutOfMemory Exception | 시스템 상 메모리 부족 |
StackOverflow Exception | 스택 오버플로우 발생 | |
예외(exception) | checked Exception | Compile 시 체크 : type, syntax 에러 |
unchecked Exception | Runtime 시 확인 : 다양함 |
| 올바른 예외 핸들링 방법
- 예외 핸들링 : 런타임 시에 발생하는 예외에 대한 핸들링
- 올바른 예외 핸들링이란?
(1) 예외 상황이 발생할 때, return을 하지 말고 throw를 해라
(2) try - catch 문을 통해 잡은 예외는 꼭 처리하여 사용자단에 안내한다.
(3) 예외 상황일 때만 예외를 던지고, 아닐 경우, 남발하지 않는다.
(4) 최상위 예외인 Exception만 사용하지 말고, 가능한 예외사항들을 모두 catch한다.
(5) call stack을 잃어버리지 않도록, custom exception 생성자 안에 전달한다.
(6) 너무 많은 custom exception 또한 가독성을 해치므로 지양한다.
** 추가적으로, 예외처리 외에 try문을 쓸 때는 가능하면 try-resource를 쓰는 게 낫다.
| 관련 배경지식
1. 스프링 부트의 디폴트 예외 처리 - BasicErrorController
- 클라이언트에서 어떤 요청사항을 전달하면, 컨트롤러에서 서비스 함수를 호출하여 로직을 실행한다.
이 과정에서 실시간으로 비즈니스 로직상의 예외가 발생할 수 있다.
- Spring Boot의 경우, 예외가 발생하면 BasicErrorController가 디폴트인 예외 정보를 전달하는데,
내부적으로는 ResponseEntity안에 에러 정보(Object타입)와, 상태(HttpStatus) 정보를 담아 전달한다.
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
>> BasicErrorController를 통해 예외 정보를 전달할 땐 timestamp, status, error, path 정보를 전달한다.
HTTP/1.1 404
Content-Type: application/json
Content-Language: ko-KR
Transfer-Encoding: chunked
Date: Fri, 14 Oct 2022 10:21:58 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"timestamp": "2022-10-14T10:21:58.621+00:00",
"status": 404,
"error": "Not Found",
"path": "/"
}
2. AOP에 대한 지식
- AOP란, Aspect Oriented Programming 의 약자로,
관점에 따라 1) 핵심, 2) 부가를 나누어 처리하는 것을 말한다.
- 일반적으로 1) 핵심은 비즈니스 로직이며,
2) 부가는 DB 트랜잭션 동기화 처리, 로깅, 예외처리, 파일 입출력 등이 될 수 있다.
Aspect = Advice + PointCut
// Advice : @Before, @After, @Around 가 붙은 메소드
// PointCut : AspectJ를 통해 정확한 @Target 지정
개념 | 정의 | 코드 |
Aspect | 부가 관심사 그 자체 | @Aspect |
JoinPoint | 클라이언트가 호출하는 모든 비즈니스 메소드 * 일종의 포인트 컷 후보 |
|
Target | Aspect를 적용할 곳 (클래스, 메소드..) * Annotation으로 지정 |
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface AccountLock{ long tryLockTime() default 5000L; } |
Advice | 실제 구현 기능 | @Before("AspectJ문") @After("AspectJ문") @AfterReturning("AspectJ문") @AfterThrowing("AspectJ문") @Around("AspectJ문") |
PointCut | 특정 조건에 의해 필터링된 조인포인트 * AspectJ 표현식 사용 |
** 링크 : https://github.com/simDev1234/Account/commit/1b255f1656acd998c3f72135892517823d569aa1
package com.example.account.service;
import com.example.account.aop.AccountLockIdInterface;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class LockAopAspect {
private final LockService lockService;
@Around("@annotation(com.example.account.aop.AccountLock) && args(request)")
public Object aroundMethod(
ProceedingJoinPoint pjp,
AccountLockIdInterface request
) throws Throwable {
// lock 취득 시도
lockService.lock(request.getAccountNumber());
try {
return pjp.proceed();
} finally {
// lock 해제
lockService.unlock(request.getAccountNumber());
}
}
}
- 예외를 핸들링하는 것도 AOP의 부가 Aspect에 해당된다.
스프링 부트에서는 @ControllerAdvice 또는 @RestControllerAdvice를 통해,
Controller 및 RestController에서 어떠한 처리가 일어날 때마다 @ExceptionHandler에 작성한 코드를 실행한다.
* 기본적으로 클래스 안에 @ExceptionHandler를 작성하면, 그 클래스에서 발생하는 예외 처리를 그 핸들러에서 한다.
| 예외 처리하기 실습
- 일반적으로 회사에서는
(1) GlobalExceptionHandler를 통해서 Custom Exception을 포함해 자주 사용하는 예외 및 최상위 예외를 핸들링한다.
(2) 이 때, 반환 타입으로는 BasicErrorController와 마찬가지로 ResponseEntity를 사용할 수 있는데,
앞서 보았듯이 ResponseEntity는 Response의 body에 넣을 데이터와, Response Status 값을 포함한다.
따라서, Response body에 넣어줄 객체를 따로 생성하는데, 나는 이걸 ErrorResponse로 두었다.
(3) 비즈니스 로직에 맞는 커스텀 예외를 만들 때 가장 먼저 RuntimeException을 상속한다.
기본적으로 RuntimeException은 String타입의 message를 포함하며, Throwable를 생성자에 넣어주어야만, throw를 할 수 있다. 자주 실수하는 부분으로 throwable를 안 넣어주면 커스텀 예외처리하는 의미가 없단다.
(4) 커스텀 예외 맴버변수로, 추가적으로 Enum타입의 ErrorCode를 넣어줄 수 있는데, 이렇게 ErrorCode로 데이터들을 싸주는 이유는, 아무래도 서비스에서 CustomException을 throw할 때, 한 줄로 던질 수 있기 때문에서라 보인다.
** throw 해서 받은 ErrorCode객체 안의 데이터들은,
(2)의 Response body에 넣어줄 비즈니스 로직 에러에 대한 영문 코드 + 한글 message와, HttpStatus를 갖는다.
public RuntimeException(String message, Throwable cause) {
super(message, cause);
}
GlobalExceptionHandler | ErrorResponse | CustomException | ErrorCode(enum) | |
용도 | 컨트롤러의 예외 발생 시 핸들링 |
Response body에 담을 데이터 묶음 |
비즈니스 로직에 맞는 사용자 정의 예외 |
|
상속 | - | - | RuntimeException | - |
Annotation | @ControllerAdvice @RestControllerAdvice |
@Builder @Value |
@Getter @AllArgs.. |
@RequiredArgsCon... @Getter |
Member | @ExceptionHandler + 사용자 정의 예외 + 자주 나타나는 예외 + 최상위 예외 처리 : ResponseEntity |
- String code - String message |
- ErrorCode code | - HttpStatus status - String message |
1. ErrorCode 만들기
package com.example.exceptionhandling.type;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
@RequiredArgsConstructor
@Getter
public enum ErrorCode {
MEMBER_NOT_FOUND_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "회원을 찾을 수 없습니다."),
MEMBER_USER_ID_NOT_MATH_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR,"아이디가 일치하지 않습니다."),
MEMBER_ALREADY_UNREGISTERED_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR,"탈퇴한 회원입니다.");
private final HttpStatus status;
private final String message;
}
2. Custom Exception 클래스 만들기
package com.example.exceptionhandling.exception;
import com.example.exceptionhandling.type.ErrorCode;
import lombok.Getter;
@Getter
public class MemberException extends RuntimeException{
private final ErrorCode code;
public MemberException(ErrorCode code) {
super(code.getMessage());
this.code = code;
}
}
3. ErrorResponse 만들기
package com.example.exceptionhandling.exception;
import lombok.Builder;
import lombok.Value;
@Builder
@Value
public class ErrorResponse {
private String code;
private String message;
}
4. GlobalExceptionHandler 클래스 만들기
package com.example.exceptionhandling.exception;
import com.example.exceptionhandling.type.ErrorCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = MemberException.class)
public ResponseEntity<ErrorResponse> handleMemberException(MemberException e){
ErrorCode code = e.getCode();
ErrorResponse response = ErrorResponse.builder()
.code(code.name())
.message(code.getMessage())
.build();
return new ResponseEntity(response, code.getStatus());
}
}
>> 만약, GlobalExceptionHandler에 사용자 예외 이외의 예외처리를 한다면 아래와 같다.
package com.example.account.exception;
import com.example.account.dto.ErrorResponse;
import com.example.account.type.ErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import static com.example.account.type.ErrorCode.INVALID_REQUEST;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// 사용자 정의 예외 사항
@ExceptionHandler(AccountException.class)
public ErrorResponse handleAccountException(AccountException e) {
log.error("{} is occured.", e.getErrorCode());
return new ErrorResponse(e.getErrorCode(), e.getErrorMessage());
}
// 자주 발생하는 예외 사항을 중간에 넣어준다. (자바 또는 스프링에 정의된 예외 사항들 -- 대체로 10개 가량)
@ExceptionHandler(DataIntegrityViolationException.class)
public ErrorResponse handleDataIntegrityViolationException(DataIntegrityViolationException e) {
log.error("DataIntegrityViolationException is occured.", e);
return new ErrorResponse(INVALID_REQUEST, INVALID_REQUEST.getDescription());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error("MethodArgumentNotValidException is occured.", e);
return new ErrorResponse(INVALID_REQUEST, INVALID_REQUEST.getDescription());
}
// Exception 예외 사항
@ExceptionHandler(Exception.class)
public ErrorResponse handleException(Exception e) {
log.error("Exception is occured.", e);
return new ErrorResponse(
ErrorCode.INTERNAL_SERVER_ERROR,
ErrorCode.INTERNAL_SERVER_ERROR.getDescription());
}
}
[ 출처 및 참조 ]
- 부트캠프 클린코딩 예외처리 수업을 들은 후 정리
- AOP 주요 개념 : https://engkimbs.tistory.com/746
- Exception Handler
- @Value https://mangkyu.tistory.com/167
'Etc > CleanCoding' 카테고리의 다른 글
노션 스터디 기록 - 북스터디, 기술면접, 공식 도큐먼트 모음 (0) | 2023.05.27 |
---|---|
[클린코딩] 가독성 높이는 습관 (0) | 2022.10.10 |
[클린코딩] 좋은 코드란 무엇인가?, 레거시 코드 (0) | 2022.10.10 |