Backend 06

Spring Boot 첫 실행, 막연함 끝!

프로젝트를 열었는데 src/main/java, resources, application.yml, build.gradle이 각각 무슨 역할인지 흐릿하다.

근거 · 교안 p41-p51

Spring Boot 첫 실행, 막연함 끝! 대표 이미지

1장: 시작점: main() 메서드와 SpringApplication.run()의 첫 만남

솔라의 손가락이 노트북 트랙패드 위에서 멈칫했다. 화면에는 방금 막 생성한 스프링 부트 프로젝트의 기본 구조가 펼쳐져 있었다. 그중에서도 DemoApplication.java 파일의 내용은 너무나도 간결해서 오히려 수상할 정도였다.

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

솔라는 옆에서 조용히 책을 읽고 있던 언니 루나에게 말을 걸었다. “언니, 잠깐 이것 좀 봐봐. 분명 ‘main() 메서드를 실행하면 스프링 애플리케이션이 시작된다’고 배웠거든. 그래서 실행 버튼을 눌렀지.”

솔라가 실행 버튼을 다시 누르자, 화면 하단의 콘솔 창에 순식간에 수십 줄의 로그가 폭포수처럼 쏟아져 내렸다. 복잡한 영문 메시지들이 정신없이 올라가다 이내 멈췄다.

“그런데… 이상해. main 메서드 안에는 SpringApplication.run() 이 한 줄밖에 없잖아. 내가 짠 코드는 사실상 없는데, 대체 이 많은 일들은 누가 다 하는 거야? 그냥 main을 실행했을 뿐인데, 마치 거대한 기계가 통째로 움직이는 것 같아.”

솔라의 목소리에는 막연함이 묻어 있었다. main 메서드 실행이 모든 것의 시작과 끝이라고 생각했던 단순한 믿음이, 눈앞의 복잡한 현실 앞에서 흔들리고 있었다.

루나는 읽던 책을 덮고 솔라의 화면을 들여다보았다. 복잡하게 얽힌 콘솔 로그와 그 모든 것을 촉발시킨 단 한 줄의 코드를 번갈아 보던 루나가 말했다.

“그럼 우리가 직접 표식을 남겨보는 건 어때? 우리의 명령이 어디까지 가고, 스프링의 영역은 어디서부터 시작되는지.”

“표식?”

“응. SpringApplication.run()이 호출되기 바로 전에, 우리가 알아볼 수 있는 메시지를 콘솔에 출력해보는 거야. 아주 간단한 걸로.”

루나는 솔라의 키보드를 가리켰다. 솔라는 잠시 생각하더니 이내 고개를 끄덕이며 코드를 수정하기 시작했다.

// ... (이전 코드 생략)

public class DemoApplication {

    public static void main(String[] args) {
        System.out.println("--- 솔라의 코드는 여기까지! ---");
        SpringApplication.run(DemoApplication.class, args);
    }

}

솔라는 main 메서드의 첫 줄에 자신의 표식을 남겼다. 그리곤 다시 실행 버튼을 눌렀다. 이번에는 콘솔 창을 뚫어져라 쳐다보았다.

결과는 즉각적이고 명확했다.

--- 솔라의 코드는 여기까지! ---

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.x.x)

... (이후 수많은 로그들) ...

가장 먼저 출력된 것은 솔라가 남긴 ‘--- 솔라의 코드는 여기까지! ---’라는 메시지였다. 그리고 바로 다음 줄부터 익숙한 스프링 부트의 로고와 함께, 이전과 똑같은 복잡한 로그들이 다시금 화면을 채웠다.

그 순간 솔라의 머릿속에서 무언가 ‘탁’ 하고 제자리를 찾는 소리가 났다.

“아…! 알겠다. main 메서드는 그냥 시작 신호를 보내는 역할만 하는 거였어. 내 코드는 System.out.println을 실행하고, 곧바로 SpringApplication.run()을 호출하면서 끝난 거네. 이 모든 복잡한 과정은 run이라는 저 메서드 안에서 벌어지는 일이었구나.”

솔라는 방금 전까지 하나의 덩어리로 보였던 ‘애플리케이션 실행’ 과정이, 사실은 두 개의 뚜렷한 단계로 나뉘어 있음을 깨달았다. 첫 번째는 main 메서드라는 출발점에서 run이라는 ‘스프링 앱 시작 호출기’를 부르는 아주 짧은 과정. 그리고 두 번째는 그 호출기가 넘겨받아 온갖 준비를 마치고 실제 애플리케이션을 구동하는, 보이지 않는 거대한 과정.

이전에는 ‘main()을 실행하면 앱이 시작된다’는 문장을 그저 ‘버튼을 누르면 끝’이라는 의미로 받아들였다. 하지만 이제는 달랐다. ‘main() 메서드 안의 SpringApplication.run()이 호출되면서, 비로소 스프링 컨텍스트를 초기화하는 거대한 작업이 시작된다’는 구체적인 장면으로 이해할 수 있었다. 마치 오케스트라의 지휘자가 단상에 올라 지휘봉을 드는 순간(main)과, 그 지휘봉이 움직이기 시작하며 모든 악기가 일제히 연주를 시작하는 순간(run)이 다른 것처럼 말이다.

솔라는 명확해진 그림에 만족하며 고개를 끄덕였다. 하지만 곧 새로운 궁금증이 고개를 들었다.

“좋아, 진짜 시작은 run 메서드가 연다는 건 알겠어. 그런데 저 똑똑한 run 메서드는 대체 뭘 가지고 저렇게 화려한 무대를 만드는 거지? 웹 서버를 띄우는 부품이나 여러 설정 정보 같은 것들은 다 어디서 가져오는 걸까?”

2장: 기반 다지기: build.gradle로 필요한 부품 준비하기

솔라는 노트북 화면에 펼쳐진 프로젝트 탐색기 창을 가만히 들여다보았다. 왼쪽에는 파일과 폴더들이 나무처럼 가지를 뻗고 있었다. 어제 SpringApplication.run()의 정체를 밝혀낸 후, 그 똑똑한 지휘자가 연주에 필요한 악기들을 어디서 가져오는지 궁금해졌다. 그녀의 시선은 DemoApplication.java 파일에서 시작해, 프로젝트 전체 구조를 훑으며 헤매고 있었다.

.gitignore, HELP.md 같은 파일들은 아닌 것 같고… 시선이 build.gradle 파일에 잠시 머물렀다. 솔라는 파일을 클릭해 열어보았다. 알 수 없는 코드들이 나타났지만, dependencies 라는 단어가 눈에 띄었다. 하지만 이내 고개를 저었다. ‘이건 그냥 프로젝트 만들 때 필요한 라이브러리 목록 적어두는 빌드 스크립트잖아. 앱이 실행되는 순간에 직접 관여하는 건 아닐 거야.’ 그녀는 build.gradle이 애플리케이션 실행과는 거리가 먼, 그저 사전 준비 단계의 도구일 뿐이라고 치부했다. 그녀의 궁금증, 즉 run 메서드가 사용하는 ‘부품’의 출처는 여전히 미궁 속에 있었다.

그때, 거실에서 들어온 루나가 솔라의 화면을 슬쩍 보더니 멈춰 섰다. 솔라가 막 닫으려던 build.gradle 파일을 손으로 가리켰다.

“오케스트라의 악보와 악기 목록은 보통 어디에 있지?”

“악보랑 악기 목록? 그야 당연히 지휘자가 공연 전에 다 확인하고 챙기는 거 아냐?”

솔라는 루나의 뜬금없는 비유에 고개를 갸웃하며 대답했다. 루나는 고개를 끄덕이며 솔라가 보고 있던 build.gradle 파일의 dependencies 블록을 콕 집었다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

“바로 여기야. 스프링 부트라는 지휘자가 사용할 악기들의 목록. 솔라 네가 방금 웹 애플리케이션을 구동하는 데 필요한 ‘웹 서버’라는 아주 중요한 악기를 어디서 가져오냐고 했었지?”

루나의 말에 솔라는 다시 화면을 바라봤다. ‘spring-boot-starter-web’. 이름만 봐도 웹과 관련된 무언가임이 틀림없었다.

“이게… 웹 서버를 가져온다는 뜻이야? 그냥 이름이 적혀있는 것뿐인데?”

솔라의 목소리에는 여전히 의심이 가득했다. 단순한 텍스트가 어떻게 거대한 웹 서버를 만들어낸단 말인가.

“그럼, 이 목록에서 가장 중요한 악기를 잠시 빼보면 어떻게 될까? 오케스트라가 연주할 곡이 ‘웹’인데, ‘웹’이라는 핵심 악기군을 통째로 빼버리는 거지.”

루나는 솔라에게 implementation 'org.springframework.boot:spring-boot-starter-web' 라인을 주석 처리해보라고 제안했다. 솔라는 반신반의하며 키보드를 움직였다.

dependencies {
    // implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

솔라는 한 줄을 주석 처리하고, IDE의 안내에 따라 Gradle 프로젝트 변경사항을 다시 불러왔다. 그리고 어제와 똑같이 DemoApplication.javamain 메서드를 실행했다.

결과는 극적이었다.

어제까지만 해도 수십 줄의 로그를 뿜어내며 위용을 자랑하던 콘솔 창이 이번에는 너무나도 조용했다. ‘--- 솔라의 코드는 여기까지! ---’ 라는 메시지가 뜨는가 싶더니, 몇 줄의 기본 정보만 출력되고는 이내 프로세스가 끝나버렸다. 어제 봤던 스프링 부트 로고도, 내장 톰캣 서버가 시작된다는 메시지도 전부 사라졌다. 애플리케이션은 시작하자마자 아무것도 하지 않고 그대로 종료되었다.

“어… 아무 일도 안 일어나.”

솔라는 허탈하게 중얼거렸다. 거대한 기계가 통째로 멈춰버린 듯한 정적. 바로 그 순간, 솔라의 머릿속에 흩어져 있던 조각들이 맞춰졌다.

“아! build.gradle은 그냥 단순한 목록이 아니었구나. 이게 바로 SpringApplication.run 메서드가 들고 가는 ‘부품 주문서’였던 거야! 주문서에 ‘웹 스타터’가 빠져 있으니, run 메서드가 아무리 똑똑해도 웹 서버라는 부품을 조립할 수 없었던 거지.”

이전까지 솔라에게 build.gradle은 그저 빌드라는 특정 시점에만 작동하는 별개의 도구였다. 하지만 이제는 달랐다. build.gradle은 애플리케이션이 시작되는 바로 그 순간, SpringApplication.run에게 ‘우리가 만들 애플리케이션은 이런 부품들로 구성되어야 합니다’라고 알려주는 핵심적인 설계도이자 지침서였다. 단순한 빌드 설정을 넘어, 애플리케이션의 정체성을 규정하는 역할, 즉 ‘부품 수급 담당자’의 역할을 하고 있었던 것이다.

솔라는 재빨리 주석을 풀고 다시 프로젝트를 동기화했다. 그리고 다시 애플리케이션을 실행했다.

--- 솔라의 코드는 여기까지! ---
...
Tomcat started on port(s): 8080 (http)
...
Started DemoApplication in ...

언제 그랬냐는 듯, 콘솔 창은 다시 활기차게 로그를 쏟아냈고, 익숙한 Tomcat started on port(s): 8080 메시지가 선명하게 나타났다. 이제 이 복잡한 로그들이 더 이상 혼란스럽지 않았다. build.gradle이라는 주문서에 따라 부품들이 정확히 배달되고, run 메서드가 그 부품들을 차곡차곡 조립하는 과정으로 보였다.

명확해진 역할 관계에 만족하며 로그를 훑어보던 솔라의 눈이 한 곳에 멈췄다.

“좋아, ‘웹’이라는 악기를 build.gradle에서 가져온다는 건 알겠어. 그런데 저기 8080이라는 포트 번호는 누가 알려준 거지? 이 주문서에는 그런 세세한 연주법까지는 안 적혀 있는 것 같은데.”

3장: 설정 조율: application.yml로 앱의 성격 정하기

솔라의 시선은 노트북 화면의 한 지점에 고정되어 있었다. 방금 전까지 활기차게 로그를 쏟아내던 콘솔 창. 그 마지막 줄에 선명하게 찍힌 Tomcat started on port(s): 8080 이라는 문구가 희미하게 빛나고 있었다. build.gradle이 ‘웹 서버’라는 악기를 가져온다는 사실은 알아냈지만, 그 악기가 왜 하필 ‘8080’이라는 음을 내는지, 그 지시는 어디에도 없었다.

솔라의 손가락이 프로젝트 탐색기 창을 분주하게 오갔다. 그녀의 직감은 src/main/resources 라는 폴더를 가리키고 있었다. ‘java’ 폴더에는 자바 코드만 있을 테니, 이런 설정 정보는 ‘resources’에 있을 확률이 높았다. 폴더를 열자 application.yml 이라는 파일이 보였다. 솔라는 망설임 없이 파일을 클릭했다.

그러나 파일은 텅 비어 있었다. 주석 몇 줄을 제외하고는 아무런 내용도 없었다.

솔라는 허탈한 웃음을 지었다. ‘역시. 이건 그냥 예시 파일일 뿐이야. 8080 포트는 아마 spring-boot-starter-web 안에 그냥 박혀있는 기본값이겠지. 바꿀 수 없는 값이거나, 바꾸려면 아주 복잡한 일을 해야 할 거야.’ 그녀는 이 파일이 애플리케이션 실행에 아무런 영향을 주지 못하는 장식품일 뿐이라고 잠정 결론 내렸다.

그때, 솔라의 어깨너머로 화면을 보던 루나가 나지막이 말했다.

“지휘자가 악기 목록(build.gradle)을 받았어. 그런데 연주회장이 바뀌면 어떨까? 매번 똑같은 방식으로 연주할까, 아니면 그날의 공연장 음향에 맞게 연주를 조금씩 조율할까?”

솔라는 루나의 말뜻을 바로 이해하지 못하고 되물었다. “공연장 음향이라니?”

spring-boot-starter-web이 가져온 톰캣 서버는 기본적으로 8080 포트에서 연주하도록 훈련된 훌륭한 악기야. 하지만 오늘은 8080 포트를 다른 프로그램이 쓰고 있거나, 우리가 특별히 다른 포트에서 연주하고 싶을 수도 있잖아. 바로 그럴 때, 이 공연장만의 특별 요청 사항을 적어두는 곳이 필요하지.”

루나는 텅 비어 있는 application.yml 파일을 손가락으로 가리켰다.

“이 파일이 바로 그 ‘특별 요청서’야. 지금은 비어 있으니, 톰캣이라는 악기는 늘 하던 대로 기본 설정인 8080 포트에서 연주를 시작한 거고.”

솔라는 여전히 미심쩍은 표정이었다. 텅 빈 파일이 무슨 힘이 있다는 말인가.

“그럼, 우리가 직접 요청서를 작성해보는 건 어때? ‘오늘 연주는 8081 포트에서 부탁합니다’ 하고 말이야.”

루나의 말에 솔라는 반신반의하며 application.yml 파일에 키보드로 내용을 입력하기 시작했다.

server:
  port: 8081

들여쓰기로 구조를 맞춘, 간단한 텍스트였다. 솔직히 말해, 이걸로 무언가 바뀔 거라고는 전혀 기대되지 않았다.

솔라는 파일을 저장하고, 의심의 눈초리를 거두지 않은 채 다시 애플리케이션 실행 버튼을 눌렀다. 그녀의 눈은 평소보다 더 집요하게 콘솔 창을 좇았다. 수많은 로그가 빠르게 스크롤되며 화면을 채웠다. 그리고…

...
Tomcat started on port(s): 8081 (http)
...

선명했다. 8080이었던 숫자가 거짓말처럼 8081로 바뀌어 있었다.

“와…!”

솔라의 입에서 짧은 탄성이 터져 나왔다. 텅 비어 있던 파일에 추가한 몇 글자가, 거대한 스프링 부트 애플리케이션의 동작을 정말로 바꿔버린 것이다.

순간, 모든 조각이 제자리를 찾았다. build.gradle이 ‘어떤 부품을 사용할지’ 결정하는 설계도라면, application.yml은 그 부품들을 ‘어떻게 동작시킬지’ 세부적으로 지시하는 조율 지침서였다. 스프링 부트는 애플리케이션을 시작할 때(SpringApplication.run), 이 약속된 위치에 있는 application.yml 파일을 먼저 샅샅이 읽어본다. 그리고 그 안에 담긴 설정들을 기본값보다 우선하여 적용하는 것이다. 마치 지휘자가 연주 시작 전, 보면대 위에 놓인 특별 메모를 확인하고 연주를 조율하는 것처럼.

이 파일은 더 이상 쓸모없는 장식품이 아니었다. 애플리케이션의 성격을 규정하고 외부 환경에 맞춰 유연하게 변신시키는 ‘앱 환경 설정 주입’의 핵심 통로였다.

솔라는 바뀐 포트 번호가 찍힌 콘솔 로그를 보며 만족스럽게 고개를 끄덕였다. 이제 build.gradle이라는 부품 주문서와 application.yml이라는 세부 튜닝 가이드의 역할이 명확하게 구분되었다. 숨겨진 흐름이 하나 더 밝혀진 기분이었다.

그런데 문득, 시선이 다시 프로젝트 탐색기 창으로 향했다.

“좋아, 이제 부품도 있고, 부품을 어떻게 쓸지에 대한 지침도 알겠어. 그런데… 진짜 중요한 ‘내 코드’는 어디에 두는 거지? 내 비즈니스 로직, 그러니까 이 애플리케이션이 실제로 해야 할 일들은 다 어디에 작성하고, 스프링은 그걸 어떻게 알아채는 걸까?”

4장: 코드와 자원의 보금자리: src/main/java와 resources

노트북 화면 속 프로젝트 구조가 솔라의 눈에는 마치 잘 정돈된 빈 선반장처럼 보였다. build.gradle이라는 선반에는 ‘웹 서버’라는 이름표가 붙은 부품 상자가 놓여 있고, application.yml이라는 작은 서랍에는 ‘8081 포트 사용’이라는 메모가 붙어 있었다. 부품도, 세부 지침도 준비되었다. 하지만 정작 가장 중요한, 이 프로젝트의 존재 이유가 될 ‘내용물’을 어디에 두어야 할지 막막했다.

솔라의 시선은 src 폴더에 머물렀다. 그 안에는 main 폴더가 있었고, 다시 그 안에 javaresources라는 두 개의 칸이 나란히 있었다. 그녀는 나름의 논리로 폴더의 용도를 추측했다. ‘이건 쉽네. java 폴더에는 자바 코드를 넣고, resources 폴더에는 그 외의 자원들을 넣으면 되겠지.’ 너무나 명쾌한 구분이라 더 이상의 고민은 불필요해 보였다. 솔라는 이 폴더들이 그저 개발자가 파일을 찾기 쉽게 정리해두는, 일종의 예의 바른 관습이라고 생각했다. 프레임워크가 이 폴더 구조에 특별한 의미를 부여하리라고는 상상하지 못했다.

가장 먼저 간단한 환영 페이지라도 하나 만들어보고 싶었다. HTML 파일이니 당연히 java 폴더는 아닐 터. 그녀는 resources 폴더를 오른쪽 클릭했지만, ‘새 파일’을 누르기 직전 손가락을 멈췄다. 그냥 이 폴더 안에 파일을 툭 던져두면, 스프링 부트가 대체 어떻게 알고 그걸 웹페이지로 보여준단 말인가? application.yml처럼 이름이 약속된 것도 아닌데. 막연한 불안감이 발목을 잡았다.

그때, 솔라의 고민을 지켜보던 루나가 조용히 다가와 화면을 들여다봤다.

“새로 이사 온 집에 가구를 들인다고 생각해 봐. 침대는 침실에, 소파는 거실에, 식기는 주방에 두겠지? 아무 데나 두지 않는 이유는 뭘까?”

“그야… 그게 제자리고, 그래야 집이 제대로 돌아가니까. 주방에 침대가 있으면 요리를 할 수가 없잖아.”

솔라는 당연한 걸 묻는다는 듯 대답했다. 루나는 고개를 끄덕이며 javaresources 폴더를 차례로 가리켰다.

“스프링 부트의 프로젝트 구조도 똑같아. 그냥 개발자 편하라고 만든 관습이 아니야. 스프링 부트라는 집주인이 ‘자바 코드는 이 방에’, ‘설정 파일이나 웹페이지 같은 자원들은 저 방에’ 하고 정해둔 약속이지. 집주인은 정해진 방만 확인하거든.”

루나는 resources 폴더를 가리킨 채 말을 이었다. “특히 웹페이지처럼 외부에서 바로 접근해야 하는 파일들은, 집주인이 손님을 맞이하기 위해 특별히 마련해 둔 ‘현관 바로 앞 응접실’ 같은 곳에 둬야 해.”

“응접실?”

“응. resources 폴더 안에 static이라는 이름으로 새 폴더를 만들어 봐. 그게 바로 스프링 부트가 약속한 정적 자원들의 응접실이야.”

솔라는 루나의 말에 따라 resources 폴더 안에 static이라는 이름의 폴더를 생성했다. 그리고 그 안에 index.html 파일을 만들었다. 파일 내용은 아주 간단하게 작성했다.

<!DOCTYPE html>
<html>
<head>
    <title>환영합니다!</title>
</head>
<body>
    <h1>솔라의 첫 웹페이지!</h1>
</body>
</html>

코드를 작성하면서도 의심은 가시지 않았다. 고작 폴더 하나를 더 만들고 그 안에 파일을 넣었을 뿐이다. 이걸로 정말 웹페이지가 뜰까? 어떤 자바 코드도, 어떤 설정도 추가하지 않았는데.

솔라는 반신반의하며 애플리케이션을 다시 실행했다. 콘솔 창에 Tomcat started on port(s): 8081 (http) 메시지가 뜨는 것을 확인하고, 브라우저를 열어 주소창에 http://localhost:8081을 입력했다. 엔터 키를 누르는 순간, 화면에 나타난 것은…

솔라의 첫 웹페이지!

굵은 글씨가 선명하게 그녀를 반겼다.

“세상에…!”

솔라는 저도 모르게 탄성을 내뱉었다. 마법 같았다. 그녀가 한 일이라고는 약속된 위치에 파일을 가져다 둔 것뿐이었다. 그 순간, 폴더 구조에 대한 그녀의 생각이 완전히 뒤집혔다.

src/main/javasrc/main/resources. 이 폴더들은 단순한 정리용 수납함이 아니었다. 스프링 부트라는 거대한 자동화 기계에 내리는 ‘보이지 않는 명령’이었다. resources/static 폴더에 파일을 넣는 행위는 ‘이 폴더 안의 파일들은 웹 서버의 루트 경로에 그대로 노출시켜줘’라는 지시와 같았다. src/main/java 폴더에 클래스를 작성하는 것 역시 ‘이 안에 있는 자바 코드들을 읽어서 내 애플리케이션의 핵심 로직으로 삼아줘’라는 약속이었던 것이다.

이것이 바로 스프링 부트가 똑똑하게 일하는 방식의 또 다른 비밀, ‘표준 디렉토리 구조’였다. 개발자가 따라야 할 최소한의 약속, 일종의 ‘표준 파일 위치 지침’을 제공하고, 그 약속을 지키기만 하면 나머지는 프레임워크가 알아서 처리해주는 것이다.

솔라는 이제 프로젝트의 모든 핵심 요소들이 각자의 역할을 가지고 있음을 명확히 이해했다. 지휘봉(main), 악기 목록(build.gradle), 연주 지침서(application.yml), 그리고 배우들의 무대 위 지정석(src 폴더 구조)까지.

모든 부품들이 제자리에 있는 듯했다. 하지만 솔라의 머릿속에는 마지막 퍼즐 조각이 남아 있었다.

“이제 각자 뭘 하는지는 알겠어. 그런데 아직도 전체 그림이 한눈에 들어오지는 않아. 내가 실행 버튼을 누르는 순간부터, 이 모든 것들이 어떤 순서로, 어떻게 서로 말을 주고받으면서 이 거대한 오케스트라를 만들어내는 거지? 그 전체 흐름이 궁금해.”

5장: 큰 그림: Spring Boot 앱의 시작부터 실행까지

솔라의 책상 위에는 그녀의 고민이 고스란히 담긴 스케치 한 장이 놓여 있었다. 네모난 상자 네 개가 엉성하게 그려져 있고, 각 상자 안에는 main(), build.gradle, application.yml, src 폴더 라고 적혀 있었다. 상자들 사이를 잇는 화살표들은 제각각 뻗어 나가다 길을 잃고, 어떤 것은 다른 화살표와 엉켜 풀 수 없는 매듭처럼 보였다. 각 부품의 역할은 이제 알지만, 이들이 모여 하나의 작품을 만드는 과정은 여전히 뒤죽박죽이었다. 각자 독립적으로 움직이는 부품들이라는 생각에서 벗어나지 못하고 있었다.

그녀는 실행 버튼을 누르는 순간을 떠올렸다. 그 찰나의 순간, 이 모든 것들이 어떻게 조화롭게 협력하여 거대한 애플리케이션을 깨우는지, 그 숨겨진 흐름을 눈으로 보고 싶었다. 하지만 머릿속에서 상상하는 그림은 스케치북 위의 그림처럼 혼란스럽기만 했다.

그때, 솔라의 어깨너머로 엉망인 다이어그램을 본 루나가 말했다.

“악기, 악보, 특별 지시서, 연주자들까지 다 모였는데, 아직 리허설을 안 해본 것 같네.”

“리허설?”

“응. 지금까지는 이미 완성된 오케스트라의 각 파트를 뜯어본 거라면, 이번엔 우리가 직접 지휘자가 되어서 백지상태부터 오케스트라를 조직해보는 거야. 아주 작은 것부터.”

루나는 솔라의 IDE(통합 개발 환경)를 가리켰다. 솔라는 잠시 고민하다, 이내 루나의 의도를 알아채고는 ‘새 프로젝트 생성’ 버튼을 눌렀다. 잠시 후, 텅 비어 있지만 익숙한 구조를 가진 새로운 스프링 부트 프로젝트가 화면에 나타났다.

“자, 지휘자님. 연주를 시작하려면 가장 먼저 무엇을 해야 하지?” 루나가 물었다.

솔라는 망설임 없이 main() 메서드가 있는 Application.java 파일을 열었다. 그녀의 손가락이 SpringApplication.run() 라인을 가리켰다. “여기서 시작 신호를 보내야지. 지휘봉을 드는 순간.” 이것이 첫 번째 단계였다.

“좋아, 신호는 준비됐어. 다음은?”

솔라는 잠시 생각에 잠겼다가, 프로젝트 탐색기에서 build.gradle 파일을 클릭했다. “오케스트라에 어떤 악기가 필요한지 알려줘야 해.” 파일 안의 dependencies 블록에 spring-boot-starter-web이 적혀 있는 것을 확인했다. “웹 서버라는 악기는 이미 준비 목록에 있네.” 이것이 두 번째, 부품을 수급하는 단계였다.

“악기도 준비됐고. 오늘 연주회의 특별한 요청 사항은?”

이번엔 src/main/resources 폴더 아래의 application.yml 파일 차례였다. 솔라는 파일을 열고 직접 타이핑했다.

server:
  port: 9000

“오늘은 9000번 포트에서 연주할 거야.” 그녀의 목소리에는 이전과 다른 확신이 담겨 있었다. 이것이 세 번째, 세부 설정을 주입하는 단계였다.

“이제 무대 위에 오를 연주자가 필요하네.”

루나의 말에, 솔라는 마지막으로 src/main/java 폴더 아래, 자신의 패키지 안에 새로운 자바 클래스 파일을 만들었다. 간단한 컨트롤러 코드를 작성했다.

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String sayHello() {
        return "오케스트라 준비 완료!";
    }
}

“‘헬로 컨트롤러’라는 이름의 연주자, 무대 위로.” 이것이 네 번째, 실제 로직을 배치하는 단계였다. 이제 모든 조각이 제자리에 놓였다.

솔라는 깊게 숨을 들이쉬고, main 메서드의 실행 버튼을 눌렀다. 이전과는 전혀 다른 마음이었다. 더 이상 마법을 기다리는 관객이 아니었다. 모든 과정을 설계한 지휘자의 심정이었다.

콘솔 창에 로그가 올라오기 시작했다.

  1. main() 메서드가 실행되며 SpringApplication.run()이 호출되었다. (지휘봉이 움직였다)
  2. 스프링 부트 로고가 뜨고, 컨텍스트가 로딩되기 시작했다. build.gradle을 참조하여 spring-boot-starter-web 의존성을 확인하고, 내장 톰캣 서버를 준비했다. (악기들이 제자리를 잡았다)
  3. 이어서 application.yml 파일이 스캔되었다. 로그 한가운데, Tomcat started on port(s): 9000 (http)라는 문구가 선명하게 찍혔다. (특별 지시서가 반영되었다)
  4. 마지막으로 src/main/java 아래의 컴포넌트들이 스캔되었고, 솔라가 방금 만든 HelloController가 빈(Bean)으로 등록되었다. (연주자가 무대에 올랐다)

모든 과정이 끝나고 애플리케이션이 성공적으로 시작되었다는 메시지가 떴다. 솔라는 브라우저를 열어 http://localhost:9000/hello로 접속했다. 화면에는 “오케스트라 준비 완료!”라는 메시지가 그녀를 반겼다.

그 순간, 솔라의 머릿속을 어지럽히던 모든 조각들이 하나의 선명한 흐름으로 이어졌다. 그것은 더 이상 독립된 부품들의 집합이 아니었다. 실행 버튼을 누르는 순간부터 시작되는, 명확한 순서를 가진 ‘협력의 과정’이었다.

솔라는 책상 위에 있던 엉망인 스케치를 치우고, 새 종이를 꺼내 다시 그리기 시작했다.

main() 실행build.gradle (의존성 확인)application.yml (설정 적용)src/main/* (코드/리소스 스캔)애플리케이션 구동

화살표는 더 이상 엉키지 않았다. 왼쪽에서 오른쪽으로, 위에서 아래로, 명확하고 질서 정연한 하나의 길을 그리고 있었다.

“이제 막연하지 않아.”

솔라가 나지막이 말했다.

“처음 프로젝트를 열었을 때 보이던 파일들이, 이제는 그냥 파일이 아니라, 애플리케이션이 시작되는 순서대로 놓인 이정표처럼 보여.”

그녀는 방금 자신이 직접 만든 프로젝트의 화면을 바라보았다. 그곳에는 더 이상 알 수 없는 파일들의 목록이 아닌, 하나의 잘 짜인 오케스트라가 첫 연주를 기다리고 있었다. 막연함이 걷힌 자리에, 이제 무엇을 어디서부터 시작해야 할지 아는 개발자의 자신감이 들어서고 있었다.