Last update: @5/11/2023
주의
본 포스팅은 인프런 강의를 통해 학습한 내용을 임의로 요약한 것으로 일부 내용의 오류 및 누락, 링크 숨김 등이 존재합니다.
“웹 서버와 서블릿 컨테이너” ~ “외부설정과 프로필2”까지 학습한 내용입니다.
내장 톰캣
•
기존
◦
WAS(Web Application Server)인 톰캣을 별도로 설치하고, 프로젝트는 WAR(Web Application Archive) 형식으로 빌드하여 톰캣 webapps 폴더 내에 배포
•
스프링 부트
◦
내장 톰캣을 라이브러리로 포함하여 JAR(Java Archive) 파일로 빌드 및 실행
◦
JAR 파일이 JVM 위에서 실행된다면, WAR는 WAS 위에서 실행됨
서블릿 컨테이너 초기화 - WAS에 직접 배포 시 초기화 방법
•
WAS 실행 시점에 필요한 초기화 작업들
◦
필터와 서블릿 등록
◦
스프링 컨테이너 생성
◦
디스패처 서블릿 등록
•
초기화 방법
◦
web.xml 설정
◦
자바 코드 이용
▪
서블릿 컨테이너가 제공하는 ServletContainerInitializer 인터페이스 구현
onStartup 메서드 내에 ServletContext를 이용해 직접 서블릿 등록
@HandlesTypes 애노테이션을 이용해 어플리케이션 초기화
스프링 컨테이너 등록
•
스프링 MVC는 위와 같은 서블릿 컨테이너 초기화 작업을 미리 만들어 둠
스프링 지원 애플리케이션 초기화를 사용하려면 WebApplicationInitializer 인터페이스 구현체 생성
내장 톰캣
•
내장 톰캣을 직접 다룰 일은 거의 없고, 깊이 학습하는 것은 별 의미가 없음
내장 톰캣 직접 등록 및 스프링 연동
JAR 파일 문제
•
JAR 파일은 JAR 파일을 포함할 수 없음
◦
FatJAR(또는 UberJAR)
▪
JAR로 된 라이브러리들을 class로 풀어서 JAR에 포함킨 것
▪
class로 풀려있기 때문에 어떤 라이브러리가 사용되고 있는 지 추적하기 어려움
▪
라이브러리가 class로 풀리기 때문에 파일명 중복이 발생함
스프링 부트
•
모든 초기화를 진행하는 클래스 제작
package hello.boot;
import hello.spring.HelloConfig;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import java.util.List;
public class MySpringApplication {
public static void run(Class configClass, String[] args) {
System.out.println("MySpringApplication.run args=" + List.of(args));
// 톰캣 설정
Tomcat tomcat = new Tomcat();
Connector connector = new Connector();
connector.setPort(8080);
tomcat.setConnector(connector);
// 스프링 컨테이너 생성
AnnotationConfigWebApplicationContext ac =
new AnnotationConfigWebApplicationContext();
ac.register(configClass);
// 스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결
DispatcherServlet dispatcher = new DispatcherServlet(ac);
// 디스패처 서블릿 등록
Context context = tomcat.addContext("", "/");
tomcat.addServlet("", "dispatcher", dispatcher);
context.addServletMappingDecoded("/", "dispatcher");
try {
tomcat.start();
} catch (LifecycleException e) {
throw new RuntimeException(e);
}
}
}
Java
복사
•
컴포넌트 스캔 기능을 추가한 @MySpringBootApplication 애노테이션 추가
package hello.boot;
import org.springframework.context.annotation.ComponentScan;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ComponentScan
public @interface MySpringBootApplication {
}
Java
복사
•
메인 클래스 추가
package hello;
import hello.boot.MySpringApplication;
import hello.boot.MySpringBootApplication;
@MySpringBootApplication
public class MySpringBootMain {
public static void main(String[] args) {
System.out.println("MySpringBootMain.main");
MySpringApplication.run(MySpringBootMain.class, args);
}
}
Java
복사
◦
이 클래스가 속한 패키지 이하 컴포넌트 스캔 및 초기화가 이루어짐
•
실제 스프링 부트의 코드
@SpringBootApplication
public class BootApplication {
public static void main(String[] args) {
SpringApplication.run(BootApplication.class, args);
}
}
Java
복사
◦
run 메서드 내부에서 스프링 컨테이너를 생성하고 내장 톰캣을 생성함
◦
그 외 수 많은 작업들이 이루어짐
스프링 부트 실행 가능 JAR
•
스프링 부트는 JAR를 포함할 수 있는 특별한 구조의 JAR를 만듦(Excutable JAR)
•
자바 표준은 아니고 스프링 부트에서 새롭게 정의한 것
•
내부 구조
◦
META-INF
▪
MANIFEST.MF
◦
org/springframework/boot/loader: 실행 가능 Jar를 실제로 구동시키는 클래스들 위치
▪
JarLauncher.class: 스프링 부트 main() 실행 클래스
◦
BOOT-INF
▪
classes: 우리가 개발한 class 파일과 리소스 파일
▪
lib: 외부 라이브러리
•
실행 과정
◦
MANIFEST.MF 인식
◦
JarLauncher.main() 실행
▪
BOOT-INF/classes/ 인식
▪
BOOT-INF/lib/ 인식
◦
BootApplication.main() 실행
라이브러리 관리 및 spring-boot-starter
•
스프링 부트는 수 많은 라이브러리의 버전을 직접 관리해줌 (호환성 테스트)
•
build.gradle에 아래처럼 dependency-management 플러그인 추가
plugins {
id 'org.springframework.boot' version '3.0.2'
id 'io.spring.dependency-management' version '1.1.0'
id 'java'
}
Java
복사
•
스프링 부트에서 버전을 관리하는 라이브러리는 build.gradle에 아래처럼 버전을 명시하지 않고 적어주면 됨
implementation 'org.springframework:spring-webmvc'
Java
복사
•
스프링 부트에서 버전을 관리하지 않는 라이브러리는 아래처럼 버전을 명시해줘야 함
implementation 'org.yaml:snakeyaml:1.30'
Java
복사
•
스프링부트는 프로젝트를 시작하는데 필요한 관련 라이브러리를 모아둔 스타터 라이브러리를 제공함
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
Java
복사
◦
이름 패턴
▪
공식: spring-boot-starter-*
▪
비공식: thirdpartyproject-spring-boot-starter
◦
자주 사용하는 스타터
▪
spring-boot-starter
▪
spring-boot-starter-jdbc
▪
spring-boot-starter-data-jpa
▪
spring-boot-starter-data-mongodb
▪
spring-boot-starter-data-redis
▪
spring-boot-starter-thymeleaf
▪
spring-boot-starter-web
▪
spring-boot-starter-validation
▪
spring-boot-starter-batch
•
외부 라이브러리 버전 변경하는 법 (거의 쓸 일 없음)
ext['tomcat.version'] = '10.1.4'
Java
복사
자동 구성(Auto Configuration)
•
필요한 클래스들을 자동으로 빈으로 등록해주는 것
•
스프링 부트는 spring-boot-autoconfigure 라이브러리에 자동구성을 모아두었고, 스프링 부트 프로젝트에 기본적으로 사용됨
•
@Conditional 애노테이션을 통해 특정 조건에 맞을 때 설정이 동작하도록 함
◦
예) 일반적으로 @ConditionalOnMissingBean(xxx.class) 애노테이션을 통해 직접 등록된 빈이 없을 경우 자동 등록시킴
Condition 인터페이스 구현 → 해당 구현체를 값으로 하는 @Conditional 애노테이션을 설정 파일에 부착하는 방법으로 적용
◦
스프링이 이미 만들어 놓은 Condition 구현체들 예시
▪
@ConditionalOnProperty
▪
@ConditionalOnClass, @ConditionalOnMissingClass
▪
@ConditionalOnBean, @ConditionalOnMissingBean
▪
@ConditionalOnResource
▪
@ConditionalOnWebApplication, @ConditionalOnNotWebApplication
▪
@ConditionalOnExpression
•
@AutoConfiguration : 라이브러리에서 어떤 클래스가 빈으로 등록되어야 하는 지에 대한 설정 파일 지정
package memory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnProperty(
name = {"memory"},
havingValue = "on"
)
public class MemoryAutoConfig {
public MemoryAutoConfig() {
}
@Bean
public MemoryController memoryController() {
return new MemoryController(this.memoryFinder());
}
@Bean
public MemoryFinder memoryFinder() {
return new MemoryFinder();
}
}
Java
복사
◦
자동 구성 대장 지정
▪
위치:
src/main/resources/META-INF/spring/
▪
파일 이름:
org.springframework.boot.autoconfigure.AutoConfiguration.imports
▪
파일 내용:
memory.MemoryAutoConfig
Java
복사
◦
스프링 부트는 시작 지점에 위 파일의 정보를 읽어서 자동 구성으로 사용함
◦
spring-boot-autoconfigure 라이브러리 내에도 똑같은 이름의 파일이 있음
내부를 보면 3.0.2버전 기준 142개의 AutoConfiguration 파일이 등록되어 있음
이 라이브러리는 이 등록된 자동 구성 클래스들의 모음인 것
•
자동 구성 동작 순서(원리)
◦
@SpringBootApplication → 내부에 @EnableAutoConfiguration 존재 → 내부에 @Import(AutoConfigurationImportSelector.class) 존재
▪
@Configuration이 있는 설정 클래스에 @Import(xxx.class)로 추가해오기
@Import(ImportSelector) 사용 → ImportSelector 인터페이스 구현
▪
AutoConfigurationImportSelector는 위 ImportSelector를 구현한 클래스로, org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일을 모두 찾아서 자동 구성 파일을 등록함
•
정리하면 각 라이브러리 제조사는 @AutoConfiguration 애노테이션이 붙은 설정파일을 제공하고, 스프링 부트는 ImportSelector를 통해 라이브러리들의 AutoConfiguration을 자동 구성으로 등록함. 그리하면 각 라이브러리마다 필요한 클래스들이 빈으로 등록됨
•
자동구성을 알아야할 경우
◦
직접 라이브러리를 만들 때 (드묾)
◦
자동 구성 관련 문제점을 찾을 때
외부 설정
•
하나의 빌드 파일을 외부 설정을 통해 다르게 작동하게 하고 싶을 경우(개발/운영 서버 분리 등) 외부 설정 사용
OS 환경 변수 사용
자바 시스템 속성 사용 (VM(Virtual Machine) 옵션)
커맨드 라인 인수 사용 - 직접 파싱 필요
커맨드 라인 옵션 인수 - 스프링 부트 제공
스프링 통합
◦
외부 파일 - 빌드 후 생기는 .jar 파일 위치에 application.properties 추가
◦
내부 파일
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
Java
복사
#---으로 구분 (구분선 위아래 # 주석 달면 안 됨)
spring.config.activate.on-profile이 없는 것이 default, 아래로 내려가면서 해당되는 프로필의 설정값으로 갱신
실행 시 프로필 설정에 따라 적용되는 것이 다름. 설정하지 않으면 맨 위 default 적용
▪
VM 옵션 실행 시
java -Dspring.profiles.active=prod -jar app-0.0.1-SNAPSHOT.jar
Java
복사
▪
커맨드 라인 옵션 인수 실행 시
java -jar app-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
Java
복사
▪
yml 사용 시 예제 (—--으로 구분) → 현업에서는 가독성 때문에 yml 많이 사용
my:
datasource:
url: local.db.com
username: local_user
password: local_pw
etc:
max-connection: 1
timeout: 60s
options: LOCAL, CACHE
---
spring:
config:
activate:
on-profile: dev
my:
datasource:
url: dev.db.com
username: dev_user
password: dev_pw
etc:
max-connection: 10
timeout: 60s
options: DEV, CACHE
---
spring:
config:
activate:
on-profile: prod
my:
datasource:
url: prod.db.com
username: prod_user
password: prod_pw
etc:
max-connection: 50
timeout: 10s
options: DEV, CACHE
Java
복사
스프링이 지원하는 외부 설정 사용 방법
Environment
•
@Value - 직접 필드에 사용할수도 있고 파라미터에 사용할수도 있음
package hello.config;
import hello.datasource.MyDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
import java.util.List;
@Slf4j
@Configuration
public class MyDataSourceValueConfig {
@Value("${my.datasource.url}")
private String url;
@Value("${my.datasource.username}")
private String username;
@Value("${my.datasource.password}")
private String password;
@Value("${my.datasource.etc.max-connection}")
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.username}") String username,
@Value("${my.datasource.password}") String password,
@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, username, password, maxConnection, timeout, options);
}
}
Java
복사
•
@ConfigurationProperties - 외부 설정 정보 묶음을 객체로 변환
package hello.datasource;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@Data
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV1 {
private String url;
private String username;
private String password;
private Etc etc;
@Data
public static class Etc {
private int maxConnection;
private Duration timeout;
private List<String> options = new ArrayList<>();
}
}
Java
복사
설정 파일은 아래처럼
package hello.config;
import hello.datasource.MyDataSource;
import hello.datasource.MyDataSourcePropertiesV1;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@Slf4j
@EnableConfigurationProperties(MyDataSourcePropertiesV1.class)
@RequiredArgsConstructor
public class MyDataSourceConfigV1 {
private final MyDataSourcePropertiesV1 properties;
@Bean
public MyDataSource dataSource() {
return new MyDataSource(
properties.getUrl(),
properties.getUsername(),
properties.getPassword(),
properties.getEtc().getMaxConnection(),
properties.getEtc().getTimeout(),
properties.getEtc().getOptions()
);
}
}
Java
복사
@EnableConfigurationProperties(MyDataSourcePropertiesV1.class) 처럼MyDataSourcePropertiesV1 객체를 필요한 곳에서 주입받아 사용하면 됨
setter 삭제하면서 안전하게 생성자 주입
package hello.datasource;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@Getter
@AllArgsConstructor
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV2 {
private String url;
private String username;
private String password;
private Etc etc;
@Getter
public static class Etc {
private int maxConnection;
private Duration timeout;
private List<String> options = new ArrayList<>();
public Etc(int maxConnection, Duration timeout, List<String> options) {
this.maxConnection = maxConnection;
this.timeout = timeout;
this.options = options;
}
}
}
Java
복사
validation 적용
package hello.datasource;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.hibernate.validator.constraints.time.DurationMax;
import org.hibernate.validator.constraints.time.DurationMin;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@Getter
@AllArgsConstructor
@ConfigurationProperties("my.datasource")
@Validated
public class MyDataSourcePropertiesV3 {
@NotEmpty
private String url;
@NotEmpty
private String username;
@NotEmpty
private String password;
private Etc etc;
@Getter
public static class Etc {
@Min(1)
@Max(999)
private int maxConnection;
@DurationMin(seconds = 1)
@DurationMax(seconds = 60)
private Duration timeout;
private List<String> options = new ArrayList<>();
public Etc(int maxConnection, Duration timeout, List<String> options) {
this.maxConnection = maxConnection;
this.timeout = timeout;
this.options = options;
}
}
}
Java
복사
@Profile
•
환경마다 빈을 다르게 등록하고싶을 경우
package hello.pay;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Slf4j
@Configuration
public class PayConfig {
@Bean
@Profile("default")
public LocalPayClient localPayClient() {
log.info("LocalPAyClient 빈 등록");
return new LocalPayClient();
}
@Bean
@Profile("prod")
public ProdPayClient prodPayClient() {
log.info("ProdPayClient 빈 등록");
return new ProdPayClient();
}
}
Java
복사