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

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를 보면서 정리한 것입니다.

스프링 부트 웹 서비스 개발

책 판매 예제

https://github.com/ihoneymon/tacademy-spring-boot

  • 서비스 기획

  • 기능 분석 및 설계

    • 소스버전 관리 시스템 : git with Github

    • 빌드 배포시스템 : 젠킨스 , aws codedeploy

    • 로그 수집

    • 메일발송( Mail Chimp, SendGrid...)

    • 앱 푸시( AWS SNS, Firebase,...)

    • 개발환경

      • local, test, dev, beta, prod

  • 구현

  • 빌드

    • 젠킨스

  • 배포

  • 운영


도메인 설계

기획자의 문서에서 엔티티로 변경하는 설계

판매 상품은 책이다.
책은 유형별로 관리 되어야한다
책은 작가가 출판사를 통해 출간한다.
책은 서비스 관리자가 등록한다.
고객이 책을 주문한다.
======>
판매 상품(item) 은 책(book)이다.
책은 유형(Category)별로 관리되어야한다.
책은 작가(Author)가 출판사( Publisher)를 통해 출간한다.
책은 서비스 관리자(Administrator)가 등록한다.
고객(Customer)이 책을 주문(Order) 한다.
...

에 - - - 문서 구분자를 사용하여서 사용

Application-api.yml


Prod: 운영

beta: 관계자 확인

dev : 개발내용 확인

test : 자동 테스트

local : 개발자 로컬 실행환경

프로파일 구성

springboot는 여러 환경에서 빌드 사용이 가능하다.

one source Multi Use


message : 알림톡,SMS, 메일 발송 등 담당

Batch : 정기적으로 실행될 배치 프로그램 모음

admin : 서비스를 관리 하기 위한 백 오피스

Api: 외부에서 정보를 제공하는 REST API 모듈

core : 프로젝트 도메인(@Entity, @Repository)

Common : 프로젝트 공통 유틸리티, 예외…

Bookstore24 프로젝트 모듈 구성


도메인을 다 뽑아내고 다시 그것을 중심으로 설계~~~

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

Spring Boot Logging (1)  (0) 2020.04.16
Request Body 의 값 BadRequest 로 보내기  (0) 2020.04.11
SpringBoot(3)-T-Academy  (0) 2019.05.22
SpringBoot(2) -T-Academy  (0) 2019.05.21
SpringBoot 시작하기(1)-T-Academy  (0) 2019.05.20

Service

스프링 Container가 anotation을 bean으로 등록하고 실행하게 되는데,

객체를 생성하고 setter를 통해서 자기가 테스트하려는 기능을 변경하는 작업을 수동으로 하는게 아니라

초기부터 변경을 할수 있기로 하겠다라고 해서 생성자 방식으로 하는 방법이 있음

테스트에서 가장 많은 생성자 주입부분을사용함

테스트케이스에 보면 before 어노테이션이 있어서 시작될때 mocked 이라는 가짜 객체를 넣어서 하는 경우도 있음


Service 계층

@Service //(1) : 전형적인 서비스 코드
@Transactional //(2) 트렌잭션을 관리 하겠다라는 표현
public class BookServiceImpl implements BookService{
 private final BookRepository repository;
 
 //생성자 주입방식으로 bean에 등록
 public BookServiceImpl(BookRepository repository){
   this.repository = repository;
}
 
 //method를 BookService에서 선언해 두었기 때문에 덮어쓰기 했다.
 @Override
 public Optional<Book> findById(Long id){
   return repository.findById(id);
}
 
 @Override
 public List<Book> findAll(OffSetPageRequest request){
   return repository.findAll(request.getPageRequest()).getContent();
}
}package com.merona.study.service;

import com.merona.study.domain.Book;

import java.util.Optional;

public interface BookService {

   Optional<Book> findById(Long id);
}
package com.merona.study.service;

import com.merona.study.domain.Book;
import com.merona.study.domain.BookRepository;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.Optional;

@Service
@Transactional
// save, delete 등을 추가 하기 위해서
public class BookServiceImpl implements BookService {

   private final BookRepository bookRepository;

   //생성자 주입
   public BookServiceImpl(BookRepository bookRepository) {
       this.bookRepository = bookRepository;
  }


   @Override
   public Optional<Book> findById(Long id) {
       return bookRepository.findById(id);
  }

}
package com.merona.study.service;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;


@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class BookServiceImplTest {


   @Autowired
   BookService bookService;

//expected 오류가 날것이라는 예측을 하는 어노테이션 옵션
   @Test(expected=RuntimeException.class)
   public void testFindById(){
       Long id = 1L;
       bookService.findById(id)
              .orElseThrow(()->new RuntimeException("Not found"));
  }

}@ControllerAdvice(annotations = {RestController.class})
@ResponseBody
public class GlobalRestControllerAdvice{
 
 @Exceptionhandler(BookNotFoundException.class)
 public ApiResponse<Void> handleException(Exception e){
   Log.error("Occurred Exception: {} ", e);
   return ApiResponse.error(e.getMassage());
}
}package springboot_api;

import com.merona.study.domain.Book;
import com.merona.study.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/books")
public class BookController {
   @Autowired
   BookService bookService;

   @GetMapping("/{bookId}")
   public ResponseEntity<Book> findById(Long bookId){
       Book book = bookService.findById(bookId)
              .orElseThrow( ()-> new RuntimeException("Not Found" + bookId));
       //http status 는 번호로 관리되는데 정상적으로 하면 200 ok 가 반환된다.
       return ResponseEntity.ok( book);
  }
}

@Profile -Type


Spring REST DOCs : api 문서에 대한 설명을 html로 만들어서 관리할 수 있다.

  • 시스템의 자원에 대한 접근 및 제어를 제공하는 API

  • 자원에 대한 접근 및 제어

    • GET /books

    • GET /books/{bookId}

    • POST /books

    • PUT /books/{bookId}

    • DELETE /books/{booksId}

  • 스프링에서는 요청에 따라 등록 되어 있는 적절한 HttpMessageConverter를 통해서 응답데이터를 반환한다.

REST api


예제 코드

restContrller에 오류가 발행사게 되면 exception handler를 반환할 수도 있다.

  • @ControllerAdvice를 이용한 처리

@ControllerAdvice를 이용한 처리

@Controller 예외 처리

 

  • DispatcherServlet에 등록된 @RequestMapping 등록되고 추후에 요청이 오면 해당하는 메서드를 찾아서 호출됨

  • 템플릿 엔진이 랜더링 할 뷰 페이지를 지정

  • 호출된 API에서 처리한 응답을 반환

@Controller

참고 : https://www.slideshare.net/hanmomhanda/spring-mvc-fullflow

anotation의 타입은 class, method에 추가할 수 있다.

@RequestMapping() : 클래스 타입의 레벨이 있을수도있고, Spring dispatcher Servelt이 controller 라고하는 anotation을 훓으면서 RequestMapping 있는 타입을 찾고 이후 차근차근 내려가게 된다.

@RestController 또는 @Controller

Controller


테스트 코드

BookRepository를 생성자에 주입하여 사용하는 BookServiceImpl 클래스

bookService 인터페이스

예제 코드

  • 트랜잭션(@Transactional) 관리 영역

    • 행위 시작, 종료 시까지 정상적으로 될때까지는 종료, 데이터 이상시에는 되돌리는 롤백을 하게됨

  • 서로 다른 도메인 연계(DI, @Autowired) 작업 영역

    • book과 category라는 엔티티를 서로 상호 작용을 하는 영역

  • @Controller와 @Repository 사이의 중계

@Service


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

Spring Boot Logging (1)  (0) 2020.04.16
Request Body 의 값 BadRequest 로 보내기  (0) 2020.04.11
SpringBoot(4) -T-Academy  (0) 2019.05.24
SpringBoot(2) -T-Academy  (0) 2019.05.21
SpringBoot 시작하기(1)-T-Academy  (0) 2019.05.20

Spring Boot 만들기

애플리케이션 계층

계층(Layer) @Component
표현(presentation) @Controller
서비스(Service) @Service
영속화(Persistence) @Repository

개발 방향은 아래에서 위로 올라간다.

Entity와 repository를 개발하고 그다음 서비스를 작성하고 서비스를 이용하는 컨트롤러를 만드는것 으로 차근차근 개발.

루트 application, 컨트롤러가 있는 애플리케이션 콘텍스트


Repository

  • ORM ( Object Relational Mapping)

    • 대부분의 개발 언어 플랫폼마다 제공
    • 객체(Object) 로 관계형 데이터베이스(RDMBS)를 관리
    • hiberate를 주로 사용
  • JPA(Java Persistence API)

    • Java 객체 정보를 영속화 하는 중간과정을 처리한다.

    • 엔티티 객체를 저장하고 변경하고 삭제하면 그에 대응하는 쿼리를 생성하고 실행한다

    • hibrate를 조금더 사용하기 쉽게 추상화 한 라이브러리가 JPA
      프로그램마다 객체와 entity 테이블 사이의 관계를 맵핑하고 쿼리를 생성해주는 프레임워크 java 에서는 hibernate, springdata jpa로 활용을 하게 된다.
      jpa가 객체의 상태를 보고 update, delete,insert, 등을 사용하게 된다
      https://www.slideshare.net/zipkyh/ksug2015-jpa1-jpa-51213397
      https://www.slideshare.net/zipkyh/spring-datajpa
      @Entity
      class Book{

        Long id;
      String isbn13;
      String isbn10;

      }

      insert into book(id,isbn13,isbn10) value (...);

    Spring Data JPA Repository interface

    public interface BookRepository extends JpaRepository<Book,Long>{
      List<Book> findByNameLike(String name);
    }

    인터페이스를 확장해서 사용하게 된다.
    이름으로 해서 유사 검색을 할수 있도록 하는 것
    Named method query 를 사용해 줌
    JpaRepository

    Optional<T> findById(ID id);
    List<T> findAll();
    List<T> findAll(Sort sort);
    List<T> findAllById(Iterable<ID> ids);
    <S extends T> List<S> saveAll(Iterable<S> entities);
    void flush();
    <S extends T> S saveAndFlush(S entity);
    void deleteInBatch(Iterable<T> entities);
    void deleteAllInBatch();
    T getOne(ID id);
  • JPA 동작원리
    BookRepository --> Spring Data JPA Proxy, SimpleJpaRepository -->Book Repository 구현체
    http://haviyj.tistory.com/28

  • 빨간색 부분이 JPA 부분

    서비스에서 레파지토리 호출 구현체에서 JPA 를 통해서 쿼리로 변경하고 jdbc에서 객체로 맵핑 되어서 가지고옴

    업무(=비즈니스 로직 ) 구현에만 집중해라!!

    • 영속화 계층(@Repository)에서는 엔티티 관리만
    • 비즈니스 로직구현은 도메인 영역에서
    • 서로 다른 도메인 사이에 연계는 서비스 계층(@Service) 에서
    • 외부 요청에 대한 처리는 컨트롤러 계층(@Controller) 에서
      entity를 가지고 오는 것이 간단함

repository만들기 실습

AbstractPersistable : JPA 추상 클래스

Entity들의 id 값은 보통 long을 많이 씀

도메인 : Book 클래스

package com.merona.study.domain;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.jpa.domain.AbstractPersistable;

import javax.persistence.Entity;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@Entity
public class Book extends AbstractPersistable<Long> {
//id 값을 Long으로 generic으로 받아서 하기 때문에 @GeneratedValue를 설정 안해도됨
//    @Id
//    private Long id;

    private String name;
    private String isbn13; //13자리
    private String isbn10; //10자리

}

AbstractPersistable 이란 ?

Repository : BookRepository

package com.merona.study.domain;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface BookRepository extends JpaRepository<Book, Long> {
    //T : entity의 타입
    //ID : Long
    List<Book> findByNameLike(String name);

}

TestCode

package com.merona.study.domain;

import jdk.nashorn.internal.ir.LiteralNode;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;


import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;


@RunWith(SpringRunner.class)
@DataJpaTest
public class BookTest {

    @Autowired
    BookRepository repository;

    @Test
    public void testTave(){

        Book book = new Book();
        book.setName("boot-spring-boot");
        book.setIsbn10("0123456789");
        book.setIsbn13("0123456789123");

//        assertThat("Simple Text",is(equalTo("Simple Text")));
        System.out.println(" book is NEw ? "+ book.isNew());
        repository.save(book);
        System.out.println(" book is NEw ? "+ book.isNew());
//        assertThat(book.isNew(),true);
    }


    @Test
    public void testFindByNameLike(){
        Book book = new Book();
        book.setName("boot-spring-boot");
        book.setIsbn10("0123456789");
        book.setIsbn13("0123456789123");

        repository.save(book);

        List<Book> books = repository.findByNameLike("boot");
        for(Book bookItem : books){
            System.out.println("bookName : "+bookItem.getName());
        }

    }
}

테스트 코드를 적는데 있어서

assertThat이 계속 오류가 나는데

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

위 패키지의 assertThat을 사용하면 해결된다.

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

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
SpringBoot 시작하기(1)-T-Academy  (0) 2019.05.20

스프링부트 만들기

프로젝트구성

spring initializer 사이트에서 받는방법

name 
groupid : 패키지명을 바탕으로 시작
artifactID : 
version : 개발 버전 
descriptoin : 프로젝트 설명
packageName : 실제로 프로젝트에서 실행하는 루트 패키지 이름
type : 그래들 패키지
packaging : jar로 패키징
javaVersion : 자바 버젼
language : 자바
bootVersion : 스프링 부트 버전
dependencies = lombok&dependencies=h2&dependencies=data-jpa&dependencies=web

lombok

  • 자바 프로젝트 필수 라이브러리
  • 클래스에서 필수적으로 작성해야 하는 접근자 / 설정자

(Getter/setter), toString, equalAndHashCode, 생성자 등을 작성하지 않아도 된다

  • 코드를 간결하게 사용 할 수 있다.
  • 배포 버전을 확인하고 결합이 있는지 확인해야한다.
  • 사용시 주의사항
    롬복 버전 업데이트시에 약간 compile 필드 값을 잘못 가지고오면서, 오류가 나는 경우가 있음(권순남 작성 위키백과 )

h2 Databaese

  • 인메모리, 파일, TCP 지원 인터세이스
  • JDBC url 설정으로 데이터베이스 작동방식 지정가능
  • 스프링부트 자동구성으로 기본 제공, /h2-console h2 webconsole 제공
  • 로컬 개발환경에서 mysql…등 설치 없이, 별도의 DB 설치 없이 빠른 프로토타이핑 지원, 개발 베타에서는 설치해야함
    • 가능한이유 : JPA 쓰기 때문에 가능함, 쓰는 사람에 따라서
  • 필요에 따라 운영 가능한 수준의 데이터베이스 활용가능
    • 임베디드로 만들어서 쓰는 경우도 있음

프로젝트 구조

setting.gradle

rootProject.name = 'spring-boot'

  • 멀티 프로젝트 구성시 사용
  • 프로젝트 이름
  • 하위 프로젝트 정의 / include = ' law projectName '
  • 하위 프로젝트 설명()

gradle Wrapper

그래들 수동으로 받아야됬다. 빌드의 차이 때문에 오류가 나는 불편한점이 있었음

프로젝트 생성 후

gradley build 커멘드를 입력하게 되면

Gradle이 wrapper의 properties와 jar파일이 있는지 확인 한다 없으면,

gradle script에 의해서 gradle war 를 받고 jar 파일을 기준으로 빌드를 한다.

gradlew(.bat) clean build


스프링부트 어플리케이션

@SpringBootApplication
public class Application{
  public static void main(String[]args){
    SpringApplication.run(Application.class, args);
  }
}

스프링부트 자바 어플리케이션안에서 실행클래스를 통해 ioc 컨테이너를 띄우고 어노테이션이 있는 위치를기준으로 스프링 빌드를 탐색

Springapplication.run 코드를 보면 어노테이션 config …. 이런콘텍스트를 확인하기 보통은 web application으로 시작하게 된다.

보통은 servelt을 사용함 .(webMvcAutoConfiguration을 보면 됨)

해당 폴더 내에 들어가서 gradlew build 하면 springboot jar 파일이 만들어지고

플러그인에 의해서 만들어진 jar 파일은 fat war 라고도 하는데, 실행환경을다 포함해서 fat jar 라고 부름

프로젝트 밑에

build/libs/snapshot …이 됨

톰캣과 재압축됨

압축된것을 풀어보면

BOOT-INF/classes 내부

static : javascript, css , image

template : template 엔진, controller가 전달해주는 모델에서 동적으로 html 생성하는 것

META-INF : 메인 클레스가 뭐고 스타트가 어디인지 알수 있다.

실직적으로는 jar launcher가 실행되고 이후 메인 클래스가 동작하게 되는것

classes : dml 설정 파일과 application class 파일 존재 , lib : 의존성 라이브러리들

rootpackage를 기준으로

실행 : Java -jar spring-boot-snapshot-1.0.0


SpringApplication:: 스프링부트 앱 시작점

Spring-boot-starter

Spring-boot-dependencies

Spring-boot-autoconfigure

Spring-boot-parent

Spring-boot-starters

위의 순서대로 상속을 받아서 starters 가 만들어지고

아래의 pom.xml에 모듈들을 사용한다고 적어주면됨

autoconfiguration으로 작성되어 있음(자동구성이 어떻게 되어있는지 확인 가능)

Spring-boot-starter-*/pom.xml

servlet과 reactive로 보통 많이 쓰임

dispatcherServlet이 가장 중요한 역할을 함


@Component

스프링 프레임워크는

component라는 스트레오 타입 어노테이션을 기준으로해서

3가지의 하위 컴포넌트들로 나뉜다.

Controller, service, repository

@Bean vs @Component

빈 : 개발자가 제어할수 없는 다른이가 작성한 클래스들을 스프링 빈으로 등록

컴포넌트 : 내가 작성한 계열의 컴포넌트 클래스에다가 붙여 넣게 됨

트랜잭션이라고해서 데이터를 저장하고 관리를 할 경우 service라는 어노테이션을 사용하고

트랜젝션 필요 없는 경우 component 선언을 해서 많이 쓰임

빈 : 콩깍찌가 씌여져있는 객체라고 생각

spring ioc 콘테이너에서 생성, 소멸 되기 까지 생명주기를 관리하는것을 bean 객체 라고 함

객체중의 하나가 bean anotation이 적혀있는것


의존성 주입

라이브러리와 프레임워크의 차이

인스턴스를 만들때는 아래와 같이 라이브러리 객체를 만들어서 객체를문자열로 쭉 작성을 하면서 호출하게 되는 과정이 일반적

public class ObjectMapperTest{
  public void test() throws JsonProcessingException{
    ObjectMapper objectMapper = new ObjectMapper();

    Book book = new Book("test-book", "test-isbn13","test-isbn10");

    String strBook = objectMapper.writeValueAsString(book);
  }
  //검증 생략
}

하지만 스프링과 같은 ioc 컨테이너에서는 @autowired를 선언해놓으면 application context가 구동되는 상황에서 objectmapper를 이미 어딘가에서 읽어서 Spring bean으로 구성해놨기 때문에, 따로 인스턴스를 만들지않고 주입해야 할 수 있다.

public class ObjectMapperTest{
  @Autowired
  ObjectMapper objectMapper;

  public void test() throws JsonProcessingException{
    Book book = new Book("test-book", "test-isbn13", "test-isbn10");

    String strBook = objectMapper.writeValueAsString(book);
    //검증생략
  }
}

프레임워크가 제어의 역전함

프레임워크 쪽에서 필요한 컴포넌트를 가지고 있다가 필요한 곳에서 가져다쓰는 차이점

호출이 아니라 가져다쓰는 차이점


의존성 주입 방법

  • 생성자 주입( 권장)

    @Service
    public class BookServiceImpl implements BookService{
      private final BookRepository repository;
    
      public BookServiceImpl(BookRepository repository){
        this.repository = repository;
      }
    
      //코드 생략
    }

    생성자가 하나만 있어야 가능하다 autowired를 붙이지 않아도 service라느 어노테이션이 있어서, 스프링에서 아~ 내가 관리해야할 컴포넌트 이구나 라고 내가 가지고 있는 Book repository를 쓰네 라고 해서 주입이 됨.
    클래스에서 생성자가 하나만 있어야 사용 가능, @Service 컴포넌트에서 알아서 BookRepository라는 인스터스를 주입을 해줌

  • 설정자 주입( setter)

    @Service
    public class BookServiceImple implements BookService{
      private BookRepository repository;
    
      @Autowired
      public void setRepository(BookRepository repository){
        this.repository = repository;
      }
      //생략
    }

    파라미터 앞에 autowire를 설정하게 되면 사용이 가능함

  • 필드( @Autowired )선언

    public class BookServiceImpl implements BookService{
      @Autowired
      private BookRepository repository;
      //코드 생략
    }

    처음에는 @Autowired를 많이 쓰임

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

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
SpringBoot(2) -T-Academy  (0) 2019.05.21

+ Recent posts