⬜ 섹션2 스프링 부트 소개
◼️ 스프링 프레임워크 등장 배경
스프링 프레임워크는 2000년대 초반에 자바(Java) 개발자들이 경험한 문제들을 해결하기 위해 등장했다.
1. 복잡한 엔터프라이즈 자바 개발 (EJB 문제) 🥵
2000년대 초반, 자바로 대규모 기업 애플리케이션을 개발할 때 주로 EJB(Enterprise JavaBeans)기술을 사용했다. 하지만, EJB는 너무 복잡하고 사용하기 어려운 부분이 많았다.개발자들은 많은 설정과 복잡한 코드를 작성해야 했기 때문에 시간이 많이 걸리고 오류 발생 가능성이 높았습니다.
2. 더 쉬운 방법에 대한 요구 🤨
개발자들은 복잡한 EJB 대신, 더 간단하고 효율적인 방법으로 애플리케이션을 개발하고자 했다. 특히, 자바의 장점을 살리면서도 코드가 깔끔하고 유지보수가 쉬운 프레임워크를 원했다.
3. 스프링의 등장 😀
이때 로드 존슨(Rod Johnson)이라는 개발자가 "Expert One-on-One J2EE Design and Development"라는 책을 출판하면서 스프링 프레임워크의 초기 버전이 세상에 등장했다. 스프링은 EJB처럼 복잡한 설정 없이도 엔터프라이즈 애플리케이션을 개발할 수 있게 해주는 경량 프레임워크였다.
🟢 스프링 프레임워크 주요 특징
- 제어의 역전(IoC): 객체의 생성과 생명 주기를 개발자가 아니라, 스프링이 관리해주어 코드가 간결해진다.
- 의존성 주입(DI): 객체 간의 의존성을 스프링이 주입해주어 객체 간 결합도를 낮추고, 테스트와 유지보수가 용이해진다.
- 모듈화: 다양한 기능(예: 데이터베이스 연동, 웹 애플리케이션 개발 등)을 위한 모듈이 제공되어 필요한 기능만 선택가능하다.
스프링 프레임워크는 이처럼 자바 기반의 대규모 애플리케이션 개발을 더 쉽고 효율적으로 만들기 위해 등장했다.
◼️ 스프링 부트 등장 배경

1. 스프링의 복잡성 문제
스프링 프레임워크는 강력하고 유연했지만, 설정이 매우 복잡하고 번거로웠다. 프로젝트를 시작할 때 많은 XML 설정 파일을 작성하거나 복잡한 자바 설정 코드를 작성해야 했다. 특히, 스프링을 처음 접하는 개발자에게는 이러한 초기 설정이 부담이 될 수 있었다.
2. 스프링 부트의 등장
이러한 배경에서 2014년, 스프링 팀은 스프링 부트(Spring Boot)를 발표했다. 스프링 부트는 스프링 프레임워크 설정을 간소화하고 개발 속도를 높이기 위해 만들어졌다.
🟢 스프링 부트의 주요 특징
- 자동 설정 (Auto Configuration): 스프링 부트는 개발자가 직접 설정할 필요 없이, 기본적으로 많이 사용되는 설정들을 자동으로 적용해준다. 이를 통해 복잡한 초기 설정 작업을 크게 줄일 수 있다.
- 내장 서버 (Embedded Server): 스프링 부트는 애플리케이션을 실행할 때 내장된 Tomcat, Jetty 같은 웹 서버를 함께 실행시켜준다. 따라서 별도의 서버 설정 없이도 애플리케이션을 쉽게 배포하고 실행할 수 있다.
- 스타터 패키지 (Starter POMs): 스프링 부트는 다양한 기능들을 쉽게 사용할 수 있도록 미리 정의된 의존성 패키지(Starter)를 제공한다. 예를 들어, spring-boot-starter-web을 사용하면 웹 애플리케이션 개발에 필요한 모든 라이브러리가 포함된 상태로 프로젝트를 시작할 수 있다.
- 프로덕션 준비 (Production-Ready Features): 애플리케이션의 모니터링, 관리, 설정 등을 쉽게 할 수 있도록 다양한 프로덕션 환경에서 바로 사용할 수 있는 기능들을 제공한다.
이렇게 스프링 부트는 개발자들이 스프링을 더욱 쉽고 빠르게 애플리케이션을 개발할 수 있도록 도와주는 역할을 한다.
정리하면, 본질 기술은 스프링 프레임워크이며 스프링 부트는 스프링 프레임워크를 쉽게 사용할 수 있게 도와주는 도구이다.
⬜ 섹션3 웹 서버와 서블릿 컨테이너
◼️ 웹 서버와 스프링부트
최근엔 스프링 부트가 내장 톰캣(WAS)을 포함하고있어, 개발자는 JAR로 빌드한다음 원하는 위치에서 실행하기만 하면 된다.
하지만 과거엔 아래와 같은 여러 과정을 통해 앱코드를 띄웠다.
🟢 과거 애플리케이션 배포 과정
1. 서버에 WAS(웹 애플리케이션 서버)를 설치
2. WAS에서 동작하도록 서블릿 스펙에 맞추어 코드 작성
3. WAR 형식으로 빌드해서 war 파일 생성
4. war파일을 WAR에 전달해 배포
음..최근에 비해 다소 복잡해보인다. 과거 애플리케이션 배포 과정으로 거슬러 올라가 차근히 발전 과정을 밟아보자!
🟢 JAR, WAR 비교
JAR 파일은 JVM 위에서 실행되고, WAR는 웹 애플리케이션 서버 위에서 실행된다. WAR는 정해진 구조를 지켜야 한다.
JAR (Java Archive) | WAR (Web Application Archive) |
정의: - 여러 자바 클래스와 리소스를 하나로 묶은 압축 파일 사용 용도: - 직접 실행 가능한 애플리케이션 - 라이브러리로 다른 프로젝트에서 사용 구성요소: - mian() 메서드 포함한 클래스 - MANIFEST.MF 파일에 메인 클래스 지정 |
정의: - 웹 애플리케이션을 배포하기 위한 압축 파일 사용 용도: 웹 애플리케이션 서버(WAS)에 배포하여 실행 구성 요소: - 정적 리소스 (예: index.html) - WEB-INF 디렉토리 (classes,lib,web.xml) |
◼️ 톰캣 설치
먼저 톰캣을 다운로드 한다. 그리고 터미널 /bin폴더에서 './startup.bat' 명령을 실행하면 :8080 포트에서 아래와 같은 화면이뜬다.
🟢 톰캣 실행 및 설정
- 실행: ./startup.sh
- 종료: ./shutdown.sh
- 권한부여: chmod 755 *
🟢 서블릿
서블릿은 웹 서버에서 실행되며, 클라이언트(보통 웹 브라우저)로부터 HTTP 요청을 받아 처리한 후, 그 결과를 다시 클라이언트에게 응답으로 보내는 역할을 한다.
가벼운 예시를 보자. 헬로서블릿이 있다고할때, 동작과정은 다음과같다.
- 웹 애플리케이션 서버는 HTTP 요청 메시지를 기반으로 request, response 객체를 만들고 helloServlet을 실행한다.
- helloServlet에서는 response 객체를 변경한다.
- 웹 애플리케이션 서버는 변경된 response 객체 정보로 HTTP 응답을 생성하여 웹 브라우저에 전송한다.
◼️프로젝트 WAR 빌드와 배포
간단한 HTML과 순수 서블릿으로 동작하는 웹 애플리케이션을 만들어 코드를 배포해볼것이다.
<!DOCTYPE html>
<html lang="en">
<body>index html</body>
</html>
/**
* http://localhost:8080/test
* */
@WebServlet(urlPatterns = "/test")
public class TestServlet extends HttpServlet {
@Override //서블릿이 호출되면 service메소드가 실행됨
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("TestServlet.service");
resp.getWriter().println("test");
}
}
이제 터미널로 프로젝트 폴더 이동후, 빌드하고 WAR압축을 풀어 내용물을 확인해보자.
- 프로젝트 빌드
#코드 복사
./gradlew build
- WAR 파일 생성 확인: build/libs/server-0.0.1-SNAPSHOT.war
- WAR 파일 압축 해제: build/libs 폴더로 이동 후, 아래 명령어로 압축을 해제하여 내용물을 확인할 수 있다.
jar -xvf server-0.0.1-SNAPSHOT.war
이제 생성된 WAR를 톰캣 서버에 실제 배포해보자. 톰켓 폴더 /webapps를 모두 삭제후, 빌드 war 파일을 복사해넣고 ROOT.war로 파일명을 변경한다. 그리고 톰켓 서버를 재실행하면 아래와 같이 잘 실행됨을 확인할 수 있다.
◼️서블릿 컨테이너 초기화1

서블릿은 아래 초기화 인터페이스로 서블릿 컨테이너를 초기화한다. 서블릿 컨테이너는 실행시점에 초기화 메서드 onStartup()을 호출한다. 따라서 이 메소드 안에서 필요기능들을 초기화 및 등록한다.
🔶 ServletContainerInitializer; 서블릿 초기화 기능 제공
public interface ServletContainerInitializer {
//c;조금더 유연한 초기화기능으로,@HandlesTypes와 함께 사용
//ctx;서블릿 컨테이너 자체의 기능 제공.이를 통해 필터나 서블릿 등록 가능
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}
위를 간단히 구현해 보자. 먼저 초기화를 구현한다.
public class MyContainerInitV1 implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
System.out.println("MyContainerInitV1.onStartup");
System.out.println("MyContainerInitV1 set = " + c);
System.out.println("MyContainerInitV1 ctx = " + ctx);
}
}
그리고 WAS에게 실행할 초기화 클래스를 알려주어야한다. 아래 경로에 파일을 생성해 클래스를 지정해준다.
- 경로: resources/META-INF/services/jakarta.servlet.ServletContainerInitializer
hello.container.MyContainerInitV1
이제 실행해보면, 아래 로그가 찍힘을 확인할 수 있다.
지금까지의 구조는 대략 아래 그림과같다. 초기화클래스를 만들고 싶다면 ServletContainerInitializer를 구현하고 초기화 클래스를 파일에 등록해놓으면, WAS 가 이를 확인후 컨테이너 초기화를 진행해준다.
◼️ 서블릿 컨테이너 초기화2
🟢 서블릿 등록 방법 두가지
- @WebServlet 애노테이션
- 프로그래밍 방식
애노테이션 방식은 간편함을 제공하지만, 유연성이 떨어진다.
프로그래밍 방식은 복잡하지만, 초기화 과정과 설정을 세밀하게 제어할 수 있어, 더 많은 요구사항에 대응할 수 있다.
여기서 말하는 유연성이란, 아래와 같은 것들을 말한다.
🌈프로그래밍 방식의 유연성
- /hello-servlet경로를 상황에 따라서 바꾸어 외부 설정을 읽어서 등록할 수 있다.
- 서블릿 자체도 특정 조건에 따라서 `if문으로 분기해서 등록하거나 뺄 수 있다.
- 서블릿을 내가 직접 생성하기 때문에 생성자에 필요한 정보를 넘길 수 있다.
🔶 @WebServlet 애노테이션 방식
@WebServlet(urlPatterns = "/test")
public class TestServlet extends HttpServlet {}
🔶 프로그래밍 방식
ServletRegistration.Dynamic helloServlet =
servletContext.addServlet("helloServlet", new HelloServlet());
helloServlet.addMapping("/hello-servlet");
🟢 애플리케이션 초기화
서블릿 컨테이너는 유연한 초기화 기능을 지원하는데, 이를 애플리케이션 초기화라고 한다.
🟢 애플리케이션 초기화 과정
- 초기화 인터페이스 생성
- @HandlesTypes 애노테이션을 사용해 초기화 인터페이스를 지정 (AppInit.class).
- 서블릿 컨테이너 초기화 시, ServletContainerInitializer는 지정된 인터페이스의 구현체를 찾는다. 리플렉션을 사용해 객체를 생성하고, 초기화 코드를 실행한다.
위 과정을 실습해보자.
먼저, 애플리케이션 초기화를 진행하기 위해선 초기화 인터페이스를 만들어야한다. (내용과 형식은 상관X)
import jakarta.servlet.ServletContext;
public interface AppInit {
void onStartup(ServletContext servletContext);
}
다음으로, 위 인터페이스를 구현해 실제 동작 코드를 만든다. 아래는 /hello-servlet 호출시 실행된다.
/**
* http://localhost:8080/hello-servlet
*/
public class AppInitV1Servlet implements AppInit{
@Override
public void onStartup(ServletContext servletContext) {
System.out.println("AppInitV1Servlet.onStartup");
//순수 서블릿 코드 등록
ServletRegistration.Dynamic helloServlet = servletContext.addServlet("helloServlet", new HelloServlet());
helloServlet.addMapping("/hello-servlet");
}
}
프로그래밍 방식의 서블릿 및 애플리케이션 초기화가 어떻게 이루어지는지를 이해하기위해 아래 코드를 살펴보자.
MyContainerInitV2는 ServletContainerInitializer를 구현하여, 서블릿 컨테이너가 실행될 때 AppInit 인터페이스의 구현체들을 찾아서 초기화한다.
@HandlesTypes(AppInit.class)
public class MyContainerInitV2 implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
System.out.println("MyContainerInitV2.onStartup");
// 탐지된 모든 AppInit 구현체에 대해 초기화 작업 수행
for (Class<?> appInitClass : c) {
try {
// 리플렉션을 사용해 AppInit 구현체의 인스턴스를 생성
// (객체 인스턴스가 아니라 클래스 정보를 전달하기 때문에 실행하려면 객체 생성해서 사용해야 함)
AppInit appInit = (AppInit) appInitClass.getDeclaredConstructor().newInstance();
// 생성된 인스턴스를 통해 초기화 작업 수행
appInit.onStartup(ctx);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
위 MyContainerIntV2또한 서블릿 컨테이너에 알려주어야한다. 설정을 추가한다.
//경로;resources/META-INF/services/jakarta.servlet.ServletContainerInitializer
hello.container.MyContainerInitV1
hello.container.MyContainerInitV2
⚪️ 코드작동 과정
- 서블릿 컨테이너가 시작되면, MyContainerInitV2의 onStartup() 메서드가 호출된다.
- @HandlesTypes(AppInit.class)로 지정된 AppInit 인터페이스의 모든 구현체들이 자동 탐지되어 Set<Class<?>>에 전달된다.
- MyContainerInitV2는 리플렉션을 사용해 탐지된 클래스의 인스턴스를 생성하고, 해당 인스턴스를 통해 초기화 작업을 수행한다이 과정에서 ServletContext를 사용하여 서블릿을 등록하거나 기타 필요한 초기화를 진행할 수 있다.
위 과정을 요약 정리하면, MyContainerInitV2.onStartup()는 서블릿 컨테이너가 시작될 때 호출되며, AppInit 구현체들을 탐지하여 각각의 초기화 작업을 호출한다. 그리고 appInit.onStartup(ctx)에서 각 AppInit 구현체에서 정의된 초기화 작업을 수행한다.
초기화 순서는 다음과같다.
먼저 서블릿 컨테이너는 초기화되며, ServletContainerInitializer 구현체의 onStartup() 메서드를 호출한다.
두 번째 단계에서 @HandlesTypes 애노테이션을 통해 탐지된 AppInit 구현체들의 초기화 작업이 수행된다.
그런데 왜 애플리케이션 초기화가 필요할까?
애플리케이션 초기화는 설정과 관리의 편리함을 제공하고, 서블릿 컨테이너와의 결합도를 낮출 수 있는 유연한 초기화 방법이다.
서블릿 컨테이너 초기화는 초기화 과정이 더 복잡하고, 서블릿 컨테이너와 강하게 결합되는 반면, 애플리케이션 초기화는 이를 간소화하고 독립성을 유지할 수 있는 장점이 있다.
🟢 애플리케이션 초기화
- 편리함
서블릿 컨테이너 초기화: 서블릿 컨테이너를 초기화하려면 ServletContainerInitializer 인터페이스를 구현해야 하고, 이 구현체를 META-INF/services/jakarta.servlet.ServletContainerInitializer 파일에 등록해야 한다. 이 과정은 설정이 복잡하고 번거롭다.
애플리케이션 초기화: 반면, 애플리케이션 초기화는 특정 인터페이스(예: AppInit)만 구현하면 된다. 이렇게 하면 서블릿 컨테이너 설정과는 별도로 애플리케이션 초기화 코드를 관리할 수 있어 더 간편하다. - 의존성 감소
서블릿 컨테이너 초기화: 서블릿 컨테이너에 종속적이다. 즉, 초기화 작업을 서블릿 컨테이너에 맞춰 작성해야 하므로 서블릿 컨테이너와 강하게 결합된다.
애플리케이션 초기화: 서블릿 컨테이너와 독립적으로 동작할 수 있는 초기화 인터페이스를 구현하게 함으로써, 서블릿 컨테이너에 대한 의존성을 줄일 수 있다. (예를 들어, ServletContext ctx와 같은 서블릿 컨텍스트 객체가 필요 없는 초기화 작업의 경우, 서블릿 컨테이너와의 의존성을 완전히 제거할 수 있다.)
◼️ 스프링 컨테이너 등록
이제 앞의 초기화를 활용해 WAS와 스프링을 통합해보자.
필요한 과정은 다음과 같다.
- 스프링 컨테이너 만들기
- 스프링MVC 컨트롤러를 스프링 컨테이너에 빈으로 등록하기
- 스프링MVC를 사용하는데 필요한 디스패처 서블릿을 서블릿 컨테이너 등록하기
먼저 스프링 관련 라이브러리를 추가한다.
dependencies {
//서블릿
implementation 'jakarta.servlet:jakarta.servlet-api:6.0.0'
//스프링 MVC 추가
implementation 'org.springframework:spring-webmvc:6.0.4'
}
간단한 스프링 컨트롤러를 만들고, 컨트롤러를 스프링 빈으로 직접 등록한다.
@RestController
public class HelloController {
@GetMapping("/hello-spring")
public String hello() {
System.out.println("HelloController.hello");
return "hello spring!";
}
}
@Configuration
public class HelloConfig {
@Bean
public HelloController helloController() {
return new HelloController();
}
}
이제 어플리케이션 초기화를 사용해 서블릿 컨테이너에 스프링 컨테이너를 생성하고 등록하자.아까 AppInit을 구현해 두었기때문에, AppInit만 구현하면 초기화가 자동 실행된다.
/**
* http://localhost:8080/spring/hello-spring
*/
public class AppInitV2Spring implements AppInit{
@Override
public void onStartup(ServletContext servletContext) {
System.out.println("AppInitV2Spring.onStartup");
//스프링 컨테이너 생성
AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
appContext.register(HelloConfig.class); // 컨테이너에 스프링 설정 추가
//스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결
// 디스패처서블릿에 HTTP 요청이 오면 스프링 컨테이너에 들어있는 컨트롤러 빈들을 호출
DispatcherServlet dispatcher = new DispatcherServlet(appContext);
//디스패처 서블릿을 서블릿 컨테이너에 등록 (이름 주의! dispatcherV2)
ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcherV2", dispatcher);
// /spring/* 요청이 디스패처 서블릿을 통하도록 설정
servlet.addMapping("/spring/*");
}
}
이제 스프링을 실행해보자.
URL(/hello-spring) 호출시, 디스패처 서블릿(dispatcherV2)은 스프링 컨트롤러를 찾아서 실행한다.
이때 서블릿을 찾아서 호출하는데 사용된 /spring 을 제외한 /hello-spring 가 매핑된 컨트롤러(HelloController)의 메서드를 찾아서 실행한다.
◼️ 스프링 MVC 서블릿 컨테이너 초기화 지원
🟢 서블릿 컨테이너 초기화 과정
- ServletContainerInitializer 인터페이스를 구현해서 서블릿 컨테이너 초기화 코드를 만든다.
- 여기에 애플리케이션 초기화를 만들기 위해 @HandlesTypes 애노테이션을 적용한다.
- /META-INF/services/jakarta.servlet.ServletContainerInitializer 파일에 서블릿 컨테이너 초기화 클래스 경로를 등록한다.
다소 복잡하다.이를 위해 스프링MVC는 서블릿 컨테이너 초기화 작업을 이미 만들어두었다. (WebApplicationInitializer)
따라서 개발자는 애플리케이션 초기화 코드만 작성하면된다.
우리가 인터페이스 구현만으로 초기화가 가능하도록, 스프링은 서블릿 컨테이너 초기화 클래스를 등록해두었다.
아래와같이, SpringServletContainerInitializer는 @HandlesTypes을 통해 인터페이스 구현체를 생성하고 실행한다.
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {}
🔶 WebApplicationInitializer; 스프링이 이미 만들어둔 애플리케이션 초기화 인터페이스
package org.springframework.web;
public interface WebApplicationInitializer {
void onStartup(ServletContext servletContext) throws ServletException;
}
WebApplicationInitializer 인터페이스를 구현한 부분을 제외하고는 이전의 AppInitV2Spring 와 거의 같은 코드이다.
/**
* http://localhost:8080/hello-spring
*
* 스프링 MVC 제공 WebApplicationInitializer 활용
* spring-web
* META-INF/services/jakarta.servlet.ServletContainerInitializer * org.springframework.web.SpringServletContainerInitializer
*/
public class AppInitV3SpringMvc implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
System.out.println("AppInitV3SpringMvc.onStartup");
//스프링 컨테이너 생성
AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
appContext.register(HelloConfig.class);
//스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결
DispatcherServlet dispatcher = new DispatcherServlet(appContext);
//디스패처 서블릿을 서블릿 컨테이너에 등록 (이름 주의! dispatcherV3)
ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcherV3", dispatcher);
//모든 요청이 디스패처 서블릿을 통하도록 설정
servlet.addMapping("/");
}
}
위는 이제까지의 실습정리 그림이다.여기서는 실습이라 위와같이 구성했지만, 일반적으로는 스프링 컨테이너를 하나 만들고,디스패처 서블릿도 하나만 만든다. 그리고 디스패처 서블릿 경로매핑도 / 로 하여 디스패처 서블릿으로 모든 것을 처리하도록 한다.
참고로 서블릿URL은 더 구체적인 것이 먼저 실행된다.
스프링 MVC는 서블릿 컨테이너 초기화 파일에 초기화 클래스를 등록하고, WebApplicationInitializer 인터페이스를 애플리케이션 초기화에 사용한다.따라서 스프링 MVC를 사용할 때는 WebApplicationInitializer 인터페이스만 구현하면, 편리하게 애플리케이션 초기화를 설정할 수 있다.
◼️ 정리
이제까지 실습한 위의 과정들은 모두 서블릿 컨테이너 위에서 동작하는 방법이다. 따라서 항상 톰캣 같은 서블릿 컨테이너에 배포를 해야만 동작한다. 과거에는 이처럼 서블릿 컨테이너 위에서 모든 것이 동작했지만, 스프링 부트와 내장 톰캣을 사용하면서 이런 부분이 바뀌기 시 작했다.
'Spring' 카테고리의 다른 글
[Spring] 스프링 핵심 원리 - 고급편 섹션11 스프링 AOP - 포인트컷 (0) | 2024.08.21 |
---|---|
[Spring] 스프링 핵심 원리 - 고급편 섹션9,10 스프링 AOP 개념,구현 (0) | 2024.08.16 |
[Spring] 스프링 핵심 원리 - 고급편 섹션8 @Aspect AOP (0) | 2024.08.16 |
[Spring] 스프링 핵심 원리 - 고급편 섹션7 빈 후처리기 (0) | 2024.08.14 |
[Spring] 스프링 핵심 원리 - 고급편 섹션6 스프링이 지원하는 프록시 (0) | 2024.08.04 |