본문 바로가기
☕️ JAVA/🍃 Spring

스프링 부트 살펴보기 / 의존성 관리 간소화, 배포 간소화, 자동 설정

by kukim 2023. 6. 4.

웹 백엔드 개발을 시작하며 많은 언어와 프레임워크를 사용해 보았습니다.
튜토리얼 수준으로 Python의 Django, Flask, Node Express, Nest를 접했고 주로 Java SpringBoot, Golang의 Gin을 사용하였습니다. 최근 Gin을 주로 사용하면서 SpringBoot가 그리울 때가 있습니다. Gin에서는 SpringBoot에서 당연히 해주는 것들이 없고, 직접 구현해야 하는 경우가 많았거든요. (언어, 프레임워크가 추구하는 바가 다르기 때문에 직접적인 비교는 실례지만요.) 아무쪼록 Golang Gin 사용 덕분에 직접 구현하며 날 것의 맛을 알게 되었고, MSG 가득한 SpringBoot의 마법(?)에도 관심이 가게 되었습니다.

 

SpringBoot를 사용하며 당연하게(또는 무지성, 감사하게) 사용했던 기술들을 조금씩 파보자~ 란 마음으로 조금씩 정리해보고자 합니다.  주로 토비 님의 스프링 부트 - 이해와 원리 강의와 책 SpringBoot Up & Running(처음부터 제대로 배우는 스프링 부트)과 부로 김영한 님의 Spring 강의들 (MVC 1, MVC 2, 원리 기본 편, 원리 고급 편), 박재성 님의 자바 웹 프로그래밍 Next Step 책을 참고합니다. (Java Spring(Boot)에 좋은 콘텐츠가 많다는 것에 감사함을 느낍니다.)


스프링 부트란

스프링 부트란 스프링을 기반으로 실무 환경에 사용 가능한 독립 실행형 애플리케이션을 복잡한 고민 없이 빠르게 작성할 수 있게 도와준다. 여러 가지 도구의 모음이라고 볼 수 있다.라고 토비 님이 말씀 주셨습니다. 실제로 스프링 부트 공식 문서에도 "부트는 00이다"라고 말하지 않고 길게 이야기하고 있죠. (Docs: Introducing Spring Boot)

 

SpringBoot Up & Running Ch1에서는 스프링 부트의 핵심 기능 3가지를 이야기합니다.

1. 의존성 관리(Dependency Management) 간소화

2. 배포(Depolyment) 간소화

3. 자동 설정(Auto Configuration)

 

1. 의존성 관리(Dependency Management) 최소화

SoftWare 개발할 때 의존성 관리는 상당히 복잡하고 골치 아픈 문제입니다. 라이브러리 버전 하나 올리는 것은 쉬워 보이지만, 라이브러리 간의 의존성이 꼬일 수 있습니다. 오죽하면 Dependency Hell(의존성 지옥)이란 말이 있을까요.

 

SpringBoot는 의존성 관리 최소화를 위해 BOM(Bills Of Materials), POM(Project Object Model), SpringBoot Starters를 사용합니다. (GitHub Repo: spring-boot-starters

REST API 개발을 위해 spring-boot-starter-web 의존성을 추가하면 의존성 하위에 또 starter가 관리되고 있고 내려가다 보면 사용하는 라이브러리가 있습니다. 의존하고 있는 모든 버전을 관리하고 있습니다. 

 

spring-boot-starter-web 예

// spring-boot-starter-web
// https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-starters/spring-boot-starter-web/build.gradle
plugins {
	id "org.springframework.boot.starter"
}

description = "Starter for building web, including RESTful, applications using Spring MVC. Uses Tomcat as the default embedded container"

dependencies {
	api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter"))
	api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-json"))
	api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-tomcat"))
	api("org.springframework:spring-web")
	api("org.springframework:spring-webmvc")
}

// spring-boot-starter-json 
// https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-starters/spring-boot-starter-json/build.gradle
plugins {
	id "org.springframework.boot.starter"
}

description = "Starter for reading and writing json"

dependencies {
	api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter"))
	api("org.springframework:spring-web")
	api("com.fasterxml.jackson.core:jackson-databind")
	api("com.fasterxml.jackson.datatype:jackson-datatype-jdk8")
	api("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
	api("com.fasterxml.jackson.module:jackson-module-parameter-names")
}

// ..

 

2. 배포(Depolyment) 간소화

애플리케이션 배포 과정은 복잡합니다. WAS(Web Application Server e.g. Tomcat, Jetty,...), DB 드라이버 설치...

Spring만 사용했을 때는 WAS를 별도로 띄웠지만(war, jar) SpringBoot는 executable-jar(실행 가능한 jar)를 만듭니다. 실행 가능한 jar 안에 WAS가 포함되어 있기 때문에 jar 실행만 하면 간편하게 배포할 수 있습니다. 애플리케이션 JAR와 종속적인 JAR들을 합쳐서 중첩된 JAR를 만드는 것이죠. 중첩 JAR는 의존성 라이브러리마다 따로 패키징하여 결합하는 방식입니다.

 

이게 가능한 이유는 SpringBoot 코드 안에 Spring Container뿐만 아니라 WAS(e.g. Tomcat), Servlet 컨테이너를 함께 실행시키기 때문입니다.  (SpringBoot의 main에서 실행되는 SpringApplication.run(*. class, args) 로직에서)

 

완벽히 동일하진 않지만, 토비 님의 강의에서도 이를 설명해주고 있습니다. (GitHub: tobyspringboot/helloboot)

 

코드 예시

// 직접 작성한 WAS, Servlet Init 예
public class MySpringApplication {
	public static void run(Class<?> applicationClass, String... args) {
		AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext() {
			@Override
			protected void onRefresh() {
				super.onRefresh();

				ServletWebServerFactory serverFactory = this.getBean(ServletWebServerFactory.class);
				DispatcherServlet dispatcherServlet = this.getBean(DispatcherServlet.class);

				WebServer webServer = serverFactory.getWebServer(servletContext -> {
					servletContext.addServlet("dispatcherServlet", dispatcherServlet)
						.addMapping("/*");
				});
				webServer.start();
			}
		};
		applicationContext.register(applicationClass);
		applicationContext.refresh();
	}
}

// main 코드 실행
@Configuration
@ComponentScan
public class HellobootApplication {
	@Bean
	public ServletWebServerFactory servletWebServerFactory() {
		return new TomcatServletWebServerFactory();
	}

	@Bean DispatcherServlet dispatcherServlet() {
		return new DispatcherServlet();
	}

	public static void main(String[] args) {
		MySpringApplication.run(HellobootApplication.class, args);
	}

}

 

3. 자동 설정(Auto Configuration)

개발 일은 대부분 반복적입니다. 예를 들어 DB를 연결하거나 애플리케이션 종료 시 Graceful shutdown, 단순한 쿼리 자동화, Server, 수많은 setup/teardown/configuration... 등 이 있습니다.

 

SpringBoot는 이를 CoC(Convention Over Configuration, 설정보단 관습)를 사용해서 자동 설정을 해줍니다. CoC 개념은 SpringBoot만의 특징은 아니고 다른 프레임워크도 지원을 합니다.(e.g. Ruby On Rails, JUnit,...) (wiki: CoC)

만약 특별한 설정이 필요한 경우에는 직접 빈을 생성하여 커스텀 설정도 가능하게 설게 되어 있습니다. 다시 말해 개발자가 정해야 하는 많은 설정들의 기본틀을 만들어주고, 단순하고 유연성을 보장합니다. CoC가 때로는 마치 마법과도 같기 때문에 자동 설정에 대해 인지하고 있어야 하겠지만요. 만약 커스텀한 설정이 필요한 경우에는 개발자는 손쉽게 커스텀 Bean을 등록하여 손쉽게 사용할 수 있습니다.

 

스프링 부트에서 자동 설정되는 빈의 목록 예는 아래와 같습니다.

SpringBoot Application 시작 시 @SpringBootApplication 어노테이션의 @EnableAutoConfiguration 어노테이션의 @Import(AutoConfigurationImportSelector.class) ImportSelector 셀렉터의 구현체 중 ImportCandidates.load(AutoConfiguration.class)을 통해 AutoConfiguration.class가 가리키는 파일의 자동 구성 목록을 읽어오게 됩니다.(META-INF/...)

 

@SpringBootConfiguration
@EnableAutoConfiguration // AutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
// 생략
}

// @EnableAutoConfiguration
// 생략
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
// 생략
}


// AutoConfigurationImportSelector 구현체
public class AutoConfigurationImportSelector implements DeferredImportSelector, /* 생략 */ {
//생략

	protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
		List<String> configurations = new ArrayList<>(
				SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
		// ImportCandidates.load() 메서드로 META-INF 파일의 패키지 목록을 읽어옴
        ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add); 
         // 생략    
		return configurations;
	}

}

spring-boot-autoconfigure, META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports


Reference

Docs: Introducing Spring Boot

Dependency Hell

spring-boot-starters

GitHub: tobyspringboot/helloboot

Next Step 책

MVC 1, MVC 2, 원리 기본편원리 고급편

SpringBoot Up & Running(처음부터 제대로 배우는 스프링 부트)

스프링 부트 - 이해와 원리

wiki: CoC

댓글