📌 DTO
DTO(Data Transfer Object)는 엔터프라이즈 애플리케이션 아키텍처 패턴의 하나 중 하나로, Transfer이라는 의미에서 알 수 있듯이 데이터를 전송하기 위한 용도의 객체를 말한다.
✔️ 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> map = new HashMap<>();
map.put("email", email);
map.put("name", name);
map.put("phone", phone);
return new ResponseEntity<Map>(map, HttpStatus.CREATED);
}
...
...
}
postMember()에서 개선해야 될 부분을 살펴보면서 DTO가 필요한 이유를 확인해보도록 하자.
예제에서 회원 정보를 저장하기 위해서 총 세 개의 @RequestParam 애너테이션을 사용하고 있다. 그런데 지금은 요청 데이터가 세 개밖에 없지만 실제로는 회원의 주소 정보, 로그인 패스워드, 패스워드 확인 정보 등 더 많은 정보들이 회원 정보에 포함되어 있을 수 있다. 따라서 postMember()에 파라미터로 추가되는 @RequestParam의 개수는 계속 늘어날 수 밖에 없다.
이 경우, 클라이언트의 요청 데이터를 하나의 객체로 모두 전달 받을 수 있다면 코드 자체가 굉장히 간결해진다.
DTO 클래스가 바로 요청 데이터를 하나의 객체로 전달 받는 역할을 해준다.
위의 예제를 DTO 클래스를 적용해서 바꾼다면 아래와 같은 모습이 된다.
@RestController
@RequestMapping("/v1/members")
public class MemberController {
@PostMapping
public ResponseEntity postMember(MemberDto memberDto) {
return new ResponseEntity<MemberDto>(memberDto, HttpStatus.CREATED);
}
...
...
}
postMember()에서 @RequestParam을 사용하는 부분이 사라지고 memberDto 가 추가되었다. 그리고 아직 비즈니스 로직이 없긴하지만 어쨌든 @RequestParam을 통해 전달 받은 요청 데이터들을 Map에 추가하는 로직이 사라지고, MemberDto 객체를 ResponseEntity 클래스의 생성자 파라미터로 전달하도록 변경되었다.
결과적으로 DTO 클래스를 사용하니 코드 자체가 매우 간결해진 것을 알 수 있다.
✔️ 데이터 유효성 검증의 단순화
DTO 클래스를 활용하면 클라이언트 요청 데이터에 대한 유효성 검증 작업(이메일, 전화번호 양식 등)을 핸들러 메서드에서 분리할 수 있어 유효성 검증 코드를 좀 더 단순화할 수 있다.
다음과 같이 이전의 코드에서 유효한 이메일 주소인지를 검증할 수 있다.
@RestController
@RequestMapping("/no-dto-validation/v1/members")
public class MemberController {
@PostMapping
public ResponseEntity postMember(@RequestParam("email") String email,
@RequestParam("name") String name,
@RequestParam("phone") String phone) {
// (1) email 유효성 검증
if (!email.matches("^[a-zA-Z0-9_!#$%&'\\*+/=?{|}~^.-]+@[a-zA-Z0-9.-]+$")) {
throw new InvalidParameterException();
}
Map<String, String> map = new HashMap<>();
map.put("email", email);
map.put("name", name);
map.put("phone", phone);
return new ResponseEntity<Map>(map, HttpStatus.CREATED);
}
...
...
}
📌 Jakarta Bean Validation
자바에서는 입력받는 데이터의 유효성을 검사할 수 있는 유효성 검증 애너테이션(Jakarta bean validation)을 지원한다.
템플릿화된 유효성 검증 애너테이션을 사용하면 DTO 클래스에 별도의 유효성 검증 코드를 작성하지 않아도 되서 코드가 좀 더 간결해진다.
다음과 같이 애너테이션을 사용해볼 수 있다.
✔️ 의존성 추가
스프링 애너테이션(@Validated)을 사용하기 위해선 build.gradle 파일에 다음과 같은 의존 라이브러리를 추가해야 한다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
...
...
}
✔️ @Validated 애너테이션 활성화
의존성 주입받은 DTO와 같은 별도의 클래스 객체를 유효성 검증하려면 스프링의 @Validated 애너테이션을 붙여주어야 한다. 해당 클래스 객체의 유효성 검사는 @Vaild를 붙여 검사한다.
@RestController
@RequestMapping("/v1/coffees")
@Validated // 컨트롤러의 유효성 검사를 위한 @Validated 애너테이션 활성화
public class CoffeeController {
// @Vaild를 통한 유효성 검사
@PostMapping
public ResponseEntity postCoffee(@Valid @RequestBody PostCoffeeDto postCoffeeDto) {
return new ResponseEntity<>(postCoffeeDto, HttpStatus.CREATED);
}
...
...
}
✔️ 유효성 검증 애너테이션 사용
다음과 같이 멤버 변수에 유효성 검증 애너테이션을 붙여 유효성 검증이 가능하다.
또한 유효성 검증을 위해 Getter 메서드가 정의되어 있어야 한다.
@Getter
public class MemberDto {
@NotBlank
@Email
private String email;
@NotBlank(message = "이름이 공백이 아니어야 합니다.")
private String name;
@Pattern(regexp = "^010-\\d{3,4}-\\d{4}$",
message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다.")
private String phone;
...
...
}
✔️ 유효성 검증 애너테이션의 종류
Annotation (@) | 대상 객체 | 설명 |
@Size | String | 문자 길이에 대한 조건 |
@NotNull | - | 해당 파라미터 값이 null이면 안됨 |
@NotEmpty | - | 1. 해당 파라미터 값이 null이면 안됨 2. "" 문자열도 안됨 |
@NotBlank | - | 1. 해당 파라미터 값이 null이면 안됨 2. "" 문자열도 안됨 3. " " 문자열도 안됨(공백) |
@Past | - | 날짜가 과거인지 체크 |
@PastOrPresent | - | 날짜가 현재 또는 과거의 날짜인지 체크 |
@Future | - | 날짜가 미래인지 체크 |
@FutureOrPresent | - | 날짜가 현재 또는 미래의 날짜인지 체크 |
@Pattern | - | 정규식을 통한 형식 체크 |
@Max | - | 최대 값 조건 설정 |
@Min | - | 최소 값 조건 설정 |
@AssertTrue | - | 참 조건 설정 |
@AssertFalse | - | 거짓 조건 설정 |
@DecimalMax | 실수를 제외한 숫자 | 지정된 값보다 작거나 같은지 체크 |
@DecimaMin | 실수를 제외한 숫자 | 지정된 값보다 크거나 같은지 체크 |
String | 올바른 형식의 이메일 주소인지 체크 | |
@Negative | - | 음수인지 체크 |
@NegativeOrZero | - | 음수 또는 0인지 체크 |
@Positive | - | 양수인지 체크 |
@PositiveOrZero | - | 양수 또는 0인지 체크 |
@Valid | - | 해당 객체의 유효성 검사 |
📌 Cunstom Validator
자바에 내장된 유효성 검증 애너테이션(Jakarta Bean Validation) 이외에 개발자의 목적에 맞는 애너테이션(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}) // (1)
public @interface NotSpace {
String message() default "공백이 아니어야 합니다"; // (2)
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- @interface 애너테이션을 통해 애너테이션 인터페이스를 작성할 수 있다.
- @Target 애너테이션을 통해 애너테이션을 적용할 위치를 지정할 수 있다.
- @Retention 애너테이션을 통해 컴파일러가 애너테이션을 다루는 방법, 시점을 지정할 수 있다.
→ 지정 시 ConstraintValidator<애너테이션 인터페이스명, String> 인터페이스를 상속받은 유효성 검증 클래스에 유효성 검증 메서드 (initialize() , isValid())를 구현해야한다. - @Constraint 애너테이션을 통해 유효성 검증 메서드를 구현할 클래스를 지정할 수 있다.
- 애너테이션 인터페이스의 기본 구성 요소
→ message() : 실패 혹은 기본 메시지 값을 설정한다.
→ groups() : 특정 validation을 사용하는 group을 지정한다.
→ payload() : 사용자가 추가 정보를 위해 전달할 수 있는 값으로 심각도(주의, 위험 등)를 나타낼 때 사용한다.
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);
}
}
@Constraint에서 지정한 유효성 검증 클래스는 ConstraintValidator<애너테이션 인터페이스명, String>을 상속받아 메서드 오버라이딩을 통해 initialize() 메서드와 isValid() 메서드를 구현해야 한다.
3. 유효성 검증이 필요한 DTO 클래스의 멤버 변수에 Custom Annotation을 추가한다.
import javax.validation.constraints.Pattern;
public class MemberPatchDto {
private long memberId;
@NotSpace(message = "회원 이름은 공백이 아니어야 합니다") // (1)
private String name;
@NotSpace(message = "휴대폰 번호는 공백이 아니어야 합니다") // (2)
@Pattern(regexp = "^010-\\d{3,4}-\\d{4}$",
message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다")
private String phone;
...
...
유효성 검증을 하고자하는 DTO 클래스의 멤버 변수에 커스텀 애너테이션(@NotSpace)을 붙혀 유효성 검사를 진행할 수 있다.
📌 JSON 통신 프로토콜을 위한 애너테이션
스프리에선 JSON 통신 프로토콜을 위해 객제나 JSON 데이터를 직렬화/역직렬화해주는 애너테이션을 제공한다.
※ 직렬화(Serialization) : Java 객체 → JSON
※ 역직렬화(Deserialization) : JSON → Java 객체
✔️ @RequestBody
요청받은 JSON 데이터를 객체로 만들어준다.
이전에 @RequestParam 애너테이션을 사용했을 때 PostMan에서 x-www-form-unlencoded 형식의 데이터 요청을 해야했다. 그러나 현업에서의 대부분의 API 통신 프로토콜은 대부분 JSON형식이다. @RequestBody 애너테이션은 JSON 형식의 통신 프로토콜을 지원한다. 앞으로 JSON 통식을 하려한다면 @RequestBody 애너테이션을 사용해야 한다.
❗️@RequestBody의 사용
...
...
@PatchMapping("/{coffee-id}")
public ResponseEntity patchCoffee(@PathVariable("coffee-id") long coffeeId,
//@RequestBody 애너테이션의 사용, 요청받은 JSON 데이터를 PatchCoffeeDto 객체로 전달받는다.
@Valid @RequestBody PatchCoffeeDto patchCoffeeDto) {
patchCoffeeDto.setCoffeeId(coffeeId);
return new ResponseEntity<>(patchCoffeeDto, HttpStatus.OK);
}
...
...
✔️ @ResponseBody
클라이언트에게 전달하기 위해 객체를 JSON 형식으로 만들어준다.
@RequestBody 애너테이션과 반대로 객체를 JSON 형식으로 변경해준다. JSON 통신 프로토콜은 요청에 대한 응답도 역시 JSON 형태로 응답하기 때문에 @ResponseBody 애너테이션을 사용해 JSON 형식으로 변경해 주어야 한다.
❗️@ResponseBody의 사용
@ResponseBody
@RequestMapping(value = "/patch-response", method = RequestMethod.GET)
public String patchCoffee() {
return "patch-response";
}
❗️ResonseEntity<>
ResonseEntity 오브젝트를 사용해도 해당 객체를 JSON 타입으로 변경해준다.
public ResponseEntity postCoffee(@Valid @RequestBody PostCoffeeDto postCoffeeDto) {
return new ResponseEntity<>(postCoffeeDto, HttpStatus.CREATED);
}
📌 Lombok
DTO 클래스 작성에 필요한 다양한 메서드를 애너테이션을 통해 자동 생성해준다.
Lombok 애너테이션을 사용하면 DTO 클래스 작성에 필요한 Getter/Setter 메서드를 자동 생성해준다. 그외에도 데이터 전송에 필요한 다양한 메서드들을 만들어준다.
✔️ 주요 Lombok 애너테이션 종류
Annotation (@) | 설명 |
@Getter | 필드 변수의 get...() 메서드를 자동 생성해준다. |
@Setter | 필드 변수의 set...() 메서드를 자동 생성해준다. |
@ToString | 필드 변수의 toString() 메서드를 자동 생성해준다. [출력 : 클래스 타입(field1=..., ..., ....)] |
@EqualsAndHashCode | 같은 인스턴스인지 비교하기 위한 equals() 메서드를 자동 생성해준다. 같은 클래스인지 비교하기 위한 canEqual() 메서드를 자동 생성해준다. 해시 값 활용을 위한 hashCode() 메서드를 자동 생성해준다. |
@Data | @Getter, @Setter, @ToString, @EqualsAndHashCode 에서 생성해주는 메서드를 자동 생성해준다. |
✔️ Lombok의 사용
Lombok을 사용하면 DTO 클래스 코드를 좀 더 간결하게 만들 수 있다.
import lombok.*;
@Getter
@Setter
public class MemberPatchDto {
private long memberId;
private String name;
private String phone;
}
📌 내부 클래스를 활용한 공통 멤버 변수 추출(작성 예정)
댓글