[실습] 서비스 계층과 API 계층의 연동 실습
서비스 계층과 API 계층의 연동 실습 개요
- 이번 실습은 실습용 샘플 프로젝트에 포함되어 있는 CoffeeController 클래스와 CoffeeService 클래스를 연동하는 실습입니다.
- MemberController와 MemberService는 여러분들이 이미 학습했으므로 실습에서 제외합니다.
- OrderController와 OrderService는 완성된 코드가 이미 포함되어 있습니다.
- 지난 챕터까지 학습했던 구현 코드들이 기본적으로 포함이 되어 있으며, 이를 기반으로 CoffeeController 클래스, CoffeeMapper 인터페이스, CoffeeService 클래스를 요구 사항에 맞게 구현하면 됩니다.
- ConffeeController 클래스는 서비스 계층과 연동되지 않은 레거시 코드가 포함되어 있습니다.
- CoffeeController 클래스에서 사용할 DTO 클래스 역시 이미 포함되어 있습니다.
- 실습에 필요한 클래스는 ‘com.springboot’ 내의 기능별 패키지(member, coffee, order)에 포함되어 있습니다.
- 기능별 패키지(member, coffee, order)에 추가적인 하위 패키지를 생성하려면 member 패키지를 참고해서 만들면 됩니다.(하위 패키지는 편의상 만들어도 되고 만들지 않아도 상관없습니다)
- 예) member 패키지 구조
- member
- controller
- service
- dto
- entity
- mapper
- member
- 예) member 패키지 구조
실습 사전 준비
- 실습용 샘플 프로젝트 복제
- 아래 github 링크에서 실습용 repository를 fork합니다.
- fork한 repository를 여러분의 PC에서 git clone 명령으로 local repository에 복제합니다.
- IntelliJ IDE로 clone 받은 forked local repository 디렉토리의 프로젝트를 Open합니다.
- 아래 실습 요구 사항에 따라 실습을 진행합니다.
- 작성한 코드는 main branch에 작성해 주세요.
- main branch가 아닌 별도의 branch를 생성해서 작업을 했다면 작업이 끝난 후, 반드시 main branch로 merge 해야 합니다.
CoffeeService 구현
공통 제한 사항
- CoffeeService 클래스는 새로 생성 후, 구현해야 합니다.
- CoffeeService 클래스에서 DTO 클래스를 사용하지 않아야 합니다.
- CoffeeService 클래스는 Spring Bean으로 등록되어야 합니다.
- CoffeeService 클래스에서 사용하는 Coffee 엔티티(Entity) 클래스의 데이터는 다음으로 제한합니다.
- coffeeId(커피 식별자): long
- korName(한글 커피명): String
- engName(영문 커피명): String
- price(가격): int
구현 내용
- 다음과 같이 총 다섯 개의 메서드를 구현합니다.
- createCoffee(Coffee coffee)
- 파라미터: 등록할 커피 정보(Coffee 클래스 객체)
- 메서드 바디
- 구현해야 될 내용 없습니다.
- 리턴 값
- 커피 정보: 파라미터로 전달받은 Coffee 클래스 객체를 그대로 리턴합니다.
- updateCoffee(Coffee coffee)
- 파라미터: 수정할 커피 정보(Coffee 클래스 객체)
- 메서드 바디
- 구현해야 될 내용 없습니다.
- 리턴 값
- 커피 정보: 파라미터로 전달받은 Coffee 클래스 객체를 그대로 리턴합니다.
- findCoffee(long coffeeId)
- 파라미터: 조회할 커피의 커피 식별자(long)
- 메서드 바디
- 구현해야 될 내용 없습니다.
- 리턴 값
- 커피 정보: 아래 Stub 데이터를 포함한 Coffee 클래스의 객체를 리턴합니다.
- 커피 식별자: 파라미터로 전달받은 커피 식별자(coffeeId)를 포함합니다.
- 한글 커피명: 아메리카노
- 영문 커피명: Americano
- 가격: 2500
- 커피 정보: 아래 Stub 데이터를 포함한 Coffee 클래스의 객체를 리턴합니다.
- findCoffees()
- 파라미터: 없음
- 메서드 바디
- 구현해야 될 내용 없습니다.
- 리턴 값
- 커피 정보: 아래 Stub 데이터를 포함한 List<Coffee>를 리턴합니다.
- 커피 정보 1
- 커피 식별자(coffeeId): 1L
- 커피명(한글): 아메리카노
- 커피명(영문): Americano
- 가격: 2500
- 커피 정보 2
- 커피 식별자(coffeeId): 2L
- 커피명(한글): 캐러멜 라떼
- 커피명(영문): Caramel Latte
- 가격: 5000
- 커피 정보 1
- 커피 정보: 아래 Stub 데이터를 포함한 List<Coffee>를 리턴합니다.
- deleteCoffee(long coffeeId)
- 파라미터: 삭제할 커피의 커피 식별자(long)
- 리턴 값: 없음
- 메서드 바디
- 구현해야 할 내용 없습니다.
- createCoffee(Coffee coffee)
CoffeeController 및 CoffeeMapper 구현
공통 제한 사항
- CoffeeController 클래스는 현재 작성되어 있는 CoffeeController를 수정해야 합니다.
- CoffeeMapper 인터페이스는 새로 생성 후, 정의해야 합니다.
- CoffeeMapper는 MapStruct를 이용해서 구현해야 합니다.
- CoffeeController 클래스에서는 DI를 이용해서 CoffeeService 클래스와 Mapper 클래스의 객체를 사용해야 합니다.
- CoffeeController 클래스에서는 서비스 계층에서 사용하는 Coffee 엔티티 클래스를 응답 데이터로 전송하지 않아야 합니다.
구현 내용
- DTO 클래스 ↔ 엔티티 클래스 매퍼(Mapper) 구현
- CoffeeController의 핸들러 메서드에서 DTO 클래스와 Coffee 엔티티 클래스 매핑에 사용될 CoffeeMapper를 MapStruct로 구현하세요.
- CoffeeController 핸들러 메서드에서 CoffeeService 클래스의 메서드 호출 연동
- CoffeeController 핸들러 메서드에서 CoffeeService 클래스의 메서드를 호출해서 결과 데이터(Coffee 엔티티 객체)를 받아오세요. (’→’는 메서드 호출을 의미합니다.)
- postCoffee() → createCoffee()
- patchCoffee() → updateCoffee()
- getCoffee() → findCoffee()
- getCoffees() → findCoffees()
- deleteCoffee() → deleteCoffee()
- coffeeService.deleteCoffee() 메서드의 경우 리턴값이 없으므로 결과 데이터를 받아올 수 없음을 참고하세요.
- CoffeeController 핸들러 메서드에서 CoffeeService 클래스의 메서드를 호출해서 결과 데이터(Coffee 엔티티 객체)를 받아오세요. (’→’는 메서드 호출을 의미합니다.)
- CoffeeController의 핸들러 메서드에 CoffeeMapper 적용
- CoffeeController의 핸들러 메서드에 CoffeeMapper를 적용하세요.
- 매퍼(Mapper)는 아래 상황에 필요합니다.
- CoffeeController 클래스에서 CoffeeService 클래스의 createCoffee(), updateCoffee() 메서드 호출 시
- CoffeeController 핸들러 메서드의 응답 데이터
CoffeeController
package com.codestates.coffee.controller;
import com.codestates.coffee.*;
import com.codestates.coffee.dto.CoffeePatchDto;
import com.codestates.coffee.dto.CoffeePostDto;
import com.codestates.coffee.dto.CoffeeResponseDto;
import com.codestates.coffee.entity.Coffee;
import com.codestates.coffee.mapper.CoffeeMapper;
import com.codestates.coffee.service.CoffeeService;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Positive;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/v5/coffees")
@Validated
public class CoffeeController {
private final CoffeeService coffeeService;
private final CoffeeMapper mapper;
public CoffeeController(CoffeeService coffeeService, CoffeeMapper mapper) {
this.coffeeService = coffeeService;
this.mapper = mapper;
}
@PostMapping
public ResponseEntity postCoffee(@Valid @RequestBody CoffeePostDto coffeePostDto) {
// TODO CoffeeService 클래스와 연동하세요.
// TODO DTO <-> Entity 변환 Mapper를 적용하세요.
Coffee coffee = mapper.CoffeePostDtoToCoffee(coffeePostDto);
Coffee response = coffeeService.createCoffee(coffee);
return new ResponseEntity<>(mapper.coffeeToCoffeeResponseDto(response), HttpStatus.CREATED);
}
@PatchMapping("/{coffee-id}")
public ResponseEntity patchCoffee(@PathVariable("coffee-id") @Positive long coffeeId,
@Valid @RequestBody CoffeePatchDto coffeePatchDto) {
coffeePatchDto.setCoffeeId(coffeeId);
// TODO CoffeeService 클래스와 연동하세요.
// TODO DTO <-> Entity 변환 Mapper를 적용하세요.
Coffee coffee = mapper.CoffeePatchDtoToCoffee(coffeePatchDto);
Coffee response = coffeeService.updateCoffee(coffee);
return new ResponseEntity<>(mapper.coffeeToCoffeeResponseDto(response), HttpStatus.OK);
}
@GetMapping("/{coffee-id}")
public ResponseEntity getCoffee(@PathVariable("coffee-id") long coffeeId) {
// TODO CoffeeService 클래스와 연동하세요.
// TODO DTO <-> Entity 변환 Mapper를 적용하세요.
Coffee coffee = coffeeService.findCoffee(coffeeId);
return new ResponseEntity<>(mapper.coffeeToCoffeeResponseDto(coffee), HttpStatus.OK);
}
@GetMapping
public ResponseEntity getCoffees() {
// TODO CoffeeService 클래스와 연동하세요.
// TODO DTO <-> Entity 변환 Mapper를 적용하세요.
List<Coffee> coffees = coffeeService.findCoffees();
List<CoffeeResponseDto> coffeeResponseDtos = mapper.coffeesToCoffeeResponseDtos(coffees);
return new ResponseEntity<>(coffeeResponseDtos, HttpStatus.OK);
}
@DeleteMapping("/{coffee-id}")
public ResponseEntity deleteCoffee(@PathVariable("coffee-id") long coffeeId) {
// TODO CoffeeService 클래스와 연동하세요.
// TODO DTO <-> Entity 변환 Mapper를 적용하세요.
coffeeService.deleteCoffee(coffeeId);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
CoffeePatchDto
package com.codestates.coffee.dto;
import com.codestates.validator.NotSpace;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Pattern;
public class CoffeePatchDto {
private long coffeeId;
@NotSpace(message = "커피명(한글)은 공백이 아니어야 합니다.")
private String korName;
@Pattern(regexp = "^([A-Za-z])(\\s?[A-Za-z])*$", message = "커피명(영문)은 영문이어야 합니다. 예) Cafe Latte")
private String engName;
@Range(min= 100, max= 50000)
private Integer price;
public long getCoffeeId() {
return coffeeId;
}
public void setCoffeeId(long coffeeId) {
this.coffeeId = coffeeId;
}
public String getKorName() {
return korName;
}
public String getEngName() {
return engName;
}
public Integer getPrice() {
return price;
}
}
CoffeePostDto
package com.codestates.coffee.dto;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
public class CoffeePostDto {
@NotBlank
private String korName;
@NotBlank
@Pattern(regexp = "^([A-Za-z])(\\s?[A-Za-z])*$",
message = "커피명(영문)은 영문이어야 합니다(단어 사이 공백 한 칸 포함). 예) Cafe Latte")
private String engName;
@Range(min= 100, max= 50000)
private int price;
public String getKorName() {
return korName;
}
public void setKorName(String korName) {
this.korName = korName;
}
public String getEngName() {
return engName;
}
public void setEngName(String engName) {
this.engName = engName;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
CoffeeResponseDto
package com.codestates.coffee.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class CoffeeResponseDto {
private long coffeeId;
private String korName;
private String engName;
private Integer price;
}
Coffee
package com.codestates.coffee.entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Coffee {
private long coffeeId;
private String korName;
private String engName;
private int price;
}
CoffeeMapper
package com.codestates.coffee.mapper;
import com.codestates.coffee.dto.CoffeePatchDto;
import com.codestates.coffee.dto.CoffeePostDto;
import com.codestates.coffee.dto.CoffeeResponseDto;
import com.codestates.coffee.entity.Coffee;
import org.mapstruct.Mapper;
import java.util.List;
@Mapper(componentModel = "spring")
public interface CoffeeMapper {
Coffee CoffeePostDtoToCoffee(CoffeePostDto coffeePostDto);
Coffee CoffeePatchDtoToCoffee(CoffeePatchDto coffeePatchDto);
CoffeeResponseDto coffeeToCoffeeResponseDto(Coffee coffee);
List<CoffeeResponseDto> coffeesToCoffeeResponseDtos(List<Coffee> coffees);
}
CoffeeService
package com.codestates.coffee.service;
import com.codestates.coffee.entity.Coffee;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class CoffeeService {
public Coffee createCoffee(Coffee coffee) {
return coffee;
}
public Coffee updateCoffee(Coffee coffee) {
return coffee;
}
public Coffee findCoffee(long coffeeId) {
return new Coffee(coffeeId, "아메리카노", "Americano", 2500);
}
public List<Coffee> findCoffees() {
return new ArrayList<>(List.of(new Coffee(1L, "아메리카노", "Americano", 2500),
new Coffee(2L, "캐러멜 라떼", "Caramel Latte", 5000)
));
}
public void deleteCoffee(long coffeeId) {}
}
[Spring MVC] 예외 처리
이번 학습에서는 Spring MVC 기반의 웹 애플리케이션에서 발생하는 예외를 어떻게 처리하는지 학습합니다.
여러분들이 샘플 애플리케이션을 구현해 보면서 만날 수 있는 예외는 크게 다음과 같습니다.
- 클라이언트 요청 데이터에 대한 유효성 검증(Validation)에서 발생하는 예외
- 서비스 계층의 비즈니스 로직에서 던져지는 의도된 예외
- 웹 애플리케이션 실행 중에 발생하는 예외(RuntimeException)
이 세 가지 유형의 예외를 효과적으로 처리할 수 있는 일반적인 방법을 이번 유닛에서 배워보도록 하겠습니다.
[Spring MVC] 예외 처리 학습을 위한 사전 준비 사항
이번 유닛의 학습을 원활하게 진행하기 위해 지금까지 여러분들이 구현해 본 Controller 및 Service 클래스들이 포함된 템플릿 프로젝트를 사용하도록 하겠습니다.
- 예외 처리 학습용 템플릿 프로젝트 복제
- 아래 github 링크에서 실습용 repository를 clone합니다.
- IntelliJ IDE로 clone 받은 local repository 디렉토리의 프로젝트를 Open합니다.
- 학습을 진행하며 학습 내용에 따라 예제 코드를 타이핑해봅니다.
API 계층 및 서비스 계층 유닛에서 학습한 샘플 애플리케이션의 Controller나 Service 클래스 구현이 아직 어렵게 느껴지는 분들은 예외 처리에 대한 학습을 진행하기 전에 이 전 유닛의 코드들을 한번 더 따라서 타이핑해보고 난 후에 예외 처리에 대한 학습을 진행하길 권장합니다.
[Spring MVC] 예외 처리 학습 참고용 레퍼런스 코드
이번 유닛에서 학습한 예제 코드는 아래 github에서 확인할 수 있습니다.
챕터에서 사용한 예제 코드는 챕터에 있는 코드들을 직접 타이핑해본 후, 학습 내용을 조금 더 구체적으로 이해하기 위한 용도로만 활용해 주세요.
- 예외 처리 유닛에 사용한 예제 코드
복사/붙여넣기 해서 돌아가는 코드는 의미가 없습니다.
꼭 여러분 스스로 타이핑해본 후, 여러분의 지식으로 만들길 바랍니다.
학습 목표
- API 계층과 서비스 계층에서 발생하는 예외를 처리할 수 있다.
- 예외 발생 시, 클라이언트에게 예외 메시지를 전달할 수 있다.
Chapter - Spring MVC에서의 예외 처리
이번 챕터에서는 이 전 챕터까지 작성한 샘플 프로젝트 코드에 예외 처리를 적용해 보도록 하겠습니다.
Spring MVC에서는 애플리케이션에서 발생하는 예외를 효율적으로 처리할 수 있는 몇 가지 방법을 제공합니다.
지난 챕터까지 작성한 코드에는 애플리케이션에서 발생하는 예외를 처리하는 프로세스가 전혀 적용되지 않았기 때문에 애플리케이션에 어떤 예외가 발생했는지 클라이언트 쪽에서 구체적으로 알 수 있는 방법이 없었습니다.
Spring MVC 기반의 애플리케이션에서 예외를 처리하는 적절한 방법은 무엇인지, 해당 예외에 대한 적절한 메시지를 클라이언트 쪽에 어떻게 알려줄 수 있는지 이번 시간에 자세히 살펴보도록 하겠습니다.
학습 목표
- @ExceptionHandler 애너테이션을 사용해서 예외를 처리할 수 있다.
- @RestControllerAdvice 애너테이션을 사용해서 예외를 처리할 수 있다.
- 예외 발생 시, 클라이언트 쪽에 적절한 예외 메시지를 제공해 줄 수 있다.
[기본] @ExceptionHandler를 이용한 예외 처리
[DTO 유효성 검증(Validation)] 챕터에서 클라이언트 요청 데이터의 유효성 검증에 실패할 경우 [그림 3-27]과 같은 응답 메시지를 확인할 수 있었습니다.
[그림 3-27] 회원 등록 시, 유효성 검증에 실패할 경우의 응답 메시지 예
[그림 3-27]과 같은 Response Body의 내용만으로는 요청 데이터 중에서 어떤 항목이 유효성 검증에 실패했는지 알 수가 없습니다.
클라이언트 쪽에서 에러메시지를 조금 더 구체적으로 친절하게 알 수 있도록 바꾸는 작업이 필요한 시점입니다.
@ExceptionHandler를 이용한 Controller 레벨에서의 예외 처리
[그림 3-27]에서 클라이언트가 전달받는 Response Body는 애플리케이션에서 예외(Exception)가 발생했을 때, 내부적으로 Spring에서 전송해 주는 에러 응답 메시지 중 하나입니다.
Spring이 처리하는 에러 응답 메시지를 우리가 직접 처리하도록 코드를 수정해 봅시다.
Spring에서의 예외는 애플리케이션에 문제가 발생할 경우, 이 문제를 알려서 처리하는 것뿐만 아니라 유효성 검증에 실패했을 때와 같이 이 실패를 하나의 예외로 간주하여 이 예외를 던져서(throw) 예외 처리를 유도합니다.
✔ MemberController에 @ExceptionHandler 적용
package com.springboot.member.controller.controller_v6;
import com.springboot.member.dto.MemberPostDto;
import com.springboot.member.entity.Member;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/v6/members")
@Validated
@Slf4j
public class MemberControllerV6 {
...
...
@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
Member member = mapper.memberPostDtoToMember(memberDto);
Member response = memberService.createMember(member);
return new ResponseEntity<>(mapper.memberToMemberResponseDto(response),
HttpStatus.CREATED);
}
...
...
@ExceptionHandler
public ResponseEntity handleException(MethodArgumentNotValidException e) {
// (1)
final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
// (2)
return new ResponseEntity<>(fieldErrors, HttpStatus.BAD_REQUEST);
}
}
[코드 3-55] @ExceptionHandler를 사용한 MemberPostDto 유효성 검증 실패 처리
[코드 3-55]는 MemberController(v6)에 @ExceptionHandler 애너테이션을 이용해서 예외를 처리하도록 handleException() 메서드를 추가했습니다.
예를 들어 회원 등록 과정에서 Request Body의 유효성 검증에 실패했을 때 예외 처리 과정을 살펴봅시다.
- 클라이언트 쪽에서 회원 등록을 위해 MemberController의 postMember() 핸들러 메서드에 요청을 전송합니다.
- RequestBody에 유효하지 않은 요청 데이터가 포함되어 있어 유효성 검증에 실패하고, MethodArgumentNotValidException이 발생합니다.
- MemberController에는 @ExceptionHandler 애너테이션이 추가된 예외 처리 메서드인 handleException()이 있기 때문에 유효성 검증 과정에서 내부적으로 던져진 MethodArgumentNotValidException을 handleException() 메서드가 전달받습니다.
- (1)과 같이 MethodArgumentNotValidException 객체에서 getBindingResult().getFieldErrors()를 통해 발생한 에러 정보를 확인할 수 있습니다.
- (1)에서 얻은 에러 정보를 (2)에서 ResponseEntity를 통해 Response Body로 전달합니다.
이제 Postman으로 회원 등록 요청을 전송해 보겠습니다.
[그림 3-28] Postman에서 회원 등록 요청을 전송한 모습
[그림 3-28]은 회원 등록 정보 중에서 유효하지 않은 이메일 주소를 포함해서 요청을 전송한 결과입니다.
Response Body를 보면 이 전과는 조금 다른 응답 메시지를 전달받은 것을 알 수 있습니다.
[
{
"codes": [
"Email.memberPostDto.email",
"Email.email",
"Email.java.lang.String",
"Email"
],
"arguments": [
{
"codes": [
"memberPostDto.email",
"email"
],
"arguments": null,
"defaultMessage": "email",
"code": "email"
},
[],
{
"arguments": null,
"defaultMessage": ".*",
"codes": [
".*"
]
}
],
"defaultMessage": "올바른 형식의 이메일 주소여야 합니다",
"objectName": "memberPostDto",
"field": "email",
"rejectedValue": "hgd@",
"bindingFailure": false,
"code": "Email"
}
]
[코드 3-56] 유효하지 않은 이메일 주소를 포함한 요청 전송에 대한 응답 메시지
[코드 3-56]은 [그림 3-28]의 요청 결과에 대한 전체 Response Body입니다.
MemberController의 handleException() 메서드에서 유효성 검사 실패에 대한 에러 메시지를 구체적으로 전송해 주기 때문에 클라이언트 입장에서는 이제 어느 곳에 문제가 있는지를 구체적으로 알 수 있게 되었습니다.
그런데 클라이언트 입장에서는 [코드 3-56]에서 의미를 알 수 없는 정보를 전부 포함한 Response Body 전체 정보를 굳이 다 전달받을 필요는 없어 보입니다.
요청 전송 시, Request Body의 JSON 프로퍼티 중에서 문제가 된 프로퍼티는 무엇인지와 에러 메시지 정도만 전달받아도 충분해 보입니다.
코드를 어떻게 수정하면 좋을까요?
[코드 3-56]의 에러 정보를 기반으로 한 Error Response 클래스를 만들어서 필요한 정보만 담은 후에 클라이언트 쪽에 전달해 주면 됩니다.
✔ ErrorResponse 클래스 적용
package com.springboot.response.v1
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
@Getter
@AllArgsConstructor
public class ErrorResponse {
// (1)
private List<FieldError> fieldErrors;
@Getter
@AllArgsConstructor
public static class FieldError {
private String field;
private Object rejectedValue;
private String reason;
}
}
[코드 3-57] Error 정보만 담아서 응답으로 전송하기 위한 ErrorResponse 클래스(V1)
[코드 3-57]은 DTO 클래스의 유효성 검증 실패 시, 실패한 필드(멤버 변수)에 대한 Error 정보만 담아서 응답으로 전송하기 위한 ErrorResponse 클래스(V1)입니다.
[그림 3-28]의 Response Body를 보면 JSON 응답 객체가 배열([ ])인 것을 알 수 있습니다.
배열인 이유를 생각해 보면 여러분들이 DTO 클래스에서 검증해야 되는 멤버 변수에서 유효성 검증에 실패하는 멤버 변수들이 하나 이상이 될 수 있기 때문에 유효성 검증 실패 에러 역시 하나 이상이 될 수 있다는 의미입니다.
그렇기 때문에 (1)과 같이 한 개 이상의 유효성 검증에 실패한 필드의 에러 정보를 담기 위해서 List 객체를 이용하며, 이 한 개의 필드 에러 정보는 FieldError라는 별도의 static class를 ErrorResponse 클래스의 멤버 클래스로 정의했습니다.
[코드 3-57]에서 FieldError 클래스는 ErrorResponse 클래스 내부에 정의되어 있다고 해서 내부(Inner) 클래스라고 부르기보다는 ErrorResponse 클래스의 static 멤버 클래스라고 부르는 것이 적절합니다.
클래스가 멤버 변수와 멤버 메서드를 포함하듯이 static 멤버 클래스를 포함할 수 있다고 생각하면 되겠습니다.
그리고 ErrorResponse는 에러 정보만 담는 클래스이기 때문에 필드의 에러 정보를 담는 FieldError 클래스 역시 에러라는 공통의 관심사를 가지고 있으므로 ErrorResponse의 멤버로 표현하는 것이 적절하다고 생각합니다.
✔ ErrorResponse를 사용하도록 MemberController의 handleException() 메서드 수정
package com.springboot.member.controller.controller_v7;
import com.springboot.member.dto.MemberPostDto;
import com.springboot.member.entity.Member;
import com.springboot.response.v1.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/v7/members")
@Validated
@Slf4j
public class MemberControllerV7 {
...
...
@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
Member member = mapper.memberPostDtoToMember(memberDto);
Member response = memberService.createMember(member);
return new ResponseEntity<>(mapper.memberToMemberResponseDto(response),
HttpStatus.CREATED);
}
...
...
@ExceptionHandler
public ResponseEntity handleException(MethodArgumentNotValidException e) {
// (1)
final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
// (2)
List<ErrorResponse.FieldError> errors =
fieldErrors.stream()
.map(error -> new ErrorResponse.FieldError(
error.getField(),
error.getRejectedValue(),
error.getDefaultMessage()))
.collect(Collectors.toList());
return new ResponseEntity<>(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);
}
}
[코드 3-58] ErrorResponse 객체를 응답으로 전송
[코드 3-55]에서는 (1)의 List<FieldError>를 통째로 ResponseEntity 클래스에 실어서 전달했지만 이번에는 (2)와 같이 필요한 정보들만 선택적으로 골라서 ErrorResponse.FieldError 클래스에 담아서 List로 변환 후, List<ErrorResponse.FieldError>를 ResponseEntity 클래스에 실어서 전달하고 있습니다.
이제 애플리케이션을 실행하고 클라이언트 쪽에서 다시 MemberController의 postMember() 핸들러 메서드에 요청을 전송해 보겠습니다.
[그림 3-29] ErrorResponse 적용 후, Postman에서 회원 등록 요청을 전송한 모습
이번에는 회원 등록 정보 중에서 유효하지 않은 이메일 주소와 유효하지 않은 이름을 포함해서 요청을 전송한 결과입니다.
[그림 3-29]에서 보시다시피 유효성 검증에 실패한 필드(멤버 변수)가 두 개이기 때문에 에러 정보 역시 두 개이며, 필요한 정보만 표시해주고 있는 것을 볼 수 있습니다.
@ExceptionHandler의 단점
@ExceptionHandler 애너테이션과 ErrorResponse 클래스를 이용해서 Request Body에 대한 유효성 검증 실패 시 필요한 에러 정보만 담아서 클라이언트에게 응답으로 전송할 수 있게 되었습니다.
그런데, @ExceptionHandler 애너테이션으로 에러 처리를 하게 되면 다음과 같은 문제점이 발생할 수 있습니다.
- 각각의 Controller 클래스에서 @ExceptionHandler 애너테이션을 사용하여 Request Body에 대한 유효성 검증 실패에 대한 에러 처리를 해야 되므로 각 Controller 클래스마다 코드 중복이 발생합니다.
- Controller에서 처리해야 되는 예외(Exception)가 유효성 검증 실패에 대한 예외(MethodArgumentNotValidException)만 있는 것이 아니기 때문에 하나의 Controller 클래스 내에서 @ExceptionHandler를 추가한 에러 처리 핸들러 메서드가 늘어납니다.
- 대표적인 예가 바로 코드 3-59와 같이 patchMember() 핸들러 메서드의 URI에 유효하지 않은 변수 값을 전송할 경우입니다.
package com.springboot.member.controller.controller_v8;
import com.springboot.member.dto.MemberPostDto;
import com.springboot.member.entity.Member;
import com.springboot.response.v1.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/v8/members")
@Validated
@Slf4j
public class MemberControllerV8 {
...
...
@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
Member member = mapper.memberPostDtoToMember(memberDto);
Member response = memberService.createMember(member);
return new ResponseEntity<>(mapper.memberToMemberResponseDto(response),
HttpStatus.CREATED);
}
@PatchMapping("/{member-id}")
public ResponseEntity patchMember(
@PathVariable("member-id") @Positive long memberId,
@Valid @RequestBody MemberPatchDto memberPatchDto) {
memberPatchDto.setMemberId(memberId);
Member response =
memberService.updateMember(mapper.memberPatchDtoToMember(memberPatchDto));
return new ResponseEntity<>(mapper.memberToMemberResponseDto(response),
HttpStatus.OK);
}
...
...
@ExceptionHandler
public ResponseEntity handleException(MethodArgumentNotValidException e) {
// (1)
final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
// (2)
List<ErrorResponse.FieldError> errors =
fieldErrors.stream()
.map(error -> new ErrorResponse.FieldError(
error.getField(),
error.getRejectedValue(),
error.getDefaultMessage()))
.collect(Collectors.toList());
return new ResponseEntity<>(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);
}
@ExceptionHandler
public ResponseEntity handleException(ConstraintViolationException e) {
/**
* - ConstraintViolationException 클래스는 getBindingResult().getFieldErrors()
* 와 같이 에러 정보를 얻을 수 없다.
* - MethodArgumentNotValidException과 다르게 또 다른 방식으로 처리가 필요.
*/
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
}
[코드 3-59] ConstraintViolationException을 처리하기 위한 @ExceptionHandler 추가된 코드
핵심 포인트
- Controller 클래스 레벨에서 @ExceptionHandler 애너테이션을 사용하면 해당 Controller에서 발생하는 예외를 처리할 수 있다.
- 필요한 Error 정보만 담을 수 있는 Error 전용 Response 객체를 사용하면 클라이언트에게 조금 더 친절한 에러 정보를 제공할 수 있다.
- @ExceptionHandler 애너테이션 방식은 Controller마다 동일하게 발생하는 예외 처리에 대한 중복 코드가 발생할 수 있다.
- @ExceptionHandler 애너테이션 방식은 다양한 유형의 예외를 처리하기에는 적절하지 않은 방식이다.
심화 학습
- @ExceptionHandler 애너테이션에 대해서 더 알아보고 싶다면 아래 링크를 참고하세요.
- MethodArgumentNotValidException에서 에러 정보를 담고 있는 BindingResult 클래스에 대해서 더 알아보고 싶다면 아래 링크를 참고하세요.
- ConstraintViolationException에서 에러 정보를 담고 있는 ConstraintViolation 인터페이스와 구현 클래스에 대해서 더 알아보고 싶다면 아래 링크를 참고하세요.
[기본] @RestControllerAdvice를 이용한 예외처리
이번 챕터에서는 @ExceptionHandler를 사용했을 때 예외 처리를 위한 코드에 중복이 발생하는 문제를 개선해 보도록 하겠습니다.
@RestControllerAdvice를 사용한 예외 처리 공통화
특정 클래스에 @RestControllerAdvice 애너테이션을 추가하면 여러 개의 Controller 클래스에서 @ExceptionHandler, @InitBinder 또는 @ModelAttribute가 추가된 메서드를 공유해서 사용할 수 있습니다.
이 말의 의미를 예외 처리 관점에서 설명하자면, @RestControllerAdvice 애너테이션을 추가한 클래스를 이용하면 예외 처리를 공통화할 수 있다는 것입니다.
이 전 챕터에서 @ExceptionHandler 애너테이션을 이용해 각각의 Controller 클래스에서 예외를 처리하던 방식을 @RestControllerAdvice 애너테이션을 이용하는 방식으로 바꿔봅시다.
@InitBinder와 @ModelAttribute 애너테이션
@InitBinder와 @ModelAttribute 애너테이션은 JSP, Thymeleaf 같은 서버 사이드 렌더링(SSR, Server Side Rendering) 방식에서 주로 사용되는 방식입니다.
우리 코스에서는 클라이언트 사이드 렌더링(CSR, Client Side Rendering) 방식인 REST API 기반 애플리케이션에 대해서만 학습하므로 @InitBinder와 @ModelAttribute 애너테이션에 대한 설명은 생략하도록 하겠습니다.
궁금한 분들이 있다면 Spring 공식 문서를 참고하기 바랍니다.
✔ MemberController 클래스에서 @ExceptionHandler 로직 제거
이번 챕터에서는 각 Controller 클래스에서 발생하는 예외를 GlobalExceptionAdvice 클래스에서 공통으로 처리할 것이므로 이 전 챕터에서 MemberController에 구현된 @ExceptionHandler가 추가된 메서드들은 모두 제거합니다.
✔ ExceptionAdvice 클래스 정의
먼저 Controller 클래스에서 발생하는 예외들을 공통으로 처리할 ExceptionAdvice 클래스를 코드 3-60과 같이 정의합니다.
package com.springboot.advice;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionAdvice {
}
[코드 3-60] Controller 클래스의 예외를 처리할 GlobalExceptionAdvice 클래스 정의
예외를 처리할 ExceptionAdvice 클래스에 @RestControllerAdvice 애너테이션을 추가하면 이 클래스는 이제 Controller 클래스에서 발생하는 예외를 도맡아서 처리하게 됩니다.
✔ Exception 핸들러 메서드 구현
이제 GlobalExceptionAdvice 클래스에서 처리할 Exception 핸들러 메서드를 구현해 주면 됩니다.
package com.springboot.advice;
import com.springboot.response.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.stream.Collectors;
@RestControllerAdvice
public class GlobalExceptionAdvice {
// (1)
@ExceptionHandler
public ResponseEntity handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
List<ErrorResponse.FieldError> errors =
fieldErrors.stream()
.map(error -> new ErrorResponse.FieldError(
error.getField(),
error.getRejectedValue(),
error.getDefaultMessage()))
.collect(Collectors.toList());
return new ResponseEntity<>(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);
}
// (2)
@ExceptionHandler
public ResponseEntity handleConstraintViolationException(
ConstraintViolationException e) {
// TODO should implement for validation
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
}
[코드 3-61] Exception 핸들러 메서드 추가 및 구현
코드 3-61에 추가된 (1), (2)의 코드는 이 전 챕터에서 MemberController에 추가했던 Exception 핸들러 메서드의 로직을 그대로 추가했습니다.
(2)의 ConstraintViolationException을 처리하는 로직은 잠시 뒤에 구현을 하도록 하고, 우선 이 상태에서 클라이언트 쪽에서 요청을 전송해 보도록 하겠습니다.
[그림 3-30] @RestControllerAdvice 적용 후, MemberController의 postMember()에 유효하지 않은 데이터 전송
[그림 3-30]은 Postman에서 MemberController의 postMember() 쪽으로 유효하지 않은 요청 데이터 전송 시, GlobalExceptionAdvice 클래스의 handleMethodArgumentNotValidException() 메서드가 에러 응답 데이터를 클라이언트에게 전송하는 화면입니다.
[그림 3-31] @RestControllerAdvice 적용 후, CoffeController의 postCoffee()에 유효하지 않은 데이터 전송
[그림 3-31]은 Postman에서 CoffeeController의 postCoffee() 쪽으로 유효하지 않은 요청 데이터 전송 시, 역시 GlobalExceptionAdvice 클래스의 handleMethodArgumentNotValidException() 메서드가 에러 응답 데이터를 클라이언트에게 전송하는 화면입니다.
이처럼 @RestControllerAdvice 애너테이션을 이용해서 예외 처리를 공통화하면 각 Controller마다 추가되는 @ExceptionHandler 로직에 대한 중복 코드를 제거하고, Controller의 코드를 단순화할 수 있습니다.
✔ ErrorResponse 수정
이제 GlobalExceptionAdvice를 통해 Controller 클래스에서 발생하는 RequestBody의 유효성 검증에 대한 에러는 유연한 처리가 가능해졌습니다.
그런데 코드 3-61의 (2)에서 URI 변수로 넘어오는 값의 유효성 검증에 대한 에러(ConstraintViolationException) 처리는 아직 구현되지 않았습니다.
ConstraintViolationException에 대한 처리도 할 수 있도록 해봅시다.
이 부분을 처리하기 전에 먼저 ErrorResponse 클래스가 ConstraintViolationException에 대한 Error Response를 생성할 수 있도록 ErrorResponse 클래스를 수정해 보도록 하겠습니다.
package com.springboot.response
import lombok.Getter;
import org.springframework.validation.BindingResult;
import javax.validation.ConstraintViolation;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Getter
public class ErrorResponse {
private List<FieldError> fieldErrors; // (1)
private List<ConstraintViolationError> violationErrors; // (2)
// (3)
private ErrorResponse(List<FieldError> fieldErrors, List<ConstraintViolationError> violationErrors) {
this.fieldErrors = fieldErrors;
this.violationErrors = violationErrors;
}
// (4) BindingResult에 대한 ErrorResponse 객체 생성
public static ErrorResponse of(BindingResult bindingResult) {
return new ErrorResponse(FieldError.of(bindingResult), null);
}
// (5) Set<ConstraintViolation<?>> 객체에 대한 ErrorResponse 객체 생성
public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
return new ErrorResponse(null, ConstraintViolationError.of(violations));
}
// (6) Field Error 가공
@Getter
public static class FieldError {
private String field;
private Object rejectedValue;
private String reason;
private FieldError(String field, Object rejectedValue, String reason) {
this.field = field;
this.rejectedValue = rejectedValue;
this.reason = reason;
}
public static List<FieldError> of(BindingResult bindingResult) {
final List<org.springframework.validation.FieldError> fieldErrors =
bindingResult.getFieldErrors();
return fieldErrors.stream()
.map(error -> new FieldError(
error.getField(),
error.getRejectedValue() == null ?
"" : error.getRejectedValue().toString(),
error.getDefaultMessage()))
.collect(Collectors.toList());
}
}
// (7) ConstraintViolation Error 가공
@Getter
public static class ConstraintViolationError {
private String propertyPath;
private Object rejectedValue;
private String reason;
private ConstraintViolationError(String propertyPath, Object rejectedValue,
String reason) {
this.propertyPath = propertyPath;
this.rejectedValue = rejectedValue;
this.reason = reason;
}
public static List<ConstraintViolationError> of(
Set<ConstraintViolation<?>> constraintViolations) {
return constraintViolations.stream()
.map(constraintViolation -> new ConstraintViolationError(
constraintViolation.getPropertyPath().toString(),
constraintViolation.getInvalidValue().toString(),
constraintViolation.getMessage()
)).collect(Collectors.toList());
}
}
}
[코드 3-62] ConstraintViolationException에 대한 Error Response 생성 기능이 포함된 ErrorResponse 클래스(V2)
코드 3-62는 ErrorResponse 클래스(V2)가 ConstraintViolationException에 대한 Error Response까지 생성 가능하도록 수정된 코드입니다.
이 전 챕터에서 사용하던 ErrorResponse(V1) 보다 꽤나 복잡해서 이해하기 어려울지도 모르지만 번호 순서대로 천천히 설명하도록 하겠습니다.
수정된 ErrorResponse는 총 두 개의 예외 유형을 처리해서 Error Response에 포함할 수 있습니다.
첫 번째가 DTO 클래스의 유효성 검증에서 발생하는 MethodArgumentNotValidException에 대한 Error Response이고 두 번째는 URI의 변수 값 검증에서 발생하는 ConstraintViolationException에 대한 Error Response입니다.
- (1)은 MethodArgumentNotValidException으로부터 발생하는 에러 정보를 담는 멤버 변수입니다. 즉, DTO 멤버 변수 필드의 유효성 검증 실패로 발생한 에러 정보를 담는 멤버 변수입니다.
- (2)는 ConstraintViolationException으로부터 발생하는 에러 정보를 담는 멤버 변수입니다. 즉, URI 변수 값의 유효성 검증에 실패로 발생한 에러 정보를 담는 멤버 변수입니다.
- (3)은 ErrorResponse 클래스의 생성자인데 특이하게도 생성자 앞에 private 접근 제한자(Access Modifier)를 지정했습니다.
이렇게 함으로써 ErrorResponse 클래스는 여러분들이 이미 알고 있는 new ErrorResponse(…,…) 방식으로 ErrorResponse 객체를 생성할 수 없습니다.
대신에 (4)와 (5)처럼 of() 메서드를 이용해서 ErrorResponse의 객체를 생성할 수 있습니다.
이렇게 코드를 구성한 이유는 ErrorResponse의 객체를 생성함과 동시에 ErrorResponse의 역할을 명확하게 해 줍니다.
- (4)는 MethodArgumentNotValidException에 대한 ErrorResponse 객체를 생성해 줍니다. MethodArgumentNotValidException에서 에러 정보를 얻기 위해 필요한 것이 바로 BindingResult 객체이므로 이 of() 메서드를 호출하는 쪽에서 BindingResult 객체를 파라미터로 넘겨주면 됩니다.
그런데 이 BindingResult 객체를 가지고 에러 정보를 추출하고 가공하는 일은 ErrorResponse 클래스의 static 멤버 클래스인 FieldError 클래스에게 위임하고 있습니다.
- (5)는 ConstraintViolationException에 대한 ErrorResponse 객체를 생성해 줍니다. ConstraintViolationException에서 에러 정보를 얻기 위해 필요한 것이 바로 Set<ConstraintViolation<?>> 객체이므로 이 of() 메서드를 호출하는 쪽에서 Set<ConstraintViolation<?>> 객체를 파라미터로 넘겨주면 됩니다.
Set<ConstraintViolation<?>> 객체를 가지고 에러 정보를 추출하고 가공하는 일은 ErrorResponse 클래스의 static 멤버 클래스인 ConstraintViolationError 클래스에게 위임하고 있습니다.
(4)와 (5)를 통해서 ErrorResponse 객체에 에러 정보를 담는 역할이 명확하게 분리됩니다.
- (6)에서는 필드(DTO 클래스의 멤버 변수)의 유효성 검증에서 발생하는 에러 정보를 생성합니다.
- (7)에서는 URI 변수 값에 대한 에러 정보를 생성합니다.
기능이 늘어남에 따라 ErrorResponse 클래스의 구현 복잡도가 늘어나긴 했지만 에러 유형에 따른 에러 정보 생성 역할을 분리함으로써 ErrorResponse를 사용하는 입장에서는 한층 더 사용하기 편리해졌다는 사실을 기억하기 바랍니다.
of() 메서드
of() 메서드는 Java 8의 API에서도 흔히 볼 수 있는 네이밍 컨벤션(Naming Convention)입니다.
주로 객체 생성 시 어떤 값들의(of~) 객체를 생성한다는 의미에서 of() 메서드를 사용한다는 점을 기억하면 좋을 것 같습니다.
✔ Exception 핸들러 메서드 수정
이제 수정된 ErrorResponse 클래스의 메서드를 사용하도록 GlobalExceptionAdvice 클래스를 수정합시다.
package com.springboot.advice;
import com.springboot.response.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
final ErrorResponse response = ErrorResponse.of(e.getBindingResult());
return response;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleConstraintViolationException(
ConstraintViolationException e) {
final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());
return response;
}
}
[코드 3-63] ConstraintViolationException에 대한 Error Response 생성 기능이 포함된 ErrorResponse 클래스(V2)
코드 3-63은 수정된 ErrorResponse 클래스를 사용하도록 개선된 GlobalExceptionAdvice(V2) 클래스의 코드입니다.
이전 버전인 코드 3-61과 비교했을 때, Error Response 정보를 만드는 역할을 ErrorResponse 클래스가 대신해주기 때문에 코드 자체가 무척 간결해진 것을 볼 수 있습니다.
또 하나 중요한 변경사항은 코드 3-61에서 ErrorResponse 객체를 ResponseEntity로 래핑해서 리턴한 반면 코드 3-63에서는 ResponseEntity가 사라지고 ErrorResponse 객체를 바로 리턴하고 있습니다.
그리고 @ResponseStatus 애너테이션을 이용해서 HTTP Status를 HTTP Response에 포함하고 있습니다.
@RestControllerAdvice vs @ControllerAdvice
Spring MVC 4.3 버전 이후부터 @RestControllerAdvice 애너테이션을 지원하는데, 둘 사이의 차이점을 한마디로 설명하자면 아래와 같습니다.
- @RestControllerAdvice = @ControllerAdvice + @ResponseBody
@RestControllerAdvice 애너테이션은 @ControllerAdvice의 기능을 포함하고 있으며, @ResponseBody의 기능 역시 포함하고 있기 때문에 JSON 형식의 데이터를 Response Body로 전송하기 위해서 ResponseEntity로 데이터를 래핑 할 필요가 없다는 사실을 기억해두길 바랍니다.
핵심 포인트
- @RestControllerAdvice 애너테이션을 추가한 클래스를 이용하면 예외 처리를 공통화할 수 있다.
- @RestControllerAdvice 애너테이션을 사용하면 JSON 형식의 데이터를 Response Body로 전송하기 위해 ResponseEntity로 래핑 할 필요가 없다.
- @ResponseStatus 애너테이션으로 HTTP Status를 대신 표현할 수 있다.
심화 학습
- Controller Advice에 대해서 더 알아보고 싶다면 아래 링크를 참고하세요.
- https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-controller-advice
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/RestControllerAdvice.html
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/ControllerAdvice.html