개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

기초개념 › Spring

스프링 부트

스프링 부트는 무엇을 대신해 주는가

스프링 부트 애플리케이션은 main 메서드가 있는 평범한 자바 클래스(App)입니다. main 메서드가 SpringApplication.run(App.class, args) 한 줄을 부르는 것으로 시작하고, 이 한 줄이 스프링을 시작하고(클래스를 훑어 빈을 만들고), 웹 서버(톰캣)를 프로그램 안에서 띄우고, 요청을 받을 준비를 합니다. 톰캣을 따로 설치하지 않고, 서버를 띄우는 코드도 우리가 쓰지 않습니다. App 위의 @SpringBootApplication은 이 클래스가 있는 패키지부터 훑고 필요한 것을 알아서 설정하라는 표시이고, 무엇을 설정할지는 프로젝트에 들어 있는 라이브러리를 보고 정합니다. 웹 라이브러리가 들어 있으면 톰캣을 띄우고, 없으면 띄우지 않습니다. 이 실습은 run 줄, @SpringBootApplication, 웹 라이브러리를 하나씩 지워 가며 java 명령으로 프로그램을 실행하고, 프로그램이 계속 실행되는지, 톰캣이 뜨는지, 브라우저의 요청에 응답하는지를 봅니다.

실행으로 대조 · 스프링 부트 4.1.1에서 여덟 가지 조합을 조합마다 소스를 컴파일해 자식 java 프로세스로 실행하고, 밖에서 대기 포트와 HTTP 요청을 관찰해 대조 (웹 라이브러리를 뺀 클래스패스는 pom의 의존 관계로 정함)

이 실습에 나오는 개념

스프링 부트
스프링 부트는 스프링 애플리케이션을 만들고 실행하는 데 필요한 설정을 줄여 주는 스프링의 라이브러리다. 애플리케이션은 main 메서드에서 SpringApplication.run(App.class, args)를 부르는 것으로 시작하며, 이 호출이 스프링을 시작해 빈을 만들고, 웹 애플리케이션이면 웹 서버(기본은 톰캣)를 프로그램 안에서 띄워 요청을 받을 준비까지 한다. 스프링 부트는 프로젝트에 들어 있는 라이브러리를 보고 무엇을 설정할지 정하므로, 같은 코드라도 웹 라이브러리가 있으면 웹 서버가 뜨고 없으면 뜨지 않는다. 웹 서버를 따로 설치하거나 서버를 띄우는 코드를 쓰지 않아도 되는 것이 이 방식의 특징이다.
@SpringBootApplication
@SpringBootApplication은 스프링 부트 애플리케이션의 시작 클래스 위에 붙이는 애너테이션이다. @EnableAutoConfiguration(스프링 부트의 자동 설정을 켠다), @ComponentScan(이 클래스가 있는 패키지부터 @Component 계열 클래스를 훑어 찾는다), @SpringBootConfiguration(추가 빈을 등록하거나 다른 설정 클래스를 가져올 수 있게 한다)을 하나로 합친 것이다. 이 애너테이션이 없으면 클래스 훑기도 자동 설정도 일어나지 않으므로, 웹 라이브러리가 있는 프로젝트에서도 웹 서버를 만드는 설정이 없어 시작에 실패한다.
스타터(Starter)
특정 기능에 필요한 라이브러리들을 서로 호환되는 버전으로 묶어 놓은 Spring Boot의 의존성 묶음이다. 웹 스타터(Spring Boot 3까지는 spring-boot-starter-web, Spring Boot 4부터는 spring-boot-starter-webmvc) 하나를 추가하면 Spring MVC, JSON 처리, 내장 톰캣 등이 함께 들어온다. 라이브러리 간 버전 충돌을 직접 맞추는 수고를 덜어 준다.
내장 서버
Spring Boot 애플리케이션 안에 포함되어 함께 실행되는 웹 서버(기본은 톰캣)다. 별도로 서버를 설치하고 애플리케이션을 배포하는 과정 없이 java -jar 명령 하나로 실행할 수 있다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 컴퓨터에서 java 명령으로 App 클래스를 실행하면, 자바가 App 클래스의 main 메서드를 부릅니다. main 메서드는 자바 프로그램이 시작하는 곳입니다.
  2. main 메서드의 SpringApplication.run(App.class, args) 줄이 실행됩니다. 이 한 줄이 스프링 부트를 시작합니다. App.class는 프로그램의 출발점이 되는 클래스를 넘기는 것이고, args는 main이 받은 인자를 그대로 넘기는 것입니다.
  3. 스프링 부트가 프로젝트에 들어 있는 라이브러리를 살펴봅니다. 웹 요청을 처리하는 스프링 MVC가 들어 있으므로 웹 애플리케이션이라고 판단하고, 웹 서버를 쓰는 스프링을 준비합니다.
  4. 스프링을 시작합니다. App 위에 @SpringBootApplication이 있으므로 App 클래스가 있는 패키지부터 클래스를 훑어 @RestController가 붙은 HelloController를 찾고, 그 객체를 만들어 보관합니다. 또 라이브러리에 맞춰 필요한 것을 알아서 설정하는데, 웹 라이브러리가 있으므로 요청을 받아 컨트롤러에 나눠 주는 DispatcherServlet도 이때 만들어집니다. 우리가 쓴 코드에는 new HelloController()도 new DispatcherServlet()도 없습니다.
  5. 스프링 부트가 톰캣(웹 서버)을 이 java 프로세스 안에서 띄우고, 만들어 둔 DispatcherServlet을 톰캣에 등록합니다. 톰캣을 따로 설치하지 않았고, 우리가 쓴 코드에도 서버를 띄우는 줄이 없습니다. 톰캣이 요청을 받는 포트는 정하지 않으면 8080입니다.
  6. SpringApplication.run 줄이 끝나고 main 메서드도 끝에 닿습니다. 그런데 java 프로세스는 끝나지 않습니다. 톰캣이 만들어 둔 스레드(따로 움직이는 실행 흐름)가 살아 있는 동안 java는 프로그램을 끝내지 않고, 톰캣은 그 스레드로 요청을 기다리기 때문입니다.
  7. 브라우저 주소창에 localhost:8080/hello 주소를 입력하면, 브라우저가 서버에 GET /hello 요청을 보냅니다.
  8. 톰캣이 요청을 받아 DispatcherServlet에 넘기고, DispatcherServlet이 HelloController의 hello() 메서드를 불러 줍니다. 이 메서드가 돌려준 글은 "안녕하세요"입니다.
  9. 브라우저가 200 응답을 받습니다. 본문은 "안녕하세요"입니다. java 프로세스는 끝나지 않고 다음 요청을 계속 기다립니다.

실행 결과

코드

// build.gradle: 이 프로젝트에 넣을 라이브러리
implementation 'org.springframework.boot:spring-boot-starter-webmvc'

// 표시: 이 패키지부터 훑고, 필요한 것을 알아서 설정한다
@SpringBootApplication
public class App {
    public static void main(String[] args) {
        // 이 한 줄이 스프링을 시작하고 서버를 띄운다
        SpringApplication.run(App.class, args);
    }
}

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "안녕하세요";
    }
}

이 모형의 전제

스프링 부트 4.1.1(스프링 프레임워크 7.0.9, 내장 톰캣 11.0.24)을 JDK 21.0.12.1에서 실행한 결과입니다. 여덟 가지 조합을 모두, 화면의 코드와 같은 구조의 소스(패키지 app의 App과 HelloController, 서버를 띄우거나 세는 줄은 없음)를 컴파일하고 java 명령으로 app.App의 main 메서드부터 새 프로세스로 실행한 뒤, 프로세스 밖에서 관찰해 대조했습니다. 판단 기준은 이렇습니다. 프로그램은 프로세스가 끝나거나 대기 포트가 열릴 때까지 기다린 뒤 3초가 더 지나도 살아 있으면 "계속 실행하며 요청을 기다림", 이미 끝났고 종료 코드가 0이면 "할 일이 없어 바로 끝남", 0이 아니면 "시작하다 실패함"입니다. 웹 서버는 그 프로세스가 연 TCP 대기 포트(netstat)가 있고 로그에 "Tomcat started on port"가 같은 포트로 찍혔는지로, 응답은 프로세스 밖에서 그 포트로 GET /hello를 보낸 실제 HTTP 결과로 정했습니다. 서버가 없는 조합에서도 미리 비어 있는 것을 확인한 임의의 포트를 server.port로 주고 그 포트로 요청을 보냈으며, 모두 연결이 거부되었습니다(포트를 0으로 주면 서버가 없을 때 연결할 곳이 없어서 이렇게 했습니다). 주소의 localhost:8080은 내 컴퓨터에서 스프링 부트를 기본 포트로 띄웠을 때의 예이며, 기본 포트가 8080이라는 것은 스프링 부트 문서의 Embedded Servlet Container Support 절을 따랐습니다. 클래스패스는 라이브러리 폴더 전체가 아니라, 화면의 의존성 한 줄이 들여오는 jar만 줬습니다. 웹 라이브러리가 있으면 spring-boot-starter-webmvc가 들여오는 39개를, 없으면 spring-boot-starter가 들여오는 21개를 썼고, 빼는 18개는 spring-boot-starter-webmvc, spring-boot-starter-jackson, spring-boot-starter-tomcat, spring-boot-starter-tomcat-runtime, spring-boot-tomcat, spring-boot-web-server, spring-boot-http-converter, spring-boot-webmvc, spring-boot-servlet, spring-boot-jackson, spring-web, spring-webmvc, tomcat-embed-core, tomcat-embed-el, tomcat-embed-websocket, jackson-databind, jackson-core, jackson-annotations입니다. 이 목록은 Gradle을 돌려 얻은 것이 아니라 Maven Central의 pom을 열어 의존 관계를 따라가 정한 것이고(스타터와 스프링 부트 모듈, spring-web, spring-webmvc, 톰캣, jackson-databind의 pom은 확인했고 jackson-core와 jackson-annotations는 열지 않았습니다), 스프링 시큐리티·JPA 같은 다른 라이브러리는 두 경우 모두 클래스패스에 없습니다. 스프링 부트 4에서 웹 스타터의 이름은 spring-boot-starter-webmvc이며(문서 Build Systems의 Starters 표에서 spring-boot-starter-web은 이것으로 대체된 것으로 표시됨), 웹 스타터가 들여오는 jar가 정확히 이 목록과 같다고는 Gradle 해석 결과로 확인하지 못했습니다. 웹 라이브러리를 뺀 조합에서는 HelloController가 컴파일되지 않아(org.springframework.web.bind.annotation 패키지가 없다는 오류를 실제로 확인) 그 클래스도 함께 지웠습니다. 대조한 것은 결과 표의 세 항목과, 단계 문장이 기대는 사실입니다. 웹 라이브러리가 있으면 SpringApplication이 웹 애플리케이션(서블릿)으로, 없으면 아님으로 판단한다는 것, 톰캣에 등록된 서블릿이 DispatcherServlet 하나이고 "/"를 맡는다는 것, HelloController와 DispatcherServlet이 스프링의 빈이라는 것, 우리 소스에 톰캣이나 서버를 띄우는 글이 없다는 것, 서버가 뜬 뒤 main 스레드는 끝났는데 톰캣의 스레드(container-0 등)가 남아 프로세스가 살아 있다는 것, 두 가지 실패 방식(run 줄이 없으면 스프링이 시작되지 않고 바로 끝나며, @SpringBootApplication이 없고 웹 라이브러리가 있으면 웹 서버를 만드는 ServletWebServerFactory 빈이 없다는 예외로 종료 코드 1로 실패함)도 실행으로 확인했습니다. 문서가 정한 것은 스프링 부트 문서 Using the @SpringBootApplication Annotation 절(@EnableAutoConfiguration, @ComponentScan, @SpringBootConfiguration을 합친 것), Auto-configuration 절(클래스패스의 jar를 보고 설정), SpringApplication의 Web Environment 절(스프링 MVC가 있으면 서블릿 웹 컨텍스트를 쓴다는 판단 규칙), Embedded Servlet Container Support 절(톰캣과 제티를 지원하고 기본 포트가 8080)입니다. 화면의 "클래스를 훑어 빈을 만든다"와 "톰캣을 띄우고 DispatcherServlet을 등록한다"를 두 단계로 나눈 것은 단순화입니다. 실제 로그에서는 스프링이 시작되는 도중에 톰캣이 만들어지고(Tomcat initialized), "Started App" 로그보다 먼저 톰캣이 연결을 받기 시작하며(Tomcat started), DispatcherServlet이 실제로 초기화되는 것은 첫 요청이 올 때였습니다. @SpringBootApplication이 없을 때 클래스를 훑지 않는다는 것은 문서가 정한 것이고, 빈 정의의 수가 @SpringBootApplication이 있을 때 더 많다는 것(웹 라이브러리가 없는 조합에서 7개와 50개)은 이 버전에서 관찰한 값입니다. 문서가 정한 것과 달리 "웹 서버가 필요한데 설정이 없으면 조용히 끝나지 않고 실패한다", "웹 라이브러리가 없으면 오류 없이 끝난다"는 이 버전에서 관찰한 동작입니다. 화면의 코드는 package와 import 줄을 줄였고, App과 HelloController가 같은 패키지에 있다고 가정했으며, build.gradle의 줄은 Gradle로 실행한 것이 아니라 그 줄이 뜻하는 jar를 클래스패스에 준 것입니다. Maven과 war 배포, 톰캣 대신 제티를 쓰는 경우, 포트나 웹 애플리케이션 종류를 설정으로 바꾸는 경우는 다루지 않았습니다. 무엇을 자동으로 설정하는지의 세부는 "세부 동작 실습: 스프링 부트 자동 설정"에서 다룹니다.

앞 실습: @Transactional · 다음 실습: 테스트 코드

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요