728x90
반응형

https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#validator-customconstraints

 

Hibernate Validator 7.0.2.Final - Jakarta Bean Validation Reference Implementation: Reference Guide

Validating data is a common task that occurs throughout all application layers, from the presentation to the persistence layer. Often the same validation logic is implemented in each layer which is time consuming and error-prone. To avoid duplication of th

docs.jboss.org

Spring Project에서 Validation 설정 후에 사용을 하다보면 내가 원하는 형태의 Validation이 필요할 때가 있다. 기본적으로 제공하는 것으로 부족할 때가 반드시 발생하게 된다. 그때에는 Custom Annotation을 만들고, Validator를 연결하여 Contraint Annotation을 만들어서 사용하면 된다. 

 

나의 경우에 가장 먼저 필요한 Custom Validation Contraint는 Request Body에 전달되는 Session Values가 Json String format인지 확인이 필요하였다. (일반적으로 Session data는 Client가 입력하고 싶은 데이터를 입력하여야 하기 때문에 그 값을 Json 형태로 저장해 두면 활용성이 올라갈 것 같아 이렇게 만들었다.)

 

하지만, 기본으로 제공하는 Validation에서는 없는 기능이라 Custom Contraint를 작성하게 되었다. 

 

Annotation Interface

우선 Custom Annotation을 만들기 위해서는 Annotation Interface가 필요하다. 

@Documented			// Javadoc 문서에 Annotation이 포함된다.
@Constraint(validatedBy = JsonStringValidator.class)	// Contraint를 수행하는 Validator class
@Target({ElementType.FIELD})			// Target은 Field에서만 가능하다. (DTO field)
@Retention(RetentionPolicy.RUNTIME)		// 이 Annotation이 동작되는 범위 - Source(컴파일 이후 없어짐), Class(클래스 참조시까지), Runtime(컴파일 이후에도 가능)
public @interface JsonStringConstraint {
    String message() default "Invalid Json String type";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

위와 같이 JsonStringConstraint Annotation Interface를 작성하였다. 엄밀히 얘기하면 Custom Contraint용 Annotation이다. 

Contraint를 작성하기 위해서는 3가지 요소가 반드시 필요한데, message(), groups(), payload()가 그것이다. 

 

  • message - Validation이 실패하였을 경우 표시하는 메세지이다. 이것은 Message Source Accessor과 함께 사용할 수 있다. 
  • groups - Contraint를 Groupping 하는 기능이다. 그룹별로 Message Source를 다르게 사용할 때 등에 사용된다. 
  • payload - 이 값은 Validator에 전달하고 싶은 값을 넣는 곳이다. 예를 들면, Contraint 의 심각도 등을 보내어 심각도에 따라 다른 행위를 하도록 할 수 있다. 

 

JsonStringValidator class

@Slf4j
public class JsonStringValidator implements ConstraintValidator<JsonStringConstraint, String> {

    @Override
    public void initialize(JsonStringConstraint constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        try {
            if (s != null) {
                final ObjectMapper mapper = new ObjectMapper();
                mapper.readTree(s);
            }
            return true;
        } catch (IOException e) {
            LOGGER.debug("String is not json format. {}", s);
            return false;
        }
    }
}

Validator class 는 ContraintValidator Interface를 구현한다. Override되는 메서드는 initailize() 와 isValid() 가 있다. 

여기에서 중요한 것은 isValid() 이다. Contraint가 값을 확인 후에 Validation 결과를 Return 해주어야 한다. 

 

Json String이 유요한지 여부는 Jackson 에 있는 ObjectMapper를 이용하여 확인하였다. 

이 Contraint는 String Field에서만 사용이 가능하다. 

 

사용방법

사용방법은 다른 Validation annotation과 동일하게 사용가능하다. 

@Getter
@Setter
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Schema(description = "Session Creation Request")
public static class SessionCreateRequest {
    @Schema(example = "1L")
    @NotNull(message = "{account.id.empty}")
    private Long accountId;
    @Schema(example = "{\"orderCount\":1}")
    @NotBlank(message = "{session.value.empty}")
    @JsonStringConstraint(message = "{session.value.not.json}")
    private String values;
}

Validation messsage는 Message Source 기능을 사용하여 가져오도록 하였다. 그리고, 다른 Validation Annotation과 함께 사용할 수 있는 것을 볼 수 있다. 

 

아래와 같이 Validation Fail 일 경우 Error response를 받을 수 있다. 

...
"errors": [
        {
            "codes": [
                "JsonStringConstraint.sessionCreateRequest.values",
                "JsonStringConstraint.values",
                "JsonStringConstraint.java.lang.String",
                "JsonStringConstraint"
            ],
            "arguments": [
                {
                    "codes": [
                        "sessionCreateRequest.values",
                        "values"
                    ],
                    "arguments": null,
                    "defaultMessage": "values",
                    "code": "values"
                }
            ],
            "defaultMessage": "String is not JSON format.",
            "objectName": "sessionCreateRequest",
            "field": "values",
            "rejectedValue": "test",
            "bindingFailure": false,
            "code": "JsonStringConstraint"
        }
    ],
    "path": "/api/v1/session"
...

 

728x90
반응형
728x90
반응형

Spring boot 에서 Validation은 보통 Entity와 Request body의 정보가 제대로 들어왔는지 확인하는 경우에 사용한다.

예를 들면, 아래와 같이 Request body의 Variable 의 값이 Null이거나 Blank 일 때를 확인해서 이러한 경우에는 Client에게 Error Reponse 를 보내거나, log 상에 에러를 발생시키게 된다.

@Getter
@Setter
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Schema(description = "Session Creation Request")
public static class SessionCreateRequest {
    @Schema(example = "1L")
    @NotNull(message = "{account.id.empty}")
    private Long accountId;
    @Schema(example = "{\"orderCount\":1}")
    @NotBlank(message = "{session.value.empty}")
    private String values;
}

 

이러한 Validation 작업은 Server의 완성도를 높여줄 수 있고 Client에게 Request 상의 문제를 알려줄 수 있기 때문에 굉장히 유용한 기능이라고 할 수 있다. 셋팅은 완료하고 나면 간단하게 Annotation을 붙이면 체크를 해주니 사용성 또한 아주 간단하다. 

나중에 포스팅을 하겠지만, Custom annotation을 만들어서 체크도 할 수 있다. 

 

그럼 설정방법에 대해서 알아보자

 

Dependency 설정

Spring boot 에서 Validation Dependency를 제공한다. 아래를 pom.xml에 추가하도록 하자.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

 

Configuration setting

Validation Configuration

아주 기본적인 Configuration이다. Configuration이 없어도 Default로 셋팅이 되지만 Validation Message Source를 넣고자 Configuration file을 만들었다. Validation Message Source는 Validation error가 발생되었을 때 그 정보를 정의해둔 메세지 형태로 Client로 보내고자 할 때 사용한다. 아래에 설정 방법을 추가하도록 하겠다. 

@Configuration
public class ValidationConfig {
    private final MessageSource validationMessageSource;

    @Bean
    public LocalValidatorFactoryBean getValidator() {
        LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
        bean.setValidationMessageSource(validationMessageSource);

        return bean;
    }
}

Message Accessor Configuration

Validation Message configuration은 ReloadableResourceBundleMessageSource를 생성하여 몇가지 옵션을 제공한다. 

@Configuration
public class MessageConfig {
    private static final String VALIDATION_MESSAGE_SOURCE_PATH = "classpath:/messages/validation_message";

    @Bean
    public MessageSource validationMessageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename(VALIDATION_MESSAGE_SOURCE_PATH);
        messageSource.setDefaultEncoding("UTF-8");

        return messageSource;
    }

    @Bean("validationMessageSourceAccessor")
    public MessageSourceAccessor validationMessageSourceAccessor() {
        return new MessageSourceAccessor(validationMessageSource());
    }
}

Basename은 Message 목록의 Path를 지정한다. 아래와 같이 Validation에는 message 옵션이 붙는데 Validation이 실패할 경우 보여줄 메세지를 지정한다. `{}`안에 있는 것이 key이며 파일에서 아래의 key를 찾아 메세지를 매핑하게 된다. 

@NotBlank(message = "{session.value.empty}")

Message source 를 생성한 후 그것에 접근할 수 있는 Accessor를 Bean에 등록한다. 

이렇게 설정할 경우에는 메세지를 더 효율적으로 관리할 수 있다. 

 

Message properties 파일

위의 설정의 Basename으로 들어간 파일의 경로에 message.properties 파일이 있어야 한다. 아래와 같이 key값에 대응되는 값을 가지고 있어야 한다. 한가지 주의할 점은 Unit test를 만들 경우에는 아래의 파일이 test/resources에도 있어야 한다. 

# JwtToken
account.id.empty=Account id is empty.
token.value.empty=Jwt token is empty.

# Session
session.value.empty=Session value is empty.

 

@Valid annotation

Vadilation check를 위한 Annotaiton을 붙였다라고 해서 Validation이 되지 않는다. 원하는 Request Body에 `@Valid` annotation을 붙여줘야 한다. 아래 Contoller source를 보면 `@RequestBody` 앞에 `@Valid` annotation이 붙은 것을 볼 수 있다. 

@PostMapping("/session")
public SessionDTO.SessionInfo createSession(@Valid @RequestBody SessionDTO.SessionCreateRequest request) {
    Session session = modelMapper.map(request, Session.class);
    Session createdSession = sessionService.create(session);

    return SessionDTO.SessionInfo.from(createdSession, modelMapper);
}

 

Validation Result

Validation 설정을 완료하고 테스트를 해보면 Validation error가 발생하였을 경우에는 아래와 같은 메세지가 나오게 된다.

{
    "timestamp": "2022-01-19T19:53:48.832+00:00",
    "status": 400,
    "error": "Bad Request",
    "trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public io.coolexplorer.session.dto.JwtTokenDTO$JwtTokenInfo io.coolexplorer.session.controller.JwtTokenController.createToken(io.coolexplorer.session.dto.JwtTokenDTO$JwtTokenCreateRequest): [Field error in object 'jwtTokenCreateRequest' on field 'jwtToken': rejected value []; codes [NotBlank.jwtTokenCreateRequest.jwtToken,NotBlank.jwtToken,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [jwtTokenCreateRequest.jwtToken,jwtToken]; arguments []; default message [jwtToken]]; default message [Jwt token is empty.]] \n\tat org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:141)\n\tat org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:179)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:146)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:681)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:764)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:540)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:895)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1732)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.base/java.lang.Thread.run(Thread.java:829)\n",
    "message": "Validation failed for object='jwtTokenCreateRequest'. Error count: 1",
    "errors": [
        {
            "codes": [
                "NotBlank.jwtTokenCreateRequest.jwtToken",
                "NotBlank.jwtToken",
                "NotBlank.java.lang.String",
                "NotBlank"
            ],
            "arguments": [
                {
                    "codes": [
                        "jwtTokenCreateRequest.jwtToken",
                        "jwtToken"
                    ],
                    "arguments": null,
                    "defaultMessage": "jwtToken",
                    "code": "jwtToken"
                }
            ],
            "defaultMessage": "Jwt token is empty.",
            "objectName": "jwtTokenCreateRequest",
            "field": "jwtToken",
            "rejectedValue": "",
            "bindingFailure": false,
            "code": "NotBlank"
        }
    ],
    "path": "/api/v1/token"
}

 

728x90
반응형
728x90
반응형

Maven Jgitflow plugin 은 Maven command를 통해서 Git flow를 수행하는 Plugin이다. Git flow의 간단한 설명은 아래 문서를 참고하자. 

2022.01.18 - [CI&CD] - Git-flow 란?

 

Git-flow 란?

Giflow is an alternative Git branching model that involves the use of feature branches and multiple primary branches. Git flow는 Git branching model로서 Git으로 관리되는 Branch 들의 관리방법을 정의..

corono.tistory.com

 

Maven Jgit flow는 아래와 같은 Feature를 가지고 있다. 

Features

  • starting a release - creates a release branch and updates pom(s) with release versions 
    • Release branch를 생성하고 Pom 파일에 Release version을 업데이트 한다.
  • finishing a release - runs a maven build (deploy or install), merges the release branch, updates pom(s) with development versions
    • Maven build를 수행하고, Release branch를 mater로 머지하고 develop branch에 Snapshot version을 업데이트 한다.
  • starting a hotfix - creates a hotfix branch and updates pom(s) with hotfix versions
    • Hotfix branch를 생성하고 Pom 파일에 Hotfix version을 업데이트 한다.
  • finishing a hotfix - runs a maven build (deploy or install), merges the hotfix branch, updates pom(s) with previous versions
    • Maven build를 수행하고, Hotfix branch를 mater로 머지하고 버전을 업데이트 한다.
  • starting a feature - creates a feature branch
    • Feature branch를 생성한다.
  • finishing a feature - merges the feature branch
    • Feature branch를 머지한다.

 

이 문서에서는 많이 사용하는 `Starting a release` 와 `Finishing a release` 에 대해서 알아보도록 하겠다. 

 

Plugin 추가

우선, Plugin을 설치해보자. Pom 파일에 추가한다.

<properties>
	<!-- Jgit Configuration -->
    <git.user> </git.user>
    <git.password> </git.password>
</properties>

<plugin>
    <groupId>external.atlassian.jgitflow</groupId>
    <artifactId>jgitflow-maven-plugin</artifactId>
    <version>1.0-m5.1</version>
    <configuration>
        <!-- For specific plugin configuration options,
        see the Goals documentation page -->
        <username>${git.user}</username>
        <password>${git.password}</password>
        <flowInitContext>
            <masterBranchName>main</masterBranchName>
            <developBranchName>develop</developBranchName>
        </flowInitContext>
        <autoVersionSubmodules>true</autoVersionSubmodules>
        <enableSshAgent>true</enableSshAgent>
        <noDeploy>true</noDeploy>
        <pushReleases>true</pushReleases>
        <pullDevelop>true</pullDevelop>
        <pullMaster>true</pullMaster>
        <scmCommentPrefix>[RELEASE] </scmCommentPrefix>
    </configuration>
</plugin>

Plugin 이외에 몇가지 Configuration을 추가하였다. 

  • username / password - Git repository의 접속 Auth 정보를 입력한다. 나의 경우에는 Command line parameter로 계정 정보를 넘겨 줄 것이기 때문에 Properties에 실제 Auth 정보를 넣지 않았다. 
  • flowInitContext - master branch와 develop branch의 이름을 넣어준다. Github이 최근 master branch 이름이 main으로 변경되어서 main으로 바꿔 주었다.
  • autoVersionSubmodules - 자동으로 버전의 입력시켜준다. False로 설정할 경우 사용자가 버전을 명시해 주어야 한다. 
  • enableSshAgent - SSH agent를 Enable 한다.
  • noDeploy - Deploy는 하지 않는다. 
  • pushReleases - Release branch를 remote stream에 push 한다. 
  • pullDevelop - Jgitflow가 초기화 되었을 때 develop branch를 pull 한다.
  • pullMaster - Jgitflow가 초기화 되었을 때 master branch를 pull한다.
  • scmCommentPrefix - Jgitflow 동작 중 변경사항을 저장시에 Prefix를 붙인다. (Release만 사용할 것이기 때문에 [Release]를 붙이도록 하였다. 

이렇게 추가를 하고 pom.xml을 다시 Reload를 하면 끝이다. 

 

Maven Command 설정

Release start

$ -Dgit.user=<username> -Dgit.password=<password> jgitflow:release-start -Dmaven.test.skip=true -Dmaven.javadoc.skip=true

 

 

Release finish

$ -Dgit.user=<username> -Dgit.password=<password> jgitflow:release-finish -Dmaven.test.skip=true -Dmaven.javadoc.skip=true

 

실행결과

아래와 같이 Github repository에 Release 가 되었고 Tagging이 되었다. 

 

728x90
반응형
728x90
반응형
Giflow is an alternative Git branching model that involves the use of feature branches and multiple primary branches.

Git flow는 Git branching model로서 Git으로 관리되는 Branch 들의 관리방법을 정의한 것이다.

Project를 여러 명이 개발하고 그 Project가 Release가 된다면 Version 관리는 필수적이다. 그리고, 소스코드의 버전관리를 위해서는 Branch의 관리가 잘되어야 한다. 아래의 Branch들의 기능을 보면 어떻게 Branch를 관리해야하는지 쉽게 이해가 가능하다.

 

글로 기능을 확인하면 내용을 정확히 이해하기 힘들다. 그림과 함께 간단히 설명해 보겠다. 

 

Release branches

Release branch 은 develop branch의 변경 사항을 master branch 로 merge 시킨 후에 Release version tag를 붙이는 작업에 주로 사용된다. 그리고 develop의 version을 Development version으로 업데이트 한다. Java project에서는 보통 SNAPSHOP 버전이 붙여진다. 

 

Hotfix branches

Hotfix branch는 Release version에서 Bug가 발생하였 때 그것을 해결하기 위한 용도로 사용한다. 위의 그림에서 v0.1 버전에서 Bug가 발생되어 v0.2 버전으로 업데이트 되었다. 이 때 Branch를 master로 merge를 할 때 develop branch의 Development version도 업데이트를 시켜준다. 

 

Feature branches

Feature branch는 새로운 Feature가 추가될 때마다 develop branch에서 파생하여 그 변경 사항을 구현 후 develop으로 머지된다. 

 

 

보통 이러한 Branch들은 기본적으로 많이 사용되기 때문에 잘 이해하고 있으면 도움이 많이 될 것이다.

728x90
반응형

+ Recent posts