유효성 검사로 객체를 검사해보자! (24.05.17)

YJ·2024년 5월 17일
post-thumbnail

블로그 작성법
목표 > 공부한 내용 > 얻었고, 앞으로 이걸 해봐야지 적기

✋ 수업 목표

  • 유효성 검사
  • DTO <-> Entity 변환을 어디서 할지 알아보자.
  • JDBC / JPA / SpringData JPA

시중 은행: Mybatic -> JPA + Spring + MSA
금융권 현주소: Mybatis, batis

🤔 공부한 내용

MSA

  • 에러를 고치는 비용이 적다.
  • 유지보수 하는 비용은 크다.

유효성 검사

내가 생각하는 유효성 검사는 입력이 잘 들어왔는지 확인하는 것이다.

유효성 검사란?
: 값이 있는지(null), 형식(ex> 이메일)에 맞는지, 숫자를 줘야하는데 문자를 줬을때

유효성 검사 위치

Service가 맞을까 Controller가 맞을까?
내 생각은 Controller가 맞을 것 같다.

유효성 검사는 입력값이 맞는지 를 검사하는 것이다.
즉, FrontEnd에서 입력 받은 값이 있는지(null), 형식(이메일)에 맞는지
숫자를 받아야 하는데 문자를 주진 않았는지 등을 체크한다.

검증
데이터 중복인지 아닌지

유효성 검사 실습

  1. MemberController - 회원가입
    1.1 DTO 생성
    DTO는 기존 Member 클래스와 동일하게
    사용자 아이디
    비밀번호
    이름
    이메일
    전화번호
    를 저장하는 DTO를 생성하였다.

DTO -> Entity는 어디에 위치하는게 좋을까?

Controller => DTO로 이사

[고려사항]
1. 누가봐도 변환한다는 의미인 convertToEntity 메소드명
2. Entity는 좀.. 원본이니까 지켜줬으면 좋겠어요.
3. 어차피 Dto or Entity로 옮길거면, 꺼내써야 하는 필드가 있는 곳으로 옮기면 어때?

@Valid

@Valid 어노테이션을 사용하여 유효성 검사를 진행할 것이다.

  1. build.gradle 업데이트
    @Valid는 Spring 업데이트 이후로 패키지를 추가해줘야 사용 가능하다.
    따라서,
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation'

해당 텍스트를 build.gradle에 추가한 후, gradle을 업데이트 해줘야 사용할 수 있다.

  1. @Valid 어노테이션 추가
  • 유효성 검사용 코드 전체

@Valid를 추가하였다.
RequestBody로 들어오는 객체에 대한 유효성 검사를 진행할 것이기에,
매개변수에 @Valid 어노테이션을 추가하였다.

  1. @Valid 코드
package com.shoppingmall.shoppingmall.member;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import jakarta.validation.constraints.*;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class MemberDto {
    /**
     * Member를 구분하기 위한 id 정보
     */
    @NotBlank
    @Size(min = 1, max = 15, message = "아이디는 1 ~ 15자 이여야 합니다!")
    private String userId;
    /**
     * Member의 비밀번호 정보
     */
    @NotBlank
    @Size(min = 1, max = 15, message = "비밀번호는 1 ~ 15자 이여야 합니다!")
    private String pw;
    /**
     * Member의 실제 이름 정보
     */

    @NotBlank
    @Size(min = 1, max = 10, message = "이름은 1 ~ 10자 이여야 합니다!")
    private String name;
    /**
     * Member의 이메일 정보
     */
    @Email
    @Size(min = 1, max = 10, message = "이메일은 1 ~ 10자 이여야 합니다!")
    private String email;
    /**
     * Member의 전화번호
     */
    @NotBlank
    @Size(min = 12, max = 13, message = "전화번호는 12 ~ 13자 이여야 합니다!")
    private String contact;

    public Member convertToEntity() {
        return new Member(userId, pw, name, email, contact);
    }

    @Override
    public String toString() {
        return "MemberDTO{" +
                "userId='" + userId + '\'' +
                ", pw='" + pw + '\'' +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", contact='" + contact + '\'' +
                '}';
    }
}
  1. Error가 발생할 경우
    다음과 같이 Postman으로 요청해보았다.

응답은 다음과 같이 bad request가 전달되었다.

Spring 터미널에는 다음과 같은 에러가 발생하였다.

2024-05-17T10:39:17.295+09:00  WARN 35788 --- [shoppingmall] [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.shoppingmall.shoppingmall.utils.ApiUtils$ApiResult<java.lang.String> com.shoppingmall.shoppingmall.member.MemberController.joinByApiResult(com.shoppingmall.shoppingmall.member.MemberDto) with 2 errors: [Field error in object 'memberDto' on field 'userId': rejected value [null]; codes [NotBlank.memberDto.userId,NotBlank.userId,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [memberDto.userId,userId]; arguments []; default message [userId]]; default message [must not be blank]] [Field error in object 'memberDto' on field 'email': rejected value [tjdbwns19@naver.com]; codes [Size.memberDto.email,Size.email,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [memberDto.email,email]; arguments []; default message [email],10,1]; default message [이메일은 1 ~ 10자 이여야 합니다!]] 

어떻게 해결해야 할까?

@Valid에 의해 발생한 Validation Error는 기본적으로 Errors 인터페이스 타입의 객체에 담긴다.

따라서 메서드의 인자로 Errors 타입의 객체를 받는지, 안받는지에 따라 처리가 달라진다.

아래는 Errors를 처리하기전 코드이다.

@PostMapping("/join/api/result") // After
    public ApiUtils.ApiResult<String> joinByApiResult(@Valid @RequestBody MemberDto memberDto) {
        
        if (Validator.isAlpha(memberDto.getName())) {
            // 유저 이름을 log로 출력
            log.info(memberDto.toString());

1차: Errors 인터페이스로 처리

이제 Errors를 처리해보았다.

    @PostMapping("/join/api/result") // After
    public ApiUtils.ApiResult<String> joinByApiResult(@Valid @RequestBody MemberDto memberDto, Errors errors) {
        try {
            if (errors.hasErrors()) {
                throw new NotValidException(errors.getObjectName());
            }
        } catch(NotValidException e) {
            log.info(e.getMessage() + " 가 유효성 검사를 통과하지 못하였습니다!");
            return error("유효성 검사 실패", HttpStatus.CONFLICT);
        }


        if (Validator.isAlpha(memberDto.getName())) {
            // 유저 이름을 log로 출력
            log.info(memberDto.toString());

            // ID 중복 체크
            // 중복이면 사용자 예외 클래스 소환
            //      1) 예외 클래스한테 니가 return 해!
            //      2) 예외만 발생 시키고.. 메세지는 내가 보낼게
            if (isDuplicateId(memberDto)) {
                return error("아이디 중복", HttpStatus.CONFLICT);
            }

            Member requestMember = memberDto.convertToEntity();

            // Repository에 저장 시도
            String userId = memberService.join(requestMember);

//            {
//	                “success” : True,
//	                “response” : 응답 데이터(객체),
//	                “error” : null
//            }

            try {
                log.info(userId);
            } catch (NullPointerException e) {
                return success(userId);
            }
            return success(userId);
        } else
            return error("아이디 숫자 포함", HttpStatus.BAD_REQUEST);
    }

위 방식으로 해결하였을때, DTO의 Error Message에 대한 궁금증이 생겼다.

2차: BindingResult 인터페이스로 처리

MemberController.java

    @PostMapping("/join/api/result") // After
    public ApiUtils.ApiResult<String> joinByApiResult(@Valid @RequestBody MemberDto memberDto, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return error(bindingResult.getAllErrors().toString(), HttpStatus.BAD_REQUEST);
        }


        if (Validator.isAlpha(memberDto.getName())) {
            // 유저 이름을 log로 출력
            log.info(memberDto.toString());

            // ID 중복 체크
            // 중복이면 사용자 예외 클래스 소환
            //      1) 예외 클래스한테 니가 return 해!
            //      2) 예외만 발생 시키고.. 메세지는 내가 보낼게
            if (isDuplicateId(memberDto)) {
                return error("아이디 중복", HttpStatus.CONFLICT);
            }

            Member requestMember = memberDto.convertToEntity();

            // Repository에 저장 시도
            String userId = memberService.join(requestMember);

//            {
//	                “success” : True,
//	                “response” : 응답 데이터(객체),
//	                “error” : null
//            }

            try {
                log.info(userId);
            } catch (NullPointerException e) {
                return success(userId);
            }
            return success(userId);
        } else
            return error("아이디 숫자 포함", HttpStatus.BAD_REQUEST);
    }

GlobalExceptionController.java

@ControllerAdvice
public class GlobalExceptionController {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Map<String, String> handleValidationExceptions(BindingResult bindingResult) {
        Map<String, String> errors = new HashMap<>();
        bindingResult.getAllErrors().forEach(c -> errors.put(((FieldError)c).getField() , c.getDefaultMessage()));

        return errors;
    }
}

위 방법은
유효성 검사에 통과하지 못할 경우
GlobalExceptionController 클래스의 handleValidationExceptions 메서드가 호출됩니다.
해당 메서드는 발생한 모든 에러들을 Map<String, String> 으로 변환하여 Cotroller의 Response Format에 전달합니다.
현재는 Response Format의 Error 객체가 message가 String 타입을 받게 되어있어, Map<String, String> -> String 으로 변환하여 전달하였습니다.

그러나, Map을 toString()으로 반환하여 전달하니
다음과 같이 응답이 찍히는 것을 확인하였다.

앞으로 다음과 같이 수정할 것이다.
1. Response Format에서 getError 리스트를 저장
2. 클라이언트 메시지를 가공하여 전달

참고자료
https://sanghye.tistory.com/36

@Valid와 @Validated의 차이

  1. 표준 vs 스프링 전용
  • @Valid는 자바 표준 스펙의 일부이며, 자바 EE 및 다른 프레임워크에서도 사용할 수 있다.
  • @Validated는 스프링 프레임워크에서 제공하는 어노테이션으로, 스프링 애플리케이션에서만 사용할 수 있다.
  1. 유효성 검증 범위
  • @Valid는 객체 속성 수준의 유효성 검증에 사용된다.
  • @Validated는 메서드 수준의 유효성 검증에 사용된다. 이를 통해 더 세부적인 제어가 가능하다.
  1. 그룹 지정
  • @Valid는 그룹 지정이 어렵다.
  • @Validated는 그룹 지정이 가능하여, 상황에 따라 다른 검증 규칙을 적용할 수 있다.
  1. 예외 처리
  • @Valid는 표준 javax.validation 예외를 사용한다.
  • @Validated는 스프링 고유의 MethodArgumentNotValidException을 사용한다.

스프링과 JPA

DB 연결 방법

JPA는 자바의 API이다.

Spring은 우리가 자바로 웹을 만들 수 있는 프레임워크

웹: 정보 공유 -> 데이터가 중요하다.

Spring은 우리가 JPA를 더 편하게 쓸 수 있게 도와주고 싶은

Spring Data JPA

알쓸송잡
* 1분 자기 소개 -> 1(나): 3 중 1 (웃상, 인자)
면접 질문 포부
코끼리 : 받고 싶은 질문
프로젝트 기반 장점 3가지
첫번째
00 프로젝트 ~경험했습니다. 접속사/는데요 금지

이슈 > 해결방안 한줄.
연습 : 40초

지식: 내 프로젝트에 들어간 것 합격 당락
- 진짜 (이해하고) 알고 있나?

문제해결
창의력(오리지널리티)


    

😉 앞으로 이걸 해봐야지

오늘은 유효성 검사를 통해 원하는 형식의 데이터가 클라이언트로부터 전달되었는지 확인하는 시간을 가졌다. 이를 통해, 전달 받을 데이터의 형식을 정의해봐야겠다. 또한, JPA에 대한 개념을 배웠으므로 주말동안 한 번 MySQL을 연결해보고 사용해보고 싶다.

profile
dev

0개의 댓글