[Spring In Action] 프로젝트 구조와 빌드 프로세스 심층 분석: JVM, Classpath, 그리고 의존성 관리

1. 문제 제기 (Introduction & Problem Statement)

관찰 현상 또는 질문

Spring Boot를 처음 시작할 때 Spring Initializr를 사용하면 실행 가능한 프로젝트가 마법처럼 뚝딱 만들어진다. 하지만, "왜 Application.java 파일은 반드시 최상위 패키지에 있어야 하는가?", "도대체 mvnw는 로컬에 설치된 Maven과 무엇이 다른가?", "설정 파일에서 흔히 보는 classpath: 접두사는 실제로 컴퓨터의 어떤 경로를 가리키는가?"와 같은 질문에 명확히 답하지 못하는 경우가 많다.

탐구 목표

본 아티클에서는 Spring Boot 프로젝트의 초기화 과정(pom.xml), 디렉토리 구조의 기술적 의도, 그리고 빌드 도구와 JVM, ClassLoader의 상호작용을 분석한다. 특히 소스 코드가 컴파일되어 target 폴더에 저장되고, 이것이 JVM의 Classpath에 로드되어 실행되기까지의 일련의 과정을 시각적으로 정리하여 프레임워크 뒤단의 동작 원리를 이해하는 것을 목표로 한다.


2. 기술 분석 및 핵심 원리 (Technical Deep Dive)

2-1. pom.xml과 의존성 관리의 핵심 원리

Spring Boot의 빌드 설정 파일인 pom.xml은 단순한 라이브러리 목록이 아니라, 버전 호환성을 보장하는 핵심 장치이다.

1. 버전 관리의 주체: <parent>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.4.0</version>
</parent>

<parent> 태그는 프로젝트의 부모 라이브러리를 지정한다. 여기에 선언된 3.4.0 버전은 수십, 수백 개의 하위 라이브러리(Spring Core, Jackson, Logback 등) 간의 검증된 버전 조합(Bill of Materials, BOM)을 정의한다. 덕분에 개발자는 하위 의존성을 추가할 때 버전을 명시하지 않아도 충돌 없는 최적의 버전을 자동으로 가져오게 된다.

2. 기능 단위의 묶음: Starter

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

spring-boot-starter-web은 단일 라이브러리가 아니라, 웹 개발에 필요한 의존성 그룹이다. 이를 추가하는 순간 Spring MVC(웹 프레임워크), Tomcat(내장 WAS), Jackson(JSON 처리), Logback(로깅)이 전이 의존성(Transitive Dependencies)으로 함께 설치된다.

2-2. 프로젝트 구조와 @ComponentScan의 제약 사항

Spring Initializr가 생성하는 표준 디렉토리 구조에는 명확한 기술적 규칙이 숨어 있다.

Application.java의 위치 선정 규칙
가장 중요한 규칙은 메인 클래스(Application.java)가 반드시 루트 패키지에 위치해야 한다는 점이다.

  • 올바른 위치: com.example.demo.Application.java
  • 동작 원리: @SpringBootApplication 어노테이션 내부에는 @ComponentScan이 포함되어 있다. 이 컴포넌트 스캔은 자신이 위치한 패키지와 그 하위 패키지만을 탐색 대상으로 삼는다.
  • 주의사항: 만약 com.example.demo 패키지 밖인 com.example.other에 클래스를 만들면, 스캔 범위 밖이므로 빈(Bean)으로 등록되지 않는 문제가 발생한다.

리소스 디렉토리 역할

  • src/main/resources/application.properties: 키-값 쌍의 설정 저장소 (예: server.port=8080).
  • src/main/resources/static/: 정적 리소스 (이미지, CSS, JS) 저장소. 브라우저에서 직접 접근 가능하다.
  • src/main/resources/templates/: Thymeleaf 등 서버 사이드 템플릿 엔진 파일 저장소.

2-3. 빌드 도구 vs JVM vs ClassLoader: 실행의 여정

우리가 작성한 .java 파일이 실제로 실행되기까지는 빌드(Build) -> 로드(Load) -> 실행(Execute)의 단계를 거친다.

구분 역할 및 동작 원리 핵심 키워드
Build Tool
(Maven/Gradle)
사람이 읽을 수 있는 .java 코드를 기계어에 가까운 .class로 컴파일하고, 필요한 라이브러리(.jar)를 다운로드하여 패키징한다. 컴파일된 결과물은 target 폴더에 저장된다. pom.xml, mvnw, Compile
ClassLoader 빌드 도구가 만들어낸 하드디스크 상의 .class 파일들을 찾아 메모리(Method Area)에 적재(Load)한다. 이때 파일을 찾는 경로 목록이 바로 Classpath이다. Loading, Classpath
JVM 메모리에 로드된 .class 바이트코드를 읽어 CPU가 이해할 수 있는 기계어로 변환 및 실행한다. JVM은 .java를 모르며 오직 .class만 인식한다. Execution, Runtime

Maven Wrapper (mvnw)
개발자마다 로컬에 설치된 Maven 버전이 다를 수 있다. mvnw는 프로젝트에 포함된 스크립트로, 해당 프로젝트가 필요로 하는 특정 버전의 Maven을 자동으로 다운로드하여 빌드를 수행함으로써 환경 간 일관성을 보장한다.

2-4. Classpath와 리소스 로딩 순서

JVM이 클래스나 리소스 파일을 찾을 때 사용하는 경로의 목록인 Classpath는 다음 순서로 구성된다.

  1. 소스 코드 컴파일 결과: src/main/java -> target/classes
  2. 리소스 파일: src/main/resources -> target/classes에 함께 복사됨
  3. 외부 라이브러리: pom.xml에 명시된 .jar 파일들

따라서 classpath:templates/home.html이라는 경로는 물리적으로 "프로젝트 폴더 내의 templates 폴더"를 의미하는 것이 아니라, "JVM이 로드한 클래스 경로 목록 중에서 templates/home.html을 찾아라"라는 명령이다.


3. 결론 및 고찰 (Conclusion & Takeaways)

핵심 요약

  • pom.xml의 부모 태그는 수많은 라이브러리의 버전 호환성을 관리하는 BOM 역할을 하며, Starter는 기능 단위로 의존성을 묶어 제공한다.
  • 메인 클래스의 위치@ComponentScan의 탐색 시작점이 되므로 반드시 루트 패키지에 위치해야 모든 빈이 정상 등록된다.
  • 빌드와 실행의 분리: 빌드 도구는 파일을 준비(target 폴더 생성)하는 역할이고, JVM은 준비된 파일을 메모리에 올려(ClassLoader) 실행하는 역할이다.

기술적 통찰 및 나의 생각

이번 학습을 통해 IDE의 'Run' 버튼 뒤에서 일어나는 과정을 구체적으로 파악했다. 특히 "JVM은 .java 파일을 모른다"는 사실과 "Classpath는 단순한 폴더 경로가 아니라 JVM이 리소스를 탐색하는 우선순위 목록"이라는 개념이 명확해졌다. classpath: 접두사를 사용할 때 더 이상 막연하게 경로를 찍는 것이 아니라, 빌드 결과물(target/classes) 내부의 구조를 상상하며 경로를 지정할 수 있게 되었다.

향후 과제 / 추가 질문

  • Gradle은 Maven과 비교했을 때 의존성 관리 방식과 빌드 속도 면에서 어떤 구체적인 차이가 있는가?
  • Spring Boot의 jar 패키징 방식(Fat Jar)은 일반적인 jar와 달리 어떻게 내장 톰캣과 라이브러리를 모두 포함하여 실행되는가?