설정과 배포 — 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 하나로 뜨는 데까지 왔다.
읽어줘서 고맙다.