Showing posts with label lombok. Show all posts
Showing posts with label lombok. Show all posts

Sunday, March 17, 2019

스프링부트와 Gradle 환경에서 Lombok 'compileJava' 'cannot find symbol' 에러 해결

Spring Boot, Gradle, 그리고 Lombok은 현대 자바 애플리케이션 개발, 특히 백엔드 개발에서 거의 표준처럼 사용되는 조합입니다. 이 강력한 삼각편대는 개발자의 생산성을 극적으로 향상시켜 주지만, 때로는 예상치 못한 곳에서 발목을 잡기도 합니다. 그중 가장 대표적이고 많은 개발자들을 혼란에 빠뜨리는 오류가 바로 'execution failed for task ':compileJava'' 또는 'cannot find symbol' 에러입니다.

분명히 IntelliJ나 Eclipse 같은 통합 개발 환경(IDE)에서는 아무런 오류도 표시되지 않고, @Getter, @Setter, @Data 어노테이션이 생성하는 메소드들에 대한 자동 완성까지 완벽하게 동작합니다. 모든 것이 정상적으로 보이는 코드인데도 불구하고, 새로운 환경에서 프로젝트를 실행하거나 gradle build, gradle bootJar 명령어로 빌드를 시도하는 순간, 붉은색 에러 메시지가 콘솔을 가득 채우는 상황은 신입 개발자뿐만 아니라 경력 개발자에게도 상당한 당혹감을 안겨줍니다. 이 글에서는 이 미스터리한 에러가 왜 발생하는지 그 근본적인 원인을 깊이 파헤치고, 명쾌한 해결책과 더 나은 예방책까지 종합적으로 제시하고자 합니다.

에러의 재구성: IDE는 침묵하고, Gradle은 비명을 지른다

이 문제의 가장 큰 특징은 IDE와 실제 빌드 도구(Gradle) 사이의 동작 불일치입니다. 구체적인 시나리오를 통해 문제를 명확히 해보겠습니다.

  1. 개발 환경: 동료 개발자의 컴퓨터나 CI/CD 서버 등, 기존에 코드를 작성하던 곳이 아닌 새로운 환경에서 프로젝트를 클론(clone) 또는 풀(pull) 받습니다.
  2. IDE 확인: 프로젝트를 IntelliJ IDEA로 엽니다. Gradle 종속성을 동기화하고 나면 프로젝트 전체에 빨간 줄(컴파일 오류 표시)이 하나도 보이지 않습니다. Lombok 어노테이션을 사용한 클래스를 열어봐도 깨끗합니다.
  3. IDE 기능 테스트: User 클래스에 @Getter 어노테이션만 붙여놓고, 다른 서비스 클래스에서 user.getName() 메소드를 호출하면 자동 완성이 완벽하게 동작하고, 해당 메소드의 선언으로 이동(Go to declaration)하는 것까지 잘 됩니다. IDE는 getName() 메소드가 존재하는 것처럼 완벽하게 인지하고 있습니다.
  4. Gradle 빌드 시도: 이제 터미널을 열고 프로젝트를 빌드하여 실행 가능한 JAR 파일을 만들기 위해 ./gradlew build 명령어를 실행합니다.
  5. 에러 발생: 빌드 프로세스가 진행되다가 :compileJava 태스크에서 실패하며, 아래와 유사한 'cannot find symbol' 오류가 대량으로 발생합니다.

> Task :compileJava FAILED

/path/to/project/com/example/service/UserService.java:25: error: cannot find symbol
    String name = user.getName();
                      ^
  symbol:   method getName()
  location: variable user of type User
  
/path/to/project/com/example/service/UserService.java:30: error: cannot find symbol
    user.setName("New Name");
        ^
  symbol:   method setName(String)
  location: variable user of type User

... (오류 다수 발생) ...

FAILURE: Build failed with an exception.

이처럼 IDE의 '지능적인' 지원을 믿고 있던 개발자는 코드에 아무런 문제가 없다고 확신했지만, 빌드 시스템은 해당 메소드를 전혀 찾지 못하는 상황에 직면하게 됩니다. 이 불일치야말로 혼란의 근원이며, 이 불일치가 발생하는 이유를 이해하는 것이 문제 해결의 첫걸음입니다.

근본 원인 탐구: 컴파일 시점과 어노테이션 프로세싱의 동작 원리

결론부터 말하면, 이 문제는 'Gradle의 의존성 구성(Dependency Configuration)에 대한 이해 부족'에서 비롯됩니다. 특히 Lombok과 같은 '어노테이션 프로세서(Annotation Processor)'가 어떻게 작동하고, 빌드 도구와 어떻게 상호작용하는지에 대한 이해가 핵심입니다. 차근차근 그 원리를 파고들어 보겠습니다.

1. 자바 소스 코드가 클래스 파일이 되기까지: 컴파일의 마법

우리가 작성하는 .java 파일은 사람이 읽기 위한 텍스트 파일일 뿐, JVM(자바 가상 머신)이 직접 실행할 수는 없습니다. JVM이 이해할 수 있는 언어는 바이트코드(bytecode)이며, 이 바이트코드가 담긴 파일이 바로 .class 파일입니다. 이 변환 과정을 '컴파일(compile)'이라고 부르며, 자바 컴파일러(javac)가 이 역할을 수행합니다.

컴파일 과정은 단순히 문법을 검사하고 코드를 변환하는 것 이상으로, 코드의 참조 관계를 확인하는 중요한 작업을 포함합니다. 예를 들어, `UserService`에서 `user.getName()`을 호출하면, 컴파일러는 `User` 클래스에 실제로 `getName()`이라는 메소드가 정의되어 있는지 확인합니다. 만약 이 메소드를 찾지 못하면, 바로 그 유명한 'cannot find symbol' 에러를 뱉어내는 것입니다.

2. Lombok은 어떻게 코드를 '만들어'내는가? 어노테이션 프로세서(JSR 269)

그렇다면 우리는 User 클래스에 getName() 메소드를 직접 작성한 적이 없는데, 어떻게 IDE는 이 메소드를 알고 있으며, 우리는 어떻게 이 메소드를 사용할 수 있을까요? 바로 여기에 Lombok어노테이션 프로세싱의 비밀이 숨어있습니다.

Lombok은 런타임에 동적으로 코드를 변경하는 라이브러리가 아닙니다. Lombok은 **컴파일 시점(compile time)**에 개입하여 우리가 작성한 코드에 '추가적인 코드'를 생성해주는 도구입니다. 이 기능을 가능하게 하는 것이 바로 '어노테이션 프로세서'입니다.

자바 6부터 도입된 JSR 269(Pluggable Annotation Processing API)는 자바 컴파일러의 동작 과정에 '플러그인'처럼 끼어들 수 있는 공식적인 방법을 제공합니다. 어노테이션 프로세서는 이 API의 구현체로, 컴파일 과정의 특정 단계에서 실행됩니다.

Lombok의 동작 순서는 다음과 같습니다.

  1. 1단계 (컴파일 시작): 개발자가 javac 또는 Gradle의 compileJava 태스크를 실행하여 컴파일을 시작합니다.
  2. 2단계 (어노테이션 스캐닝 및 프로세싱): 자바 컴파일러는 소스 코드를 분석하기 전에, 등록된 어노테이션 프로세서들을 먼저 실행합니다. 이때 Lombok의 어노테이션 프로세서가 동작을 시작합니다.
  3. 3단계 (코드 생성): Lombok 프로세서는 소스 코드에서 @Getter, @Setter, @Data, @Builder 등의 Lombok 어노테이션을 찾아냅니다. 그리고 이 어노테이션들의 규칙에 따라 필요한 메소드(e.g., `getName()`, `setName()`)의 소스 코드를 메모리 상에서 또는 임시 파일로 생성합니다. 이 생성된 코드는 추상 구문 트리(AST, Abstract Syntax Tree)를 직접 조작하는 방식으로 이루어집니다.
  4. 4단계 (최종 컴파일): 어노테이션 프로세서의 작업이 모두 끝나면, 컴파일러는 '원본 소스 코드 + Lombok이 생성한 코드'를 모두 합친 완전한 형태의 소스 코드를 가지고 최종적인 컴파일을 진행하여 .class 파일을 생성합니다.

즉, `compileJava` 태스크의 관점에서 `user.getName()`을 호출하는 코드를 컴파일 할 때, `User.java` 파일에는 이미 Lombok에 의해 `getName()` 메소드가 '존재하는' 상태여야만 합니다. 만약 어노테이션 프로세싱 단계가 제대로 실행되지 않았다면, 컴파일러는 `getName()` 메소드를 찾을 수 없어 'cannot find symbol' 에러를 발생시키는 것입니다.

3. Gradle의 의존성 구성: `compileOnly`와 `annotationProcessor`의 결정적 차이

이제 핵심 퍼즐 조각인 Gradle의 의존성 구성(Dependency Configuration)을 살펴볼 차례입니다. Gradle은 dependencies { ... } 블록 안에 다양한 키워드를 사용하여 의존성의 '범위(scope)'를 지정합니다. 이 범위에 따라 해당 라이브러리가 언제, 어디서 사용될지가 결정됩니다.

  • implementation: 가장 흔하게 사용되는 구성입니다. 컴파일 시점과 런타임 시점 모두에 필요하며, 이 모듈을 의존하는 다른 모듈에게는 노출되지 않습니다.
  • api: implementation과 유사하지만, 이 모듈을 의존하는 다른 모듈에게도 해당 의존성이 전이(transitive)됩니다. 라이브러리 프로젝트에서 주로 사용합니다.
  • compileOnly: 이름 그대로 **'오직 컴파일 시에만 필요한'** 의존성을 의미합니다. 이 의존성은 최종적으로 만들어지는 결과물(JAR, WAR 파일)에는 포함되지 않습니다. 왜냐하면 런타임에는 필요 없기 때문입니다. Lombok이 여기에 완벽하게 부합합니다. 일단 Lombok이 getter/setter 메소드를 생성하여 .class 파일에 포함시키고 나면, 런타임 환경에서는 더 이상 @Getter 어노테이션이나 Lombok 라이브러리 자체가 필요하지 않습니다. 따라서 compileOnly로 선언하여 최종 결과물을 가볍게 유지하는 것이 모범 사례입니다.
  • annotationProcessor: 이것이 바로 오늘의 주인공입니다. 이 구성은 해당 의존성이 **'어노테이션 프로세서'**임을 Gradle에게 명시적으로 알려주는 역할을 합니다. Gradle은 이 `annotationProcessor`로 등록된 라이브러리를 컴파일 과정 중 javac의 어노테이션 프로세싱 단계에서 실행시켜 줍니다. 즉, **Lombok의 코드 생성 마법을 실제로 발동시키는 스위치**가 바로 이 구성입니다.

이제 모든 퍼즐이 맞춰졌습니다. IDE에서는 문제가 없고 Gradle 빌드에서만 실패하는 이유는 다음과 같습니다.

'잘못된 설정'의 시나리오: build.gradlecompileOnly 'org.projectlombok:lombok'만 있고, annotationProcessor 'org.projectlombok:lombok'가 없는 경우.

  • IDE의 동작: IntelliJ와 같은 최신 IDE는 자체적으로 강력한 어노테이션 프로세싱 기능을 내장하고 있습니다. 프로젝트 설정에서 'Enable annotation processing' 옵션이 켜져 있으면, Gradle의 설정과는 별개로 IDE가 직접 Lombok을 어노테이션 프로세서로 인지하고 실행합니다. 따라서 IDE 내부적으로는 getter/setter가 생성된 것처럼 코드를 분석하고 자동 완성 및 탐색 기능을 제공합니다. 개발자는 모든 것이 정상이라고 느끼게 됩니다.
  • Gradle의 동작: 그러나 터미널에서 gradle build를 실행하면, 이 과정은 IDE의 지능적인 기능과 무관하게 오직 build.gradle 파일의 설정에만 의존합니다. Gradle은 compileOnly 설정을 보고 "아, 개발 중에 Lombok 어노테이션을 사용하겠구나. 문법 에러는 내지 말아야지."라고 생각합니다. 하지만 annotationProcessor 설정이 없기 때문에, **"Lombok을 어노테이션 프로세서로 실행하라는 명령은 없었어."** 라고 판단하고 코드 생성 단계를 건너뛰게 됩니다. 결국 Lombok의 마법이 발동되지 않은 상태에서 순수한 원본 코드만으로 컴파일을 시도하게 되고, 당연히 존재하지 않는 `getName()`, `setName()` 등을 찾지 못해 'cannot find symbol' 에러를 뿜어내는 것입니다.

문제 해결: `build.gradle`을 올바르게 수정하기

원인을 파악했으니 해결은 간단합니다. Gradle 빌드 스크립트에 Lombok을 어노테이션 프로세서로 사용하라고 명시해주기만 하면 됩니다.

1. 올바른 의존성 설정 추가

사용하고 있는 Gradle DSL(Domain Specific Language)에 맞춰 아래와 같이 수정합니다.

Groovy DSL (build.gradle 파일)

가장 일반적인 `build.gradle` 파일의 경우입니다. dependencies 블록 안에 annotationProcessor 구성을 추가해야 합니다.

수정 전 (잘못된 설정):


dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    // annotationProcessor가 누락됨
    compileOnly 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

수정 후 (올바른 설정):


dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    
    // 컴파일 시에만 Lombok 라이브러리가 필요함을 명시
    compileOnly 'org.projectlombok:lombok'
    // 컴파일 과정에서 Lombok 어노테이션 프로세서를 실행하도록 명시
    annotationProcessor 'org.projectlombok:lombok'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Kotlin DSL (build.gradle.kts 파일)

코틀린 DSL을 사용하는 프로젝트의 경우, 문법이 약간 다릅니다.

수정 전 (잘못된 설정):


dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    // annotationProcessor가 누락됨
    compileOnly("org.projectlombok:lombok")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

수정 후 (올바른 설정):


dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")

    // 컴파일 시에만 Lombok 라이브러리가 필요함을 명시
    compileOnly("org.projectlombok:lombok")
    // 컴파일 과정에서 Lombok 어노테이션 프로세서를 실행하도록 명시
    annotationProcessor("org.projectlombok:lombok")
    
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

2. 테스트 코드도 잊지 말자!

만약 src/test/java 경로의 테스트 코드에서도 Lombok을 사용하고 있다면, 테스트 코드 컴파일을 위한 설정도 추가해주어야 합니다. 그렇지 않으면 메인 코드는 잘 빌드되다가 테스트 코드 컴파일(compileTestJava) 단계에서 똑같은 오류를 만나게 됩니다.

Groovy DSL (build.gradle 파일)


dependencies {
    // ... (기존 설정)
    
    // 테스트 코드에서도 Lombok을 사용하기 위한 설정
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
}

Kotlin DSL (build.gradle.kts 파일)


dependencies {
    // ... (기존 설정)
    
    // 테스트 코드에서도 Lombok을 사용하기 위한 설정
    testCompileOnly("org.projectlombok:lombok")
    testAnnotationProcessor("org.projectlombok:lombok")
}

3. 변경 후 Gradle 프로젝트 새로고침 및 캐시 정리

build.gradle 파일을 수정한 후에는 IDE에서 'Reload All Gradle Projects' 버튼을 눌러 변경사항을 프로젝트에 적용해야 합니다. 또한, 만약을 위해 이전의 잘못된 빌드 결과가 캐시에 남아 문제를 일으키는 것을 방지하기 위해 터미널에서 `clean` 작업을 수행하는 것이 좋습니다.


./gradlew clean build

clean 태스크는 이전 빌드에서 생성된 모든 파일(build 디렉토리)을 삭제하여, 완전히 새로운 상태에서 빌드를 시작하도록 보장합니다.

더 나은 방법: Gradle Lombok 플러그인으로 간소화하기

매번 compileOnlyannotationProcessor를 쌍으로 관리하는 것은 번거롭고 실수의 여지를 남깁니다. 다행히도 이 과정을 자동화해주는 매우 유용한 Gradle 플러그인이 있습니다. 바로 io.freefair.lombok 플러그인입니다.

이 플러그인을 사용하면 build.gradle 설정이 훨씬 깔끔해지고, 앞서 언급한 문제들을 근본적으로 예방할 수 있습니다.

Lombok 플러그인 적용 방법

Groovy DSL (build.gradle 파일)

plugins 블록에 플러그인 ID를 추가하고, dependencies 블록에서는 lombok 이라는 새로운 구성을 사용하여 의존성을 한 번만 선언하면 됩니다. 이 플러그인이 알아서 compileOnly, annotationProcessor, testCompileOnly, testAnnotationProcessor 등을 자동으로 설정해줍니다.


plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.5'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    id 'io.freefair.lombok' version '6.5.1' // Lombok 플러그인 추가
}

// ... (생략)

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

    // 'lombok' 구성으로 한 번만 선언하면 플러그인이 나머지를 처리해 줌
    lombok 'org.projectlombok:lombok'
    
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Kotlin DSL (build.gradle.kts 파일)


plugins {
    java
    id("org.springframework.boot") version "2.7.5"
    id("io.spring.dependency-management") version "1.0.15.RELEASE"
    id("io.freefair.lombok") version "6.5.1" // Lombok 플러그인 추가
}

// ... (생략)

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    
    // 'lombok' 구성으로 한 번만 선언하면 플러그인이 나머지를 처리해 줌
    lombok("org.projectlombok:lombok")
    
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

새로운 프로젝트를 시작하거나 기존 프로젝트를 리팩토링할 기회가 있다면, 이 플러그인을 도입하여 빌드 스크립트의 안정성과 가독성을 높이는 것을 강력히 추천합니다.

결론 및 최종 정리

Spring Boot와 Gradle 환경에서 발생하는 Lombok의 'cannot find symbol' 에러는 코드 자체의 결함이 아니라, 빌드 도구와 어노테이션 프로세서의 상호작용에 대한 오해에서 비롯된 전형적인 문제입니다.

핵심을 다시 한번 요약하면 다음과 같습니다.

  • IDE는 믿지 마라: IDE의 내장 기능은 실제 빌드 환경과 다를 수 있습니다. 빌드의 유일한 진실은 build.gradle 파일과 커맨드라인입니다.
  • compileOnly는 선언일 뿐이다: 이 구성은 "이 라이브러리를 컴파일 중에 참조할게"라고 알려주는 역할만 합니다.
  • annotationProcessor가 실행의 열쇠다: 이 구성은 "이 라이브러리는 그냥 라이브러리가 아니라 컴파일 과정에 개입하는 도구이니, 실행해줘!"라고 명령하는 핵심 스위치입니다.
  • 플러그인을 사용하라: 가능하다면 io.freefair.lombok 플러그인을 사용하여 설정의 복잡성을 줄이고 실수를 예방하는 것이 현명한 선택입니다.

이러한 빌드 시스템의 동작 원리를 정확히 이해한다면, Lombok 관련 에러뿐만 아니라 QueryDSL(Q-Type 생성) 등 다른 어노테이션 프로세서 기반 라이브러리에서 발생하는 유사한 문제들도 손쉽게 해결할 수 있는 능력을 갖추게 될 것입니다. 당혹스러운 컴파일 에러 앞에서 더 이상 헤매지 않고, 문제의 근원을 파악하여 자신감 있게 대처하는 개발자로 성장하기를 바랍니다.

Thursday, June 7, 2018

Gradle 프로젝트 Lombok 컴파일 오류부터 IDE 문제까지 해결법

자바(Java) 개발 생태계에서 'Lombok'은 반복적인 코드(Boilerplate Code)를 획기적으로 줄여주는 필수 라이브러리로 자리매김했습니다. 생성자, Getter, Setter, `toString()` 등의 메서드를 단 하나의 애너테이션으로 자동 생성해주니, 개발자는 비즈니스 로직에 더 집중할 수 있고 코드 가독성은 비약적으로 향상됩니다. 하지만 이 강력한 편의성 뒤에는 복잡한 동작 원리가 숨어 있으며, 이로 인해 Gradle 기반 프로젝트에서 Lombok을 처음 연동할 때 많은 개발자들이 예상치 못한 컴파일 오류와 IDE의 비정상적인 동작에 부딪히게 됩니다.

`cannot find symbol` 오류가 콘솔을 가득 채우거나, IDE에서 분명히 존재해야 할 메서드를 인식하지 못해 빨간 줄이 그어지는 경험은 개발 의욕을 꺾는 주범 중 하나입니다. 이 문제는 단순히 의존성 하나를 `build.gradle` 파일에 추가하는 것만으로 해결되지 않습니다. Lombok은 빌드 도구(Gradle), 컴파일러(javac), 그리고 통합 개발 환경(IDE)이라는 세 가지 요소가 완벽한 삼각편대를 이루어야만 정상적으로 작동하기 때문입니다.

이 글에서는 Lombok이 왜 그토록 많은 설정 문제를 야기하는지에 대한 근본적인 원인을 파헤치고, Gradle 프로젝트에서 Lombok을 '올바르게' 설정하는 방법부터 IntelliJ IDEA와 Eclipse(STS) 환경에서 발생하는 구체적인 문제 해결 전략까지, A부터 Z까지 심도 있게 다룹니다. 단순히 "이렇게 하세요" 식의 나열이 아닌, '왜' 그렇게 해야 하는지에 대한 깊이 있는 이해를 통해 어떤 상황에서도 유연하게 대처할 수 있는 능력을 기르는 것을 목표로 합니다.

1. 모든 문제의 근원: Lombok의 두 얼굴, 애너테이션 프로세싱

Lombok 연동 실패의 원인을 이해하려면, 먼저 Lombok이 어떻게 마법처럼 코드를 생성하는지 알아야 합니다. Lombok은 자바의 애너테이션 프로세싱(Annotation Processing) 기술을 기반으로 동작합니다.

애너테이션 프로세싱은 자바 컴파일러(javac)가 소스 코드를 바이트 코드로 변환하는 과정 중간에 개입하여, 특정 애너테이션이 붙은 코드를 분석하고 새로운 소스 코드를 생성하거나 기존 코드를 수정할 수 있게 해주는 강력한 기능입니다. 즉, 우리가 `@Getter`, `@Setter`, `@NoArgsConstructor`와 같은 애너테이션을 클래스에 붙이면, 컴파일 시점에 Lombok의 애너테이션 프로세서가 이를 인지하고 실제 `getXXX()`, `setXXX()`, `public User() {}` 와 같은 자바 코드를 생성하여 컴파일 과정에 슬쩍 끼워 넣는 것입니다.

바로 이 지점에서 문제가 발생합니다. 개발 과정을 크게 두 단계로 나누어 볼 수 있습니다.

  1. 컴파일 및 빌드 단계: Gradle과 같은 빌드 도구가 `javac`를 실행하여 프로젝트 전체를 컴파일하고 `.class` 파일들을 생성하여 `jar` 또는 `war` 파일로 패키징하는 단계입니다.
  2. 코딩(IDE) 단계: IntelliJ IDEA나 Eclipse(STS) 같은 IDE가 자체적인 컴파일러와 인덱싱 기능을 통해 실시간으로 코드의 유효성을 검사하고, 자동 완성, 메서드 탐색 등의 기능을 제공하는 단계입니다.

대부분의 라이브러리는 런타임에 클래스가 로드되어 사용되지만, Lombok은 컴파일 타임에만 필요합니다. 생성된 코드는 이미 `.class` 파일에 포함되어 있기 때문에, 애플리케이션이 실행되는 시점(런타임)에는 Lombok 라이브러리 자체가 필요하지 않습니다. 또한, IDE는 Gradle의 빌드 과정과는 별개로 동작하기 때문에, IDE 역시 Lombok이 코드를 생성했다는 사실을 인지하고 있어야만 우리가 코딩하는 동안 오류를 표시하지 않습니다.

핵심 요약: Lombok 연동 문제는 '빌드 도구(Gradle)''IDE', 이 두 곳에 각각 "이제부터 애너테이션 프로세싱을 사용할 것이니, Lombok을 활용해서 소스 코드를 제대로 읽고 처리해줘!" 라고 명확하게 알려주지 않았기 때문에 발생합니다.

이제 이 두 가지 축을 중심으로 올바른 설정 방법을 살펴보겠습니다.

2. 첫 번째 관문: build.gradle 완벽 설정 가이드

모든 설정의 시작은 빌드 스크립트인 `build.gradle` (또는 Kotlin DSL의 경우 `build.gradle.kts`) 파일입니다. Lombok을 Gradle 프로젝트에서 사용하기 위해서는 최소 두 가지 의존성 설정이 필요합니다.

2.1. `compileOnly` vs `annotationProcessor` - 역할의 명확한 구분

Gradle 4.6 이전 버전에서는 `provided` 스코프와 `apt` 같은 플러그인을 사용했지만, 현재는 네이티브하게 지원되는 `compileOnly`와 `annotationProcessor`를 사용하는 것이 표준입니다. 이 둘의 차이를 이해하는 것이 매우 중요합니다.

  • compileOnly: 이름 그대로 '오직 컴파일 시점에만 필요한' 의존성을 의미합니다. 소스 코드에서 `@Getter`와 같은 Lombok 애너테이션을 사용하려면 컴파일러가 해당 애너테이션의 존재를 알아야 합니다. 따라서 `compileOnly` 스코프에 Lombok을 추가하여 컴파일러에게 애너테이션 정보를 제공합니다. 이렇게 설정된 라이브러리는 컴파일 클래스패스에만 포함되고, 최종 빌드 결과물(JAR, WAR)에는 포함되지 않습니다. 이는 Lombok의 특성과 완벽하게 부합합니다.
  • annotationProcessor: 애너테이션 프로세서 자체를 지정하는 스코프입니다. `javac`에게 "이 경로에 있는 애너테이션 프로세서(`lombok.jar` 안에 있는 `AnnotationProcessor` 구현체)를 사용해서 코드를 처리해라"고 지시하는 역할을 합니다. 즉, 실제 코드 생성 작업을 수행할 주체를 명시하는 것입니다.

과거에는 `compile`이나 `implementation` 스코프에 Lombok을 포함시키는 경우가 많았으나, 이는 불필요하게 런타임 라이브러리에 Lombok을 포함시켜 패키징 크기를 늘리고 잠재적인 충돌을 야기할 수 있는 잘못된 방식입니다.

2.2. 표준 `build.gradle` (Groovy DSL) 설정 예시

다음은 Spring Boot 3.x 버전을 기준으로 한 가장 표준적인 `build.gradle` 설정입니다. Spring Boot를 사용하지 않는 순수 Gradle 프로젝트에서도 동일하게 적용할 수 있습니다.


plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.5' // 예시 버전
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

// Lombok 설정을 위한 전용 configuration 블록 (권장)
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    // Spring Boot Starter
    implementation 'org.springframework.boot:spring-boot-starter-web'
    // ... 기타 의존성들

    // --- Lombok 설정의 핵심 ---
    // 1. 컴파일 시점에만 Lombok 애너테이션 API를 사용
    compileOnly 'org.projectlombok:lombok'
    
    // 2. 애너테이션 프로세서로 Lombok을 지정
    annotationProcessor 'org.projectlombok:lombok'

    // Spring Boot Configuration Processor (선택 사항, 그러나 권장)
    // @ConfigurationProperties를 사용할 경우 필요
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    
    // 테스트 코드에서도 Lombok을 사용하기 위한 설정
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
}

tasks.named('test') {
    useJUnitPlatform()
}

💡 `configurations` 블록은 왜 필요한가?

위 예제의 `configurations` 블록은 `compileOnly`가 `annotationProcessor`를 상속하도록 설정합니다. 이는 `dependencies` 블록에서 Lombok 의존성을 두 번 선언하는 대신, 아래와 같이 한 번만 선언할 수 있게 해주는 편의 설정입니다.

// configurations 블록을 사용했을 경우
dependencies {
    // ...
    compileOnly 'org.projectlombok:lombok'
    // annotationProcessor 'org.projectlombok:lombok' 는 자동으로 포함됨
    // ...
}
    

하지만 코드의 명확성을 위해 두 스코프에 각각 명시적으로 선언하는 방식을 더 선호하는 개발자도 많습니다. 어느 쪽을 선택하든 기능상의 차이는 없습니다. 이 글에서는 명시성을 위해 두 번 선언하는 방식을 기준으로 설명합니다.

2.3. Kotlin DSL (`build.gradle.kts`) 설정 예시

Kotlin DSL을 사용하는 프로젝트의 경우 문법만 다를 뿐, 원리는 동일합니다.


import org.springframework.boot.gradle.plugin.SpringBootPlugin

plugins {
    id("java")
    id("org.springframework.boot") version "3.2.5"
    id("io.spring.dependency-management") version "1.1.4"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"

java {
    sourceCompatibility = JavaVersion.VERSION_17
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    
    // --- Lombok 설정 ---
    compileOnly("org.projectlombok:lombok")
    annotationProcessor("org.projectlombok:lombok")

    // 테스트 코드용 Lombok
    testCompileOnly("org.projectlombok:lombok")
    testAnnotationProcessor("org.projectlombok:lombok")
    
    // 기타 의존성
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<Test> {
    useJUnitPlatform()
}

이처럼 `build.gradle` 파일에 정확한 스코프로 Lombok 의존성을 추가했다면, 이제 Gradle은 빌드 시점에 Lombok을 올바르게 사용하여 코드를 생성할 준비를 마친 것입니다. 하지만 이것만으로는 충분하지 않습니다. 이제 두 번째 관문인 IDE 설정을 통과해야 합니다.

3. 두 번째 관문: IDE와의 완벽한 동기화

Gradle 빌드가 성공하더라도 IDE에서 `cannot find symbol` 오류가 발생한다면, 이는 IDE가 Lombok이 생성한 코드를 인지하지 못하고 있다는 명백한 증거입니다. 각 IDE 별로 해결 전략을 자세히 알아봅시다.

3.1. IntelliJ IDEA: 가장 흔한 함정과 해결책

IntelliJ는 현대적인 자바 개발 환경에서 가장 널리 사용되는 IDE이며, Lombok 지원 기능이 매우 강력하지만 몇 가지 핵심 설정을 놓치기 쉽습니다.

3.1.1. 필수 조건 1: Lombok 플러그인 설치 및 활성화

IntelliJ는 Lombok을 기본적으로 지원하지 않습니다. 반드시 Lombok 플러그인을 설치해야 합니다.

  1. File > Settings (Windows/Linux) 또는 IntelliJ IDEA > Preferences (macOS)로 이동합니다.
  2. 왼쪽 메뉴에서 Plugins를 선택합니다.
  3. Marketplace 탭에서 "Lombok"을 검색하여 설치합니다.
  4. 설치 후 IDE를 재시작하라는 메시지가 나타나면 반드시 재시작합니다.

플러그인이 이미 설치되어 있다면, 비활성화(disabled) 상태는 아닌지 확인해야 합니다. Installed 탭에서 Lombok을 찾아 체크박스가 선택되어 있는지 확인하세요.

3.1.2. 필수 조건 2: 애너테이션 프로세서 활성화 (Annotation Processing Enable)

이것이 IntelliJ 사용자들이 가장 많이 놓치는 설정입니다. 플러그인을 설치하는 것만으로는 충분하지 않으며, IntelliJ가 Gradle 프로젝트를 임포트할 때 애너테이션 프로세싱을 사용하도록 명시적으로 설정해주어야 합니다.

  1. File > Settings (or Preferences)로 이동합니다.
  2. Build, Execution, Deployment > Compiler > Annotation Processors로 이동합니다.
  3. 오른쪽 패널에서 Enable annotation processing 체크박스를 반드시 클릭하여 활성화합니다.
  4. 그 아래 `Store generated sources relative to` 옵션은 `Module content root`로 두는 것이 일반적입니다.

이 설정은 IntelliJ에게 "이 프로젝트를 컴파일하거나 분석할 때, `annotationProcessor` 스코프에 명시된 라이브러리들을 사용하여 코드를 동적으로 생성하고, 그 생성된 코드를 소스 코드의 일부로 인식하라"고 지시하는 것과 같습니다. 이 설정이 꺼져 있으면, IntelliJ는 `build.gradle` 파일을 읽어 Lombok 의존성을 인지하더라도, 실제로 코드 생성 작업을 수행하지 않기 때문에 `@Getter`가 붙은 클래스의 `getXxx()` 메서드를 찾지 못하는 것입니다.

IntelliJ Annotation Processors 설정 화면

3.1.3. 문제 해결을 위한 추가 조치

위의 두 가지 필수 설정을 완료했는데도 문제가 지속된다면, 다음 단계를 시도해 보세요.

  • Gradle 프로젝트 새로고침(Reload): `build.gradle` 파일을 수정한 후에는 반드시 IntelliJ가 변경사항을 인지하도록 프로젝트를 새로고침해야 합니다. Gradle 도구 창(보통 IDE 우측에 위치)에서 'Reload All Gradle Projects' 버튼을 클릭합니다.
  • 캐시 무효화 및 재시작 (Invalidate Caches / Restart): IntelliJ의 캐시가 꼬여서 발생하는 문제일 수 있습니다. `File` > `Invalidate Caches...`를 선택하고, 나타나는 대화상자에서 `Invalidate and Restart` 버튼을 클릭하여 캐시를 전부 지우고 IDE를 재시작합니다. 이는 많은 IDE 문제의 만병통치약과 같은 역할을 합니다.
  • 프로젝트 재빌드: `Build` > `Rebuild Project` 메뉴를 실행하여 프로젝트 전체를 처음부터 다시 빌드합니다.

3.2. Eclipse / Spring Tool Suite (STS): 전통과 현대의 혼재

Eclipse 및 그 파생 IDE인 STS는 IntelliJ와는 다른 방식으로 Lombok을 처리하며, 때로는 더 까다로울 수 있습니다.

3.2.1. 현대적인 접근: Buildship 플러그인과 자동 설정

최신 버전의 Eclipse/STS는 Gradle 통합 플러그인인 'Buildship'을 통해 `build.gradle`의 `annotationProcessor` 설정을 자동으로 인식하고 처리하는 기능이 개선되었습니다. 따라서 이상적인 시나리오는 다음과 같습니다.

  1. `build.gradle` 파일에 위에서 설명한 대로 `compileOnly`와 `annotationProcessor`를 정확하게 설정합니다.
  2. 프로젝트 탐색기(Package Explorer)에서 프로젝트를 우클릭합니다.
  3. Gradle > Refresh Gradle Project 메뉴를 실행합니다.
  4. 이 과정에서 Buildship이 애너테이션 프로세싱 설정을 인지하고, Eclipse의 `.factorypath` 파일을 자동으로 구성해줍니다.
  5. 프로젝트 우클릭 > Properties > Java Compiler > Annotation Processing 메뉴에 진입하여 Lombok이 정상적으로 활성화되었는지 확인할 수 있습니다.
  6. IDE를 재시작하여 변경사항을 완전히 적용합니다.

대부분의 경우, 이 방법으로 문제가 해결되어야 합니다.

3.2.2. 고전적이지만 확실한 방법: Lombok.jar 실행 및 설치

만약 위의 방법으로 해결되지 않거나 구버전의 Eclipse를 사용하는 경우, Lombok을 Eclipse 자체에 '설치'하는 전통적인 방법을 사용해야 합니다. 이 방법은 Lombok이 IDE의 설정 파일(`eclipse.ini`)을 직접 수정하여, Eclipse가 시작될 때부터 Lombok 애너테이션 프로세서를 자바 에이전트(java agent)로 로드하도록 만드는 것입니다.

  1. 먼저 Gradle 캐시에서 `lombok.jar` 파일의 위치를 찾아야 합니다. 보통 사용자 홈 디렉토리 아래의 `.gradle/caches/modules-2/files-2.1/org.projectlombok/lombok/...` 경로에 존재합니다. 정확한 위치를 찾기 어렵다면, 터미널이나 CMD에서 프로젝트 루트 디렉토리로 이동한 뒤 다음 명령어를 실행하여 의존성의 실제 파일 경로를 확인할 수 있습니다.
    
    # Windows
    gradlew dependencies | findstr lombok
    
    # macOS/Linux
    ./gradlew dependencies | grep lombok
            
  2. 찾은 `lombok-{version}.jar` 파일을 직접 실행합니다. 터미널에서 `java -jar lombok-1.18.32.jar` 와 같이 실행하거나, 파일 탐색기에서 더블클릭하여 실행합니다. (GUI 인스톨러가 없는 경우도 있으니 터미널 실행을 권장합니다.)
  3. Lombok 인스톨러 창이 나타나면, 자동으로 시스템에 설치된 Eclipse/STS를 찾아 목록에 보여줍니다. 만약 목록에 나타나지 않는다면 `Specify Location...` 버튼을 눌러 수동으로 `eclipse.exe` 또는 `SpringToolSuite4.exe` 파일의 위치를 지정해줍니다.
  4. 설치할 IDE를 선택하고 `Install / Update` 버튼을 클릭합니다.
  5. "Installation successful" 메시지를 확인한 후 인스톨러를 종료합니다.
  6. 반드시 Eclipse/STS를 완전히 종료했다가 재시작합니다.

이 과정을 거치면 `eclipse.ini` 파일 끝에 `-javaagent:...(경로).../lombok.jar`와 같은 라인이 추가된 것을 확인할 수 있습니다. 이로써 Eclipse는 모든 프로젝트에 대해 Lombok을 인지할 준비를 마치게 됩니다.

Lombok Jar 인스톨러 실행 화면

3.2.3. Eclipse/STS 문제 해결을 위한 추가 조치

  • 프로젝트 클린 (Project Clean): Eclipse 메뉴에서 `Project` > `Clean...`을 선택하고, 해당 프로젝트를 클린하여 이전 빌드 아티팩트를 제거합니다.
  • Gradle 프로젝트 새로고침은 필수: `build.gradle`을 변경했다면, IntelliJ와 마찬가지로 `Gradle` > `Refresh Gradle Project`를 항상 실행하는 습관을 들여야 합니다.

4. 고급 시나리오: 또 다른 복병들과의 만남

기본 설정을 모두 마쳤음에도 불구하고 문제가 발생한다면, 좀 더 복잡한 상황일 수 있습니다.

4.1. 다른 애너테이션 프로세서와의 충돌 (e.g., MapStruct, QueryDSL)

프로젝트에 Lombok 외에 MapStruct(객체 매핑), QueryDSL(타입-세이프 쿼리 생성)과 같이 다른 애너테이션 프로세서를 함께 사용하는 경우, 프로세서 실행 순서가 중요해질 수 있습니다.

예를 들어, MapStruct는 Lombok이 Getter/Setter, 생성자를 모두 생성한 *이후에* 동작해야 올바른 매핑 코드를 생성할 수 있습니다. 대부분의 경우 Gradle은 이 순서를 자동으로 처리하지만, 문제가 발생한다면 `annotationProcessor` 의존성 순서를 명시적으로 조정하거나, `lombok-mapstruct-binding`과 같은 특수 바인딩 라이브러리를 추가해야 할 수 있습니다.


// build.gradle 예시
dependencies {
    // ...
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    implementation 'org.mapstruct:mapstruct:1.5.5.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'

    // Lombok과 MapStruct를 함께 사용할 때 순서 보장을 위한 바인딩
    annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'

    // QueryDSL 설정
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
    // ...
}

이처럼 복수의 애너테이션 프로세서가 있다면, 각 라이브러리의 공식 문서를 참조하여 Gradle 설정 가이드를 정확히 따르는 것이 중요합니다.

4.2. Lombok 설정 파일 `lombok.config`

프로젝트의 루트 디렉토리나 특정 소스 패키지에 `lombok.config` 파일을 위치시켜 Lombok의 동작을 세밀하게 제어할 수 있습니다. 예를 들어, 특정 로그 라이브러리를 사용하도록 `@Log` 애너테이션의 기본 동작을 변경하거나, 생성되는 필드 이름의 접두사를 제거하는 등의 설정이 가능합니다.


# lombok.config 예시

# @Log, @Slf4j 등의 로그 필드 이름을 'log'가 아닌 'logger'로 설정
lombok.log.fieldName = logger

# @Data, @Getter 등에서 생성하는 메서드에 final 키워드 추가
lombok.addLombokGeneratedAnnotation = true

# 특정 애너테이션 비활성화
lombok.setter.flagUsage = error

팀 전체가 일관된 규칙을 따르기 위해 `lombok.config` 파일을 버전 관리에 포함시키는 것은 좋은 전략입니다.

4.3. 빌드 서버(CI/CD)에서의 실패

로컬 환경에서는 빌드가 잘 되는데 Jenkins, GitLab CI, GitHub Actions 같은 CI/CD 파이프라인에서 빌드가 실패하는 경우가 종종 있습니다. 이는 거의 100% `build.gradle` 설정 문제입니다. CI 서버는 IDE 없이 오직 `gradlew build` 명령어에만 의존하기 때문에, `build.gradle`에 `compileOnly`와 `annotationProcessor` 설정이 완벽하게 되어 있지 않다면 컴파일 오류가 발생할 수밖에 없습니다. 로컬에서 IDE의 도움으로 어찌어찌 동작했던 것일 뿐, 근본적인 설정이 잘못되었을 가능성이 높습니다. 이 경우, 이 글의 2번 챕터로 돌아가 `build.gradle` 설정을 다시 한번 꼼꼼히 점검해야 합니다.

결론: 체계적인 접근이 해답이다

Gradle 프로젝트에서 Lombok 연동 오류를 마주했을 때, 인터넷에서 발견한 단편적인 해결책을 무작정 적용하기보다 체계적인 접근 방식을 취하는 것이 중요합니다. 문제를 해결하는 핵심 원리는 다음과 같이 요약할 수 있습니다.

  1. 원리의 이해: Lombok은 컴파일 타임에 코드를 생성하는 애너테이션 프로세서이며, 이로 인해 '빌드 도구'와 'IDE' 양쪽에 모두 정확한 설정이 필요하다는 사실을 인지하는 것이 가장 중요합니다.
  2. Gradle 설정 점검: `build.gradle` 파일에 `compileOnly`와 `annotationProcessor` 스코프를 사용하여 Lombok 의존성이 올바르게 명시되었는지 최우선으로 확인합니다. 이것이 모든 것의 기초입니다.
  3. IDE 설정 점검:
    • IntelliJ: Lombok 플러그인 설치와 함께 'Enable annotation processing' 설정이 켜져 있는지 반드시 확인합니다.
    • Eclipse/STS: `Gradle > Refresh Gradle Project`를 통해 설정을 동기화하거나, 전통적인 `lombok.jar` 인스톨러를 사용하여 IDE 자체에 Lombok을 인식시킵니다.
  4. 캐시와 재시작 활용: 문제가 해결되지 않을 때 'Invalidate Caches / Restart'(IntelliJ) 또는 'Project Clean'과 IDE 재시작(Eclipse)은 가장 효과적인 문제 해결 도구 중 하나입니다.

Lombok은 분명히 자바 개발자의 생산성을 극대화하는 강력한 도구입니다. 초반의 설정 과정이 다소 까다롭게 느껴질 수 있지만, 이 글에서 제시한 원리와 해결 전략을 충분히 이해한다면 더 이상 Lombok으로 인해 개발 흐름이 끊기는 일은 없을 것입니다. 이제 지루한 보일러플레이트 코드의 속박에서 벗어나, 창의적인 코드 작성의 즐거움을 만끽하시기 바랍니다.