[{"data":1,"prerenderedAt":1560},["ShallowReactive",2],{"post-document:\u002Fjava\u002Fspring\u002F2026\u002F08\u002F15\u002Fspring-rest-api-validation\u002F":3},{"id":4,"title":5,"body":6,"categories":1539,"date":1541,"description":1542,"extension":1543,"image":1544,"key_concepts":1544,"last_modified_at":1544,"legacyPath":1545,"meta":1546,"navigation":763,"part":1548,"path":1549,"published":763,"robots":1544,"seo":1550,"series":1551,"stem":1552,"strengths":1544,"summary":1544,"tags":1553,"tradeoffs":1544,"__hash__":1559},"posts\u002Fposts\u002FBackend\u002FSPRING\u002FSpring\u002F2026-08-15-spring-rest-api-validation.md","Spring 기초 Part 3: REST API 요청값과 입력값 검증",{"type":7,"value":8,"toc":1524},"minimark",[9,13,20,31,34,39,178,180,184,250,256,259,292,303,308,311,341,350,355,358,382,437,443,445,449,452,476,479,514,552,558,560,564,571,574,601,605,731,734,834,836,844,850,956,959,965,1007,1014,1016,1020,1026,1120,1123,1156,1158,1162,1228,1282,1284,1288,1293,1299,1302,1373,1416,1419,1421,1425,1491,1496,1500,1520],[10,11,5],"h2",{"id":12},"spring-기초-part-3-rest-api-요청값과-입력값-검증",[14,15,16],"blockquote",{},[17,18,19],"p",{},"Controller는 HTTP 요청값을 자바 값이나 객체로 변환하고, 규칙에 맞는지 검증한 뒤 Service에 전달한다.",[21,22,28],"pre",{"className":23,"code":25,"language":26,"meta":27},[24],"language-text","HTTP Request\n    ↓\n요청값 바인딩\n    ↓\n입력값 검증\n    ↓\nController\n    ↓\nService\n","text","",[29,30,25],"code",{"__ignoreMap":27},[32,33],"hr",{},[35,36,38],"h3",{"id":37},"_1-먼저-알아둘-단어","1. 먼저 알아둘 단어",[40,41,42,58],"table",{},[43,44,45],"thead",{},[46,47,48,52,55],"tr",{},[49,50,51],"th",{},"용어",[49,53,54],{},"정의",[49,56,57],{},"쉽게 말하면",[59,60,61,75,86,97,112,123,134,151,165],"tbody",{},[46,62,63,67,70],{},[64,65,66],"td",{},"Endpoint",[64,68,69],{},"Client가 호출할 수 있는 API 주소와 HTTP Method의 조합",[64,71,72],{},[29,73,74],{},"POST \u002Fapi\u002Fusers",[46,76,77,80,83],{},[64,78,79],{},"Parameter",[64,81,82],{},"HTTP 요청에 포함된 입력값",[64,84,85],{},"사용자 ID, 검색어, 페이지 번호",[46,87,88,91,94],{},[64,89,90],{},"Request Body",[64,92,93],{},"HTTP 요청 본문에 담긴 데이터",[64,95,96],{},"JSON 회원가입 정보",[46,98,99,102,105],{},[64,100,101],{},"Binding",[64,103,104],{},"HTTP 요청값을 자바 값이나 객체로 변환하는 과정",[64,106,107,108,111],{},"JSON을 ",[29,109,110],{},"UserCreateRequest","로 변환",[46,113,114,117,120],{},[64,115,116],{},"DTO",[64,118,119],{},"계층 사이에서 데이터를 전달하는 객체",[64,121,122],{},"API 요청 전용 객체",[46,124,125,128,131],{},[64,126,127],{},"Validation",[64,129,130],{},"값이 정해진 규칙을 만족하는지 검사하는 과정",[64,132,133],{},"이메일 형식과 나이 확인",[46,135,136,139,142],{},[64,137,138],{},"Constraint",[64,140,141],{},"입력값이 지켜야 하는 검증 규칙",[64,143,144,147,148],{},[29,145,146],{},"@NotBlank",", ",[29,149,150],{},"@Email",[46,152,153,156,159],{},[64,154,155],{},"직렬화",[64,157,158],{},"자바 객체를 JSON 같은 전송 형식으로 변환",[64,160,161,164],{},[29,162,163],{},"UserResponse"," → JSON",[46,166,167,170,173],{},[64,168,169],{},"역직렬화",[64,171,172],{},"JSON을 자바 객체로 변환",[64,174,175,176],{},"JSON → ",[29,177,110],{},[32,179],{},[35,181,183],{"id":182},"_2-http-요청값을-받는-세-가지-방법","2. HTTP 요청값을 받는 세 가지 방법",[40,185,186,199],{},[43,187,188],{},[46,189,190,193,196],{},[49,191,192],{},"어노테이션",[49,194,195],{},"값을 가져오는 위치",[49,197,198],{},"예시",[59,200,201,220,238],{},[46,202,203,208,211],{},[64,204,205],{},[29,206,207],{},"@PathVariable",[64,209,210],{},"URL 경로",[64,212,213,216,217],{},[29,214,215],{},"\u002Fapi\u002Fusers\u002F1","의 ",[29,218,219],{},"1",[46,221,222,227,230],{},[64,223,224],{},[29,225,226],{},"@RequestParam",[64,228,229],{},"Query String",[64,231,232,216,235],{},[29,233,234],{},"\u002Fapi\u002Fusers?name=민형",[29,236,237],{},"name",[46,239,240,245,248],{},[64,241,242],{},[29,243,244],{},"@RequestBody",[64,246,247],{},"HTTP 요청 본문",[64,249,96],{},[251,252,254],"h4",{"id":253},"pathvariable",[29,255,207],{},[17,257,258],{},"특정 자원을 식별할 때 사용한다.",[21,260,264],{"className":261,"code":262,"language":263,"meta":27,"style":27},"language-java shiki shiki-themes github-light github-dark","@GetMapping(\"\u002Fapi\u002Fusers\u002F{id}\")\npublic UserResponse getUser(@PathVariable Long id) {\n    return userService.getUser(id);\n}\n","java",[29,265,266,274,280,286],{"__ignoreMap":27},[267,268,271],"span",{"class":269,"line":270},"line",1,[267,272,273],{},"@GetMapping(\"\u002Fapi\u002Fusers\u002F{id}\")\n",[267,275,277],{"class":269,"line":276},2,[267,278,279],{},"public UserResponse getUser(@PathVariable Long id) {\n",[267,281,283],{"class":269,"line":282},3,[267,284,285],{},"    return userService.getUser(id);\n",[267,287,289],{"class":269,"line":288},4,[267,290,291],{},"}\n",[21,293,297],{"className":294,"code":295,"language":296,"meta":27,"style":27},"language-http shiki shiki-themes github-light github-dark","GET \u002Fapi\u002Fusers\u002F1\n","http",[29,298,299],{"__ignoreMap":27},[267,300,301],{"class":269,"line":270},[267,302,295],{},[251,304,306],{"id":305},"requestparam",[29,307,226],{},[17,309,310],{},"검색, 필터, 정렬처럼 선택적인 조건을 전달할 때 자주 사용한다.",[21,312,314],{"className":261,"code":313,"language":263,"meta":27,"style":27},"@GetMapping(\"\u002Fapi\u002Fusers\")\npublic List\u003CUserResponse> searchUsers(\n        @RequestParam(required = false) String name) {\n    return userService.searchUsers(name);\n}\n",[29,315,316,321,326,331,336],{"__ignoreMap":27},[267,317,318],{"class":269,"line":270},[267,319,320],{},"@GetMapping(\"\u002Fapi\u002Fusers\")\n",[267,322,323],{"class":269,"line":276},[267,324,325],{},"public List\u003CUserResponse> searchUsers(\n",[267,327,328],{"class":269,"line":282},[267,329,330],{},"        @RequestParam(required = false) String name) {\n",[267,332,333],{"class":269,"line":288},[267,334,335],{},"    return userService.searchUsers(name);\n",[267,337,339],{"class":269,"line":338},5,[267,340,291],{},[21,342,344],{"className":294,"code":343,"language":296,"meta":27,"style":27},"GET \u002Fapi\u002Fusers?name=민형\n",[29,345,346],{"__ignoreMap":27},[267,347,348],{"class":269,"line":270},[267,349,343],{},[251,351,353],{"id":352},"requestbody",[29,354,244],{},[17,356,357],{},"JSON 같은 요청 본문을 자바 객체로 변환할 때 사용한다.",[21,359,361],{"className":261,"code":360,"language":263,"meta":27,"style":27},"@PostMapping(\"\u002Fapi\u002Fusers\")\npublic UserResponse createUser(@RequestBody UserCreateRequest request) {\n    return userService.createUser(request);\n}\n",[29,362,363,368,373,378],{"__ignoreMap":27},[267,364,365],{"class":269,"line":270},[267,366,367],{},"@PostMapping(\"\u002Fapi\u002Fusers\")\n",[267,369,370],{"class":269,"line":276},[267,371,372],{},"public UserResponse createUser(@RequestBody UserCreateRequest request) {\n",[267,374,375],{"class":269,"line":282},[267,376,377],{},"    return userService.createUser(request);\n",[267,379,380],{"class":269,"line":288},[267,381,291],{},[21,383,387],{"className":384,"code":385,"language":386,"meta":27,"style":27},"language-json shiki shiki-themes github-light github-dark","{\n  \"name\": \"민형\",\n  \"email\": \"min@example.com\",\n  \"age\": 25\n}\n","json",[29,388,389,395,411,423,433],{"__ignoreMap":27},[267,390,391],{"class":269,"line":270},[267,392,394],{"class":393},"sVt8B","{\n",[267,396,397,401,404,408],{"class":269,"line":276},[267,398,400],{"class":399},"sj4cs","  \"name\"",[267,402,403],{"class":393},": ",[267,405,407],{"class":406},"sZZnC","\"민형\"",[267,409,410],{"class":393},",\n",[267,412,413,416,418,421],{"class":269,"line":282},[267,414,415],{"class":399},"  \"email\"",[267,417,403],{"class":393},[267,419,420],{"class":406},"\"min@example.com\"",[267,422,410],{"class":393},[267,424,425,428,430],{"class":269,"line":288},[267,426,427],{"class":399},"  \"age\"",[267,429,403],{"class":393},[267,431,432],{"class":399},"25\n",[267,434,435],{"class":269,"line":338},[267,436,291],{"class":393},[17,438,439,440,442],{},"Spring MVC와 Jackson이 JSON을 ",[29,441,110],{}," 객체로 변환한다.",[32,444],{},[35,446,448],{"id":447},"_3-entity-대신-요청-dto를-사용한다","3. Entity 대신 요청 DTO를 사용한다",[17,450,451],{},"요청 JSON을 Entity로 바로 받으면 API 입력 구조와 DB 구조가 강하게 연결된다.",[21,453,455],{"className":261,"code":454,"language":263,"meta":27,"style":27},"\u002F\u002F 권장하지 않는 형태\npublic UserResponse createUser(@RequestBody User user) {\n    \u002F\u002F 요청이 Entity 구조에 직접 의존\n}\n",[29,456,457,462,467,472],{"__ignoreMap":27},[267,458,459],{"class":269,"line":270},[267,460,461],{},"\u002F\u002F 권장하지 않는 형태\n",[267,463,464],{"class":269,"line":276},[267,465,466],{},"public UserResponse createUser(@RequestBody User user) {\n",[267,468,469],{"class":269,"line":282},[267,470,471],{},"    \u002F\u002F 요청이 Entity 구조에 직접 의존\n",[267,473,474],{"class":269,"line":288},[267,475,291],{},[17,477,478],{},"요청 전용 DTO를 만들면 API가 허용할 값과 검증 규칙을 명확하게 표현할 수 있다.",[21,480,482],{"className":261,"code":481,"language":263,"meta":27,"style":27},"public record UserCreateRequest(\n        String name,\n        String email,\n        int age\n) {\n}\n",[29,483,484,489,494,499,504,509],{"__ignoreMap":27},[267,485,486],{"class":269,"line":270},[267,487,488],{},"public record UserCreateRequest(\n",[267,490,491],{"class":269,"line":276},[267,492,493],{},"        String name,\n",[267,495,496],{"class":269,"line":282},[267,497,498],{},"        String email,\n",[267,500,501],{"class":269,"line":288},[267,502,503],{},"        int age\n",[267,505,506],{"class":269,"line":338},[267,507,508],{},") {\n",[267,510,512],{"class":269,"line":511},6,[267,513,291],{},[40,515,516,526],{},[43,517,518],{},[46,519,520,523],{},[49,521,522],{},"객체",[49,524,525],{},"역할",[59,527,528,536,544],{},[46,529,530,533],{},[64,531,532],{},"Request DTO",[64,534,535],{},"Client가 보낸 입력값 표현",[46,537,538,541],{},[64,539,540],{},"Entity",[64,542,543],{},"데이터베이스에 저장하고 JPA가 관리",[46,545,546,549],{},[64,547,548],{},"Response DTO",[64,550,551],{},"Client에 반환할 데이터 표현",[21,553,556],{"className":554,"code":555,"language":26,"meta":27},[24],"JSON\n  → Request DTO\n  → Service\n  → Entity\n  → Repository\n  → Database\n",[29,557,555],{"__ignoreMap":27},[32,559],{},[35,561,563],{"id":562},"_4-bean-validation","4. Bean Validation",[17,565,566,567,570],{},"Bean Validation은 어노테이션으로 입력값 규칙을 선언하는 표준이다. Spring Boot 3 계열에서는 일반적으로 ",[29,568,569],{},"jakarta.validation"," 패키지를 사용한다.",[17,572,573],{},"검증 기능을 사용하려면 Validation Starter를 추가한다.",[21,575,579],{"className":576,"code":577,"language":578,"meta":27,"style":27},"language-xml shiki shiki-themes github-light github-dark","\u003Cdependency>\n    \u003CgroupId>org.springframework.boot\u003C\u002FgroupId>\n    \u003CartifactId>spring-boot-starter-validation\u003C\u002FartifactId>\n\u003C\u002Fdependency>\n","xml",[29,580,581,586,591,596],{"__ignoreMap":27},[267,582,583],{"class":269,"line":270},[267,584,585],{},"\u003Cdependency>\n",[267,587,588],{"class":269,"line":276},[267,589,590],{},"    \u003CgroupId>org.springframework.boot\u003C\u002FgroupId>\n",[267,592,593],{"class":269,"line":282},[267,594,595],{},"    \u003CartifactId>spring-boot-starter-validation\u003C\u002FartifactId>\n",[267,597,598],{"class":269,"line":288},[267,599,600],{},"\u003C\u002Fdependency>\n",[251,602,604],{"id":603},"자주-사용하는-검증-어노테이션","자주 사용하는 검증 어노테이션",[40,606,607,619],{},[43,608,609],{},[46,610,611,613,616],{},[49,612,192],{},[49,614,615],{},"검증 내용",[49,617,618],{},"적용 예시",[59,620,621,638,652,667,679,692,705,718],{},[46,622,623,628,635],{},[64,624,625],{},[29,626,627],{},"@NotNull",[64,629,630,631,634],{},"값이 ",[29,632,633],{},"null","이면 실패",[64,636,637],{},"객체, 숫자 필수값",[46,639,640,644,649],{},[64,641,642],{},[29,643,146],{},[64,645,646,648],{},[29,647,633],{},", 빈 문자열, 공백 문자열이면 실패",[64,650,651],{},"이름, 제목",[46,653,654,659,664],{},[64,655,656],{},[29,657,658],{},"@NotEmpty",[64,660,661,663],{},[29,662,633],{},"이거나 길이가 0이면 실패",[64,665,666],{},"문자열, 컬렉션",[46,668,669,673,676],{},[64,670,671],{},[29,672,150],{},[64,674,675],{},"이메일 형식 검사",[64,677,678],{},"이메일 주소",[46,680,681,686,689],{},[64,682,683],{},[29,684,685],{},"@Positive",[64,687,688],{},"0보다 큰 값인지 검사",[64,690,691],{},"가격, 수량",[46,693,694,699,702],{},[64,695,696],{},[29,697,698],{},"@Min",[64,700,701],{},"지정한 최솟값 이상인지 검사",[64,703,704],{},"나이",[46,706,707,712,715],{},[64,708,709],{},[29,710,711],{},"@Max",[64,713,714],{},"지정한 최댓값 이하인지 검사",[64,716,717],{},"점수",[46,719,720,725,728],{},[64,721,722],{},[29,723,724],{},"@Size",[64,726,727],{},"문자열이나 컬렉션의 길이 검사",[64,729,730],{},"비밀번호 길이",[17,732,733],{},"요청 DTO에 규칙을 선언한다.",[21,735,737],{"className":261,"code":736,"language":263,"meta":27,"style":27},"import jakarta.validation.constraints.Email;\nimport jakarta.validation.constraints.Max;\nimport jakarta.validation.constraints.Min;\nimport jakarta.validation.constraints.NotBlank;\n\npublic record UserCreateRequest(\n        @NotBlank(message = \"이름은 필수입니다.\")\n        String name,\n\n        @NotBlank(message = \"이메일은 필수입니다.\")\n        @Email(message = \"이메일 형식이 올바르지 않습니다.\")\n        String email,\n\n        @Min(value = 1, message = \"나이는 1 이상이어야 합니다.\")\n        @Max(value = 150, message = \"나이는 150 이하여야 합니다.\")\n        int age\n) {\n}\n",[29,738,739,744,749,754,759,765,769,775,780,785,791,797,802,807,813,819,824,829],{"__ignoreMap":27},[267,740,741],{"class":269,"line":270},[267,742,743],{},"import jakarta.validation.constraints.Email;\n",[267,745,746],{"class":269,"line":276},[267,747,748],{},"import jakarta.validation.constraints.Max;\n",[267,750,751],{"class":269,"line":282},[267,752,753],{},"import jakarta.validation.constraints.Min;\n",[267,755,756],{"class":269,"line":288},[267,757,758],{},"import jakarta.validation.constraints.NotBlank;\n",[267,760,761],{"class":269,"line":338},[267,762,764],{"emptyLinePlaceholder":763},true,"\n",[267,766,767],{"class":269,"line":511},[267,768,488],{},[267,770,772],{"class":269,"line":771},7,[267,773,774],{},"        @NotBlank(message = \"이름은 필수입니다.\")\n",[267,776,778],{"class":269,"line":777},8,[267,779,493],{},[267,781,783],{"class":269,"line":782},9,[267,784,764],{"emptyLinePlaceholder":763},[267,786,788],{"class":269,"line":787},10,[267,789,790],{},"        @NotBlank(message = \"이메일은 필수입니다.\")\n",[267,792,794],{"class":269,"line":793},11,[267,795,796],{},"        @Email(message = \"이메일 형식이 올바르지 않습니다.\")\n",[267,798,800],{"class":269,"line":799},12,[267,801,498],{},[267,803,805],{"class":269,"line":804},13,[267,806,764],{"emptyLinePlaceholder":763},[267,808,810],{"class":269,"line":809},14,[267,811,812],{},"        @Min(value = 1, message = \"나이는 1 이상이어야 합니다.\")\n",[267,814,816],{"class":269,"line":815},15,[267,817,818],{},"        @Max(value = 150, message = \"나이는 150 이하여야 합니다.\")\n",[267,820,822],{"class":269,"line":821},16,[267,823,503],{},[267,825,827],{"class":269,"line":826},17,[267,828,508],{},[267,830,832],{"class":269,"line":831},18,[267,833,291],{},[32,835],{},[35,837,839,840,843],{"id":838},"_5-valid로-검증-실행하기","5. ",[29,841,842],{},"@Valid","로 검증 실행하기",[17,845,846,847,849],{},"DTO에 검증 어노테이션만 붙이면 끝이 아니다. Controller 매개변수에 ",[29,848,842],{},"를 붙여 검증을 실행한다.",[21,851,853],{"className":261,"code":852,"language":263,"meta":27,"style":27},"@RestController\n@RequestMapping(\"\u002Fapi\u002Fusers\")\npublic class UserController {\n\n    private final UserService userService;\n\n    public UserController(UserService userService) {\n        this.userService = userService;\n    }\n\n    @PostMapping\n    public ResponseEntity\u003CUserResponse> createUser(\n            @Valid @RequestBody UserCreateRequest request) {\n\n        UserResponse response = userService.createUser(request);\n\n        return ResponseEntity\n                .status(HttpStatus.CREATED)\n                .body(response);\n    }\n}\n",[29,854,855,860,865,870,874,879,883,888,893,898,902,907,912,917,921,926,930,935,940,946,951],{"__ignoreMap":27},[267,856,857],{"class":269,"line":270},[267,858,859],{},"@RestController\n",[267,861,862],{"class":269,"line":276},[267,863,864],{},"@RequestMapping(\"\u002Fapi\u002Fusers\")\n",[267,866,867],{"class":269,"line":282},[267,868,869],{},"public class UserController {\n",[267,871,872],{"class":269,"line":288},[267,873,764],{"emptyLinePlaceholder":763},[267,875,876],{"class":269,"line":338},[267,877,878],{},"    private final UserService userService;\n",[267,880,881],{"class":269,"line":511},[267,882,764],{"emptyLinePlaceholder":763},[267,884,885],{"class":269,"line":771},[267,886,887],{},"    public UserController(UserService userService) {\n",[267,889,890],{"class":269,"line":777},[267,891,892],{},"        this.userService = userService;\n",[267,894,895],{"class":269,"line":782},[267,896,897],{},"    }\n",[267,899,900],{"class":269,"line":787},[267,901,764],{"emptyLinePlaceholder":763},[267,903,904],{"class":269,"line":793},[267,905,906],{},"    @PostMapping\n",[267,908,909],{"class":269,"line":799},[267,910,911],{},"    public ResponseEntity\u003CUserResponse> createUser(\n",[267,913,914],{"class":269,"line":804},[267,915,916],{},"            @Valid @RequestBody UserCreateRequest request) {\n",[267,918,919],{"class":269,"line":809},[267,920,764],{"emptyLinePlaceholder":763},[267,922,923],{"class":269,"line":815},[267,924,925],{},"        UserResponse response = userService.createUser(request);\n",[267,927,928],{"class":269,"line":821},[267,929,764],{"emptyLinePlaceholder":763},[267,931,932],{"class":269,"line":826},[267,933,934],{},"        return ResponseEntity\n",[267,936,937],{"class":269,"line":831},[267,938,939],{},"                .status(HttpStatus.CREATED)\n",[267,941,943],{"class":269,"line":942},19,[267,944,945],{},"                .body(response);\n",[267,947,949],{"class":269,"line":948},20,[267,950,897],{},[267,952,954],{"class":269,"line":953},21,[267,955,291],{},[17,957,958],{},"검증 흐름은 다음과 같다.",[17,960,961],{},[962,963,964],"strong",{},"Request Body 검증 흐름",[21,966,970],{"className":967,"code":968,"language":969,"meta":27,"style":27},"language-mermaid shiki shiki-themes github-light github-dark","flowchart TD\n    C[Client JSON 요청] --> J[Jackson이 DTO로 변환]\n    J --> V[@Valid 검증]\n    V -->|성공| CT[Controller 실행]\n    CT --> S[Service 실행]\n    V -->|실패| E[MethodArgumentNotValidException]\n    E --> R[400 Bad Request 응답]\n","mermaid",[29,971,972,977,982,987,992,997,1002],{"__ignoreMap":27},[267,973,974],{"class":269,"line":270},[267,975,976],{},"flowchart TD\n",[267,978,979],{"class":269,"line":276},[267,980,981],{},"    C[Client JSON 요청] --> J[Jackson이 DTO로 변환]\n",[267,983,984],{"class":269,"line":282},[267,985,986],{},"    J --> V[@Valid 검증]\n",[267,988,989],{"class":269,"line":288},[267,990,991],{},"    V -->|성공| CT[Controller 실행]\n",[267,993,994],{"class":269,"line":338},[267,995,996],{},"    CT --> S[Service 실행]\n",[267,998,999],{"class":269,"line":511},[267,1000,1001],{},"    V -->|실패| E[MethodArgumentNotValidException]\n",[267,1003,1004],{"class":269,"line":771},[267,1005,1006],{},"    E --> R[400 Bad Request 응답]\n",[17,1008,1009,1010,1013],{},"검증에 실패하면 보통 ",[29,1011,1012],{},"MethodArgumentNotValidException","이 발생하고 HTTP 400 응답으로 처리된다.",[32,1015],{},[35,1017,1019],{"id":1018},"_6-검증-오류-응답-통일하기","6. 검증 오류 응답 통일하기",[17,1021,1022,1025],{},[29,1023,1024],{},"@RestControllerAdvice","를 사용하면 여러 Controller의 예외를 한곳에서 처리할 수 있다.",[21,1027,1029],{"className":261,"code":1028,"language":263,"meta":27,"style":27},"@RestControllerAdvice\npublic class GlobalExceptionHandler {\n\n    @ExceptionHandler(MethodArgumentNotValidException.class)\n    public ResponseEntity\u003CMap\u003CString, String>> handleValidation(\n            MethodArgumentNotValidException exception) {\n\n        Map\u003CString, String> errors = new LinkedHashMap\u003C>();\n\n        exception.getBindingResult()\n                .getFieldErrors()\n                .forEach(error -> errors.put(\n                        error.getField(),\n                        error.getDefaultMessage()\n                ));\n\n        return ResponseEntity.badRequest().body(errors);\n    }\n}\n",[29,1030,1031,1036,1041,1045,1050,1055,1060,1064,1069,1073,1078,1083,1088,1093,1098,1103,1107,1112,1116],{"__ignoreMap":27},[267,1032,1033],{"class":269,"line":270},[267,1034,1035],{},"@RestControllerAdvice\n",[267,1037,1038],{"class":269,"line":276},[267,1039,1040],{},"public class GlobalExceptionHandler {\n",[267,1042,1043],{"class":269,"line":282},[267,1044,764],{"emptyLinePlaceholder":763},[267,1046,1047],{"class":269,"line":288},[267,1048,1049],{},"    @ExceptionHandler(MethodArgumentNotValidException.class)\n",[267,1051,1052],{"class":269,"line":338},[267,1053,1054],{},"    public ResponseEntity\u003CMap\u003CString, String>> handleValidation(\n",[267,1056,1057],{"class":269,"line":511},[267,1058,1059],{},"            MethodArgumentNotValidException exception) {\n",[267,1061,1062],{"class":269,"line":771},[267,1063,764],{"emptyLinePlaceholder":763},[267,1065,1066],{"class":269,"line":777},[267,1067,1068],{},"        Map\u003CString, String> errors = new LinkedHashMap\u003C>();\n",[267,1070,1071],{"class":269,"line":782},[267,1072,764],{"emptyLinePlaceholder":763},[267,1074,1075],{"class":269,"line":787},[267,1076,1077],{},"        exception.getBindingResult()\n",[267,1079,1080],{"class":269,"line":793},[267,1081,1082],{},"                .getFieldErrors()\n",[267,1084,1085],{"class":269,"line":799},[267,1086,1087],{},"                .forEach(error -> errors.put(\n",[267,1089,1090],{"class":269,"line":804},[267,1091,1092],{},"                        error.getField(),\n",[267,1094,1095],{"class":269,"line":809},[267,1096,1097],{},"                        error.getDefaultMessage()\n",[267,1099,1100],{"class":269,"line":815},[267,1101,1102],{},"                ));\n",[267,1104,1105],{"class":269,"line":821},[267,1106,764],{"emptyLinePlaceholder":763},[267,1108,1109],{"class":269,"line":826},[267,1110,1111],{},"        return ResponseEntity.badRequest().body(errors);\n",[267,1113,1114],{"class":269,"line":831},[267,1115,897],{},[267,1117,1118],{"class":269,"line":942},[267,1119,291],{},[17,1121,1122],{},"잘못된 요청을 보내면 다음과 같은 응답을 만들 수 있다.",[21,1124,1126],{"className":384,"code":1125,"language":386,"meta":27,"style":27},"{\n  \"name\": \"이름은 필수입니다.\",\n  \"email\": \"이메일 형식이 올바르지 않습니다.\"\n}\n",[29,1127,1128,1132,1143,1152],{"__ignoreMap":27},[267,1129,1130],{"class":269,"line":270},[267,1131,394],{"class":393},[267,1133,1134,1136,1138,1141],{"class":269,"line":276},[267,1135,400],{"class":399},[267,1137,403],{"class":393},[267,1139,1140],{"class":406},"\"이름은 필수입니다.\"",[267,1142,410],{"class":393},[267,1144,1145,1147,1149],{"class":269,"line":282},[267,1146,415],{"class":399},[267,1148,403],{"class":393},[267,1150,1151],{"class":406},"\"이메일 형식이 올바르지 않습니다.\"\n",[267,1153,1154],{"class":269,"line":288},[267,1155,291],{"class":393},[32,1157],{},[35,1159,1161],{"id":1160},"_7-직접-요청해보기","7. 직접 요청해보기",[21,1163,1167],{"className":1164,"code":1165,"language":1166,"meta":27,"style":27},"language-bash shiki shiki-themes github-light github-dark","curl -i -X POST 'http:\u002F\u002Flocalhost:8080\u002Fapi\u002Fusers' \\\n  -H 'Content-Type: application\u002Fjson' \\\n  -d '{\n    \"name\": \"민형\",\n    \"email\": \"min@example.com\",\n    \"age\": 25\n  }'\n","bash",[29,1168,1169,1190,1200,1208,1213,1218,1223],{"__ignoreMap":27},[267,1170,1171,1175,1178,1181,1184,1187],{"class":269,"line":270},[267,1172,1174],{"class":1173},"sScJk","curl",[267,1176,1177],{"class":399}," -i",[267,1179,1180],{"class":399}," -X",[267,1182,1183],{"class":406}," POST",[267,1185,1186],{"class":406}," 'http:\u002F\u002Flocalhost:8080\u002Fapi\u002Fusers'",[267,1188,1189],{"class":399}," \\\n",[267,1191,1192,1195,1198],{"class":269,"line":276},[267,1193,1194],{"class":399},"  -H",[267,1196,1197],{"class":406}," 'Content-Type: application\u002Fjson'",[267,1199,1189],{"class":399},[267,1201,1202,1205],{"class":269,"line":282},[267,1203,1204],{"class":399},"  -d",[267,1206,1207],{"class":406}," '{\n",[267,1209,1210],{"class":269,"line":288},[267,1211,1212],{"class":406},"    \"name\": \"민형\",\n",[267,1214,1215],{"class":269,"line":338},[267,1216,1217],{"class":406},"    \"email\": \"min@example.com\",\n",[267,1219,1220],{"class":269,"line":511},[267,1221,1222],{"class":406},"    \"age\": 25\n",[267,1224,1225],{"class":269,"line":771},[267,1226,1227],{"class":406},"  }'\n",[40,1229,1230,1240],{},[43,1231,1232],{},[46,1233,1234,1237],{},[49,1235,1236],{},"부분",[49,1238,1239],{},"의미",[59,1241,1242,1252,1262,1272],{},[46,1243,1244,1249],{},[64,1245,1246],{},[29,1247,1248],{},"-i",[64,1250,1251],{},"응답 Header까지 출력",[46,1253,1254,1259],{},[64,1255,1256],{},[29,1257,1258],{},"-X POST",[64,1260,1261],{},"POST Method 사용",[46,1263,1264,1269],{},[64,1265,1266],{},[29,1267,1268],{},"Content-Type",[64,1270,1271],{},"요청 본문이 JSON임을 알림",[46,1273,1274,1279],{},[64,1275,1276],{},[29,1277,1278],{},"-d",[64,1280,1281],{},"서버로 보낼 요청 본문",[32,1283],{},[35,1285,1287],{"id":1286},"_8-형식-검증과-비즈니스-검증","8. 형식 검증과 비즈니스 검증",[17,1289,1290,1292],{},[29,1291,842],{},"는 값의 기본 형식을 검사하는 데 적합하다.",[21,1294,1297],{"className":1295,"code":1296,"language":26,"meta":27},[24],"@NotBlank → 이름이 비어 있는가?\n@Email    → 이메일 형태가 맞는가?\n@Positive → 수량이 양수인가?\n",[29,1298,1296],{"__ignoreMap":27},[17,1300,1301],{},"하지만 DB 조회가 필요한 업무 규칙은 Service에서 검사하는 것이 자연스럽다.",[21,1303,1305],{"className":261,"code":1304,"language":263,"meta":27,"style":27},"@Transactional\npublic UserResponse createUser(UserCreateRequest request) {\n    if (userRepository.existsByEmail(request.email())) {\n        throw new DuplicateEmailException(request.email());\n    }\n\n    User user = User.create(\n            request.name(),\n            request.email(),\n            request.age()\n    );\n\n    return UserResponse.from(userRepository.save(user));\n}\n",[29,1306,1307,1312,1317,1322,1327,1331,1335,1340,1345,1350,1355,1360,1364,1369],{"__ignoreMap":27},[267,1308,1309],{"class":269,"line":270},[267,1310,1311],{},"@Transactional\n",[267,1313,1314],{"class":269,"line":276},[267,1315,1316],{},"public UserResponse createUser(UserCreateRequest request) {\n",[267,1318,1319],{"class":269,"line":282},[267,1320,1321],{},"    if (userRepository.existsByEmail(request.email())) {\n",[267,1323,1324],{"class":269,"line":288},[267,1325,1326],{},"        throw new DuplicateEmailException(request.email());\n",[267,1328,1329],{"class":269,"line":338},[267,1330,897],{},[267,1332,1333],{"class":269,"line":511},[267,1334,764],{"emptyLinePlaceholder":763},[267,1336,1337],{"class":269,"line":771},[267,1338,1339],{},"    User user = User.create(\n",[267,1341,1342],{"class":269,"line":777},[267,1343,1344],{},"            request.name(),\n",[267,1346,1347],{"class":269,"line":782},[267,1348,1349],{},"            request.email(),\n",[267,1351,1352],{"class":269,"line":787},[267,1353,1354],{},"            request.age()\n",[267,1356,1357],{"class":269,"line":793},[267,1358,1359],{},"    );\n",[267,1361,1362],{"class":269,"line":799},[267,1363,764],{"emptyLinePlaceholder":763},[267,1365,1366],{"class":269,"line":804},[267,1367,1368],{},"    return UserResponse.from(userRepository.save(user));\n",[267,1370,1371],{"class":269,"line":809},[267,1372,291],{},[40,1374,1375,1385],{},[43,1376,1377],{},[46,1378,1379,1382],{},[49,1380,1381],{},"검증 위치",[49,1383,1384],{},"담당할 규칙",[59,1386,1387,1394,1402],{},[46,1388,1389,1391],{},[64,1390,532],{},[64,1392,1393],{},"빈 값, 길이, 숫자 범위, 이메일 형식",[46,1395,1396,1399],{},[64,1397,1398],{},"Service",[64,1400,1401],{},"이메일 중복, 재고 부족, 권한 등 업무 규칙",[46,1403,1404,1407],{},[64,1405,1406],{},"Database",[64,1408,1409,147,1412,1415],{},[29,1410,1411],{},"NOT NULL",[29,1413,1414],{},"UNIQUE",", 외래키 등 최종 무결성",[17,1417,1418],{},"검증을 한 계층에만 의존하지 않고 각 계층이 책임져야 할 규칙을 나누는 것이 중요하다.",[32,1420],{},[35,1422,1424],{"id":1423},"핵심-정리","핵심 정리",[40,1426,1427,1437],{},[43,1428,1429],{},[46,1430,1431,1434],{},[49,1432,1433],{},"개념",[49,1435,1436],{},"핵심 내용",[59,1438,1439,1448,1457,1466,1473,1482],{},[46,1440,1441,1445],{},[64,1442,1443],{},[29,1444,207],{},[64,1446,1447],{},"URL 경로의 식별자를 받는다.",[46,1449,1450,1454],{},[64,1451,1452],{},[29,1453,226],{},[64,1455,1456],{},"Query String의 검색 조건을 받는다.",[46,1458,1459,1463],{},[64,1460,1461],{},[29,1462,244],{},[64,1464,1465],{},"JSON 요청 본문을 자바 객체로 변환한다.",[46,1467,1468,1470],{},[64,1469,116],{},[64,1471,1472],{},"API와 Entity의 구조를 분리한다.",[46,1474,1475,1479],{},[64,1476,1477],{},[29,1478,842],{},[64,1480,1481],{},"DTO에 선언한 Bean Validation을 실행한다.",[46,1483,1484,1488],{},[64,1485,1486],{},[29,1487,1024],{},[64,1489,1490],{},"여러 Controller의 오류 응답을 통일한다.",[14,1492,1493],{},[17,1494,1495],{},"Controller는 요청값을 받고 검증하는 입구이며, 실제 비즈니스 규칙은 Service에 맡긴다.",[35,1497,1499],{"id":1498},"참고-자료","참고 자료",[1501,1502,1503,1513],"ul",{},[1504,1505,1506],"li",{},[1507,1508,1512],"a",{"href":1509,"rel":1510},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Freference\u002Fcore\u002Fvalidation\u002Fbeanvalidation.html",[1511],"nofollow","Spring Framework Validation 공식 문서",[1504,1514,1515],{},[1507,1516,1519],{"href":1517,"rel":1518},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Freference\u002Fweb\u002Fwebmvc.html",[1511],"Spring Framework Web MVC 공식 문서",[1521,1522,1523],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":27,"searchDepth":276,"depth":276,"links":1525},[1526],{"id":12,"depth":276,"text":5,"children":1527},[1528,1529,1530,1531,1532,1534,1535,1536,1537,1538],{"id":37,"depth":282,"text":38},{"id":182,"depth":282,"text":183},{"id":447,"depth":282,"text":448},{"id":562,"depth":282,"text":563},{"id":838,"depth":282,"text":1533},"5. @Valid로 검증 실행하기",{"id":1018,"depth":282,"text":1019},{"id":1160,"depth":282,"text":1161},{"id":1286,"depth":282,"text":1287},{"id":1423,"depth":282,"text":1424},{"id":1498,"depth":282,"text":1499},[263,1540],"spring","2026-08-15 09:00:00 +0900","PathVariable, RequestParam, RequestBody로 요청값을 받고 DTO와 Bean Validation으로 검증하는 흐름을 정리한다.","md",null,"\u002Fjava\u002Fspring\u002F2026\u002F08\u002F15\u002Fspring-rest-api-validation\u002F",{"layout":1547},"post","3","\u002Fposts\u002Fbackend\u002Fspring\u002Fspring\u002F2026-08-15-spring-rest-api-validation",{"title":5,"description":1542},"Spring 기초","posts\u002FBackend\u002FSPRING\u002FSpring\u002F2026-08-15-spring-rest-api-validation",[1554,1555,1556,1557,127,116,1558],"Java","Spring","Spring Boot","REST API","RequestBody","zk9iFoZAh1ZHjX79zNGggSf2wLd4y_9vPGhAndGODS8",1788744786176]