본문 바로가기

개발중/Spring

[SpringBoot] Spring Validation 기록 끄적 📸

728x90
반응형

Spring Validation

 

Spring Validation 에 대해서 알아보자.

오늘 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술의 Bean Validation 에 대해서 강의를 들었다.

 

 

사실 개발을 할 때 큰 고민중에 하나가 "검증" 부분이다.

 

풀스택으로 개발할 때는 검증에 대해 큰 고민이 없었던게 사실이다.

'프론트에서 내가 값을 잘 보내면 되니까' 라는 생각이었던 것 같다.

 

그러다 백엔드와 프론트를 분리 후 협업하며

내가 API 를 제공하는 포지션에 서있다보니 검증에 대한 중요성에 대해 깊이 고민하게 되었던 것 같다.

 

예를 들어 User 라는 dto가 있을 때 각 컬럼에 대해서 유효성을 설정하고

@Getter
@RequiredArgsConstructor
public class User {

	@Email
	private final String email;

	@NotBlank
	private final String pw;

	@NotNull
	private final UserRole userRole;

	@Min(12)
	private final int age;

}

 

아래와 같이 Controller 메소드에 @Valid 를 붙혀줌으로써 검증을 할 수 있다고 한다.

@PostMapping("/user/add") 
public ResponseEntity<Void> addUser(@RequestBody @Valid User user) {
      ...
}

 

근데 현재 스웨거로 제공할 때 아래와 같이 parameter 를 dto 로 받는게 아닌 각 속성을 받고 있다.

 @ApiOperation(
            value   = "사용자 저장"
            , notes = "사용자를 저장한다")
    @GetMapping(value = "/user/add")
    public ResponseEntity<Void> addUser(  
            @ApiParam(
                name          = "age"
                , type        = "int"
                , value       = "나이"
                , example     = "1")
            @RequestParam(value = "age", defaultValue = "0")
            @Nullable int age

            , @ApiParam(
                name          = "email"
                , type        = "String"
                , value       = "이메일"
                , example     = "")
            @RequestParam(value = "email", defaultValue = "")
            @Nullable String email 
            
            , ... ) {
            
     . . .
     
}

 

dto 로 받지 않는 이유는 모든 api 마다 받아야하는 속성이 다른데,

dto 로 받게 되면 사용하지 않는 모든 속성들이 parameter 에 노출되기 때문이다.

 

더보기

를 들어서 나는 dept 를 받을 필요가 없는데

dto 의 모든 속성이 노출되기 때문에 API 를 사용하는 입장에서 헷갈릴 수 밖에 없다.

 

 

그래서 나는 dto 별로 api 에서 다르게 속성을 보이게 할 수 없을까 ? 라는

고민을 여전히 하는중이다.

 

솔직히 하나하나 명시해주기도 귀찮고 , ㅋ

 

같은 dto 를 사용하는데

A 이벤트에서는 hobby 가 필수값이고

B 이벤트에서는 hobby 가 필수값이 아닐수도 있으니 ..

 

API parameter 에 dto 를 쓰지 못하는게 내 개발방식이다 ..

그러니 오늘 배운 @Valid와 @Validated 를 사용하지 못하는게 너무 아쉽다.

 

혹은 내가 잘못하고 있는 생각 때문에 이 좋은 @Valid와 @Validated들을

프로젝트에 쉽사리 적용하지 못하는 것은 아닐까 ? 라는 의문도 든다.

 

@Valid와 @Validated를 적용하지 못하고

유효성 체크를 꼼꼼히 하는 탓에 서비스단이 너무 길어지는 것이 싫어서

나는 각 API 별로 Validate 를 따로 관리를 하며 개발을 한다.

 

 

이 검증들도 어마무시하게 길다.

 

더보기
package com.rsn.POMS.api.crowdsourcing.labellingwork.service;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import com.rsn.POMS.api.crowdsourcing.labellingwork.dao.LabellingWorkDao;
import com.rsn.POMS.api.crowdsourcing.project.service.ProjectService;
import com.rsn.POMS.api.crowdsourcing.project.vo.ProjectVO;
import com.rsn.POMS.api.lucy.site.service.SiteService;
import com.rsn.POMS.api.lucy.site.vo.SiteVO;
import com.rsn.POMS.api.lucy.site.vo.response.Site;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.rsn.POMS.api.code.common.service.CodeService;
import com.rsn.POMS.api.code.common.vo.CodeVo;
import com.rsn.POMS.api.crowdsourcing.labellingwork.vo.LabellingWorkVO;
import com.rsn.POMS.api.setting.exception.CustomException;
import com.rsn.POMS.api.setting.utils.ApiResultErrorCode;

import javax.annotation.PostConstruct;

/**
 * @create @author 정수빈 @Date 2022.08
 */
@RequiredArgsConstructor
@Service("com.rsn.POMS.api.crowdsourcing.labellingwork.service.LabellingWorkValidate")
public class LabellingWorkValidate {

	private final LabellingWorkDao labellingWorkDao;
	private final SiteService siteService;
	private final ProjectService projectService;
	private final CodeService codeService;

	// 표시 유형
	private Map<String, CodeVo> crowdsourcingCode1;
	private Map<String, CodeVo> crowdsourcingCode2;
	private Map<String, CodeVo> crowdsourcingCode3;


	@PostConstruct
	public void init () {

		crowdsourcingCode1 = new HashMap<>();
		crowdsourcingCode2 = new HashMap<>();
		crowdsourcingCode3 = new HashMap<>();

		// 표시 유형
		codeService.selectCommonCodeByCodeGroupAndCodeType(CodeVo.builder().code_group("crowdsourcing").code_type("labellingworkstatus").sys_code(1).build())
				.forEach( x -> { crowdsourcingCode1.put(x.getCode_value(), x);});

		// 라벨링 수집 구분
		codeService.selectCommonCodeByCodeGroupAndCodeType(CodeVo.builder().code_group("crowdsourcing").code_type("label_channel_cd").sys_code(1).build())
				.forEach( x -> { crowdsourcingCode2.put(x.getCode_value(), x);});

		// 검색 채널
		codeService.selectCommonCodeByCodeGroupAndCodeType(CodeVo.builder().code_group("channel").code_type("channel").sys_code(1).build())
				.forEach( x -> { crowdsourcingCode3.put(x.getCode_value(), x);});
	}


	public void getLabellingWork(LabellingWorkVO vo) {

		/*
		 * workStatus
		 */
		if(vo.getWorkStatus() != 0 && !crowdsourcingCode1.containsKey(Integer.toString(vo.getWorkStatus()))) {
			throw new CustomException(ApiResultErrorCode.FieldValueException, "작업 상태를 확인해주세요. (G : crowdsourcing / T : labellingworkstatus) / 0은 전체");
		}

		/*
		 * orderByField
		 */
		String[] allowOrderByField = new String[]{"workName", "regDate", "workStatus", ""};

		if(!Arrays.stream(allowOrderByField).anyMatch(s -> s.equals(vo.getOrderByField().trim()))) {
			throw new CustomException(ApiResultErrorCode.FieldValueException, "orderByField 를 확인해주세요. ('workName' or 'regDate' or 'workStatus' or '')");
		}

		/*
		 * orderByValue
		 */
		String[] allowOrderByValue = new String[]{"ASC", "DESC", ""};

		if(!Arrays.stream(allowOrderByValue).anyMatch(s -> s.equals(vo.getOrderByValue()))) {
			throw new CustomException(ApiResultErrorCode.FieldValueException, "orderByValue 를 확인해주세요. ('ASC' or 'DESC' or '')");
		}
	}

	public void getLabellingWorkTotalCnt(LabellingWorkVO vo) {

		// 프로젝트 유효성
		Optional<ProjectVO> projectVO = Optional.ofNullable(projectService.getCroudSourcingProjectByPmSeq(ProjectVO.builder().pmSeq(vo.getPmSeq()).build()));
		if(!projectVO.isPresent()){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "존재하지 않는 프로젝트입니다.");
		}

		if(vo.getWorkStatus() != 0 && !crowdsourcingCode1.containsKey(Integer.toString(vo.getWorkStatus()))) {
			throw new CustomException(ApiResultErrorCode.FieldValueException, "작업 상태를 확인해주세요.  (G : crowdsourcing / T : labellingworkstatus) / 0은 전체");
		}
	}

	public void postLabellingWork(LabellingWorkVO vo) {

		// 프로젝트 유효성
		Optional<ProjectVO> projectVO = Optional.ofNullable(projectService.getCroudSourcingProjectByPmSeq(ProjectVO.builder().pmSeq(vo.getPmSeq()).build()));
		if(!projectVO.isPresent()){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "존재하지 않는 프로젝트입니다.");
		}

		// 검색 날짜 필수
		if(vo.getISdate() == null || vo.getIEdate() == null || vo.getISdate().equals("") || vo.getIEdate().equals("")){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "검색 날짜를 확인해주세요.");
		}

		// 작업 기간 필수
		if(vo.getWorkSdate() == null || vo.getWorkEdate() == null || vo.getWorkSdate().equals("") || vo.getWorkEdate().equals("")){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "작업 기간를 확인해주세요.");
		}

		// Date Format 체크
		isDateFormat(vo.getISdate(), "yyyy-MM-dd", "iSdate 형식을 확인해주세요. (2022-01-01)");
		isDateFormat(vo.getIEdate(), "yyyy-MM-dd", "iEdate 형식을 확인해주세요. (2022-01-01)");
		isDateFormat(vo.getWorkSdate(), "yyyy-MM-dd", "workSdate 형식을 확인해주세요. (2022-01-01)");
		isDateFormat(vo.getWorkEdate(), "yyyy-MM-dd", "workEdate 형식을 확인해주세요. (2022-01-01)");

		// 작업명
		if(vo.getWorkName() == null || vo.getWorkName().equals("")){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "workName 를 확인해주세요.");
		}

		// 검색 채널 OR 사이트 지정 둘 중 하나는 필수 값
		if(vo.getChannelCd() != null && !crowdsourcingCode2.containsKey(vo.getChannelCd())) {
			throw new CustomException(ApiResultErrorCode.FieldValueException, "channelCd 를 확인해주세요. (G : crowdsourcing / T : label_channel_cd) ");
		}

		// 검색 채널 값 유효성
		if(vo.getChannelCd().equals("1")){
			for(String channelSeq : vo.getSearchSite().split(",")){
				if(vo.getSearchSite() == null || !crowdsourcingCode3.containsKey(channelSeq)){
					throw new CustomException(ApiResultErrorCode.FieldValueException, "유효하지 않은 채널 값입니다.");
				}
			}

			// 사이트 값 유효성
		}else if(vo.getChannelCd().equals("2")){
			for(String siteSeq : vo.getSearchSite().split(",")){
				Optional<Site> site = Optional.ofNullable( siteService.getSiteBysSeq(SiteVO.builder().sSeq(Integer.parseInt(siteSeq)).build()));
				if(!site.isPresent()){
					throw new CustomException(ApiResultErrorCode.FieldValueException, "유효하지 않은 사이트 값입니다.");
				}
			}
		}

		// 파일 다운로드 허용여부
		if(!vo.getDownloadAllowYn().equals("Y") && !vo.getDownloadAllowYn().equals("N")) {
			throw new CustomException(ApiResultErrorCode.FieldValueException, "downloadAllowYn 를 확인해주세요. ( Y or N )");
		}

		// 작업자 지정 여부
		if(!vo.getWorkerAssignYn().equals("Y") && !vo.getWorkerAssignYn().equals("N")) {
			throw new CustomException(ApiResultErrorCode.FieldValueException, "workerAssignYn 를 확인해주세요. ( Y or N )");
		}

		if(vo.getWorkerAssignYn().equals("Y") && vo.getWorkerSeqList().trim().equals("")) {
			throw new CustomException(ApiResultErrorCode.FieldValueException, "workerAssignYn 가 'Y' 인데 작업자가 선택되지 않았습니다.");
		}

		// 작업자별 데이터건수
		if(vo.getWorkerAssignYn().equals("Y") && vo.getDataCntPerWorker() <= 0) {
			throw new CustomException(ApiResultErrorCode.FieldValueException, "dataCntPerWorker 를 확인해주세요. ( 0건 이상 )");
		}

		// 중복 체크
		if(labellingWorkDao.overLapLabellingWork(vo) > 0){
			throw new CustomException(ApiResultErrorCode.OverlapException, "중복된 값 입니다.");
		}
	}

	/*
	 * 라벨링 작업 - 데이터 업로드 / EXCEL,CSV 유효성
	 */
	public void uploadLabellingWorkByExcelOrCsv (LabellingWorkVO vo){

		if(vo.getFile().isEmpty()){
			throw new CustomException(ApiResultErrorCode.OverlapException, "빈 파일입니다.");
		}

		// 라벨링 작업 - 데이터 업로드 공통 유효성
		uploadLabellingWork(vo);
	}

	/*
	 * 라벨링 작업 - 데이터 업로드 / RDB 유효성
	 */
	public void uploadLabellingWorkByRdb (LabellingWorkVO vo){

		// RDB 정보 : IP/URL
		if(StringUtils.isBlank(vo.getRdbIP())){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "rdbIP 를 확인해주세요.");
		}

		// RDB 정보 : PORT
		if(StringUtils.isBlank(vo.getRdbPort())){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "rdbPort 를 확인해주세요.");
		}

		// RDB 정보 : DB NAME
		if(StringUtils.isBlank(vo.getRdbName())){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "rdbName 를 확인해주세요.");
		}

		// RDB 정보 : ID
		if(StringUtils.isBlank(vo.getRdbId())){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "rdbId 를 확인해주세요.");
		}

		// RDB 정보 : PASSWORD
		if(StringUtils.isBlank(vo.getRdbPassword())){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "rdbPassword 를 확인해주세요.");
		}

		// RDB 정보 : SQL
		if(StringUtils.isBlank(vo.getRdbSql())){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "rdbSql 를 확인해주세요.");
		}

		// 라벨링 작업 - 데이터 업로드 공통 유효성
		uploadLabellingWork(vo);
	}

	/*
	 * 라벨링 작업 - 데이터 업로드 / HDFS 유효성
	 */
	public void uploadLabellingWorkByHdfs (LabellingWorkVO vo){

		// HDFS flie path
		if(StringUtils.isBlank(vo.getHdfsFilePath())){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "hdfsFilePath 를 확인해주세요.");
		}

		// 라벨링 작업 - 데이터 업로드 공통 유효성
		uploadLabellingWork(vo);
	}

	/*
	 * 라벨링 작업 - 데이터 업로드 / 공통 유효성
	 */
	public void uploadLabellingWork(LabellingWorkVO vo) {

		// 프로젝트 유효성
		Optional<ProjectVO> projectVO = Optional.ofNullable(projectService.getCroudSourcingProjectByPmSeq(ProjectVO.builder().pmSeq(vo.getPmSeq()).build()));
		if(!projectVO.isPresent()){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "존재하지 않는 프로젝트입니다.");
		}

		// 작업 기간 필수
		if(vo.getWorkSdate() == null || vo.getWorkEdate() == null || vo.getWorkSdate().equals("") || vo.getWorkEdate().equals("")){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "작업 기간를 확인해주세요.");
		}

		// Date Format 체크
		isDateFormat(vo.getWorkSdate(), "yyyy-MM-dd", "workSdate 형식을 확인해주세요. (2022-01-01)");
		isDateFormat(vo.getWorkEdate(), "yyyy-MM-dd", "workEdate 형식을 확인해주세요. (2022-01-01)");

		// 작업명
		if(vo.getWorkName() == null || vo.getWorkName().equals("")){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "workName 를 확인해주세요.");
		}

		// 파일 다운로드 허용여부
		if(!vo.getDownloadAllowYn().equals("Y") && !vo.getDownloadAllowYn().equals("N")) {
			throw new CustomException(ApiResultErrorCode.FieldValueException, "downloadAllowYn 를 확인해주세요. ( Y or N )");
		}

		// 작업자 지정 여부
		if(!vo.getWorkerAssignYn().equals("Y") && !vo.getWorkerAssignYn().equals("N")) {
			throw new CustomException(ApiResultErrorCode.FieldValueException, "workerAssignYn 를 확인해주세요. ( Y or N )");
		}

		// 작업자 지정일 때 유효성
		if(vo.getWorkerAssignYn().equals("Y")){

			// 작업자 일련번호
			if(StringUtils.isBlank(vo.getWorkerSeqList()) || vo.getWorkerSeqList().trim().equals("")) {
				throw new CustomException(ApiResultErrorCode.FieldValueException, "workerSeqList 를 확인해주세요. ( workerAssignYn 값이 Y 이므로 필수값입니다. )");
			}

			// 작업자별 데이터건수
			if(vo.getDataCntPerWorker() <= 0) {
				throw new CustomException(ApiResultErrorCode.FieldValueException, "dataCntPerWorker 를 확인해주세요. ( 0건 이상 )");
			}

			// 할당 작업자 수 유효성 체크
			if(vo.getDataCntPerWorker() * vo.getWorkerSeqList().trim().split(",").length > vo.getLwmdiCnt()){
				throw new CustomException(ApiResultErrorCode.FieldValueException, "할당할 수 있는 데이터가 적습니다. ( (이렇게 되야해요) 작업자 수 * 작업자별 데이터건수 < 업로드 데이터 건수 )");
			}
		}

		// 중복 체크
		if(labellingWorkDao.overLapLabellingWork(vo) > 0){
			throw new CustomException(ApiResultErrorCode.OverlapException, "중복된 값 입니다.");
		}

		// 제목 Field
		if(StringUtils.isBlank(vo.getTitleField())){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "titleField 를 확인해주세요.");
		}

		// 내용 Field
		if(StringUtils.isBlank(vo.getContentField())){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "contentField 를 확인해주세요.");
		}

		// 출처 Field
		if(StringUtils.isBlank(vo.getSourceField())){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "sourceField 를 확인해주세요.");
		}

		// 수집일시 Field
		if(StringUtils.isBlank(vo.getCollectDateField())){
			throw new CustomException(ApiResultErrorCode.FieldValueException, "collectDateField 를 확인해주세요.");
		}
	}

	public static void isDateFormat(String str2cmp, String dateFormat, String msg) {
		try {
			//  검증할 날짜 포맷 설정
			SimpleDateFormat dateFormatParser = new SimpleDateFormat(dateFormat);
			//  parse()에 잘못된 값이 들어오면 Exception을 리턴하도록 setLenient(false) 설정
			dateFormatParser.setLenient(false);
			// 대상 인자 검증
			dateFormatParser.parse(str2cmp);
		} catch (Exception e) {
			throw new CustomException(ApiResultErrorCode.FieldValueException, msg);
		}
	}

	public void patchLabelWorkManageByWorkStatus(LabellingWorkVO vo){
		if(vo.getWorkStatus() != 0 && !crowdsourcingCode1.containsKey(Integer.toString(vo.getWorkStatus()))) {
			throw new CustomException(ApiResultErrorCode.FieldValueException, "작업 상태를 확인해주세요.  (G : crowdsourcing / T : labellingworkstatus) ");
		}
	}
}

 

 

이게 맞나 ? 아직도 잘 모르겠다. 확신은 없는 나름 최선이라고 생각한 나의 개발방식이다.

아무튼 여기까지가 @Valid와 @Validated 를 공부하며 든 나의 생각이다.

 

( 피드백 환영 )

 

이제 @Valid와 @Validated를 제대로 정리해보도록 하자.


🌱 Validation

올바르지 않은 데이터를 걸러내고 보안을 유지하기 위해 데이터 검증(validation)은 여러 계층에 걸쳐서 적용된다.


Client의 데이터는 조작이 쉬울 뿐더러 모든 데이터가 정상적인 방식으로 들어오는 것도 아니기 때문에, 

Client Side뿐만 아니라 Server Side에서도 데이터 유효성을 검사해야 할 필요가 있다.


스프링부트 프로젝트에서는 @validated를 이용해 유효성을 검증할 수 있습니다.

 

🌱 Bean Validation

스프링의 기본적인 validation인 Bean validation은 클래스 "필드"에 특정 annotation을 적용하여

필드가 갖는 제약 조건을 정의하는 구조로 이루어진 검사이다.


validator가 어떠한 비즈니스적 로직에 대한 검증이 아닌, 그 클래스로 생성된 객체 자체의 필드에 대한 유효성 여부를 검증합니다.

❗️ @Valid, @Validated 차이

@Valid는 Java 에서 지원해주는 어노테이션이고 @Validated는 Spring에서 지원해주는 어노테이션이다.
@Validated는 @Valid의 기능을 포함하고, 유효성을 검토할 그룹을 지정할 수 있는 기능을 추가로 가지고 있다.

 

🌱 Spring Boot Validation 적용하기

1. spring boot 2.3 version 이상부터는 spring-boot-starter-web 의존성 내부에 있던 validation이 사라졌기 때문에, 의존성을 따로 추가해줍니다.

 

- Gradle 의존성 추가

implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation', version: '2.5.6'

 

- Maven 의존성 추가

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>2.5.6</version>
</dependency>

 

2. Contoller에서 유효성 검사를 적용할 API의 Request객체 앞에 @validated 어노테이션을 추가합니다.

 

userController.java

    @PostMapping
    public ResponseEntity<?> createUSer(@Validated @RequestBody final UserCreateRequestDto userCreateRequestDto, BindingResult bindingResult){
        if (bindingResult.hasErrors()) {
            List<String> errors = bindingResult.getAllErrors().stream().map(e -> e.getDefaultMessage()).collect(Collectors.toList());
            // 200 response with 404 status code
            return ResponseEntity.ok(new ErrorResponse("404", "Validation failure", errors));
            // or 404 request
            //  return ResponseEntity.badRequest().body(new ErrorResponse("404", "Validation failure", errors));
        }
        try {
            final User user = userService.searchUser(userCreateRequestDto.toEntity().getId());
        }catch (Exception e){
            return ResponseEntity.ok(
                    new UserResponseDto(userService.createUser(userCreateRequestDto.toEntity()))
            );
        }
        // user already exist
        return ResponseEntity.ok(
                new UserResponseDto(userService.searchUser(userCreateRequestDto.toEntity().getId()))
        );

    }

 

ErrorResponse.java

package com.springboot.server.common;
import lombok.*;
import java.util.ArrayList;
import java.util.List;

@Getter
@Setter
public class ErrorResponse {

    private String statusCode;
    private String errorContent;
    private List<String> messages;

    public ErrorResponse(String statusCode, String errorContent, String messages) {
        this.statusCode = statusCode;
        this.errorContent = errorContent;
        this.messages = new ArrayList<>();
        this.messages.add(messages);
    }

    public ErrorResponse(String statusCode, String errorContent, List<String> messages) {
        this.statusCode = statusCode;
        this.errorContent = errorContent;
        this.messages = messages;
    }
}

 

@Validated로 검증한 객체가 유효하지 않은 객체라면 Controller의 메서드의 파라미터로 있는 

BindingResult 인터페이스를 확장한 객체로 들어온다.


때문에 bindingResult.hasError() 메서드는 유효성 검사에 실패했을 때 true를 반환한다.

3. Request를 핸들링할 객체를 정의할 때 Validation 어노테이션을 통해 필요한 유효성 검사를 적용합니다.

import lombok.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;

@Getter
@Builder
@NoArgsConstructor
public class UserCreateRequestDto {

    @NotBlank(message="NAME_IS_MANDATORY")
    private String name;
    @NotBlank(message="PASSWORD_IS_MANDATORY")
    private String password;
    @Email(message = "NOT_VALID_EMAIL")
    private String email;

    public User toEntity(){
        return User.builder()
                .user_name(name)
                .email(email)
                .password(password)
                .build();
    }
}

 

유효성 검사에 적용할 수 있는 어노테이션은 다음과 같다.

 

@Null  // null만 혀용합니다.
@NotNull  // null을 허용하지 않습니다. "", " "는 허용합니다.
@NotEmpty  // null, ""을 허용하지 않습니다. " "는 허용합니다.
@NotBlank  // null, "", " " 모두 허용하지 않습니다.

@Email  // 이메일 형식을 검사합니다. 다만 ""의 경우를 통과 시킵니다
@Pattern(regexp = )  // 정규식을 검사할 때 사용됩니다.
@Size(min=, max=)  // 길이를 제한할 때 사용됩니다.

@Max(value = )  // value 이하의 값을 받을 때 사용됩니다.
@Min(value = )  // value 이상의 값을 받을 때 사용됩니다.

@Positive  // 값을 양수로 제한합니다.
@PositiveOrZero  // 값을 양수와 0만 가능하도록 제한합니다.

@Negative  // 값을 음수로 제한합니다.
@NegativeOrZero  // 값을 음수와 0만 가능하도록 제한합니다.

@Future  // 현재보다 미래
@Past  // 현재보다 과거

@AssertFalse  // false 여부, null은 체크하지 않습니다.
@AssertTrue  // true 여부, null은 체크하지 않습니다.

해당 기능들에 대한 더 세부적인 내용은 이 사이트를 참고.

🌱 Exception Handling

위에 적용한 것 처럼 에러를 처리하는 객체를 따로 생성해 가공하는 것 외에도,
@ExceptionHandler 어노테이션을 이용해 예외를 처리할 수 있습니다.

 

example1

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ConstraintViolationException.class)
public Object exception(Exception e) {
	return e.getMessage(); 
}

example2

@ExceptionHandler(MethodArgumentNotValidException.class)
protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
    log.error("MethodArgumentNotValidException : " + e.getMessage());
    final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult());
    return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}    

 

🌱 Test Code 작성

import com.springboot.server.controller.user.UserCreateRequestDto;
import org.junit.jupiter.api.*;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;


public class UserValidationTest {
    private static ValidatorFactory factory;
    private static Validator validator;

    @BeforeAll
    public static void init() {
        factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

    @AfterAll
    public static void close() {
        factory.close();
    }

    @DisplayName("빈문자열 전송 시 에러 발생")
    @Test
    void blank_validation_test() {
        // given
        UserCreateRequestDto userCreateRequestDto = new UserCreateRequestDto("", "test","email@naver.com");
        // when
        Set<ConstraintViolation<UserCreateRequestDto>> violations = validator.validate(userCreateRequestDto); // 유효하지 않은 경우 violations 값을 가지고 있다.
        // then
        assertThat(violations).isNotEmpty();
        violations
                .forEach(error -> {
                    assertThat(error.getMessage()).isEqualTo("NAME_IS_MANDATORY");
                });
    }

    @DisplayName("이메일 형식 아닌 경우 에러 발생")
    @Test
    void email_validation_test() {
        // given
        UserCreateRequestDto userCreateRequestDto = new UserCreateRequestDto("name", "test","email");
        // when
        Set<ConstraintViolation<UserCreateRequestDto>> violations = validator.validate(userCreateRequestDto);
        // then
        assertThat(violations).isNotEmpty();
        violations
                .forEach(error -> {
                    assertThat(error.getMessage()).isEqualTo("NOT_VALID_EMAIL");
                });
    }

    @DisplayName("유효성 검사 성공")
    @Test
    void validation_success_test() {
        // given
        UserCreateRequestDto userCreateRequestDto = new UserCreateRequestDto("name", "test","email@naver.com");
        // when
        Set<ConstraintViolation<UserCreateRequestDto>> violations = validator.validate(userCreateRequestDto);
        // then
        assertThat(violations).isEmpty(); // 유효한 경우
    }
}

 


 

728x90
반응형