Search

[강의 요약] 김영한 - 스프링 부트 - 핵심 원리와 활용

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
복사