본문 바로가기
빌드노트 검색

빌드노트

Maven 테스트에서는 되는데 main에서는 패키지를 못 찾는다

같은 JAR이 있어도 Maven의 test scope는 main 컴파일 경로에 포함되지 않는다. 소스 위치와 scope를 바꾼 실제 비교로 실패 단계를 확인하고, 테스트 도우미를 옮길지 앱의 컴파일 의존성으로 선언할지 판단한다.

Maven 프로젝트에서 src/test/java의 코드를 src/main/java로 옮긴 뒤 package ... does not exist가 나면, 그 라이브러리의 의존 범위를 먼저 확인한다. test scope로 선언한 라이브러리는 테스트를 컴파일하고 실행할 때 쓸 수 있지만 main 소스의 컴파일 경로에는 들어가지 않는다. JAR을 이미 내려받았어도 이 차이는 남는다.

고칠 곳은 코드의 용도에 따라 달라진다. 테스트 데이터나 검증을 위한 도우미라면 테스트 소스에 두고 test를 유지한다. 실제 앱 코드가 직접 사용하는 일반 라이브러리라면 그 모듈에 맞는 컴파일 의존성으로 선언한다. import 오류를 없애려고 모든 테스트 의존성을 한꺼번에 바꿀 필요는 없다.

실패하는 모듈에서 scope를 읽는다

JDK와 Maven이 설치된 환경에서 오류가 나는 소스의 pom.xml이 있는 모듈로 이동한다. 아래는 Windows의 PowerShell 7에서 Commons Lang를 조회하는 명령이다. 자신의 문제라면 org.apache.commons:commons-lang3를 해당 라이브러리의 groupId:artifactId로 바꾸고, 평소 컴파일할 때 사용하는 프로필과 설정을 동일하게 적용한다.

mvn.cmd -B -ntp org.apache.maven.plugins:maven-dependency-plugin:3.8.1:tree "-Dincludes=org.apache.commons:commons-lang3"

별도로 만든 비교 프로젝트에서는 다음 줄이 나왔다. includes는 트리를 해당 좌표로 좁히는 옵션이며, 이 명령에는 scope 필터를 주지 않았다. Dependency Plugin 3.8.1의 tree 옵션 (새 탭에서 열림)

[INFO] \- org.apache.commons:commons-lang3:jar:3.18.0:test

마지막 test가 이 의존성의 범위다. 트리에 나타났다는 사실만으로 main 컴파일에서도 쓸 수 있다고 판단하면 안 된다. Maven 공식 가이드는 scope를 classpath, 즉 컴파일러나 실행기가 클래스를 찾는 경로에 의존성을 언제 포함할지 정하는 설정으로 설명한다. test는 테스트 컴파일·실행에만 포함되고, scope를 생략했을 때의 기본값은 compile이다. Maven 의존 범위 설명 (새 탭에서 열림)

조회 명령 자체가 다운로드나 저장소 인증 오류로 실패했다면 아직 scope를 판정한 것이 아니다. 트리가 비어 있다면 좌표·모듈·활성 프로필부터 다시 확인한다. 선택된 항목이 이미 compile이라면 이 글의 원인으로 단정하지 말고, 오류에 나온 패키지가 그 버전의 JAR에 실제 있는지와 컴파일 대상 모듈을 확인한다.

test를 요청했어도 main 컴파일에서 멈출 수 있다

의존 범위를 확인한 뒤, 같은 모듈에서 아래 명령으로 예전 컴파일 결과를 지우고 실패 위치를 읽는다. clean은 기본 구성의 target 산출물을 지우므로 보관할 결과가 있다면 먼저 옮겨 둔다.

mvn.cmd -B -ntp clean test

비교 프로젝트에서 Commons Lang를 사용하는 코드를 main 경로에 두고 scope는 test로 유지하자 종료 코드 1이 나왔다. 로그의 목표와 오류 문구를 발췌하면 다음과 같다. 소스의 절대 경로와 반복되는 오류 줄은 덜어냈다.

[INFO] --- compiler:3.14.1:compile (default-compile) @ scope-lab ---
package org.apache.commons.lang3 does not exist
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.14.1:compile (default-compile) on project scope-lab: Compilation failure: Compilation failure:

실패한 목표는 testCompile이나 Surefire의 test가 아니라 main 소스를 컴파일하는 compile이었다. 일반적인 JAR 프로젝트에서 mvn test는 main 컴파일, 테스트 컴파일, 테스트 실행 순으로 진행한다. main 컴파일이 실패하면 뒤의 테스트에는 도달하지 못한다. Maven 빌드 단계와 JAR의 기본 목표 (새 탭에서 열림)

Compiler Plugin 3.14.1도 앱 소스를 맡는 compile과 테스트 소스를 맡는 testCompile의 의존성 해석 범위를 각각 compile, test로 명시한다. 같은 프로젝트 안에서도 두 컴파일 경로가 다른 이유다. compile 목표 (새 탭에서 열림), testCompile 목표 (새 탭에서 열림)

같은 JAR로 소스 위치와 scope만 비교했다

이 차이를 확인하려고 앱과 분리한 단일 모듈 프로젝트 세 개를 만들었다. Windows 11, PowerShell 7.6.6, Maven 3.9.16, OpenJDK 17.0.2에서 Commons Lang 3.18.0, Compiler Plugin 3.14.1, Surefire 3.5.4, JUnit 4.13.2를 고정했다. 별도 Maven 설정과 전용 로컬 저장소를 사용했다.

비교 대상인 TextProbe.blank(String value)는 org.apache.commons.lang3.StringUtils를 import하고 StringUtils.isBlank(value)를 반환한다. 테스트 하나가 공백 문자열 " "의 결과는 true, "hello"의 결과는 false인지 단언한다. 세 프로젝트에서 이 클래스와 테스트의 내용은 같고, 클래스 위치와 Commons Lang의 scope만 달리했다. JUnit은 모두 test로 유지했다.

  • 테스트 경로 + test scope: TextProbe를 src/test/java에 두자 clean test가 종료 코드 0으로 끝났고 테스트 1개가 통과했다.
  • main 경로 + test scope: 같은 클래스를 src/main/java에 둔 조건에서는 위의 패키지 오류가 났다. 테스트 실행 보고서도 만들어지지 않았다.
  • main 경로 + 기본 compile scope: main 소스는 그대로 두고 Commons Lang의 scope 항목만 제거하자 main 컴파일과 테스트 1개가 모두 통과했다.

세 경우 모두 로컬 저장소에 commons-lang3-3.18.0.jar가 있었고 SHA-256도 같았다. 실패한 경우의 의존성 트리 조회 역시 성공했다. 따라서 이 비교에서 바뀐 결과를 파일을 못 내려받은 탓으로 설명할 수는 없다.

Compiler Plugin의 디버그 로그에서도 실제 Classpath를 대조했다. 첫 조건에서는 main 컴파일 경로에 Commons Lang가 없고 테스트 컴파일 경로에는 있었다. 두 번째 조건의 main 경로에도 없었다. 세 번째 조건에서는 main 경로에 같은 JAR이 추가됐고 JUnit은 여전히 빠져 있었다. 파일이 저장소에 있는 것과 특정 컴파일 경로에 포함되는 것은 별개라는 점을 트리와 컴파일 결과로 함께 확인했다.

운영 코드에 필요한 의존성인지 먼저 정한다

실제 앱이 Commons Lang를 직접 호출해야 한다면 해당 모듈 POM의 일반 dependencies에서 기존 선언을 아래처럼 바꿀 수 있다. 실험과 같은 3.18.0을 표시했으며, 자기 프로젝트의 버전 정책을 이 숫자로 교체하라는 뜻은 아니다. test 항목을 남긴 채 같은 의존성을 중복 추가하지 않고, 기존 선언의 scope를 제거하거나 compile로 고친다.

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
  <version>3.18.0</version>
</dependency>

위 비교에서는 scope를 생략하자 같은 트리 명령의 마지막 값이 3.18.0:compile로 바뀌었다. 같은 mvn.cmd -B -ntp clean test를 다시 실행하면 main 컴파일을 지난 뒤 Tests run: 1, Failures: 0, Errors: 0, Skipped: 0이 나왔고 종료 코드는 0이었다. compile 의존성은 테스트 경로에서도 쓸 수 있으므로 테스트용 중복 선언은 필요하지 않았다.

반대로 테스트 준비나 단언에만 쓰는 코드라면 src/test/java에 두는 쪽이 맞다. 비교의 첫 조건이 그 배치로 통과했다. JUnit·Mockito 같은 테스트 도구까지 운영 의존성으로 바꾸는 것은 import 오류의 기본 해법이 아니다. 같은 일반 라이브러리라도 테스트에서만 사용한다면 test가 맞다고 Maven 공식 문서도 설명한다. test scope의 용도 (새 탭에서 열림)

이 글의 수정은 앱이 직접 호출하고 실행할 때도 필요한 일반 라이브러리를 전제로 한다. 컨테이너가 제공하는 API 등은 실행 환경에 맞는 범위를 별도로 판단해야 한다. runtime으로 바꾸는 것만으로는 main 컴파일 경로에 들어가지 않는다. compile·provided·runtime의 조건 (새 탭에서 열림)

직접 확인한 범위는 고정 버전의 표준 소스 경로를 쓰는 Maven 3 단일 모듈에서 main·테스트 컴파일과 테스트 실행까지다. IDE의 별도 실행 경로, Java 모듈 시스템, 여러 모듈 사이의 전이 의존성, 최종 배포 파일과 운영 실행은 검증하지 않았다. 수정 후에도 오류가 같다면 먼저 같은 모듈·프로필의 트리와 실패 목표를 다시 대조한다. POM을 바꿨다는 사실만으로 다른 실행 환경까지 고쳐졌다고 보지는 않는다.

← 전체 글 보기