본문 바로가기
Spring Framework

[API 계층]DTO

by mozzi329 2022. 8. 22.
728x90
 

 

JSON 통신 프로토콜을 위해 사용하는 DTO(Data Transfer Object) 클래스

📌 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 실수를 제외한 숫자 지정된 값보다 크거나 같은지 체크
@Email 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;
    
}

 

📌 내부 클래스를 활용한 공통 멤버 변수 추출(작성 예정)

 

댓글