Spring๐ŸŒธ

Spring MVC - ์˜ˆ์™ธ ์ฒ˜๋ฆฌ(1)

Jeein0313 2023. 4. 17. 23:32

์ƒ˜ํ”Œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌํ˜„ํ•ด๋ณด๋ฉด์„œ ๋งŒ๋‚  ์ˆ˜ ์žˆ๋Š” ์˜ˆ์™ธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

 

  • ํด๋ผ์ด์–ธํŠธ ์š”์ฒญ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ(Validation)์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ
  • ์„œ๋น„์Šค ๊ณ„์ธต์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์—์„œ ๋˜์ ธ์ง€๋Š” ์˜๋„๋œ ์˜ˆ์™ธ
  • ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰ ์ค‘์— ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ(RuntimeException)

 

DTO ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์ธก์—์„œ ํด๋ผ์ด์–ธํŠธ์˜ ์š”์ฒญ ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์— ์‹คํŒจํ•  ๊ฒฝ์šฐ, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์‘๋‹ต ๋ฉ”์‹œ์ง€๋ฅผ ํ™•์ธ ๊ฐ€๋Šฅํ•˜๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์€ Response Body ๋‚ด์šฉ๋งŒ์œผ๋กœ๋Š” ์š”์ฒญ ๋ฐ์ดํ„ฐ ์ค‘์—์„œ ์–ด๋–ค ํ•ญ๋ชฉ์ด ์œ ํšจ์„ฑ ๊ฒ€์ฆ์— ์‹คํŒจํ–ˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์—†๋‹ค. 

ํด๋ผ์ด์–ธํŠธ ์ชฝ์—์„œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์กฐ๊ธˆ ๋” ๊ตฌ์ฒด์ ์œผ๋กœ ์นœ์ ˆํ•˜๊ฒŒ ์•Œ ์ˆ˜ ์žˆ๋„๋ก ๋ฐ”๊พธ๋Š” ์ž‘์—…์ด ํ•„์š”ํ•˜๋‹ค.

 

 

๐Ÿ’ก@ExceptionHandler๋ฅผ ์ด์šฉํ•œ Controller ๋ ˆ๋ฒจ์—์„œ์˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

 

ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ „๋‹ฌ๋ฐ›๋Š” Request Body๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ, ๋‚ด๋ถ€์ ์œผ๋กœ Spring์—์„œ ์ „์†กํ•ด์ฃผ๋Š” ์—๋Ÿฌ ์‘๋‹ต ๋ฉ”์‹œ์ง€ ์ค‘ ํ•˜๋‚˜. Spring์ด ์ฒ˜๋ฆฌํ•˜๋Š” ์—๋Ÿฌ ์‘๋‹ต ๋ฉ”์‹œ์ง€๋ฅผ ์šฐ๋ฆฌ๊ฐ€ ์ง์ ‘ ์ฒ˜๋ฆฌํ•˜๋„๋ก ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•ด๋ณด์ž.

 

Spring์—์„œ์˜ ์˜ˆ์™ธ๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ, ์ด๋ฅผ ์•Œ๋ ค์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์— ์‹คํŒจํ–ˆ์„ ๋•Œ์™€ ๊ฐ™์ด ์ด ์‹คํŒจ๋ฅผ ํ•˜๋‚˜์˜ ์˜ˆ์™ธ๋กœ ๊ฐ„์ฃผํ•˜์—ฌ ์ด ์˜ˆ์™ธ๋ฅผ ๋˜์ ธ์„œ(throw) ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ์œ ๋„ํ•จ.

 

package com.codestates.member.controller;

import com.codestates.member.dto.MemberPatchDto;
import com.codestates.member.dto.MemberPostDto;
import com.codestates.member.dto.MemberResponseDto;
import com.codestates.member.entity.Member;
import com.codestates.member.mapper.MemberMapper;
import com.codestates.member.service.MemberService;
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 javax.validation.constraints.Positive;
import java.util.List;


/**
 * - DI ์ ์šฉ
 * - Mapstruct Mapper ์ ์šฉ
 */
@RestController
@RequestMapping("/v6/members")
@Validated
@Slf4j
public class MemberControllerV6 {
    private final MemberService memberService;
    private final MemberMapper mapper;

    public MemberControllerV6(MemberService memberService, MemberMapper mapper) {
        this.memberService = memberService;
        this.mapper = mapper;
    }

    @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 handlerException(MethodArgumentNotValidException e){
        final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        return new ResponseEntity<>(fieldErrors, HttpStatus.BAD_REQUEST);
    }
}
  • ํด๋ผ์ด์–ธํŠธ ์ชฝ์—์„œ ํšŒ์› ๋“ฑ๋ก์„ ์œ„ํ•ด MemberController ์˜ postMember() ํ•ธ๋“ค๋Ÿฌ ๋ฉ”์„œ๋“œ์— ์š”์ฒญ์„ ์ „์†กํ•จ.
  • RequestBody ์— ์œ ํšจํ•˜์ง€ ์•Š์€ ์š”์ฒญ ๋ฐ์ดํ„ฐ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์–ด ์œ ํšจ์„ฑ ๊ฒ€์ฆ์— ์‹คํŒจํ•˜๊ณ , MethodArgumentNotValidException ์ด ๋ฐœ์ƒํ•จ.
  • MemberController ์—๋Š” @ExceptionHandler ์• ๋„ˆํ…Œ์ด์…˜์ด ์ถ”๊ฐ€๋œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ฉ”์„œ๋“œ์ธ handlerException() ์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๊ณผ์ •์—์„œ ๋‚ด๋ถ€์ ์œผ๋กœ ๋˜์ ธ์ง„ MethodArgumentNotValidException ์„ handleException() ๋ฉ”์„œ๋“œ๊ฐ€ ์ „๋‹ฌ๋ฐ›์Œ.
  • (1)๊ณผ ๊ฐ™์ด MethodArgumentNotValidException ๊ฐ์ฒด์—์„œ getBindingResult().getFieldErrors() ๋ฅผ ํ†ตํ•ด ๋ฐœ์ƒํ•œ ์—๋Ÿฌ ์ •๋ณด๋ฅผ ํ™•์ธ ๊ฐ€๋Šฅํ•จ.
  • (1)์—์„œ ์–ป์€ ์—๋Ÿฌ ์ •๋ณด๋ฅผ (2)์—์„œ ResponseEntity ๋ฅผ ํ†ตํ•ด Response Body๋กœ ์ „๋‹ฌํ•จ.

 

ํšŒ์› ๋“ฑ๋ก ์ •๋ณด์—์„œ ์œ ํšจํ•˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ ์ฃผ์†Œ๋ฅผ ํฌํ•จํ•ด ์š”์ฒญ์„ ์ „์†กํ•œ ๊ฒฐ๊ณผ๋กœ Response Body๋ฅผ ๋ณด๋ฉด ์ด์ „๊ณผ ๋‹ค๋ฅธ ์‘๋‹ต ๋ฉ”์‹œ์ง€๋ฅผ ์ „๋‹ฌ ๋ฐ›์€ ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Œ.

[
    {
        "codes": [
            "Email.memberPostDto.email",
            "Email.email",
            "Email.java.lang.String",
            "Email"
        ],
        "arguments": [
            {
                "codes": [
                    "memberPostDto.email",
                    "email"
                ],
                "arguments": null,
                "defaultMessage": "email",
                "code": "email"
            },
            [],
            {
                "defaultMessage": ".*",
                "codes": [
                    ".*"
                ],
                "arguments": null
            }
        ],
        "defaultMessage": "must be a well-formed email address",
        "objectName": "memberPostDto",
        "field": "email",
        "rejectedValue": "jeein@",
        "bindingFailure": false,
        "code": "Email"
    }
]

์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ์— ๋Œ€ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ์ฒด์ ์œผ๋กœ ์ „๋‹ฌํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ํด๋ผ์ด์–ธํŠธ ์ž…์žฅ์—์„œ๋Š” ์ด์ œ ์–ด๋А ๊ณณ์— ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š”์ง€๋ฅผ ๊ตฌ์ฒด์ ์œผ๋กœ์•Œ ์ˆ˜ ์žˆ๊ฒŒ ๋จ.

๊ทธ๋Ÿฐ๋ฐ ํด๋ผ์ด์–ธํŠธ ์ž…์žฅ์—์„œ๋Š” ์˜๋ฏธ๋ฅผ ์•Œ ์ˆ˜ ์—†๋Š” ์ •๋ณด๋ฅผ ์ „๋ถ€ ํฌํ•จํ•œ Response Body ์ „์ฒด ์ •๋ณด๋ฅผ ๊ตณ์ด ๋‹ค ์ „๋‹ฌ ๋ฐ›์„ ํ•„์š”๋Š” ์—†์–ด ๋ณด์ž„.

์š”์ฒญ ์ „์†ก์‹œ, Request Body์˜ JSON ์˜ ํ”„๋กœํผํ‹ฐ ์ค‘์—์„œ ๋ฌธ์ œ๊ฐ€ ๋œ ํ”„๋กœํผํ‹ฐ๋Š” ๋ฌด์—‡์ธ์ง€์™€ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ •๋„๋งŒ ์ „๋‹ฌ๋ฐ›์•„๋„ ์ถฉ๋ถ„!

์—๋Ÿฌ ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ Error Response ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด์„œ ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ๋‹ด์€ ํ›„์— ํด๋ผ์ด์–ธํŠธ ์ชฝ์— ์ „๋‹ฌํ•ด์ฃผ๋ฉด ๋จ.

 

 

๐Ÿ’กErrorResponse ํด๋ž˜์Šค ์ ์šฉ

package com.codestates.response.v1;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.validation.FieldError;

import java.util.List;


@Getter
@AllArgsConstructor
public class ErrorResponse {
    private List<FieldError> fieldErrors;

    @Getter
    @AllArgsConstructor
    public static class FieldError{
        private String field;
        private Object rejectedValue;
        private String reason;
    }
}

DTO ํด๋ž˜์Šค์˜ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์‹คํŒจ ์‹œ, ์‹คํŒจํ•œ ํ•„๋“œ(๋ฉค๋ฒ„ ๋ณ€์ˆ˜)์— ๋Œ€ํ•œ Error์ •๋ณด๋งŒ ๋‹ด์•„์„œ ์‘๋‹ต์œผ๋กœ ์ „์†กํ•˜๊ธฐ ์œ„ํ•œ ErrorResponse ํด๋ž˜์Šค.

Response Body๋ฅผ ๋ณด๋ฉด JSON ์‘๋‹ต ๊ฐ์ฒด๊ฐ€ ๋ฐฐ์—ด์ธ ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Œ.

๋ฐฐ์—ด์ธ ์ด์œ ๋ฅผ ์ƒ๊ฐํ•ด ๋ณด๋ฉด DTO ํด๋ž˜์Šค์—์„œ ๊ฒ€์ฆํ•ด์•ผ ๋˜๋Š” ๋ฉค๋ฒ„ ๋ณ€์ˆ˜์—์„œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์— ์‹คํŒจํ•˜๋Š” ๋ฉค๋ฒ„ ๋ณ€์ˆ˜๋“ค์ด ํ•˜๋‚˜ ์ด์ƒ์ด ๋  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์‹คํŒจ ์—๋Ÿฌ ์—ญ์‹œ ํ•˜๋‚˜ ์ด์ƒ์ด ๋  ์ˆ˜ ์žˆ๋‹ค๋Š” ์˜๋ฏธ.

ํ•œ ๊ฐœ์˜ ํ•„๋“œ ์—๋Ÿฌ ์ •๋ณด๋Š” FieldError ๋ผ๋Š” ๋ณ„๋„์˜ static class๋ฅผ ErrorResponse ํด๋ž˜์Šค์˜ ๋ฉค๋ฒ„ ํด๋ž˜์Šค๋กœ ์ •์˜.

 

package com.codestates.member.controller.controller_v7;

import com.codestates.member.dto.MemberPostDto;
import com.codestates.member.entity.Member;
import com.codestates.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);
    }
}

(2)์™€ ๊ฐ™์ด ํ•„์š”ํ•œ ์ •๋ณด๋“ค๋งŒ ์„ ํƒ์ ์œผ๋กœ ๊ณจ๋ผ์„œ ErrorResponse.FieldError ํด๋ž˜์Šค์— ๋‹ด์•„์„œ List๋กœ ๋ณ€ํ™˜ ํ›„, List<ErrorResponse.FieldError>๋ฅผ ResponseEntity ํด๋ž˜์Šค์— ์‹ค์–ด์„œ ์ „๋‹ฌ

 

๐Ÿ’ก@ExceptionHandler์˜ ๋‹จ์ 

 

@ExceptionHandler ์• ๋„ˆํ…Œ์ด์…˜๊ณผ ErrorResponse ํด๋ž˜์Šค๋ฅผ ์ด์šฉํ•ด์„œ Request Body์— ๋Œ€ํ•œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์‹คํŒจ ์‹œ ํ•„์š”ํ•œ ์—๋Ÿฌ ์ •๋ณด๋งŒ ๋‹ด์•„์„œ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์‘๋‹ต์œผ๋กœ ์ „์†กํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋จ.

 

ํ•˜์ง€๋งŒ, ๊ฐ๊ฐ์˜ Controller ํด๋ž˜์Šค์—์„œ @ExceptionHandler ์• ๋„ˆํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ Request Body์— ๋Œ€ํ•œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์‹คํŒจ์— ๋Œ€ํ•œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์•ผ ๋˜๋ฏ€๋กœ ๊ฐ Controller ํด๋ž˜์Šค๋งˆ๋‹ค ์ฝ”๋“œ ์ค‘๋ณต์ด ๋ฐœ์ƒํ•จ.

Controller์—์„œ ์ฒ˜๋ฆฌํ•ด์•ผ ๋˜๋Š” ์˜ˆ์™ธ(Exception)๊ฐ€ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์‹คํŒจ์— ๋Œ€ํ•œ ์˜ˆ์™ธ(MethodArgumentNotValidException)๋งŒ ์žˆ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ํ•˜๋‚˜์˜ Controller ํด๋ž˜์Šค ๋‚ด์—์„œ @ExceptionHandler๋ฅผ ์ถ”๊ฐ€ํ•œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ํ•ธ๋“ค๋Ÿฌ ๋ฉ”์„œ๋“œ๊ฐ€ ๋Š˜์–ด๋‚จ.

@ExceptionHandler
public ResponseEntity handleException(ConstraintViolationException e){
    return new ResponseEntity(HttpStatus.BAD_REQUEST);
}

patchMember() ํ•ธ๋“ค๋Ÿฌ ๋ฉ”์„œ๋“œ์˜ URI ๋ณ€์ˆ˜์ธ "/{member-id}" ์— 0์ด ๋„˜์–ด์˜ฌ ๊ฒฝ์šฐ, ConstraintViolationException์ด ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์— ConstraintViolationException ์„ ์ฒ˜๋ฆฌํ•  @ExceptionHandler๋ฅผ ์ถ”๊ฐ€ํ•œ ๋ฉ”์„œ๋“œ๋ฅผ ํ•˜๋‚˜ ๋” ์ถ”๊ฐ€ํ•จ.

์ด์ฒ˜๋Ÿผ @ExceptionHandler ์• ๋„ˆํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•œ ๋ฐฉ์‹์€ ์ฝ”๋“œ ์ค‘๋ณต๊ณผ ์œ ์—ฐํ•˜์ง€ ์•Š์€ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์ด๊ธฐ ๋•Œ๋ฌธ์— ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์— ๋Œ€ํ•œ ๊ฐœ์„ ์ด ํ•„์š”ํ•จ.

 

 

๐Ÿ’ก@RestControllerAdvice๋ฅผ ์‚ฌ์šฉํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๊ณตํ†ตํ™”

 

ํŠน์ • ํด๋ž˜์Šค์— @RestControllerAdvice ์• ๋„ˆํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์—ฌ๋Ÿฌ ๊ฐœ์˜ Controller ํด๋ž˜์Šค์—์„œ @ExceptionHandler, @InitBinder, ๋˜๋Š” @ModelAttribute ๊ฐ€ ์ถ”๊ฐ€๋œ ๋ฉ”์„œ๋“œ๋ฅผ ๊ณต์œ ํ•ด์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•จ.

 

cf. @InitBinder๋Š” ์š”์ฒญ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ Java Bean ๊ฐ์ฒด์— ๋ฐ”์ธ๋”ฉํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” WebDataBinder ์ธ์Šคํ„ด์Šค๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” ์ฃผ์„. 

@InitBinder, @ModelAttribute ์• ๋„ˆํ…Œ์ด์…˜์€ JSP, Thymeleaf ๊ฐ™์€ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง(SSR, Server Side Rendering) ๋ฐฉ์‹์—์„œ ์ฃผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ๋ฐฉ์‹.

 

์ด ๋ง์˜ ์˜๋ฏธ๋ฅผ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๊ด€์ ์—์„œ ๋ณด๋ฉด, @RestControllerAdvice ์• ๋„ˆํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•œ ํด๋ž˜์Šค๋ฅผ ์ด์šฉํ•˜๋ฉด ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ๊ณตํ†ตํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ.

 

๐Ÿ’กExceptionAdvice ํด๋ž˜์Šค ์ •์˜

 

Controller ํด๋ž˜์Šค์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋“ค์„ ๊ณตํ†ต์œผ๋กœ ์ฒ˜๋ฆฌํ•  ExceptionAdvice ํด๋ž˜์Šค๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ •์˜.

package com.codestates.advice;

import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionAdvice {
    
}

์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•  ExceptionAdvice ํด๋ž˜์Šค์— @RestControllerAdvice ์• ๋„ˆํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์ด ํด๋ž˜์Šค๋Š” ์ด์ œ Controller ํด๋ž˜์Šค์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋ฅผ ๋„๋งก์•„์„œ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ๋จ.

 

 

๐Ÿ’กException ํ•ธ๋“ค๋Ÿฌ ๋ฉ”์„œ๋“œ ๊ตฌํ˜„

package com.codestates.advice;

import com.codestates.response.v1.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)
    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){
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }
}

@RestControllerAdvice ์• ๋„ˆํ…Œ์ด์…˜์„ ์ด์šฉํ•ด์„œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ๊ณตํ†ตํ™”ํ•˜๋ฉด ๊ฐ Controller๋งˆ๋‹ค ์ถ”๊ฐ€๋˜๋Š” @ExceptionHandler ๋กœ์ง์— ๋Œ€ํ•œ ์ค‘๋ณต ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ , Controller์˜ ์ฝ”๋“œ๋ฅผ ๋‹จ์ˆœํ™”ํ•  ์ˆ˜ ์žˆ์Œ.

 

 

๐Ÿ’กErrorResponse ์ˆ˜์ •

 

GlobalExceptionAdvice๋ฅผ ํ†ตํ•ด Controller ํด๋ž˜์Šค์—์„œ ๋ฐœ์ƒํ•˜๋Š” RequestBody์˜ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์— ๋Œ€ํ•œ ์—๋Ÿฌ๋Š” ์œ ์—ฐํ•œ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ด์ง.

URI ๋ณ€์ˆ˜๋กœ ๋„˜์–ด์˜ค๋Š” ๊ฐ’์˜ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์— ๋Œ€ํ•œ ์—๋Ÿฌ(ConstraintViolationException)์ฒ˜๋ฆฌ๋Š” ์•„์ง ๊ตฌํ˜„๋˜์ง€ ์•Š์Œ.

ConstraintViolationException์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋„ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•จ. ์ด ๋ถ€๋ถ„์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์ „์— ๋จผ์ € ErrorResponse ํด๋ž˜์Šค๊ฐ€ ConstraintViolationException์— ๋Œ€ํ•œ Error Response๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋„๋ก ErrorResponse ํด๋ž˜์Šค๋ฅผ ์ˆ˜์ •.

package com.codestates.response.v1;

import lombok.Getter;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;

import javax.validation.ConstraintViolation;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;


@Getter
public class ErrorResponse {
    //(1)์œ ํšจ์„ฑ ๊ฒ€์ฆ์— ์‹คํŒจํ•œ ํ•„๋“œ์˜ ์—๋Ÿฌ ์ •๋ณด๋ฅผ ๋‹ด๋Š” ๋ฉค๋ฒ„ ๋ณ€์ˆ˜.
    private List<FieldError> fieldErrors;

    //(2)URI ๋ณ€์ˆ˜ ๊ฐ’์˜ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์— ์‹คํŒจ๋กœ ๋ฐœ์ƒํ•œ ์—๋Ÿฌ ์ •๋ณด๋ฅผ ๋‹ด๋Š” ๋ฉค๋ฒ„ ๋ณ€์ˆ˜
    private List<ConstraintViolationError> violationErrors;

    //(3)ErrorResponse ์ƒ์„ฑ์ž์— private ์ ‘๊ทผ ์ œํ•œ์ž๋ฅผ ์ง€์ •ํ•จ์œผ๋กœ์จ of() ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•ด์•ผ ํ•˜๋ฉฐ, ์ด๋ฅผ ํ†ตํ•ด ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•จ๊ณผ ๋™์‹œ์— ErrorResponse ์—ญํ• ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ํ•ด ์คŒ.
    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;

        public 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 ๊ฐ€๊ณต, URI ๋ณ€์ˆ˜ ๊ฐ’์— ๋Œ€ํ•œ ์—๋Ÿฌ ์ •๋ณด๋ฅผ ์ƒ์„ฑํ•จ.
    @Getter
    public static class ConstraintViolationError{
        private String propertyPath;
        private Object rejectedValue;
        private String reason;

        public 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());
        }
    }
}

 

๐Ÿ’กException ํ•ธ๋“ค๋Ÿฌ ๋ฉ”์„œ๋“œ ์ˆ˜์ •

 

package com.codestates.advice;

import com.codestates.response.v1.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.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.stream.Collectors;

@RestControllerAdvice
public class GlobalExceptionAdvice {
    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e){
        final ErrorResponse response=ErrorResponse.of(e);
        return response;
    }


    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleConstraintViolationException(ConstraintViolationException e){
        final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());
        return response;
    }
}

Error Response ์ •๋ณด๋ฅผ ๋งŒ๋“œ๋Š” ์—ญํ• ์„ ErrorResponse ํด๋ž˜์Šค๊ฐ€ ๋Œ€์‹ ํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ์ฝ”๋“œ ์ž์ฒด๊ฐ€ ๊ฐ„๊ฒฐํ•ด์ง.

๋˜ ํ•˜๋‚˜ ์ค‘์š”ํ•œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์€ ErrorResponse ๊ฐ์ฒด๋ฅผ ResponseEntity๋กœ ๋ž˜ํ•‘ํ•ด์„œ ๋ฆฌํ„ดํ•˜๋Š” ๋ฐ˜๋ฉด, ์œ„์˜ ์ฝ”๋“œ์—์„œ๋Š” ResponseEntity๊ฐ€ ์‚ฌ๋ผ์ง€๊ณ  ErrorResponse ๊ฐ์ฒด๋ฅผ ๋ฐ”๋กœ ๋ฆฌํ„ดํ•˜๊ณ  ์žˆ์Œ.

@ResponseStatus ์• ๋„ˆํ…Œ์ด์…˜์„ ์ด์šฉํ•ด์„œ HTTP Status๋ฅผ HTTP Response์— ํฌํ•จํ•˜๊ณ  ์žˆ์Œ.

 

 

๐Ÿ’ก@RestControllerAdvice vs @ControllerAdvice

 

Spring MVC 4.3 ๋ฒ„์ „ ์ดํ›„๋ถ€ํ„ฐ @RestControllerAdvice ์• ๋„ˆํ…Œ์ด์…˜์„ ์ง€์›ํ•˜๋Š”๋ฐ, ๋‘˜ ์‚ฌ์ด์˜ ์ฐจ์ด์ ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

@RestControllerAdvice = @ControllerAdvice + @RepsonseBody

@RestControllerAdvice ์• ๋„ˆํ…Œ์ด์…˜์€ @ControllerAdvice์˜ ๊ธฐ๋Šฅ์„ ํฌํ•จํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, @ResponseBody์˜ ๊ธฐ๋Šฅ ์—ญ์‹œ ํฌํ•จํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— JSON ํ˜•์‹์˜ ๋ฐ์ดํ„ฐ๋ฅผ Response Body๋กœ ์ „์†กํ•˜๊ธฐ ์œ„ํ•ด์„œ ResponseEntity๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ž˜ํ•‘ ํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค