순수 Java 프로젝트에서 시작해 Gradle로 빌드 과정을 자동화하고, 실행 가능한 JAR를 만든 뒤 JUnit 테스트를 적용하는 과정을 학습한다.
1. 학습 목표
이 프로젝트에서는 다음 흐름을 직접 확인한다.
순수 Java 코드 작성
→ javac와 java로 직접 컴파일·실행
→ IntelliJ에서 컴파일·실행
→ Gradle 프로젝트로 전환
→ Gradle로 빌드·실행
→ 실행 가능한 JAR 생성
→ JUnit 테스트 작성·실행
주요 학습 내용은 다음과 같다.
.java,.class, JVM의 관계- IntelliJ와 Gradle의 역할 차이
- Gradle 표준 프로젝트 구조
build,run,java -jar의 차이- JUnit과 Gradle의 관계
- 테스트 실패가 빌드에 미치는 영향
2. 순수 Java 프로젝트 작성

처음에는 Gradle 없이 Java 파일만 작성한다.
src/
├── Calculator.java
└── Main.java
이 실습에서는 패키지 구조보다 컴파일과 실행 과정에 집중하기 위해 package를 사용하지 않는다.
Calculator.java
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
Main.java
public class Main {
public static void main(String[] args) {
Calculator calculator = new Calculator();
System.out.println(calculator.add(2, 3));
}
}
Main.main()은 프로그램의 실행 진입점이다.
public static void main(String[] args)
3. javac, java로 직접 실행
터미널에서 src 폴더로 이동한다.
cd src
Java 소스 코드를 컴파일한다.
javac Calculator.java Main.java
컴파일이 완료되면 .class 파일이 생성된다.
src/
├── Calculator.java
├── Calculator.class
├── Main.java
└── Main.class

프로그램을 실행한다.
java Main
실행 결과:
5
java Main에서 .class 확장자는 작성하지 않는다. JVM이 현재 클래스 경로에서 Main.class를 찾아 실행한다.
전체 과정은 다음과 같다.
.java 소스 코드
→ javac로 컴파일
→ .class 바이트코드 생성
→ java 명령으로 JVM에서 실행
실습 후 생성된 .class 파일을 삭제한다.
rm Calculator.class Main.class
4. IntelliJ에서 실행
Git 저장소나 기존 폴더를 IntelliJ에서 바로 열면, IntelliJ는 처음에 어느 폴더가 Java 소스 영역인지 알지 못할 수 있다.
따라서 순수 Java 단계에서는 다음 설정이 필요하다.
- Project SDK에 JDK를 지정한다.
src폴더를 Sources Root로 지정한다.Main.main()옆의 실행 버튼을 누른다.Run 'Main.main()'을 선택한다.
Sources Root는 IntelliJ에 다음 내용을 알려주는 설정이며, 이 폴더 아래의 파일을 Java 소스 코드로 취급하고 컴파일한다.


Sources Root를 지정하면 다음 IDE 기능을 사용할 수 있다.
- Java Class 생성
- 자동완성
- 문법 오류 검사
- 패키지 인식
- 리팩터링
- 실행 버튼을 이용한 컴파일 및 실행
IntelliJ도 본질적으로는 다음 과정을 수행한다.
Java 소스 컴파일
→ .class 파일 생성
→ JVM 실행
사용자가 javac, java 명령을 직접 입력하지 않을 뿐이다.
IDE 설정에 따라 컴파일 결과는 out/ 폴더에서 확인할 수 있다.

5. Gradle이 필요한 이유
클래스가 한두 개뿐인 프로젝트는 javac, java 또는 IntelliJ만으로도 실행할 수 있다.
하지만 프로젝트가 커지면 직접 관리해야 하는 작업이 늘어난다.
여러 Java 파일 컴파일
→ 실행할 클래스 지정
→ 외부 라이브러리 다운로드
→ 테스트 코드 컴파일·실행
→ JAR 파일 생성
Gradle은 이러한 작업을 자동화하고 프로젝트의 공식 빌드 방법을 정의한다.
직접 여러 명령 실행
→ Gradle 설정에 따라 빌드·실행·테스트
IntelliJ와 Gradle의 역할
| 구분 | 역할 |
|---|---|
| IntelliJ | 개발자가 코드를 편리하게 작성하고 실행하도록 지원 |
| Gradle | 프로젝트를 빌드·테스트·패키징하는 규칙을 정의 |
IntelliJ에서만 실행하도록 구성하면 개인의 IDE 설정에 의존할 수 있다.
Gradle 설정은 Git 저장소에 함께 저장되므로, 다른 사람이나 CI 서버도 동일한 명령으로 프로젝트를 실행할 수 있다.
./gradlew build
./gradlew test
./gradlew run
즉:
IDE = 개발 편의 도구
Gradle = 프로젝트의 공식 빌드 도구
6. Gradle 프로젝트로 전환
순수 Java 단계에서는 src 전체를 Sources Root로 지정했다.
Gradle로 전환할 때는 다음 순서로 변경한다.
기존 src의 수동 Sources Root 설정 해제
→ src/main/java 폴더 생성
→ Java 파일 이동
→ Gradle 프로젝트 연결
→ Gradle이 Sources Root를 자동 설정
프로젝트 구조를 다음과 같이 변경한다.
java-gradle-junit-socket-lab/
├── build.gradle.kts
├── settings.gradle.kts
└── src/
└── main/
└── java/
├── Calculator.java
└── Main.java
아직 JUnit을 추가하지 않았으므로 src/test/java는 만들지 않는다.
Gradle은 기본적으로 다음 위치를 실제 Java 소스 디렉터리로 사용한다.
src/main/java
settings.gradle.kts
rootProject.name = "java-gradle-junit-socket-lab"
초기 build.gradle.kts
Gradle 설정은 Kotlin DSL을 사용한다.
plugins {
application
}
application {
mainClass.set("Main")
}
application 플러그인은 Java 애플리케이션에 필요한 빌드 및 실행 기능을 추가한다.
application {
mainClass.set("Main")
}
이 설정은 ./gradlew run이 실행할 클래스를 지정한다.
현재 Main 클래스에 패키지가 없으므로 "Main"으로 설정한다.
만약 패키지가 있다면 전체 클래스 이름을 지정해야 한다.
application {
mainClass.set("com.example.Main")
}
7. Gradle Wrapper와 IntelliJ 연결
시스템에 Gradle이 설치되어 있다면 프로젝트 루트에서 Wrapper를 생성한다.
gradle wrapper
생성 결과:
java-gradle-junit-socket-lab/
├── gradle/
│ └── wrapper/
├── gradlew
└── gradlew.bat

이후에는 시스템 Gradle 대신 프로젝트의 Gradle Wrapper를 사용한다.
./gradlew build
./gradlew run
Wrapper를 사용하면 프로젝트에서 정한 Gradle 버전을 모든 환경에서 동일하게 사용할 수 있다.
실행 권한 오류
다음과 같은 오류가 발생할 수 있다.
permission denied: ./gradlew
실행 권한을 추가한다.
chmod +x gradlew
실행 파일은 gradle이 아니라 gradlew이다.
# 잘못된 명령
./gradle build
# 올바른 명령
./gradlew build
IntelliJ의 Gradle Reload
IntelliJ에서 Load Gradle Project, Load Gradle Changes 또는 Gradle Reload를 실행하면 IDE가 build.gradle.kts를 읽고 프로젝트 설정을 동기화한다.
동기화되는 주요 항목은 다음과 같다.
src/main/java의 Sources Root 설정- Gradle Task 목록
- 외부 라이브러리
- IDE 자동완성
- Gradle 실행 설정
Gradle Reload는 IntelliJ를 위한 동기화 작업이다.
터미널에서는 Reload 없이도 Gradle이 build.gradle.kts를 직접 읽는다.
./gradlew build
즉:
Gradle Reload
→ IntelliJ가 Gradle 설정을 다시 인식
./gradlew build
→ Gradle이 실제 빌드를 수행
8. Gradle로 빌드하고 실행
./gradlew build
프로젝트를 빌드한다.
./gradlew build
현재는 테스트를 작성하지 않았으므로 주요 과정은 다음과 같다.
main 소스 코드 컴파일
→ JAR 생성
→ 프로젝트 빌드 결과 확인
Gradle에는 test Task도 포함되어 있지만, 아직 테스트 소스가 없으므로 실행할 테스트가 없다.
주요 결과물은 다음 위치에서 확인할 수 있다.
build/
├── classes/
│ └── java/
│ └── main/ # 컴파일된 .class 파일
└── libs/ # 생성된 JAR 파일
./gradlew run
Gradle로 프로그램을 실행한다.
./gradlew run
실행 결과:
5
run을 실행하기 전에 build를 먼저 실행할 필요는 없다.
Gradle이 run에 필요한 작업을 자동으로 실행한다.
소스 코드 변경 확인
→ 필요한 경우 컴파일
→ Main.main() 실행
run은 JAR를 실행하지 않는다
./gradlew run은 build/libs/*.jar 파일을 실행하는 명령이 아니다.
Gradle은 컴파일된 클래스와 실행에 필요한 클래스 경로를 준비한 뒤 Main.main()을 실행한다.
개념적으로 다음 명령과 비슷하다.
java -cp build/classes/java/main Main
classpath는 JVM이 클래스 파일과 라이브러리를 찾을 위치 목록이다.
build/classes/java/main
외부 라이브러리 A.jar
외부 라이브러리 B.jar
현재 프로젝트에는 실행 시 필요한 외부 라이브러리가 없으므로 Main.class와 Calculator.class를 중심으로 실행된다.
./gradlew clean
기존 build/ 결과물을 삭제한다.
./gradlew clean
clean은 매번 실행할 필요가 없다. Gradle은 변경된 부분만 다시 처리할 수 있다.
완전히 새로 빌드 결과를 만들고 싶을 때 다음과 같이 실행한다.
./gradlew clean build
9. 실행 가능한 JAR 만들기
./gradlew build를 실행하면 다음 위치에 JAR가 생성된다.
build/libs/java-gradle-junit-socket-lab.jar
JAR는 프로젝트의 컴파일된 클래스와 리소스를 하나의 파일로 묶은 결과물이다.
Main.class
+ Calculator.class
+ 리소스
→ java-gradle-junit-socket-lab.jar
기본 JAR 실행 시 발생하는 문제
초기 설정으로 생성된 JAR를 실행하면 다음 오류가 발생한다.
java -jar build/libs/java-gradle-junit-socket-lab.jar
no main manifest attribute
JAR 내부에 어느 클래스를 먼저 실행해야 하는지 지정되어 있지 않기 때문이다.
다음 설정은 ./gradlew run을 위한 설정이다.
application {
mainClass.set("Main")
}
하지만 java -jar는 JAR 내부의 META-INF/MANIFEST.MF에서 Main-Class를 찾는다.
따라서 build.gradle.kts에 다음 설정을 추가한다.
tasks.jar {
manifest {
// java -jar 실행 시 시작할 클래스 지정
attributes["Main-Class"] = application.mainClass.get()
}
}
이 시점의 build.gradle.kts는 다음과 같다.
plugins {
application
}
application {
mainClass.set("Main")
}
tasks.jar {
manifest {
// java -jar 실행 시 시작할 클래스 지정
attributes["Main-Class"] = application.mainClass.get()
}
}
다시 빌드한다.
./gradlew clean build
생성된 JAR를 실행한다.
java -jar build/libs/java-gradle-junit-socket-lab.jar
실행 결과:
5
JAR가 필요한 이유
개발 중에는 다음 명령으로 편리하게 실행할 수 있다.
./gradlew run
하지만 서버나 Docker에서는 일반적으로 프로젝트 소스 전체와 Gradle을 전달하지 않는다.
Gradle로 생성한 배포 결과물을 서버에 전달한다.
개발 환경
→ Gradle로 빌드
→ JAR 생성
→ 서버 또는 Docker로 전달
→ java -jar로 실행
즉:
./gradlew run
→ 개발·실습 중 편리하게 실행
java -jar app.jar
→ 빌드된 배포 결과물을 직접 실행
둘 다 같은 Main.main()을 실행할 수 있지만 실행 경로가 다르다.
Gradle run
→ 컴파일된 클래스와 의존성을 classpath로 구성하여 실행
java -jar
→ JAR의 Manifest에 지정된 Main-Class를 실행
기본 JAR에는 프로젝트의 클래스와 리소스가 포함되지만, 실행에 필요한 외부 라이브러리까지 항상 포함되는 것은 아니다.
현재는 실행 코드가 외부 라이브러리를 사용하지 않으므로 생성된 JAR만으로 실행할 수 있다.
10. JUnit 테스트 추가
이제 Calculator가 올바르게 동작하는지 자동으로 검증한다.
JUnit과 Gradle의 역할은 다르다.
JUnit
→ 테스트 코드 작성과 검증 기능 제공
Gradle
→ JUnit 다운로드, 테스트 컴파일, 실행, 결과 보고서 생성
테스트 디렉터리 생성
다음 디렉터리를 추가한다.
src/test/java
프로젝트 구조는 다음과 같이 변경된다.
src/
├── main/
│ └── java/
│ ├── Calculator.java
│ └── Main.java
└── test/
└── java/
└── CalculatorTest.java
Gradle은 기본적으로 다음과 같이 인식한다.
src/main/java
→ 실제 프로그램 코드
src/test/java
→ 테스트 코드
JUnit 의존성 추가
build.gradle.kts에 저장소, JUnit 의존성, 테스트 설정을 추가한다.
최종 build.gradle.kts는 다음과 같다.
plugins {
application
}
application {
mainClass.set("Main")
}
tasks.jar {
manifest {
// java -jar 실행 시 시작할 클래스 지정
attributes["Main-Class"] = application.mainClass.get()
}
}
repositories {
mavenCentral()
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:6.1.1")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test {
useJUnitPlatform()
testLogging {
events("passed", "failed", "skipped")
exceptionFormat =
org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
}
}
주요 JUnit 설정
repositories {
mavenCentral()
}
JUnit 라이브러리를 Maven Central에서 내려받는다.
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:6.1.1")
}
JUnit Jupiter를 테스트 코드에서 사용할 수 있도록 추가한다.
testImplementation 의존성은 테스트 코드에서만 사용된다. 실제 Main 프로그램을 실행하는 데에는 포함되지 않는다.
tasks.test {
useJUnitPlatform()
}
Gradle의 test Task가 JUnit Platform을 사용하도록 설정한다.
testLogging {
events("passed", "failed", "skipped")
exceptionFormat =
org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
}
터미널에서도 테스트의 성공, 실패, 예외 정보를 자세히 확인할 수 있도록 설정한다.
CalculatorTest.java
테스트 메서드명은 영어로 작성하고, 사람이 읽기 쉬운 한글 설명은 @DisplayName으로 작성한다.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class CalculatorTest {
@Test
@DisplayName("Calculator 인스턴스가 생성된다")
void shouldCreateCalculatorInstance() {
Calculator calculator = new Calculator();
assertNotNull(calculator);
}
@Test
@DisplayName("두 수를 더하면 올바른 결과를 반환한다")
void shouldReturnCorrectSum() {
Calculator calculator = new Calculator();
int result = calculator.add(2, 3);
assertEquals(5, result);
}
}
첫 번째 테스트는 Calculator 인스턴스가 정상적으로 생성되는지 확인한다.
두 번째 테스트는 다음 두 가지를 함께 확인한다.
add()가 값을 반환하는가
반환된 덧셈 결과가 올바른가
11. 테스트 실행과 최종 프로젝트 확인
Gradle에서 테스트 실행
./gradlew test
테스트 전에 ./gradlew build를 실행할 필요는 없다.
Gradle이 테스트 실행에 필요한 작업을 자동으로 수행한다.
main 코드 컴파일
→ 테스트 코드 컴파일
→ JUnit 테스트 실행
→ 테스트 결과 생성
주요 테스트 결과는 다음 위치에서 확인할 수 있다.
build/
├── classes/java/test/ # 컴파일된 테스트 클래스
├── reports/tests/test/index.html # HTML 테스트 보고서
└── test-results/test/ # 테스트 결과 데이터
HTML 보고서를 macOS에서 열려면 다음 명령을 사용할 수 있다.
open build/reports/tests/test/index.html
실패 테스트 확인
예상값을 일부러 잘못 작성한다.
assertEquals(6, result);
다시 테스트를 실행한다.
./gradlew test
JUnit은 다음 차이를 보고한다.
Expected: 6
Actual: 5
실패를 확인한 뒤 예상값을 다시 5로 수정한다.
IntelliJ와 Gradle 테스트 실행 차이
동일한 JUnit 테스트를 실행하지만 결과를 보여주는 방식에는 차이가 있다.

./gradlew test
→ 터미널에 성공·실패 로그 출력
→ HTML 보고서 생성
IntelliJ에서 테스트 실행
→ 테스트 트리 표시
→ Expected와 Actual을 보기 쉽게 표시
→ 실패한 코드 위치로 바로 이동
IntelliJ 설정에 따라 테스트를 IntelliJ 자체 Runner로 실행하거나 Gradle의 test Task에 위임할 수 있다.
개발 중에는 IntelliJ의 시각적인 결과 화면이 편리하다.
프로젝트 전체를 검증하거나 CI 환경에서 실행할 때는 Gradle 명령을 기준으로 한다.
./gradlew test
./gradlew build
JUnit 추가 후 build
JUnit을 추가한 이후 build는 크게 다음 두 영역을 검증한다.
assemble
→ main 코드 컴파일
→ JAR 생성
check
→ 테스트 코드 컴파일
→ JUnit 테스트 실행
실행:
./gradlew build
테스트가 실패하면 전체 build도 실패한다.
JAR 파일이 이미 생성되어 있더라도 BUILD FAILED라면 검증을 통과한 배포 결과물로 취급해서는 안 된다.
최종 JAR 실행 확인
테스트를 통과한 뒤 생성된 JAR를 실행한다.
java -jar build/libs/java-gradle-junit-socket-lab.jar
실행 결과:
5
JUnit은 testImplementation으로 추가되었기 때문에 테스트에서만 사용된다.
현재 실행 가능한 JAR에는 JUnit이 필요하지 않다.
최종 프로젝트 구조
java-gradle-junit-socket-lab/
├── build.gradle.kts
├── settings.gradle.kts
├── gradlew
├── gradlew.bat
├── gradle/
│ └── wrapper/
└── src/
├── main/
│ └── java/
│ ├── Calculator.java
│ └── Main.java
└── test/
└── java/
└── CalculatorTest.java
build/, out/, .class 파일은 소스 코드가 아니라 컴파일과 빌드 과정에서 생성되는 결과물이다.
사용한 주요 명령어
# 순수 Java 코드 컴파일
javac Calculator.java Main.java
# 순수 Java 프로그램 실행
java Main
# Gradle Wrapper 생성
gradle wrapper
# Gradle Wrapper 실행 권한 추가
chmod +x gradlew
# 기존 빌드 결과 삭제
./gradlew clean
# Gradle로 프로그램 실행
./gradlew run
# JUnit 테스트 실행
./gradlew test
# 컴파일·테스트·JAR 생성
./gradlew build
# 완전히 새로 빌드
./gradlew clean build
# 생성된 JAR 직접 실행
java -jar build/libs/java-gradle-junit-socket-lab.jar
확인한 오류 정리
permission denied: ./gradlew
Gradle Wrapper에 실행 권한을 추가한다.
chmod +x gradlew
Could not find or load main class Main
다음 내용을 확인한다.
Main.java가src/main/java에 있는가Main에public static void main(String[] args)가 있는가application.mainClass가 실제 클래스 이름과 일치하는가package선언과 디렉터리 구조가 일치하는가
현재 실습에서는 패키지를 사용하지 않으므로 다음과 같이 설정한다.
application {
mainClass.set("Main")
}
no main manifest attribute
application.mainClass 설정만으로는 java -jar의 시작 클래스가 지정되지 않는다.
다음 Manifest 설정이 필요하다.
tasks.jar {
manifest {
attributes["Main-Class"] = application.mainClass.get()
}
}
'기록' 카테고리의 다른 글
| RAG, Agentic 시대에 놓칠 수 없는 흐름 (RAG 실습 환경 세팅 기록) (0) | 2026.06.11 |
|---|