자바 9 모듈화


아침에 조금씩 살펴보는 중이다.


현재는 자바 8을 기반으로 개발하고 있는데, 얼마 지나지 않아 자바 9 이상(아마도 11?)으로 개발해야할테니 그에 맞춰 슬슬 준비하려는 생각으로 하고 있는데,

시각적으로 보이는 큰 차이라면 기존에는 jdk에 있는 rt.jar 안에서 필요한 클래스를 탐색하여 사용했다면, 이제는 분리된 모듈을 확인하고 그에 따라 사용할 모듈을 선언하는 방식으로 변경되었다 할까? @_@);;;

Java 8 까지 

 Java 9 이후

 



 


사용하는 방식도 제법 많이 달라질 것으로 예상된다. @_@);;
뭐 일단 하나씩...

이미 나온지 꽤 시간이 흐른 Junit 5를 살펴보기 시작한다.


[spring-boot] Junit5 적용기

JUnit5 의존성 추가

JUnit5가 세상에 모습을 드러내놓은지는 제법 됐다. 새로운 프로젝트를 시작하면서 JUnit5 와 Spock 을 기반으로 한 테스트를 작성하고자 한다.

스프링 부트 2에서 JUnit5 에 대한 의존성을 추가하고 테스트를 작성하는 방법을 설명한다.

buildscript {
    ext {
        springBootVersion = '2.0.6.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'io.honeymon.study'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 10

repositories {
    mavenCentral()
}

dependencies {
    implementation('org.springframework.boot:spring-boot-starter-data-jpa')
    implementation('org.springframework.boot:spring-boot-starter-web')

    runtimeOnly('com.h2database:h2')
    compileOnly('org.projectlombok:lombok')

    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude module: 'junit'
    }
    testImplementation('org.junit.jupiter:junit-jupiter-api:5.2.0')
    testCompile('org.junit.jupiter:junit-jupiter-params:5.2.0')
    testRuntime('org.junit.jupiter:junit-jupiter-engine:5.2.0')
}

test {
    useJUnitPlatform()
}

junit4 제외

스프링 부트 스타터 org.springframework.boot:spring-boot-starter-test는 junit4에 대한 의존성을 가지고 있다. 그래서 junit5를 사용하기 위해서는 spring-boot-starter-test에 추가되어 있는 junit4를 제외해야 한다.

testImplementation('org.springframework.boot:spring-boot-starter-test') {
    exclude module: 'junit'
}
Note

그레이들에서 모듈에 대한 의존성 정의는 3개 부분으로 나뉜다.

  • 예: org.springframework.boot:spring-boot-starter-test

    • group: org.springframework.boot

    • module: spring-boot-starter-test

    • version: 생략

이런 구분을 이해하고 나면 위에서 설명한 제외(exclude)방법을 활용할 수 있을 것이다.

  • 그룹과 모듈을 정의하는 경우: exclude group: 'junit', module: 'junit' 으로 정의할 수 있다.

이어서 junit5에 대한 의존성을 추가한다.

dependencies {
    // 생략
    testImplementation('org.junit.jupiter:junit-jupiter-api:5.2.0')
    testCompile('org.junit.jupiter:junit-jupiter-params:5.2.0')
    testRuntime('org.junit.jupiter:junit-jupiter-engine:5.2.0')
    // 생략
}

이어서 test 태스크에서 useJUnitPlatform()를 선언한다. useJUnitPlatform는 테스트 실행시 JUnit 플랫폼(JUnit 5)이라는 것을 정의한다.

/**
 * Specifies that JUnit Platform (a.k.a. JUnit 5) should be used to execute the tests. <p> To configure JUnit platform specific options, see {@link #useJUnitPlatform(Action)}.
 *
 * @since 4.6
 */
@Incubating
public void useJUnitPlatform() {
    useJUnitPlatform(Actions.<JUnitPlatformOptions>doNothing());
}

이렇게 해서 build.gradle에서 JUnit5를 사용하기 위한 위존성 정리를 마쳤다.

테스트 작성

package io.honeymon.study.junit5;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)  // (1)
@SpringBootTest
public class SpringBoot2Junit5ApplicationTests {

    @Test // (2)
    public void contextLoads() {
    }

}
  1. ExtendWith는 JUnit5 에서 반복적으로 실행되는 클래스나 메서드에 선언한다. SpringExtension는 스프링 5에 추가된 JUnit 5의 주피터(Jupiter) 모델에서 스프링 테스트컨텍스트(TestContext)를 사용할 수 있도록 해준다.

  2. `@Test의 경로도 변경(org.junit.Testorg.junit.jupiter.api.Test)되었다.

이제 JUnit5를 기반으로 통합테스트를 위한 준비를 마쳤다.

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

Note

junit5는 람다를 기반으로 한 선언(assertion)을 지원한다. junit4에서 지원했던 기능이 부족하여 assertJ 의존성을 추가해야 했던 불편함을 해소할 수 있다.

20181015 애플리케이션 아키텍처와 객체지향

2018/10/15 사내세미나로 조영호님이 '애플리케이션 아키텍처와 객체지향’이라는 주제로 다음과 같은 주제로 발표하셨다.

1. 개요

Note
  • 애플리케이션 설계방식에 대한 설명 진행

    1. 절차지향 설계

    2. 객체지향 설계

2. 과정 설명

애플리케이션을 설계하는 과정에 대한 설명

  • 참가자들이 도메인에 대한 이해가 필요한 경우에는 천천히 진행

3. 도메인주도설계

  • 시스템을 만들기 위해 도메인 분석

Important

발표를 위해 잘 맞춰진 데이터모델과 도메인모델을 사용했다.

현실은 그렇게 호락호락하지 않아!

3.1. 도메인

  • 영화(Movie)

    • 제목

    • 상영시간

  • 상영(Showing): 영화가 재생되는 시간

    • 영화

    • 상영일시

    • 회차

    • 상영관

  • 할인정핵(DiscountStrategy)

    • 금액 할인(AmountDiscountStrategy): ex) 8,000 - 800 = 7,200

    • 비율 할인(PercentDiscountStrategy) ex) 8,000 - (8,000 * 0.1) = 7,200

  • 할인규칙(Rule)

    • 회차규칙(SequenceRule): 1회차(조조), 10회차

    • 시간규칙(TimeRule): 시간대

  • 예매(Reservation)

    • 상영

    • 인원

3.2. 도메인 예제

Movie(1) - (0..1)Discount(1) - (1..N)Rule

  • 도메인로직

    • 영화는 할인정책을 1개만 가질 수 있다.

    • 할인정책은 최소 1개 혹은 여러 개의 할인규칙을 가질 수 있다.

Note

이끼(8000원) 영화에 대해서 800원할인(금액할인)을 제공한다. 그 영화는 조조, 10회차, 월요일(10:00 ~ 12:00 상영), 목요일(18:00 ~ 21:00) 인 경우 할인한다.

4. 레이어 아키텍처

  • 표현/도메인/모델

    • 위에서 거른된 것들은 도메인 영역이다.

  • 절차지향적으로 작성하는 경우와 도메인적으로 작성하는 경우 모양이 다르게 된다.

5. 도메인 레이어를 설계하는 방법

5.1. 절차(Procedural)지향: 트랜잭션 스크립트(Transaction Script)

  • 데이터와 프로세스를 서로 다른 모듈에 구분짓는다.

  • 기능분할을 우선한다.

  • 프로세스를 분리하고 필요한 도메인을 도출한다.

  • 실무에서 사용하는 경우에는 운영데이터를 기준으로 한다.

    • 업무에 필요한 데이터 모델을 만드는 것을 우선한다. → 나쁜점: 이렇게 만들어진 데이터 모델이 설계를 주도한다.

    • 데이터 모델 1:1 대응 객체를 작성

      • 클래스를 데이터에 등록

      • 1 domain - 1 DAO

  • 예매처리 알고리즘

@Transactional
public Reservation reserveShowing(int customerId, int showingId, int audienceCount) {
  // 1. 데이터베이스로부터 Movie, Showing, Rule 조회
  // 2. Showing 에 적용할 수 있는 Rule 판단
  // 3. if(Rule 존재) {
  //    Discount를 읽어 할인금액 계산
  // } else {
  //    영화상영금액 계산
  // }
  //
  // 4. Reservation 생성 후 데이터베이스 저장
}

시퀀스 다이어그램을 그려보면 중앙집중식 제어(관련된 모델에 대한 제어가 한 곳에서 처리) 스타일이 나타나게된다.

5.2. 객체지향: 도메인 모델(Domain Model)

  • 절차지향과 차이점

    • 프로세스와 데이터를 하나의 객체(도메인) 안에 넣는다.

    • 객체는 행위와 데이터의 조합

5.2.1. CRC Card

  • 책임과 협력을 표현하기 위한 객체지향 설계 도구

  • Candidate(Role or Object): 후보

  • Responsibility: 후보가 수행할 역할

  • Collaborator: 후보와 협력할 객체

Important

기능에 대한 고민이 우선한다.

5.2.2. 예매 생성 책임할당

  • 어떤 기능을 개발해야하는지에 대해서는 정보 전문가에게 책임 할동

    • Reservation ← Movie

  • 영화상영 정보를 알고 있는 전문가에게 할당(Creator)

  • 영화가격 정보를 알고 있는 전문가에게 할당(Information Expert)

    • 영화가격정보를 가장 잘 알고 있는 것은 영화다.

  • 할인율을 적용할 책임을 가진 객체 추가

  • 할인정책을 판단하는 책임을 가진 Specification 객체 추가

  • 책임을 할당할 때는 결합도와 응집도를 따져야 한다. tradeOff 비용

5.2.3. 코드 구현

public class Showing {
    public Reservation reserve(Custom custom, int audienceCount) {}

    pubic Money calculate() {
      return movie.calculateFee(this).time(audienceCount);
    }
}

public Reservation {

}

public abstract class DiscountStrategy {
    public Money calculateDiscountFee(Showing showing) {

      return Money.ZERO;
    }
}

public interface Rule {

}

5.2.4. 위임식(delegated), 분산식(separated) 스타일

  • 도메인 모델

  • 다른 객체에게 위임하게 되면서 응집도는 높이고 결합도는 낮출 수 있다.

  • 아키텍처적인 관점에서 "도메인 모델"

6. 도메인 레이어와 아키텍처

  • 도메인 모델을 사용할 때

    • 객체를 기반으로 작성한 경우

      Note

      도메인 모델이 애플리케이션 아키텍처에 영향을 받아 뒤틀릴 수밖에 없다.

  • 트랜잭션과 DB 영향을 받음

6.1. 캡슐화

  • 트랜잭션을 잡기 위해 서비스 레이어를 구분짓는다.

6.1.1. 서비스 레이어

  • 애플리케이션 경계

  • 애플리케이션 경계 조합

  • 서비스 레이어

    @Transactional
    public ReservationService {
      public Reservation reserveShowing(int customerId, int showingId, int audienceCount) {
        Customer customer = customerRepository.findById(customerId);
        Showing showing = showingRepository.findById(showingId);
    
        Reservation reservation  = showing.reserve(customer, audienceCount);
    
        return reservationRepository.save(reservation);
      }
    }
  • 트랜잭션 스크립트를 사용할 떄

    • 비즈니스 로직이 서비스에 녹아있음

    • 별도의 서비스 레이어 불필요

  • 도메인 모델 단점

    • 도메인 모델을 사용할 때 DB매핑하는 것은 쉽지 않음

    • DB모델을 만드는 기준(중복제거)와 도메인모델을 만드는 기준이 서로 다름

6.1.2. 객체-관계 임피던스(impedance) 불일치 발생

  • 객체 모델과 DB스키마 사이의 불일치

  • 객체 패러다임과 관계 패러다임 간의 불일치

6.1.3. 데이터 매퍼

  • 객체모델과 DB 스키마 간의 독립성 유지

  • ORM(Object-relation Mapper): 객체와 엔티티 사이의 관계를 매핑

6.2. 트랜잭션 스크립트

  • DAO(테이블 데이터 게이트웨이)

7. 선택의 기로

우리가 짜는 프로그램은 두 가지 요구사항을 만족시켜야 한다. 우리는 오늘 완성해야 하는 기능을 구현하는 코드를 짜야하는 동시에 내일 쉽게 변경할 수 있는 코드를 짜야 한다.

— 샌디 메츠

어떤 변경이 발생할 지 알아야 잘 설계됐는지 여부를 알 수 있다.

Note

객체지향의 장점은 명사를 사용하여 개념을 유추할 수 있다는 것이다.

  • OCP(Open-Closed principal): 확장에는 열려있고 변경에는 닫혀있는 정책

  • 여러 영화할인규칙을 가지는 할인정보를 만든다.

  • 요구사항이 어떻게 변경될지 모른다면…​

  • 코드를 간단하게 작성하고 변경에 대한 대응하도록 작성하기

  • 변경이 발생할 때마다 리팩토링하여 변경하기 쉬운 코드

  • 행위에 집중되어 있기에 빈번한 변경이 발생할 수 있다.

    • 객체지향설계인 경우 변경에 대해서 코드를 변경하지 않고 확장가능한 코드를 작성할 수 있다.

도메인 모델복잡성을 알고리즘에서 분리하고 객체 간의 관계로 만들 수 있다.

유효성 검사, 계산, 파생 등이 포함된 복잡하고 끊임없이 변하는 비즈니스 규칙을 구현해야 한다면 객체모델을 사용해 규칙을 처리하는 것이 현명하다.

— Martin Fowler

8. 정리

명확하게 딱 떨어지는 도메인과 그에 부합하는 데이터모델을 설계하고 구현하는 일은 쉽지 않다. 비즈니스 로직을 끊임없이 분석하고 정의하여 그것을 구현하고 리팩토링해야 한다. 소프트웨어는 끊임없이 변화하는 그 순간까지는 살아있는 녀석이다.

[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 형식에 맞춰서 파일을 재구성한다.

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

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

윈도우즈 환경에서 자바 개발환경 구축하기

+ Recent posts