웹 서버와 서블릿 컨테이너

전통적인 방식

  • 서버에 톰캣(WAS)를 설치한다.
  • WAS에서 동작하도록 서블릿 스펙에 맞춰서 코드 작성
  • WAR 형식으로 빌드 해서 war파일을 생성 후 배포하는 방식

톰캣 cli

  • 실행
    • ./startup.sh
  • 종료
    • ./shutdown.sh

최근 방식

  • 스프링 부트가 내장 톰캣을 포함하고 있다.
  • 개발자는 코드를 작성하고 JAR로 빌드한 후 실행하면 WAS도 함께 실행된다.

WAR 빌드와 배포

JAR

  • 자바는 여러 클래스와 리소스를 묶어서 JAR라는 압축 파일을 만든다.
  • 이 파일은 JVM 위에서 실행되거나 라이브러리로도 제공 된다.
  • 직접 실행하는 경우 main()메소드가 필요하다.

WAR

  • Web Application Archive(웹 애플리케이션 서버에 배포할 때 사용하는 파일)
  • 웹 애플리케이션 서버 위에서 실행
  • JAR와 비교해서 구조가 더 복잡하다.

구조

  • WEB-INF
    • classes: 실행 클래스 모음
    • lib: 라이브러리 모음
    • web.xml: 웹 서버 배치 설정 파일
  • index.html: 정적 리소스 WEB-INF 를 제외한 나머지 영역은 HTML, CSS 같은 정적 리소스가 사용되는 영역이다.

서블릿 컨테이너 초기화 - 1

  • WAS를 실행하는 시점에 필요한 초기화 작업
  • WAS가 제공하는 초기화 기능을 사용하면 실행 시점에 초기화 과정을 진행할 수 있다.

초기화 개발

서블릿은 ServletContainerInitializer라는 초기화 인터페이스를 제공한다. 서블릿 컨테이너는 실행 시점에 초기화 메소드인 onStartup()을 호출한다.

public interface ServletContainerInitializer {
	public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}

예제

  • 아래와 같이 위 인터페이스를 상속받아서 실행 시 동작 설정을 할 수 있다.
public class MyContainerInitV1 implements ServletContainerInitializer {  
  
    @Override  
    public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {  
        System.out.println("MyContainerInitV1.onStartup");  
        System.out.println("MyContainerInitV1 set = " + set);  
        System.out.println("MyContainerInitV1 servletContext = " + servletContext);  
    }  
}
  • resources/META-INF/services/jakarta.servlet.ServletContainerInitializer 파일에 초기화한 클래스를 패키지 명과 함께 넣어줘야 한다.

서블릿 컨테이너 초기화 - 2

등록하는 방법

  • @WebServlet 애노테이션
  • 프로그래밍 방식

애플리케이션 초기화

  • 애플리케이션 초기화를 진행하려면 먼저 인터페이스를 만들어야 한다.

프로그래밍 방식으로 하는 이유

  • @WebServlet을 사용하면 애노테이션 하나로 서블릿을 편하게 등록할 수 있지만 유연하게 변경하는 것이 어렵다. 반면에 프로그래밍 방식은 코딩을 더 많이 해야하고 불편하지만 무한한 유연성을 제공한다.

초기화 과정

  1. @HandlesTypes애노테이션에 애플리케이션 초기화 인터페이스를 지정한다.
  2. Set<Class<?>>에서 넘어오는 인터페이스 정보가 전달된다.
  3. 클래스가 넘어오기 때문에 리플랙션을 사용하세 객체를 실제 생성(인스턴스 생성)해서 메소드를 실행해 줘야 한다.

애플리케이션 초기화를 하는 이유

  • 편리함
    • 여러 곳을 수정할 필요 없이 특정 인터페이스만 구현하면 된다.
  • 의존성
    • 서블릿 컨테이너와 상관 없는 모양으로 인터페이스를 만들 수 있다.
    • 초기화 코드가 서블릿 컨테이터에 대한 의존을 줄일 수 있다.

스프링 컨테이너 등록

  • Servlet은 외부의 요청을 먼저 받는 곳
  • AnnotationConfigWebApplicationContext은 애노테이션 기반 설정과 웹 기능을 지원하는 스프링 컨테이너

순서

  • 스프링 컨테이너 생성
  • 스프링 MVC 디스패처 서블릿 생성 및 스프링 컨테이너 연결
  • 디스패처 서블릿을 서블릿 컨테이너에 등록

스프링 MVC 서블릿 컨테이너 초기화 지원

  • WebApplicationInitializer이 인터페이스를 상속받아서 구현해주면 된다.
public class AppInitV3SpringMvc implements WebApplicationInitializer {  
  
    @Override  
    public void onStartup(ServletContext servletContext) throws ServletException {  
        System.out.println("AppInitV3SpringMvc.onStartup");
        
	}
  • url 실행의 우선순위는 구체적인 게 더 먼저 실행된다.

스프링 부트와 내장 톰캣

WAR 배포 방식의 단점

  • 톰캣 같은 웹 어플리케이션을 별도로 설치 해야 한다.
  • 개발 환경 설정이 복잡하다.
    • 단순한 자바와 다르게 WAS를 실행하고 WAR하고 연동하기 위한 설정이 필요하다.
  • 톰캣의 버전을 변경하려면 톰캣을 다시 설치해야한다.(별도로 버전관리를 해야 한다)

외장 서버 vs 내장 서버

  • WAR은 외부 외장서버 WAS에 WAR파일을 배포하는 방식
  • JAR안에 WAS 라이브러리도 포함하는 방식으로 main()메서드를 실행해서 바로 동작할 수 있다.

스프링 연결

  • 구현 순서 주석
// EmbededTomcatSpringMain
 
// 톰캣 설정
 
// 스프링 컨테이너 생성
 
// 스프링 MVC에 디스패처 서블릿 생성, 스프링 컨테이너 연결
 
// 디스패처 서블릿 등록 
 

내장 톰켓 - 빌드와 배포 1

  • 내장서버에서 배포하려면 jar형식으로 빌드 해야 한다.
  • 아래와 같이 빌드를 하면 lib 파일이 존재하지 않는다.
task buildJar(type: Jar) {  
    manifest {  
        attributes 'Main-Class': 'hello.embed.EmbedTomcatSpringMain'  
    }  
    with jar  
}

jar파일은 jar파일을 포함할 수 없다.

  • jar 파일을 포함할 수도 없고 포함한다고 해도 인식이 안된다.
  • 이 대안으로는 fat jar, uber jar라고 불리는 방법이다. jar안에는 jar파일을 포함할 수는 없지만 class는 포함이 가능하다.
  • 아래는 fat 자르의 예시
tasks.register('buildFatJar', Jar) {  
    manifest {  
        attributes 'Main-Class': 'hello.embed.EmbedTomcatSpringMain'  
    }  
    duplicatesStrategy = DuplicatesStrategy.WARN  
    // 라이브러리들을 하나씩 돌리면서 class 파일을 뽑아서 빌드에 넣어준다는 의미이다.
    from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }  
    with jar  
}

FAT-JAR로 WAR 단점 해결

  • 톰캣 같은 WAS를 별도 설치할 필요 없다.
  • 별도의 개발 환경을 설정할 필요 없이 main()메서드만 실행하면 된다.
  • 배포 환경도 WAS, WAR를 설치할 필요 없이 JAR를 만들고 이것만 원하는 위치에서 실행하면 된다.
  • 톰캣의 버전 관리도 build.gradle에서 관리할 수 있다.

FAT-JAR의 단점

  • 어떤 라이브러리가 포함되어 있는 지 확인이 어렵다.
  • 파일명 중복을 해결할 수 없다.
    • 클래스나 리소스 명이 중복되는 경우 하나를 포기해야 한다.

편리한 부트 클래스 만들기

  • 구현 순서 주석
MySpringApplication
 
// static run method
 
// run에 기존 코드 복사 
 
// 애노테이션 만들기 
MySpringBootApplication
 
 
// 실행 파일
MySpringBootMain

스프링 부트 웹 서버

  • 내장 톰캣을 사용해서 빌드와 배포를 편리하게 한다.
  • 빌드시 하나의 Jar를 사용하면서 동시에 Fat Jar 문제도 해결한다.

부트 웹 서버 실행 과정

  • 스프링 부트를 실행할 때는 main()메서드에서 run을 실행하면 된다.
  • @SpringBootApplication애노테이션이 있는 현재 클래스를 넘겨주게 된다.
  • 핵심 동작
    • 스프링 컨테이너를 생성한다.
    • WAS(내장 톰캣)를 생성한다.

부트 빌드와 배포

JAR를 푼 결과

  • boot-0.0.1.jar
    • META-INF
      • MANIFEST.MF
    • org/springframework/boot/loader
      • JarLauncher.class: 스프링 부트 main()실행 클래스
    • BOOT-INF
      • classes: 우리가 개발한 class 파일과 리소스 파일
      • lib: 외부 라이브러리
        • spring-webmvc-xxx.jar
        • tomcat-embed-corre-xxx.jar
      • classpath.idx: 외부 라이브러리 경로
      • layers.idx: 스프링 부트 구조 경로

스프링 부트 실행 가능 Jar

실행 가능 Jar

스프링 부트는 이런 문제를 해결하기 위해서 jar 내부에 jar를 포함할 수 있는 구조의 jar를 만들었다.

  • 어떤 라이브러리가 포함되어 있는 지 확인하기 어렵다
    • lib안에 jar가 포함되어 있어서 쉽게 확인할 수 있다.
  • 파일명 중복을 해결할 수 있다.
    • jar 내부에 jar를 포함하기 때문에 내부 경로에 있어서 중복문제가 없다.

실행 순서

  • java -jar xxx.jar를 실행하면 MANIFEST.MF파일을 찾는다.
    • 여기서 Main-Class가 JarLauncher로 지정되어 있다.
    • jarLauncher.main()실행
      • jar 내부에 jar를 읽어드리는 기능을 JarLauncher가 처리를 해준 다음에 우리가 설정한 Start-Class로 지정된 hello.boot.BootApplication을 실행해준다.
      • BOOT-INF/classes/ 인식
      • BOOT-INF/lib/ 인식
    • BootApplication.main()실행

라이브러리 직접 관리

라이브러리를 직접 관리하면 발생하는 문제점

  • 라이브러리 간에 호환이 될 수도 있지만 버전에 따라서 호환이 안될 위험성이 있다.

스프링 부트 라이브러리 버전 관리

  • 개발자는 원하는 라이브러리만 고르면 부트가 알아서 라이브러리의 버전 관리를 해준다.
id 'io.spring.dependency-management' version '1.1.0' // 버전 관리 해주는 플러그인

dependency-management 버전 관리

  • io.spring.dependency-management를 사용하면 spring-boot-dependenceis에 있는 다음 bom 정보를 참고한다.
  • 현재 프로젝트에서 지정한 스프링 부트 버전을 참고한다.
  • 스프링 부트가 관리하지 않는 라이브러리는 직접 버전을 적어줘야 한다.

스프링 부트 스타터

  • 프로젝트를 시작하는 데 필요한 스타트 라이브러리를 제공해준다.
  • 사용하기 편리하게 의존성을 모아놓은 세트
  • starterstarter를 포함할 수 있다.
  • 이름 패턴
    • spring-boot-starter-*

라이브러리 버전 변경

  • ext['tomcat.version'] = '10.1.4'

자동 구성(Auto Configuration)

스프링 부트의 자동 구성

  • 일반적으로 자주 사용하는 수 많은 빈들을 자동으로 등록해주는 기능

JdbcTemplateAutoConfiguration의 예시

  • @AutoConfiguration: 자동 구성을 위해서 필요한 애노테이션
  • @ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
    • IF문과 유사한 기능
    • 이런 클래스가 있는 경우에만 설정이 동작한다.
  • @Import : 스프링에서 자바 설정을 포함해서 추가할 때 사용한다.
  • JdbcTemplateConfiguration여기서 돌아가 보면 빈을 등록하는 로직을 확인할 수 있다.
    • @ConditionalOnMissingBean(JdbcOperations.class)해당 빈이 없는 경우에만 실행한다.
    • JdbcOperationsJdbcTemplate의 부모 인터페이스

용어(둘 다 사용)

자동 설정

  • 빈들을 자동으로 등록해서 스프링 환경을 자동으로 설정해준다.

자동 구성

  • 구성, 배치
  • 자동 구성은 스프링 실행에 필요한 빈들을 자동으로 배치해주는 것이다.

@Conditional

  • 같은 소스 코드인데 특정 상황일 때만 특정 빈들을 등록해서 사용하도록 도와주는 기능

Condition인터페이스

  • @Conditional 애노테이션 내부에 구현이 필요한 클래스
  • matches()메서드가 true를 반환하면 조건에 만족해서 동작한다.
package org.springframework.context.annotation;
 
public interface Condition {
	boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

@Conditional다양한 기능

@ConditionalOnXxx는 스프링이 개발자가 편리하게 사용할 수 있도록 제공한다.

  • @ConditionalOnClass , @ConditionalOnMissingClass
    • 클래스가 있는 경우 동작한다.
  • @ConditionalOnBean , @ConditionalOnMissingBean
    • 빈이 등록되어 있는 경우 동작한다.
  • @ConditionalOnProperty
    • 환경 정보가 있는 경우 동작한다.
  • 나머지는 공식 메뉴얼에서 확인 가능하다.

정리

  • @Conditional: 특정 조건에 맞을 때 설정이 동작하도록 한다.
  • @AutoConfiguration: 자동 구성이 어떻게 동작하는 지 원리 이해

순수 라이브러리 만들기

파일로 넣기

implementation files('libs/memory-v1.jar') // 추가
  • 여기서 문제는 개발자가 라이브러리의 기능을 진행할 때 어떤 빈을 등록하고 어떤 작업을 해야하는 지 어려움이 있을 수 있다. 이런 부분을 자동으로 처리해주는 것이 Auto Configuration 이다.

자동 구성 라이브 만들기

자동 구성 추가

  • @AutoConfiguration
  • @ConditionalOnProperty
package memory;
 
@AutoConfiguration  
@ConditionalOnProperty(name = "memory", havingValue = "on")  
public class MemoryAutoConfig {
 
	@Bean  
	public MemoryFinder memoryFinder() {  
	    return new MemoryFinder();  
	}
 
}

자동 구성 대상 지정

  • 아래의 파일에 추가를 해야 한다.
    • src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
memory.MemoryAutoConfig

자동 구성의 이해

찾는 파일 대상

  • resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`

동작 원리

@SpringBootApplication @EnableAutoConfiguration(자동 구성을 활성화 하는 기능) @Import(AutoConfigurationImportSelector.class

ImportSelector란

설정 정보를 추가하는 방법

  • 정적인 방법: @Import클래스 이것은 정적이다. 코드에 대상이 박혀있어서 동적으로 변경할 수 없다.
// A와 B가 다 필요하네
@Import({AConfig.class, BConfig.class})
  • 동적인 방법: 코드로 프로그래밍해서 설정으로 사용할 대상을 동적으로 선택할 수 있다.
    • 스프링은 설정 정보 대상을 동적으로 선택할 수 있도록 ImportSelector 인터페이스를 제공한다.
package org.springframework.context.annotation;
public interface ImportSelector {
 
	// String 정보에 내가 빈으로 등록하기 원하는 설정 정보를 반환하면 등록이 된다.
	String[] selectImports(AnnotationMetadata importingClassMetadata);
	//...
}

실제 예시

  • 여기서는 고정 String을 넣었지만 동적으로 해줄 수 있다.
public class HelloImportSelector implements ImportSelector {  
  
    @Override  
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {  
        return new String[]{"hello.selector.HelloConfig"};  
    }  
}

동작 방식

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {…}
  • AutoConfigurationImportSelector는 ImportSeletor의 구현체이다.
  • 위에서 지정한 imports 안에 있는 파일들을 찾아서 설정 정보를 갖고 온다.

언제 사용하는가

  • 라이브러리를 만들어서 제공할 때 주로 사용한다.
  • 본인이 작업할 때 필요한 빈들은 알아서 추가하지만 라이브러리를 만들어서 제공할 때 유용한다.

외부설정과 프로필

  • 개발 환경: 개발 서버, 개발 DB 사용
  • 운영 환경: 운영 서버, 운영 DB 사용

외부 설정 이란

  • 빌드는 하나로 하고 실행 시점에 외부 설정값을 주입한다.
  • 개발 버전과 운영 버전의 빌드 결과물이 같기 때문에 운영 환경에도 문제가 발생하지 않는다.
  • 환경이 추가되도 문제가 되지 않는다.

유지보수하기 좋은 애플리케이션 개발의 기본 원칙은 변하는 것과 변하지 않는 것을 분리하는 것이다.

사용하는 4가지 방법

  • OS 환경 변수
    • 운영체제 자체에서 지원한다.
  • 자바 시스템 속성
    • JVM 안에서 사용할 수 있다.
  • 자바 커맨드 라인 인수
    • 실행 시 main(args)메소드에서 사용
  • 외부 파일
    • 프로그램에서 외부 파일을 직접 읽어서 사용

OS 환경 변수

  • 해당 OS를 사용하는 모든 프로그램에서 읽을 수 있는 설정
  • System.getenv()

자바 시스템 속성

  • 자바 프로그램을 실행할 때 사용한다.
  • java -Durl=dev -jar app.jar
  • vm option에 아래와 같이 추가하면 된다.
    • -Durl=devdb -Dusername=dev_user -Dpassword=dev_pw
  • 시스템 내부에서 주입도 가능하다.(하지만 코드 안에서 동작하기 때문에 외부 분리 효과는 없다)
    • System.setProperty(propertyName, "propertyValue")

커맨드 라인 인수

  • java -jar app.jar dataA dataB
public static void main(String[] args) {  
    for (String arg : args) {  
        log.info("arg: {}", arg);  
    }  
}
  • 문제는 Key=value 형식이 아니다.
    • 이 데이터를 사용하려면 파싱해서 사용해야 하는 불편함이 있다.

커맨드 라인 옵션 인수

  • 스프링에서 커맨드 라인 인수를 key=value형식으로 편리하게 사용할 수 있도록 정의
    • --username=userA --username=userB
    • 같은 키를 여러개 받을 수 있다.
ApplicationArguments arguments = new DefaultApplicationArguments(args);  
log.info("SourceArgs: {}", List.of(arguments.getSourceArgs()));  
log.info("NotOptionalArgs: {}", arguments.getNonOptionArgs());  
log.info("OptionalArgs: {}", arguments.getOptionNames());  
  
for (String optionName : arguments.getOptionNames()) {  
    log.info("option args {}={}", optionName,  
        arguments.getOptionValues(optionName));  
}  
  
Set<String> optionNames = arguments.getOptionNames();  
List<String> url = arguments.getOptionValues("url");  
List<String> username = arguments.getOptionValues("username");  
List<String> password = arguments.getOptionValues("password");  
List<String> mode = arguments.getOptionValues("mode");  

Spring에서 사용 방법

@Component
public class CommandLineBean {
 
	private final ApplicationArguments arguments;
	
	// 생성자
 
 
	// 다음과 같이 접근해서 사용 가능하다.
	public void init() {
		log.info("source {}", List.of(arguments.getSourceArgs()));
	}
}

외부설정 - 스프링 통합

  • 지금까지 방법을 확인해보면 외부 설정마다 읽어야 하는 방식이 다르다는 단점이 있다.
  • 이를 해결하기 위해서 추상화를 통해서 문제를 해결한다.

PropertySource

  • 추상 클래스를 제공해서 각각의 외부 설정을 조회하는 구현체를 만들어 뒀다.
  • 스프링은 로딩 시점에 필요한 PropertySource들을 생성 한 후에 Environment에서 사용할 수 있게 연결한다.

Environment

  • 외부 설정에 종속되지 않고 일관성 있게 key=value 형식으로 외부 설정에 접근할 수 있다.
  • 모든 외부 설정은 Environment를 통해 조회할 수 있다.
// 외부에서 주입 받는다.
private final Environment env;
public EnvironmentCheck(Environment env) {
	this.env = env;
}
 
public void init() {
	// 이런식으로 값을 갖고 올 수 있다.
	String url = env.getProperty("url");
}
 

우선 순위

  • 더 유연한 것이 우선권을 갖는다.
  • 범위가 넓은 것 보다 좁은 것이 우선권을 갖는다.

설정 데이터 - 외부

  • 예를 들어 서버 밖에 application.properties라는 이름의 파일을 준비 한다음 로딩 시점에 해당 파일을 읽어서 그 속에 있는 값들을 외부 설정값으로 사용할 수 있다.
  • 외부 설정 값을 파일로 관리할 수 있다.

단점

  • 서버가 여러대면 설정 파일을 자체를 관리하는 것도 번거로워 질 수 있다.
  • 설정 파일이 외부에 있으면 변경 이력을 확인하기가 어렵다.

설정 데이터 - 내부

  • 설정 파일을 외부에 관리하는 것은 번거로운 일이다. 이를 해결하기 위해서 프로젝트 내부에 포함해서 관리하는 것이다.

설명

  • 프로젝트 안에 환경 변수도 함께 포함해서 관리한다.
  • 빌드 시점에 개발 및 운영 설정 파일을 모두 포함해서 빌드한다.
  • 실행할 때 어떤 데이터를 읽어야 할 지 구분자를 사용한다.
    • 프로필을 dev, prod이런 식으로 사용할 수 있다.
    • dev
      • application-dev.properties
    • prod
      • application-prod.properties

프로필

  • 이렇게 구분을 위해서 프로필이라는 개념을 지원한다.
  • 프로필을 지정하지 않고 실행하면 default라는 이름의 프로필을 사용한다.
  • spring.profiles.active
    • application-{profile}.properties

내부 파일 합체

  • 스프링은 물리적인 하나의 파일 안에서 논리적으로 영역을 구분할 수 있다.
  • spring.config.activate.on-profile에 프로필 값을 지정해야 한다.
spring.config.activate.on-profile=dev
url=dev.db.com
username=dev_user
password=dev_pw
#---
spring.config.activate.on-profile=prod
url=prod.db.com
username=prod_user
password=prod_pw
  • application.yml 구분 방법 ---(dash 3)
  • #---위 아래에는 주석을 사용하면 안된다.

우선 순위 - 설정 데이터

  • 스프링은 문서를 위에서 아래로 순서대로 읽으면서 사용할 값을 설정한다.
    • 기존데이터가 있으면 덮어쓴다.
  • 아래와 같이 기본값을 지정할 수 있다.
url=local.db.com
username=local_user
password=local_pw
#---
spring.config.activate.on-profile=dev
url=dev.db.com
username=dev_user
password=dev_pw
#---
spring.config.activate.on-profile=prod
url=prod.db.com
username=prod_user
password=prod_pw

자주 사용하는 우선순위(아래가 더 높다)

  • 설정 데이터(application.properties)
  • OS 환경변수
  • 자바 시스템 속성
  • 커맨드 라인 옵션 인수
  • @TestPropertySource (테스트에서 사용)

설정 데이터 우선 순위

  • jar 내부 application.properties
  • jar 내부 프로필 적용 파일 application-{profile}.propterties
  • jar 외부 application.properties
  • jar 외부 프로필 적용 파일 application-{profile}.propterties

스프링이 지원하는 외부 설정 조회

  • Environment
  • @Value
  • @ConfigurationProperties - 타입 안전한 설정 속성

properties에서는 캐밥 표기법을 권장한다. max-connection=1

Environment

  • Environment.getProperty(keym Type)을 호출할 때 타입 정보를 주면 해당 타입으로 변환해준다.
    • Duration timeout = env.getProperty("my.datasource.etc.timeout", Duration.class);

@Value

  • 아래와 같이 2가지의 방법으로 사용할 수 있다.
  • Value 사용하면 타입에 따라서 타입컨버팅이 가능하다.
@Value("${my.datasource.url}")  
private String url;  
@Value("${my.datasource.etc.max-connection:1}")   // : 를 붙여서 기본 값을 줄 수 있다.
private int maxConnection;  
@Value("${my.datasource.etc.timeout}")  
private Duration timeout;  
@Value("${my.datasource.etc.options}")  
private List<String> options;  
  
@Bean  
public MyDataSource myDataSource1() {  
    return new MyDataSource(url, username, password, maxConnection, timeout, options);  
}  
  
@Bean  
public MyDataSource myDataSource2(  
    @Value("${my.datasource.url}") String url,  
    @Value("${my.datasource.etc.max-connection}") int maxConnection,  
    @Value("${my.datasource.etc.timeout}") Duration timeout,  
    @Value("${my.datasource.etc.options}") List<String> options) {  
    return new MyDataSource(url, maxConnection, timeout, options);  
}

@ConfigurationPropertis

스프링은 외부 설정의 묶음 정보를 개체로 변환하는 기능을 제공 타입 안전한 설정 속성이라고 한다. 외부 설정을 자바 코드로 관리할 수 있다.

  • 자바 빈 주입 방식이라 Getter, Setter 필요하다.

설정 예시

  • properties를 생성하는 방법
@Getter
@Setter
@ConfigurationProperties("my.datasource")  
public class MyDataSourcePropertiesV1 {  
    private String url;  
    private String username;  
    private String password;  
    private Etc etc = new Etc();  
    @Data  
    public static class Etc {  
        private int maxConnection;  
        private Duration timeout;  
        private List<String> options = new ArrayList<>();  
    }  
}
  • 생성자로 생성도 가능하다. (권장)
public Etc(int maxConnection, Duration timeout, @DefaultValue("DEFAULT") List<String> options) {
	this.maxConnection = maxConnection;
	this.timeout = timeout;
	this.options = options;
}
  • properties를 사용하는 방법
    • 아래와 같이 하면 Bean으로 Properties가 등록이 되고 주입 받아서 사용하면 된다.
// 이렇게 사용하는 곳에서 등록을 해야 한다.
@EnableConfigurationProperties(MyDataSourcePropertiesV1.class)  
public class MyDataSourceConfigV1 {  
  
    private final MyDataSourcePropertiesV1 properties;  
  
    public MyDataSourceConfigV1(MyDataSourcePropertiesV1 properties) {  
        this.properties = properties;  
    }  
    
    @Bean
	public MyDataSource dataSource() {
		return new MyDataSource(
		properties.getUrl(),
		properties.getUsername(),
	    ...
    }
}

표기법 주의

  • 스프링은 케밥 표기법을 CamelCase로 잘 변환해준다.
    • max-connection maxConnection

Properties를 글로벌로 선언하는 방법

  • @EnableConfigurationProperties(MyDataSourcePropertiesV1.class)매번 이와 같이 등록하기가 번거론다.
  • MainClass에서 @ConfiguratinoPropertiesScan을 사용하면 Properties를 컴포넌트 스캔 처럼 사용할 수 있다.
  • 빈을 직접등록하는 것과 컴포넌트 스캔을 사용하는 차이의 느낌이다.

입력 값을 검증

  • 타입이 동일할 때 숫자의 범위 및 문자의 길이 같은 경우는 검증이 어렵다.
@ConfigurationProperties("my.datasource")  
@Validated  
public class MyDataSourcePropertiesV3 {  
  
    public MyDataSourcePropertiesV3(String url, String username, String password, Etc etc) {  
        this.url = url;  
        this.username = username;  
        this.password = password;  
        this.etc = etc;  
    }  
  
    @NotEmpty  
    private String url;  
  
    @NotEmpty  
    private String username;  
  
    @NotEmpty  
    private String password;  
  
    @NotEmpty  
    private Etc etc ;  
  
    // 클래스가 중첩되는 경우에 넣어 줘야 한다. 
    @Valid
    public static class Etc {  
  
	    public Etc(int maxConnection, Duration timeout, List<String> options) {  
	        this.maxConnection = maxConnection;  
	        this.timeout = timeout;  
	        this.options = options;  
	    }  
  
        @Min(1)  
        @Max(99)  
        private int maxConnection;  
  
        @DurationMax(seconds = 10)  
        private Duration timeout;  
        private List<String> options = new ArrayList<>();  
    }  
}

예외 확인

가장 좋은 예외는 컴파일 예외 애플리케이션 로딩 시점 예외 고객 서비스 중간에 발생하는 런타입 예외

장점

  • 외부 설정을 객체로 편리하게 변환할 수 있다.
  • 외부 설청의 계층을 편리하게 표현할 수 있다.
  • 타입 안전한 속성을 지정할 수 있다. 타입이 일치하지 않으면 컴파일 시점 시 오류가 발생한다.
  • 검증기를 적용할 수 있다.

YAML

  • YAML Ain’t Markup Language
  • 사람이 읽기 좋은 데이터 구조를 목표로 한다.
  • ---3dash를 사용해서 properties처럼 프로필을 구분할 수 있다.

예시

environments:
	dev:
		url: "https://dev.example.com"
		name: "Developer Setup"
	prod:
		url: "https://another.example.com"
		name: "My Cool App"
  • 사람이 읽기 좋게 계층 구조를 이룬다.

주의

  • application.propertiesapplication.yml을 같이 사용하면 properties가 우선권을 갖는다.

Profile이란

  • 설정값이 다른 정도가 각 환경마다 다른 빈을 등록해야 할다면 어떻게 할까

예시

  • 아래와 같이 결제 관련 모듈이 있다고 가정한다
public interface PayClient {
	void pay(int money);
}
  • 로컬 환경
@Slf4j
public class LocalPayClient implements PayClient {
	@Override
	public void pay(int money) {
		log.info("로컬 결제 money={}", money);
	}
}
  • 운영환경
public class ProdPayClient implements PayClient {
	@Override
	public void pay(int money) {
	log.info("운영 결제 money={}", money);
	}
}
  • Config
    • 아래와 같이 프로필에 맞춰서 빈을 등록해 줄 수 있다.
@Slf4j
@Configuration
public class PayConfig {
	@Bean
	@Profile("default") // 보통 프로필을 설정하지 않으면 default이다. 
		public LocalPayClient localPayClient() {
		log.info("LocalPayClient 빈 등록");
		return new LocalPayClient();
	}
	
	@Bean
	@Profile("prod")
		public ProdPayClient prodPayClient() {
		log.info("ProdPayClient 빈 등록");
		return new ProdPayClient();
	}
}

ApplicationRunner

  • 스프링 빈 등록이 다 끝나고 애플리케이션이 구동 완료 되는 시점에 동작한다.
@Component
@RequiredArgsConstructor
public class OrderRunner implements ApplicationRunner {
	
	private final OrderService orderService;
@Override
	public void run(ApplicationArguments args) throws Exception {
		orderService.order(1000);
	}
}

@Profile의 정체

  • 조건에 따라서 해당 빈을 등록 여부를 결정하는데 이것은 바로 @Conditional이다.