[Spring Data JPA Tutorial] 12. @ControllerAdvice와 @ExceptionHandler를 이용한 전역 Error Handling

2022. 4. 18. 12:04Spring/Spring Data JPA Tutorial

반응형
  1. 전역 Error Handling의 필요성
  2. 전역 예외처리 설정
    1. 커스텀 RuntimeException 정의
    2. @ControllerAdvice를 이용한 전역 Error Handling
  3. Service, Controller 메서드 수정
    1. user 단건 조회
    2. user 추가
    3. user 수정
    4. user 삭제
  4. 변경화면
    1. 단건조회 api Example Value 개선
    2. 익셉션 발생시 Response

1. 전역 Error Handling의 필요성

이전시간에 만들었던 API에서 아쉬웠던 점이, 존재하지 않는 사용자나 가게를 조회할 때에 BaseResponse를 내보내도록 정의하여, Swagger 상에 실질적인 사용자 단건조회 또는 가게 단건조회의 response 형태를 Swagger 웹문서에서 미리보기 하기 어렵다는 점이었습니다.

03-11

03-10


그 뿐만아니라, 기존 코드를 보면 if절이 false인 경우 BaseResponse를 통해 FAIL 되었을 때의 원인을 컨트롤러 상으로 정의하게 되어있는데,

사실 이부분은 예기치 못한 상황을 마주했을 때(Exception)에 대한 응답사항이라는 공통점이 있습니다.

@PostMapping("")
public BaseResponse insert(@RequestBody @Valid final UserRequest user) {
    if(userService.insert(user)) {
        return new BaseResponse();
    }
    return new BaseResponse("이미 등록된 회원 정보입니다.");
}
@PutMapping("/{id}")
public BaseResponse update(@PathVariable("id") long id, @RequestBody @Valid final UserRequest user) {
    if(userService.update(id, user) > 0) {
        return new BaseResponse();
    }
    return new BaseResponse("일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
}

컨트롤러상에서는 입력값이 이상이 없어, api가 정상적으로 작동되었을 때에 대한 처리만 정의하고,
서비스 메서드에서 예기치 못한 상황이 발생했을 때에 대한 처리를 한다면 코드는 보다 깔끔해질 것입니다.

이번시간에는, Service 로직에서 원치않은 결과를 마주했을 때 Exception을 발생시키도록 합니다.


2. 전역 예외처리 설정

@ControllerAdvice는 특정 컨트롤러 클래스나 전역 컨트롤러에 Exception 타입에 따른 JSON 형태의 return타입을 정의할 수 있습니다.

기존에 정의되어있는 익셉션을 활용해도 되지만, 우리는 RuntimeException을 상속받은 InvalidInputException 라는 익셉션을 직접 정의하여, 에러 핸들링에 확용할 것입니다.


2.1. 커스텀 RuntimeException 정의

msg를 직접 정의할 수 있는 RuntimeException을 상속받은 InvalidInputException를 추가합니다.

public class InvalidInputException extends RuntimeException {

    private static final long serialVersionUID = -4708623386108060912L;

    public InvalidInputException() {
        super("필수 입력값을 잘못 입력하였습니다.");
    }

    public InvalidInputException(String msg) {
        super(msg);
    }

}

2.2. @ControllerAdvice를 이용한 전역 Error Handling

InvalidInputException 익셉션에 대한 @ExceptionHandler을 설정합니다.
여기에서 InvalidInputException 익셉션이 발생되었을 시 설정할 Http Status Code를 정의할 수 되어있는데, 유효하지 않은(= 존재하지 않은) 리소스에 접근했을 때 발생시킬 에러로 이 익셉션을 이용할 것이기 때문에 400 BAD_REQUEST 로 설정하였습니다.

@ControllerAdvice
public class CustomResponseEntityExceptionHandler {

    @ExceptionHandler(InvalidInputException.class)
    protected ResponseEntity<BaseResponse> invalidInputException(InvalidInputException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new BaseResponse(e.getMessage()));
    }

}

2.2.1. HttpStatus code 설정 전

참고로, 이전의 API에서는 조회할 수 없는 리소스를 조회할 때 200 OK Http Status Code가 표현되고 있었습니다.

03-17


2.2.2. HttpStatus code 설정 후

HttpStatus.BAD_REQUEST 로 설정한 후에는 아래와같이 400 Http Status Code 가 출력됩니다.

03-15


3. Service, Controller 메서드 수정

3.1. user 단건 조회

3.1.1. 수정 전

// Service
public User select(Long id) {
    User user = userRepository.findDistinctWithStoresById(id).orElse(null);
    return user;
}
// Controller
@GetMapping("/{id}")
public BaseResponse select(@PathVariable("id") long id) {
    User user = userService.select(id);
    if(user != null) {
        return new DataResponse<>(user);
    }
    return new BaseResponse("일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
}

3.1.2. 수정 후

유효하지 않는 사용자 id(존재하지 않은 사용자 id)를 이용했을때 발생되는 error 메시지는 똑같습니다.
관련 문구는 MessageUtils에 상수로 정의하여 활용하면 문구 관리가 편리해집니다.

public class MessageUtils {
    public static final String SUCCESS = "SUCCESS";
    public static final String FAIL = "FAIL";

    public static final String INVALID_USER_ID = "일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.";
}

만일, user가 존재하지 않을 경우에는 Service 의 select 메서드에서 InvalidInputException이 발생됩니다.
InvalidInputException 익셉션에 대한 처리는 @ControllerAdvice에서 대신해주기 때문에 Controller 메서드가 매우 깔끔해졌고, return 타입은 DataResponse<User> 로 수정되었습니다.

// Service
public User select(Long id) {
    User user = userRepository.findDistinctWithStoresById(id)
            .orElseThrow(() -> new InvalidInputException(MessageUtils.INVALID_USER_ID));
    return user;
}
// Controller
@GetMapping("/{id}")
public DataResponse<User> select(@PathVariable("id") long id) {
    return new DataResponse<>(userService.select(id));
}

3.2. user 추가

3.2.1. 수정 전

// Service
@Transactional
public boolean insert(final UserRequest u) {
    if(userRepository.findByEmail(u.getEmail()).isPresent()) {
        return false;
    }
    userRepository.save(User.builder()
            .birthDate(u.getBirthDate()).email(u.getEmail())
            .name(u.getName()).password(u.getPassword()).type(u.getType())
            .phoneNumber(u.getPhoneNumber()).sex(u.getSex()).build());

    return true;
}
// Controller
@PostMapping("")
public BaseResponse insert(@RequestBody @Valid final UserRequest user) {
    if(userService.insert(user)) {
        return new BaseResponse();
    }
    return new BaseResponse("이미 등록된 회원 정보입니다.");
}

3.2.2. 수정 후

MessageUtils에 기존에 문구를 정의하고, 서비스 메서드에서 조회되는 사용자가 있을경우 InvalidInputException 을 발생시킵니다.

메서드 실행중 InvalidInputException이 발생되지 않는다면, insert는 정상적으로 마친 것이기 때문에, return 타입을 void로 정의해도 됩니다.

public class MessageUtils {
    ...
    public static final String DUPLICATE_USER_EMAIL = "이미 등록된 이메일입니다.";
}
// Service
@Transactional
public void insert(final UserRequest u) {
    if(userRepository.findByEmail(u.getEmail()).isPresent()) {
        throw new InvalidInputException(MessageUtils.DUPLICATE_USER_EMAIL);
    }
    userRepository.save(User.builder()
            .birthDate(u.getBirthDate()).email(u.getEmail())
            .name(u.getName()).password(u.getPassword()).type(u.getType())
            .phoneNumber(u.getPhoneNumber()).sex(u.getSex()).build());
}
// Controller
@PostMapping("")
public BaseResponse insert(@RequestBody @Valid final UserRequest user) {
    userService.insert(user);
    return new BaseResponse();
}

3.3. user 수정

3.3.1. 수정 전

// Service
@Transactional
public int update(long id, final UserRequest u) {
    Optional<User> oUser = userRepository.findById(id);
    if(!oUser.isPresent())
        return 0;

    User user = oUser.get();
    user.setBirthDate(u.getBirthDate());
    user.setEmail(u.getEmail());
    user.setName(u.getName());
    user.setPassword(u.getPassword());
    user.setPhoneNumber(u.getPhoneNumber());
    user.setSex(u.getSex());
    user.setType(u.getType());
    userRepository.save(user);
    return 1;
}
// Controller
@PutMapping("/{id}")
public BaseResponse update(@PathVariable("id") long id, @RequestBody @Valid final UserRequest user) {
    if(userService.update(id, user) > 0) {
        return new BaseResponse();
    }
    return new BaseResponse("일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
}

3.3.2. 수정 후

Service의 update 메서드 실행중 InvalidInputException이 발생되지 않는다면, update는 정상적으로 마친 것이기 때문에, return 타입을 void로 정의해도 됩니다.

// Service
@Transactional
public void update(long id, final UserRequest u) {
    User user = userRepository.findById(id)
            .orElseThrow(() -> new InvalidInputException(MessageUtils.INVALID_USER_ID));
    user.setBirthDate(u.getBirthDate());
    user.setEmail(u.getEmail());
    user.setName(u.getName());
    user.setPassword(u.getPassword());
    user.setPhoneNumber(u.getPhoneNumber());
    user.setSex(u.getSex());
    user.setType(u.getType());
    userRepository.save(user);
}
// Controller
@PutMapping("/{id}")
public BaseResponse update(@PathVariable("id") long id, @RequestBody @Valid final UserRequest user) {
    userService.update(id, user);
    return new BaseResponse();
}

3.4. user 삭제

3.4.1. 수정 전

// Service
@Transactional
public int delete(long id) {
    Optional<User> oUser = userRepository.findById(id);
    if(oUser.isPresent()) {
        userRepository.delete(oUser.get());
        return 1;
    }
    return 0;
}
// Controller
@DeleteMapping("/{id}")
public BaseResponse delete(@PathVariable("id") long id) {
    if(userService.delete(id) > 0) {
        return new BaseResponse();
    }
    return new BaseResponse("일치하는 회원 정보가 없습니다. 사용자 id를 확인해주세요.");
}

3.4.2. 수정 후

삭제 api에서도, 서비스 메서드에서 별도의 return 타입을 지정하지 않아도 기능상 문제가 없습니다.
만일, 삭제 대상이 없다면 InvalidInputException 익셉션을 발생시킬테니까요!

// Service
@Transactional
public void delete(long id) {
    User user = userRepository.findById(id)
            .orElseThrow(() -> new InvalidInputException(MessageUtils.INVALID_USER_ID));
    userRepository.delete(user);
}
// Controller
@DeleteMapping("/{id}")
public BaseResponse delete(@PathVariable("id") long id) {
    userService.delete(id);
    return new BaseResponse();
}

그 밖에 다른 api들도 위와 같은 방식으로 수정합니다.


4. 변경화면

4.1. 단건조회 api Example Value 개선

이제는 swagger 웹화면에서 단건조회 api에 대한 Example Value 를 확인해볼 수 있습니다.

03-13

03-14


4.2. 익셉션 발생시 Response

또, api이용중 resource id를 잘못입력했을 시, 아래와 같은 response와 함께 400 에러를 출력합니다.

03-15

03-16


GitHub에서 demo 프로젝트(v1.0.8)를 다운받아 볼 수 있습니다.


++

  • @ControllerAdvice, @ExceptionHandler를 이용한 예외처리
728x90
반응형