학습 자료

설정과 배포 — 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강에서 봤던 그 둘, 지금 다시
파일4강 때지금
bookstore-0.0.1-SNAPSHOT-plain.jar1,509 바이트15,076 바이트
bookstore-0.0.1-SNAPSHOT.jar약 19 MB54 MB
우리 코드는 15KB 다. 나머지 54MB 는 우리가 안 쓴 것이다.

4강에서는 자바 파일이 하나뿐이었다. 그 뒤로 스무 강을 지나며 클래스가 늘었고, JPA 와 H2 와 검증을 넣었다. 우리 코드는 15KB 로 자랐고, 함께 들어간 짐은 19MB 에서 54MB 가 됐다.

큰 쪽을 열어본다.

unzip -l build/libs/bookstore-0.0.1-SNAPSHOT.jar
jar 안
  • bookstore-0.0.1-SNAPSHOT.jar약 54 MB
    • BOOT-INF/classes/우리가 쓴 것 — 15 KB
      • kr/dcodelab/bookstore/Book.class · BookController.class · BookService.class …
      • application.propertiesschema.sql 도 함께 들어간다
    • BOOT-INF/lib/라이브러리 jar 72개
      • hibernate-core-7.4.1.Final.jar15 MB — 제일 크다
      • tomcat-embed-core-11.0.22.jar3.6 MB — 웹서버가 여기 들어 있다
      • h2-2.4.240.jar2.7 MB — 데이터베이스도
    • org/springframework/boot/loader/이 jar 를 여는 코드
jar 안에 jar 가 72개 들어 있다. 톰캣도 데이터베이스도 그 안에 있다.

웹서버가 파일 안에 들어 있다. 4강에서 톰캣이 우리 프로그램 안에서 뜬다고 했는데, 그 톰캣이 여기 있다.

자바 편 21강은 jar cf myapp.jar . 로 끝났다. 그건 우리 클래스만 묶는다. plain.jar 15KB 가 딱 그것이다. 저 54MB 는 그것에 필요한 모든 것을 다 넣은 물건이다.

main 이 우리 것이 아니다

MANIFEST.MF 를 열면 이상한 것이 보인다.

META-INF/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-ClassJarLauncher 다. 우리 BookstoreApplication 이 아니다.

자바 편 2강에서 배운 것이 있다. javaMain-Classmain 부터 실행한다. 그러니 java -jar 를 치면 먼저 도는 것은 Spring 의 코드다.

왜 이런 짓을 하나. 자바는 원래 jar 안의 jar 를 못 읽는다. BOOT-INF/lib/ 에 든 72개를 그냥은 못 쓴다. JarLauncher 가 그것을 읽을 줄 아는 클래스로더를 만들고, 그다음에 우리 Start-Class 를 부른다.

java -jar 를 쳤을 때
  1. java -jarMANIFEST 의 Main-Class 를 본다
  2. JarLauncherjar 안의 jar 72개를 읽는 클래스로더를 만든다
  3. BookstoreApplication.mainStart-Class. 우리 코드가 여기서 시작한다
  4. SpringApplication.run1강부터 봐온 그 줄이다
자바 편 2강의 "main 이 시작점"은 여전히 맞다. 다만 그 main 이 우리 것이 아닐 뿐이다.

빈 폴더에서 띄운다

이제 진짜로 해본다. 빈 폴더를 만들고 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.portSERVER_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강에서 약속을 배웠다. 문법이 아니라 관습이고, 어겨도 컴파일 에러는 안 나지만 어기면 아무도 안 쓴다고 했다.

우리가 그 약속을 지켰는지 본다.

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강에서 전부 없앴다지켰다
3강에서 우리 서버는 무엇을 물어도 같은 대답만 하는 물건이었다.

이 시리즈를 관통한 것이 넷 있었다.

네 갈래
  1. 01

    누가 객체를 만들어주는가

    1강에서는 new 로 직접 만들었다. 5강에서 컨테이너가 대신 만들었고, 6강에서 생성자로 받았고, 7강에서 표식만 붙이면 찾아갔다. 23강에서 그 자리에 가짜를 끼웠다. 생성자로 받았기 때문에 가능했다.

  2. 02

    경계를 넘으면 타입이 사라진다

    2강에서 HTTP 는 글자 몇 줄이었다. 10강에서 객체가 JSON 이 됐고, 11강에서 JSON 이 객체가 됐다. 19강에서 그 경계에 문을 세웠고, 21강에서 에러도 JSON 이 됐다.

  3. 03

    실패를 어떻게 알리는가

    3강에서 상태코드를 배웠다. 17강에서 Optional 이 "없을 수 있다"를 타입에 적어 건넸고, 20강이 400 을, 21강이 404돌려줬다.

  4. 04

    객체는 어디에 남는가

    13강에서 껐다 켜면 사라졌다. 14강에서 데이터베이스가 생겼고, 15강에서 id 가 정체성이 됐다. 18강에서 영속성 컨텍스트를 봤고, 22강에서 트랜잭션이 그것을 지켰다.

네 갈래가 각자 다른 강의에서 시작해 다른 강의에서 닫혔다.

가장 이상했던 순간을 기억하는가. 18강이다.

우리는 여섯 강에 걸쳐 == 로 비교하지 말라고 배웠다. 자바 편 4강이 "참조형은 상자에 주소를 담는다"고 했고, 자바 편 14강이 Objectequals 를 열어 "그냥 == 다"라고 했다. 15강에서는 데이터베이스가 그 답을 거부하고 번호를 요구했다.

그런데 18강에서 같은 책을 두 번 꺼냈더니 ==true 였다. 영속성 컨텍스트라는 책상 위에서만은 주소가 다시 답이 됐다.

새로 배운 것으로 옛것을 뒤집은 게 아니다. 자바 편 4강의 "주소" 하나로 전부 설명됐다.

그래서 무엇을 할 수 있게 됐나

Book 하나로 여기까지 왔다. 실무 도메인은 한 번도 안 꺼냈다. 그런데 지금 우리 프로젝트에는 이런 것들이 있다.

24강의 프로젝트
  • kr.dcodelab.bookstore우리가 쓴 것 전부
    • BookControllerHTTP 를 안다. 도메인 규칙은 모른다
    • BookService규칙을 안다. HTTP 는 모른다. 트랜잭션이 여기 있다
    • BookRepository인터페이스 한 줄. 구현은 Spring 이 만든다
    • Book표의 한 행. setter 가 없다
    • BookRequest · BookResponse · ErrorResponse손님과 주고받는 종이. 전부 record
    • ApiExceptionHandler실패를 응답으로 옮긴다
    • BookServiceTest · BookApiTest기계가 대신 봐준다
게시판을 만들든 주문을 만들든 뼈대는 이것이다. Book 자리에 다른 것이 들어갈 뿐이다.

이 구조에서 Book 을 다른 것으로 바꾸면 그게 여러분의 첫 API 다.

고생했다. 스물네 강을 끝까지 읽었다면 Spring Boot 로 CRUD REST API 를 만들 수 있다. 모르는 표식이 나와도 괜찮다. 7강에서 봤듯이 그건 누군가 읽어주기를 기다리는 글자일 뿐이고, 여러분은 이제 누가 그것을 읽는지 안다.

new 를 안 쓰게 된 이유부터 시작해서, jar 하나로 뜨는 데까지 왔다.

읽어줘서 고맙다.

설정과 배포 — jar 하나로 뜬다 — 디코드랩(DCODELAB)