
eGovFrame VSCode Initializr로 Web 템플릿과 Boot 템플릿을 생성해보면서 자연스럽게 Maven과 Gradle도 함께 살펴보게 되었다.
처음에는 Maven과 Gradle을 단순히 “프로젝트 빌드할 때 쓰는 도구” 정도로만 생각했지만, 학습을 진행하며 둘은 같은 목적을 가지지만 접근 방식이 꽤 다르다는 것을 알 수 있었다.
또 eGovFrame 5.0 기반 프로젝트를 실행하려면 Tomcat 버전도 중요했다. 기존 Java EE의 javax.* 패키지가 Jakarta EE의 jakarta.* 패키지로 바뀌면서, Tomcat 10 이상이 필요해졌기 때문이다.
Java 프로젝트를 만든다고 해서 단순히 .java 파일만 작성하면 끝나는 것은 아니다.
실제 프로젝트에서는 다음과 같은 작업이 필요하다.
이런 많은 작업을 매번 개발자가 직접 하면 매우 번거로울 뿐만 아니라 실수도 많이 발생할 것이다. 그래서 등장한 것이 빌드 자동화 도구다.
빌드 자동화 도구는 프로젝트 설정 파일을 읽고, 필요한 라이브러리를 가져오고, 소스 코드를 컴파일하고, 최종 결과물인 JAR 또는 WAR 파일을 만들어준다. 그 대표적인 빌드 도구가 바로 Java 생태계에서는 Maven과 Gradle인 것이다.
Maven은 Java 프로젝트를 빌드하고 관리하기 위한 대표적인 빌드 자동화 도구다. Maven은 pom.xml 파일을 중심으로 동작한다.
pom.xml은 Project Object Model의 약자로, 프로젝트의 정보를 담고 있는 설정 파일이다. 여기에는 다음과 같은 정보가 들어간다.
예를 들어 eGovFrame Web 템플릿의 pom.xml에는 다음과 같은 설정이 들어 있다.
<groupId>com.egovframework</groupId>
<artifactId>egovweb</artifactId>
<packaging>war</packaging>
<version>1.0.0</version>
Maven의 핵심은 정해진 규칙과 라이프사이클이다. Maven은 프로젝트가 어떤 흐름으로 빌드되어야 하는지 미리 정해두었다.
대표적인 라이프사이클 단계는 다음과 같다.
validate: 프로젝트가 올바른지 확인한다.
compile: Java 소스 코드를 컴파일한다.
test: 테스트 코드를 실행한다.
package: 컴파일된 결과를 JAR이나 WAR로 패키징한다.
verify: 패키징 결과를 검증한다.
install: 로컬 Maven 저장소에 설치한다.
deploy: 원격 저장소에 배포한다.
예를 들어 mvn clean package 명령어를 실행하면 Maven은 아래와 같은 작업들을 순서대로 수행한다.
eGovFrame Web 템플릿에서는 packaging이 war이므로 최종 결과물로 .war 파일이 생성되고, Spring Boot Boot 템플릿에서는 실행 가능한 .jar 파일이 생성된다.
eGovFrame Web 템플릿의 pom.xml에서 중요한 부분은 부모 POM이다.
<parent>
<groupId>org.egovframe.web</groupId>
<artifactId>egovframe-web-config-parent</artifactId>
<version>5.0.0</version>
</parent>
부모 POM은 여러 라이브러리의 버전을 한 곳에서 관리해준다.
예를 들어 아래와 같은 의존성이 있다고 해보자.
<dependency>
<groupId>org.egovframe.rte</groupId>
<artifactId>egovframe-rte-ptl-mvc</artifactId>
</dependency>
여기에는 <version>이 없다. 그런데도 빌드가 된다. 그 이유는 부모 POM에서 해당 라이브러리의 버전을 관리해주기 때문이다.
이 방식의 장점은 명확하다. 여러 라이브러리의 버전을 프로젝트마다 직접 맞추지 않아도 된다는 것이다. 부모 POM에서 검증된 버전 조합을 관리하면, 하위 프로젝트는 그 조합을 그대로 사용할 수 있다. 이런 방식은 eGovFrame처럼 여러 실행환경 라이브러리와 Spring, MyBatis, Jakarta 관련 라이브러리를 함께 사용하는 프로젝트에서 매우 중요하다.
Maven 의존성에는 scope라는 개념이 있다. 예를 들어 Web 템플릿의 pom.xml을 보면 다음과 같은 설정이 나온다.
<dependency>
<groupId>jakarta.servlet.jsp</groupId>
<artifactId>jakarta.servlet.jsp-api</artifactId>
<scope>provided</scope>
</dependency>
여기서 provided는 컴파일할 때는 필요하지만, 실행할 때는 외부 환경이 제공한다는 뜻이다.
예를 들어 Servlet API나 JSP API는 코드를 컴파일할 때 필요하다. 하지만 실제 실행 시점에는 Tomcat이 이미 Servlet/JSP 관련 기능을 제공한다. 따라서 이런 라이브러리를 WAR 파일 안에 또 넣으면 충돌이 발생할 수 있다.
정리하면 다음과 같다.
| scope | 의미 |
|---|---|
compile | 컴파일과 실행 모두 필요. 기본값 |
provided | 컴파일할 때만 필요. 실행 환경이 제공 |
test | 테스트할 때만 필요 |
장점: 규칙이 명확하다는 것이다. 프로젝트 구조, 빌드 단계, 의존성 관리 방식이 어느 정도 정해져 있다. 그래서 처음 보는 Maven 프로젝트라도 pom.xml을 보면 프로젝트가 어떤 라이브러리를 사용하고, 어떤 방식으로 빌드되는지 파악하기 쉽다. 또한 eGovFrame처럼 여러 라이브러리 버전을 안정적으로 맞춰야 하는 프로젝트에서는 부모 POM을 통한 버전 관리가 큰 장점이 된다.
단점: XML 기반이라 설정이 길고 장황해질 수 있다. 또한 정해진 라이프사이클에 맞춰 동작하기 때문에 복잡한 커스텀 빌드 로직을 작성하기에는 유연성이 부족할 수 있다. 즉, Maven은 안정적이고 예측 가능하지만, 복잡한 빌드 자동화를 세밀하게 제어하고 싶을 때는 답답하게 느껴질 수 있다.
Gradle도 Maven처럼 Java 프로젝트를 빌드하고 의존성을 관리하는 빌드 자동화 도구다. 하지만 Maven과는 접근 방식이 다르다.
Maven이 pom.xml이라는 XML 파일에 정해진 구조로 빌드를 선언한다면, Gradle은 build.gradle 또는 build.gradle.kts 파일에 Groovy나 Kotlin DSL로 빌드 로직을 작성한다.
예를 들어 Gradle에서는 의존성을 다음처럼 작성한다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
}
보다시피 Maven보다 문법이 짧고, 조건문이나 반복문 같은 프로그래밍적 표현도 사용할 수 있다. Gradle의 핵심은 태스크(Task)다. Gradle은 빌드를 여러 태스크의 조합으로 바라본다.
예를 들어 다음과 같은 태스크가 있을 수 있다.
compileJava
processResources
test
jar
bootRun
build
Gradle은 이 태스크들 사이의 의존 관계를 분석해서 필요한 작업만 실행한다.
Gradle 프로젝트를 보면 다음과 같은 파일들을 Gradle Wrapper라고 한다.
gradlew
gradlew.bat
gradle/wrapper/gradle-wrapper.properties
Gradle Wrapper가 있으면 로컬 PC에 Gradle이 설치되어 있지 않아도 프로젝트에서 지정한 Gradle 버전으로 빌드할 수 있다.
예를 들어 ./gradlew build 명령어를 실행하면 Wrapper가 필요한 Gradle 버전을 확인하고, 없으면 자동으로 다운로드한 뒤 빌드를 수행한다.
이 방식은 팀 프로젝트에서 매우 편리한데, 팀원마다 로컬에 설치된 Gradle 버전이 다르면 빌드 결과가 달라질 수 있는 부분들을 Wrapper를 사용하면 모든 팀원이 프로젝트에서 지정한 동일한 Gradle 버전으로 빌드할 수 있다.
CI/CD 환경에서도 마찬가지다. 빌드 서버에 Gradle을 따로 설치하지 않아도 ./gradlew build만 실행하면 프로젝트가 요구하는 버전으로 빌드된다.
Gradle은 Maven보다 빌드 성능 측면에서 유리한 기능을 많이 제공한다.
대표적으로 다음과 같은 기능이 있다.
Incremental Build: 이전 빌드 이후 변경된 부분만 다시 빌드하는 방식이다. 예를 들어 소스 코드가 바뀌지 않았다면 굳이 같은 컴파일 작업을 다시 하지 않을 수 있다.
Build Cache: 이미 수행한 빌드 결과를 저장해두었다가 같은 입력에 대해 다시 사용할 수 있게 해준다. 동일한 입력으로 동일한 결과가 나오는 작업이라면, Gradle은 매번 새로 작업하지 않고 캐시된 결과를 재사용할 수 있다.
Gradle Daemon: Gradle 실행 프로세스를 백그라운드에 유지해서 매번 새로 JVM을 띄우는 비용을 줄여준다. 이런 기능 덕분에 규모가 큰 프로젝트에서는 Gradle이 Maven보다 빠르게 느껴질 수 있다. 다만 Gradle은 자유도가 높은 만큼 처음 배우는 사람에게는 Maven보다 어렵게 느껴질 수 있다. 빌드가 어떻게 동작하는지 이해하려면 태스크, 플러그인, DSL, Wrapper 구조를 함께 이해해야 하기 때문이다.
Maven과 Gradle은 모두 Java 프로젝트를 빌드하고 의존성을 관리하는 도구다. 둘의 목표는 비슷하지만 철학이 다르다.
Maven은 정해진 규칙을 따르는 선언형 빌드 도구에 가깝고, Gradle은 필요한 빌드 흐름을 코드처럼 구성할 수 있는 유연한 빌드 도구에 가깝다.
| 비교 항목 | Maven | Gradle |
|---|---|---|
| 설정 파일 | pom.xml | build.gradle, build.gradle.kts |
| 설정 언어 | XML | Groovy DSL 또는 Kotlin DSL |
| 빌드 철학 | 정해진 라이프사이클 중심 | 태스크 그래프 중심 |
| 구조 | 규칙이 강하고 예측 가능 | 자유도가 높고 유연함 |
| 의존성 관리 | 안정적이고 명확함 | Maven 저장소 사용 가능, 표현이 간결함 |
| 성능 | 단순하고 안정적이지만 반복 빌드가 느릴 수 있음 | 캐시, 증분 빌드, 데몬으로 빠른 빌드 가능 |
| 커스텀 빌드 | XML 플러그인 설정이 장황할 수 있음 | 코드처럼 작성 가능 |
| 학습 난이도 | 초보자에게 상대적으로 이해하기 쉬움 | DSL과 태스크 개념을 익혀야 함 |
| 공공/기업 레거시 | 많이 사용됨 | 점점 증가 중 |
| eGovFrame 공식 구조 | Maven 중심으로 이해하기 쉬움 | 별도 BOM/플러그인 설정 이해 필요 |
pom.xml 기반 의존성 관리와 부모 POM 구조를 이해하고 싶은 경우
이번 실습을 통해 느낀 점은, eGovFrame 프로젝트를 처음 공부한다면 Maven부터 이해하는 것이 좋다는 것이다.
이유는 간단하다. eGovFrame Web 템플릿의 pom.xml은 부모 POM을 통해 버전을 관리하고, 전자정부 저장소와 실행환경 의존성을 명확하게 보여준다. 따라서 Maven 구조를 이해하면 eGovFrame 프로젝트가 어떤 라이브러리에 의존하고, 어떤 방식으로 빌드되는지 파악하기 쉽다.
Gradle도 충분히 사용할 수 있다. 하지만 Maven의 parent POM 상속 구조를 Gradle에서는 BOM 형태로 따로 설정해야 할 수 있다.
예를 들어 Gradle에서는 다음과 같이 플랫폼 의존성을 명시하는 방식이 필요할 수 있다.
dependencies {
implementation platform('org.egovframe.boot:egovframe-dependencies:5.0.0')
implementation 'org.egovframe.rte:egovframe-rte-ptl-mvc'
}
즉, Gradle은 유용하지만 eGovFrame 공식 구조를 이해하지 않은 상태에서 바로 변환하면 오히려 더 헷갈릴 수 있다.
eGovFrame 5.0 기반 프로젝트를 실행할 때는 Tomcat 버전을 반드시 확인해야 한다. 결론부터 말하면 Tomcat 10 버전을 사용해야 한다. 왜 그럴까? 그냥 최신 버전이니까? 이유는 Tomcat 10부터 Java 웹 표준 API의 패키지 이름이 javax.*에서 jakarta.*로 바뀌었기 때문이다.
예전 Java EE 기반 프로젝트에서는 다음과 같은 패키지를 사용했다.
import javax.servlet.http.HttpServletRequest;
하지만 Jakarta EE 기반 프로젝트에서는 패키지명이 다음처럼 바뀌었다.
import jakarta.servlet.http.HttpServletRequest;
즉, 단순히 이름만 바뀐 것이 아니라 Java 웹 생태계의 표준 패키지 네임스페이스가 javax.*에서 jakarta.*로 이동한 것이다. 이 차이 때문에 Tomcat 버전도 중요하다.
Tomcat 9 이하 버전은 기본적으로 javax.* 기반이고, Tomcat 10 이상부터 Jakarta 네임스페이스를 지원한다. eGovFrame 5.0은 Spring Framework 6 / Spring Boot 3 계열 흐름에 맞춰 Jakarta EE 기반으로 전환된 프로젝트이기 때문에 Tomcat 9 이하에서 실행하면 관련 클래스를 찾지 못하는 문제가 발생할 수 있는 것이다.
정리하면 다음과 같다.
| 구분 | 주요 네임스페이스 | 대표 Tomcat 버전 |
|---|---|---|
| Java EE 기반 | javax.* | Tomcat 9 이하 |
| Jakarta EE 기반 | jakarta.* | Tomcat 10 이상 |
근데 왜 굳이 javax.*에서 jakarta.*로 이동시킨걸까? 이 부분은 프로그래밍 얘기는 아니므로 그냥 그러려니 하고 넘어가자.
원래 처음 Java EE는 Sun Microsystems가 주도했고, 이후 Sun을 Oracle이 인수하면서 Oracle이 관리하게 됐는데 시간이 지나면서 Java EE의 발전 속도가 느려졌고, 오픈소스 커뮤니티 중심으로 더 빠르게 발전시키기 위해 Oracle이 Java EE를 Eclipse Foundation으로 이관했다. 이때 Java EE가 Jakarta EE로 이름이 바뀐 것이다.
자세히 말하자면, Eclipse 재단이 Java EE 기술을 넘겨받았지만, Java라는 이름과 javax.* 네임스페이스에 대한 권리는 Oracle이 가지고 있었기 때문에 Eclipse 재단은 Jakarta EE를 발전시키면서도 기존의 javax.* 패키지 이름을 계속 자유롭게 변경하거나 확장할 수 없었던 것이다.
쉽게 말해, 새 표준은 Eclipse 재단이 관리하지만, 패키지 이름은 Oracle의 Java 상표권과 얽혀 있는 약간 애매한 상황이 돼 버린건데, 그래서 결국 Jakarta EE는 기존 javax.*를 버리고 새로운 네임스페이스를 선택한 것이다.
Spring Framework 6, Spring Boot 3부터는 Jakarta EE 기반으로 전환돼서 Spring Boot 3 기반 프로젝트에서는 더 이상 예전 javax.servlet.*, javax.validation.*, javax.persistence.*를 쓰지 않고, jakarta.*를 사용한다.
eGovFrame 5.0도 이 흐름을 따라가면서 Jakarta 기반으로 전환된 것이다. 그래서 앞서 생성한 eGovFrame 5.0 Web 템플릿에서도 jakarta.servlet, jakarta.validation 같은 의존성이 등장하고, 외부 Tomcat도 Jakarta를 지원하는 Tomcat 10 이상이 필요해진 것이다.

Tomcat을 실행할 때는 Tomcat 폴더의 bin 디렉터리로 이동한 뒤 startup.sh를 실행하면 된다.
cd ~/dev/apache-tomcat-10.1.47/bin
chmod +x startup.sh
./startup.sh
정상 실행되면 아래와 같은 로그가 출력된다.
byeonguk@bagbyeong-ug-ui-MacBookPro bin % ./startup.sh
Using CATALINA_BASE: /Users/byeonguk/dev/apache-tomcat-10.1.47
Using CATALINA_HOME: /Users/byeonguk/dev/apache-tomcat-10.1.47
Using CATALINA_TMPDIR: /Users/byeonguk/dev/apache-tomcat-10.1.47/temp
Using JRE_HOME: /Users/byeonguk/Library/Java/JavaVirtualMachines/temurin-24.0.2/Contents/Home
Using CLASSPATH: /Users/byeonguk/dev/apache-tomcat-10.1.47/bin/bootstrap.jar:/Users/byeonguk/dev/apache-tomcat-10.1.47/bin/tomcat-juli.jar
Using CATALINA_OPTS:
Tomcat started.