모델 바인딩과 유효성 검사
들어온 값을 형식에 담고 옳은지 봅니다.
담는 일과 보는 일은 다릅니다
요청에 실려 오는 것은 전부 글자입니다. 주소의 7 도,
본문의 JSON 도 그렇습니다. 그것을 int 나 우리가 만든
형식에 옮겨 담는 것이 모델 바인딩입니다.
담기는 것과 담긴 값이 옳은 것은 다릅니다. 9 는 숫자라
int 에 담기지만, 중요도가 1에서 5 사이여야 한다면
옳은 값이 아닙니다. 그것을 보는 것이 유효성 검사입니다.
둘은 차례로 일어납니다. 담지 못하면 검사까지 가지 않습니다. 둘 다 실패하면 400 이지만 돌아오는 내용이 다릅니다.
규칙을 형식에 적습니다
검사 규칙은 코드로 적지 않고 속성 위에 붙입니다. 어떤 값이 허용되는지가 그 형식을 보면 드러납니다.
using System.ComponentModel.DataAnnotations; public class TodoInput { [Required(ErrorMessage = "제목은 반드시 적어야 합니다.")] [StringLength(20, MinimumLength = 2, ErrorMessage = "제목은 2자에서 20자 사이입니다.")] public string? Title { get; set; } [Range(1, 5, ErrorMessage = "중요도는 1에서 5 사이입니다.")] public int Priority { get; set; } [EmailAddress(ErrorMessage = "전자 메일 모양이 아닙니다.")] public string? Notify { get; set; } } // ───────────────────────────── [HttpPost] public IActionResult Add(TodoInput input) => Ok(input);
이 코드는 서버가 있어야 하므로 브라우저에서 실행할 수 없습니다. 아래에서 같은 규칙을 실행해 볼 수 있습니다.
마지막 것을 눈여겨보십시오. 틀린 곳이 셋이었는데 셋을 모두 돌려주었습니다. 처음 하나에서 멈추지 않습니다. 부르는 쪽이 한 번에 고칠 수 있어야 하기 때문입니다.
같은 규칙을 서버 없이
[ApiController] 가 부르는 것이
Validator 입니다. 직접 부르면 서버 없이도 같은 판정을
볼 수 있습니다.
using System.ComponentModel.DataAnnotations; void 검사(TodoInput input) { var errors = new List<ValidationResult>(); var ok = Validator.TryValidateObject(input, new ValidationContext(input), errors, true); Console.WriteLine(ok ? "통과" : "막힘"); foreach (var e in errors) Console.WriteLine(" " + string.Join(", ", e.MemberNames) + " : " + e.ErrorMessage); } 검사(new TodoInput { Title = "우유 사기", Priority = 3 }); 검사(new TodoInput { Title = "우", Priority = 9, Notify = "abc" }); class TodoInput { [Required(ErrorMessage = "제목은 반드시 적어야 합니다.")] [StringLength(20, MinimumLength = 2, ErrorMessage = "제목은 2자에서 20자 사이입니다.")] public string? Title { get; set; } [Range(1, 5, ErrorMessage = "중요도는 1에서 5 사이입니다.")] public int Priority { get; set; } [EmailAddress(ErrorMessage = "전자 메일 모양이 아닙니다.")] public string? Notify { get; set; } }
코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.
마지막 인수 true 를 빼면
속성에 붙인 것을 보지 않습니다. 형식 자체에 붙인 것만 봅니다.
빠뜨리기 쉬운 자리이므로 규칙을 붙였는데 통과한다면 여기를 먼저 보십시오.
값이 어디서 오는지 적을 수 있습니다
2.1에서 본 대로 적지 않아도 알아서 정합니다. 다만 이름이 겹치거나 다른 곳에서 가져와야 할 때는 직접 적습니다.
[HttpGet("{id:int}/where")] public object Where( [FromRoute] int id, [FromQuery] string? q, [FromHeader(Name = "X-Trace")] string? trace) => new { id, q, trace };
| 어트리뷰트 | 가져오는 곳 |
|---|---|
| FromRoute | 주소의 자리표 |
| FromQuery | 질의 문자열 |
| FromBody | 본문(JSON). 한 매개 변수에만 붙일 수 있습니다. |
| FromForm | 폼으로 보낸 값. 파일 올리기가 이쪽입니다. |
| FromHeader | 요청 머리글 |
| FromServices | 등록해 둔 서비스 |
자주 사용하는 검사 규칙
| 규칙 | 보는 것 |
|---|---|
| Required | 값이 있어야 합니다. |
| StringLength | 글자 수의 위아래를 정합니다. |
| Range | 숫자와 날짜의 범위를 정합니다. |
| EmailAddress · Url · Phone | 정해진 모양인지 봅니다. |
| RegularExpression | 직접 적은 모양과 맞는지 봅니다. |
| Compare | 다른 속성과 같은지 봅니다. 비밀번호 확인이 이것입니다. |
ErrorMessage 를 적지 않으면 영어 문구가 나갑니다.
사용자에게 그대로 보일 수 있으므로 적어 두는 편이 낫습니다.
물음표 없는 문자열은 이미 규칙입니다
Required 를 붙이지 않아도 막히는 자리가 있습니다.
nullable 을 켜 둔 프로젝트에서 물음표 없이 선언한 문자열이
그렇습니다. 값을 보내지 않으면 400 입니다.
public class NoInit { public string Name { get; set; } // 물음표 없음 public string? Nickname { get; set; } }
그런데 무엇을 막는지가 짐작과 다릅니다. 네 가지를 보내 보았습니다.
{} // 아예 없음
{"name": null} // null 이라고 적음
{"name": ""} // 빈 문자열
{"name": "홍길동"} // 값이 있음
빈 문자열은 지나갑니다. 막는 것은 null
뿐입니다. 제목이 비어 있으면 안 된다면 이것만으로는 모자라므로,
[Required] 나
[StringLength(MinimumLength = 1)] 을 함께 적어야 합니다.
한 가지가 더 있습니다. 선언에 기본값을 주어 두면 아예 막지
않습니다. 값을 보내지 않아도 그 기본값이 남아 null
이 되지 않기 때문입니다.
public string Name { get; set; } = ""; // {} 를 보내도 200, name 은 ""
컴파일러 경고를 없애려고 = "" 를 붙이는 일이 흔한데,
그때 검사도 함께 사라집니다. 입력을 받는 형식에서는 물음표를 붙여
string? 로 두고 규칙을 어트리뷰트로 적는 편이 뜻이
분명합니다.
언제부터 있었나
물음표 없는 참조 형식을 Required 로 보는 것은
nullable 참조 형식(C# 8.0)이 들어온 뒤에 생겼습니다. 그 전에는 물음표에
아무 뜻이 없어 검사에 사용할 수 없었습니다.
이 동작이 달갑지 않다면 끌 수 있습니다.
SuppressImplicitRequiredAttributeForNonNullableReferenceTypes
를 켜면 어트리뷰트로 적은 것만 봅니다. 옛 코드를 옮겨 올 때 사용합니다.
직접 해보기
위 실행 예제의 TodoInput 에 마감일을 더하고,
오늘 이후여야 한다는 규칙을 붙여보세요.
Range 로도 되지만 오늘을 어트리뷰트에 적을 수 없습니다. 형식 자체에 규칙을 두는 방법을 생각해보세요.
public string Title { get; set; } 만으로는 빈
문자열이 지나갑니다. 공백만 적은 것도 막으려면 어떻게 해야 할지 적어보세요.
StringLength 로는 막히지 않습니다.{"priority":"셋"} 과 {"priority":9} 는 둘 다 400 입니다. 무엇이 다른지, 그리고 응답에서 어떻게 알아볼 수 있는지 적어보세요.
- 담는 일(바인딩)과 보는 일(검사)은 차례로 일어납니다. 담지 못하면 검사까지 가지 않습니다.
- 규칙은 코드가 아니라 속성 위 어트리뷰트로 적습니다. 틀린 곳이 여럿이면 한꺼번에 돌려줍니다.
- 서버 없이 확인하려면
Validator.TryValidateObject를 직접 부릅니다. 마지막 인수를true로 두어야 속성의 규칙을 봅니다. - 물음표 없는 문자열은 null 만 막고 빈 문자열은 지나갑니다.
- 선언에
= ""를 붙이면 그 검사가 사라집니다.