설정 파일이 세 개였던 시절
Spring MVC로 웹 애플리케이션을 만들려면 최소 세 개의 설정 파일이 필요했다. web.xml, root-context.xml, servlet-context.xml. 각각 서블릿 등록, 빈 스캔, ViewResolver 설정을 담당했다. 프로젝트를 시작할 때마다 이 세 파일을 손으로 작성해야 했고, 빠뜨린 설정 하나가 런타임 오류로 이어졌다.
Spring Boot는 이 반복을 없앴다. @SpringBootApplication 어노테이션 하나와 main() 메서드 하나로 서버가 뜬다. 설정 파일은 application.properties 하나만 남는데, 그마저도 ViewResolver 경로만 직접 쓰면 된다.
Insight: Spring Boot의 핵심은 "합리적인 기본값(Convention over Configuration)"이다. 모든 프로젝트가 공통으로 필요한 설정은 자동으로 처리하고, 다른 것만 명시하게 한다.
Bad Code: 반복되던 설정들
Spring MVC 시절 web.xml에는 이런 내용이 반드시 들어갔다.
<!-- web.xml -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- 인코딩 필터도 직접 등록 -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
</filter>
servlet-context.xml에는 ViewResolver, 컴포넌트 스캔, 리소스 핸들러를 또 작성했다. 프로젝트마다 똑같은 내용이 반복됐다.
<!-- servlet-context.xml -->
<context:component-scan base-package="com.example.mvc" />
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".jsp" />
</bean>
<mvc:resources mapping="/resources/**" location="/resources/" />
Insight: 모든 Spring MVC 프로젝트가 이 설정을 반복했다. 다른 건 패키지명 정도뿐이었다. 이 반복이 Spring Boot 탄생의 이유다.
Spring Boot: 설정 파일이 하나로
Spring Boot 프로젝트의 설정 전부다.
# application.properties
spring.application.name=MyApp
# JSP ViewResolver (이것만 직접 설정)
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
DispatcherServlet 등록, 인코딩 필터, 컴포넌트 스캔, 리소스 핸들러는 Spring Boot가 자동으로 처리한다. ViewResolver prefix/suffix만 프로젝트마다 다르니까 이것만 명시한다.
진입점도 단순하다.
@SpringBootApplication
public class AppMain {
public static void main(String[] args) {
SpringApplication.run(AppMain.class, args);
}
}
@SpringBootApplication은 세 어노테이션의 합성이다.
| 포함된 어노테이션 | 역할 |
@SpringBootConfiguration |
이 클래스가 설정 클래스임을 선언 (@Configuration과 동일) |
@EnableAutoConfiguration |
classpath의 의존성을 보고 필요한 빈 자동 등록 |
@ComponentScan |
이 클래스 위치 기준 하위 패키지 전체 스캔 |
Insight: @EnableAutoConfiguration이 핵심이다. spring-boot-starter-web 의존성이 있으면 웹 관련 빈을, Jackson이 있으면 JSON 변환 빈을 자동으로 올린다. 의존성이 곧 설정이다.
160개의 빈 — 자동 등록의 실체
Spring Boot 앱을 실행하고 등록된 빈 목록을 출력해봤다.
@SpringBootApplication
public class AppMain {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(AppMain.class, args);
for (String bean : context.getBeanDefinitionNames())
System.out.println(bean);
}
}
결과는 160여 개. 그 중 개발자가 직접 만든 건 두 개뿐이었다.
helloController // 내가 만든 것
userController // 내가 만든 것
// 나머지 158개는 Spring Boot 자동 등록
자동 등록된 빈들이 무엇을 처리하는지 분류하면 이렇다.
| 빈 이름 | 역할 |
dispatcherServlet |
요청 라우팅 (web.xml에 직접 등록하던 것) |
tomcatServletWebServerFactory |
내장 Tomcat 서버 |
viewResolver, defaultViewResolver |
뷰 이름 → JSP 경로 변환 |
jacksonObjectMapper |
Java 객체 ↔ JSON 변환 |
multipartResolver |
파일 업로드 처리 |
characterEncodingFilter |
UTF-8 인코딩 필터 |
requestMappingHandlerMapping |
@GetMapping, @PostMapping URL 매핑 |
basicErrorController |
에러 페이지 처리 |
liveReloadServer |
devtools 핫리로드 |
Spring MVC 시절에는 이것들을 하나씩 XML에 등록하거나 JavaConfig로 구현했다. Spring Boot 이전 개발자들이 Spring Boot를 처음 봤을 때 충격받은 이유가 여기 있다. main() 하나로 서버가 뜨는 게 말이 안 됐던 것이다.
Insight: 자동 등록된 빈들은 숨겨진 게 아니다. 언제든getBeanDefinitionNames()로 확인할 수 있고,application.properties로 오버라이드할 수 있다. Spring Boot는 설정을 없앤 게 아니라 기본값을 제공한 것이다.
JSP를 쓰려면 의존성이 더 필요한 이유
Spring Boot 내장 Tomcat은 기본적으로 JSP를 지원하지 않는다. REST API가 주류가 되면서 JSP 렌더링 기능을 기본에서 제외했기 때문이다. JSP를 사용하려면 pom.xml에 세 가지를 추가해야 한다.
<!-- JSP 컴파일러 (Jasper) -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<!-- JSTL 인터페이스 -->
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
</dependency>
<!-- JSTL 구현체 (GlassFish) -->
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
</dependency>
| 의존성 | 역할 |
tomcat-embed-jasper |
.jsp 파일을 서블릿으로 컴파일하는 Jasper 엔진 |
jstl-api |
JSTL 태그 인터페이스 정의 (<c:if>, <c:forEach> 등) |
jstl (GlassFish) |
JSTL 실제 구현체 |
Thymeleaf를 사용하면 이 세 가지가 필요 없다. spring-boot-starter-thymeleaf 하나로 끝난다. 현업에서 신규 프로젝트에 JSP보다 Thymeleaf나 React/Vue를 선호하는 이유 중 하나다.
Insight: JSTL은 API와 구현체가 분리되어 있다. 인터페이스를 정의한 스펙과 그것을 실제로 동작하게 만든 구현체가 별개 라이브러리다. Jakarta EE 생태계의 전반적인 설계 방식이기도 하다.
세션과 @ModelAttribute — 로그인 흐름 구현
Spring Boot 컨트롤러 코드는 Spring MVC와 완전히 동일하다. 바뀐 건 설정뿐이다. 로그인 처리 코드를 통해 두 가지 개념을 짚는다.
@Controller
public class AccountController {
@GetMapping("/login")
public String loginForm() {
return "/account/loginForm";
}
@PostMapping("/login")
public String login(@ModelAttribute Member member, HttpSession session) {
session.setAttribute("loginMember", member.getId());
return "redirect:/home";
}
@GetMapping("/logout")
public String logout(HttpSession session) {
session.invalidate(); // 세션 전체 초기화
return "redirect:/";
}
}
@ModelAttribute Member member — 빈 컨테이너에서 꺼내는 게 아니다. POST 요청의 폼 데이터를 객체에 바인딩한다. id=hong&pw=1234가 넘어오면 Spring이 new Member()를 만들고 setId("hong"), setPw("1234")를 호출한다. 파라미터 이름과 setter 이름이 일치하면 된다.
HttpSession session — 직접 생성하지 않아도 된다. Spring이 현재 요청의 세션을 파라미터에 자동 주입한다. 세션은 Map<String, Object>처럼 동작한다.
session.setAttribute("loginMember", "hong"); // Map.put
session.getAttribute("loginMember"); // Map.get → "hong"
session.invalidate(); // Map 전체 비우기
"loginMember"는 Java 어딘가 정의된 필드가 아니라 개발자가 정한 key 문자열이다. JSP에서 ${loginMember}로 꺼낼 수 있다.
return "redirect:/home" — 뷰 이름이 아니라 리다이렉트다. ViewResolver를 거치지 않고 브라우저에게 해당 URL로 다시 GET 요청하라는 응답을 보낸다. 로그인 POST 처리 후 새로고침하면 폼이 중복 제출되는 문제를 막기 위해 사용한다 (PRG 패턴).
Insight: HTTP는 요청/응답이 끝나면 상태를 기억하지 않는다. 로그인 상태 유지는 서버가 세션 Map을 관리하고, 브라우저는 JSESSIONID 쿠키만 들고 다니는 방식으로 구현된다.
Q. Spring Boot에서 Tomcat을 별도로 설치해야 하나?
필요 없다. spring-boot-starter-web이 내장 Tomcat을 포함한다. ./mvnw spring-boot:run 하면 Tomcat이 자동으로 뜬다. 배포 시에도 mvnw package로 만든 JAR 파일에 Tomcat이 내장되어 있어서 java -jar app.jar로 실행된다.
Q. application.properties 말고 YAML 파일로 설정할 수도 있나?
가능하다. application.yml로 작성해도 된다. 계층 구조가 있는 설정은 YAML이 가독성이 좋아서 현업에서도 많이 쓴다. spring.mvc.view.prefix는 YAML에서 spring: mvc: view: prefix: 형태가 된다. 둘 다 동시에 있으면 properties가 우선한다.
Q. 자동 등록된 빈의 기본 설정을 바꾸고 싶으면?
두 가지 방법이 있다. application.properties에 해당 속성을 명시하면 자동 설정이 그 값으로 오버라이드된다. 더 세밀한 제어가 필요하면 @Configuration 클래스에서 해당 빈을 직접 정의하면 자동 등록 대신 그 빈이 사용된다.
Spring Boot는 설정을 없앤 게 아니라 반복되는 기본값을 대신 써준다. 160개의 자동 빈은 숨겨진 마법이 아니라 언제든 확인하고 오버라이드할 수 있는 기본 설정이다. 컨트롤러 코드는 Spring MVC와 동일하고, 달라진 건 설정 파일 세 개가 application.properties 다섯 줄로 줄었다는 것뿐이다. Spring Boot를 쓴다는 건 선택지가 줄어드는 게 아니라 시작점이 달라지는 것이다.
Environment: Windows 11, JDK 17, Spring Boot 3.4.4, VS Code