<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>토리의 기록실</title>
    <link>https://tory4953.tistory.com/</link>
    <description>공부한 내용을 나만의 언어로 정리하는 블로그</description>
    <language>ko</language>
    <pubDate>Thu, 4 Jun 2026 05:30:42 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>토리8</managingEditor>
    <item>
      <title>레벨2 소프트 스킬 강의 후기 - 회복 탄력성 : 흔들려도 무너지지 않는 힘</title>
      <link>https://tory4953.tistory.com/4</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;자아존중감 - 나는 나에게 얼마나 관대했나?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서 자아존중감을 이렇게 정의했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;자신이 사랑받을 만한 가치가 있는 존재임을 인정하고 존중하는 마음&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정의를 듣고 나서 돌아봤다. 지금까지 우테코를 다니면서 미션이 끝난 후, 혹은 레벨이 끝난 후에 나 자신에게 &quot;잘했다&quot;고 말한 적이 단 한 번이라도 있었나?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;없었다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 다른 크루들과 비교하면서 내가 부족한 부분만 눈에 들어왔다. 그 부족함을 채워야 한다는 열망으로 스스로를 채찍질했다. 그게 성장 의지라고 생각했는데, 돌아보면 나 자신을 한 번도 인정해준 적 없이 몰아붙이기만 했던 것 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;감정을 무시하는 것이 습관이 되어 있었다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서 이런 말이 나왔다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;감정을 무시하고 밝게 지내는 것은 회복이 아닌 회피입니다. 감정을 인식하고 흘려보내는 것이 회복의 첫걸음입니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 나는 감정을 인식하고 있었나? 솔직히 모르겠다. 무시하는 게 습관이 되어서인지, 어떤 감정이 드는지 생각조차 잘 안 했던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열등감도 그랬다. 다른 크루들 대화에서 소외감을 느낄 때, 페어 프로그래밍에서 내 의견을 숨길 때, 그 감정들을 그냥 흘려보내거나 억누르는 게 익숙했다. 인식하는 것 자체를 하지 않았으니 흘려보낼 수도 없었던 거다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회복탄력성은 실패 직후에 측정된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서 가장 인상 깊었던 말이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;회복탄력성은 평소에 측정하면 측정이 안된다. 실패 경험 직후에 측정했을 때 측정이 잘된다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;얼마나 빨리 일어나는가가 아니라, 어떻게 일어서는지를 아는 사람이 되는 것이다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 말이 레벨2 미션을 진행하면서 와닿았다. 한 미션에서는 페어와 함께 결정 사유를 문서로 남기고, 방향이 달라도 끝까지 짚고 넘어가면서 진행했다. 미션이 끝났을 때 처음으로 &quot;이번엔 진심으로 임했다&quot;는 느낌이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 어떤 미션에서는 빠른 PR을 위해 대충 협의하고 넘어갔다. 결과물은 비슷했을지 몰라도, 미션이 끝난 후 찜찜함이 남았다. 결과가 아니라 과정에서 내가 어떻게 임했느냐가 나한테는 중요한 것 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대인관계 - 나를 숨기지 않는 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회복탄력성의 핵심 요소 중 대인관계가 있었다. 공감력, 소통력, 자아확장력.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 페어 프로그래밍에서 나는 나를 많이 숨겼다. 모르는 게 있어도 대충 넘어가고, 내 의견이 틀릴까봐 확신이 없으면 말을 아꼈다. 상대방이 나를 &quot;부족한 사람&quot;으로 볼까봐 두려웠던 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 돌아보면, 페어가 &quot;나도 잘 모른다&quot;고 먼저 말해줬을 때 오히려 내가 주도적으로 할 수 있었고, 그게 제일 좋은 협업이었다. 내가 먼저 그 역할을 할 수도 있다는 걸 이제는 알것 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 강의를 듣기 전까지 나는 자아존중감을 제대로 들여다본 적이 없었다. 그냥 나는 열등감이 많은 사람이라고 치부하고 넘겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 강의를 들으면서, 그리고 스스로를 돌아보면서 조금 달리 보이기 시작했다. 자존감이 없는 게 아니라, 나 자신한테만 유독 가혹한 기준을 들이대고 있었던 것 같다. 남이 모른다고 하면 괜찮은데, 내가 모른다고 하면 안 된다고 느끼는 것처럼.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔들려도 무너지지 않는 힘은 빠르게 일어나는 게 아니라고 했다. 어떻게 일어서는지 아는 것이라고. 나는 아직 배워가는 중이지만, 적어도 지금은 내가 어떨 때 잘 되는지는 알 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;그걸로 충분한 시작이라고 생각한다. 이제부터는 결과보다 과정에 집중하고, 진심으로 임한 나 자신을 칭찬하는 것부터 시작해보려 한다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>우아한테크코스 8기</category>
      <category>소프트스킬</category>
      <category>회복탄력성</category>
      <author>토리8</author>
      <guid isPermaLink="true">https://tory4953.tistory.com/4</guid>
      <comments>https://tory4953.tistory.com/4#entry4comment</comments>
      <pubDate>Sun, 31 May 2026 16:32:29 +0900</pubDate>
    </item>
    <item>
      <title>MethodArgumentNotValidException 발생 조건부터 핸들링까지</title>
      <link>https://tory4953.tistory.com/3</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;방탈출 사용자 예약 미션에서 요청 DTO 검증과 전역 예외 처리를 구현하며 &lt;code&gt;MethodArgumentNotValidException&lt;/code&gt;을 &lt;code&gt;@ExceptionHandler&lt;/code&gt;로 처리한 경험이 있다. 당시에는 이 예외가 왜 발생하는지, 내부적으로 어떻게 동작하는지 제대로 이해하지 못한 채 구현에 임했다. 미션이 끝난 지금, 동작 원리를 제대로 정리해두고자 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;MethodArgumentNotValidException&lt;/code&gt;이란 무엇인가?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BindException to be thrown when validation on an argument annotated with @Valid fails. &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/MethodArgumentNotValidException.html&quot;&gt;Spring Docs - MethodArgumentNotValidException&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Valid&lt;/code&gt;가 붙은 파라미터의 Bean Validation 검증이 &lt;b&gt;실패했을 때&lt;/b&gt; Spring이 던지는 예외다. &lt;code&gt;BindException&lt;/code&gt;을 상속하기 때문에, 검증에 실패한 모든 오류 정보를 &lt;code&gt;BindingResult&lt;/code&gt;를 통해 꺼낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;BindingResult&lt;/code&gt;에는 두 종류의 에러가 담긴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;FieldError&lt;/code&gt;: 특정 필드에서 발생한 검증 실패 (예: &lt;code&gt;name&lt;/code&gt;이 비어있음)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ObjectError&lt;/code&gt;: 객체 전체 수준의 검증 실패 (Cross-field validation)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;MethodArgumentNotValidException은&lt;/b&gt; &lt;code&gt;@Valid&lt;/code&gt; 검증 실패 시 발생하며, &lt;b&gt;어떤 필드가 왜 실패했는지&lt;/b&gt; 상세 정보를 담고 있는 예외다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;MethodArgumentNotValidException&lt;/code&gt; 예외는 언제 발생할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MethodArgumentNotValidException&lt;/code&gt;은 &lt;code&gt;@RequestBody&lt;/code&gt; 파라미터에 &lt;code&gt;@Valid&lt;/code&gt;가 선언된 상태에서 Bean Validation 검증이 실패했을 때 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로는 &lt;code&gt;RequestResponseBodyMethodProcessor&lt;/code&gt;의 &lt;code&gt;resolveArgument()&lt;/code&gt;에서 다음 순서로 동작한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Object arg = readWithMessageConverters(...)             // 1. JSON &amp;rarr; Java 객체 변환
WebDataBinder binder = binderFactory.createBinder(...) // 2. DataBinder 생성
validateIfApplicable(binder, parameter)                // 3. Validator에게 검증 위임

if (binder.getBindingResult().hasErrors()
        &amp;amp;&amp;amp; isBindExceptionRequired(binder, parameter)) {
    throw new MethodArgumentNotValidException(...)     // 4. 에러 있으면 예외 던짐
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;HttpMessageConverter&lt;/code&gt;를 통해 JSON을 Java 객체로 변환&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DataBinder&lt;/code&gt;를 생성하고 &lt;code&gt;validateIfApplicable()&lt;/code&gt;로 Validator에게 검증 위임&lt;/li&gt;
&lt;li&gt;검증 완료 후 아래 두 조건을 &lt;b&gt;동시에&lt;/b&gt; 확인
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;binder.getBindingResult().hasErrors()&lt;/code&gt; &amp;rarr; 검증 실패 에러가 존재하는가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;isBindExceptionRequired()&lt;/code&gt; &amp;rarr; 컨트롤러 메서드 파라미터에 &lt;code&gt;BindingResult&lt;/code&gt;가 없는가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;두 조건이 모두 참이면 &lt;code&gt;MethodArgumentNotValidException&lt;/code&gt;을 던짐
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;둘 중 하나라도 거짓이면 예외를 던지지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주목할 조건은 &lt;code&gt;isBindExceptionRequired()&lt;/code&gt;다. 컨트롤러 메서드 파라미터 목록에 &lt;code&gt;Errors&lt;/code&gt; 또는 &lt;code&gt;BindingResult&lt;/code&gt;가 함께 선언되어 있으면 &lt;code&gt;false&lt;/code&gt;를 반환한다. 이 경우 예외를 던지지 않고, 검증 실패 정보를 메서드가 직접 받아 처리할 수 있도록 넘긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;MethodArgumentNotValidException은&lt;/b&gt; 검증 실패 결과를 메서드가 직접 받지 않는 경우에만 발생하며, 예외를 던지는 주체는 Spring MVC의 &lt;code&gt;RequestResponseBodyMethodProcessor&lt;/code&gt;다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;MethodArgumentNotValidException&lt;/code&gt; 발생했을 때 어떻게 적절한 에러 응답을 반환하는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;MethodArgumentNotValidException&lt;/code&gt;은 두 가지 방식으로 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 전역 처리 &amp;mdash; &lt;code&gt;@RestControllerAdvice&lt;/code&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 전체에서 발생하는 &lt;code&gt;MethodArgumentNotValidException&lt;/code&gt;을 한 곳에서 처리한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity&amp;lt;ErrorResponse&amp;gt; handle(MethodArgumentNotValidException e,
                                                HttpServletRequest request) {
        List&amp;lt;FieldErrorResponse&amp;gt; fieldErrors = e.getBindingResult().getFieldErrors().stream()
                .map(fe -&amp;gt; new FieldErrorResponse(fe.getField(), fe.getDefaultMessage()))
                .toList();
        ErrorResponse errorResponse = new ErrorResponse(
                &quot;INVALID_CONSTRAINT&quot;, request.getRequestURI(), &quot;요청 값이 유효하지 않습니다.&quot;, fieldErrors
        );
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;getFieldErrors()&lt;/code&gt;로 꺼낸 &lt;code&gt;FieldError&lt;/code&gt;에서 &lt;code&gt;getField()&lt;/code&gt;(실패한 필드명)와 &lt;code&gt;getDefaultMessage()&lt;/code&gt;(제약 조건에 선언한 메시지)를 사용해 에러 응답을 구성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 핸들러가 반환하는 에러 응답은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;code&quot;: &quot;INVALID_CONSTRAINT&quot;,
  &quot;path&quot;: &quot;/reservations&quot;,
  &quot;message&quot;: &quot;요청 값이 유효하지 않습니다.&quot;,
  &quot;fieldErrors&quot;: [
    {
      &quot;field&quot;: &quot;name&quot;,
      &quot;message&quot;: &quot;이름은 필수입니다.&quot;
    },
    {
      &quot;field&quot;: &quot;date&quot;,
      &quot;message&quot;: &quot;날짜는 필수입니다.&quot;
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 로컬 처리 &amp;mdash; 컨트롤러 내 &lt;code&gt;@ExceptionHandler&lt;/code&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 컨트롤러에서만 다르게 처리하고 싶을 때 사용한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@RestController
public class ReservationController {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity&amp;lt;ErrorResponse&amp;gt; handle(MethodArgumentNotValidException e) {
        // 이 컨트롤러에서만 적용되는 처리
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;MethodArgumentNotValidException 처리의 핵심&lt;/b&gt;은 &lt;code&gt;e.getBindingResult().getFieldErrors()&lt;/code&gt;로 실패한 필드 목록을 꺼내 클라이언트에게 &lt;b&gt;어떤 필드가 왜 잘못됐는지&lt;/b&gt; 구체적으로 전달하는 것이라고 생각한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;code&gt;ResponseEntityExceptionHandler&lt;/code&gt;를 통해서 핸들링하는 방식&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;code&gt;ResponseEntityExceptionHandler&lt;/code&gt;란?&lt;/b&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A class with an @ExceptionHandler method that handles all Spring MVC raised exceptions by returning a &lt;a title=&quot;class in org.springframework.http&quot; href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/ResponseEntity.html&quot;&gt;ResponseEntity&lt;/a&gt; with RFC 9457 formatted error details in the body.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC가 기본으로 제공하는 추상 클래스로, &lt;code&gt;MethodArgumentNotValidException&lt;/code&gt;을 포함한 스프링 내부 표준 예외들을 처리하는 &lt;code&gt;@ExceptionHandler&lt;/code&gt; 메서드들이 미리 구현되어 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;왜 쓰는가? 직접 &amp;nbsp;&lt;code&gt;@ExceptionHandler&lt;/code&gt;&amp;nbsp; 작성 방식과의 차이&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 작성 방식은 &lt;code&gt;@ExceptionHandler(MethodArgumentNotValidException.class)&lt;/code&gt;를 선언하고 처리 로직을 처음부터 구현해야 한다. 반면 &lt;code&gt;ResponseEntityExceptionHandler&lt;/code&gt;를 상속하면, 이미 등록된 핸들러 메서드를 오버라이드하는 방식으로 응답 형식만 바꿀 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;code&gt;MethodArgumentNotValidException&lt;/code&gt; 하나만이 아니라 Spring이 내부적으로 발생시키는 수십 개의 표준 예외들을 일관된 구조로 처리할 수 있다는 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;어떻게 쓰는가 (상속 + 오버라이드)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity&amp;lt;Object&amp;gt; handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex,
            HttpHeaders headers,
            HttpStatusCode status,
            WebRequest request) {

        List&amp;lt;FieldErrorResponse&amp;gt; fieldErrors = ex.getBindingResult().getFieldErrors().stream()
                .map(fe -&amp;gt; new FieldErrorResponse(fe.getField(), fe.getDefaultMessage()))
                .toList();

        ErrorResponse errorResponse = new ErrorResponse(
                &quot;INVALID_CONSTRAINT&quot;, &quot;요청 값이 유효하지 않습니다.&quot;, fieldErrors
        );

        return ResponseEntity.status(status).body(errorResponse);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 작성 방식과 비교했을 때 두 가지 차이가 눈에 띈다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;HttpStatus.BAD_REQUEST&lt;/code&gt;를 하드코딩하지 않고, 파라미터로 넘어온 &lt;code&gt;status&lt;/code&gt;를 그대로 사용한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HttpServletRequest&lt;/code&gt; 대신 &lt;code&gt;WebRequest&lt;/code&gt;를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;ResponseEntityExceptionHandler는&lt;/b&gt; 응답 형식만 커스터마이징하고 예외 등록은 Spring에 위임하는 방식으로, 표준 예외를 일관되게 처리할 때 유리하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자료 조사하면서 더 알아볼 키워드&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;BindingResult&lt;/code&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;FieldError&lt;/code&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;HttpMessageConverter&lt;/code&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;MappingJackson2HttpMessageConverter&lt;/code&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>MethodArgumentNotValidException</category>
      <category>ResponseEntityExceptionHandler</category>
      <author>토리8</author>
      <guid isPermaLink="true">https://tory4953.tistory.com/3</guid>
      <comments>https://tory4953.tistory.com/3#entry3comment</comments>
      <pubDate>Sat, 30 May 2026 20:16:54 +0900</pubDate>
    </item>
    <item>
      <title>@Valid vs @Validate 차이는 무엇인가?</title>
      <link>https://tory4953.tistory.com/2</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;우테코 미션을 진행하면서 Spring Validation에 대해서 학습하면서 @Valid와 @Validated에 대해서 알게 되었는데 이를 상황에 맞게 선택하기 위해, 두 애노테이션이 동작하는 메커니즘의 차이를 정리하고자 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@Valid이란 무엇인가?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Valid는 JSR 표준 애노테이션으로, @Constraint 애노테이션이 선언된 DTO를 검증할 때 사용한다. ArgumentResolver가 객체를 바인딩한 뒤, Validator가 @Valid를 트리거로 삼아 객체 내부의 제약 조건들을 검증하는 방식이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;@Validated이란 무엇인가?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Validated는 Spring이 추가한 애노테이션으로, 클래스 위에 선언한다. @RequestParam, @PathVariable 같은 단순 파라미터에도 @NotNull 등의 제약 조건을 적용할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 &lt;b&gt;Spring AOP&lt;/b&gt;를 활용한다. 프록시 객체가 해당 메서드를 가로채어, 메서드가 실행되기 &lt;b&gt;전에&lt;/b&gt; 파라미터를 검증하는 방식이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;@Valid 하나만으로도 문제없는 거 아냐? / 왜 굳이 @Validated를 쓰는 걸까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요청 DTO 검증 &amp;mdash; @Valid로 충분하다&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;d&quot;&gt;&lt;code&gt;@PostMapping  
public ResponseEntity&amp;lt;ReservationResponse&amp;gt; register(@Valid @RequestBody ReservationRequest reservationRequest) {  
    ReservationResponse reservationResponse = reservationService.register(reservationRequest);  
    return ResponseEntity.created(URI.create(&quot;/reservations/&quot; + reservationResponse.id()))  
            .body(reservationResponse);  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO처럼 객체가 매개변수로 들어오는 경우에는 @Valid만으로 충분하다. ArgumentResolver가 객체를 바인딩한 직후 Validator가 내부 필드를 검증하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단순 파라미터 검증 &amp;mdash; @Valid로는 불가능하다&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@DeleteMapping(value = &quot;/{id}&quot;)  
public ResponseEntity&amp;lt;Void&amp;gt; cancelByIdAndName(@Positive @PathVariable Long id, @NotBlank @RequestParam String username) {  
    reservationService.cancelByIdAndName(id, username);  
    return ResponseEntity.noContent().build();  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@PathVariable, @RequestParam처럼 단순 타입이 매개변수로 들어오는 경우는 다르다. 이들은 ArgumentResolver가 객체로 바인딩하는 대상이 아니기 때문에, @Valid의 검증 트리거가 동작하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 &lt;b&gt;클래스 레벨에 @Validated를 선언&lt;/b&gt;하면 해결된다. Spring AOP의 프록시가 메서드 실행 전에 파라미터를 가로채 검증을 수행하기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Validated  // 클래스 레벨에 선언
@RestController
public class ReservationController {

    @DeleteMapping(value = &quot;/{id}&quot;)  
    public ResponseEntity&amp;lt;Void&amp;gt; cancelByIdAndName(@Positive @PathVariable Long id,  
                                                  @NotBlank @RequestParam String username) {  
        reservationService.cancelByIdAndName(id, username);  
        return ResponseEntity.noContent().build();  
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  @Valid는 객체 바인딩 이후 검증 / @Validated는 메서드 실행 이전 검증&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;두 방식의 차이점은?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① 발생 예외 타입이 다르다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Valid&lt;/code&gt;와 &lt;code&gt;@Validated&lt;/code&gt;는 검증 실패 시 던지는 예외가 다르기 때문에, &lt;code&gt;@ExceptionHandler&lt;/code&gt;에서 각각 처리해야 한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;@Valid&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;@Validated&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;발생 예외&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MethodArgumentNotValidException&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ConstraintViolationException&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② 적용 가능한 위치(&lt;code&gt;@Target&lt;/code&gt;)가 다르다&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;@Valid&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;@Validated&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;@Target&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FIELD&lt;/code&gt;, &lt;code&gt;METHOD&lt;/code&gt;, &lt;code&gt;PARAMETER&lt;/code&gt; 등&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TYPE&lt;/code&gt;, &lt;code&gt;METHOD&lt;/code&gt;, &lt;code&gt;PARAMETER&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Valid&lt;/code&gt;는 클래스 레벨(&lt;code&gt;TYPE&lt;/code&gt;)에 선언할 수 없다. 반면 &lt;code&gt;@Validated&lt;/code&gt;는 클래스 레벨에 선언할 수 있기 때문에 AOP 포인트컷 역할이 가능하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;호기심 많은 개발자라면 궁금할 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 흥미로운 점은, 실제로는 @Validated 없이 @Constraint 애노테이션만으로도 단순 파라미터 검증이 가능한 경우가 있다. 이 부분은 &lt;b&gt;HandlerMethodValidationException&lt;/b&gt;을 정리하면서 따로 다룰 예정이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자료 조사하면서 더 알아볼 키워드&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;HandlerMethodArgumentResolver&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring AOP 프록시 메커니즘 (JDK Dynamic Proxy vs CGLIB)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Bean Validation Group 검증&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MethodArgumentNotValidException&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>@Valid</category>
      <category>@Validated</category>
      <author>토리8</author>
      <guid isPermaLink="true">https://tory4953.tistory.com/2</guid>
      <comments>https://tory4953.tistory.com/2#entry2comment</comments>
      <pubDate>Sun, 24 May 2026 21:28:13 +0900</pubDate>
    </item>
    <item>
      <title>Bean Validation으로 요청 DTO 검증하기</title>
      <link>https://tory4953.tistory.com/1</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;레벨2 방탈출 미션에서 요청 DTO 검증을 도메인 검증과 동일하게 validate() 메서드로 직접 처리했다. 리뷰어분이 Spring Validation을 키워드로 제시해줬고, 이를 계기로 Bean Validation을 도입하게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 Bean Validation을 적용하면서 겪은 시행착오와 오해를 솔직하게 정리한 내용이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-heading=&quot;&amp;#96;Bean Validation&amp;#96;이란 무엇인가?&quot; data-ke-size=&quot;size26&quot;&gt;Bean Validation이란 무엇인가?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bean Validation은 JavaBean 유효성 검증을 위한 메타데이터 모델과 API에 대한 정의이며 여기서 언급하고 있는 JavaBean은 직렬화 가능하고 매개변수가 없는 생성자를 가지며, Getter 와 Setter Method를 사용하여 프로퍼티에 접근이 가능한 객체를 의미합니다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 객체의 필드 값이나 상태에 대해서 &lt;b&gt;제약 조건(Constraint)&lt;/b&gt;를 애노테이션 통해서 간편하게 검증할 수 있도록 지원하는 기술이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-heading=&quot;&amp;#96;Bean Validation&amp;#96; 없이도 문제없는 거 아냐?&quot; data-ke-size=&quot;size26&quot;&gt;Bean Validation 없이도 문제없는 거 아냐?&lt;/h2&gt;
&lt;h3 data-heading=&quot;**미션을 진행하면서 느낀 기존 방식의 불편함**&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;미션을 진행하면서 느낀 기존 방식의 불편함&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bean Validation 없이도 필드마다 &lt;b&gt;검증 메서드를 직접 작성해 호출하는 방식&lt;/b&gt;으로 충분히 구현할 수 있다. 지금은 필드가 4개뿐이라 괜찮아 보이지만, 필드가 늘어나거나 요구사항이 바뀐다면 어떨까? 검증 메서드가 그만큼 늘어나고, 변경할 곳도 많아진다. 결국 코드의&amp;nbsp;&lt;b&gt;가독성과 유지보수성이 눈에 띄게 떨어진다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;public record ReservationRequest(
        String name,
        LocalDate date,
        Long timeId,
        Long themeId) {

    public ReservationRequest {
        validateName(name);
        validateDate(date);
        validateTimeId(timeId);
        validateThemeId(themeId);
    }

    private static void validateName(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException(&quot;예약자 이름은 빈값일 수 없습니다.&quot;);
        }
        if (name.length() &amp;gt; 20) {
            throw new IllegalArgumentException(&quot;예약자 이름은 20자 이하여야 합니다.&quot;);
        }
    }

    private static void validateDate(LocalDate date) {
        if (date == null) {
            throw new IllegalArgumentException(&quot;예약 날짜는 필수입니다.&quot;);
        }
    }

    private static void validateTimeId(Long timeId) {
        if (timeId == null) {
            throw new IllegalArgumentException(&quot;예약 시간은 필수입니다.&quot;);
        }
    }

    private static void validateThemeId(Long themeId) {
        if (themeId == null) {
            throw new IllegalArgumentException(&quot;예약 테마는 필수입니다.&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;**Bean Validation을 적용함으로서 간편해진 코드**&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Bean Validation을 적용함으로서 간편해진 코드&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;public record ReservationRequest(  
        @NotBlank(message = &quot;예약자 이름은 빈값일 수 없습니다.&quot;)  
        @Size(max = 20, message = &quot;예약자 이름은 20자 이하여야 합니다.&quot;)  
        String name,  
  
        @NotNull(message = &quot;예약 날짜는 필수입니다.&quot;)  
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = &quot;yyyy-MM-dd&quot;)  
        LocalDate date,  
  
        @NotNull(message = &quot;예약 시간은 필수입니다.&quot;)  
        Long timeId,  
  
        @NotNull(message = &quot;예약 테마는 필수입니다.&quot;)  
        Long themeId) {  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-heading=&quot;제약 선언과 검증 실행은 다르다&quot; data-ke-size=&quot;size26&quot;&gt;제약 선언과 검증 실행은 다르다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미션을 진행하면서&amp;nbsp;@NotNull,&amp;nbsp;@Size&amp;nbsp;등의 애노테이션을 추가하면 Spring이 알아서 검증을 수행해줄 것이라고 생각했다. 하지만 실제 테스트에서는 검증이 동작하지 않았고, &lt;b&gt;Controller 파라미터에&amp;nbsp;@Valid를 추가해야만 검증이 수행됐다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;d&quot;&gt;&lt;code&gt;@PostMapping  
public ResponseEntity&amp;lt;ReservationResponse&amp;gt; register(@Valid @RequestBody ReservationRequest reservationRequest) {  
    ReservationResponse reservationResponse = reservationService.register(reservationRequest);  
    return ResponseEntity.created(URI.create(&quot;/reservations/&quot; + reservationResponse.id()))  
            .body(reservationResponse);  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@NotNull,&amp;nbsp;@Size&amp;nbsp;같은 애노테이션은&amp;nbsp;&lt;b&gt;&quot;이 필드는 이런 제약이 있다&quot;고 선언만 할 뿐&lt;/b&gt;, 실제로 검증을 실행하지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Valid를 컨트롤러 파라미터에 선언해야 Spring이&amp;nbsp;&lt;b&gt;&quot;아, 이 객체는 검증이 필요하구나&quot;&lt;/b&gt;&amp;nbsp;를 인식하고, 선언된 제약 조건들을 실제로 실행시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 제약 조건 애노테이션은&amp;nbsp;&lt;b&gt;규칙서&lt;/b&gt;이고,&amp;nbsp;@Valid는&amp;nbsp;&lt;b&gt;&quot;이 규칙서를 실제로 검사해라&quot;는 명령&lt;/b&gt;인 셈이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-heading=&quot;(i+1) 그렇다면 &amp;#96;@Valid&amp;#96;는 어떤식으로 검증이 진행되는가?&amp;#96;&quot; data-ke-size=&quot;size26&quot;&gt;(i+1) 그렇다면 @Valid는 어떤식으로 검증이 진행되는가?`&lt;/h2&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@RequestBody JSON 역직렬화
   &amp;darr;
ReservationRequest 객체 생성 (DTO 생성 완료)
   &amp;darr;
@Valid 감지 (ArgumentResolver)
   &amp;darr;
검증 위임 (LocalValidatorFactoryBean)
   &amp;darr;
@Constraint 애노테이션 탐색 (Validator)
   &amp;darr;
실제 검증 수행 (ConstraintValidator)
   &amp;darr;
실패 시 MethodArgumentNotValidException 발생
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@RequestBody에 의해 JSON이 역직렬화되어 DTO 객체가 먼저 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 컨트롤러 파라미터에 @Valid가 선언되면, Spring MVC의 ArgumentResolver가 이를 감지해 LocalValidatorFactoryBean에게 검증을 위임한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Validator는 &lt;b&gt;생성된 DTO 객체의&lt;/b&gt; 필드에 선언된 @Constraint 애노테이션들을 탐색하고, 각 애노테이션에 연결된 ConstraintValidator가 실제 검증 로직을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증에 실패하면 MethodArgumentNotValidException이 발생한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-heading=&quot;  호기심 많은 개발자라면 궁금할 점&quot; data-ke-size=&quot;size23&quot;&gt;(i+1) 호기심 많은 개발자라면 궁금할 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DTO가 아닌 서비스 레이어에서도 Bean Validation을 적용할 수 있을까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 컨트롤러에서 @Valid로 DTO를 검증했는데, 서비스 메서드 파라미터에도 동일하게 적용할 수 있는지 궁금했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하면, &lt;b&gt;@Valid는 기본적으로 컨트롤러 계층에서만 동작한다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 요청은 프론트 컨트롤러인 DispatcherServlet을 통해 컨트롤러로 전달된다. 이 과정에서 ArgumentResolver가 컨트롤러 메서드의 객체를 만들어주는데, @Valid 역시 ArgumentResolver에 의해 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@RequestBody의 경우 ArgumentResolver의 구현체인 RequestResponseBodyMethodProcessor가 JSON을 객체로 변환하며, 이 내부에서 @Valid로 시작하는 애노테이션이 있을 경우 유효성 검사를 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;mdash; &lt;a href=&quot;https://mangkyu.tistory.com/174&quot; data-tooltip-position=&quot;top&quot;&gt;MangKyu's Diary&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, @Valid는 ArgumentResolver가 동작하는 컨트롤러 계층에 종속되어 있기 때문에, 서비스나 레포지토리 같은 다른 계층에서는 기본적으로 검증이 수행되지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;더 알아볼 수 있는 키워드&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ArgumentResolver&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LocalValidatorFactoryBean&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ConstraintValidator&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <author>토리8</author>
      <guid isPermaLink="true">https://tory4953.tistory.com/1</guid>
      <comments>https://tory4953.tistory.com/1#entry1comment</comments>
      <pubDate>Sat, 23 May 2026 19:51:56 +0900</pubDate>
    </item>
  </channel>
</rss>