그레들로 프로젝트의 테스트를 실행할때마다
테스트에서 사용하는 자원파일을 찾을 수 없다고 나오길래

왜 못찾는 것이냐?

라고 무시하고(?) 넘어간지 3개월여만에...
그레들 문서(https://docs.gradle.org/current/userguide/java_plugin.html)를 보면서 확인한지 30여분만에 문제를 발견.

src/test/resource

...

src/test/resources

였어야 했는데 말이지...!!! 크앙!!
결함이 발생하던 테스트들 수정하고 정상동작하는 것 확인.

그런데 말입니다.
왜 유닛테스트는 통과되었던거지??


SpringBoot으로 구현한 애플리케이션의 DB를 H2Database(http://h2database.com)를 사용한다.

물론 다른 것을 사용할 수도 있다. 개발하고 있는 제품의 특성에 맞춰 h2Database를 선택했을 뿐.

h2database에 접속할 수 있는 웹콘솔로 h2console이 제공되는데, 이 녀석을 애플리케이션에서
함께 실행할 수 있는 방법을 찾고 있었다.

그러다가 찾은 정보가!

spring boot default H2 jdbc connection (and H2 console)

h2database 에 포함된 org.h2.server.web.WebServlet을 등록하면 콘솔을 빈으로 등록하고 웹상으로 접근이 가능한 것이다!

@Bean
public ServletRegistrationBean h2servletRegistration() {
    ServletRegistrationBean registration = new ServletRegistrationBean(new WebServlet());
    registration.addUrlMappings("/h2console/*");
    return registration;
}

위처럼 빈Bean 선언을 하고 실행을 하면! localhost:{port}/h2console으로 접근하면!



JDBC URL은 database 설정에서 url을 복사해서 넣으면 된다. 테스트하려면 jdbc:h2:mem:test

의 모습을 볼 수 있다.

애플리케이션에서는 스프링시큐리티를 이용하여 접근제어를 하고 있다.

로그인을 하면 다음과 같은 백지화면이 나타난다. 그 이유를 찾아보려고 개발자도구를 열어보니



와 같은 메시지가 뜨는 것을 확인한다. 재빨리 검색을 들어가서!

Content-Security-Policy Spring Security - stackoverflow

스프링시큐리티 설정코드 부분에

@Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      // ...
      .headers()
        .addHeaderWriter(new StaticHeadersWriter("X-Content-Security-Policy","script-src 'self'"))
      // ...
  }

다음과 같이 "X-Content-Security-Policy" 선언을 해줘야 한다. 애플리케이션을 재시작하면!



참 쉽죠잉??


코드의 품질을 높이기 위해서 많이 사용되는 것이 코드에 대한 정적분석을 통해서 코드의 품질을 평가하는 것이다.

코드정적분석에 사용되는 도구로는 checkstyle, findbugs, PMD 등 이 있다. 그레들gradle에 checkstyle, PM 를 추가하는 방법을 설명한다.

1. checkstyle

1.1. 사용방법

1.1.1. gradle's checkstyle 플러그인 추가

apply plugind: 'checkstyle'

build.gradle 파일에 플러그인을 추가한다.

1.1.2. checkstyle.xml 작성

1.1.3. build.gradle 파일에 checkstyle 태스크를 정의한다.

checkstyle {
  ignoreFailures = true // 분석결과 예외가 발생하면 빌드실패 발생시키는 것을 제외
    configFile = file("checkstyle.xml") // 1.1.2 에서 작성한 checkstyle 파일 지정
    reportsDir = file("${buildDir}/checkstyle-output") // 리포트 파일이 위치할 디렉토리 지정
}
 
checkstyleMain {
    reports {
        xml.destination = file("${checkstyle.reportsDir}/checkstyle-report.xml") // 리포트 파일의 위치 및 파일명 지정
    }
}

1.1.4. checkstyle 실행

$ gradle check

checkstyle 플러그인을 추가하면 그레들의 check 태스크를 수정하여 checkstyle 의 checkstyleMain, checkstyleTest 들을 실행시킨다.

1.2. 태스크Tasks

Task nameDepends onTypeDescription
checkstyleMainclassesCheckstyleRuns Checkstyle against the production Java source files.
checkstyleTesttestClassesCheckstyleRuns Checkstyle against the test Java source files.
checkstyleSourceSetsourceSetClassesCheckstyleRuns Checkstyle against the given source set's Java source files.

1.3. checkstyle.xml 기본구조

<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
          "-//Puppy Crawl//DTD Check Configuration 1.3//EN"
          "http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
 
<module name="Checker">
    <property name="charset" value="UTF-8" />
    <property name="severity" value="warning" />
    <property name="fileExtensions" value="java, properties, xml" />
 
    <module name="TreeWalker">
        <!-- 사용되는 모듈과 모듈속성을 정의-->
    </module>
</module>

2. PMD

2.1. 사용방법

2.1.1. gradle's pmd 플러그인 추가

apply plugind: 'pmd'

build.gradle 파일에 플러그인을 추가한다.

2.1.2. pmd 태스크의 속성을 정의한다.

pmd {
  ignoreFailures = true // 분석결과 예외가 발생하면 빌드실패 발생시키는 것을 제외
    reportsDir = file("${buildDir}/pmd-output")
}
 
pmdMain {
    reports {
        xml.destination = file("${pmd.reportsDir}/pmd-report.xml")
        xml.enabled = true
    }
}

2.1.3. 실행

$ gradle check

PMD 플러그인이 check 태스크에 의존성을 추가한다.

2.2. 태스크Tasks

태스크명의존유형설명
pmdMain-Pmd출시 소스파일을 대상으로 PMD 실행
pmdTest-Pmd테스트 소스파일을 대상으로 PMD 실행
pmdSourceSet-Pmd지정된 소스셋SourceSet을 대상으로 PMD 실행

3. 정리

checkstyle과 pmd 에서 생성한 XML 보고서는 JENKINS에서 사용하기 위한 분석데이터로 사용된다.

JENKINS에서 checkstyle, pmd 플러그인을 설치한 후 report.xml 경로를 지정한 후



빌드가 정상적으로 진행되면서 분석문서가 생기면 다음과 같은 항목들이 왼쪽 메뉴에 추가가 되고,



각각의 결과를 확인할 수 있다.




○ 참고문헌


스프링시큐리티의 GrantedAuthority 인터페이스를 enum 타입으로 구현했는데, 이 구현체에 toString() 을 선언하면서 의도와는 다르게 동작하는 문제가 발생했다.

public enum MemberAuthority implements OrderedGrantedAuthorityCodeableEnum {
    /**
     * Administrator
     */
    ADMINISTRATOR("administrator""code.memberAuthority.administrator"1),
    /**
     * Project Manager(project, project member, jobs of project management)
     */
    PROJECT_MANAGER("project-manager""code.memberAuthority.projectManager"2),
    /**
     * Operator( operator jobs of project)
     */
    OPERATOR("operator""code.memberAuthority.operator"3),
    /**
     * Inspector(monitoring)
     */
    INSPECTOR("inspector""code.memberAuthority.inspector"4);
 
    private String code;
    private String key;
    private int order;

이런 코드인데, toString() code, key, order 에 대한 내용을 출력하도록 만들면...MemberAuthority[code="project", key="code.memberAuthority.administrator", order=1]의 형태로 나오게 된다. 스프링시큐리티에서는 권한을 문자열로 받기에 ADMINISTRATOR 으로 나와야하는데... 전혀 다른 형태가 되어버리니 권한 체크가 제대로 되지 않을 수밖에...

○ 정리

  • enum 타입에 대해서 toString()을 적용할 때는 잠시만 고심해보자.


○ SpringBoot 레퍼런스 가이드 설정부분

# FLYWAY (FlywayProperties)
flyway.check-location=false # check that migration scripts location exists
flyway.locations=classpath:db/migration # locations of migrations scripts
flyway.schemas= # schemas to update
flyway.init-version= 1 # version to start migration
flyway.init-sqls= # SQL statements to execute to initialize a connection immediately after obtaining it
flyway.sql-migration-prefix=V
flyway.sql-migration-suffix=.sql
flyway.enabled=true
flyway.url= # JDBC url if you want Flyway to create its own DataSource
flyway.user= # JDBC username if you want Flyway to create its own DataSource
flyway.password= # JDBC password if you want Flyway to create its own DataSource

예제에 보면 1 이 등록된 것을 볼 수 있다. 하지만!!

Caused by: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'flyway' on field 'initVersion': rejected value [1]; codes [typeMismatch.flyway.initVersion,typeMismatch.initVersion,typeMismatch.org.flywaydb.core.api.MigrationVersion,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [flyway.initVersion,initVersion]; arguments []; default message [initVersion]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'org.flywaydb.core.api.MigrationVersion' for property 'initVersion'; nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.String] to required type [org.flywaydb.core.api.MigrationVersion] for property 'initVersion': no matching editors or conversion strategy found]
    at org.springframework.boot.bind.PropertiesConfigurationFactory.validate(PropertiesConfigurationFactory.java:296)
    at org.springframework.boot.bind.PropertiesConfigurationFactory.doBindPropertiesToTarget(PropertiesConfigurationFactory.java:255)
    at org.springframework.boot.bind.PropertiesConfigurationFactory.bindPropertiesToTarget(PropertiesConfigurationFactory.java:227)
    at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(ConfigurationPropertiesBindingPostProcessor.java:296)
    ... 26 more

ㅡ_-);; 그대로 따라해보면 저렇게 오류를 뱉는다.

스프링부트 1.2.3 에서 Flyway와 관련된 ... 부분에서 initVersion 항목이 Integer 가 아니라 MigrationVersion으로 변경이 되었다. 스프링부트 1.2.3 에서 제공하는 flywayDB는 3.1 버전으로, Flyway.class의 코드를 살펴보면

/**
 * Sets the version to tag an existing schema with when executing baseline.
 *
 * @param initVersion The version to tag an existing schema with when executing baseline. (default: 1)
 * @deprecated Use setBaselineVersion() instead. Will be removed in Flyway 4.0.
 */
@Deprecated
public void setInitVersion(MigrationVersion initVersion) {
    LOG.warn("Flyway.setInitVersion() is deprecated. Use setBaselineVersion() instead. Will be removed in Flyway 4.0.");
    this.baselineVersion = initVersion;
}
 
/**
 * Sets the version to tag an existing schema with when executing baseline.
 *
 * @param initVersion The version to tag an existing schema with when executing baseline. (default: 1)
 * @deprecated Use setBaselineVersion() instead. Will be removed in Flyway 4.0.
 */
@Deprecated
public void setInitVersion(String initVersion) {
    LOG.warn("Flyway.setInitVersion() is deprecated. Use setBaselineVersion() instead. Will be removed in Flyway 4.0.");
    this.baselineVersion = MigrationVersion.fromVersion(initVersion);
}

에서 보는 것처럼, 1, 2, 3으로 정의하던 initVersion 관련 항목이 @Depercated 처리되었다.

initVersion 으로 설정하는 부분

String initVersionProp = properties.getProperty("flyway.initVersion");
if (initVersionProp != null) {
    LOG.warn("flyway.initVersion is deprecated and will be removed in Flyway 4.0. Use flyway.baselineVersion instead.");
    setBaselineVersion(MigrationVersion.fromVersion(initVersionProp));
}

baselineVersion

String baselineVersionProp = properties.getProperty("flyway.baselineVersion");
if (baselineVersionProp != null) {
    setBaselineVersion(MigrationVersion.fromVersion(baselineVersionProp));
}

flyway의 릴리즈 노트를 보면 init 관련항목들이 baseline으로 변경된 것을 확인할 수 있었다. 두둥.

Issue 860 Deprecated init(), use baseline() instead.
Issue 860 Deprecated initVersion, use baselineVersion instead.
Issue 860 Deprecated initDescription, use baselineDescription instead.
Issue 860 Deprecated initOnMigrate, use baselineOnMigrate instead.
Issue 860 Deprecated FlywayCallback.beforeInit(), use FlywayCallback.beforeBaseline() instead.
Issue 860 Deprecated FlywayCallback.afterInit(), use FlywayCallback.afterBaseline() instead.
Issue 860 Deprecated MigrationState.PREINIT, use MigrationState.BELOW_BASELINE instead.
Issue 860 Deprecated MigrationType.INIT, use MigrationType.BASELINE instead.

◎ 정리

  • initVersion 보다는 baselineVersion으로 옮겨가자.
  • MigrationVersion 은 LATEST와 EMPTY 로 변환가능...
  • 스프링부트에서 사용한 외부 라이브러리에서 오류가 발생한다면, 외부라이브러리와 관련된 가이드를 살펴보자.
    • 스프링부트 레퍼런스 가이드에 반영이 안되어 있을수도 있다.


+ Recent posts