스프링부트

Spring REST 서비스 예외 처리 방법

알쓸개잡 2023. 11. 9.

Spring에서는 전체 예외 처리에 대한 추상화를 제공하고 몇 가지 annotation만으로 예외 처리를 손쉽게 할 수 있다.

이번 포스팅에서는 Spring REST 서비스에서 예외를 처리하는 방법과 HTTP 응답 상태코드를 반환하는 방법에 대해서 기록한다.

 

1. 예외 수동 처리

다음 Controller 코드는 HttpStatus와 함께 응답 body 페이로드를 포함하는 ResponseEntity를 반환한다.

  • 예외가 발생하지 않으면 200 코드와 함께 body 페이로드로 Member를 응답한다.
  • ResourceNotFoundException 예외가 발생하는 경우 empty body와 404 코드를 응답한다.
  • MemberServiceException 예외가 발생하는 경우 empty body 와 500 코드를 응답한다.

예외 클래스는 RuntimeException을 확장한 ResourceNotFoundException과 MemberServiceException이다.

package com.example.jpa.exception;

public class ResourceNotFoundException extends RuntimeException{
	public ResourceNotFoundException() {
		super();
	}

	public ResourceNotFoundException(String message) {
		super(message);
	}
}
package com.example.jpa.exception;

public class MemberServiceException extends RuntimeException{
	public MemberServiceException() {
		super();
	}

	public MemberServiceException(String message) {
		super(message);
	}
}
@RestController
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {
	private final MemberService memberService;

	@GetMapping("/{memberId}")
	public ResponseEntity<Member> getMember(@PathVariable Long memberId) {
		Member member;
		try {
			member = memberService.getMember(memberId);
		} catch (MemberServiceException e) {
			return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
		} catch (ResourceNotFoundException e) {
			return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
		}

		return ResponseEntity.ok(member);
	}
}

컨트롤러의 endpoint 메서드 내에서 직접 예외 처리를 하고 있는데 이 방식의 문제점은 중복이다.

DELETE, POST를 처리하는 다른 endpoint에서도 중복되는 예외 처리 코드가 필요할 수 있다.

 

 

2. 예외 클래스에 ResponseStatus annotation 사용

두 번째 방법은 예외 클래스에 @ResponseStatus annotation을 지정하여 해당 예외가 발생했을 때 지정된 ResponseStatus 코드로 자동 응답 하도록 할 수 있다.

package com.example.jpa.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException{
	public ResourceNotFoundException() {
		super();
	}

	public ResourceNotFoundException(String message) {
		super(message);
	}
}
package com.example.jpa.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public class MemberServiceException extends RuntimeException{
	public MemberServiceException() {
		super();
	}

	public MemberServiceException(String message) {
		super(message);
	}
}
@RestController
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {
	private final MemberService memberService;

	@GetMapping("/{memberId}")
	public ResponseEntity<Member> getMember(@PathVariable Long memberId) {
		Member member = memberService.getMember(memberId);
		return ResponseEntity.ok(member);
	}
}

memberService.getMember 메서드 내부에서 MemberServiceException, ResourceNotFoundException이 발생하는 경우 각 예외 클래스에 @ResponseStatus에 지정된 응답 코드로 응답을 한다.

예외가 발생한 경우 다음과 같이 응답 payload도 함께 전송된다.

{
    "timestamp": "2023-11-09 00:05:56",
    "status": 404,
    "error": "Not Found",
    "path": "/members/1"
}

 

 

3. Controller Advice 클래스 (@ControllerAdvice)

Spring은 @ControllerAdvice 클래스 annotation 이 지정된 클래스를 통해서 중앙 집중적으로 모든 예외에 대한 처리를 할 수 있도록 지원한다.

@ControllerAdvice
public class ControllerAdviser {

	@ExceptionHandler({RuntimeException.class})
	public ResponseEntity<String> runtimeException(RuntimeException e) {
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
	}

	@ExceptionHandler({ResourceNotFoundException.class})
	public ResponseEntity<String> resourceNotFoundException(ResourceNotFoundException e) {
		return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
	}

	@ExceptionHandler({MemberServiceException.class})
	public ResponseEntity<String> memberServiceException(MemberServiceException e) {
		return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
	}
}

@ExceptionHandler annotation에 지정된 예외를 처리하는 handler 메서드를 등록하여 처리한다.

@RestController
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {
	private final MemberService memberService;

	@GetMapping("/{memberId}")
	public ResponseEntity<Member> getMember(@PathVariable Long memberId) {
		Member member = memberService.getMember(memberId);
		return ResponseEntity.ok(member);
	}
}

 

 

@ExceptionHandler annotation 이 지정된 handler 메서드에 @ResponseStatus annotation을 지정할 수 있다.

@ControllerAdvice
public class ControllerAdviser {

	@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
	@ExceptionHandler({RuntimeException.class})
	public void runtimeException() {}

	@ResponseStatus(HttpStatus.NOT_FOUND)
	@ExceptionHandler({ResourceNotFoundException.class})
	public void resourceNotFoundException() {}

	@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
	@ExceptionHandler({MemberServiceException.class})
	public void memberServiceException() {}
}

위와 같이 지정된 경우에는 body payload 없이 코드만 응답한다.

예외 클래스에 @ResponseStatus 어노테이션이 적용되어 있고 @ControllerAdvice 클래스의 예외 핸들러에 @ResponseStatus 어노테이션이 적용되어 있는 경우 @ControllerAdvice 클래스의 예외 핸들러가 동작한다.

댓글

💲 추천 글