[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

intelliJ를 사용해서 프로젝트 시작 중 아래와 같은 에러가 발생했을때

CreateProcess error=206, 파일 이름이나 확장명이 너무 깁니다

아래와 같은 플러그 인으로 해결

요약

문제 원인 :

gradle 에서 java를 실행할때, commandline으로 실행을 하는데, classpath의 길이가 너무 길어서 발생

이것을 manifest jar로 만들어서 실행해줌


gradle-util-plugins

When classpath for a Gradle JavaExec task is long, Windows command executions give error because of limitation to command line length greater than 32K.

With a number of classpath dependencies in a large project, typically JavaExec Gradle task fails with error "The filename or extension is too long" and this would be a stopping error. To solve this issue, use ManifestClasspath plugin.

ManifestClasspath plugin creates a manifest jar for jar files in the classpath of JavaExec task and sets the classpath with manifest jar.

Usage

To use the plugin, define a dependency in build script and have plugin entry in Gradle project.

build.gradle snippet to use ManifestClasspath plugin

Build script snippet for plugins DSL for Gradle 2.1 and later

plugins {
  id "com.github.ManifestClasspath" version "0.1.0-RELEASE"
}

Build script snippet for use in older Gradle versions or where dynamic configuration is required

buildscript {
  repositories {
    maven {
      url "https://plugins.gradle.org/m2/"
    }
  }
  dependencies {
    classpath "gradle.plugin.com.github.viswaramamoorthy:gradle-util-plugins:0.1.0-RELEASE"
  }
}

apply plugin: "com.github.ManifestClasspath"

https://github.com/viswaramamoorthy/gradle-util-plugins

'BackEnd > ETC' 카테고리의 다른 글

Git remote: Permission to  (0) 2020.03.08
http 상태 코드  (0) 2020.01.19
CQRS란 ?  (1) 2020.01.19
STORM 정리  (0) 2017.12.18

Elasticsearch를 설치하고 실행할 때, 아래와 같은 오류가 나서 실행이 되지 않는다.

[WARN ][o.e.b.Natives            ] unable to loa
d JNA native support library, native methods will be disabled.
java.lang.UnsatisfiedLinkError: /tmp/elasticsearch.PRdXLpjT/jna--19853545
63/jna2901201660730912229.tmp: /tmp/elasticsearch.PRdXLpjT/jna--198535456
3/jna2901201660730912229.tmp: failed to map segment from shared object: O
peration not permitted
        at java.lang.ClassLoader

Operation not permitted
tmp/elasticsearch 를 허가하지않는것(tmp디렉토리 접근 관련 권한 문제)

해결 방법
jvm.options 파일의 -Djava.io.tmpdir 의 경로를 루트의 tmp가 아닌 elasticsearch를 사용하는 유저의 디렉토리로 설정 해서 해결
 68 # log4j 2
 69 -Dlog4j.shutdownHookEnabled=false
 70 -Dlog4j2.disable.jmx=true
 71
 72 #-Djava.io.tmpdir=${ES_TMPDIR} ##원본
 73 -Djava.io.tmpdir=/home/userid/es_temp ##수정 한부분

SLF4J: Detected both log4j-over-slf4j.jar AND bound slf4j-log4j12.jar on the class path, preempting StackOverflowError

위와 같은 이슈가 발생했다.

다른 프로젝트를 gradle compile 로 가지고 오는데 다른프로젝트의 내부에서 slf4j-log4j12 라이르러리를 가지고 있어서 , springboot에서 빌드 할때 bridge가 log4j12-over-slf4j 로 되어있고, binding이 slf4j-log4j12 로 가지게 되면 계속 무한 반복이 일어나기 때문에 아래와 같은 에러를 발생시키는것이다.

아래 세부 로그를 보면 binding 할때 두개의 라이브러리를 찾았다. logback 과 slf4j-log4j12 이다 .

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/C:/Users/KDY/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.2.3/7c4f3c474fb2c041d8028740440937705ebb473a/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/C:/Users/KDY/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-log4j12/1.7.16/54c6dd23a7c420e40b8848e962d5f2a3534260af/slf4j-log4j12-1.7.16.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.

import 하는 프로젝트의 gradle 설정에서 아래와 같이 slf4j-log4j12 바인딩의 라이브러리를 제외 시켜 줌 으로써 해결

configurations.all{
    exclude group: 'org.slf4j', module: 'slf4j-log4j12'
}

스프링 부트 관련 Bridge, Binding 에 대한 설명은 아래의 포스트에 설명이 되어있다.
https://deviscreen.tistory.com/118

'BackEnd > SpringBoot' 카테고리의 다른 글

[SpringBoot]Validation 에러 처리하기  (0) 2021.08.22
Spring Boot Logging (1)  (0) 2020.04.16
Request Body 의 값 BadRequest 로 보내기  (0) 2020.04.11
SpringBoot(4) -T-Academy  (0) 2019.05.24
SpringBoot(3)-T-Academy  (0) 2019.05.22

스프링 부트에서의 로깅

스프링 부트에서 알아서로깅을 해주는데 어떻게 해주는 것일까 ?

스프링 캠프 백기선님의 스프링 부트 와 로깅 을 들으면서 정리한 페이지 입니다.

https://www.youtube.com/watch?v=o2-JaRD9qQE&t=907s

스프링 부트와 로깅

  • 스프링 부트는 JCL을 사용하여 로깅 코드를 작성
  • 스프링 부트 애플리케이션은 SLF4J를 사용한다.
  • 스프링 부트 애플리케이션은 Logback을 사용한다.
  • 원한다면 얼마든지 다른 로거를 사용 할 수 있다.

JCL (Jakarta Commons Logging)

org.apache.commons.logging.log

JCL을 사용할 때는 debug모드 일때만 사용할 수 있도록

if문을 사용해서 로깅을 했었음 .

예시

if(this.log.isDebugEnabled()){
  this.log.debug("Jakarta Commons Logging")
}

특징

  • 로깅 라이브러리가 아니라 로깅 추상화 라이브러리

  • 로깅 라이브러리 선택권은 애플리케이션 개발자의 것

  • 라이브러리나 프레임워크는 주로 로깅 추상화 라이브러리를 사용한다.

    LOG --> LOG4J

로거 추상화 라이브러리 어떻게 찾을 것인가 ?

JCL이 로깅 구현체를 찾는 방법

  • 설정 파일에서 찾기
  • 애플리케이션 클래스 패스에서 Log4J 구현체 찾아보기
  • 애플리케이션이 JDK 1.4에서 구동중인지 확인하기
  • 아무것도 못찾으면 기본 구현체 사용

JCL 사용시 겪을수 있는 클래스 로더 문제

http://articles.qos.ch/classloader.html

JCL을 사용 안하는이유

  • 자바 클래스로더의 기본 동작방식
    • getClass 등 classloader를 통해서 읽어오는데, deligation이 되어있다. 상위에서 먼저 찾아오고 이후에 없으면 기본 자기자신이 읽어올수 있는 클래스를 읽어온다.
  • 서블릿 컨테이너의 클래스로더 동작 방식
    • 클래스로더가 어떻게 동작해서 어떻게 읽어오는 스펙이 없다. 그리고 구현체(jetty,tomcat) 마다 각자 다 다르다.
    • JCL을 사용할때는 여러가지 classloader문제가 발생 할 수 있다.
    • 같은 클래스를 넣어뒀는데도 불구하고 casting을 할 수 없을 수도 있다.
    • 메모리 누수 문제 발생 할 수 도 있다.
  • JCL은 클래스로더에 의존적인 방법으로 구현체를 찾는다.

Spring은 의존성이 없다. 말은 즉 개발자가 어떤 것을 쓸지는 maven에 optional로 지원한다.

Spring에 JCL의 의존성은 가지고 온다. context.jar에서 가지고 온다.

springboot는 JCL의 의존성을 끊어 두었다.(https://spring.io/blog/2009/12/04/logging-dependencies-in-spring/)

Spring의 필수 로깅 종속성은 Jakarta Commons Logging API (JCL)입니다. JCL에 대해 컴파일하고 스프링 프레임 워크를 확장하는 클래스에 JCL 로그 객체를 표시합니다. 모든 버전의 Spring이 동일한 로깅 라이브러리를 사용하는 것이 중요합니다 .Spring을 확장하는 응용 프로그램과도 역 호환성이 유지되므로 마이그레이션이 쉽습니다.이를 수행하는 방법은 Spring의 모듈 중 하나가 커먼즈 로깅에 명시 적으로 의존하도록하는 것입니다. (JCL의 정식 구현) 다음 다른 모든 모듈을 컴파일 타임에 의존하도록하십시오. 예를 들어 Maven을 사용하고 커먼즈 로깅에 대한 종속성을 어디에서 선택했는지 궁금하다면 Spring에서, 특히 spring-core라는 중앙 모듈에서 가져온 것입니다.
Commons-Logging의 좋은 점은 응용 프로그램을 작동시키기 위해 다른 것이 필요하지 않다는 것입니다. 클래스 경로의 잘 알려진 위치에서 다른 로깅 프레임 워크를 찾고 런타임에 알 맞는 것으로 생각되는 프레임 워크를 사용합니다 (필요한 경우 어떤 프레임 워크인지 알 수 있음). 다른 것이 없다면 JDK (java.util.logging 또는 JUL)에서 아주 멋진 로그를 얻습니다. 대부분의 상황에서 Spring 응용 프로그램이 작동하고 콘솔에서 행복하게 로그에 기록되는 것이 중요합니다.
불행히도 커먼즈 로깅에 대한 최악의 점과 새로운 툴로 인기를 얻지 못한 것은 런타임 발견 알고리즘이기도합니다. 시계를 되돌리고 지금 새 프로젝트로 Spring을 시작할 수 있다면 다른 로깅 종속성을 사용합니다. 아마도 첫 번째 선택은 SLF4J (Simple Logging Facade for Java ) 일 것입니다.이 로깅 은 사람들이 애플리케이션에서 Spring과 함께 사용하는 다른 많은 도구에서도 사용됩니다.
공통 로깅을 해제하는 것은 쉽습니다. 런타임에 클래스 경로에 있지 않은지 확인하십시오. Maven 용어로 종속성을 제외하고 Spring 종속성이 선언되는 방식으로 한 번만 수행하면됩니다.

SLF4J

  • Simple Logging Facade For Java

  • 로깅 라이브러리를 런타임이 아닌 컴파일 시점에 의존성 정보를 판단하여 실행됨

    • dependency에 어떤것이 있으니까 어떤 것을 써야겠구나 하는 것을 결정함.
    • Dependency 설정을 잘해야한다.
  • 세가지 모듈(Bridging, API, Binding) 제공

    • SLF4J API

      • 로깅 인터페이스 : 그냥 껍데기 일 뿐
      • slf4j-api-{version}.jar
      • org.slf4j.Logger
      • Org.slf4j.LoggerFactory
        JCL 과는 달리 if문으로 별도 debug를 설정 하지 않아도 부하가 없다.
    • Binding

      • SLF4J api 인터페이스를 로깅 구현체로 연결
      • 어댑터 역할
        • 여러 로그로 연결 해주는 역할
        • logback, slf4j 의존성등 다양하게 사용이 가능
          아래와 같은 것 중 하나만 사용해야 한다.
          • slf4j-log4j12-{version}.jar
          • slf4j-jdk14-{version}.jar
          • slf4j-nop-{version}.jar
          • slf4j-jcl-{version}.jar
          • logback-classic-{logbook-version}.jar
      • SLF4J 인터페이스를 직접 구현한 구현체도 있다.
      • 라이브러리나 프레임워크는 사용 하지 말기!!!

      API --> Binding


    • Bridge

      • 로거 호출을 slf4j 인터페이스로 연결
        이전의 만들어진 레거시 (jcl, log4j, jul) 코드를 최종적으로 slf4j 로 변경 해주는 것

        • Log4J 호출을 SLF4J API 로 연결하기

          • Log4j-over-slf4j.jar 추가
            • Log4j 호출을 받아서 SLF4J API 호출
            • Log4J 인터페이스를 구현하고 있음
          • 의존성 에서 log4j.jar 제거
          • Slf4j-log12.jar랑 같이 쓰면 안됨
            • 무한루프 생성
        • JCL 호출을 SLF4J 로 연결하기

          • 의존성에 jcl-over-slf4j.jar 추가

            • JCL 호출을 받아서 SLF4J API 호출
            • JCL 인터페이스를 구현하고 있음
          • 의존성에 common-logging.jar 제거

          • Slf4j-jcl.jar랑 같이 쓰면 안됨

            • 무한루프
      Log4J --> Bridge --> API

브릿지랑 바인더랑 같은 종류를 쓰면 안됨 이유는 아래와 같이 Slf4j 브릿지와 slf4j 바인딩을 같이 쓰면 무한 반복이 될수 있기 때문에 같은 것을 쓰면안된다.

    모든 로깅을 logback으로 하려면?

로그백을 제외한 다른 로거에 호출을 다 지워주고 바인더는 logback 바인더만 호출하면 된다.

Springboot 에서는

Springboot-logging 이라는 것을 사용해서 마지막 그림의 설정을 해주었음.

Java에서 UserDto를 통해 requestbody를 받아 온다.

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
    private String firstName;
    private String address;
    private LocalDateTime birthday;
    private int age;
}

user객체에 필요한 값만 가지고 와서 modelMapper를 통해 맵핑 해준다.

import org.modelmapper.ModelMapper;
...
@Autowired
ModelMapper modelMapper

@PostMapping
    public ResponseEntity createUser(@RequestBody UserDto userDto){

        User user = modelMapper.map(userDto, User.class);
        User newUser = this.userRepository.save( user);

        //create는 uri를 반환해야한다.
        URI  createdURI = linkTo(
                UserController.class) //EventController 클래스에서 메서드를 선언
                .slash(newUser.getId())
                .toUri();
//        event.setId(100);
        return ResponseEntity.created(createdURI).body(user);
    }

여기서 이번시간에 공부할 내용은 userDto에서 다른 값(id, sex)이 들어온다고 하면 badrequest를 보내고 싶다.

하지만 지금 코드에서는 201로 응답 한다.

해결방법 :

SpringBoot 에서는 UserDto를 requestbody json으로 받으면 Jackson 라이브러리를 통해 알아서 serialization되어 객체로 반환한다.

이럴때 application.properteis 에 설정을 해주면 400 badrequest를 반환한다.

spring.jackson.deserialization.fail-on-unknown-properties=false

Jackson 라이브러리에서의 serialization, desrialization

deserialization : json을 객체로 변환하는 것

serialization : 객체를 json으로 변환하는 것

인프런 강좌 백기선 SpringRestAPI를 보면서 정리한 것입니다.

아래와 같은 에러가 발생하는 이슈가 있다.

Caused by: org.apache.spark.SparkException: Unable to create database default as failed to create its directory /home/morris/server/spark/spark-warehouse
        at org.apache.spark.sql.catalyst.catalog.InMemoryCatalog.liftedTree1$1(InMemoryCatalog.scala:114)
        at org.apache.spark.sql.catalyst.catalog.InMemoryCatalog.createDatabase(InMemoryCatalog.scala:108)
        at org.apache.spark.sql.internal.SharedState.<init>(SharedState.scala:99)
        at org.apache.spark.sql.SparkSession$$anonfun$sharedState$1.apply(SparkSession.scala:101)
        at org.apache.spark.sql.SparkSession$$anonfun$sharedState$1.apply(SparkSession.scala:101)
        at scala.Option.getOrElse(Option.scala:121)
        at org.apache.spark.sql.SparkSession.sharedState$lzycompute(SparkSession.scala:101)
        at org.apache.spark.sql.SparkSession.sharedState(SparkSession.scala:100)
        at org.apache.spark.sql.internal.SessionState.<init>(SessionState.scala:157)
        ... 26 more
Caused by: java.net.ConnectException: Call From localhost/192.168.76.102 to namenode:9000 failed on connection exception: java.net.ConnectException: 연결이 거부됨; For more details see:  http://wiki.apache.org/hadoop/ConnectionRefused
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)

위의 내용은 데이터 파일을 읽어올 때, 경로가 아래와 같이 일반 url이 적혀있어서 에러가 발생한다.
        Dataset<Row> dataset = sparkSession.read()
                .format("csv")
                .option("header", "true")
                .load("/home/morris/data/TestData.csv");

위와 같이 아무것도 적어주지 않으면 default로 hdfs의 namenode에 접속을 한다.

로컬의 파일을 선택할 경우에는 아래와 같이 file:// 을 붙여줘야한다.

        Dataset<Row> dataset = sparkSession.read()
                .format("csv")
                .option("header", "true")
                .load("file:///home/morris/data/TestData.csv");

SparkSQL JDBC

read.format("jdbc") 와 read.jdbc 의 차이는 뭘까 ?

결과 : 불러오는 것에 대한 코드는 같지만, format을 통해 다양한 옵션을 사용해서 JDBC에서 데이터를 불러오는 성능을 향상 시킬수 있다.

// Loading data from a JDBC source
//전체 데이터를 나눠서 사용할수 있는 옵션 설정 가능
val jdbcDF = spark.read
  .format("jdbc")
  .option("url", "jdbc:postgresql:dbserver")
  .option("dbtable", "schema.tablename")
  .option("user", "username")
  .option("password", "password")
  .load()

val connectionProperties = new Properties()
connectionProperties.put("user", "username")
connectionProperties.put("password", "password")
//전체 데이터를 한번에
val jdbcDF2 = spark.read
  .jdbc("jdbc:postgresql:dbserver", "schema.tablename", connectionProperties)


// Specifying the custom data types of the read schema
connectionProperties.put("customSchema", "id DECIMAL(38, 0), name STRING")
val jdbcDF3 = spark.read
  .jdbc("jdbc:postgresql:dbserver", "schema.tablename", connectionProperties)

https://spark.apache.org/docs/latest/sql-data-sources-jdbc.html

데이터브릭스의 홈페이지에서 이유를 알수 있었다.

Push down a query to the database engine

You can push down an entire query to the database and return just the result. The table parameter identifies the JDBC table to read. You can use anything that is valid in a SQL query FROM clause.

Scala

// Note: The parentheses are required.
val pushdown_query = "(select * from employees where emp_no < 10008) emp_alias"
val df = spark.read.jdbc(url=jdbcUrl, table=pushdown_query, properties=connectionProperties)
display(df)

spark.read.jdbc 의 메서드를 사용하면, 전체 데이터를 가지고 오는 경우가 된다.

해당 쿼리를 하나의 Executor에서 DBMS로 쿼리를 날려서 갖고오게 되는데, 이 경우 메모리가 부족하면 GC가 발생할 수 있다.

그래서 더 좋은 방법은 spark.read.format("jdbc").option("key","value").load() 를 하는 방법도 있다. 하지만 파티션을 지정하지 않는다면 JDBC의 단일 쿼리를 사용해서 하나의 파티션에 모든 데이터를 가지고 온다. 그렇기 때문에 파티션을 설정하고 executor에 나눠서 데이터를 가지고 오는것을 추천한다.

val jdbcDF = spark.read
  .format("jdbc")
  .option("url", "jdbc:postgresql:dbserver")
  .option("dbtable", "schema.tablename")
  .option("user", "username")
  .option("password", "password")
    .option("partitionColumn",)
    .option("lowerBound",1)
    .option("upperBound",10000)
    .option("numberPartitions",5)
  .load()

주요 옵션 설정

partitionColumn : 데이터 집합을 분할할때 사용되는 열의 이름

lowerBound : 읽어올 최소값

upperBound : 읽을 최대값

numberPartitions : 파티션의 수

fetchSize : 원격의 데이터베이스에서 fetch되는 행 수를 제어하는 매개변수
기본 값에서 약간만 늘리면 성능이 크게 향상 될 수 있다.

값을 너무 낮게 하면 전체결과 집합을 가지고 오기 위해서 외부 데이터베이스 간에 많은 요청으로 작업부하에 지연이됨.

너무 많은 요청을 DBMS에서 핸들링 못하는 것에 대한 에러가 발생 할 수 있다.

https://docs.databricks.com/data/data-sources/sql-databases.html

https://medium.com/@radek.strnad/tips-for-using-jdbc-in-apache-spark-sql-396ea7b2e3d3

https://jaceklaskowski.gitbooks.io/mastering-spark-sql/spark-sql-DataFrameReader.html

+ Recent posts