본문 바로가기

기록

간단한 Java 코드를 Gradle과 JUnit으로 확장하기

순수 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 프로젝트 작성

 

IntelliJ IDEA로 Java Project Open (빈 파일)

 

처음에는 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 단계에서는 다음 설정이 필요하다.

 

  1. Project SDK에 JDK를 지정한다.
  2. src 폴더를 Sources Root로 지정한다.
  3. Main.main() 옆의 실행 버튼을 누른다.
  4. Run 'Main.main()'을 선택한다.

Sources Root는 IntelliJ에 다음 내용을 알려주는 설정이며, 이 폴더 아래의 파일을 Java 소스 코드로 취급하고 컴파일한다.

 

화면 캡쳐

 

화면 캡쳐

 

 

Sources Root를 지정하면 다음 IDE 기능을 사용할 수 있다.

  • Java Class 생성
  • 자동완성
  • 문법 오류 검사
  • 패키지 인식
  • 리팩터링
  • 실행 버튼을 이용한 컴파일 및 실행

IntelliJ도 본질적으로는 다음 과정을 수행한다.

Java 소스 컴파일
→ .class 파일 생성
→ JVM 실행

 

사용자가 javac, java 명령을 직접 입력하지 않을 뿐이다.

IDE 설정에 따라 컴파일 결과는 out/ 폴더에서 확인할 수 있다.

 

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 wrapper 실행 후 캡쳐

 

 

이후에는 시스템 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 runbuild/libs/*.jar 파일을 실행하는 명령이 아니다.

 

Gradle은 컴파일된 클래스와 실행에 필요한 클래스 경로를 준비한 뒤 Main.main()을 실행한다.

 

개념적으로 다음 명령과 비슷하다.

java -cp build/classes/java/main Main

 

classpath는 JVM이 클래스 파일과 라이브러리를 찾을 위치 목록이다.

build/classes/java/main
외부 라이브러리 A.jar
외부 라이브러리 B.jar

 

현재 프로젝트에는 실행 시 필요한 외부 라이브러리가 없으므로 Main.classCalculator.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 테스트를 실행하지만 결과를 보여주는 방식에는 차이가 있다.

 

IntelliJ 에서 확인한 테스트 실행 화면

 

./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.javasrc/main/java에 있는가
  • Mainpublic 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()
    }
}

 

 

https://github.com/Yu-Jaeyoung/java-gradle-junit-socket-lab