Spring Boot와 내장 톰캣

박계현·2025년 4월 30일
2

스프링

목록 보기
10/11

들어가며

이제 드디어 스프링 부트에 대해서 알아보려고 합니다. 저는 스프링 부트 이전을 몰랐기 때문에 처음 스프링으로 개발을 시작할 때, 원래 스프링은 시작 버튼 누르면 서버가 시작되는 건 줄 알았습니다(그 때는 WAS가 뭔지도 몰랐습니다. 그냥 스프링이 하나의 프로그램인 줄..). 이번 글에서는 스프링 부트란 뭔지, 스프링 부트 이전에 어떻게 스프링 프로젝트를 빌드해 배포하였고, 스프링 부트가 어떻게 이 과정을 자동화해줬는지 정리해보겠습니다.

요즘 스프링 프로젝트를 시작하는 법

저는 https://start.spring.io/ 에 들어가서 Spring Web, Lombok 등 종속성을 추가해서 압축 파일을 받아 Eclipes나 Intellij로 열어서 실행하면 되는 걸로 처음 배웠?습니다.

아래 "GENERATE" 버튼을 누르면 압축 파일을 딱 만들어줍니다.

해당 압축을 풀어서 인텔리제이 등으로 열면 바로 실행을 할 수 있습니다.

실행 버튼을 누르면 실행 로그가 찍히고,

http://localhost:8080/ 으로 들어가보면 서버가 성공적으로 떴다는 것을 알 수 있습니다.

정말 간단하게 로컬에서 서버를 띄울 수 있었고, 이대로 빌드를 하면 AWS EC2 같은 실제 서버에서도 손쉽게 배포할 수 있습니다.

그런데 이게 처음부터 이렇게 쉬웠던게 아니란 말씀. 사실 이 과정에는 수많은 자동화가 포함되어 있고, 그게 바로 스프링 부트가 제공하는 많은 기능 중 하나입니다.

톰캣(Tomcat)

🐈 톰캣(Tomcat)은 서블릿(Servlet) 기반의 웹 애플리케이션 서버(Web Application Server, WAS)입니다. 톰캣은 서블릿 컨테이너에 등록된 서블릿으로 각 요청을 전달하고, 응답을 받아 클라이언트에게 반환합니다.

실행 로그를 다시 자세히 살펴보면 온 동네방네 Tomcat이라고 적혀있는 것을 확인할 수 있습니다. 톰캣(Tomcat)이 뭐냐면 바로 저희가 작성한 스프링 애플리케이션 코드가 동작하는 웹 애플리케이션 서버(Web Application Server, WAS)입니다. WAS에 대한 자세한 내용은 이전 글을 참고해주세요.

여기서 톰캣과 관련되어 일어난 일을 간략히 정리하면 다음과 같습니다:

  1. 스프링 부트 애플리케이션이 시작됩니다.
  2. 애플리케이션에 라이브러리 형태로 내장된 톰캣 서버가 8080 포트로 초기화됩니다.
  3. 톰캣 서버의 서블릿 엔진이 시작됩니다.
  4. 스프링의 디스패처 서블릿이 톰캣의 서블릿 컨테이너에 등록됩니다.
  5. 디스패처 서블릿이 초기화되고 이후 요청을 디스패처 서블릿이 처리합니다.

기존에는 위와 같은 과정을 모두 개발자가 처리해주어야 했습니다. 실제로 어떻게 했는지 한번 살펴보겠습니다.

외장 톰캣

톰캣은 클라이언트로부터 실제 HTTP 요청을 받고 응답을 반환하는 가장 바깥쪽 주체입니다. 이는 톰캣이 내장이든 아니든 마찬가지 입니다.

이렇게 톰캣의 서블릿 컨테이너에 스프링의 디스패처 서블릿을 등록해 요청을 처리하도록 하고, 우리는 이 디스패처 서블릿이 각 요청(URL)에 따라 호출할 Controller를 작성하는 식입니다. 톰캣은 우리가 제공하는 애플리케이션 코드를 WAR 형태로 받아서 실행하고, 그 애플리케이션 코드 내부에서 디스패처 서블릿을 톰캣에 등록합니다.

좀 더 쉽게 말해서, 톰캣은 스프링 코드를 실행해주는 서버입니다. 톰캣이 제공해주는 기능들이 있으며, 우리는 이 기능을 우리 애플리케이션 코드에서 사용할 수 있습니다.

외장 톰캣 설치

톰캣은 압축된 버전을 다운받아서 풀고 실행하면 실행이 됩니다. https://tomcat.apache.org/download-11.cgi 에서 자신의 환경에 맞는 걸 다운 받습니다. 제가 윈도우라 나머지 설명도 윈도우 기준으로 작성됩니다.

압축을 풀고 /bin 폴더로 들어가 '터미널에서 열기'를 해줍니다.

.\startup.bat을 하면 톰캣을 실행할 수 있습니다. (주의: 8080포트에 열린 프로세스가 없어야 합니다)

이런 창이 뜨는데 한국어 때문에 깨져보입니다. 톰캣 폴더에서 logs 폴더에 들어가 catalina.2025-04-30 과 같이 현재 날짜에 해당하는 파일을 메모장 등으로 열면 로그를 확인할 수 있습니다.

http://localhost:8080/ 에 접속했을 때, 위와 같은 페이지가 나오면 기본 톰캣이 잘 설치되어 실행되고 있는 것입니다.

프로젝트 빌드 및 배포

외장 톰캣에 우리가 만든 자바 프로젝트를 배포하기 위해서는 프로젝트를 WAR로 빌드해야 합니다. WAR는 Web Application Archive로 기존 JAR(Java Archive)가 JVM에서 돌아가기 위해서라면 WAR는 웹 애플리케이션 서버에서 실행되기 위한 스펙을 가지고 있습니다. WAR의 자세한 내용은 생략합니다.

@WebServlet(urlPatterns = "/test")
public class TestServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        System.out.println("TestServlet.service");
        resp.getWriter().println("test");
    }
}

단순히 "test"를 적어서 반환하는 서블릿을 포함하고 있는 프로젝트를 만들어 WAR로 빌드하겠습니다.

plugins {
 id 'java'
 id 'war'
}

build.gradle에 위와 같이 플러그인이 설정되어 있습니다.

프로젝트 폴더에서 .\gradlew clean build 를 해주면 빌드가 진행됩니다. /build/lib 폴더로 들어가면 프로젝트 이름-0.0.1-SNAPSHOT.war 파일이 만들어져있는 것을 볼 수 있습니다.

실행되고 있다면 톰캣을 내리고, 톰캣 폴더에 /webapps 폴더 내 다른 폴더를 모두 지우고 빌드한 war 파일을 붙여넣습니다. 붙여넣은 다음에는 이름을 ROOT.war로 바꿉니다.

톰캣을 다시 실행해서 http://localhost:8080/test 를 입력하면 우리가 등록한 서블릿이 응답을 반환해줍니다.

이 과정을 IntelliJ등 최신 IDE들은 실행 환경 설정을 통해 톰캣이 설치된 위치를 입력하면 IDE 내에서 바로 실행할 수 있도록 해줍니다. 해당 과정은 여기서 생략합니다.

내장 톰캣

이렇게 외장 톰캣을 통해 서버를 배포하기 위해서는

  1. 톰캣을 설치하고
  2. WAR로 빌드하고
  3. 톰캣에 복사(배포)하는

귀찮은 과정을 거쳐야합니다. 이는 지금 우리가 아는 스프링 부트의 실행 과정과는 많이 다릅니다. 애플리케이션의 main() 메서드만 실행하면 웹 애플리케이션 서버까지 동작했으면 좋겠다는 아이디어에서 톰캣은 라이브러리 형태의 내장 톰캣을 제공합니다.

Servlet 설정

public class EmbedTomcatServletMain {
    public static void main(String[] args) throws LifecycleException {
        System.out.println("EmbedTomcatServletMain.main");
        // 톰캣 설정
        Tomcat tomcat = new Tomcat();
        Connector connector = new Connector();
        connector.setPort(8080);
        tomcat.setConnector(connector);

        // 서블릿 등록
        Context context = tomcat.addContext("", "/");
        tomcat.addServlet("", "helloServlet", new HelloServlet());
        context.addServletMappingDecoded("/hello-servlet", "helloServlet");
        
        // 톰캣 시작
        tomcat.start();
    }
}

위 코드는 내장 톰캣 라이브러리를 이용해 메인 메서드에서 직접 톰캣을 초기화하고 서블릿을 추가하여 톰캣을 실행하는 코드입니다. 만약 스프링을 이용하려면 각 요청에 대한 서블릿을 생성하는 것이 아닌 DispatcherServlet 하나만 등록하고 디스패처 서블릿이 등록된 핸들러(컨트롤러)를 호출하게 됩니다.

Spring 설정

public class EmbedTomcatSpringMain {
    public static void main(String[] args) throws LifecycleException {
        System.out.println("EmbedTomcatSpringMain.main");

        // 톰캣 설정
        Tomcat tomcat = new Tomcat();
        Connector connector = new Connector();
        connector.setPort(8080);
        tomcat.setConnector(connector);

        // 스프링 컨테이너 생성
        AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
        appContext.register(HelloConfig.class);

        // 스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결
        DispatcherServlet dispatcherServlet = new DispatcherServlet(appContext);

        // 디스패처 서블릿 등록
        Context context = tomcat.addContext("", "/");
        tomcat.addServlet("", "dispatcherServlet", dispatcherServlet);
        context.addServletMappingDecoded("/", "dispatcherServlet");

		// 톰캣 시작
        tomcat.start();
    }
}

위에 설명한대로 스프링 컨테이너를 생성하여 디스패처 서블릿을 연결해 톰캣에 서블릿으로 등록하는 코드입니다.

이제 main() 메서드를 실행하는 것만으로 톰캣이 또 스프링 컨테이너를 안은 디스패처 서블릿을 들고 실행이 됩니다. 전체 코드는 JAR로 빌드되어 그 자체로 실행할 수 있습니다.

빨갛게 나와서 좀 그렇지만 제대로 실행이 되었습니다.

http://localhost:8080 에서의 접속도 확인이 됩니다.

부트 클래스

내장 톰캣을 통해 훨씬 편해졌지만, 더 편해질 수 있습니다! 위 코드에서는 간단히 디스패처 서블릿만 등록했지만, 실제 스프링부트를 초기화하는 과정은 컴포넌트 스캔부터 해서 훨씬 복잡합니다. 이런 코드를 하나의 클래스로 나눌 수 있습니다.

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 appContext = new AnnotationConfigWebApplicationContext();
        appContext.register(configClass);

        // 스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결
        DispatcherServlet dispatcherServlet = new DispatcherServlet(appContext);

        // 디스패처 서블릿 등록
        Context context = tomcat.addContext("", "/");
        tomcat.addServlet("", "dispatcherServlet", dispatcherServlet);
        context.addServletMappingDecoded("/", "dispatcherServlet");

		// 톰캣 시작
        try {
            tomcat.start();
        } catch (LifecycleException e) {
            throw new RuntimeException(e);
        }
    }
}

Configuration 클래스를 받아서 스프링 컨테이너를 생성하도록 합니다. args도 받도록 변경되었습니다. 실제로는 다양하게 사용되지만 여기서는 출력만 하도록 구현되어 있습니다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ComponentScan
public @interface MySpringBootApplication {
}

컴포넌트 스캔(@ComponentScan)이 달린 어노테이션입니다. 기본 설정으로 해당 어노테이션이 붙은 클래스 위치를 기준으로 컴포넌트 스캔을 실행합니다.
@Documented는 javadoc과 관련된 어노테이션이라고 합니다.

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

main() 메서드가 들어가는 클래스입니다. 어노테이션이 붙어있어서 해당 클래스 위치를 기준으로 컴포넌트 스캔을 합니다(따라서 위치에 주의합니다).

이렇게 하면 개발자는 @MySpringBootApplication 어노테이션과 MySpringApplication.run() 메서드만 알고 있으면 됩니다. 이렇게까지 해놓고 나니 이제 좀 어디서 본 것 같지 않나요?

맨 처음 살펴본 스프링 부트의 모습과 같습니다. (영한님의 강의 구성력에 무한 감탄!) 이렇게 만든 코드를 라이브러리로 만들어 배포하면 그것이 스프링 부트인 것입니다.

이렇게 만들어진 코드는 JAR로 빌드하여 배포하면 java -jar를 통해 바로 실행할 수 있습니다. 원래 JAR는 내부에 JAR를 포함할 수 없지만 스프링은 실행 시 내부에서 런타임에 다른 JAR를 로드하는 과정이 포함되어 있습니다.

이 외에도 스프링 부트는 웹 개발 등 각 상황에 맞게 주로 사용되는 라이브러리들(Web MVC, Jackson(JSON 매핑), Embeded Tomcat, Slf4J(로깅) 등등)을 하나로 모아서 스프링 부트 스타터로 제공합니다. 또한, 자동 구성(Auto Configuration)을 통해서 외부 라이브러리가 제공하는 기능을 이용하는데 필요한 빈 등록 등을 자동화해줍니다. 그 외 정말 다양한 기능을 제공하고 지금도 늘어나고 있습니다.

스프링 부트는 따라서 스프링 생태계에서 개발하는 데 필요한 많은 과정을 자동화해주고, 동시에 필요하다면 커스텀하게 해주는 프레임워크 + 플랫폼입니다.

마치며

이 부분은 개인적인 사담이 많습니다 🙂

스프링을 좀 제대로 공부해야지 마음을 먹고 약 네 달이 지났습니다. 스프링 프레임워크의 코어 기능들(빈, AOP)과 웹 MVC(WAS, MVC, 디스패처 서블릿, 쿠키, 세션), 데이터 접근 기술(트랜잭션, 트랜잭션 전파), 그리고 미처 블로그에 기록하지 못한 여러 내용들을 걸쳐 드디어 스프링 부트에 도달했습니다.

기본적으로 인프런 김영한님의 스프링 완전 정복 로드맵을 따라 강의들을 보았고 정말 많은 도움이 되었습니다. 그 외 추가적으로 궁금한 것들을 공식 문서와 다른 자료들을 참고하며 공부했습니다. (백엔드에서) 가장 먼저 접한 프레임워크가 스프링이라 스프링으로 개발을 계속했었는데, 기본적인 작동 원리와 기능들을 좀 이해하고 나니 특별히 더 예뻐보이는 것 같습니다.

누군가 스프링 공부를 하고 싶다고 하면 저는 항상 김영한님 강의를 적극 추천하고 다닙니다. 김영한님 강의가 아니더라도 기본기를 충분히 익히기를 추천합니다(근데 그 기본기와 원리를 김영한님이 정말 잘 설명해주십니다ㅋㅋㅋ). 그때 그때 필요한 정말 작은 것들만 익혀가면서 개발을 해오다 기본기를 이해하고 나니 정말 세상이 달라졌습니다. 물론 그런 부딪힘이 이전에 있었기 때문에 그만큼 아! 이게 이거였구나! 하고 공부할 수 있었던 것도 같습니다. 그래서 추가적으로 학습을 시작하시기 전에 스프링부트로 간단한 게시판 하나 정도를 만들어보고 강의나 학습을 시작하는 것도 추천합니다.

(갑자기 냅다 상반기 회고가 된 것 같긴 한데)

'기본기를 익히자'는 생각으로, 당장 대단한 포트폴리오를 하나라도 더 만들고 싶은 급한 마음을 내려두고 공부를 했습니다. 자바, 스프링, DB 부분을 전체적으로 다시 공부하려고 했고, 이후에는 JPA와 추가적으로 관심 있는 것들을 이제는 드디어 프로젝트와 함께 공부하려고 하고 있습니다. 몇 달간 기본기를 공부하고 나서 느낀 바를 간단하게 여기 공유해보려고 합니다.

  1. 일단 개발이 전보다 훨씬 재밌습니다.
    이전에는 문제가 생겨도 어디서 왜 생겼고, 해결해도 무엇 때문에 해결되었는지 확신이 없었는데, 이제는 전반적인 해결과정을 제가 설계하고 시도하면서, 제대로 안 되더라도 무언가 하나를 더 배울 수 있게 된 것 같습니다.

  2. GPT와의 대화도 훨씬 재밌어졌습니다.
    이전에는 냅다 코드 긁어서 "여기 버그 잡아줘." 혹은, "이런 기능 구현해줘." 하면 GPT가 열심히 답해준 걸 뭔지는 잘 모르겠지만 일단 적용해보고, 되면 오케이, 안 되면 GPT를 더 갈구기만 했습니다. 정말 답답한 과정의 연속이었습니다. 그런데 지금은 무언가 물어보기 전에 이미 GPT가 뭐라 답할지 예상이 갑니다. 그리고 그 예상이 적중하면 "오! 역시." 하고 기분이 좋고, 빗나가면 "오! 이건 몰랐네." 하고 새로운 걸 배우게 되서 또 기분이 좋습니다.

  3. 테스트 작성이 즐거워졌습니다.
    스프링 내부 원리를 알고 나니 전보다 훨씬 의미있는 테스트 작성이 가능해졌습니다. 원래는 이 테스트가 의미가 있을까? 바빠 죽겠는데 테스트를 어떻게 다 쓰나? 이거 써놓는다고 다시 보지도 않는데 뭐가 달라지나? 하고 생각했는데, 이제는 테스트 초록불 뜨는 거 보는 맛에 개발하는 것 같습니다ㅋㅋㅋ. 테스트는 개발을 계획하고, 내부 동작을 이해하고, 그 자체로 시스템을 표현하며, 내 작업에 쉼표를 찍어주는 좋은 친구입니다.

뭐지 뭐 엄청 길어졌는데 아무튼 그렇습니다. 다음에는 스프링 부트의 액츄에이터와 모니터링 방법 혹은 JPA에 대해서 다뤄보겠습니다.

참고 자료

profile
안녕하세요! 차근차근 성장하는 백엔드 엔지니어 박계현입니다😊

2개의 댓글

comment-user-thumbnail
2025년 5월 2일

다시 읽어봐도 유익하네요 ㅎㅎ

1개의 답글