[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/

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

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

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

Spark Jersey dependency 충돌

이슈 : 2020-03-23 16:21:03.349 [SparkUI-28] WARN o.s.jetty.servlet.ServletHandler - Error for /api/v1/applications
java.lang.NoSuchFieldError: INCLUDE_ALL

Jersey 라이브러리와 충돌이 나서 발생했던것

2020-03-23 16:21:03.349 [SparkUI-28] WARN  o.s.jetty.servlet.ServletHandler - Error for /api/v1/applications
java.lang.NoSuchFieldError: INCLUDE_ALL
    at org.glassfish.jersey.server.ResourceConfig$State.<init>(ResourceConfig.java:114)
    at org.glassfish.jersey.server.ResourceConfig.<init>(ResourceConfig.java:356)
    at org.glassfish.jersey.servlet.WebComponent.createResourceConfig(WebComponent.java:578)
    at org.glassfish.jersey.servlet.WebComponent.<init>(WebComponent.java:356)
    at org.glassfish.jersey.servlet.ServletContainer.init(ServletContainer.java:177)
    at org.glassfish.jersey.servlet.ServletContainer.init(ServletContainer.java:369)
    at javax.servlet.GenericServlet.init(GenericServlet.java:244)
    at org.spark_project.jetty.servlet.ServletHolder.initServlet(ServletHolder.java:643)
    at org.spark_project.jetty.servlet.ServletHolder.getServlet(ServletHolder.java:499)
    at org.spark_project.jetty.servlet.ServletHolder.ensureInstance(ServletHolder.java:791)
    at org.spark_project.jetty.servlet.ServletHolder.prepare(ServletHolder.java:776)
    at org.spark_project.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:580)
    at org.spark_project.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1180)
    at org.spark_project.jetty.servlet.ServletHandler.doScope(ServletHandler.java:513)
    at org.spark_project.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1112)
    at org.spark_project.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
    at org.spark_project.jetty.server.handler.gzip.GzipHandler.handle(GzipHandler.java:493)
    at org.spark_project.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:213)
    at org.spark_project.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:134)
    at org.spark_project.jetty.server.Server.handle(Server.java:539)
    at org.spark_project.jetty.server.HttpChannel.handle(HttpChannel.java:333)
    at org.spark_project.jetty.server.HttpConnection.onFillable(HttpConnection.java:251)
    at org.spark_project.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:283)
    at org.spark_project.jetty.io.FillInterest.fillable(FillInterest.java:108)
    at org.spark_project.jetty.io.SelectChannelEndPoint$2.run(SelectChannelEndPoint.java:93)
    at org.spark_project.jetty.util.thread.strategy.ExecuteProduceConsume.executeProduceConsume(ExecuteProduceConsume.java:303)
    at org.spark_project.jetty.util.thread.strategy.ExecuteProduceConsume.produceConsume(ExecuteProduceConsume.java:148)
    at org.spark_project.jetty.util.thread.strategy.ExecuteProduceConsume.run(ExecuteProduceConsume.java:136)
    at org.spark_project.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:671)
    at org.spark_project.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:589)
    at java.lang.Thread.run(Thread.java:748)

해결 방법

기존의 build.gradle에 Jetty를 별도로추가해서 충돌이 일어나 발생하는것 같다.

예전 gradle.build

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
    implementation 'org.apache.spark:spark-core_2.11:2.4.5'
    implementation 'org.apache.spark:spark-sql_2.11:2.4.5'
    implementation 'org.apache.spark:spark-hive_2.11:2.4.5'
    implementation 'org.codehaus.janino:janino:3.0.8'
    implementation 'org.slf4j:integration:1.7.29'
    implementation 'ch.qos.logback:logback-classic:1.2.3'
    implementation 'org.postgresql:postgresql:42.2.10.jre7'
    //요부분 jersey
    implementation 'org.glassfish.jersey.inject:jersey-hk2:2.30'

    implementation 'com.google.code.gson:gson:2.8.6'
    implementation 'org.projectlombok:lombok:1.18.10'
    annotationProcessor 'org.projectlombok:lombok:1.18.10'
}

해결한 gradle.build

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
    implementation 'org.apache.spark:spark-core_2.11:2.4.5'
    implementation 'org.apache.spark:spark-sql_2.11:2.4.5'
    implementation 'org.apache.spark:spark-hive_2.11:2.4.5'
    implementation 'org.codehaus.janino:janino:3.0.8'

    implementation 'org.slf4j:integration:1.7.29'
    implementation 'ch.qos.logback:logback-classic:1.2.3'
    compile 'org.postgresql:postgresql:42.2.10.jre7'

    implementation 'com.google.code.gson:gson:2.8.6'
    implementation 'org.projectlombok:lombok:1.18.10'
    annotationProcessor 'org.projectlombok:lombok:1.18.10'
}

+ Recent posts