코드스테이츠 백엔드 부트캠프 42일차 - [Spring MVC] API 계층2

wish17·2023년 2월 14일
0
post-thumbnail

Daily Coding - 21번

배열 요소 중 3개를 뽑아 곱했을 때 나올 수 있는 최대값을 구하라.

  • 배열의 요소는 음수와 0을 포함하는 정수
public int largestProductOfThree(int[] arr) { // 배열 요소 중 3개를 뽑아 곱했을 때 나올 수 있는 최대값을 구하라
        int[] arr2 = arr.clone();
        int[] arr3 = arr.clone();
        int result = -1;
        for(int j=0; j<arr3.length; j++) {     // 절대값 큰 순서대로 정렬
            int max = 0;
            int index = 0;
            for (int i = 0; i < arr2.length; i++) {
                if (Math.abs(arr2[i]) > max) {
                    index = i;
                    max = Math.abs(arr2[i]);
                }
            }
            arr3[j] = arr2[index];
            arr2[index]=0;
        }
        boolean containPositive = false;
        for(int i = 0; i < arr3.length; i++){
            if(arr3[i]>0) {
                containPositive = true;
                break;
            }
        }


        if(!containPositive) { //arr3가 전부 음수면
                result = arr3[arr3.length-1]*arr3[arr3.length-2]*arr3[arr3.length-3];
                return result;
        }

        for(int j=0; j<=arr3.length-3; j++){
            result = arr3[0]*arr3[1]*arr3[2+j];
            if(result>0) break;
        }

        if(result<0){
            for(int k=2; k<=arr3.length-2; k++){
                result = arr3[0]*arr3[1+k]*arr3[2];
                if(result>0) break;
            }
        }

        if(result<0){
            for(int i=3; i<=arr3.length-2; i++){
                result = arr3[0+i]*arr3[1]*arr3[2];
                if(result>0) break;
            }
        }

        return result;
    }

모든테스트 통과

public int largestProductOfThree2(int[] arr) { // 간단하게 압축 가능
        //배열을 오름차순으로 정리합니다.
        Arrays.sort(arr);
        int arrLength = arr.length;
        //가장 큰 양수 3가지를 곱한 값
        int candidate1 = arr[arrLength - 1] * arr[arrLength - 2] * arr[arrLength - 3];
        //가장 작은 음수 2가지와, 가장 큰 양수를 곱한 값
        int candidate2 = arr[arrLength - 1] * arr[0] * arr[1];
        return Math.max(candidate1, candidate2);
    }

이렇게 훨씬 간단하게도 가능하다.


[Spring MVC] API 계층

DTO(Data Transfer Object)

엔터프라이즈 애플리케이션 아키텍처 패턴의 하나

  • 요청 데이터를 하나의 객체로 전달 받는 역할을 해줌

HTTP 요청/응답에서의 DTO(Data Transfer Object)

@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()에 파라미터로 추가되는 @RequestParam의 개수가 계속해서 늘어난다.

@RestController
@RequestMapping("/v1/members")
public class MemberController {
    @PostMapping
    public ResponseEntity postMember(MemberDto memberDto) {
        return new ResponseEntity<MemberDto>(memberDto, HttpStatus.CREATED);
    }

		...
		...
}

DTO 클래스를 적용하면 위와 같이 간단하게 가능
마찬가지로 필요한 데이터를 전달 받기 위해 데이터를 검증하는 "유효성(Validation)검증" 도 MemberDto 클래스에 작성해 기능분리가 가능해진다.

public class MemberDto {
    @Email // email양식에 맞는지 검사해주는 애너테이션
    private String email;
    private String name;
    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;
    }
}

DTO 클래스 적용을 위한 코드 리팩토링 절차

  1. 정보를 전달 받을 DTO 클래스를 생성
  • 각 멤버 변수에 해당하는 getter 메서드가 있어야 한다.
    • 현업에서는 아예 lombok이라는 라이브러리를 이용해서 getter/setter 메서드를 내부에서 자동으로 만들어 사용
  1. 클라이언트 쪽에서 전달하는 요청 데이터를 @RequestParam 애너테이션으로 전달 받는 핸들러 메서드를 찾는다.
  • Request Body가 필요한 핸들러는 HTTP POST, PATCH, PUT 같이 리소스의 추가나 변경이 발생할 때다. (GET은 필요 없음)
  1. @RequestParam 쪽 코드를 DTO 클래스의 객체로 수정
  2. Map 객체로 작성되어 있는 Response Body를 DTO 클래스의 객체로 변경

DTO 클래스의 대표적인 단점

  • Controller 클래스가 늘어남에 따라 DTO 클래스가 두 배
    (ex. xxxxPostDto + xxxxPatchDto)씩 늘어나게 된다.
    • 공통된 멤버 변수의 추출 및 내부 클래스를 이용해서 어느 정도 개선 가능

사용기능,용어 정리

@RequestBody

  • (JSON 형식의) Request Body를 MemberPostDto 클래스의 객체로 변환을 시켜주는 역할

@ResponseBody

  • (JSON 형식의) Response Body를 클라이언트에게 전달하기 위해 DTO 클래스의 객체를 Response Body로 변환하는 역할

Spring MVC에서는 핸들러 메서드에 @ResponseBody 애너테이션이 붙거나 핸들러 메서드의 리턴 값이 ResponseEntity일 경우, 내부적으로 HttpMessageConverter가 동작하게 되어 응답 객체(여기서는 DTO 클래스의 객체)를 JSON 형식으로 바꿔준다.

역직렬화(Deserialization)
요청 받은 JSON 형식의 데이터를 DTO 같은 Java의 객체로 변환하는 것

직렬화(Serialization)
응답데이터를 전송하기 위해 DTO 같은 Java의 객체를 JSON 형식으로 변환하는 것

핵심 정리

  • JSON 형식의 Request Body를 전달 받기 위해서는 DTO 객체에 @RequestBody 애너테이션을 붙여야 한다.
  • Response Body를 JSON 형식으로 전달하기 위해서는 @ResponseBody 애너테이션을 메서드 앞에 붙여 주어야하지만 ResponseEntity 객체를 리턴 값으로 사용할 경우 @ResponseBody 를 생략할 수 있다.

DTO 유효성 검증(Validation)

1차적으로 프론트엔드 쪽에서 유효성 검사를 진행하지만 자바스크립트로 전송되는 데이터는 브라우저의 개발자 도구를 사용해서 브레이크포인트(breakpoint)를 추가한 뒤에 얼마든지 그 값을 조작할 수 있기 때문에 서버 쪽에서 한번 더 유효성 검사를 진행해야 된다.

유효성 검증을 위한 의존 라이브러리 추가

DTO 클래스에 유효성 검증을 적용하기 위해서는 Spring Boot에서 지원하는 Starter가 필요하다.

build.gradle 파일의 dependencies 항목에 'org.springframework.boot:spring-boot-starter-validation’을 추가해야 한다.

기능정리

@NotBlank(message = "~~~")

  • 정보가 비어있지 않은지를 검증
  • null 값이나 공백(””), 스페이스(” “) 같은 값들을 모두 허용하지 않음
  • 유효성 검증에 실패하면 @NotBlank 의 message 애트리뷰트에 지정한 문자열이 에러 메시지로 콘솔에 출력
    • 괄호 안쓰고 @NotBlank만 쓰면 유효성 검증에 실패했을 때 에러 메시지가 콘솔에 출력

@Email

  • 유효한 이메일 주소인지를 검증
  • 유효성 검증에 실패하면 내장된 디폴트 에러 메시지가 콘솔에 출력

@Pattern(regexp = "^010-\\d{3,4}-\\d{4}$", message = "~~~")

  • 휴대폰 정보가 정규표현식(Reqular Expression)에 매치되는 유효한 번호인지를 검증
  • 유효성 검증에 실패하면 내장된 디폴트 에러 메시지가 콘솔에 출력

@Valid

  • 유효성 검증 애너테이션이 추가된 DTO 클래스에서 유효성 검증 로직이 실행되게 하기 위해서는 아래와 같이 DTO 클래스에 @Valid 애너테이션을 추가해야 한다.
  • 메서드 매개변수쪽(@RequestBody앞)에 사용
@RestController
@RequestMapping("/v1/members")
public class MemberController {
    @PostMapping
    public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
        return new ResponseEntity<>(memberDto, HttpStatus.CREATED);
    }
		...
		...
}

@Pattern()

@Pattern(regexp = "^\\S+(\\s?\\S+)*$", message = "~~~")
  • 이름 정보가 비어있으면(null) 유효성 검증을 하지 않는다.
  • 이름 정보가 비어 있지 않고(not null), 공백 문자열이라면 검증에 실패한다.
  • 시작 문자가 공백이면 검증에 실패한다.
  • 끝 문자가 공백이면 검증에 실패한다.
  • 문자와 문자 사이 공백이 1개를 이상이면 검증에 실패한다.

정규 표현식(Reqular Experssion)

  • ‘^’은 문자열의 시작을 의미
  • ‘$’는 문자열의 끝을 의미
  • ’는 ‘’ 앞에 평가할 대상이 0개 또는 1개 이상인지를 평가
  • ‘\s’는 공백 문자열을 의미
  • ‘\S’ 공백 문자열이 아닌 나머지 문자열을 의미
  • ‘?’는 ‘?’ 앞에 평가할 대상이 0개 또는 1개인지를 의미
  • ‘+’는 ‘+’ 앞에 평가할 대상이 1개인지를 의미

@Min(1)

  • 1이상일 경우에만 유효성 검증 통과

@Validated

  • @PathVariable이 추가된 변수에 유효성 검증이 정상적으로 수행되려면 추가해줘야하는 애너테이션
  • class레벨(@RequestMapping뒤)에 사용

DTO 클래스의 유효성 검증을 위해서 사용한 위 기능들은 Jakarta Bean Validation이라는 유효성 검증을 위한 표준 스펙에서 지원하는 내장 애너테이션들이다.

Jakarta Bean Validation

  • 라이브러리처럼 사용할 수 있는 API가 아닌 스펙(사양, Specification) 자체다. ( 일종의 기능 명세를 의미 )
  • Java Bean 스펙을 준수하는 Java 클래스라면 Jakarta Bean Validation의 애너테이션을 사용해서 유효성 검증을 할 수 있다.

Custom Validator를 사용한 유효성 검증

Jakarta Bean Validation에 내장된(Built-in) 애너테이션 외에도 필요한 기능이 있다면 직접 애너테이션을 정의해 사용할 수 있다.

Custom Validator를 구현하기 위한 절차

  1. Custom Validator를 사용하기 위한 Custom Annotation을 정의한다.
  2. 정의한 Custom Annotation에 바인딩 되는 Custom Validator를 구현한다.
  3. 유효성 검증이 필요한 DTO 클래스의 멤버 변수에 Custom Annotation을 추가한다.

DTO 유효성 검사실습

NotSpace 인터페이스

  • 공백을 허용하지 않는 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 {};
}

@Target, @Retention는 Section1내용 참고

@Target = 애너테이션을 적용할 “대상"을 지정하는 데 사용
@Retention = 특정 애너테이션의 지속 시간을 결정하는 데 사용

@Constraint (제약조건)

  • @NotSpace 애너테이션이 멤버 변수에 추가되었을 때, 동작 할 Custom Validator를 연결해주는 용도로 사용했다고 생각하면 된다.

NotSpaceValidator 클래스

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

<NotSpace, String> 제네릭에서 NotSpace는 CustomValidator와 매핑된 Custom Annotation(@NotSpace)을 의미하며, String은 Custom Annotation으로 검증할 대상 멤버 변수의 타입을 의미

CoffeeController 클래스

import com.codestates.coffee.CoffeePatchDto;
import com.codestates.coffee.CoffeePostDto;
import org.springframework.http.HttpStatus;
//import org.springframework.http.MediaType;
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;
//import java.util.HashMap;
//import java.util.Map;
//import java.util.Objects;

@RestController
@RequestMapping("/v1/coffees")
@Validated // @Min() 등을 처리해주는 것
public class CoffeeController {

    @PostMapping
    public ResponseEntity postCoffee(@Valid @RequestBody CoffeePostDto coffeePostDto){

        return new ResponseEntity<>(coffeePostDto, 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}") // 클라이언트가 서버에 리소스를 조회할 때 사용하는 애너테이션
    public  ResponseEntity getCoffee(@PathVariable("coffee-id")long coffeeId){ // 특정 회원의 정보를 클라이언트 쪽에 제공하는 핸들러 메서드
        System.out.println("# coffeeId: " + coffeeId);

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

    @GetMapping
    public ResponseEntity getMembers() { // 회원 목록을 클라이언트에게 제공하는 핸들러 메서드
        System.out.println("# get coffees");

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

@Valid와 @Validated의 차이점에 대해 의문이 생겨 추가로 블로깅했다.

CoffeePostDto 클래스

import org.hibernate.validator.constraints.Range;

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

public class CoffeePostDto {
    @NotBlank
    private String korName;
    @NotBlank
    @Pattern(regexp = "^([a-zA-Z]+\\s?[a-zA-Z])+$") // 영어만 허용
    // [A]* : A가 0개 이상이다.
    // [A]+ : A가 1개 이상이다.
    // \\s? : 공백이 0개 or 1개다.
    // 즉, "^([a-zA-Z]+\\s?[a-zA-Z])*$"는 알파벳(몇개인지 상관x) + 공백 + 알파벳(몇개인지 상관x)이다.
    // n\d* : n 뒤에 숫자가 0개 이상이라는 의미. “n”, “n1”, “n123” 에 모두 매치된다.
    private String engName;
    @Range(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;
    }
}

CoffeePostDto 클래스에서 영어만 허용하도록 @Pattern을 사용하는 과정에서 정규표현식 때문에 고생 좀 했다.

CoffeePatchDto 클래스

package com.codestates.coffee;

import com.codestates.member.NotSpace;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Pattern;

public class CoffeePatchDto {
    private long coffeeId;
    @NotSpace // null가능하게 해줘야 함
    // @Pattern(regexp ="^\\S+(\\s?\\S+)*$") // << 이거로 대신 써도 됨
    private String korName;
    @Pattern(regexp = "^([a-zA-Z]+\\s?[a-zA-Z])*$")
    private String engName;
    @Range(min = 100, max= 50000)
    private Integer price; // int로 하면 null이 안들어간다. 기본값인 0이 들어가기 때문에 range에 걸려서 선택적으로 사용 못함

    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 Integer getPrice() {
        return price;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }
}

price변수 유효성 검사에서 null을 수용할 수 잇게 int 대신 Integer를 써야 했다.


정규표현식 문법 정리

정규표현식 확인 사이트

^ 문자열의 시작
$ 문자열의 끝
. 임의의 한 문자
* 문자가 0번 이상 발생
+ 문자가 1번 이상 발생
? 문자가 0번 혹은 1번 발생
[ ]  문자의 집합 범위를 나타냄
[0-9] : 숫자 (0부터 9)
[a-z] : 알파벳 (a부터 z)

앞에 ^가 나타나면 not을 의미
{ }  횟수 또는 범위를 의미
( )  소괄호 안의 문자를 하나의 문자로 인식
|  or 조건
\ 확장 문자의 시작 
\b 단어의 경계
\B 단어가 아닌 것의 경계
\A 입력의 시작부분
\G 이전 매치의 끝
\Z 입력의 끝이지만 종결자가 있는 경우
\z 입력의 끝
\s 공백문자
\S 공백문자가 아닌 나머지 문자
\w 알파벳이나 숫자
\W 알파벳이나 숫자를 제외한 문자
\d [0-9]와 동일
\D 숫자를 제외한 모든 문자

java.validation 어노테이션 정리

Anotation 제약조건
@NotNull Null 불가
@Null Null만 입력 가능
@NotEmpty Null, 빈 문자열 불가
@NotBlank Null, 빈 문자열, 스페이스만 있는 문자열 불가
@Size(min=,max=) 문자열, 배열등의 크기가 만족하는가?
@Pattern(regex=) 정규식을 만족하는가?
@Max(숫자) 지정 값 이하인가?
@Min(숫자) 지정 값 이상인가
@Future 현재 보다 미래인가?
@Past 현재 보다 과거인가?
@Positive 양수만 가능
@PositiveOrZero 양수와 0만 가능
@Negative 음수만 가능
@NegativeOrZero 음수와 0만 가능
@Email 이메일 형식만 가능
@Digits(integer=, fraction = ) 대상 수가 지정된 정수와 소수 자리 수 보다 작은가?
@DecimalMax(value=)  지정된 값(실수) 이하인가?
@DecimalMin(value=) 지정된 값(실수) 이상인가?
@AssertFalse false 인가?
@AssertTrue true 인가?

0개의 댓글