DTO는 Data Transfer Object의 약자로 마틴 파울러(Martin Fowler)가 ‘Patterns of Enterprise Application Architecture’ 라는 책에서 처음 소개한 엔터프라이즈 애플리케이션 아키텍처 패턴의 하나이다.
DTO 클래스가 요청 데이터를 하나의 객체로 전달 받는 역할을 해주어 코드가 매우 간결해진다.
서버 쪽에서 유효한 데이터를 전달 받기 위해 데이터를 검증하는 것을 유효성(Validation)검증이라고 한다.
HTTP 요청을 전달 받는 핸들러 메서드는 요청을 전달 받는 것이 주 목적이기 때문에 최대한 간결하게 작성되는 것이 좋다.
DTO 클래스를 사용하면 유효성 검증 로직을 DTO 클래스로 빼내어 핸들러 메서드의 간결함을 유지할 수 있다.
DTO 클래스를 사용하는 가장 중요한 목적은 비용이 많이 드는 작업인 HTTP 요청의 수를 줄이기 위함이라고 할 수 있다.
public class MemberPostDto {
private String email;
private String korName;
public String getEmail() {
return email;
}
public String getKorName() {
return korName;
}
}
클라이언트에서 전달하는 요청 데이터를 @RequestParam 애너테이션으로 전달 받는 핸들러 메서드 탐색한다.
@RequestParam의 코드를 DTO 클래스의 객체로 변경한다.
Map 객체로 작성된 Request Body를 DTO 클래스의 객체로 변경한다.
// 2), 3), 4) 변경 전
@RestController
@RequestMapping("/v1/members")
public class MemberController {
@PostMapping
public ResponseEntity postMember(@RequestParam("email") String email,
@RequestParam("name") String name,
@RequestParam("phone") String phone) {
Map<String, String> body = new HashMap<>();
body.put("email", email);
body.put("name", name);
body.put("phone", phone);
return new ResponseEntity<Map>(body, HttpStatus.CREATED);
}
}
// 2), 3), 4) 변경 후
@RestController
@RequestMapping("/v1/members")
public class MemberController {
@PostMapping
public ResponseEntity postMember(@RequestBody MemberPostDto memberPostDto) {
return new ResponseEntity<>(memberPostDto, HTTPStatus.CREATED);
}
}
일반적으로 프론트엔드 쪽에서 유효성 검사를 진행하여 통과한 뒤, 서버 쪽으로 HTTP Request 요청이 전송된다.
하지만 자바스크립트로 전송되는 데이터는 브라우저의 개발자 도구를 사용해서 브레이크포인트(breakpoint)를 추가한 뒤에 얼마든지 그 값을 조작할 수 있기 때문에 프론트엔드 쪽에서 유효성 검사를 진행했다고 하더라고 서버 쪽에서 한 번 더 유효성 검사를 진행해야 한다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
...
...
}
유효성 검증 요구 사항에 맞게 제약 사항을 작성한다.
Ex) email : 값이 비어있지 않거나 공백이 아니어야 한다.
요청으로 전달 받는 DTO 클래스의 각 멤버 변수에 유효성 검증을 위한 애너테이션을 추가한다.
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
public class MemberPostDto {
@NotBlank
@Email
private String email;
@NotBlank(message = "이름은 공백이 아니어야 합니다.")
private String korName;
@Pattern(regexp = "^010-\\d{3,4}-\\d{4}$",
message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다.")
private String phoneNumber;
...
... // 각 멤버 변수의 getter, setter 메서드 생략
}
Controller 클래스의 핸들러 메서드 내 URI path에서 사용되는 @Pathvariable 및 이하 변수 또한 유효성 검증 대상이다.
클래스 레벨에 @Validated 애너테이션이 추가하고, @Pathvariable 애너테이션에 검증 애너테이션을 추가한다.
@RestController
@RequestMapping("/v1/members")
@Validated
public class MemberController {
...
...
@PatchMapping("/{member-id}")
// @Min 애너테이션 : 1 이상의 숫자일 경우에만 유효성 검증에 통과
public ResponseEntity patchMember(@PathVariable("member-id") @Min(1) long memberId,
@Valid @RequestBody MemberPatchDto memberPatchDto) {
memberPatchDto.setMemberId(memberId);
return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
}
}
Jakarta Bean Validation에 내장된(Built-in) 애너테이션 중에 여러분들의 목적에 맞는 애너테이션이 존재하지 않을 때 목적에 맞는 애너테이션을 직접 만들어 유효성 검증에 적용 가능하다.
// Custom Annotation 정의
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
// NotSpace 애너테이션이 멤버 변수에 추가되었을 때 동작할 Custom Validator를 추가
@Constraint(validatedBy = {NotSpaceValidator.class})
public @interface NotSpace {
// 유효성 검증 실패 시 표시되는 디폴트 메시지 지정
String message() default "공백이 아니어야 합니다";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// Custom Validator 구현
import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
// CustomValidator를 구현하기 위해서는 ContraintValidator 인터페이스를 구현해야 함
// ConstraintValidator<NotSpace, String>에서
// NotSpace는 CustomValidator와 매핑된 Custom Annotation을 의미
// String은 Custom Annotation으로 검증할 멤버 변수의 타입을 의미
public class NotSpaceValidator implements ConstraintValidator<NotSpace, String> {
@Override
public void initialize(NotSpace constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value == null || StringUtils.hasText(value);
}
}
// 유효성 검증을 위해 DTO 클래스의 멤버 변수에 Custom Annotation 추가
import javax.validation.constraints.Pattern;
public class MemberPatchDto {
private long memberId;
// message 애트리뷰트를 사용하여 유효성 검증 실패 시 표시되는 메시지 지정
@NotSpace(message = "회원 이름은 공백이 아니어야 합니다")
private String name;
@Pattern(regexp = "^010-\\d{3,4}-\\d{4}$",
message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다")
private String phone;
...
... // 각 멤버 변수의 getter, setter 메서드 생략
}