스프링 부트 2.1.0 에는 스프링 5.1.0 프레임워크가 반영되었다.

스프링 5.1.0 은 컴포넌트 탐색과정에서 발생하는 오버헤드를 감소시키기 위한 여러가지 정책이 반영되었는데, 그 중에 하나가 생성한 빈을 덮어쓰는 상황을 강제적으로 제한한다.

그래서 동일한 이름을 가진 스프링 빈이 등록되려고 하면 BeanDefinitionOverrideException 이 발생한다.

DefaultListableBeanFactory 중 일부
BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
if (existingDefinition != null) {
    if (!isAllowBeanDefinitionOverriding()) {
        throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
    }
    else if (existingDefinition.getRole() < beanDefinition.getRole()) {
        // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
        if (logger.isInfoEnabled()) {
            logger.info("Overriding user-defined bean definition for bean '" + beanName +
                    "' with a framework-generated bean definition: replacing [" +
                    existingDefinition + "] with [" + beanDefinition + "]");
        }
    }
    else if (!beanDefinition.equals(existingDefinition)) {
        if (logger.isDebugEnabled()) {
            logger.debug("Overriding bean definition for bean '" + beanName +
                    "' with a different definition: replacing [" + existingDefinition +
                    "] with [" + beanDefinition + "]");
        }
    }
    else {
        if (logger.isTraceEnabled()) {
            logger.trace("Overriding bean definition for bean '" + beanName +
                    "' with an equivalent definition: replacing [" + existingDefinition +
                    "] with [" + beanDefinition + "]");
        }
    }
    this.beanDefinitionMap.put(beanName, beanDefinition);
}

이는 스프링 부트의 설정이 아닌 스프링 프레임워크에서 빈을 등록하는 과정에서 발생하는 것으로 이에 대한 속성을 비활성화할 수 있는 기능을 제공한다.

spring.main.allow-bean-definition-overriding: true

위와 같이 spring.main.allow-bean-definition-overriding 속성을 true 로 선언하면 문제가 해결될 것이다. 이후에는 중복되는 빈을 찾아서 중복이 발생하지 않도록 조치해야 한다.


[springboot] 스프링 부트 플러그인: spring-boot-gradle-plugin

Note
Boot Spring Boot

Boot Spring Boot! 출간 이후 발표할 기회가 몇 번 있었고 그 때마다 개발자 인생은

B(uild) - C(oding) - D(eploy)

이라고 이야기 하고 있다. 개발자는 빌드(Build)와 배포(Deploy) 사이에 코드를 작성한다.

스프링 부트를 기반으로 한 개발자는 지속적으로 자신이 작성한 코드를 빌드하여 배포하는데, 빌드동작을 제대로 이해하지 못하는 경우가 많다. 책 홍보도 할겸(!!!) 스프링 부트 빌드 플러그인의 동작방식에 대한 이야기를 풀어보고자 한다(이걸 작성하면서도 내게 공부가 되었다).


스프링 부트는 자바에서 사용하는 범용적인 빌드 도구로 그레이들과 메이븐을 지원한다(ANT 등을 지원하지만 그건 거론하지 않는다). '지원한다’는 이야기는 각 도구별로 스프링 부트에 빌드 및 배포에 필요한 플러그인을 제공한다는 뜻이다. 스프링 부트는 컨테이너를 내장하여 실행가능한 JAR로 배포하기 위한 리패키징 과정을 거치게 되는데 스프링 부트 빌드도구 플러그인(그레이들, 메이븐)이 담당한다.

Note

스프링 부트 그레이들 플러그인을 기준으로 설명한다.

  • bootRepackage: 1.5 까지 사용된 스프링 부트 태스크

  • bootJar(or bootWar): 자바 압축방식 jar와 war 를 지원하는 태스크가 분리되었다.

스프링 부트 2.0 부터 스프링 부트 그레이들 플러그인에서는 bootRepackage 가 각각 jar 태스크를 확장한 bootJarwar 태스크를 확장한 bootWar로 분리되었다.

bootJarjar 태스크를, bootWarwar 태스크를 비활성화한다.

Note

jar 혹은 war 태스크를 사용하기 위해서는 다음과 같이 명시적으로 태스크를 활성화시켜야 한다.

jar {
  enabled = true
}

멀티 모듈 프로젝트에서 다른 모듈에서 참조하는 참조 모듈인 경우에는 굳이 스프링 부트 리패키징 과정을 거칠 필요가 없기 때문에 다음과 같이 선언한다.

bootJar {
  enabled = false
}

jar {
  enabled = true
}

스프링 부트 배포파일 구조

스프링 부트 리패키징을 통해 생성된 실행가능한 JAR는 크게 3개 부분으로 나눠 압축처리한다.

  • META-INF: 자바 애플리케이션 메타 정보 제공

  • org.springframework.boot.classloader: 스프링 부트 클래스 로더

  • BOOT-INF 애플리케이션 실행에 필요자원 제공(컴파일된 바이트코드, 리소스, 의존 라이브러리)

실제 생성되는 애플리케이션 배포파일을 풀어보면 대략 다음과 같은 구조를 가진다.

boot-spring-boot.jar
 +-META-INF // (1)
 |  +-MANIFEST.MF
 +-org
 |  +-springframework
 |     +-boot
 |        +-loader // (2)
 |           +-<spring boot loader classes>
 +-BOOT-INF // (3)
    +-classes
    |  +-io.honeymon.boot.springboot
    |     +-BootSpringBoot.class
    +-lib
       +-dependency1.jar
       +-dependency2.jar

스프링 부트 빌드 플러그인이 생성하는 실행가능한 JAR는 배포파일에 대한 사양(Jar File Specification)을 따른다.

META-INF 디렉터리에는 자바 플랫폼에서 실행되는 애플리케이션에 대한 구성, 확장, 클래스 로더 및 서비스 등을 등록하는 MANIFEST.MF 파일이 생성된다. 이 파일을 생성할 때 추가할 수 있는 속성은 다음과 같이 스프링 부트 그레이들 플러그인을 통해 제공하는 bootJar.manifest 를 추가 정의가능하다.

bootJar {
    manifest {
        attributes("Implementation-Title": "${project.name}", // (1)
                "Implementation-Version": "${project.version}") // (2)
    }
}

이렇게 정의된 속성은 빌드를 통해 다음과 같이 MANIFEST.MF에 추가된다.

Manifest-Version: 1.0
Implementation-Title: boot-spring-boot
Implementation-Version: 1.0.0.RELEASE
Start-Class: io.honeymon.boot.springboot.BootSpringBootApplication // (1)
Main-Class: org.springframework.boot.loader.JarLauncher //(2)
  1. 실행되어야 할 애플리케이션 지정

  2. 자바 메인 클래스로 선언된 것은 JarLauncher다. 이 속성이 실행되어 BootSpringBootApplication를 적재하고 실행한다.

이렇게 추가된 정보는 애플리케이션이 실행될때 활용되며 다음과 같이 배너(banner.txt)에 정의하여 사용할 수 있다.

   ___            __    ____         _             ___            __
  / _ )___  ___  / /_  / __/__  ____(_)__  ___ _  / _ )___  ___  / /_
 / _  / _ \/ _ \/ __/ _\ \/ _ \/ __/ / _ \/ _ `/ / _  / _ \/ _ \/ __/
/____/\___/\___/\__/ /___/ .__/_/ /_/_//_/\_, / /____/\___/\___/\__/
                        /_/              /___/

* Github Repository: https://github.com/ihoneymon/boot-spring-boot
* Application: ${application.title}${application.formatted-version}, Spring Boot Version:${spring-boot.formatted-version} // (1)
  1. application.titleapplication.formatted-version 외에 다른 속성을 활용하고 싶은 경우는 스프링 부트 배너 사용자 정의 사용부분을 읽어보기 바란다.

  • application.titleMANIFEST.MF파일에서 Implementation-Title: boot-spring-boot를 읽어온다.

  • application.formatted-versionMANIFEST.MF파일에서 Implementation-Version: 1.0.0.RELEASE를 읽어오며 접두어 v를 추가한다.

이렇게 작성된 배너가 리패키징된 jar를 실행하면 다음과 같이 출력되는 것을 확인할 수 있다.

   ___            __    ____         _             ___            __
  / _ )___  ___  / /_  / __/__  ____(_)__  ___ _  / _ )___  ___  / /_
 / _  / _ \/ _ \/ __/ _\ \/ _ \/ __/ / _ \/ _ `/ / _  / _ \/ _ \/ __/
/____/\___/\___/\__/ /___/ .__/_/ /_/_//_/\_, / /____/\___/\___/\__/
                        /_/              /___/

* Github Repository: https://github.com/ihoneymon/boot-spring-boot
* Application: boot-spring-boot (v1.0.0.RELEASE), Spring Boot Version: (v2.0.4.RELEASE)

스프링 부트에서는 3가지 유형의 런처(JarLauncher, WarLauncher 그리고 PropertiesLauncher)를 제공한다. 이 런처의 목적은 실행가능한 JAR 파일에 포함되어 의존 라이브러리로부터 있는 자원(.class 및 기타 파일)을 적재하는 것이다. JarLauncherBOOT-INF/lib를 찾아보고 WarLauncherWEB-INF/lib를 찾아본다. PropertiesLauncher의 경우 BOOT-INF/lib를 탐색하며 환경변수 LOADER_PATH 혹은 loader.properties 파일에서 loader.path 속성을 통해 정의할 수 있다.

jar 로 배포하는 경우에는 JarLauncher가 기본구성된다. war 로 배포하는 경우에는 WarLauncher가 기본구성된다.

실행할 런처를 사용자 정의하는 방식은 다음과 같다.

build.gradle
bootWar {
	manifest {
		attributes 'Main-Class': 'org.springframework.boot.loader.PropertiesLauncher'
	}
}

BOOT-INF

애플리케이션 실행에 필요한 메인 애플리케이션 클래스(ex: BootSpringBoot)를 비롯 실행하는데 필요한 애플리케이션 속성 파일 및 의존 라이브러리, 자원을 포함하고 있다. 개발자가 작성한 코드가 담겨있는 영역이라고 보면 된다.

각 디렉터리별 구성은 다음과 같다.

  • classes:: 개발자 작성 코드

    • application.yml

    • banner.txt

    • git.properties

    • META-INF:: 애플리케이션 메타정보를 가진 파일들이 위치한다.

      • build-info.properties:: 빌드정보 파일

      • spring-configuration-metadata.json:: spring-boot-configuration-processor가 생성한 사용자 정의 애프리케이션 속성으로 @ConfigurationProperties 애너테이션을 선언하고 작성한 클래스 정보를 추출하여 생성

      • spring.factories:: 스프링 부트 기동시 실행되어야 하는 리스너나 자동구성 등 정의

    • 템플릿 및 정적 자원: templates, static 등 포함

  • lib: 애플리케이션 실행에 필요한 의존성 라이브러리 위치

정리

스프링 부트 빌드 플러그인은 실행가능한 jar 혹은 war로 배포하는 경우 리패키징이라는 과정을 통해 패키징된 애플리케이션 배포파일을 실행할 내장 컨테이너(톰캣, 언더토우 등)와 함께 애플리케이션 및 애플리케이션이 가지는 의존 라이브러리를 하나의 묵직한 jar(Fat jar 라고도 부르는)로 재압축하는 과정을 거치게 된다. 이 과정에서 실행가능한 jar 형식에 맞춰서 파일을 재구성한다.

이 재구성되는 방식을 이해하면 애플리케이션 실행시 메타정보를 정의하고 이를 활용하는 것도 가능해진다. 스프링 부트 애플리케이션이 실행될 때 메인 클래스 및 의존 라이브러리들이 적재되는 과정도 이해할 수 있다.

굳이 암기할 필요는 없다. 아~ 이렇게 되는구나 하고 이해만 해도 충분하다.

https://jiandson.co.kr/event/bookconcert/boot-spring-boot



스프링 부트 관련한 책을 내고 하는 첫번째 방어전 입니다.

많은 분들의 도전을 기다리고 있습니다. ^^


스프링 부트와 관련한 이런저런 이야기를 하고 나눌 수 있기를 기대하고 있습니다.


2018/08/27(월) 19:30 역삼역 부근(이라기에는 좀 멀리 있는) 마루 180 에서 세미나를 진행합니다.


10일 밖에 남지 않았지만... 어떤 이야기를 해야할까 아직도 고민중입니다.


[springboot] 2.0 을 사용하면서 패키징과 관련된 문제가 발생한다면

스프링 부트 2.0 을 사용하면서 패키징과 관련된 찝찝한 느낌을 받고 있는 요즘이다.

그 주요 원인은 스프링 부트 그레이들 플러그인 2.0 때문이다. 스프링 부트 그레이들 플러그인 2.0이 되면서 기존에 있던 bootRepackagebootJarbootWar 으로 변경(Spring Boot’s new Gradle plugin)되면서 부터다.

실행가능한 jar와 war를 만드는데 사용했던 bootRepackage를 배포파일에 따라서 각각 JarbootJarWarbootWar로 확장하는 과정에서 이런 상황이 발생했다.

핵심요점만 이야기하자면 bootJarbootWar를 비활성화하면 패키징 태스크가 실행되지 않는다.

//bootJar 비활성화시
bootJar.enabled = false
jar.enabled = true

//bootWar 비활성화시
bootWar.enabled = false
war.enabled = true

위와 같은 형태로 상위 태스크를 활성화시켜줘야지만 패키징 태스크가 실행된다. 이러한 사실을 알게된 것은

가 있었고, 오늘은 EBS(AWS Elastic Beanstalk)에서 "구성 파일(.ebextensions)을 사용하여 고급 환경 사용자 지정" 기능을 사용하기 위해서 프로젝트 내에 .ebextensions 디렉터리를 war 파일 패키징시 이동하기 위해 그레이들내에서 다음과 같이 선언했는데 .ebextensions 디렉터리가 패키징에서 누락된 것을 발견했다.

build.gradle
project(":api-module") {
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'
    apply plugin: 'war'

    war {
        from('./src/main/ebextensions') {
            into('.ebextensions')
        }
    }

    dependencies {
        compile project(":core-module")

        compile('org.springframework.boot:spring-boot-starter-data-rest')
        compile('org.springframework.boot:spring-boot-starter-hateoas')
        compile('org.springframework.boot:spring-boot-starter-web-services')
        compile('org.springframework.boot:spring-boot-starter-webflux')
        compile('org.springframework.data:spring-data-rest-hal-browser')

        compileOnly "org.springframework.boot:spring-boot-configuration-processor"

        testCompile('org.springframework.boot:spring-boot-starter-test')
        testCompile('io.projectreactor:reactor-test')
    }
}

위와 같이 모듈을 정의했는데, war 태스크를 정의한 부분이 무시됐다. (이런!!)

하지만 그렇다고 분노하거나 노여워하지 말자(그건 내가 다 했으니…​).

여기서 대응할 수 있는 방법은 크게 2가지가 있다.

  • 새로추가된 booWar 태스크를 사용하는 경우

  • 기존 war 태스크를 유지하는 경우

Note

스프링 부트를 기반으로 할 때는 실행가능하게 패키징(Repackaging)을 하는 bootJarbootWar를 사용한다고 생각하면 마음의 평화가 찾아올 듯 하다.

추천방법: 새로 추가된 booWar 태스크를 사용하는 경우

project(":api-module") {
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'
    apply plugin: 'war'

    bootWar {
        from('./src/main/ebextensions') {
            into('.ebextensions')
        }
    }

    dependencies {
        compile project(":core-module")

        compile('org.springframework.boot:spring-boot-starter-data-rest')
        compile('org.springframework.boot:spring-boot-starter-hateoas')
        compile('org.springframework.boot:spring-boot-starter-web-services')
        compile('org.springframework.boot:spring-boot-starter-webflux')
        compile('org.springframework.data:spring-data-rest-hal-browser')

        compileOnly "org.springframework.boot:spring-boot-configuration-processor"

        testCompile('org.springframework.boot:spring-boot-starter-test')
        testCompile('io.projectreactor:reactor-test')
    }
}

기존 war 태스크를 유지하는 경우

Note

톰캣과 같은 WAS에 war 파일을 배포한다면 실행가능하게 패키징을 필요로 하지 않으니 bootWar를 비활성화 하자.

project(":api-module") {
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'
    apply plugin: 'war'

    bootWar.enabled = false // (1)
    war.enabled = true  // (2)

    war {
        from('./src/main/ebextensions') {
            into('.ebextensions')
        }
    }

    dependencies {
        compile project(":core-module")

        compile('org.springframework.boot:spring-boot-starter-data-rest')
        compile('org.springframework.boot:spring-boot-starter-hateoas')
        compile('org.springframework.boot:spring-boot-starter-web-services')
        compile('org.springframework.boot:spring-boot-starter-webflux')
        compile('org.springframework.data:spring-data-rest-hal-browser')

        compileOnly "org.springframework.boot:spring-boot-configuration-processor"

        testCompile('org.springframework.boot:spring-boot-starter-test')
        testCompile('io.projectreactor:reactor-test')
    }
}
  1. bootWar를 비활성화한다.

  2. war를 활성화한다.

정리

  • 기술을 사용할 때는 참고문서를 보자.

참고문헌

[spring-boot] gradle multi module 사용하면서 gradle plugin bootJar.enabled=false 선언했을 때 jar 파일 생성안된다면

멀티 모듈을 가지는 스프링 부트 기반의 멀티프로젝트를 구성하는 과정에서 조금 당황스런 상황을 겪었다.

bootJar.enabled=false

빌드 했을 때 위처럼 선언된 공통 모듈이 빌드된 배포본에 포함되지 않는 상황이 발생했다.

Note

인텔리제이에서 테스트 러너를 그레이들로 설정하지 않으면 당황스런 순간을 맞이하게 된다.

인텔리제이 기본 테스트 러너에서는 인텔리제이에서 컴파일한 build 디렉터리를 기반으로 테스트를 진행해서 별다른 문제가 없지만 그레이들 빌드 테스트의 경우에는 컴파일 및 빌드를 하면서 테스트가 진행되는데 위에서 언급한 bootJar.enabled=false만 선언된 모듈은 jar 파일을 생성하지 않기 때문에 이를 참조하는 하위 모듈에서 관련된 파일을 읽어오지 못하게 된다.

이와 관련된 문제를 찾아보다가 발견한 한줄기 빛!

마지막 댓글을 보면

bootJar.enabled=false
jar.enabled=true

jar.enabled=true 옵션을 추가하면 Jar 파일 생성이 진행된다. 우후!

스프링 부트에서 bootRepackage 에서 bootJar 로 변경되면서 뭔가 이상한 짓을 한 듯 하다.

Note

BootJar 문서를 살펴보면 확장하면서 재정의한 영향으로 보인다. jar.enabled 옵션을 활성화한다.

+ Recent posts