IDE 에선 가능한 파일(sqlite) 읽기 jar에선 왜 안돼??
오늘 ide 에서만 테스트하던 프로젝트를 개발계 도커 환경에 배포하고 테스트를 진행하였다. (아니 정확히는 오늘 배포는 아니지,, 배포한지는 한참 되었고 이걸(내무 sqlite) 쓰논곳이 거이 없으니,,)
내 프로젝트에서는 데이터를 저장하는 mariadb 말고도 이기종으로 기존 형태소 분석기에서 필요하던 텍스트 파일을 sqlite로 관리를 하였다, (이부분을 crud 하는게 지식이 있는 사람이 아니면 컨트롤하기가 너무 힘들었기에,,)
일단 지금은 형태소 분석기를 사용하는것도 나밖에 없고,, 해서,, 이제야 확인한..
형태소 분석기 Resource 들을 불러 올때 발생한 에러,, “sqlite no such table” 이라는 에러 메세지 -0-;;;
신기한건 ide 에서는 잘 되는데??
결론 부터 말하자면 resource 내에 sqlite db 파일에 접근 하기 위해선 아래와 같이(::) 쓰면된다.
"jdbc:sqlite::{resource:}"
https://github.com/xerial/sqlite-jdbc
목차
- 클래스패스?
- maven 기본 폴더 구조
- 오늘 예제 소스 확인
- 현재 실행위치가 IDE 이지? 아니면 jar 인지?
- 리소스 파일 불러오기
- getResource, getResourceAsStreame, ClassPathResource
- getResource 대신 getResourceAsStreame 사용하기
- sqlite class path ㅠ
1. class path란?
자바를 하면 3가지 path를 자주 다루게 된다. Path, Class Path, JAVA_HOME
이것들이 각각 어떤 패스인지 알아보도록 하자.
1-1. Path
OS에서 명령어를 실행할 떄 명령어를 찾아야 하는 폴더의 순위를 설정하는 환경변수
1-2. JAVA_HOME
Jdk가 설치된 홈 디렉토리를 설정하기 위한 환경변수.
1-3. ClassPath
자바가 클래스를 사용하려고 탐색을 할 떄, JVM 혹은 자바 컴파일러가 사용하는 파라미터로 클래스나 패키지를 찾을 떄 기준이 되는 경로를 말함.
소스 코드 (.java)를 컴파일 하면 바이트 코드 (.class)로 변환이 되고, JVM이 바이트 코드로 된 파일을 실행하려면 (.class) 파일을 찾아야 하는데 이 바이트 코드 까지의 경로를 클래스 패스라고 한다.
클래스 패스 설정 방법 -classpath(-cp) 옵션
javac <option> <source files>
컴파일러가 컴파일 하기 위해서 필요로 하는 참조할 클래스 파일들을 찾기 위해 컴파일시 파일 경로를 지정해주는 옵션
만약 Example.java 파일이 C:\Java\ 디렉터리에 존재하고 필요한 클래스 파일들이 C:\Java\Example.class 에 위치한다면
javac -classpath C:\Java\Example.Class C:\Java\Example.java
로 설정한다.
이러한 -classPath 옵션을 사용하지 않을 경우 클래스 파일이 위치한 현재 디렉토리를 기본 클래스패스로 잡는다.
환경변수 classpath
이 값을 지정하면 실행할 떄마다 -classpaht(-cp) 옵션을 안해줘도 된다.
운영체제를 변경하면 클래스 패스가 유지 되지 않기 때문에 !!!이식성 측면에서는 좋지 못하다!!!.
JVM이 시작될 때 JVM의 클래스로더는 이 환경변수를 호출한다. 그래서 환경변수에 설정되어 있는 디렉토리가 호출되면 그 디렉토리에 있는 클래스들을 먼저 JVM에 로드한다.
그러므로 CLASSPATH 환경 변수에는 필수 클래스들이 위치한 디렉토리를 등록 하도록 한다.
1-4. IDE 자동 클래스패스
위에서 언급한것처럼 이식성 측면 떄문에 운영체제 상의 환경변수로 클래스 패스를 설정하는 것은 지양 하고 IDE__나 __빌드 도구 를 통해서 클래스 패스를 설정한다.
이러한 차이에서 우리가 제목에서 언급한 “IDE 에선 가능한 파일(sqlite) 읽기 jar에선 왜안돼??” 와 같은 문제가 발생하는 것이다.
intelliJ의 경우 Project Structure - Project Setting - Modules 에 메뉴에서 Add Content Root로 소스코드, 테스트 소스, 리소스, 외부 폴더 등의 클래스 패스를 확인하고 설정이 가능해진다.
2. maven 기본 폴더 구조
이번 시간에는 class path를 다루는 시간이므로 maven 에 대한 내용은 다음 포스팅으로 넘기도록 한다.
자바에서 메이븐을 사용하면 아래와 같은 구조를 표준처럼 사용하고 있다.
src/main/java 폴더 하위에 있는 java 파일은 빌드후 target/classes 하위에 위치하게 된다. src/main/resources/static 폴더는 빌드 후 target/static 폴더 아래에 위치하게 된다.
자바 파일이든 그 외 파일이든 결국 빌드 후에는 target 디렉토리가 루트 디렉토리가 된다.
3. 오늘 예제 소스 확인
pom.xml
...
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.5.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>compile</scope>
</dependency>
</dependencies>
...
</project>
main class
@Slf4j
public class App {
public static void main(String[] args) throws IOException {
// path.of 는 java 1.8 에서는 지원을 안하는가?..
ResourceLoader resourceLoader =
new ResourceLoader("/static", Paths.get("/static"));
resourceLoader.loadResourceAsFile("example.txt");
}
}
이러면 어? 절대경로 /static 을 찾는건가? 하지만 저 ‘/’는 classes 디렉토리를 가르킨다.
@Slf4j
@RequiredArgsConstructor
public class ResourceLoader {
private final String root;
private final Path rootPath;
public void loadResourceAsFile(String resourceLocation) throws IOException {
log.info("*** getResource() + File 방식");
log.info("content root: {}", rootPath);
log.info("resourceLocation: {}", resourceLocation);
URL resourceURL = this.getClass().getResource(root + resourceLocation);
log.info("resourceURL: {}", resourceURL);
String fileLocation = resourceURL.getFile();
log.info("fileLocation from URL: {}", fileLocation);
File file = new File(fileLocation);
FileReader fileReader = new FileReader(file);
char[] chars = new char[(int) file.length()];
fileReader.read(chars);
log.info("resource contents: {}", new String(chars));
}
}
출력 결과
00:27:02.714 [main] INFO io.github.hansw90.ResourceLoader - *** getResource() + File 방식
00:27:02.717 [main] INFO io.github.hansw90.ResourceLoader - content root: /static
00:27:02.720 [main] INFO io.github.hansw90.ResourceLoader - resourceLocation: /folder1/example.txt
00:27:02.720 [main] INFO io.github.hansw90.ResourceLoader - resourceURL: file:/Users/luke/Documents/workspace/kosis-data-updater/example/target/classes/static/folder1/example.txt
00:27:02.720 [main] INFO io.github.hansw90.ResourceLoader - fileLocation from URL: /Users/luke/Documents/workspace/kosis-data-updater/example/target/classes/static/folder1/example.txt
00:27:02.720 [main] INFO io.github.hansw90.ResourceLoader - example.txt
00:27:02.720 [main] INFO io.github.hansw90.ResourceLoader - resource contents: hello
ide 에서 실행시 resourceURL 값이 file: 로 시작하는걸 알수 있다. (잘 기억해 두자) 이건 우리가 리소스를 읽은 경로가 /Users/luke/Documents/workspace/kosis-data-updater/example/target/classes/ 라는 것을 알수 있다.
자 그러면 jar 파일로 패키징 하여 실행해 보도록 하자.
*** getResource() + File 방식
00:34:19.878 [main] INFO io.github.hansw90.ResourceLoader - content root: /static
00:34:19.878 [main] INFO io.github.hansw90.ResourceLoader - resourceLocation: /folder1/example.txt
00:34:19.878 [main] INFO io.github.hansw90.ResourceLoader - resourceURL: jar:file:/Users/luke/Documents/workspace/kosis-data-updater/example/target/maven-fat-jar.jar!/static/folder1/example.txt
00:34:19.878 [main] INFO io.github.hansw90.ResourceLoader - fileLocation from URL: file:/Users/luke/Documents/workspace/kosis-data-updater/example/target/maven-fat-jar.jar!/static/folder1/example.txt
00:34:19.878 [main] INFO io.github.hansw90.ResourceLoader - example.txt
Exception in thread "main" java.io.FileNotFoundException: file:/Users/luke/Documents/workspace/kosis-data-updater/example/target/maven-fat-jar.jar!/static/folder1/example.txt (No such file or directory)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at java.io.FileReader.<init>(FileReader.java:72)
at io.github.hansw90.ResourceLoader.loadResourceAsFile(ResourceLoader.java:34)
at io.github.hansw90.App.main(App.java:17)
luke@SeungWooui-MacBookPro target %
에러가 발생하였다.
에러 내용을 확인 해보면 URL 중간 중간에 ’!’ 가 경로 안에 포함이 되어 있다. 이러한 !는 경로에 실제로는 존재 하지 않기 떄문에 위와 같은 에러가 발생하게 된다.
즉 IDE 에서 실행할 떄는 실제 파일 시스템 기준 경로를 따르므로 에러가 발생하지 않았고, jar 파일을 읽을떄는 jar 파일이 !와 함께 표시가 되어 실제 파일 시스템 경로에 맞지 않아 에러가 발생하게 된다.
또한 resourceURL값이 IDE에서는 file: 로 시작 했는데, jar로 시작할때는 jar:file: 로 시작한다는걸 볼수 있다.
4. getResourceAsStream,
public void loadResourceAsStream(String resourceLocation) throws IOException {
log.info("OOO getResourceAsStream() 방식");
log.info("content root: {}", root);
log.info("resourceLocation: {}", resourceLocation);
InputStream ins = this.getClass().getResourceAsStream(root + resourceLocation);
// java9 이상
// byte[] bytes = resourceAsStream.readAllBytes();
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int read;
byte[] data = new byte[16384];
while ((read = ins.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, read);
}
log.info("resource contents: {}", new String(data, StandardCharsets.UTF_8));
ins.close();
}
getResource()는 URL을 반환한다. URL은 위와 같이 jar 파일을 !와같이 표시 하기 떄문에 jar 실행시 에러가 발생하였다. 하지만 getResourceAsStream()은 InputStream을 반환한다. (ClassPathResource) 또한 Inputstream으로 반환이 가능하다.
참고
https://blog.daum.net/debianizer/16995534 http://june0313.github.io/2018/08/11/read-resource-file-on-spring/ https://github.com/xerial/sqlite-jdbc