[Java/TIL] 도커 환경에서 파일 경로를 못찾는 문제 해결하기

Loopy·2025년 2월 16일
0

삽질기록

목록 보기
28/29
post-thumbnail

문제 상황

로컬에서는 파일의 경로를 찾지만, 도커 환경에서는 Spring Boot 컨테이너에서 logback-spring.xml 파일 경로 못찾는 에러가 발생했다.

Caused by: java.io.FileNotFoundException: src/main/resources/logback-spring.xml (No such file or directory)
	at java.base/java.io.FileInputStream.open0(Native Method)
	at java.base/java.io.FileInputStream.open(FileInputStream.java:216)
	at java.base/java.io.FileInputStream.<init>(FileInputStream.java:157)
	at ch.qos.logback.core.joran.GenericXMLConfigurator.doConfigure(GenericXMLConfigurator.java:92)

문제 원인

우선 문제가 발생한 logback 파일을 사용하는 SimpleSockerServer 의 내부 구성 코드를 파악해보니, FileInputStream을 활용해 파일에 접근하고 있었다.

1. 파일이 JAR 내부에 포함되지 않았을 가능성

우선 src/main/resources/ 내부에 있는 파일들은 Jar 파일의 BOOT-INF/classes/ 에 위치한다. 따라서 혹시나 빌드 과정에서 파일이 포함되지 않았는지 확인해보자.

나와 같은 경우는 스프링 부트 컨테이너 내부로 접속해서, JAR 파일 압축을 풀어 확인해봤더니 logback-spring.xml 이 위치하고 있었다.

sudo docker exec -it {컨테이너 ID} /bin/bash

2. 컨테이너 내부에서 잘못된 경로로 접근을 하는 경우

1번에서 파일이 잘 있는 것을 확인했으니, 컨테이너에서 제대로 인식을 하지 못하는 원인만 남아있었다. 근데 곰곰히 생각해보니 완전 기본적인 것을 놓치고 있었던 것이다.

File 클래스는 파일 시스템 내의 리소스를 읽는다. 즉 FileReader 가 데이터를 가져오는 스트림인 FileInputStream 이 OS 파일의 경로를 읽기 때문에 파일 시스템 경로를 참조해야 한다.

따라서, 파일 시스템 경로를 참조하는 경우가 아니라면 일반적으로 프로젝트 루트 디렉터리 기준 경로 또는 classpath:logback-xml 과 같은 클래스패스 경로를 통해 파일을 참조할 수 있다.

예를 들어, CSV 파일을 읽을때는 아래와 같이 읽을 수 있다. CSVReader 는 Reader(문자 스트림) 기반으로 동작하므로 InputStreamReader 로 감싼 getResourceAsStream() 결과를 바로 전달할 수 있기 때문이다.

InputStream inputStream = getClass().getResourceAsStream("/user.csv");
CSVReader csvReader = new CSVReader(new InputStreamReader(inputStream));
// CSVReader csvReader = new CSVReader(new FileReader(Objects.requireNonNull(getClass().getResource("/user.csv")).getFile()));

🔗 classpath: 접두사
JVM이 리소스를 찾을 때 사용하는 특별한 경로 접두사이다. 도커 컨테이너에서는 JAR 내부의 파일을 직접 참조하는 게 일반적이기 때문에, OS 시스템에 독립적인 classpath: 을 사용하는게 더 권장된다.

하지만 당연히 logback-spring.xml 파일은 압축된 JAR 파일 내부에 포함된 리소스기 때문에, 파일 시스템 상에 직접 존재하지 않아 파일 경로를 찾지 못한다는 오류가 발생했다.

해결 방법

일반적인 해결 방법은 두가지가 있다.

  1. 실제 도커 컨테이너의 파일 시스템으로 복사해서 FileReader 가 파일 경로를 읽을 수 있도록 한다.
  2. FileReader 를 사용하지 않고, InputStream → InputStreamReader → BufferedReader 흐름을 활용해 가져온다.
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("logback-spring.xml");
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
}

사실상 해당 방식이 FileReader 보다 코드가 조금 길어지는거 빼고는 1)JAR 내부 / 파일 시스템의 리소스 모두 접근이 가능하고 2)명시적으로 인코딩도 지정해줄 수 있어 훨씬 범용적인 방식이다.

하지만 2번 방식은 현재와 같이 SimpleSockerServer 라이브러리를 활용하는 특수한 상황에서는 해결이 되지 않기 때문에, 1번 방식으로 어떻게 해결할 수 있는지 확인해보자. 빌드 과정에서 해당 파일을 도커 컨테이너의 파일 시스템으로 복사해주면 문제를 해결할 수 있다.

🔗 1. 컨테이너 파일 시스템

도커 파일 시스템은 크게 컨테이너 레이어와 이미지 레이어로 나누어진다.

  • 컨테이너 레이어 : docker run 이후 만들어지는 Read-Write 레이어
  • 이미지 레이어 : docker build 시 만들어지는 Only Read 레이어이다.

이때 도커에서는 Copy-On-Write 전략에 의해, 쓰기 작업은 상위 컨테이너 레이어로 복사되어 이루어진다. 따라서 하나의 이미지로부터 복수의 컨테이너를 실행시켜도 정상적으로 동작하게 된다.

실제 컨테이너 사이즈를 출력했을 때 나오는 크기는 컨테이너 레이어의 크기를 의미한다.

sudo docker ps -s  -- size 출력

  • Size : 컨테이너 레이어의 데이터의 크기
  • Virtual Size : 컨테이너 레이어 + 이미지 레이어 데이터 크기

🔗 빌드 과정에서 복사하기

깃허브 액션을 통해서 배포를 하고 있었기 때문에, CI 서버 내에 파일을 업로드
할 때 logback-spring.xml 을 추가해주면 된다.

      - name: Build with Gradle
        run: SPRING_PROFILES_ACTIVE=test ./gradlew :${{ env.MODULE_NAME }}:clean :${{ env.MODULE_NAME }}:build
        shell: bash
        
      - name: Upload build artifact (JAR and Dockerfile)
        uses: actions/upload-artifact@v4
        with:
          name: build-artifacts
          path: |
            ./${{ env.MODULE_NAME }}/build/libs/*.jar
            ./${{ env.MODULE_NAME }}/build/resources/main/logback-spring.xml
            ./${{ env.MODULE_NAME }}/*.Dockerfile

그리고 DockerFile에 COPY 명령어를 통해 CI 서버에 있던 logback-spring.xml 파일을 도커 이미지의 config/ 디렉토리로 복사하자.

FROM eclipse-temurin:17-jdk-jammy
...
COPY build/resources/main/logback-spring.xml config/logback-spring.xml
ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul",  "-jar", "app.jar", "--spring.profiles.active=${PROFILE}"]
  • COPY : 도커 이미지 빌드 시, 해당 파일을 이미지 내부로 포함시킨다.

즉 아까 위에서 언급한 파일 시스템을 참고해보면, 다음과 같이 복사 과정이 일어나게 된다.

  1. 이미지 빌드 단계에서 logback-spring.xml 파일이 새로운 이미지 레이어에 저장된다.
  2. 도커 컨테이너 실행 시, 이미지에서 읽기 전용 파일 시스템을 로드한다.
  3. 만약 파일을 수정하면 컨테이너 레이어에서 만들어진 새로운 복사본에서 수정 작업이 일어난다.

결론

나와 같이 사용하는 라이브러리에서 FileInputStream을 사용하고 있어 바꾸지 못하는 경우가 아니라면, 1번 방식은 파일이 1개가 불필요하게 더 생성된다는 단점이 있다.

따라서 2번 방식으로 FileReader를 사용하지 않고 InputStreamReader로 읽어서 해결하는게 가장 좋은 방식으로 생각된다.

참고 자료
https://docs.docker.com/reference/dockerfile/#copy
https://docs.docker.com/get-started/docker-concepts/building-images/understanding-image-layers/#create-a-base-image

profile
개인용으로 공부하는 공간입니다. 피드백 환영합니다 🙂

0개의 댓글