HTTP 요청/응답 DTO
DTO(Data Transfer Object)
- 데이터를 전송하기 위한 객체
- API Layer - Controller 예시 코드의 경우 요청 데이터가 늘어날 경우 @RequestParam 애너테이션의 개수도 증가함
- DTO를 통해 클라이언트의 요청 데이터를 하나의 객체로 모두 전달 가능
- 유효성 검사를 DTO내에서 실행해 핸들러 메서드의 간결함을 유지 가능
- HTTP 요청 수를 줄이기 위해 사용
DTO 구현
@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);
}
@PatchMapping("/{member-id}")
public ResponseEntity patchMember(@PathVariable("member-id") long memberId,
@RequestParam String phone) {
Map<String, Object> body = new HashMap<>();
body.put("memberId", memberId);
body.put("email", "hgd@gmail.com");
body.put("name", "홍길동");
body.put("phone", phone);
return new ResponseEntity<Map>(body, HttpStatus.OK);
}
@GetMapping("/{member-id}")
public ResponseEntity getMember(@PathVariable("member-id") long memberId) {
System.out.println("# memberId: " + memberId);
return new ResponseEntity<Map>(HttpStatus.OK);
}
@GetMapping
public ResponseEntity getMembers() {
System.out.println("# get Members");
return new ResponseEntity<Map>(HttpStatus.OK);
}
@DeleteMapping("/{member-id}")
public ResponseEntity deleteMember(@PathVariable("member-id") long memberId) {
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
}
public class MemberPostDTO {
private String email;
private String name;
private String phone;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
public class MemberPatchDTO {
private long memberId;
private String name;
private String phone;
public long getMemberId() {
return memberId;
}
public void setMemberId(long memberId) {
this.memberId = memberId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
@RestController
@RequestMapping("/v1/members")
public class MemberController {
@PostMapping
public ResponseEntity postMember(@RequestBody MemberPostDTO memberPostDTO) {
return new ResponseEntity<>(memberPostDTO, HttpStatus.CREATED);
}
@PatchMapping("/{member-id}")
public ResponseEntity patchMember(@PathVariable("member-id") long memberId,
@Valid @RequestBody MemberPatchDTO memberPatchDTO) {
memberPatchDTO.setMemberId(memberId);
return new ResponseEntity(memberPatchDTO, HttpStatus.OK);
}
@GetMapping("/{member-id}")
public ResponseEntity getMember(@PathVariable("member-id") long memberId) {
System.out.println("# memberId: " + memberId);
return new ResponseEntity<>(HttpStatus.OK);
}
@GetMapping
public ResponseEntity getMembers() {
System.out.println("# get Members");
return new ResponseEntity<>(HttpStatus.OK);
}
@DeleteMapping("/{member-id}")
public ResponseEntity deleteMember(@PathVariable("member-id") long memberId) {
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
}
- 각 멤버 변수에 해당하는 getter 메서드가 있어야 함
- HTTP Request Body가 JSON 형식이어야 하기 때문에 클라이언트 쪽에서 전달하는 Request Body도 JSON 형식으로 입력
- @ReuestBody 애너테이션
- JSON 형식의 Request Body를 DTO 클래스의 객체로 변환시켜줌(역직렬화)
- @ResponseBody 애너테이션
- DTO 클래스의 객체를 JSON 형식의 Response Body로 변환(직렬화)
- 핸들러 메서드의 반환값이 ResponseEntity인 경우 생략 가능
- @ResponseBody 애너테이션이 붙거나 핸들러 메서드의 반환 값이 ResponseEntity인 경우 내부적으로 HTTPMessageConverter가 동작하여 응답 객체를 JSON 형식으로 바꿔줌
DTO 유효성 검증(Validation)
Jakarta Bean Validation
- API가 아닌 스펙
- 해당 스펙의 구현체가 Hibernate Validator
- Java Bean 스펙을 준수하는 Java 클래스라면 사용 가능
1. 예시 코드
- 유효성 검증을 적용한 MemberPostDTO class
public class MemberPatchDTO {
private long memberId;
@Pattern(regexp = "^\\S+(\\s?\\S+)*$", message = "회원 이름은 공백이 아니어야 합니다.")
private String name;
@Pattern(regexp = "^010-\\d{3,4}-\\d{4}$",
message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다.")
private String phone;
public long getMemberId() {
return memberId;
}
public void setMemberId(long memberId) {
this.memberId = memberId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
- 유효성 검증을 적용한 MemberPatchDTO class
public class MemberPatchDTO {
private long memberId;
@Pattern(regexp = "^\\S+(\\s?\\S+)*$", message = "회원 이름은 공백이 아니어야 합니다.")
private String name;
@Pattern(regexp = "^010-\\d{3,4}-\\d{4}$",
message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다.")
private String phone;
public long getMemberId() {
return memberId;
}
public void setMemberId(long memberId) {
this.memberId = memberId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
- 유효성 검증을 적용한 MemberController class
@RestController
@RequestMapping("/v1/members")
@Validated
public class MemberController {
@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberPostDTO memberPostDTO) {
return new ResponseEntity<>(memberPostDTO, HttpStatus.CREATED);
}
@PatchMapping("/{member-id}")
public ResponseEntity patchMember(@PathVariable("member-id") @Min(1) long memberId,
@Valid @RequestBody MemberPatchDTO memberPatchDTO) {
memberPatchDTO.setMemberId(memberId);
return new ResponseEntity(memberPatchDTO, HttpStatus.OK);
}
@GetMapping("/{member-id}")
public ResponseEntity getMember(@PathVariable("member-id") long memberId) {
System.out.println("# memberId: " + memberId);
return new ResponseEntity<>(HttpStatus.OK);
}
@GetMapping
public ResponseEntity getMembers() {
System.out.println("# get Members");
return new ResponseEntity<>(HttpStatus.OK);
}
@DeleteMapping("/{member-id}")
public ResponseEntity deleteMember(@PathVariable("member-id") long memberId) {
return new ResponseEntity(HttpStatus.NO_CONTENT);
}
}
2. 필드 유효성 검증 애너테이션
- @Valid : 핸들러 메서드에 추가하여 유효성 검증
- @Email
- @NotBlank
- 데이터가 비어있지 않은지
- null, 공백(""), 스페이스(" ") 허용하지 않음
- @Pattern
- @Pattern(regexp = "^\S+(\s?\S+)*$")
- 정규표현식에 매치되는지 검증
- null인 경우 유효성 검사 수행하지 않음
- 문자열 필드에 적용
- 정규표현식
- '^' : 문자열의 시작
- '$' : 문자열의 끝
- '*' : 앞에 평가할 대상이 0개 또는 1개 이상인지
- '\s' : 앞에 평가할 대상이 공백 문자열인지
- '\S' : 앞에 평가할 대상이 공백이 아닌 문자열인지
- '?' : 앞에 평가할 대상이 0개 또는 1개 인지
- '+' : 앞에 평가할 대상이 1개인지
- 정규표현식 예시
- 숫자 검사 : "^[0-9]*$"
- 영어 검사 : "^[a-zA-Z]*$"
- 한글 검사 : "[가-힣]*$"
- 정수형일 경우 Integer 타입으로 @Min, @Max 혹은 @Range(min = 1, max = 10) 등으로 제한 가능
3. 쿼리 파라미터, @Pathvariable 유효성 검증
- @Validated : 클래스 레벨에 사용하여 유효성 검증
- 핸들러 메서드에 @Min(1) 등으로 제약 가능
Custom Validator 유효성 검사
공백을 허용하지 않는 Custom Validator 구현
1. Custom Validator를 사용하기 위한 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)
@Constraint(validatedBy = {NotSpaceValidator.class})
public @interface NotSpace {
String message() default "공백이 아니어야 합니다";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- (1) : 해당 애너테이션이 멤버 변수에 추가됐을때 동작할 Custom Validator를 추가
- (2) : 검증 실패 시, 표시되는 디폴트 메시지
2. 정의한 Custom Annotation에 바인딩되는 Custom Validator를 구현
import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
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);
}
}
- ConstraintValidator<NotSpace, String>
- NotSpace는 CustomValidator와 매핑된 Custom Annotation을 의미
- String은 Custom Annotation으로 검증할 대상의 멤버 변수 타입
3. 유효성 검증이 필요한 DTO 클래스의 멤버 변수에 Custom Annotation을 추가한다.
public class MemberPatchDto {
private long memberId;
@NotSpace(message = "회원 이름은 공백이 아니어야 합니다")
private String name;
@Pattern(regexp = "^010-\\d{3,4}-\\d{4}$",
message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다")
private String phone;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public long getMemberId() {
return memberId;
}
public void setMemberId(long memberId) {
this.memberId = memberId;
}
}
- @Pattern(regexp ="^\S+(\s?\S+)*$") 대신 구현한 Custom Annotation @NotSpace 추가