Backend 04
Spring Boot의 자동 설정 이해
Spring과 Spring Boot가 둘 다 Spring이라면 무엇이 실제로 달라졌는지 모르겠다.
근거 · 교안 p25-p28
1장: Spring과 Spring Boot, 이름은 같은데 왜 다를까?
솔라의 손가락이 노트북 트랙패드 위에서 멈칫했다. 화면에는 두 개의 웹 브라우저 탭이 나란히 열려 있었다. 하나는 ‘Spring으로 웹 프로젝트 시작하기’, 다른 하나는 ‘Spring Boot로 5분 만에 API 서버 만들기’. 두 튜토리얼 모두 ‘스프링’이라는 단어를 품고 있었지만, 프로젝트를 시작하는 방식은 완전히 달라 보였다.
“언니, 이거 좀 이상해.”
거실 테이블 건너편에서 책을 읽던 루나가 고개를 들었다. 솔라는 노트북 화면을 루나 쪽으로 돌리며 말을 이었다.
“분명 둘 다 ‘Spring’인데, 어떤 글에서는 프로젝트 설정 파일이 너덧 개는 기본이라고 하고, 다른 글에서는 파일 하나만 만들면 그냥 실행이 된대. Spring Boot는 그냥 Spring의 최신 버전 같은 거 아니었어? 왜 이렇게 시작부터 다른 거야?”
솔라의 목소리에는 이름이 주는 친숙함과 실제 사용법의 간극에서 오는 미묘한 배신감이 묻어났다. 마치 같은 부모에게서 태어난 자매인데 성격이 완전히 다른 것처럼, ‘스프링’이라는 같은 이름을 쓰는 두 기술이 그녀를 혼란스럽게 만들고 있었다.
루나는 잠시 솔라의 화면을 들여다보더니, 자신의 노트북을 열었다.
“말로만 들으면 더 헷갈릴 수 있겠다. 우리 눈으로 직접 확인해볼까? 아주 간단한 ‘Hello, World!’를 화면에 보여주는 웹 애플리케이션을 만든다고 상상해보자. 그리고 그 목표를 달성하기 위해, ‘전통적인 Spring 방식’과 ‘Spring Boot 방식’에 각각 뭐가 필요한지 나란히 늘어놔 보는 거야.”
루나는 IDE를 켜고 두 개의 텅 빈 프로젝트를 생성했다. 왼쪽에는 classic-spring-project, 오른쪽에는 modern-boot-project.
“자, 먼저 왼쪽. 전통적인 스프링 방식으로 웹 애플리케이션을 만들려면…”
루나는 실제로 코드를 작성하는 대신, 필요한 파일과 설정의 목록을 하나씩 만들어나갔다.
“일단 pom.xml 파일에 spring-webmvc 라이브러리를 추가해야겠지. 그리고 웹 요청을 처리할 서블릿 컨테이너, 예를 들면 톰캣 라이브러리도 넣어줘야 하고. 버전끼리 충돌하지 않게 신경 써야 하는 건 덤이야.”
루나의 손길에 따라 classic-spring-project 폴더 안에는 가상의 파일 목록이 그려졌다.
pom.xml(의존성 설정)web.xml(DispatcherServlet 설정)servlet-context.xml(웹 계층 빈 설정)root-context.xml(비즈니스 계층 빈 설정)MyController.java(실제 로직)
“XML 파일 대신 자바 클래스로 설정을 할 수도 있지만, 핵심은 같아. ‘웹 요청이 들어오면 DispatcherServlet이 가장 먼저 받도록 해라’, ‘컨트롤러는 이 패키지 안에서 찾아라’, ‘뷰 파일의 경로는 여기다’ 같은 지시 사항을 우리가 개발자가 직접, 명시적으로 알려줘야만 해.”
파일 목록만 봐도 머리가 지끈거리는 기분이었다. 이제 막 ‘Hello, World!’를 시작하려는 것뿐인데, 배보다 배꼽이 더 커 보였다.
“그럼 오른쪽은?”
솔라가 modern-boot-project 폴더를 가리키며 물었다. 루나는 오른쪽 프로젝트 폴더를 클릭했다. 그 안은 놀랍도록 휑했다.
pom.xmlModernBootApplication.java
단 두 개의 파일이 전부였다.
“오른쪽은 start.spring.io 같은 곳에서 ‘Spring Web’ 의존성 하나만 선택하면 딱 이 상태로 시작해. 그리고 우리는 ModernBootApplication.java 파일 안에 컨트롤러 코드를 바로 추가하기만 하면 돼.”
솔라는 믿기지 않는다는 듯 화면을 번갈아 쳐다봤다. 왼쪽은 무언가를 시작하기 위해 거쳐야 할 수많은 의식과 절차처럼 보였다. 반면 오른쪽은 마치 잘 닦인 고속도로 입구 같았다.
“잠깐만. 오른쪽은 이게 끝이야? 왼쪽은 실행하기도 전에 저렇게나 많은 약속을 정해야 하는데? 오른쪽은… 그냥 바로 달리면 되는 거네?”
“바로 그거야.”
루나가 말했다.
“왼쪽 방식에서는 우리가 스프링 프레임워크에게 하나부터 열까지 모든 걸 지시해야 해. 마치 가구 부품을 잔뜩 사 와서 설명서를 보며 하나하나 조립하는 것과 같지. 하지만 오른쪽, 스프링 부트는 달라. 우리가 ‘웹 프로젝트를 만들고 싶다’는 의사만 내비치면, 예를 들어 spring-boot-starter-web이라는 의존성 하나만 추가하면, 스프링 부트가 스스로 생각하는 거야.”
루나는 잠시 말을 고르며, 솔라가 이해하기 쉬운 비유를 찾았다.
“‘아, 이 개발자는 웹 애플리케이션을 만드려는구나. 그럼 당연히 내장 톰캣 웹 서버가 필요하겠네. 웹 요청을 처리할 DispatcherServlet도 자동으로 등록해줘야겠다. 별다른 설정이 없으니, 아마 가장 보편적인 방식으로 구성해주면 좋아하겠지?’ 이렇게 가장 널리 쓰이는 ‘관례(Convention)’를 기준으로 필요한 설정들을 미리 다 해주는 거야.”
그제야 솔라의 얼굴에 퍼져 있던 혼란의 안개가 걷히기 시작했다. 두 기술의 차이는 버전의 차이나 기능의 유무가 아니었다. 그것은 프레임워크를 대하는 ‘철학’의 차이였다.
“아하! 그럼 Spring Boot가 완전히 새로운 프레임워크가 아니라, 기존 Spring Framework를 훨씬 쉽게 쓸 수 있도록 도와주는 일종의 ‘헬퍼’ 또는 ‘스타터 팩’ 같은 거구나! 개발자들이 늘상 하던 반복적인 설정 작업을 ‘이런 관례대로 할 거지?’ 하고 먼저 제안해서 자동화해주는 거고.”
솔라는 방금 전 자신이 내렸던 판단, ‘Spring Boot는 Spring의 새 버전일 뿐’이라는 생각이 얼마나 얕았는지 깨달았다. 진짜 핵심은 ‘자동화’에 있었고, 그 자동화의 근거는 ‘관례’였다.
명쾌해진 머릿속으로 새로운 질문이 스며들었다.
“신기하다. 근데 Spring Boot는 그걸 어떻게 아는 거지? 내가 pom.xml에 spring-boot-starter-web이라는 글자 몇 개를 추가했다는 사실만으로, 어떻게 웹 서버를 띄워야 한다는 판단까지 하는 걸까? 그 ‘starter’라는 꾸러미 안에 무슨 비밀이라도 숨어있는 거야?”
2장: ‘자동 설정’, 마법이 아니라 원리가 있다!
솔라의 질문이 채 공기 중에서 흩어지기도 전에, 루나의 손가락이 움직였다. 이전 장에서 비교를 위해 열어두었던 modern-boot-project의 pom.xml 파일을 다시 화면에 띄웠다. 솔라가 ‘비밀 꾸러미’ 같다고 말했던 spring-boot-starter-web 의존성 항목 옆에는 작은 화살표가 있었다. 루나는 말없이 그 화살표를 클릭했다.
화면이 순식간에 수십 줄의 새로운 목록으로 채워졌다. spring-boot-starter-web이라는 한 줄 아래로, spring-web, spring-webmvc, tomcat-embed-core 등 낯익고도 낯선 이름들이 끝없이 펼쳐졌다. 마치 잘 포장된 선물 상자를 열었더니 그 안에 또 다른 작은 상자들이 가득 들어있는 모습이었다. 솔라는 자기도 모르게 “헉” 하고 짧은 숨을 내뱉었다.
“이게… 뭐야? 난 분명 starter-web 딱 하나만 추가했는데, 얘네들은 다 어디서 온 거야?”
솔라는 마술의 비밀을 목격한 아이처럼 눈을 동그랗게 떴다. 그녀의 머릿속에서 ‘자동 설정’은 여전히 모든 것을 알아서 처리해주는 편리하지만 모호한, 마법 같은 개념이었다.
“바로 그게 ‘starter’의 첫 번째 비밀이야.”
루나가 차분하게 입을 열었다.
“starter는 그 자체로 어떤 기능을 가진 코드가 아니야. 오히려 잘 짜인 ‘쇼핑 리스트’에 가까워. 우리가 ‘웹 애플리케이션을 만들고 싶다’고 말하면, spring-boot-starter-web은 ‘아, 그럼 웹 서버로 톰캣이 필요하고, 스프링의 웹 MVC 기능도 필요하고, JSON 처리 라이브러리도 있어야겠네요. 제가 알아서 장바구니에 담아드릴게요’ 하고 관련 라이브러리들을 한꺼번에 가져오는 역할을 하는 거지. 버전 충돌 걱정 없이, 가장 안정적인 조합으로 말이야.”
“아, 그러니까 starter는 그냥 필요한 부품들을 모아놓은 꾸러미였구나. 진짜 마법은 아니었네.”
솔라는 고개를 끄덕였지만, 여전히 무언가 풀리지 않은 표정이었다.
“그런데 부품이 전부 주방에 도착했다고 해서 요리가 저절로 되진 않잖아. 이 부품들을 가져다가 웹 서버를 띄우고, 요청을 받도록 조립하는 건 대체 누가 하는 건데?”
솔라의 질문은 정확히 핵심을 꿰뚫고 있었다. 라이브러리를 가져오는 것은 준비 단계일 뿐, 진짜 ‘자동 설정’은 그 다음부터였다.
루나는 pom.xml 창을 닫고, 프로젝트의 유일한 자바 파일이었던 ModernBootApplication.java를 열었다. 파일의 내용은 단출했다. 그리고 그 맨 위에, 마치 이 애플리케이션의 시작을 알리는 관문처럼 @SpringBootApplication이라는 어노테이션이 붙어 있었다.
“두 번째 비밀의 열쇠는 바로 여기 있어.”
루나가 그 어노테이션을 가리켰다.
“이 어노테이션 안에는 여러 기능이 숨어 있는데, 그중 가장 중요한 것이 @EnableAutoConfiguration이야. ‘자동 설정을 활성화하라’는 강력한 스위치지.”
루나는 솔라가 이해할 수 있도록 그림을 그리기 시작했다. 간단한 순서도였다.
-
Starter가 부품을 모은다 (
pom.xml)spring-boot-starter-web이tomcat-embed-core,spring-webmvc같은 라이브러리들을 클래스패스(Classpath)라는 작업 공간에 가져다 놓는다.
-
@EnableAutoConfiguration이 작업 공간을 훑어본다- 애플리케이션이 실행되는 순간, 이 스위치가 켜지면서 클래스패스에 어떤 부품들이 있는지 스캔하기 시작한다.
-
‘조건’에 맞는 설정을 자동으로 조립한다
- (조건) “어? 클래스패스에
tomcat-embed-core가 있네?” → (실행) “그럼 내장 톰캣 웹 서버를 띄워야겠다!” - (조건) “클래스패스에
spring-webmvc가 있네?” → (실행) “그럼 웹 요청을 받을DispatcherServlet을 등록해야겠다!” - (조건) “개발자가 직접
DataSource(데이터베이스 연결) 설정을 안 했고, 클래스패스에H2Database가 있네?” → (실행) “그럼 테스트용 인메모리 H2 데이터베이스를 자동으로 연결해줘야겠다!”
- (조건) “어? 클래스패스에
“마법이 아니라, 아주 체계적인 ‘조건부 자동화’였던 거야. ‘만약 A라는 라이브러리가 있으면, B라는 설정을 적용한다. 단, 사용자가 직접 B 설정을 이미 했다면, 나는 조용히 물러난다.’는 원칙을 따르는 거지.”
솔라의 눈이 반짝였다. 흩어져 있던 퍼즐 조각들이 제자리를 찾아가는 느낌이었다. ‘관례’라는 단어의 의미가 훨씬 더 구체적으로 다가왔다. 개발자들이 톰캣과 DispatcherServlet을 설정하는 것은 너무나 당연한 ‘관례’였기에, 스프링 부트는 그저 그 관례를 코드로 구현해 놓은 것에 불과했다.
“와… 그러니까 ‘Spring Boot가 Spring Framework의 설정을 자동화한다’는 말이, 그냥 편리하다는 뜻이 아니었네. starter로 약속된 라이브러리들을 가져오고, @EnableAutoConfiguration이 그걸 감지해서 약속된 설정들을 대신 해주는 구체적인 ‘원리’가 있었던 거구나.”
솔라는 이제 ‘자동 설정’이 마법이 아니라, 잘 설계된 규칙의 집합이라는 사실을 받아들였다. 하지만 이 명쾌한 원리를 이해하자, 이전에는 보이지 않던 더 근본적인 질문이 고개를 들었다.
“가만있어 봐. 방금 언니가 그린 그림을 보면, Spring Boot가 자동화해주는 대상이 결국 DispatcherServlet이나 DataSource 같은 기존 Spring Framework의 핵심 부품들이잖아. 자기만의 새로운 부품을 만드는 게 아니라. 그렇다면… Spring Boot는 대체 Spring Framework랑 무슨 관계인 거지? 독립된 새로운 프레임워크가 아니라, 그냥 Spring Framework를 도와주는 아주 똑똑한 보조 도구 같은 건가?”
3장: Spring Boot는 Spring 위에 얹어진 ‘편리한 층’
솔라의 책상 위에는 하얀 A4 용지 한 장이 놓여 있었다. 그 위에는 서툰 솜씨로 그린 네모 상자 두 개가 있었다. 왼쪽 상자 안에는 ‘Spring Framework (부품들: DispatcherServlet, DataSource…)’라고 적혀 있었고, 오른쪽 상자에는 ‘Spring Boot (자동 조립공)’이라고 쓰여 있었다. 두 상자는 서로 연결되지 않은 채, 그저 나란히 놓여 있을 뿐이었다. 그 사이의 넓은 여백에 솔라는 커다란 물음표를 그려 넣었다. 이 그림은 바로 지금, 그녀의 머릿속 그 자체였다.
‘자동 설정’의 원리를 파악하자 새로운 혼란이 찾아왔다. Spring Boot가 기존 Spring Framework의 부품들을 가져다 ‘자동으로 조립’해준다는 것은 이제 알겠다. 하지만 그 둘의 관계가 여전히 모호했다. 마치 숙련된 조수가 주인 기술자의 도구를 멋대로 가져다 쓰는 것처럼, 혹은 완전히 다른 공장에서 똑같은 부품으로 제품을 만드는 것처럼 보였다. 조수인가, 경쟁자인가?
“언니, 내 생각이 맞다면… Spring Boot로 만든 프로젝트에서도, 예전 Spring Framework 방식처럼 설정 파일을 직접 만들어서 제어할 수 있는 거야?”
솔라가 용지 위의 물음표를 툭툭 치며 물었다. 그녀의 목소리에는 ‘만약 그렇다면, 둘은 대체 무슨 관계지?’라는 풀리지 않은 의문이 담겨 있었다. Spring Boot가 모든 것을 알아서 해주는 완벽한 새 세상이라면, 굳이 구시대의 유물 같은 수동 설정이 끼어들 여지가 있을까? 만약 끼어들 수 있다면, 그건 Spring Boot가 불완전하다는 뜻일까?
루나는 솔라의 책상으로 다가와, 그녀가 그린 그림을 잠시 들여다보았다. 두 개의 분리된 상자, 그리고 그 사이의 물음표. 루나는 말없이 솔라의 modern-boot-project를 다시 화면에 띄웠다.
“직접 확인해보자. 우리가 이 ‘자동 조립된 완제품’에 우리만의 부품을 한번 끼워 넣어보는 거야. Spring Boot가 어떻게 반응하는지.”
루나는 솔라가 지난 장에서 봤던 ‘조건부 자동화’ 순서도를 다시 떠올리게 했다.
“Spring Boot는 클래스패스에 데이터베이스 관련 라이브러리가 있고, 우리가 별다른 설정을 하지 않으면 알아서 테스트용 인메모리 데이터베이스(H2)를 연결해준다고 했지? 그 ‘자동 설정’에 한번 쐐기를 박아보는 거야. ‘아니, 데이터베이스 연결은 내가 직접 할게!’ 하고 선언하는 거지.”
루나의 제안에 따라 솔라는 프로젝트에 AppConfig.java라는 새 파일을 만들었다. 그리고 그 안에, 전통적인 Spring Framework에서나 보던 @Configuration 어노테이션과 함께, 데이터베이스 연결 정보를 담는 DataSource를 직접 생성하고 설정하는 코드를 몇 줄 추가했다. 일부러 눈에 띄게, 연결 URL에 my-manual-db라는 가짜 이름을 붙였다.
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.h2.Driver");
// 자동 설정이 아닌, 우리가 직접 지정한 DB임을 명시
dataSource.setUrl("jdbc:h2:mem:my-manual-db");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
}
이제 프로젝트 안에는 두 개의 의지가 공존하게 되었다. 하나는 Spring Boot의 보이지 않는 자동 설정(@EnableAutoConfiguration), 다른 하나는 솔라가 방금 작성한 눈에 보이는 수동 설정(@Bean DataSource). 솔라는 긴장된 표정으로 애플리케이션을 다시 실행했다. 과연 누가 이길까.
결과는 로그에 명확히 찍혔다. 애플리케이션이 구동되면서 H2 데이터베이스 콘솔에 접속할 수 있는 URL이 나타났는데, 그 주소 끝에 솔라가 지정한 my-manual-db라는 이름이 선명하게 보였다. Spring Boot의 자동 설정은 온데간데없이 사라지고, 솔라가 만든 설정이 그 자리를 차지한 것이다.
“와…!”
솔라는 짧은 탄성을 내뱉었다.
“Spring Boot가… 그냥 물러났어. 내가 직접 하겠다고 하니까, ‘아, 그러세요? 그럼 전 신경 끌게요’ 하고 조용히 비켜준 거네.”
그 순간, 솔라는 자신이 그렸던 두 개의 분리된 상자가 얼마나 잘못된 그림이었는지 깨달았다. 둘은 경쟁 관계가 아니었다. 애초에 같은 땅 위에 지어진 1층과 2층 건물 같은 관계였다.
“알겠다! Spring Boot는 완전히 새로운 프레임워크가 아니었어. 기존 Spring Framework라는 튼튼한 기반 위에 지어진, 아주 편리한 ‘상위 추상화 계층’이었던 거야!”
솔라는 책상 위의 A4 용지를 집어 들었다. 그녀는 물음표와 두 개의 분리된 상자를 가차없이 지웠다. 그리고 다시 그리기 시작했다. 이번에는 커다란 상자 하나를 먼저 그렸다. 그 안에는 ‘Spring Framework (기반, 규칙, 핵심 부품)’이라고 적었다. 그리고 그 상자 바로 위에, 더 얇고 작은 상자를 겹쳐 그렸다.
[ Spring Boot: 관례 기반 자동 설정, 스타터 (편리한 층) ] [ Spring Framework: IoC/DI, AOP, 트랜잭션 등 (기반 층) ]
새로운 그림은 완벽하게 이해됐다. Spring Boot는 Spring Framework를 대체하는 것이 아니라, 그 위에 살포시 얹혀서 개발자가 가장 자주 사용하는 기능들을 ‘알아서’ 해주는 친절한 층이었다. 하지만 언제든 개발자가 “그 부분은 내가 할게”라고 말하면, 그 부분만큼은 투명하게 비켜서 아래층의 Spring Framework가 직접 드러나도록 자리를 내주는 구조였다.
솔라는 처음 이 주제에 가졌던 질문을 떠올렸다. ‘새 프로젝트를 시작할 때 Spring Boot를 써야 할까, 말아야 할까?’ 이제 그 질문 자체가 얼마나 어색한지 알 수 있었다.
그녀는 노트북을 열어 새로운 메모장을 켰다. 제목은 ‘Spring Boot 적용 의사결정 가이드’라고 붙였다. 그리고 첫 번째 원칙을 망설임 없이 써 내려갔다.
- 판단 기준 1: Spring Boot를 쓸까 말까 고민하지 않는다.
- 질문은 ‘어디까지 Spring Boot의 자동 설정을 믿고, 어디부터 내가 직접 Spring Framework의 제어권을 가져올 것인가?’가 되어야 한다.
- 예시: 일반적인 웹 API 프로젝트라면, 일단 Boot로 시작한다. 웹 서버, 로깅, JSON 변환 등 90%는 Boot에 맡긴다. 만약 아주 특이한 외부 시스템 연동이 필요하다면? 그 부분만 수동 설정(
@Configuration)으로 제어권을 가져오면 된다. 편리함과 제어권, 둘 다 놓칠 필요가 없다.
솔라는 만족스러운 표정으로 자신이 쓴 글을 바라보았다. ‘Spring Boot가 Spring Framework의 설정을 자동화한다’는 단순한 문장이, 이제는 ‘기반 위에서 동작하는 상위 계층의 양보하는 자동화’라는 훨씬 더 깊고 구체적인 그림으로 보였다. 마법은 풀렸고, 그 자리에는 명쾌한 설계 원리가 남았다.