본문 바로가기
IDE

같은 패키지에 있는데 왜 못읽지? JsonUtils.read() 의 비밀

by Jammini 2025. 11. 21.
728x90

목차

  1. 개요
  2. 삽질의 여정
    1. 첫 번째 가설 : 파일 경로가 틀렸나?
    2. 두 번째 가설: 코드 문제?
  3. Eclipse vs IntelliJ 빌드 방식 차이 
    1. Eclipse 의 자동 복사 기능
    2. 왜 IntelliJ와 차이가 있는가?
  4. 해결 방법
    1. 가장 확실한 방법: resources 폴더 사용
    2. Gradle 프로젝트 설정
  5. 결론

1. 개요

테스트 코드를 살펴보던 중 이상한 일이 발생하였다. 분명 테스트 코드에 작성된 같은 패키지에 request_1.json 파일을 두었는데, 계속 NullPointerException이 발생하는 것이다.

String requestDto = JsonUtils.read(
    ReportCreateServiceTest.class, 
    "request_1.json"
); // NullPointerException 발생! 😱

콘솔에는 무정한 NullPointerException만 찍혀있었다.

src/test/java/com/example/service/
├── ReportCreateServiceTest.java
└── request_1.json  ← 여기 있잖아!

파일도 제대로 있고, 경로도 맞는 것 같은데 왜 못 읽는 걸까? 이렇게 나의 삽질이 시작되었다.

2. 삽질의 여정

2.1 첫 번째 가설 : 파일 경로가 틀렸나?

가장 먼저 의심한 것은 파일 경로였다. 혹시 오타가 있나?

// 파일명 확인
"request_1.json" ✅ 맞음
"request_1.jsno" ❌ 오타 없음

// 대소문자 확인
"Request_1.json" ❌ 
"request_1.json" ✅ 정확함

파일명도 정확하고, IDE에서도 파일이 분명히 보인다. 그렇다면 코드 문제일까?

2.2 두 번째 가설: 코드 문제?

JsonUtils.read() 메서드를 열어봤다.

public static String read( Class clazz, String path ) throws Exception {
	return read( clazz.getResourceAsStream( path ) );	
}
@CallerSensitive
public InputStream getResourceAsStream(String name) {
    name = resolveName(name);

    Module thisModule = getModule();
    if (thisModule.isNamed()) {
        // check if resource can be located by caller
        if (Resources.canEncapsulate(name)
            && !isOpenToCaller(name, Reflection.getCallerClass())) {
            return null;
        }

        // resource not encapsulated or in package open to caller
        String mn = thisModule.getName();
        ClassLoader cl = getClassLoader0();
        try {

            // special-case built-in class loaders to avoid the
            // need for a URL connection
            if (cl == null) {
                return BootLoader.findResourceAsStream(mn, name);
            } else if (cl instanceof BuiltinClassLoader) {
                return ((BuiltinClassLoader) cl).findResourceAsStream(mn, name);
            } else {
                URL url = cl.findResource(mn, name);
                return (url != null) ? url.openStream() : null;
            }

        } catch (IOException | SecurityException e) {
            return null;
        }
    }

    // unnamed module
    ClassLoader cl = getClassLoader0(); 
    if (cl == null) {
        return ClassLoader.getSystemResourceAsStream(name);
    } else {
        return cl.getResourceAsStream(name); // 🎯 여기서 null이 반환되고 있다!
    }
}

범인을 찾았다! getResourceAsStream()이 null을 반환하고 있었다.

그렇다면 getResourceAsStream()은 어떻게 동작할까?

public InputStream getResourceAsStream(String name) {
    Objects.requireNonNull(name);
    URL url = getResource(name); // 여기서 리소스를 찾는다
    try {
        return url != null ? url.openStream() : null;
    } catch (IOException e) {
        return null;
    }
}

getResource()가 파일을 찾지 못해서 null을 반환하는 것이었다. 하지만 왜? 파일은 분명 같은 폴더에 있는데!

@Test
public void debugResourcePath() {
    // 1. 클래스 파일이 어디 있는지 확인
    URL classUrl = ReportCreateServiceTest.class
        .getResource("ReportCreateServiceTest.class");
    System.out.println("Class location: " + classUrl);
    // 출력: file:/project/out/test/classes/com/example/service/ReportCreateServiceTest.class
    
    // 2. JSON 파일을 찾을 수 있는지 확인
    URL jsonUrl = ReportCreateServiceTest.class
        .getResource("request_1.json");
    System.out.println("JSON location: " + jsonUrl);
    // 출력: null
}

JSON 파일이 빌드 출력 폴더에 없었고 Java는 소스 폴더가 아니라 빌드 출력 폴더(클래스패스)에서 리소스를 찾는다.

즉, 빌드 시 JSON 파일이 복사되지 않는다.

3. Eclipse vs IntelliJ 빌드 방식 차이

이상했다. Eclipse에서는 같은 방식으로 해도 잘 동작하는 것 같은데…

3.1 Eclipse 의 자동 복사 기능

Eclipse 프로젝트 구조:
src/test/java/com/example/service/
├── ReportCreateServiceTest.java
└── request_1.json

빌드 후 (bin 폴더):
bin/com/example/service/
├── ReportCreateServiceTest.class
└── request_1.json 자동으로 복사됨!

Eclipse는 기본적으로 .java 파일을 제외한 모든 파일을 자동으로 빌드 출력 폴더에 복사된다고 한다.

그래서 별다른 설정 없이도 JSON, XML, properties 파일 등을 같은 폴더에 두고 사용할 수 있는 것이다.

IntelliJ는 명시적으로 리소스 폴더로 지정된 곳의 파일만 복사한다.

기본적으로 리소스 폴더는:

  • src/main/resources (메인 코드용)
  • src/test/resources (테스트 코드용)

src/test/java 폴더는 리소스 폴더가 아니므로, .java 파일만 컴파일되고 나머지 파일은 무시된다.

3.2 왜 IntelliJ와 차이가 있는가?

IntelliJ의 접근 방식이 더 "표준적"이다. Maven과 Gradle 같은 빌드 도구들도 IntelliJ처럼 동작한다.

Maven/Gradle 표준 디렉토리 구조:
src/
├── main/
│   ├── java/       ← 소스 코드만
│   └── resources/  ← 리소스 파일만
└── test/
    ├── java/       ← 테스트 코드만
    └── resources/  ← 테스트 리소스만

이 구조를 따르면 어떤 빌드 도구, 어떤 IDE를 사용하든 동일하게 동작한다.

4. 해결 방법

이제 문제의 원인을 알았으니 해결 방법을 알아보자.

4.1 가장 확실한 방법: resources 폴더 사용

가장 권장하는 방법이다. 파일을 리소스 폴더로 이동하자.

// Before
src/test/java/com/example/service/request_1.json

// After
src/test/resources/com/example/service/request_1.json
  • 코드는 전혀 수정할 필요 없다
  • Eclipse, IntelliJ, Maven, Gradle 모두에서 동일하게 동작

4.2 Gradle 프로젝트 설정

Gradle을 사용한다면 build.gradle에 추가

sourceSets {
    test {
        resources {
            // 기존 resources 폴더
            srcDirs = ['src/test/resources']
            
            // src/test/java도 리소스로 추가
            srcDirs += ['src/test/java']
            
            // .json 파일만 포함
            include '**/*.json'
            include '**/*.xml'
            include '**/*.properties'
        }
    }
}

추가 후 ./gradlew clean test 다시 빌드 해주자.

5. 결론

src/
├── test/
    ├── java/          ← 코드만
    └── resources/     ← 리소스만

모든 빌드 도구와 IDE에서 동일하게 동작하기 때문에 위와 같은 구조로 resources 폴더를 사용하는게 좋다고 생각한다.

 

"같은 폴더에 있는데 왜 못 읽지?" 이 단순한 의문에서 시작된 여정은 Java의 클래스로딩, IDE별 빌드 방식의 차이, 그리고 표준 프로젝트 구조의 중요성까지 깨닫게 해주었다.

 

간단하게 아래 세가지를 기억하자.

1. Java는 소스 폴더가 아닌 빌드 출력 폴더(클래스패스)에서 리소스를 찾는다.

2. Eclipse는 관대하게, IntelliJ는 엄격하게 빌드한다.

3. 표준 구조(`src/test/resources`)를 따르는 것이 최선이다.

 

이상.. 같은 문제로 고민하는 개발자분들께 이 글이 도움이 되길 바라며.

혹시 다른 해결 방법이나 더 좋은 방법을 알고 계시다면 댓글로 공유 부탁드립니다.

반응형