첫 서버 — start.spring.io 가 만들어준 것들
Article
3강 끝에서 이렇게 말했다. 시작줄을 잘라 메서드와 경로를 꺼내고, 경로를 조각내 숫자를 뽑고, 메서드마다 if 를 세우고, 상태줄을 조립하고, Content-Length 를 세고… 아무도 그렇게 하지 않는다.
이번 강의에서 그 일을 대신 해줄 것을 받아온다. 코드는 거의 안 쓴다. 대신 우리가 쓰지 않은 코드가 무슨 일을 하는지 본다.
프로젝트를 받아온다
start.spring.io 에 들어가면 항목 몇 개를 고르는 화면이 나온다. 골라서 내려받으면 압축 파일 하나가 떨어진다.
터미널에서 바로 받을 수도 있다.
curl -o starter.zip "https://start.spring.io/starter.zip?type=gradle-project&language=java&bootVersion=4.1.0&baseDir=bookstore&groupId=kr.dcodelab&artifactId=bookstore&name=bookstore&packageName=kr.dcodelab.bookstore&javaVersion=17&dependencies=web"
unzip starter.zip
주소가 길지만 뜯어보면 화면에서 고르는 항목 그대로다. 빌드 도구는 Gradle, 언어는 자바, Spring Boot 는 4.1.0, 자바는 17, 그리고 의존성으로 web 하나.
57,593 바이트짜리 압축 파일 하나가 떨어진다. 풀면 이렇게 생겼다.
- bookstore
- build.gradle
- settings.gradle
- gradlew
- gradlew.bat
- gradle/wrapper
- gradle-wrapper.jar
- gradle-wrapper.properties
- src/main/java/kr/dcodelab/bookstore
- BookstoreApplication.java
- src/main/resources
- application.properties
- src/test/java/kr/dcodelab/bookstore
- BookstoreApplicationTests.java
- HELP.md · .gitignore · .gitattributes
kr/dcodelab/bookstore 라는 폴더 깊이는 자바 편 21강에서 본 패키지다. packageName 에 적은 것이 그대로 폴더가 됐다.
build.gradle — 무엇을 가져다 쓸지 적는 곳
plugins {
id 'java'
id 'org.springframework.boot' version '4.1.0'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'kr.dcodelab'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
자바 편에서 우리는 javac 로 컴파일하고 java 로 실행했다. 남의 코드를 가져다 쓸 일이 없었으니 그걸로 충분했다.
이제는 다르다. Spring 도, 톰캣도, JSON 을 다루는 도구도 전부 남이 만든 것이다. 그것들을 인터넷 어딘가에서 찾아 내려받고, 서로 맞는 버전을 골라 짝지어야 한다. 그 일을 하는 것이 Gradle 이다.
dependencies 블록에 딱 세 줄 적혀 있다. implementation 으로 시작하는 첫 줄이 우리가 고른 web 이다.
starter 라는 이름이 붙은 것들은 묶음이다. spring-boot-starter-webmvc 하나를 적으면 톰캣, JSON 변환기, 웹 요청을 다루는 도구가 통째로 딸려 온다. 하나씩 골라 담을 필요가 없다.
gradlew — Gradle 을 설치하지 않는다
이 컴퓨터에는 Gradle 이 깔려 있지 않다. 그런데도 빌드가 된다.
./gradlew --version
- Gradle 9.5.1
- Build time: 2026-05-12 13:19:42 UTC
- Launcher JVM: 17.0.17 (Eclipse Adoptium 17.0.17+10)
- OS: Windows 11 10.0 amd64
gradlew 는 Gradle 을 내려받아 실행해주는 작은 스크립트다. 어느 버전을 받을지는 gradle-wrapper.properties 에 적혀 있다.
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
덕분에 이 프로젝트를 받는 사람은 아무것도 설치할 필요가 없다. 모두가 정확히 같은 버전의 Gradle 로 빌드한다. 그래서 명령 앞에 항상 ./ 가 붙는다. gradle 이 아니라 ./gradlew 다.
빌드해본다
./gradlew build
- Starting a Gradle Daemon (subsequent builds will be faster)
- > Task :compileJava
- > Task :bootJar
- > Task :test
- > Task :build
- BUILD SUCCESSFUL in 31s
- 7 actionable tasks: 7 executed
build/libs 에 파일 두 개가 생겼다.
| 파일 | 크기 | 무엇이 들었나 |
|---|---|---|
bookstore-0.0.1-SNAPSHOT-plain.jar | 1,509 바이트 | 우리가 쓴 코드만 |
bookstore-0.0.1-SNAPSHOT.jar | 약 19 MB | 톰캣까지 전부 |
1,509 바이트와 19MB. 이 차이가 우리가 안 쓴 코드의 양이다.
우리 코드는 이게 전부다
package kr.dcodelab.bookstore;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BookstoreApplication {
public static void main(String[] args) {
SpringApplication.run(BookstoreApplication.class, args);
}
}
main 이 있다. 자바 편 2강에서 JVM 이 찾아 들어간다고 했던 그 main 이다. public static void main(String[] args) 네 단어가 하나도 안 바뀌었다.
그런데 1강에서 우리는 main 이 끝나면 프로그램이 죽는다고 했다. 여기 main 은 한 줄뿐이다. SpringApplication.run(...) 을 부르고 끝난다. 그럼 곧바로 죽어야 하지 않나.
run 은 돌아온다. main 도 끝난다. 그런데도 프로그램은 안 죽는다.
1강의 문장을 조금 더 정확하게 고쳐야 한다. JVM 은 main 이 끝날 때 죽는 게 아니라, 살아 있는 스레드가 하나도 안 남았을 때 죽는다. 1강의 서버는 스레드가 main 하나뿐이었으니 그 둘이 같은 말이었을 뿐이다.
톰캣은 자기 스레드를 따로 띄운다. main 이 끝나기 직전에 살아 있는 스레드를 찍어보면 이렇다.
- >>> main 이 끝나기 직전. 살아 있는 스레드를 본다
- >>> 비데몬 스레드: Catalina-utility-2
- >>> 비데몬 스레드: main
- >>> 비데몬 스레드: Catalina-utility-1
- >>> 비데몬 스레드: container-0
Catalina 는 톰캣의 옛 이름이다. 1강의 while (true) 와 accept() 는 저 스레드들 안에서 돌고 있다. main 이 먼저 퇴근해도 저들이 남아서 문을 지킨다.
그리고 클래스 위에 표식이 하나 붙어 있다. @SpringBootApplication. 자바 편 21강에서 애노테이션은 실행되지 않는 표식이고 누군가 읽어야 뜻이 생긴다고 했다. 지금은 그냥 지나간다. 8강에서 해부한다.
띄운다
./gradlew bootRun
- . ____ _ __ _ _
- /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
- :: Spring Boot :: (v4.1.0)
- INFO --- Starting BookstoreApplication v0.0.1-SNAPSHOT using Java 17.0.17 with PID 3192
- INFO --- No active profile set, falling back to 1 default profile: "default"
- INFO --- Tomcat initialized with port 8080 (http)
- INFO --- Starting Servlet engine: [Apache Tomcat/11.0.22]
- INFO --- Tomcat started on port 8080 (http) with context path '/'
- INFO --- Started BookstoreApplication in 1.44 seconds
프롬프트가 돌아오지 않는다. 1강의 그 서버와 똑같다.
로그에서 두 줄만 읽으면 된다.
- 01
Tomcat started on port 8080
톰캣이 1강의
ServerSocket자리에 있다. 우리는new ServerSocket(8080)을 쓴 적이 없는데 누군가 8080 을 열었다. - 02
Started BookstoreApplication in 1.44 seconds
1.44초 동안 무엇을 했나. 클래스를 뒤지고, 객체를 만들고, 서로 연결하고, 톰캣을 띄웠다. 그 1.44초가 5강부터 8강까지의 내용이다.
아직 아무것도 안 만들었는데
컨트롤러를 하나도 안 썼다. 그 상태로 요청을 보내보자.
curl -i http://localhost:8080/
- HTTP/1.1 404
- Content-Type: application/json
- Transfer-Encoding: chunked
- Date: Thu, 09 Jul 2026 15:21:28 GMT
- {"timestamp":"2026-07-09T15:21:28.042Z","status":404,"error":"Not Found","path":"/"}
404 다. 우리 서버는 아직 아무 경로도 모르니, 없는 것을 달라고 하면 없다고 답한다.
1강의 서버는 없는 책을 달라고 해도 200 OK 라고 했다. 3강에서 그게 가장 흔한 잘못이라고 했다. Spring 은 아무것도 시키지 않아도 그 약속을 지킨다. 게다가 에러 내용을 JSON 으로 담아 보내준다.
이 응답에서 세 가지를 짚고 넘어간다.
| 보이는 것 | 무슨 뜻인가 | 어디서 배웠나 |
|---|---|---|
HTTP/1.1 404 뒤가 비었다 | 이유 문구를 아예 안 보냈다. 계약은 숫자에 있으니 없어도 된다 | 3강 |
Transfer-Encoding: chunked | `Content-Length` 가 없다. 길이를 미리 안 세고 조각내서 보낸다 | 2강 |
Content-Type: application/json | 에러도 JSON 으로 온다. 사람이 아니라 프로그램이 읽을 응답이다 | 10강 |
8080 이 이미 쓰이고 있으면
1강에서 이 에러를 봤다. 스택 트레이스가 여덟 줄 쏟아졌었다.
1강의 서버
직접 짠 것
java.net.BindException: Address already in use: bindat sun.nio.ch.Net.bind0(Native Method)at java.net.ServerSocket.<init>(...)- 무엇을 하라는 말은 없다
Spring Boot
방금 띄운 것
APPLICATION FAILED TO STARTDescription:Web server failed to start. Port 8080 was already in use.Action: Identify and stop the process that's listening on port 8080- 무엇을 하라는지 알려준다
포트를 바꾼다
설정은 application.properties 에 적는다.
spring.application.name=bookstore
server.port=9090
다시 띄우면 로그가 바뀐다.
- INFO --- Tomcat started on port 9090 (http) with context path '/'
server.port 라는 이름을 우리가 정한 적이 없다. Spring 이 미리 알고 있는 이름이다. 안 적으면 8080, 적으면 그 값. 이런 식으로 "안 정하면 이걸로 한다"가 수백 개 쌓여 있는 것을 자동설정이라고 부른다. 8강에서 본다.
정리하면
손으로 짜던 것이 전부 사라졌다. 그런데 무엇 하나 우리가 만들지 않았다.
- 01
start.spring.io가 뼈대를 만들어준다자바 파일 두 개, 설정 파일 몇 개. 그중 하나는 테스트다.
- 02
gradlew덕분에 Gradle 을 설치하지 않는다적힌 버전을 알아서 내려받는다. 그래서
gradle이 아니라./gradlew다../gradlew bootRun - 03
main은 끝나는데 프로그램은 안 죽는다run은 돌아온다. 톰캣이 띄운 비데몬 스레드가 남아 있어서 JVM 이 나가지 않는 것이다.SpringApplication.run(BookstoreApplication.class, args); - 04
톰캣이 1강의 ServerSocket 자리에 있다
우리는 new ServerSocket(8080) 을 쓴 적이 없다. 누가 만들었는지가 다음 Part 의 주제다.
- 05
컨트롤러가 없으면 404 를 돌려준다
3강의 약속을 아무것도 시키지 않아도 지킨다. 이유 문구는 아예 비워서 보낸다.
Part 1 이 끝났다.
1강에서 서버가 왜 안 죽는지 봤고, 2강에서 오가는 글자를 뜯어봤고, 3강에서 그 글자에 담긴 약속을 배웠다. 그리고 4강에서 그 셋을 전부 대신 해주는 물건을 띄웠다.
이제 남은 질문은 하나다. 기동 로그의 그 1.44초 동안 무슨 일이 있었나.
우리는 new Tomcat() 을 쓴 적이 없다. new DispatcherServlet() 도 쓴 적이 없다. 그런데 로그에는 둘 다 만들어졌다고 적혀 있다.
누가 만들었나. 자바 편 15강에서 OrderService 가 Notifier 를 스스로 만들지 않고 생성자로 받았던 것을 기억하는지. 그때 "밖에서 만들어서 넣어준다"고 했고, 그 밖이 누구인지는 말하지 않았다.
5강에서 그 이름을 알려준다.