웹 서버와 서블릿 컨테이너
전통적인 방식
- 서버에 톰캣(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을 사용하면 애노테이션 하나로 서블릿을 편하게 등록할 수 있지만 유연하게 변경하는 것이 어렵다. 반면에 프로그래밍 방식은 코딩을 더 많이 해야하고 불편하지만 무한한 유연성을 제공한다.
초기화 과정
@HandlesTypes애노테이션에 애플리케이션 초기화 인터페이스를 지정한다.Set<Class<?>>에서 넘어오는 인터페이스 정보가 전달된다.- 클래스가 넘어오기 때문에 리플랙션을 사용하세 객체를 실제 생성(인스턴스 생성)해서 메소드를 실행해 줘야 한다.
애플리케이션 초기화를 하는 이유
- 편리함
- 여러 곳을 수정할 필요 없이 특정 인터페이스만 구현하면 된다.
- 의존성
- 서블릿 컨테이너와 상관 없는 모양으로 인터페이스를 만들 수 있다.
- 초기화 코드가 서블릿 컨테이터에 대한 의존을 줄일 수 있다.
스프링 컨테이너 등록
- 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-INFMANIFEST.MF
org/springframework/boot/loaderJarLauncher.class: 스프링 부트main()실행 클래스
BOOT-INFclasses: 우리가 개발한 class 파일과 리소스 파일lib: 외부 라이브러리spring-webmvc-xxx.jartomcat-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/인식
- jar 내부에 jar를 읽어드리는 기능을
BootApplication.main()실행
- 여기서 Main-Class가
라이브러리 직접 관리
라이브러리를 직접 관리하면 발생하는 문제점
- 라이브러리 간에 호환이 될 수도 있지만 버전에 따라서 호환이 안될 위험성이 있다.
스프링 부트 라이브러리 버전 관리
- 개발자는 원하는 라이브러리만 고르면 부트가 알아서 라이브러리의 버전 관리를 해준다.
id 'io.spring.dependency-management' version '1.1.0' // 버전 관리 해주는 플러그인dependency-management 버전 관리
io.spring.dependency-management를 사용하면spring-boot-dependenceis에 있는 다음 bom 정보를 참고한다.- 현재 프로젝트에서 지정한 스프링 부트 버전을 참고한다.
- 스프링 부트가 관리하지 않는 라이브러리는 직접 버전을 적어줘야 한다.
스프링 부트 스타터
- 프로젝트를 시작하는 데 필요한 스타트 라이브러리를 제공해준다.
- 사용하기 편리하게 의존성을 모아놓은 세트
starter도starter를 포함할 수 있다.- 이름 패턴
spring-boot-starter-*
라이브러리 버전 변경
ext['tomcat.version'] = '10.1.4'
자동 구성(Auto Configuration)
스프링 부트의 자동 구성
- 일반적으로 자주 사용하는 수 많은 빈들을 자동으로 등록해주는 기능
JdbcTemplateAutoConfiguration의 예시
@AutoConfiguration: 자동 구성을 위해서 필요한 애노테이션@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })- IF문과 유사한 기능
- 이런 클래스가 있는 경우에만 설정이 동작한다.
@Import: 스프링에서 자바 설정을 포함해서 추가할 때 사용한다.JdbcTemplateConfiguration여기서 돌아가 보면 빈을 등록하는 로직을 확인할 수 있다.@ConditionalOnMissingBean(JdbcOperations.class)해당 빈이 없는 경우에만 실행한다.JdbcOperations은JdbcTemplate의 부모 인터페이스
용어(둘 다 사용)
자동 설정
- 빈들을 자동으로 등록해서 스프링 환경을 자동으로 설정해준다.
자동 구성
- 구성, 배치
- 자동 구성은 스프링 실행에 필요한 빈들을 자동으로 배치해주는 것이다.
@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.activeapplication-{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_pwapplication.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.properties와application.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이다.