0314 - 0320


# 0314 - 0320

# 0314 - spring-boot-maven-plugin

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
 
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
 
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

플러그인 등록 유무에 따른 jar파일 내의 META-INF/MANIFEST.MF 정보

  • Main-Class : Springboot application이기 때문에 Main-Class의 Value에 Springboot의 org.springframework.boot.loader.JarLauncher로 지정
  • Start-Class : 어플리케이션의 MainClass를 Start-Class의 Value에 정의한다.
  • Spring-Boot-Classes : 컴파일된 class파일들이 위치한 경로를 정의한다.
  • Spring-Boot-Lib : pom.xml에 추가한 의존하고 있는 라이브러리들이 위치한 경로를 정의한다.

위와 같은 정보들을 spring-boot-maven-plugin이 추가해준다.

spring-boot-maven-plugin은 단순히 MANIFEST.MF에 추가적인 정보를 등록해주는 것만은 아니다. 이플러그인의 핵심은 패키징할 때 다음과 같이 실행 가능한 jar파일로 구조를 변경해준다. (spring-boot-maven-plugin은 BOOT-INF에 컴파일된 class파일을 두는 특이한 구조를 갖추고있다. 그리고 JarLauncher가 이 구조에 있는 class파일/jar파일을 로딩하는 역할을 한다.)


# 0315 - swagger 3 변경 사항

  1. springdoc-openapi-ui 종속성 추가
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.6.6</version>
</dependency>
  1. annotations 변경
  • @Api → @Tag
  • @ApiIgnore → @Parameter(hidden = true) or @Operation(hidden = true) or @Hidden
  • @ApiImplicitParam → @Parameter
  • @ApiImplicitParams → @Parameters
  • @ApiModel → @Schema
  • @ApiModelProperty(hidden = true) → @Schema(accessMode = READ_ONLY)
  • @ApiModelProperty → @Schema
  • @ApiOperation(value = "foo", notes = "bar") → @Operation(summary = "foo", description = "bar")
  • @ApiParam → @Parameter
  • @ApiResponse(code = 404, message = "foo") → @ApiResponse(responseCode = "404", description = "foo")
  1. config 변경
@Bean
public GroupedOpenApi publicApi() {
    return GroupedOpenApi.builder()
            .group("springshop-public")
            .pathsToMatch("/public/**")
            .build();
}
@Bean
public GroupedOpenApi adminApi() {
    return GroupedOpenApi.builder()
            .group("springshop-admin")
            .pathsToMatch("/admin/**")
            .addMethodFilter(method -> method.isAnnotationPresent(Admin.class))
            .build();
}
@Bean
public OpenAPI springShopOpenAPI() {
    return new OpenAPI()
            .info(new Info().title("SpringShop API")
            .description("Spring shop sample application")
            .version("v0.0.1")
            .license(new License().name("Apache 2.0").url("http://springdoc.org")))
            .externalDocs(new ExternalDocumentation()
            .description("SpringShop Wiki Documentation")
            .url("https://springshop.wiki.github.org/docs"));
}

https://www.baeldung.com/spring-rest-openapi-documentation
https://springdoc.org/


# 0316 - @Valid와 @Validated를 이용한 유효성 검증의 동작 원리 및 사용법 예시 (1/2)

Spring으로 개발을 하다 보면 DTO 또는 객체를 검증해야 한다. 이를 별도의 검증 클래스로 만들어 사용할 수 있지만 간단한 검증의 경우에는 JSR 표준을 이용해 간견ㄹ하게 처리할 수 있따.

# 1. @Valid와 @Validated

# [ @Valid를 이용한 유효성 검증 ]

# < @Valid의 개념 및 사용법 >

@Valid는 JSR-303 표준 스펙으로써 제약 조건이 부여된 객체에 대해 빈 검증기(Bean Validator)를 이용해서 검증하도록 지시하는 어노테이션이다. Spring에서는 LocalValidatorFactoryBean을 이용해 JSR 표준의 검증 기능을 사용할 수 있는데, LocalValidatorFactoryBean은 JSR-303의 검증 기능을 이용할 수 있도록 해주는 일종의 어댑터에 해당한다.
JSR 표준의 빈 검증 기술의 특징은 객체의 필드에 달린 제약조건 어노테이션을 참고해 검증을 편리하게 할 수 있다는 것이다. 이 빈 검증 기능을 이용하려면 LocalValidatorFactoryBean을 빈으로 등록하고 ValidationService를 제공해주어야 하는데, SpringBoot에서는 의존성만 추가하면 검증을 위한 빈들이 자동으로 등록된다.

# < @Valid의 동작 원리 >

모든 요청은 프론트 컨트롤러인 디스패처 서블릿을 통해 컨트롤러로 전달된다. 그리고 컨트롤러의 메소드 호출하는 과정에는 메소드의 값을 처리해주는 ArgumentResolver가 동작하는데, @Valid 역시 ArgumentResolver에 의해 처리가 된다.
대표적으로 @RequestBody가 있는 경우에는 Json 메세지를 객체로 변환해주는 작업이 ArgumentResolver의 구현체인 RequestResponseBodyMethodProcessor가 처리하며, 이 내부에서 @Valid로 시작하는 어노테이션이 있을 경우에 유효성 검사를 진행한다. (이러한 이유로 @Valid가 ㅇ아니라 커스톰 어노테이션인 @Valid커스텀네임여도 동작한다.) 만약 ModelAttribute를 사용중이라면 ModelAttributeMethodProcessor에 의해 @Valid가 처리된다. 그리고 검증에 오류가 있다면 MethodArgumentNotValidException 예외가 발생하게 되고, 디스패처 서블릿에 기본으로 등록된 예외 리졸벼(Exception Resolver)인 DefaultHandlerExceptionResolver에 의해 400 BadRequest에러가 발생한다.

이러한 이유로 @Valid는 기본적으로 컨트롤러에서만 동작하며 기본적으로 다른 계층에서는 검증이 되지 않는다. 다른 계층에서 파라미터를 검증하기 위해서는 @Validated와 결합되어야 한다.

# [ @Validated를 이용한 유효성 검증 ]

# < @Validated의 개념 및 사용법 >

파라미터의 유효성 검증은 컨트롤러에서 처리하고 서비스나 레포지토리 계층에서는 유효성 검증을 하지 않는 것이 바람직하다. 하지만 개발을 진행하다보면 불가피하게 다른 곳에서 파라미터를 검증해야 할 수 있다.
Spring에서는 이런 경우를 위해 AOP기반으로 메소드를의 요청을 가로채서 유효성 검증을 진행 할 수 있는 @Validated를 제공하고 있다. @Validated는 JSR 표준 기술이 아니며 Spring 프레임워크에서 제공하는 어노테이션 및 기능이다.
다음과 같이 클래스 레벨에 @Validated를 붙여주고, 유효성을 검증할 메소드의 파라미터에 @Valid를 분여주면 유효성 검증이 진행된다.

@Service
@Validated
public class UserService {
    public void addUser(@Valid UserRequestDto userRequestDto) { ...}
}
# < @Validated의 동작 원리 >

특정 ArgumentResolver에 의해 유효성 검사가 진행되었던 @Valid와 달리 @Validated는 AOP 기반으로 메소드 요청을 인터셉터하여 처리된다. @Validated를 클래스 레벨에 선언하면 해당 클래스에 유효성 검증을 위한 인터셉터가 등록된다. 그리고 해당 클래스의 메소드들이 호출될 때 AOP의 포인트 컷으로써 요청을 가로채서 인터셉터를 통해 유효성 검증을 진행한다 (물론 이러한 동작을 위해 @Validated가 붙은 클래스는 CGLib 기반으로 바이트 조작이 된다.)
이러한 이유로 @Validated를 사용하면 컨트롤러, 서비스, 레포지토리 등 계층에 무관하게 스프링 빈이라면 유효성 검증을 진행할 수 있다. 물론 @Validated에 의한 예외 클래스도 다른데 @Valid에 의한 예외 클래스는 MethodArgumentNotValidException이며, @Validated에 의한 예외 클래스는 ConstrainViolationException이다. 이러한 정보를 알고 있으면 나중에 예외 처리를 할 때 도움이 된다.

# < Validated의 또 다른 기능(유효성 검증 그룹의 지정) >

객체를 검증하기 위한 방법이 클래스에 따라 달라질 수 있다. 예를 들면 일반 사용자의 요청과 관리자의 요청을 보내는 경우에 같은 객체로 요청이 오지만 다른 방식으로 검증해야 하는 경우이다. 이런 경우에는 검증에 사용할 제약 조건이 2가지로 나누어져야 한다.
Spring에서는 이런 경우를 위해 제약 조건 어노테이션에 조건이 적용될 검증 그룹을 지정하여 적용할 수 있는 기능 역시 @Validated를 통해 제공하고 있다.
검증 그룹을 지정하기 위해서는 (내용이 없는)마커 인터페이스를 간단히 정의해야 한다. 위의 예시의 경우에는 사용자인 경우와 관리자인 경우를 분리해야 하므로 다음과 같은 2개의 마커 인터페이스를 만들 수 있다.

public interface UserValidationGroup {}
public interface AdminValidationGroup {}

그리고 해당 제약 조건이 적용될 그룸을 groups로 지정해줄 수 있다. 제약 조건이 적용될 그룹이 여러 개라면 {}를 이용해 그룹의 이름을 모두 넣어주면 된다.

@NotEmpty(groups = {UserValidationGroup.class, AdminValidationGroup.class})
private String name;

@NotEmpty(groups = UserValidationGroup.class)
private String userId;

@NotEmpty(groups = AdminValidationGroup.class)
private String adminId;

그리고 컨트롤러에서도 다음과 같이 제약조건 검증을 적용할 클래스를 지정해주면 된다.

@PostMapping("/user/add") 
public ResponseEntity<Void> addUser(@RequestBody @Validated(UserValidationGroup.class) UserRequestDto userRequestDto) { 
    ... 
}

만약 위와 같이 UserValidationGroup을 @Validated의 파라미터로 넣어주었다면 UserValidationGroup에 해당하는 제약 조건만 검증이 된다. 만약 @Validated에 특정 마커를 지정해주지 않았거나, groups가 지정되어 있는데 @Valid를 이용한다면 다음과 같이 처리된다.

  • @Validated에 특정 클래스를 지정하지 않는 경우 : groups가 없는 속성들만 처리
  • @Valid or @Valdated에 특정 클래스를 지정한 경우 : 지정된 클래스를 groups로 가진 제약사항만 처리

# [ 다양한 제약 조건 어노테이션 ]

JSR 표준 스펙은 다양한 제약 조건 어노테이션을 제공하고 있는데, 대표적인 어노테이션으로는 다음과 같은 것들이 있다.

@NotNull: 해당 값이 null이 아닌지 검증함
@NotEmpty: 해당 값이 null이 아니고, 빈 스트링("") 아닌지 검증함(" "은 허용됨)
@NotBlank: 해당 값이 null이 아니고, 공백(""과 " " 모두 포함)이 아닌지 검증함
@AssertTrue: 해당 값이 true인지 검증함
@Size: 해당 값이 주어진 값 사이에 해당하는지 검증함(String, Collection, Map, Array에도 적용 가능)
@Min: 해당 값이 주어진 값보다 작지 않은지 검증함
@Max: 해당 값이 주어진 값보다 크지 않은지 검증함
@Pattern: 해당 값이 주어진 패턴과 일치하는지 검증함

그 외에도 주어진 값이 이메일 형식에 해당하는지 검증하는 @Email 등 다양한 어노테이션을 제공하고 있는데, 필요한 어노테이션이 있는지는 자바공식문서 (opens new window)를 참고하면 된다
그 외에도 hibernate의 Validator는 해당 값이 URL인지를 검증하는 @URL 등과 같은 어노테이션을 제공하고 있다. 즉, 우리가 필요로 하는 대부분의 제약 사항 어노테이션은 이미 구현되어 있으므로 찾아서 활용한다.

# @Valid와 @Validated 유효성 검증 차이

# @Valid

  • JSR-303 자바 표준 스펙
  • 특정 ArgumentResolver를 통해 진행되어 컨트롤러 메소드의 유효성 검증만 가능
  • 유효성 검증에 실패할 경우 MethodArgmentNotValidException이 발생

# @Validated

  • 자바 표준 스펙이 아닌 스프링 프레임워크가 제공
  • AOP 기반으로 스프링 빈의 유효성 검증을 위해 사용되며 클래스에는 @Validated를, 메소드에는 @Valid를 붙여준다
  • 유효성 검증에 실패할 경우 ConstraintViolationException이 발생

별개로 Message Interpolator를 이용하면 유효성 검증이후 메세지를 다국어 처리 할 수 도 있다.

https://mangkyu.tistory.com/174


# 0320 - 커스텀 애노테이션(Custom Annotation)로 직접 유효성 검사

# [ 검증을 위한 커스텀 어노테이션 생성 ]

@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
@Documented
public @interface Phone {
    String message() default "Invalid Phone";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
  • @Target({FIELD}) : 해당 어노테이션을 필드에만 선언 가능함
  • @Retention(RUNTIME) : 해당 어노테이션이 유지되는 시간으로써 런타임까지 유효함
  • @Constraint(validatedBy = PhoneValidator.class) : PhoneValidator를 통해 유효성 검사를 진행함
  • @Documented : JavaDoc 생성시 Annotation에 대한 정보도 함께 생성

아래의 3가지 속성들은 JSR-303 표준 어노테이션등리 갖는 공통 속성들이다. 해당 속성들은 각각 다음의 역할을 한다.

  • message : 유효하지 않을 경우 반환할 메세지
  • groups : 유효성 검증이 진행될 그룹
  • payload : 유효성 검증 시에 전달할 메타 정보

# [ 검증을 위한 Validator 구현 ]

구현할 Validator는 JSR에서 제공하는 javax.validation의 ConstraintValidator 인터페이스를 구현해주어야 한다. 해당 인터페이스는 다음과 같이 생성되어 있다.

public interface ConstraintValidator<A extends Annotation, T> { 
    
    default void initialize(A constraintAnnotation) { 

    } 
    
    boolean isValid(T value, ConstraintValidatorContext context); 

}

위의 인터페이스는 2가지 제네릭 타입을 받고 있는데, 순서대로 적용될 어노테이션과 적용될 타입에 해당된다. 또한 ConstraintValidator가 갖는 메소드들은 각각 다음의 역할을 한다.

  • initialize : Valdator를 초기화하기 위한 메소드
  • isValid : 유효성을 검증하는 메소드

intialize는 기본적으로 default 메소드로 구현되어 있으므로 초기화할 작업이 없다면 따로 구현해주지 않아도 된다. initialize는 isValid가 처음 호출될 때 1회 호출된다. isValid에는 우리가 검증할 로직을 구현주면 된다.
이제 연락처를 검증하기 위한 Validator를 구현하면 다음과 같다.

public class PhoneValidator implements ConstraintValidator<Phone, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        Pattern pattern = Pattern.compile("\\d{3}-\\d{4}-\\d{4}"); // 예제
        Matcher matcher = pattern.matcher(value);
        return matcher.matches();
    }
}
Last update: September 13, 2022 21:44
Contributors: ahnjs