[SpringBoot]Validation 에러 처리하기

Validation Check 하기

스프링에서 받는 request들의 validation check를 하는 방법은 아래와 같이 두 가지 방법이 있다.

  1. 수동으로 하나씩 코드를 작성해서 맞는지 확인하는 방법
  2. JSR-303 Validator 를 사용하는 방법

1. 수동으로 하나씩 코드를 작성해서 맞는지 확인하는 방법

첫번째 방법인 수동으로 개발자가 작성하여서 validation check를 한다면 아래와 같은 방법으로 만들수가 있다.

장점 : 여러가지 자신이 원하는 값들이 들어오지않으면 명확하게 세분화해서 작성 할 수 있었다.

단점 : null 값 체크, 빈값, 최소값, 최대값, 패턴 등 다양한 방법을 하기 위해서는 시간과 노력이 많이 든다.

@PostMapping
public String saveSchool(@RequestBody RequestSchool requestSchool) {

    validationCheck(requestSchool);

    schoolService.save(requestSchool);

    return "school saved";
}

private void validationCheck(RequestSchool requestSchool) {
    Optional.ofNullable(requestSchool.getAccount()).orElseThrow(RuntimeException::new);
    Optional.ofNullable(requestSchool.getPhoneNumber()).orElseThrow(RuntimeException::new);
    Optional.ofNullable(requestSchool.getEstablishedYear()).orElseThrow(RuntimeException::new);
    Optional.ofNullable(requestSchool.getNumberOfStudent()).orElseThrow(RuntimeException::new);
}

2. Bean Validation를 사용

Bean Validation 초기에는 JSR 303으로 처음 제안이 되어 만들어 지기 시작해서 JSR-349 거처 현재는 Bean Validation JSR-380 까지 지원을 한다.

JSR-380을 사용하려면 최소 자바 버전은 8 을 사용해야한다.

380에서 추가 지원하는 것들

  • Support for validating container elements by annotating type arguments of parameterized types, e.g. List<@Positive Integer> positiveNumbers; this also includes:
    • More flexible cascaded validation of collection types; e.g. values and keys of maps can be validated now: Map<@Valid CustomerType, @Valid Customer> customersByType
    • Support for java.util.Optional
    • Support for the property types declared by JavaFX
    • Support for custom container types by plugging in additional value extractors
  • Support for the new date/time data types for @Past and @Future; fine-grained control over the current time and time zone used for validation
  • New built-in constraints: @Email, @NotEmpty, @NotBlank, @Positive, @PositiveOrZero, @Negative, @NegativeOrZero, @PastOrPresent and @FutureOrPresent
  • All built-in constraints are marked as repeatable now
  • Parameter names are retrieved using reflection
  • ConstraintValidator#initialize() is a default method

Bean Validation 을 간단히 말하면 @javax.validation.Valid 어노테이션을 사용하여 내부적으로 검증이 수행 하는 것이다.

참고 : https://en.wikipedia.org/wiki/Bean_Validation

간단히 jsr-380에서 알아봤으면 Spring 에서 적용 하는방법을 알아보자

Spring에서 JSR-380 사용하기(정리)

springboot에서 jsr-380 validation을 사용하기 위해서는 아래의 패키지를 추가 해주어야한다

//maven

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

//gradle
implementation 'org.springframework.boot:spring-boot-starter-validation:2.5.4'

org.springframework.boot:spring-boot-starter-validation:2.5.4 를 살펴 보면

<dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
      <version>2.5.4</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-el</artifactId>
      <version>9.0.52</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.hibernate.validator</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>6.2.0.Final</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>

hibername-validator가 추가가 된것을 볼수 있다.

BeanValidation은 명세만 한것이지 구현한 코드가 아니기 때문에 hibernate.validator의 코드로 구현된것을 사용한다.

구조를 살펴 보면 아래와 같다 .

hibernate.validator.contraints 패키지 내부에 보면 아래와 같이 미리 정의된 메시지들과, 클래스들을 확인할수 있다. 자세한 내용은 다음에 ...

hibernate에 미리 정의된 나라별 메시지들

실습

실습 환경 : SpringBoot

School 이라는 정보를 생성하기 위해 RequestSchool 이라는 객체로 받을 때

RequestSchool.java

package com.morris.validation;

import lombok.*;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.Email;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RequestSchool {

    @NotNull(message = "이름이 비어 있습니다.")
    private String name;

    @Pattern(regexp = "^\\d{3}-\\d{3,4}-\\d{4}$", message = "000-0000-0000 의 형식이 맞지 않습니다.")
    private String phoneNumber;

    @Range(min = 1800, max= 2200, message = "설립 연도는 1800년 부터, 2200년도까지 등록이 가능합니다.")
    private Integer establishedYear;

    @Email(message="이메일 형식이 아닙니다.")
    private String account;

    @Min(value =  0 ,message = "학생의 수는 0이 아니여야 합니다.")
    private Integer numberOfStudent;
}

SchoolController.java

RequestBody에는 @Valid 어노테이션을 붙여서 사용한다.

@Valid 어노테이션을 사용하면 RequestBody를 객체로 변환할때 validation check를 하게 된다.

package com.morris.validation;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

@RestController
@RequestMapping("/school")
public class SchoolController {

    private final SchoolService schoolService;

    public SchoolController(SchoolService schoolService) {
        this.schoolService = schoolService;
    }

        @PostMapping
    public ResponseEntity<?> saveSchool(@RequestBody @Valid RequestSchool requestSchool) {

        schoolService.save(requestSchool);

        return ResponseEntity.ok("school saved");
    }

}

테스트

package com.morris.validation;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SchoolControllerTest {

    @Autowired
    private SchoolController schoolController;

    @Autowired
    private MockMvc mockMvc;

    private ObjectMapper objectMapper;

    @BeforeAll
    public void setup() {
        objectMapper = new ObjectMapper();
    }

    @Test
    void createSchoolValidationCheck() throws Exception {

        RequestSchool requestSchool = RequestSchool.builder()
                .build();

        mockMvc.perform( post("/school")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(requestSchool))
        ).andDo(print())
        .andExpect(status().isBadRequest())
        ;
    }

}

테스트의 결과는 아래와 같다.

Body에 보면 모두 null 값이 들어갔기 때문에 status().isBadRequest() 아래와 같이 예상결과로 두었다.

결과를 보면 org.springframework.web.bind.MethodArgumentNotValidException 이 발생한 것을 알수 있다.

MockHttpServletRequest:
  HTTP Method = POST
  Request URI = /school
   Parameters = {}
      Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"93"]
         Body = {"name":null,"phoneNumber":null,"establishedYear":null,"account":null,"numberOfStudent":null}
Session Attrs = {}

Handler:
         Type = com.morris.validation.SchoolController
       Method = com.morris.validation.SchoolController#saveSchool(RequestSchool)

Async:
Async started = false
 Async result = null

Resolved Exception:
         Type = org.springframework.web.bind.MethodArgumentNotValidException

하지만 내가 원하는 MethodArgumentNotValidException에 대한 자세한 에러 메시지를 확인하기 위해서는

mockMvc로 테스트 한값을 result로 받아서 에러 메시지를 출력 받기 위해 코드를 추가 를 했다.

@Test
void createSchoolValidationCheck() throws Exception {

    RequestSchool requestSchool = RequestSchool.builder()
            .build();

    MvcResult result = mockMvc.perform(post("/school")
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(requestSchool))
    ).andDo(print())
            .andExpect(status().isBadRequest())
            .andReturn();

    String errorMessage = result.getResolvedException().getMessage();

    System.out.println(errorMessage);

}

결과는 아래로, Request객체에서 정의를 해두었던 이름이 비어 있습니다. 가 제대로 출력 되는 것을 볼수 있다.

Validation failed for argument [0] in public java.lang.String com.morris.validation.SchoolController.saveSchool(com.morris.validation.RequestSchool): [Field error in object 'requestSchool' on field 'name': rejected value [null]; codes [NotNull.requestSchool.name,NotNull.name,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [requestSchool.name,name]; arguments []; default message [name]]; default message [이름이 비어 있습니다.]]

에러 처리가 된것을 확인 했으니 결과 값을 보내려면 에러를 어떻게 핸들링을 해야할까??

컨트롤러의 메서드 뒤에 사용하게 되면 바인딩 할때 BindingResult 를 사용하면 validation의 결과를 받아 확인이 가능하다.

@PostMapping
public ResponseEntity<?> saveSchool(@RequestBody @Valid RequestSchool requestSchool, BindingResult bindingResult) {

    if( bindingResult.hasErrors() ) {

        ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();

        return ResponseEntity.badRequest().body(objectError.getDefaultMessage());
    }

    schoolService.save(requestSchool);

    return ResponseEntity.ok("school saved");
}

테스트 코드

이전에는 MvcResult result에서 result.getResolvedException().getMessage(); 를 통해서 결과를 받아왔었지만, body에 default 메시지를 포함해서 보낼 수 있다.

@Test
  void createSchoolValidationCheck() throws Exception {

      RequestSchool requestSchool = RequestSchool.builder()
              .build();

      MvcResult result = mockMvc.perform(post("/school")
              .contentType(MediaType.APPLICATION_JSON)
              .content(objectMapper.writeValueAsString(requestSchool))
      ).andDo(print())
              .andExpect(status().isBadRequest())
              .andReturn();

  }

결과

MockHttpServletResponse:
           Status = 400
    Error message = null
          Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"30"]
     Content type = text/plain;charset=UTF-8
             Body = 이름이 비어 있습니다.
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

하지만 여기서 다시 Controller 에 validation 체크의 코드들을 전부 적어준다면, 코드에 중복된 코드들이 많아진다.

이런 부분들을 @ExceptionHandler를 사용해서 Exception이 발생했을 경우에 ResponseEntity를 custom한 객체로 만들어서 보내줄수 있다.

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> ValidationException(MethodArgumentNotValidException exception) {
    exception.getBindingResult().getAllErrors().forEach((error) -> {
        String fieldName = ((FieldError) error).getField();
        String errorMessage = error.getDefaultMessage();
    });
    ObjectError objectError = exception.getBindingResult().getAllErrors().stream().findFirst().get();
    String errorMsg = "field : " + ((FieldError) objectError).getField() + ", errorMessage" + objectError.getDefaultMessage();
    return ResponseEntity.badRequest().body(errorMsg);
}

참고 사항

Controller의 파라미터에 BindingResult를 사용하면서 내부에 BindingResult에 대한 비지니스 로직이 없으면

BindingResult를 통해서 에러를 찾았다고 해서 그냥 다음의 비지니스 로직으로 실행이 되므로 BindingResult를 사용한다면 반드시 result에 대한 에러처리를 해주어야하고, ExceptionHandler를 통해서 한다고 하면 BindingResult 를 파라미터에 함게 사용하면 안 된다.

참고 사이트

https://medium.com/@SlackBeck/javabean-validation과-hibernate-validator-그리고-spring-boot-3f31aee610f5

https://www.baeldung.com/spring-boot-bean-validation

validation 알아보기

https://beagle-dev.tistory.com/142

https://sanghye.tistory.com/36

https://jeong-pro.tistory.com/195

https://www.popit.kr/javabean-validation과-hibernate-validator-그리고-spring-boot/

JNDI는 디렉터리 서비스에서 제공하는 데이터 및 객체를 발견하고 참고(look up) 하기 위한 자바 API .

자바애플리케이션을 외부 디렉터리 서비스에 연결( 데이터베이스 , LDAP 서버)

자바 애플릿이 호스팅 웹 컨테이너가 제공하는 구성 정보를 참고

JNDI (Java Naming and Directory Interface)는 Java 프로그래밍 언어로 작성된 애플리케이션에 이름 지정 및 디렉토리 기능을 제공합니다. 특정 이름 지정 또는 디렉터리 서비스 구현과 독립적으로 설계되었습니다. 따라서 새로운 서비스, 새로운 서비스, 이미 배포 된 서비스 등 다양한 서비스에 공통된 방식으로 액세스 할 수 있습니다.

JNDI 아키텍처는 API (Application Programming Interface)와 SPI (Service Provider Interface)로 구성됩니다. Java 애플리케이션은이 API를 사용하여 다양한 이름 지정 및 디렉토리 서비스에 액세스합니다. SPI는 다양한 이름 지정 및 디렉토리 서비스를 투명하게 플러그인 할 수 있도록하여 JNDI 기술의 API를 사용하는 Java 애플리케이션이 서비스에 액세스 할 수 있도록합니다.

Concepts :

컴퓨팅 시스템의 기본 기능은 이름 지정 서비스

이름을 개체와 연결하고 이름을 기반으로 개체를 찾는 수단

모든 컴퓨터ㅇ 프로그램이나 시스템을 사용할 때 개체 또는 다른 개체의 이름을 지정한다.

컴퓨터의 파일에 엑세스 하려면 해당 이름을 제공하야한다.

예를들어 인터넷 도메인 이름 시스템(DNS) 는 컴퓨터 이름 IP를 ip 주소에 매핑한다.

예시 : www.morriskim.com ==> 192.168.58.29

파일 시스템은 프로그램이 파일 내용에 엑세스 하는데 사용할 수 있는 파일 참조에 파일이름을 매핑한다.

두 가지에는 인터넷상의 개체 이름 지정부터 로컬 파일 시스템의 파일 이름지정에 이르기 까지 이름 지정서비스가 존재하는 광범위한 규모를 보여준다.

예시 : c:\bin\exec.bat

Naming system에서 개체를 조회하려면 개체 이름을 제공하게 되는데

이름 지정 시스템은 이름이 따라야하는 구문을 결정한다.

명명규칙이라고 하는데, 이름의 포현은 이름의 구성 요소를 표시하는 구성요소 구분으로 기호로 구성됨

명명 시스템 구성 요소 분리 기호 이름
unix "/" /usr/hello
DNS "." morris.com
LDAP " , " 또는 " =. ". Cn = Rosana Lee

Context

모든 컨텍스트에는 연관된 명명 규칙이 존제함

하위 컨텍스트에 바인딩 된 컨텍스트의 몇 가지 예입니다.

이처럼 Java 프로그래밍 언어를 사용해서 작성된 애플리케이션에 이름 지정 및 디렉토리 기능을 제공하는 API

Architecture

JNDI 아키텍쳐는 API DHK SPI (SERVICE PROVIDER INTERFACE) 로 구성된다.

JNDI 아키텍처

Java 어플리케이션은 JNDI API를 사용하여 이름 지정 및 디렉토리 서비스에 접근한다. SPI를 사용하면 다양한 이름 지정 및 디렉토리 서비스를 플러그인으로 할수 있으므로 JNDI API를 사용하는 Java 애플리케이션이 서비스에 엑세스 할 수 있다.

JNDI 는 Java SE 플랫폼에 포함되어 있습니다. JNDI를 사용하려면 JNDI 클래스와 하나 이상의 서비스 공급자가 있어야한다.

  • LDAP ( Lightweight Directory Access Protocol)
  • CORBA ( Common Object Request Broker Architecture), COS( Common Object Services) 이름 서비스
  • JAVA RMI
  • DNS (도메인 이름 서비스)

JNDI 5개 패키지로 나뉨

  • avax.naming
  • javax.naming.directory
  • javax.naming.ldap
  • javax.naming.event
  • javax.naming.spi

Tutorial

JNDI를 사용하여 네임 스페이스 업데이트를 위한 읽거 및 이름 지정 작업을 할 수 있다.

구성 하는 방법 :

이름 지정, 또는 디렉터리 서비스에 대한 작업을 수행하기 전에 네임 스페이스의 시작점인 초기 컨텍스트를 획득 해야합니다.

이름 지정 및 디렉터리 서비스에 대한 모든 방법이 일부 컨텍스트에 상대적으로 수행 되기 때문에 초기 컨텍스트를 얻으려면 아래의 단계를 거쳐야한다.

  1. 엑세스 하려는 서비스의 제공업체 선택
  2. 초기 컨텍스트에 필요한 구성 지정
  3. InitialContext 생성자 호출

1단계 서비스 공급자 선택

환경 들을 Hashtable을 만들고 여기에 서비스 공급자 클래스의 이름을 추가, 초기 컨텍스트에 사용할 서비스 공급자 지정하기

LDAP서비스를 사용하는 경우에는

Hashtable <String, Object> env = new Hashtable <String, Object> ();
env.put (Context.INITIAL_CONTEXT_FACTORY, 
        "com.sun.jndi.ldap.LdapCtxFactory");

파일 서비스를 지정하려면

Hashtable <String, Object> env = new Hashtable> String, Object> ();
env.put (Context.INITIAL_CONTEXT_FACTORY, 
        "com.sun.jndi.fscontext.RefFSContextFactory")

2단계 초키 컨텍스트의 정보 지정


package javax.naming.Context 를 보면 아래를 보면 Context에서는 미리 서비스 공급자를 설정하도록 설정이 이미 들어가있다.

아래의 getInitialContext 메서드를 보면 getInitialContext를 에 hashTable을

env.put (Context.PROVIDER_URL, "ldap : //ldap.wiz.com : 389");
env.put (Context.SECURITY_PRINCIPAL, "joeuser");
env.put (Context.SECURITY_CREDENTIALS, "joepassword");

아래를 보면 Java.naming Context 안에는 아래와 같은 메서드들이 이미 설정 되어있다.

3단계 초기 컨텍스트 생성

이전에 설정한 파일을 InitialContext를 생성자로 전달해서 생성

그러면 추후부터는 Context Object에 대한 참조가 있으므로 이름 지정 서비스에 접근이 가능하다.


사용방법

Naming Interface

JNDI 이름에 대한 구성요소 이름과 구문을 관리할수 있는 기능

 try {
            Name objectName = new CompositeName("java:comp/env/jdbc");
            Enumeration<String> elements = objectName.getAll();
            while(elements.hasMoreElements()){
                System.out.println(elements.nextElement());
            }
            objectName.add("AddComposition");
            objectName.get(1);

            System.out.println(objectName.get(objectName.size()-1));
        } catch (
            InvalidNameException e) {
            e.printStackTrace();
        }

/ 는 하위 컨텍스트의 구분 기호

출력 내용

java:comp
env
jdbc
AddComposition

Context Interface

컨텍스트에는 이름 지정 및 디렉터리 서비스에 대한 속성도 포함이 됨

Spring의 SimpleNamingContextBuilder NamingManager는 JNDI 공급자를 생성한 다음 NamingManager를 사용하여 빌더를 실행함

JNDI가 어떤것인지에 대해 알아보는 것만 알아보는 내용이었으므로 이후 세부적인 내용은 아래의 자세한 내용은 아래의 블로그를 참고하시면됩니다.

https://epthffh.tistory.com/entry/Spring%EC%97%90%EC%84%9C-JNDI%EC%84%A4%EC%A0%95hikaricp

참고 :

https://docs.oracle.com/javase/tutorial/jndi/index.html

https://docs.oracle.com/javase/tutorial/jndi/overview/index.html

https://www.baeldung.com/jndi

'ProgramLanguage > Java' 카테고리의 다른 글

JVM 메모리 구조 (1)  (0) 2020.03.22
Java8- Default 메서드(Abstract, Interface)  (0) 2020.03.18
JVM 튜닝  (0) 2020.02.10
Lambda_Expression(2)  (0) 2020.01.25
객체 지향 설계 5원칙 - SOLID  (0) 2019.12.26

한글 컬럼을 불러오고 싶다면 

` 백틱을 앞뒤에 붙여주면된다.

select `이름` from student

+ Recent posts