Spring MVC - API ๊ณ„์ธต - DTO

๊น€์†Œํฌยท2023๋…„ 4์›” 12์ผ
1

๐Ÿ“ƒ ๋ชฉ์ฐจ

  • DTO
  • DTO ์‹ค์Šต์ฝ”๋“œ

DTO(Data Transfer Object)

DTO๋Š” ๊ณ„์ธต ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ตํ™˜์„ ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ๊ฐ์ฒด๋กœ, DTO๋Š” ๋กœ์ง์„ ๊ฐ€์ง€์ง€ ์•Š๋Š” ์ˆœ์ˆ˜ํ•œ ๋ฐ์ดํ„ฐ ๊ฐ์ฒด(getter & setter ๋งŒ ๊ฐ€์ง„ ํด๋ž˜์Šค)์ž…๋‹ˆ๋‹ค.

์š”์ฒญ ๋ฐ์ดํ„ฐ์— ํšŒ์›์˜ ์ด๋ฆ„, ์ด๋ฉ”์ผ, ์ „ํ™”๋ฒˆํ˜ธ, ์ฃผ์†Œ, ๋กœ๊ทธ์ธ ํŒจ์Šค์›Œ๋“œ, ํŒจ์Šค์›Œ๋“œ ํ™•์ธ ์ •๋ณด ๋“ฑ๋“ฑ๋งŽ์€ ์ •๋ณด๋“ค์ด ํšŒ์› ์ •๋ณด์— ํฌํ•จ๋˜์–ด ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์ด ์ •๋ณด๋“ค์„ ํ•˜๋‚˜์”ฉ postMember()์— ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ถ”๊ฐ€ํ•˜๋ฉด @RequestParam์˜ ๊ฐœ์ˆ˜๊ฐ€ ๋งŽ์•„์งˆ ๊ฒƒ ์ž…๋‹ˆ๋‹ค.

์ด๋Ÿฐ ๊ฒฝ์šฐ์— DTO ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์š”์ฒญ ๋ฐ์ดํ„ฐ๋ฅผ ํ•˜๋‚˜์˜ ๊ฐ์ฒด๋กœ ์ „๋‹ฌ ๋ฐ›๋Š” ์—ญํ• ์„ ํ•˜์—ฌ ์ฝ”๋“œ๊ฐ€ ๊ฐ„๊ฒฐํ•ด์ง‘๋‹ˆ๋‹ค.

๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์˜ ๋‹จ์ˆœํ™”

์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์–‘์‹์— ๋งž๊ฒŒ ์ œ์ถœ๋˜์—ˆ๋Š”์ง€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ๊ฒƒ์„ ์œ ํšจ์„ฑ(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๊ฐœ์˜ ๋Œ“๊ธ€