설정과 배포 — jar 하나로 뜬다
Article
지금까지 서버를 띄운 방법은 하나뿐이었다.
./gradlew bootRun
이건 개발자의 명령이다. 소스가 있어야 하고, Gradle 이 있어야 하고, 인터넷도 필요하다. 남의 컴퓨터에서 이걸 칠 수는 없다.
빌드한다.
./gradlew build
- > Task :test
- BUILD SUCCESSFUL in 19s
- 8 actionable tasks: 8 executed
Task :test 가 보인다. 23강에서 만든 테스트가 빌드에 끼어 있다. 테스트가 하나라도 깨지면 빌드가 실패하고 산출물이 안 나온다. 우리가 시킨 적 없는데 그렇게 되어 있다.
두 개의 jar
build/libs 를 본다.
| 파일 | 4강 때 | 지금 |
|---|---|---|
bookstore-0.0.1-SNAPSHOT-plain.jar | 1,509 바이트 | 15,076 바이트 |
bookstore-0.0.1-SNAPSHOT.jar | 약 19 MB | 약 54 MB |
4강에서는 자바 파일이 하나뿐이었다. 그 뒤로 스무 강을 지나며 클래스가 늘었고, JPA 와 H2 와 검증을 넣었다. 우리 코드는 15KB 로 자랐고, 함께 들어간 짐은 19MB 에서 54MB 가 됐다.
큰 쪽을 열어본다.
unzip -l build/libs/bookstore-0.0.1-SNAPSHOT.jar
- bookstore-0.0.1-SNAPSHOT.jar
- BOOT-INF/classes/
- kr/dcodelab/bookstore/
- application.properties
- BOOT-INF/lib/
- hibernate-core-7.4.1.Final.jar
- tomcat-embed-core-11.0.22.jar
- h2-2.4.240.jar
- org/springframework/boot/loader/
- BOOT-INF/classes/
웹서버가 파일 안에 들어 있다. 4강에서 톰캣이 우리 프로그램 안에서 뜬다고 했는데, 그 톰캣이 여기 있다.
자바 편 21강은 jar cf myapp.jar . 로 끝났다. 그건 우리 클래스만 묶는다. plain.jar 15KB 가 딱 그것이다. 저 54MB 는 그것에 필요한 모든 것을 다 넣은 물건이다.
main 이 우리 것이 아니다
MANIFEST.MF 를 열면 이상한 것이 보인다.
- Main-Class: org.springframework.boot.loader.launch.JarLauncher
- Start-Class: kr.dcodelab.bookstore.BookstoreApplication
- Spring-Boot-Version: 4.1.0
- Spring-Boot-Classes: BOOT-INF/classes/
- Spring-Boot-Lib: BOOT-INF/lib/
Main-Class 가 JarLauncher 다. 우리 BookstoreApplication 이 아니다.
자바 편 2강에서 배운 것이 있다. java 는 Main-Class 의 main 부터 실행한다. 그러니 java -jar 를 치면 먼저 도는 것은 Spring 의 코드다.
왜 이런 짓을 하나. 자바는 원래 jar 안의 jar 를 못 읽는다. BOOT-INF/lib/ 에 든 72개를 그냥은 못 쓴다. JarLauncher 가 그것을 읽을 줄 아는 클래스로더를 만들고, 그다음에 우리 Start-Class 를 부른다.
- java -jarMANIFEST 의 Main-Class 를 본다
- JarLauncherjar 안의 jar 72개를 읽는 클래스로더를 만든다
- BookstoreApplication.mainStart-Class. 우리 코드가 여기서 시작한다
- SpringApplication.run1강부터 봐온 그 줄이다
빈 폴더에서 띄운다
이제 진짜로 해본다. 빈 폴더를 만들고 jar 하나만 복사한다. 소스도 Gradle 도 없다.
mkdir deploy
cp build/libs/bookstore-0.0.1-SNAPSHOT.jar deploy/app.jar
cd deploy
java -jar app.jar
- Tomcat started on port 8080 (http) with context path '/'
- Started BookstoreApplication in 5.514 seconds
- $ curl -o /dev/null -w '%{http_code}' http://localhost:8080/books
- 200
떴다. 파일 하나다. 자바만 깔려 있으면 어디서든 이 한 줄로 뜬다.
폴더를 다시 보면 data/ 가 생겨 있다. 14강에서 만든 H2 파일이다. 설정도 jar 안에 함께 들어갔기 때문이다.
그런데 설정이 jar 안에 있다
여기서 4강이 밟았던 함정이 돌아온다. 그때 이런 일이 있었다.
application.properties 에서 포트를 바꾸고 java -jar 옛jar 로 띄웠더니 여전히 8080 이었다. 설정 파일이 빌드할 때 jar 안으로 복사되기 때문이다.
배포에서는 이게 더 큰 문제가 된다. 내 컴퓨터와 서버는 설정이 다르다. 포트도, 데이터베이스 주소도 다르다.
설정을 바꾸겠다고 jar 를 다시 만들 수는 없다. 그건 테스트를 통과한 그 물건이 아니다.
Spring Boot 는 밖에서 덮어쓰게 해준다. 세 가지 방법이 있고, 셋 다 jar 를 안 건드린다.
1. 명령줄에 적는다
java -jar app.jar --server.port=9090
- Tomcat started on port 9090 (http) with context path '/'
2. 환경변수로 준다
SERVER_PORT=9091 java -jar app.jar
- Tomcat started on port 9091 (http) with context path '/'
server.port 가 SERVER_PORT 가 됐다. 점은 밑줄로, 글자는 대문자로 바꾸면 된다.
3. jar 옆에 파일을 둔다
deploy 폴더에 application.properties 를 만든다. jar 안이 아니라 옆이다.
server.port=9000
spring.datasource.url=jdbc:h2:file:./prod-data/bookdb
java -jar app.jar
- H2 console available at '/h2-console'. Database available at 'jdbc:h2:file:./prod-data/bookdb'
- Tomcat started on port 9000 (http) with context path '/'
포트도 데이터베이스 위치도 바뀌었다. prod-data/ 폴더가 새로 생겼다. jar 는 한 글자도 안 고쳤다.
24강을 마치며
3강에서 약속을 배웠다. 문법이 아니라 관습이고, 어겨도 컴파일 에러는 안 나지만 어기면 아무도 안 쓴다고 했다.
우리가 그 약속을 지켰는지 본다.
| 약속 | 우리 코드 | 지켰나 |
|---|---|---|
GET 은 가져간다 | GET /books · GET /books/1 | 지켰다 |
POST 는 새로 만든다 | 201 + Location 을 돌려준다 (13강) | 지켰다 |
PUT 은 통째로 바꾼다 | changeTo 하나로 바꾼다 (17강) | 지켰다 |
DELETE 는 지운다 | 204, 본문 없음 (13강) | 지켰다 |
POST 만 멱등하지 않다 | 두 번 보내면 두 권 (13강 실측) | 지켰다 |
4xx 는 손님 잘못 | 빈 제목 → 400 (20강) · 없는 책 → 404 (21강) | 지켰다 |
5xx 는 서버 잘못 | 12강 · 17강의 500 을 21강에서 전부 없앴다 | 지켰다 |
이 시리즈를 관통한 것이 넷 있었다.
- 01
누가 객체를 만들어주는가
1강에서는
new로 직접 만들었다. 5강에서 컨테이너가 대신 만들었고, 6강에서 생성자로 받았고, 7강에서 표식만 붙이면 찾아갔다. 23강에서 그 자리에 가짜를 끼웠다. 생성자로 받았기 때문에 가능했다. - 02
경계를 넘으면 타입이 사라진다
2강에서 HTTP 는 글자 몇 줄이었다. 10강에서 객체가 JSON 이 됐고, 11강에서 JSON 이 객체가 됐다. 19강에서 그 경계에 문을 세웠고, 21강에서 에러도 JSON 이 됐다.
- 03
실패를 어떻게 알리는가
3강에서 상태코드를 배웠다. 17강에서
Optional이 "없을 수 있다"를 타입에 적어 건넸고, 20강이400을, 21강이404를 돌려줬다. - 04
객체는 어디에 남는가
13강에서 껐다 켜면 사라졌다. 14강에서 데이터베이스가 생겼고, 15강에서
id가 정체성이 됐다. 18강에서 영속성 컨텍스트를 봤고, 22강에서 트랜잭션이 그것을 지켰다.
가장 이상했던 순간을 기억하는가. 18강이다.
우리는 여섯 강에 걸쳐 == 로 비교하지 말라고 배웠다. 자바 편 4강이 "참조형은 상자에 주소를 담는다"고 했고, 자바 편 14강이 Object 의 equals 를 열어 "그냥 == 다"라고 했다. 15강에서는 데이터베이스가 그 답을 거부하고 번호를 요구했다.
그런데 18강에서 같은 책을 두 번 꺼냈더니 == 가 true 였다. 영속성 컨텍스트라는 책상 위에서만은 주소가 다시 답이 됐다.
새로 배운 것으로 옛것을 뒤집은 게 아니다. 자바 편 4강의 "주소" 하나로 전부 설명됐다.
그래서 무엇을 할 수 있게 됐나
Book 하나로 여기까지 왔다. 실무 도메인은 한 번도 안 꺼냈다. 그런데 지금 우리 프로젝트에는 이런 것들이 있다.
- kr.dcodelab.bookstore
- BookController
- BookService
- BookRepository
- Book
- BookRequest · BookResponse · ErrorResponse
- ApiExceptionHandler
- BookServiceTest · BookApiTest
이 구조에서 Book 을 다른 것으로 바꾸면 그게 여러분의 첫 API 다.
고생했다. 스물네 강을 끝까지 읽었다면 Spring Boot 로 CRUD REST API 를 만들 수 있다. 모르는 표식이 나와도 괜찮다. 7강에서 봤듯이 그건 누군가 읽어주기를 기다리는 글자일 뿐이고, 여러분은 이제 누가 그것을 읽는지 안다.
new 를 안 쓰게 된 이유부터 시작해서, jar 하나로 뜨는 데까지 왔다.
읽어줘서 고맙다.