[Spring MVC] DTO ์‹ค์Šต

Updated:

Categories:

Tags: , ,

๐Ÿ“Œ ๊ฐœ์ธ์ ์ธ ๊ณต๊ฐ„์œผ๋กœ ๊ณต๋ถ€๋ฅผ ๊ธฐ๋กํ•˜๊ณ  ๋ณต์Šตํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ๋ธ”๋กœ๊ทธ์ž…๋‹ˆ๋‹ค.
์ •ํ™•ํ•˜์ง€ ์•Š์€ ์ •๋ณด๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ฐธ๊ณ ๋ฐ”๋ž๋‹ˆ๋‹ค :๐Ÿ˜ธ
[ํ‹€๋ฆฐ ๋‚ด์šฉ์€ ๋Œ“๊ธ€๋กœ ๋‚จ๊ฒจ์ฃผ์‹œ๋ฉด ๋ณต๋ฐ›์œผ์‹ค๊ฑฐ์—์š”]

[Spring MVC] DTO ์‹ค์Šต

์ฒ˜์Œ์ž‘์„ฑ - ์˜ค๋ฅ˜๋ฐœ์ƒ

  1. CoffeePostDto

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
     public class CoffeePostDto {
         @NotBlank
         private String korName;
        
         @NotBlank
         @Pattern(regexp ="^[A-Za-z]+(\\s[A-Za-z]+)*$", message = "์˜๋ฌธ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.")
         private String engName;
        
         @Min(100)
         @Max(50000)
         private int price;
         }
    
  2. CoffeePatchDto

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
     package com.springboot.coffee;
     import com.springboot.member.NotSpace;
     import javax.validation.constraints.*;
        
     @Getter @Setter
     public class CoffeePatchDto {
     		@Positive    
         private long coffeeId;
        
         @NotBlank
         private String korName;
        
         @NotSpace
         @Pattern(regexp ="^[A-Za-z]+(\\s[A-Za-z]+)*$", message = "์˜๋ฌธ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.")
         @NotBlank
         private String engName;
        
         @Min(100)
         @Max(50000)
         private int price;
     }
    
  3. Coffeecontroller ํด๋ž˜์Šค์˜ patchCoffee Method

    1
    2
    3
    4
    5
    6
    7
    8
    
        
     @PatchMapping("/{coffee-id}")
     public ResponseEntity<CoffeePatchDto> patchCoffee(@PathVariable("coffee-id") @Positive long coffeeId,
                                                       @RequestBody @Valid CoffeePatchDto coffeePatchDto) {
         coffeePatchDto.setCoffeeId(coffeeId);
        
         return new ResponseEntity<>(coffeePatchDto, HttpStatus.OK);
     }
    
  4. ๋ฌธ์ œ

    1. ์˜ค๋ฅ˜๋ฐœ์ƒ โ‡’ localhost:8080/v1/coffees/1 ๋กœ patchํ•˜๋ฉด id๋Š” 0์ด์ƒ๋งŒ ์˜ฌ ์ˆ˜ ์žˆ๋‹ค๊ณ  ์—๋Ÿฌ๋‚จ
      • MethodArgumentNotValidException
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      
        Validation failed for argument [1] in 
        public org.springframework.http.ResponseEntity 
        com.springboot.coffee.CoffeeController
        .patchCoffee(long,com.springboot.coffee.CoffeePatchDto): 
        [Field error in object 'coffeePatchDto' on field 'coffeeId'
        : rejected value [0]; codes [Positive.coffeePatchDto.coffeeId,
        Positive.coffeeId,Positive.long,Positive]; 
        arguments [org.springframework.context.support
        .DefaultMessageSourceResolvable: codes [coffeePatchDto.coffeeId,coffeeId]; 
        arguments []; default message [coffeeId]]; default message [0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค]] ]
      
      • CoffeePatchDto์—์„œ @Positive private long coffeeId; - ์ด๊ฒŒ ๋ฌธ์ œ๊ฐ€ ๋จ.
      • ์™œ๋ƒ๋ฉด, CoffeePatchDto.coffeeId์˜ ์ดˆ๊ธฐํ™” ๊ฐ’์ด 0์ด ๋˜๊ณ  @Positive ์˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๊ฐ€ ์‹คํŒจ๋˜์–ด ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜๊ธฐ ์ „์— ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์—์„œ ์ด๋ฏธ ์‹คํŒจํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒ โ‡’ CoffeePatchDto์˜ coffeeId์˜ @Positive ์–ด๋…ธํ…Œ์ด์…˜์„ ์ œ๊ฑฐํ•˜๋ฉด coffeeId์— ๋Œ€ํ•œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๊ฐ€ ์ˆ˜ํ–‰๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ๋„ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ, coffeePatchDto.setCoffeeId(coffeeId); ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ ํ›„์— ์ œ๋Œ€๋กœ ๋™์ž‘
      • @PathVariable(โ€œcoffee-idโ€) @Positive long coffeeId, ์—ฌ๊ธฐ๋งŒ positive ์ž‘์„ฑํ•ด์ฃผ๋ฉด ๋จ.

      ์›์‹œํƒ€์ž…์€ ๊ธฐ๋ณธ์ดˆ๊ธฐํ™” ๊ฐ’์ด ์žˆ์Œ.

      Untitled

      Referece type์€ ์ดˆ๊ธฐํ™” ํ•ด์ฃผ์–ด์•ผ ํ•จ.


      ์ถ”๊ฐ€. coffeeId ๊ฒ€์ฆ

      Untitled

      @PathVariable์˜ ๊ฒ€์ฆ์ด ์ œ๋Œ€๋กœ ๋˜๋ ค๋ฉด class์œ„์— @Validated ํ•„์š”

      Untitled

      @Valid๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœย ์ปจํŠธ๋กค๋Ÿฌ์—์„œ๋งŒ ๋™์ž‘ํ•˜๋ฉฐ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋‹ค๋ฅธ ๊ณ„์ธต์—์„œ๋Š” ๊ฒ€์ฆ์ด ์•ˆ๋จ, ๋‹ค๋ฅธ ๊ณ„์ธต์—์„œ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” @Validated์™€ ๊ฒฐํ•ฉ๋˜์–ด์•ผ ํ•จ.

      []

      @Valid ๋™์ž‘์›๋ฆฌ

      ๋ชจ๋“  ์š”์ฒญ์€ ํ”„๋ก ํŠธ ์ปจํŠธ๋กค๋Ÿฌ์ธ ๋””์ŠคํŒจ์ฒ˜ ์„œ๋ธ”๋ฆฟ์„ ํ†ตํ•ด ์ปจํŠธ๋กค๋Ÿฌ๋กœ ์ „๋‹ฌ๋œ๋‹ค. ์ „๋‹ฌ ๊ณผ์ •์—์„œ๋Š” ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์†Œ๋“œ์˜ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์ฃผ๋Š” ArgumentResolver๊ฐ€ ๋™์ž‘ํ•˜๋Š”๋ฐ, @Valid ์—ญ์‹œ ArgumentResolver์— ์˜ํ•ด ์ฒ˜๋ฆฌ๊ฐ€ ๋œ๋‹ค.๋Œ€ํ‘œ์ ์œผ๋กœ @RequestBody๋Š” Json ๋ฉ”์„ธ์ง€๋ฅผ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•ด์ฃผ๋Š” ์ž‘์—…์ด ArgumentResolver์˜ ๊ตฌํ˜„์ฒด์ธRequestResponseBodyMethodProcessor๊ฐ€ ์ฒ˜๋ฆฌํ•˜๋ฉฐ, ์ด ๋‚ด๋ถ€์—์„œ @Valid๋กœ ์‹œ์ž‘ํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ์„ ๊ฒฝ์šฐ์— ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ์ง„ํ–‰ํ•จ.

      @Valid๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœย ์ปจํŠธ๋กค๋Ÿฌ์—์„œ๋งŒ ๋™์ž‘ํ•˜๋ฉฐ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋‹ค๋ฅธ ๊ณ„์ธต์—์„œ๋Š” ๊ฒ€์ฆ์ด ์•ˆ๋จ, ๋‹ค๋ฅธ ๊ณ„์ธต์—์„œ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” @Validated์™€ ๊ฒฐํ•ฉ๋˜์–ด์•ผ ํ•จ.

      ์ถœ์ฒ˜ : https://mangkyu.tistory.com/174

      @Validated ๋™์ž‘์›๋ฆฌ

      ํŠน์ • ArgumnetResolver์— ์˜ํ•ด ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๊ฐ€ ์ง„ํ–‰๋˜์—ˆ๋˜ @Valid์™€ ๋‹ฌ๋ฆฌ, @Validated๋Š” AOP ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฉ”์†Œ๋“œ ์š”์ฒญ์„ ์ธํ„ฐ์…‰ํ„ฐํ•˜์—ฌ ์ฒ˜๋ฆฌ๋œ๋‹ค. @Validated๋ฅผย ํด๋ž˜์Šค ๋ ˆ๋ฒจ์— ์„ ์–ธํ•˜๋ฉด ํ•ด๋‹น ํด๋ž˜์Šค์— ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ์œ„ํ•œ AOP์˜ ์–ด๋“œ๋ฐ”์ด์Šค ๋˜๋Š” ์ธํ„ฐ์…‰ํ„ฐ(MethodValidationInterceptor)๊ฐ€ ๋“ฑ๋ก๋œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ํ•ด๋‹น ํด๋ž˜์Šค์˜ ๋ฉ”์†Œ๋“œ๋“ค์ด ํ˜ธ์ถœ๋  ๋•Œ AOP์˜ ํฌ์ธํŠธ ์ปท์œผ๋กœ์จ ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑ„์„œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ์ง„ํ–‰ํ•œ๋‹ค.์ด๋Ÿฌํ•œ ์ด์œ ๋กœ @Validated๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ปจํŠธ๋กค๋Ÿฌ, ์„œ๋น„์Šค, ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๋“ฑ ๊ณ„์ธต์— ๋ฌด๊ด€ํ•˜๊ฒŒ ์Šคํ”„๋ง ๋นˆ์ด๋ผ๋ฉด ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋Œ€์‹  ํด๋ž˜์Šค์—๋Š” ์œ ํšจ์„ฑ ๊ฒ€์ฆ AOP๊ฐ€ ์ ์šฉ๋˜๋„๋กย @Validated๋ฅผ, ๊ฒ€์ฆ์„ ์ง„ํ–‰ํ•  ๋ฉ”์†Œ๋“œ์—๋Š” @Valid๋ฅผ ์„ ์–ธํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.์ด๋Ÿฌํ•œ ์ด์œ ๋กœ @Valid์— ์˜ํ•œ ์˜ˆ์™ธ๋Š” MethodArgumentNotValidException์ด๋ฉฐ, @Validated์— ์˜ํ•œ ์˜ˆ์™ธ๋Š”ย  ConstraintViolationException

๋‹ค์‹œ ์ž‘์„ฑ- ์˜ค๋ฅ˜ ๊ฐœ์„ , but Body์˜ attribute๊ฐ€ null์ผ ๋•Œ๋„ ์œ ํšจ์„ฑ ๊ฒ€์ฆํ•จ

1
2
3
4
5
6
7
8
    @PatchMapping("/{coffee-id}")
    public ResponseEntity patchCoffee(@PathVariable("coffee-id") @Positive long coffeeId,
            @RequestBody @Valid CoffeePatchDto coffeePatchDto) {

        coffeePatchDto.setCoffeeId(coffeeId);

        return new ResponseEntity<>(coffeePatchDto, HttpStatus.OK);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.springboot.coffee;
import com.springboot.member.NotSpace;

import javax.validation.constraints.*;

public class CoffeePatchDto {

    private long coffeeId;

    @NotSpace
    private String korName;

    @Pattern(regexp ="^[A-Za-z]+(\\s[A-Za-z]+)*$", message = "์˜๋ฌธ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.")
    @NotBlank
    private String engName;

    @Min(100)
    @Max(50000)
    private integer price;

    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;
    }

    public long getCoffeeId() {
        return coffeeId;
    }

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

}

Price์— null๊ฐ’์ด ์•ˆ๋“ค์–ด์™€์„œ @NotBlank ์‹œ๋„ โ‡’ UnexpectedTypeException ๋ฐœ์ƒ

์•Œ๊ณ ๋ณด๋‹ˆ @NotBlank๋„ null ๋ชป ๋“ค์–ด์˜ค๊ณ  ์˜คํžˆ๋ ค @min, @max๋Š” null์ด ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ์Œ.

๊ทธ๋ž˜์„œ int๋Š” ์ดˆ๊ธฐํ™” ๊ฐ’์ด 0์ด์—ฌ์„œ ๊ทธ๋ ‡๋‹ค๋Š” ๊ฑธ ์•Œ๊ฒŒ ๋˜์—ˆ๊ณ ,

Integer๋กœ ๋ฐ”๊ฟ”๋„ ์—๋Ÿฌ ๋‚˜์„œ ์™œ ๊ทธ๋Ÿฐ๊ฐ€ ํ–ˆ๋”๋‹ˆ, ์œ„์— ํ•„๋“œ์—๋งŒ ๋ณ€๊ฒฝํ•˜๊ณ 

getter์™€ setter๋ฅผ ๋ฏธ๋ฆฌ ๋งŒ๋“ค์—ˆ๋”๋‹ˆ ์—ฌ๊ธฐ๋Š” int๊ฐ’์œผ๋กœ ์œ ์ง€๋˜๊ณ  ์žˆ์—ˆ์Œโ€ฆ.!

๋ชจ๋‘ ๋ณ€๊ฒฝํ•˜๋‹ˆ null๊ฐ’์ด ์ž˜ ๋“ค์–ด์˜ด.

์™„์„ฑ

[CoffeePatchDto]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import lombok.Getter;
import lombok.Setter;
import com.springboot.member.NotSpace;
import javax.validation.constraints.*;

@Getter @Setter
public class CoffeePatchDto {

    private long coffeeId;
    @NotSpace //customAnnotation
    private String korName;

    @Pattern(regexp ="^[A-Za-z]+(\\s[A-Za-z]+)*$", message = "์˜๋ฌธ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.")
    private String engName;

    @Min(100)
    @Max(50000)
    private Integer price;
}

[CoffeePostDto]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import lombok.Getter;
import lombok.Setter;

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

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

    @NotBlank
    @Pattern(regexp ="^[A-Za-z]+(\\s[A-Za-z]+)*$", message = "์˜๋ฌธ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.")
    private String engName;

    @Min(100)
    @Max(50000)
    private int price;

}

[CoffeeController]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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.Positive;

@Validated
@RestController
@RequestMapping("/v1/coffees")
public class CoffeeController {
    // 1. DTO ํด๋ž˜์Šค ๋ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ์ ์šฉํ•˜์„ธ์š”.
    @PostMapping
    public ResponseEntity postCoffee(@RequestBody @Valid CoffeePostDto coffeePostDto) {
        return new ResponseEntity<>(coffeePostDto, HttpStatus.CREATED);
    }

    // 2. DTO ํด๋ž˜์Šค ๋ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ์ ์šฉํ•˜์„ธ์š”.
    @PatchMapping("/{coffee-id}")
    public ResponseEntity patchCoffee(@PathVariable("coffee-id") @Positive long coffeeId,
            @RequestBody @Valid CoffeePatchDto coffeePatchDto) {

        coffeePatchDto.setCoffeeId(coffeeId);

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

Comment

์‹ค๋ฌด์—๋„ ์ด๋ ‡๊ฒŒ null์„ ๋ฐ›๋‚˜ ์‹ถ์–ด์„œ ์˜๋ฌธ์ด ๊ณ„์† ๋“ค์—ˆ๊ณ  DB๋ฅผ ์•„์ง ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„์„œ DB์—๋Š” ๋ณ€๊ฒฝํ•œ ๊ฐ’๋งŒ ๋“ค์–ด๊ฐ€๋Š” ๊ฑด์ง€ ๊ถ๊ธˆํ–ˆ๋Š”๋ฐ

๊ฐ€๊ฒฉ์„ ๋ฐ”๊พธ๊ณ  ์‹ถ์–ด์„œ PATCH์— Body ๊ฐ’์œผ๋กœ {โ€priceโ€:3000} ๋งŒ ๋„ฃ๊ฒŒ ๋˜๋ฉด(korName, engName

๋งŒ์•ฝ DB์— null์„ ๋ณด๋‚ด๋ฉด null์ด ๋“ค์–ด๊ฐ€๊ฒŒ ๋œ๋‹ค๊ณ  ํ•จ.

์ด๊ฑธ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด Optional class ๋˜๋Š” ๋žŒ๋‹ค์‹์„ ์‚ฌ์šฉํ•ด์„œ ๋Œ€์ฒดํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํ•จ. (์‹ค๋ฌด์—์„œ ๋Œ€๋ถ€๋ถ„ ๋žŒ๋‹ค์‹ ์‚ฌ์šฉ)

๋ฌดํŠผ int๋ž‘ integer๋•Œ๋ฌธ์— ์•  ๋งŽ์ด ๋จน์—ˆ๋Š”๋ฐ ๊ฐ•์‚ฌ๋‹˜์€ NotZero๋ผ๋Š” annotation์„ custom์œผ๋กœ ๋งŒ๋“ค์–ด ๋ฒ„๋ฆผ. ๐Ÿ˜ฎ ,,, ๊ทธ๋ฆฌ๊ณ  custom Annotation ์‹ค๋ฌด์—์„œ๋„ ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š”๋ฐ ํ”„๋กœ์ ํŠธ ์ „์— ํ•œ๋ฒˆ ๋งŒ๋“ค์–ด๋ณด๋Š” ๊ฒƒ๋„ ์ข‹๋‹ค๊ณ  ํ•˜์‹ฌ, !








๐Ÿ’ ๊ณต์ง€

- ์ •๋ณด ๊ณต์œ ๊ฐ€ ์•„๋‹Œ ๊ฐœ์ธ์ด ๊ณต๋ถ€ํ•˜๊ณ  ๊ธฐ๋กํ•˜๊ธฐ ์œ„ํ•œ ์šฉ๋„์ž…๋‹ˆ๋‹ค.

Spring ์นดํ…Œ๊ณ ๋ฆฌ ๋‚ด ๋‹ค๋ฅธ ๊ธ€ ๋ณด๋Ÿฌ๊ฐ€๊ธฐ

Leave a comment