개발소설

DTO(Data Transfer Object) 본문

Spring Framework

DTO(Data Transfer Object)

ChaeHing 2023. 4. 13. 00:08
  • Data Transfer Object의 약자로 마틴 파울러(Martin Fowler)가 ‘Patterns of Enterprise Application Architecture’ 라는 책에서 처음 소개한 엔터프라이즈 애플리케이션 아키텍처 패턴의 하나
  • DTO는 주로 클라이언트에서 서버 쪽으로 전송하는 요청 데이터를 전달 받을 때, 서버에서 클라이언트 쪽으로 전송하는 응답 데이터를 전송하기 위한 용도로 사용
  • DTO가 필요한 이유
    • 클라이언트의 Request Body를 하나의 객체로 모두 전달 받을 수 있기때문에 코드 자체가 간결
    • Request Body의 데이터 유효성(Validation) 검증이 단순해진다.
  • JSON 형식의 Request Body를 전달 받기 위해서는 DTO 객체에 @RequestBody 애너테이션을 붙여야 한다.
  • Response Body를 JSON 형식으로 전달하기 위해서는 @ResponseBody 애너테이션을 메서드 앞에 붙여 주어야하지만 ResponseEntity 객체를 리턴 값으로 사용할 경우 @ResponseBody 를 생략할 수 있다.
  • 클라이언트 쪽에서 JSON 형식의 데이터를 서버 쪽으로 전송하면 서버 쪽의 웹 애플리케이션은 전달 받은 JSON 형식의 데이터를 DTO 같은 Java의 객체로 변환하는데 이를 역직렬화(Deserialization)이라고 한다.
  • 서버 쪽에서 클라이언트에게 응답 데이터를 전송하기 위해서 DTO 같은 Java의 객체를 JSON 형식으로 변환하는 것을 직렬화(Serialization)라고 한다.
  • java -> json 직렬화, json -> java 역직렬화

 

DTO 유효성 검증 (Validation)

  • 프론트엔드 쪽에서 유효성 검증을 진행했다 하더라도 서버 쪽에서 추가적으로 유효성 검증을 반드시 진행해야 한다.
  • 프론트엔드 쪽에서의 유효성 검증 프로세스는 사용자 편의성 측면에서 필요한 작업이다.
  • Jakarta Bean Validation의 애너테이션을 이용하면 Controller 로직에서 유효성 검증 로직을 분리할 수 있다.
  • Jakarta Bean Validation은 애너테이션 기반 유효성 검증을 위한 표준 스펙이다.
  • Hibernate Validator는 Jakarta Bean Validation 스펙을 구현한 구현체이다.
  • Spring에서 지원하는 @Validated 애너테이션을 사용하면 쿼리 파라미터(Query Parameter 또는 Query String) 및 @Pathvariable에 대한 유효성 검증을 진행할 수 있다.
  • Jakarta Bean Validation에서 빌트인(Built-in)으로 지원하지 않는 애너테이션은 Custom Validator를 통해 Custom Annotation을 구현한 후, 적용할 수 있다.

실습 예제

 

Controller

@RestController
@RequestMapping("/v1/coffees")
public class CoffeeController {
    // 1. DTO 클래스 및 유효성 검증을 적용하세요.
    @PostMapping
    public ResponseEntity postCoffee(@Valid @RequestBody CoffeePostDto coffeePostDto){
        return new ResponseEntity<>(coffeePostDto, HttpStatus.CREATED);
    }

    // 2. DTO 클래스 및 유효성 검증을 적용하세요.
    @PatchMapping("/{coffee-id}")
    public ResponseEntity patchCoffee(@PathVariable("coffee-id") @Min(1) long coffeeId,
                                      @Valid @RequestBody CoffeePatchDto coffeePatchDto) {
        coffeePatchDto.setCoffeeId(coffeeId);
        return new ResponseEntity<>(coffeePatchDto, HttpStatus.OK);
    }

    @GetMapping("/{coffee-id}")
    public ResponseEntity getCoffee(@PathVariable("coffee-id") long coffeeId) {
        System.out.println("# coffeeId: " + coffeeId);

        // not implementation

        return new ResponseEntity<>(HttpStatus.OK);
    }

    @GetMapping
    public ResponseEntity getCoffees() {
        System.out.println("# get Coffees");

        // not implementation

        return new ResponseEntity<>(HttpStatus.OK);
    }

    @DeleteMapping("/{coffee-id}")
    public ResponseEntity deleteCoffee(@PathVariable("coffee-id") long coffeeId) {
        // No need business logic

        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }
}
  • @RequestBody : DTO 객체를 ResponseEntity 클래스의 생성자 피라미터로 전달
  • @Valid : DTO 객체에 유효성 검증을 적용하게 해주는 애너테이션
  • @Min(N) :  해당 피라미터가 N이상 인지 검증하는 애너테이션 - @Min(1) -> 1이상
    • 예제에선 식별자가 1이상인 경우 검증성공

 

DTO 클래스 (Patch)

package com.codestates.coffee;

import com.codestates.CustomAnnotation.PriceRange;

import javax.validation.constraints.*;

public class CoffeePatchDto {

    private long coffeeId;

    @Pattern(regexp = "^\\S+(\\s?\\S+)*$",
            message= "한글명은 공백만으로 구성되지 않아야 합니다.")
    private String korName;

    @Pattern(regexp = "^[a-zA-Z ]*$",
            message = "영문명은 영문(대소문자 모두 가능)만 허용합니다.")
    @Pattern(regexp = "^\\S+(\\s?\\S+)*$",
            message = "영문명은 워드 사이에 한칸의 공백(스페이스)만 포함 될 수 있습니다.")
    private String engName;

    @PriceRange
    private int price;

    public long getCoffeeId() {
        return coffeeId;
    }

    public void setCoffeeId(long coffeeId) {
        this.coffeeId = coffeeId;
    }

    public String getKorName() {
        return korName;
    }

    public void setKorName(String korName) {
        this.korName = korName;
    }

    public String getEngName() {
        return engName;
    }

    public void setEngName(String engName) {
        this.engName = engName;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}
  • @Pattern : 정규표현식(Reqular Expression)에 매치되는지 검증 
    • regexp : 정규표현식
    • message : 유효성검사를 통과하지 못하는경우 발생시키는 메시지
      • ex) 한글명은 공백만으로 구성되지 않아야 합니다.
      • 미설정시 내장된 디폴트 에러 메시지가 콘솔에 출력
    • 정규표현식 해석
      • ^[a-zA-Z ]*$ - 대소문자+공백 가능
      • ^\\S+(\\s?\\S+)*$ - 공백 아닌 문자 1개 이상((공백인 문자 0개 또는 1개)(공백이 아닌 문자 1개 이상)) -> 마지막 맨 바깥 쪽 괄호 조건이 0개 이상(즉, 있어도 되고 없어도 된다)
  • @PriceRange : 직접 만든 유효성검사 애너테이션 - 포스트 맨 아래 코드 확인 (Custom Validator - PriceRange)

 

DTO 클래스 (POST)

package com.codestates.coffee;

import com.codestates.CustomAnnotation.PriceRange;

import javax.validation.constraints.*;

public class CoffeePostDto {
    @NotBlank(message = "한글명은 공백이 아니어야 합니다.")
    private String KorName;

    @NotNull(message = "영문명을 반드시 입력해야 합니다.")
    @Pattern(regexp = "^[a-zA-Z ]*$",
            message = "영문명은 영문(대소문자 모두 가능)만 허용합니다.")
    @Pattern(regexp = "^\\S+(\\s?\\S+)*$",
            message = "영문명은 워드 사이에 한칸의 공백(스페이스)만 포함 될 수 있습니다.")
    private String engName;

    @NotNull(message = "가격을 반드시 입력해야 합니다.")
    @Max(value = 50000, message = "50000이하")
    @Min(value = 100, message = "100이상")
    private Integer price;

    public String getKorName() {
        return KorName;
    }

    public void setKorName(String korName) {
        KorName = korName;
    }

    public String getEngName() {
        return engName;
    }

    public void setEngName(String engName) {
        this.engName = engName;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}
  • @NotBlank
    • 정보가 비어있지 않은지를 검증
    • null 값이나 공백(””), 스페이스(” “) 같은 값들을 모두 허용하지 않음
    • 유효성 검증에 실패하면 에러 메시지가 콘솔에 출력
  • @Max(N)
    • 값이 N이하 인지 검증
    • @Max(50000) - 50000이하면 검증성공

 

 

Custom Validator - PriceRange

package com.codestates.CustomAnnotation;

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 = {PriceRangeValidator.class}) // (1)
public @interface PriceRange {
    String message() default "가격은 100이상 50000이하를 입력해야합니다."; // (2)
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
  • PriceRange 애너테이션이 멤버 변수에 추가되었을때, 동작 할 Custom Validator를 (1)과 같이 추가
  • (2) : 애너테이션 추가 시 별도로 정의하지 않으면 유효성 검증 실패 시, 표시되는 디폴트 메시지
package com.codestates.CustomAnnotation;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class PriceRangeValidator implements ConstraintValidator<PriceRange, Integer> {

    @Override
    public void initialize(PriceRange constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        return value >= 100 && value <= 50000 || value == 0;
    }
}
  • ConstraintValidator<PriceRange, String>에서 PriceRange는 CustomValidator와 매핑된 Custom Annotation을 의미하며, Integer은 Custom Annotation으로 검증할 대상 멤버 변수의 타입을 의미
  • isValid() : 검증할 대상 멤버 변수 검증 로직, return값이 true일 경우 검증 성공, failed일 경우 검증 실패
    • 예제에선 값이 0(Integer의 초기값(입력을 안받았을 경우)) 이거나 값이 100~50000사이일 경우 검증 성공

 

그외 유효성검사 애너테이션 https://thinkground.studio/spring-boot-%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%82%AC-validation-%EA%B4%80%EB%A0%A8-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98/

 

Spring Boot 유효성 검사 (Validation) 관련 어노테이션 - ThinkGround

Spring Boot 유효성 검사 (Validation) 관련 어노테이션 관련 포스팅입니다.어떤 어노테이션이 있는지 확인이 필요하신 분들을 위해 표로 정리하여 공유드립니다. 주로 쓰이는 어노테이션에 대해서 정

thinkground.studio

 

자주 사용하는 정규표현식 - https://zzokma.tistory.com/1644

정규표현식 - https://regexr.com/

'Spring Framework' 카테고리의 다른 글

Spring MVC 예외처리  (0) 2023.04.16
DI를 통한 서비스 계층 ↔ API 계층 연동, 매퍼(Mapper), 엔티티(Entity)  (0) 2023.04.14
SpringMVC - Controller  (0) 2023.04.12
Spring MVC  (0) 2023.04.12
AOP(Aspect Oriented Programming)  (0) 2023.04.08
Comments