Spring MVC - API 계층 - DTO

김소희·2023년 4월 12일

DTO(Data Transfer Object)

DTO는 계층 간 데이터 교환을 하기 위해 사용하는 객체로, DTO는 로직을 가지지 않는 순수한 데이터 객체(getter & setter 만 가진 클래스)입니다.

요청 데이터에 회원의 이름, 이메일, 전화번호, 주소, 로그인 패스워드, 패스워드 확인 정보 등등많은 정보들이 회원 정보에 포함되어 있을 수 있습니다.
이 정보들을 하나씩 postMember()에 파라미터로 추가하면 @RequestParam의 개수가 많아질 것 입니다.

이런 경우에 DTO 클래스를 사용하면 요청 데이터를 하나의 객체로 전달 받는 역할을 하여 코드가 간결해집니다.

폼 데이터의 유효성 검증

데이터를 파라미터로 받아 비즈니스 로직에 따른 데이터 처리를 하기전에,
파라미터가 데이터로 사용 가능한지 파악하는 과정이 필요합니다.
클라이언트(html)에서 자바스크립트를 통해 1차 검증을 하여 유효성이 검증되지 않은 데이터를 서버로 보내지 않음으로 네트워크 트래픽 낭비를 막고, 서버의 부하를 줄여주게끔 되어있더라도, 악의적인 url 호출에 의해 검증되지 않은 데이터가 서버로 올라올 수 있으므로 서버에서도 반드시 확인해야 합니다.

예전에는 각자만의 방식으로 데이터를 검증하는 로직을 코드로 작성해야 했지만,
스프링의 Validator 인터페이스는 누가 하더라도 동일한 방식으로 검증하기 때문에 편리하다.

데이터 유효성 검증의 단순화

사용자가 입력한 데이터가 양식에 맞게 제출되었는지 데이터를 검증하는 것을 유효성(Validation)검증이라고 합니다. 유효성 검사는 데이터베이스에 저장하기 전에 먼저 검증하도록 하는 것이 일반적입니다. 이를 통해 잘못된 데이터를 방지하고, 사용자 경험을 개선할 수 있습니다.

 if (!email.matches("^[a-zA-Z0-9_!#$%&'\\*+/=?{|}~^.-]+@[a-zA-Z0-9.-]+$")) {
            throw new InvalidParameterException();
        }

이렇게 직접 코드를 써서 유효성 검사를 할 수도 있지만 핸드러 메소드 내에 유효성 검사 코드가 섞여서 코드의 복잡도가 높아지게 됩니다.

따라서 DTO 클래스에서 유효성 검증 로직을 사용하면 핸들러 메소드가 간결해집니다.

Java에서 기본적으로 제공하는 javax.validation 패키지를 사용하여 @NotNull, @Size, @Min, @Max 등의 어노테이션으로 다양한 검증 규칙을 설정하고, 스프링에서 제공하는 Validator 인터페이스를 이용해서 유효성 검사 로직을 작성할 수 있습니다.

  1. build.gradle 파일의 dependencies 항목에 'org.springframework.boot:spring-boot-starter-validation’을 추가하기.
// build.gradle 파일에 validation를 추가하고 코끼리를 누릅니다.
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-validation'
  1. 유효성 검증을 적용시키기.
package com.codestates.member;

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 name;
    @Pattern(regexp = "^010-\\d{3,4}=\\d{4}$",
    message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다.")
    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;
    }
}
  1. 유효성 검증 애너테이션을 추가한 MemberPostDto 클래스를 사용하는 MemberController 클래스의 postMember() 핸들러 메서드의 코드에는 @Valid 애너테이션을 추가 합니다.
@RestController
@RequestMapping("/v1/members")
public class MemberController {

    @PostMapping
    public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberPostDto) {
        return new ResponseEntity<>(memberPostDto, HttpStatus.CREATED);
    }
    
    이하생략
  1. Postman을 사용해서 email, name, phone 정보를 모두 유효하지 않은 정보로 입력해서 postMember() 핸들러 메서드로 요청을 전송했더니 응답 결과는 400 ‘Bad Request’를 전달 받았습니다.
    클라이언트의 요청 데이터가 유효성 검증에 실패하면 클라이언트의 요청은 거부(reject)됩니다.


DTO 실습코드

컨트롤러 작성

package com.codestates.coffee;
import com.codestates.coffee.CoffeePostDto;
import com.codestates.coffee.CoffeePatchDto;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.Positive;


@RestController
@RequestMapping("/v1/coffees")
@Validated
public class CoffeeController {
    
    @PostMapping // 추가
    public ResponseEntity postCoffee(@Valid @RequestBody CoffeePostDto coffeeDto) {

        return new ResponseEntity<>(coffeeDto, HttpStatus.CREATED);
    }

    @PatchMapping("/{coffee-id}") // 수정
    public ResponseEntity patchCoffee(@PathVariable("coffee-id") @Positive long coffeeId,
                                      @Valid @RequestBody CoffeePatchDto coffeePatchDto) {
        coffeePatchDto.setCoffeeId(coffeeId);

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

    @GetMapping("/{coffee-id}") // 1개 조회
    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);
    }
}

유효성 검사기능이 있는 CoffeePatchDto 클래스

package com.codestates.coffee;

import com.codestates.member.NotSpace;

import javax.validation.constraints.*;

public class CoffeePatchDto {

    private long coffeeId;

    @NotSpace(message = "커피명은 공백이 아니어야 합니다")
    @Pattern(regexp = "^[ㄱ-ㅎ가-힣]*$", message = "한글 커피명은 한글만 입력 가능합니다")
    private String korName;


    @Pattern(regexp = "^[a-zA-Z]+(\\s[a-zA-Z]+)*$",
            message = "영문 커피명은 영문(대소문자)과 스페이스만 입력 가능합니다.")
    private String engName;

    @Min(value = 100)
    @Max(value = 50000)
    private Integer price;


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

    public long getCoffeeId() {
        return coffeeId;
    }

    public String getKorName() {
        return korName;
    }

    public String getEngName() {
        return engName;
    }

    public int getPrice() {
        return price;
    }
}

유효성 검사기능이 있는 CoffeePostDto 클래스

package com.codestates.coffee;

import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.*;

public class CoffeePostDto {

    @NotBlank
    @Pattern(regexp = "^[ㄱ-ㅎ가-힣]*$", message = "한글 커피명은 한글만 입력 가능합니다")
    private String korName;
    
    @NotBlank
    @Pattern(regexp = "^[a-zA-Z]+(\\s[a-zA-Z]+)*$",
            message = "영문 커피명은 영문(대소문자)과 스페이스만 입력 가능합니다.")
    private String engName;

    @Range(min=100, max=50000)
    private Integer price;



    public String getKorName() {
        return korName;
    }

    public String getEngName() {
        return engName;
    }

    public Integer getPrice() {
        return price;
    }
}

DTO클래스에 int로 데이터 타입을 지정하면 값이 없을 경우 초기값이 0으로 0이라는 값이 들어오게 됩니다. Null로 받기 위해서 Integer타입으로 지정했습니다.
정규표현식은 처음이라 chatGPT 에게 물어봐서 해결했는데 정규표현식 이 사이트에서 테스트 해가면서 찾아보는 연습도 기회가 되면 해봐야 겠습니다.

최종수정된 DTO 코드

롬북에있는 Setter를 이용하여 코드를 간결하게 만들었다.

package com.codestates.coffee.dto;

import lombok.Getter;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;

@Getter
public class CoffeePostDto {
    @NotBlank
    private String korName;

    @NotBlank
    @Pattern(regexp = "^([A-Za-z])(\\s?[A-Za-z])*$",
            message = "커피명(영문)은 영문이어야 합니다(단어 사이 공백 한 칸 포함). 예) Cafe Latte")
    private String engName;

    @Range(min= 100, max= 50000)
    private int price;

    @NotBlank
    @Pattern(regexp = "^([A-Za-z]){3}$",
            message = "커피 코드는 3자리 영문이어야 합니다.")
    private String coffeeCode;


}
profile
백엔드 개발자의 노트

0개의 댓글