yml 파일 분리의 필요성

팀 프로젝트를 진행하면서 yml(YAML) 파일의 길이가 꾸준히 늘어나고 있다.

yml 파일를 수정해야 했던 이유를 정리해보면 아래와 같았다.

  • 의존성 추가(Sprind Data Redis, Oauth2 Client 등)
  • DB 설정 분리(로컬은 H2, 배포에선 MySQL)
  • 기능 추가 시 Secret과 같은 환경 변수 설정
  • 쿠키, 스웨거 등의 도메인 설정(로컬에선 localhost, 배포에선 배포 도메인 주소)

배포하고 도메인까지 연결해보니 로컬에서 테스트할 때와 배포 환경에서 다르게 해줘야할 설정이 점점 많아졌다.

그래서 매번 빌드할때와 테스트할 때 yml 파일을 여러 군데 수정해야하는 불편함까지 생기기 시작했다.

로컬에서 테스트 할때와 빌드를 할때 수정할 부분을 최소화 하기 위해 yml 분리 방법을 간단하게 공부하고 정리해보려 한다.

개발 환경은 본인이 스프링을 사용하므로 스프링 기준이다.


yml 파일들을 분리하기 위한 규칙

1. 파일 이름 규칙

yml 파일 이름은 나름의 규칙을 지켜야한다.

application-{profile이름}.yml 의 규칙으로 짓는다.

스프링에서는 {profile이름}으로 yml 파일을 구분하고 활용할 수 있다.

2. application.yml 위치

이건 따로 변경할 수 있다곤 하지만 아래처럼 resources 경로에 둔다.

3. 다른 프로필의 yml 파일 위치

아래처럼 depth를 나눠서 사용할 수도 있지만 본인은 application.yml 과 같은 depth로 사용하려 한다.

project
├── src
│   ├── main
│   ├── java
│   ├── resources
│   │   ├── static
│   │   ├── templates
│   │   ├── yaml
│   │   │   ├── logging
│   │   │   │   └─ application-log.yml
│   │   │   ├── application-dev.yml
│   │   │   └─ application-local.yml
│   │   └─ application.yml

yml 파일 분리 해보기

application.yml

분리하는 기준은 정하기 나름이겠지만 본인은 application.yml 파일에 공통 설정을 담았다.

server:
  port: 8080
spring:
  profiles:
    group:                # yml 파일 여러개를 그룹화 하고싶을 때 사용
      test: local, log   # 이렇게 local, log를 그룹화하고 사용할 때는 active: test
    active: local         # 로컬에서 테스트할 때는 local, 빌드 시엔 prod 로 이 부분만 변경

  data:
    redis:
      host: localhost
      port: 6379
      repositories:
        enabled: false
  main:
    allow-bean-definition-overriding: true

pawland:
  front-test-url: "http://localhost:3000"    # 해당 라인과 아래 라인도 local, prod 로 나누면 하나로 관리할 수 있었을듯
  front-deploy-url: "https://www.pawland.store"
  jwt-key: # JWT 키

gmail:
  host: smtp.gmail.com
  port: 587
  username: hyukkind@gmail.com
  password: # 구글 앱 비밀번호
  smtpProperties:
      auth: true
      starttls-enable: true
      starttls-required: true
      connection-timeout: 5000
      timeout: 5000
      writeTimeout: 5000

aws:
  access-key: # AWS 액세스 키
  secret-key: # AWS 시크릿 키
  s3-image-prefix:  # S3 이미지 프리픽스
  s3-access-point: # S3 액세스 포인트

application-local.yml

로컬에서 사용하는 yml 설정이다.

로컬에서는 인메모리에서 테스트하고, ddl-auto 기능도 마음대로 사용할 수 있다.

spring:
  config:
    active:
      on-profile: local  # 프로필 이름 설정

  datasource:
    url: jdbc:h2:mem:testdb;MODE=MySQL
  hikari.maximum-pool-size: 20
  h2:
    console:
      enabled: true
      path: /h2-console
  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: create   # 로컬에선 ddl-auto create로 해도 됨
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        show_sql: true
        dialect: org.hibernate.dialect.H2Dialect
        
pawland:
  back-domain: "localhost"
  back-url: "http://localhost:8080"

application-prod.yml

배포할 때 사용할 yml 설정이다.

배포 환경에서는 ddl-auto 때문에 낭패를 볼 수 있으므로 none으로 설정하고

DB도 새로운 버전으로 배포해도 데이터가 유지되도록 H2 인메모리 DB에서 MySQL로 바꿔준다.

spring:
  config:
    active:
      on-profile: prod

  datasource:
    url: jdbc:mysql:// # RDS or EC2로 띄운 MySQL 주소
    username: # 사용자
    password: # 비밀번호

  hikari.maximum-pool-size: 20

  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: none   # ddl-auto 는 prod 에선 none
    properties:
      hibernate:
        format_sql: true  # 이 부분도 false 해도 될듯
        show_sql: true    # 이 부분도 false 해도 될듯
        dialect: org.hibernate.dialect.MySQLDialect
        
pawland:
  back-domain: "midcon.store"
  back-url: "https://www.midcon.store"

 

참고자료

 

[Spring] 하나의 YAML을 여러 개로 나누어 환경 분리하기

스프링에서는 다양한 환경 변수 및 정보들을 활용하기 위해 yaml 및 properties를 지원한다. 하나의 yaml 파일들을 여러 개의 yaml로 나누는 방법을 익혀보자.

velog.io

 

'백엔드 > Spring' 카테고리의 다른 글

@ConfigurationProperties로 환경 변수 관리  (0) 2023.10.08
ArgumentResolver  (0) 2023.09.29
Interceptor 써보기  (0) 2023.09.29
Filter  (0) 2023.09.29
Interceptor  (0) 2023.09.29

@ConfigurationProperties

스프링으로 개발을 하다 보면 properties 나 yml 파일에 설정한 값을 불러와야할 때가 있다.

대표적으로 외부에 노출되서는 안되는 SecretKey 값 같은 값이 있다.

AWS IAM Key 같은 값은 외부에 노출 됐다가 채굴당해서 요금폭탄 나오는 경우도 많으니 특히 조심해야한다.

전에는 @Value를 사용하곤 했지만 같은 변수를 재사용할 수 없다는 단점이 있었다.

이번에는 @ConfigurationProperties를 통해 properties 나 yml 파일에서 값을 불러오는 법을 정리해두려 한다.

@ConfigurationProperties 로 값 바인딩 하는법 ]

  1.  setter 로 바인딩
  2.  생성자로 바인딩

기본 설정

이 기능을 쓰기 위해 아래처럼 의존성을 추가해준다.

build.gradle

annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'

 

아래의 application.yml 파일의 데이터 값을 AppConfig 로 가져와보도록 한다.

application.yml

midcon:
  jwt-key: "pjx7jVXbdaeOmw0ZO1SotIHLVApe8FZ+LmGCuMKa8T8="

AppConfig

public class AppConfig {

    private String jwtKey;
}

DunpleApplication

스프링을 실행하고 컨테이너가 시작될 때 AppConfig 를 초기화하고 빈으로 생성하도록 등록을 해주어야 한다.

이를 위해서는 @EnableConfigurationProperties를 등록하고 해당 클래스를 추가해주어야 한다.
일반적으로 SpringBoot의 메인 클래스에 @EnableConfigurationProperties 어노테이션을 다음과 같이 추가해준다.

@EnableConfigurationProperties(AppConfig.class)
@SpringBootApplication
public class DunpleApplication {

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

1. setter 로 바인딩

우선 setter 로 바인딩 하는법을 알아보자.

가져오려는 값의 prefix 또한 설정할 수 있다. 여기서는 "midcon" 으로 설정하겠다.

@ConfigurationProperties는 기본적으로 자바빈 프로퍼티 방식으로 동작하기 때문에 Setter가 반드시 필요하다.

@Setter
@ConfigurationProperties(prefix = "midcon")
public class AppConfig {

    private String jwtKey;
}

 

실행해보면 값이 잘 바인딩 된걸 확인할 수 있다.


2. 생성자로 바인딩

하지만 Setter 를 열어두는건 그다지 좋은 방법이 아니다. 

스프링 컨테이너에 의해 싱글톤으로 관리되는 객체에 변경 가능성이 열려있기 때문이다. 

그러므로 해당 클래스의 변수들을 final로 선언하고 생성자로 바인딩하여 불변성을 보장하는 것이 좋다.

변경 가능성을 닫기 위해 해당 변수를 final로 선언하고 생성자를 추가한다.

그리고 생성자를 이용해 yml 파일의 값을 바인딩하도록 @ConstructorBinding 어노테이션을 추가해준다.

(참고로 이 글은 스프링부트 2.7 점대 버전 기준임. 3.0 이상부터는 @ConstructorBinding 불필요 해당 글 참고)

@RequiredArgsConstructor
@ConstructorBinding
@ConfigurationProperties(prefix = "midcon")
public class AppConfig {

    private final String jwtKey;
}

 

실행해보면 값이 잘 바인딩 된걸 확인할 수 있다.

 

 

관련글

JWT 를 이용한 인증

스프링부트 3.0 이상의 경우

 

 

참고자료

 

[SpringBoot] final 변수를 갖는 클래스에 프로퍼티(Properties) 설정 값 불러오기, 생성자 바인딩(Construct

Spring 프레임워크로 개발을 하다 보면 프로퍼티(Properties)에 저장된 특정한 설정 값들을 불러와야 하는 경우가 있다. 많은 글 들에서 프로퍼티(Properties)를 불러오는 내용들을 설명하고 있는데, 이

mangkyu.tistory.com

 

'백엔드 > Spring' 카테고리의 다른 글

yml 파일 분리해보기  (0) 2024.04.26
ArgumentResolver  (0) 2023.09.29
Interceptor 써보기  (0) 2023.09.29
Filter  (0) 2023.09.29
Interceptor  (0) 2023.09.29

ArgumentResolver

애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있었다.
@RequestParam, @ModelAttribute 는 HTTP 요청 파라미터 데이터를 사용하고,
@RequestBody 는 HTTP 메시지 바디의 데이터를 사용하는 등 애노테이션으로 사용할 데이터를 구분하였다.
이렇게 애노테이션에 따라 파라미터를 유연하게 처리할 수 있었던게 바로 ArgumentResolver 덕분이다.

 

동작 방식

아래는 HandlerMethodArgumentResolver 구현체이다.

public class AuthResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return false;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        return null;
    }
}

1. supportsParameter()

ArgumentResolver 의 supportsParameter() 를 호출해서 해당 파라미터를 지원하는지 체크한다.

2. resolveArgument()

supportsParameter() 의 값이 true 이면 resolveArgument() 를 호출해서 실제 객체를 생성한다.

이렇게 생성된 객체가 컨트롤러 호출시 아래의 그림처럼 넘어간다.

 

 

 

 

관련글

ArgumentResolver 실습

'백엔드 > Spring' 카테고리의 다른 글

yml 파일 분리해보기  (0) 2024.04.26
@ConfigurationProperties로 환경 변수 관리  (0) 2023.10.08
Interceptor 써보기  (0) 2023.09.29
Filter  (0) 2023.09.29
Interceptor  (0) 2023.09.29

Interceptor 사용해보기

PostController

간단하게 실험용으로 Get 요청 시 Hello World 를 반환하는 컨트롤러를 만든다.

@GetMapping("/hello")
public String hello() {
    return "Hello World";
}

AuthIntercepteor

인터셉터를 구현하기 위해 우선 HandlerInterceptor 를 상속받는 인터셉터 클래스를 하나 만든다.

윈도우 기준 ctrl + i 를 누르면 구현할 수 있는 아래 사진처럼 메서드 목록이 나온다.

전부 다 구현하고 어떻게 동작하는지 확인할겸 로그를 찍어볼것이다.

@Slf4j
public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info(">> preHandle");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info(">> postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info(">> afterCompletion");
    }
}

 preHandle

이 글에서 설명했듯 prehandle 은 boolean 값을 return 값으로 반환한다.

true 면 다음 단계를 진행하고 false 면 해당 요청을 거기서 중단한다.

WebMvcConfig

인터셉터를 적용하기 위해 아래처럼 Config 를 설정해준다.

이렇게 설정하면 모든 컨트롤러에 대해 인터셉터가 동작할것이다.

애플리케이션을 실행하거나 Mock 테스트를 수행하고 로그를 확인하면 아래처럼 동작하는걸 확인할 수 있다.

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor());
    }
}

 


ControllerTest

위에서 작성한 AuthIntercepteor 의 preHandle 값에 따른 인터셉터 호출을 비교해보기 위해 간단한 Mock 테스트를 수행한다.

@DisplayName("/hello 요청시 Hello World 를 출력한다.")
@Test
void hello() throws Exception {
    mockMvc.perform(
        MockMvcRequestBuilders.get("/hello")
        )
        .andExpect(status().isOk())
        .andExpect(content().string("Hello World"))
        .andDo(print());
}

ㄴ preHandle - true

prehandle, postHandle, afterCompletion 모두 호출된다.

ㄴ preHandle - false

preHandle 만 호출된다.

'백엔드 > Spring' 카테고리의 다른 글

@ConfigurationProperties로 환경 변수 관리  (0) 2023.10.08
ArgumentResolver  (0) 2023.09.29
Filter  (0) 2023.09.29
Interceptor  (0) 2023.09.29
BeanValidation 써보기  (0) 2023.09.22

필터

사용자 인증과 같은 웹과 관련된 공통 관심 사항을 효과적으로 처리할 수 있도록 서블릿이 지원하는 기능이다.

여기서 말하는 서블릿은 DispatcherServlet  이 아닌 서블릿 기술 자체를 말한다.

필터는 스프링에 종속되지 않은 기술이므로org.springframework 패키지가 아닌 javax.servlet 패키지에 속해있다.

 

공통 관심사는 스프링의 AOP로도 해결할 수 있지만 아래와 같은 이유로 웹과 관련된 공통 관심사는

서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다. 

  • 웹과 관련된 공통 관심사를 처리할 때는 쿠키나 세션을 확인해야 하기 때문에 HTTP의 헤더나 URL의 정보들이 필요하다.
  • 서블릿 필터나 스프링 인터셉터는 HttpServletRequest 를 제공하기 때문에 이를 처리하기 용이하다.

 

필터 흐름

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러

 

필터의 흐름은 위와 같다. 필터를 적용하면 필터가 호출 된 다음에 서블릿(DispatcherServlet )이 호출된다.

그래서 모든 고객의 요청 로그를 남기는 요구사항이 있다면 필터를 사용하면 된다.

필터는 특정 URL 패턴에 적용할 수 있다. URL 패턴을 ("/*") 이라고 하면 모든 요청에 필터가 적용된다.

 

필터 제한

로그인 사용자
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러       

비 로그인 사용자
HTTP 요청 -> WAS -> 필터  <<< 적절하지 않은 요청, 서블릿 호출 X    

 

필터에서 적절하지 않은 요청이라고 판단하면 서블릿을 호출하지 않고 거기에서 끝낼수도 있다. 

 

필터 체인

HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 필터3 -> 서블릿 -> 컨트롤러

 

필터는 체인으로 구성되는데 중간에 필터를 자유롭게 추가할 수 있다.

예를 들어서 로그를 남기는 필터를 먼저 적용하고 그 다음에 로그인 여부를 체크하는 필터를 만들 수 있다.

 

 

 

참고자료)

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com

 

'백엔드 > Spring' 카테고리의 다른 글

ArgumentResolver  (0) 2023.09.29
Interceptor 써보기  (0) 2023.09.29
Interceptor  (0) 2023.09.29
BeanValidation 써보기  (0) 2023.09.22
@ModelAttribute 와 @RequestBody 써보기  (0) 2023.09.21

인터셉터

사용자 인증과 같은 웹과 관련된 공통 관심 사항을 효과적으로 처리할 수 있도록 스프링 MVC 가 제공하는 기능이다.

서블릿 필터 서블릿이 제공하는 기능이라면 스프링 인터셉터 스프링 MVC 가 제공하는 기능이다.

 

공통 관심사는 스프링의 AOP로도 해결할 수 있지만 아래와 같은 이유로 웹과 관련된 공통 관심사는

서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다. 

  • 웹과 관련된 공통 관심사를 처리할 때는 쿠키나 세션을 확인해야 하기 때문에 HTTP의 헤더나 URL의 정보들이 필요하다.
  • 서블릿 필터나 스프링 인터셉터는 HttpServletRequest 를 제공하기 때문에 이를 처리하기 용이하다

 

스프링 인터셉터 흐름

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러

 

스프링 인터셉터의 흐름은 위와 같다. 스프링을 사용하기 때문에 위 흐름에서 표시된 서블릿은 DispatcherServlet 이다.

인터셉터는 스프링 MVC 가 제공하는 기능이기 때문에 서블릿을 지난 다음 적용된다.

 

스프링 인터셉터 제한

로그인 사용자
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러       

비 로그인 사용자                                              
HTTP 요청 -> WAS -> 필터  -> 서블릿 -> 스프링 인터셉터 <<< 적절하지 않은 요청, 컨트롤러 호출 X    

 

인터셉터에서 적절하지 않은 요청이라고 판단하면 서블릿을 호출하지 않고 거기에서 끝낼수도 있다. 

 

인터셉터 체인

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 인터셉터3 -> 컨트롤러

 

스프링 인터셉터는 체인으로 구성되는데 중간에 인터셉터를 자유롭게 추가할 수 있다.

예를 들어서 로그를 남기는 인터셉터를 먼저 적용하고 그 다음에 로그인 여부를 체크하는 인터셉터를 만들 수 있다.

 


필터와의 차이점

위까지만 보면 사실 필터와 크게 다른 차이를 못느낄것이다.

필터는 서블릿(DispatcherServlet ) 호출 전에 적용되기 때문에 Response 와 Request 정도만 조정할 수 있지만

인터셉터는 서블릿 호출 이후에 적용되므로 컨트롤러 호출 전, 호출 후, 요청 완료 이후 까지 세분화해서 조정할 수 있다.

 

인터셉터 호출 흐름

1. 정상 흐름

 

  1. preHandle
    - 컨트롤러 전에 호출된다.
    - preHandle 의 응답값이 true 면 다음으로 진행하고, false 면 요청을 여기서 끝낸다.
      false 일 경우 체인된 나머지 인터셉터는 물론이고 핸들러 어댑터도 호출되지 않는다.
  2. postHandle
    - 컨트롤러 호출 후에 호출된다. (정확히는 핸들러 어댑터 호출 후)
  3. afterCompletion
    - 뷰가 렌더링 된 이후에 호출된다.
    - 항상 호출되며, 예외 상황 시 예외 정보를 포함해서 호출된다.

2. 스프링 인터셉터 예외 상황

 

  1. preHandle
    - 컨트롤러 전에 호출된다.
  2. postHandle
    - 컨트롤러에서 예외가 발생하면 postHandle 은 호출되지 않는다.
  3. afterCompletion
    - 항상 호출되며, 예외 상황 시 예외 정보를 포함해서 호출된다.

위처럼 afterCompletion은 예외가 발생해도 호출된다.

예외가 발생하면 postHandle() 은 호출되지 않으므로 예외와 무관하게 공통 처리를 하려면

afterCompletion() 을 사용해야 한다.

 

 

참고자료)

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com

 

'백엔드 > Spring' 카테고리의 다른 글

Interceptor 써보기  (0) 2023.09.29
Filter  (0) 2023.09.29
BeanValidation 써보기  (0) 2023.09.22
@ModelAttribute 와 @RequestBody 써보기  (0) 2023.09.21
DispatcherServlet  (0) 2023.08.01

BeanValidation

검증 로직을 모든 프로젝트에 적용할 수 있게 공통화하고 표준화 한 것이 바로 Bean Validation 이다.

스프링에서 BeanValidation 을 사용하려면 우선 gradle 에 의존성을 추가해야한다.

implementation 'org.springframework.boot:spring-boot-starter-validation'

 

스프링 MVC는 어떻게 Bean Validator를 사용?

스프링 부트가 spring-boot-starter-validation 라이브러리를 넣으면

자동으로 Bean Validator를 인지하고 스프링에 통합한다.

 

스프링 부트는 자동으로 글로벌 Validator로 등록한다.

LocalValidatorFactoryBean 을 글로벌 Validator로 등록한다.

스프링 부트는 gradle 에 spring-boot-starter-validation 의존성을 추가하면 자동으로

BeanValidator 를 글로벌 Validator로 등록한다.

이렇게 글로벌 Validator가 적용되어 있기 때문에, @Valid , @Validated 만 적용하면 된다.

검증 오류가 발생하면, FieldError , ObjectError 를 생성해서 BindingResult 에 담아준다.

 

PostController

@PostMapping("/posts")
public String post(@RequestBody @Valid PostCreate params) throws Exception {
    log.info("params = {}", params.toString());
    return "Hello";
}

ㄴ @Valid

위처럼 컨트롤러에서 검증할 객체 앞에 애노테이션 @Valid 를 사용한다.

 

PostCreate

DTO 의 검증하고자 하는 필드에 BeanValidation 에서 제공하는 검증 애노테이션을 사용한다.

여기서는 빈값 / 공백(" ")만 있는 경우를 허용하지 않는 @NotBlank 를 사용한다.

@Setter
@Getter
@ToString
public class PostCreate {

    @NotBlank
    private String title;

    @NotBlank
    private String content;

    public PostCreate(String title, String content) {
        this.title = title;
        this.content = content;
    }
}

ㄴ 검증 애노테이션

  • @NotBlank : 빈값 + 공백만 있는 경우를 허용하지 않는다.
  • @NotNull : null 을 허용하지 않는다.
  • @Range(min = 1000, max = 1000000) : 범위 안의 값이어야 한다.
  • @Max(9999) : 최대 9999까지만 허용한다.

 

이제 결과를 살펴보자.

 

 

스프링MVC에서 컨트롤러에 진입하기도 전에 예외가 400 예외가 발생하기 때문에 컨트롤러가 호출되지도 않았다.

BeanValidation 을 사용하여 잘못된 값이 입력되는건 막았지만 어떤 부분이 잘못 됐는지 알 수가 없다.

에러 메시지를 확인할 수 있는 방법은 없을까?

 


BindingResult

BeanValidation 는 스프링이 제공하는 검증 오류를 보관하는 객체이다.

검증 오류가 발생하면, FieldError , ObjectError 를 생성해서 BindingResult 에 담아준다

BindingResult 를 사용하면 요청 데이터를 PostCreate 객체에 바인딩 시 오류가 발생해도 컨트롤러가 호출된다.

그럼 이제 이 BindingResult 를 이용해서 컨트롤러 로직을 수정하고 JSON 을 검증하는 테스트코드도 개선해보자.

 

PostController

@PostMapping("/posts")
public Map<String, String> post(
			@RequestBody @Valid PostCreate params, 
			BindingResult result
			) throws Exception {
            
    if (result.hasErrors()) {
        List<FieldError> fieldErrors = result.getFieldErrors();
        FieldError fieldError = fieldErrors.get(0);
        String fieldName = fieldError.getField();
        String errorMessage = fieldError.getDefaultMessage();

        Map<String, String> error = new HashMap<>();
        error.put(fieldName, errorMessage);
        return error;
    }
    return Map.of();
}

 

PostCreate

@Setter
@Getter
@ToString
public class PostCreate {

    @NotBlank(message = "제목을 입력해주세요.")
    private String title;

    @NotBlank(message = "내용을 입력해주세요.")
    private String content;

    public PostCreate(String title, String content) {
        this.title = title;
        this.content = content;
    }
}

ㄴ @NotBlank(message = "")

괄호 안에 원하는 메시지를 입력하면 defaultMessage 로 설정 가능하다.

 

PostControllerTest

    @DisplayName("/posts 요청 시 title 값은 필수다.")
    @Test
    void post() throws Exception {
        mockMvc.perform(
                MockMvcRequestBuilders.post("/posts")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(
                    "{\"title\" : null, \"content\" : \"글 내용입니다 하하\"}"
                    )
            )
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.title").value("제목을 입력해주세요."))
            .andDo(print());
    }

ㄴ andExpect(jsonPath("$.field").value("value"));

MockMvc의 jsonpath() 를 이용하면 Json 형식의 API Response를 검증할 수 있다.

이번 테스트에서는 title 값이 비었으니 @NotBlank 에서 검증 오류가 발생하고

응답값 중 title 이라는 field 에 "제목을 입력해주세요." 라는 문자열이 들어올것이다.

 

 

테스트를 돌려보면 성공한다.

MockMvc 의 jsonPath 를 이용하면 JSON 을 검증하는 더 풍부한 테스트를 작성할 수 있다.

'백엔드 > Spring' 카테고리의 다른 글

Filter  (0) 2023.09.29
Interceptor  (0) 2023.09.29
@ModelAttribute 와 @RequestBody 써보기  (0) 2023.09.21
DispatcherServlet  (0) 2023.08.01
Thread Pool  (0) 2023.07.25

둘의 차이점

@ModelAttribute 와 @RequestBody 는 각각 다루는 HTTP 요청 데이터 차이가 있다.

 

@ModelAttribute 는 HTTP 요청 파라미터(URL 쿼리 파라미터, HTML Form)를 다룰 때 사용한다. 

@RequestBody 는 HTTP 메시지 바디의 데이터를 객체로 변환하며 주로 API JSON 요청을 다룰 때 사용한다.

 

둘은 DTO 객체를 통해 데이터를 받을 때도 차이가 있는데 요약하자면 다음과 같다.

  • URL 쿼리파라미터 혹은 x-www-form-urlencoded@ModelAttribute 로 받는다.
  • JSON 은 @RequestBody 로 받는다.

 

x-www-form-urlencoded와 JSON 의 차이점은 여기에 정리해두었다.

 

1. @ModelAttribute

아래 같은 HTTP 요청 파라미터를 다룬다.

 

URL 쿼리 파라미터

http://localhost:8080/request?username=hello&age=20

 

HTML Form  

message body:
username=hello&age=20

 

PostController

데이터를 받는 DTO 객체 PostCreate 를 하나 만들고 요청 데이터가 잘 받아지는지 확인해볼것이다.

DTO 에 값이 잘 담겼는지는 로그를 통해 확인한다. POST 요청이 성공하면 "Hello" 문자열을 반환한다.

@Slf4j
@RestController
public class PostController {

    @PostMapping("/posts/model")
    public String postModelAttribute(@ModelAttribute PostCreate params) {
        log.info("params = {}", params.toString());
        return "Hello";
    }
}

 

참고로 스프링의 기본 설정은 @ModelAttribute 이기 때문에 아래처럼 생략해도 정상 동작한다.

@Slf4j
@RestController
public class PostController {

    @PostMapping("/posts/model")
    public String postModelAttribute(PostCreate params) {
        log.info("params = {}", params.toString());
        return "Hello";
    }
}

 

PostCreate

public class PostCreate {

    public String title;
    public String content;

    public PostCreate(String title, String content) {
        this.title = title;
        this.content = content;
    }

    @Override
    public String toString() {
        return "PostCreate{" +
            "title='" + title + '\'' +
            ", content='" + content + '\'' +
            '}';
    }
}

 

PostControllerTest

("/posts") 로 POST 요청 시 상태코드 200 과 응답값으로 "Hello" 가 반환되는지 확인하는 테스트를 작성한다.

여기서는 HTML Form 에 쿼리 파라미터 형식으로 전달한다.

URL 에서 쿼리 스트링 형식으로 전달하는것도 @ModelAttribute 로 받을 수 있는데

해당 케이스는 이 글의 컨트롤러 테스트를 참고한다.

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest
class PostControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @DisplayName("/posts 요청 시 Hello 를 출력한다.")
    @Test
    void postModelAttribute() throws Exception {
        mockMvc.perform(
            post("/posts")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .param("title", "글 제목입니다")
                .param("content", "글 내용입니다 하하")
            )
            .andExpect(status().isOk())
            .andExpect(content().string("Hello"))
            .andDo(print());
    }
}

테스트는 성공하고 위 사진의 맨 윗줄의 params 에 값이 잘 담겼음을 확인할 수 있다.

 

2. @RequestBody

아래 같은 HTTP 메시지 바디 를 다룬다.

message body:
{
       "username": "hello",
       "age": 20
}

 

PostController

위와 같이 DTO 에 값이 잘 담겼는지는 로그를 통해 확인한다.

POST 요청이 성공하면 "Hello" 문자열을 반환한다. PostCreate 는 위의 ModelAttribute 케이스와 동일하다.

@Slf4j
@RestController
public class PostController {

    @PostMapping("/posts/request")
    public String postModelRequestBody(@RequestBody PostCreate params) {
        log.info("params = {}", params.toString());
        return "Hello";
    }
}

 

PostControllerTest

("/posts") 로 POST 요청 시 상태코드 200 과 응답값으로 "Hello" 가 반환되는지 확인하는 테스트를 작성한다.

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest
class PostControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @DisplayName("/posts 요청 시 Hello 를 출력한다. @requestBody")
    @Test
    void postModelRequestBody() throws Exception {
        mockMvc.perform(
            post("/posts")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content("{\"title\" : \"글 제목입니다\", \"content\" : \"글 내용입니다 하하\"}")
            )
            .andExpect(status().isOk())
            .andExpect(content().string("Hello"))
            .andDo(print());
    }
}

테스트는 성공하고 위 사진의 맨 윗줄의 params 에 값이 잘 담겼음을 확인할 수 있다.

'백엔드 > Spring' 카테고리의 다른 글

Interceptor  (0) 2023.09.29
BeanValidation 써보기  (0) 2023.09.22
DispatcherServlet  (0) 2023.08.01
Thread Pool  (0) 2023.07.25
Servlet  (0) 2023.07.25

+ Recent posts