이번에 새 팀프로젝트로 코팅 테스트 문제 풀이를 할 수 있는 사이트를 만들어보기로 했다. 그래서 문자열로 받은 코드를 어떻게 실행할까 고민하고 찾아보다가 컴파일러 API를 통해 동적으로 코드를 컴파일할 수 있다는 것을 알았다. 이후에는 자바 리플렉션 기능을 이용해서 동적으로 클래스를 불러오는데 이 부분은 내가 평소에 자주 접하지 못했던 내용이라 흥미로웠다.
실행 과정
https://openjdk.org/groups/compiler/guide/compilerAPI.html
Draft: Java™ Compiler API
Java™ Compiler API The Java compiler framework (javax.tools) is an API (Application Program Interface) for running compilers (and other tools) as well as an SPI (Service Provider Interface) by which certain aspects of a compiler can be customized. <!-- <
openjdk.org
public class CodeRunService {
String className = "Hello";
String sourceCode =
"public class " + className + " { " +
"public void sayHello() { " +
"System.out.println(\"Hello!!!\"); " +
"} " +
"}";
public void logic() throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
// 컴파일러 인스턴스 얻기
JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = javac.getStandardFileManager(null, null, null);
// 소스 파일 생성
File tempSourceFile = new File("./" , className + ".java");
try (Writer writer = new BufferedWriter(new FileWriter(tempSourceFile))) {
writer.write(sourceCode);
}
// 파일 컴파일
Iterable<? extends JavaFileObject> fileObjects = fileManager.getJavaFileObjects(tempSourceFile);
javac.getTask(null, fileManager, null, null, null, fileObjects ).call();
// 클래스 로드
URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{tempSourceFile.getParentFile().toURI().toURL()});
Class<?> loadedClass = Class.forName(className, true, classLoader);
// 인스턴스 생성 및 메서드 호출
Object instance = loadedClass.getDeclaredConstructor().newInstance();
Method method = loadedClass.getMethod("sayHello");
// 메인 로직 메서드 실행
method.invoke(instance);
// .java 파일 삭제
tempSourceFile.delete();
// .class 파일 삭제
File classFile = new File("./", className + ".class");
classFile.delete();
fileManager.close();
}
}
일단 먼저 받은 문자열을 가지고 Hello.java라는 파일을 만들어준다. 코딩 테스트 문제를 실행한 뒤에는 파일이 더 이상 필요없기 때문에 삭제를 할 필요가 있다. 그런데 나는 처음에 임시 파일로 만들어보자고 생각해서 아래와 같이 작성해서 실행했는데.... 파일명이 실제 코드 내 클래스명과 달라지는 문제가 발생했다. 알고 보니 임시파일로 생성하게 되면 중복 방지를 위해 임의의 숫자가 뒤에 붙는다고 한다.
위와 같이 File.createTempFile()을 이용하면 위와 같은 임시 폴더 경로에 파일이 생성된다. 그런데 문제는 파일명과 public class로 설정한 클래스명이 다르다는 점이다. 이 문제 때문에 아래와 같은 무시무시한 빨간 글을 마주하게 된다.
그래서 그냥 new File()로 생성했다. 이렇게 하면 경로를 지정해줘야 하는데 어차피 수행 후 바로 삭제할 예정이기 때문에 같은 경로에다 생성하기로 했다. 이후에 해당 경로에 생성한 .java 파일과 .class 파일을 잘 삭제해주기만 하면 된다.
이제 저장한 .java 파일을 가져와 컴파일해야 한다.
// 파일 컴파일
Iterable<? extends JavaFileObject> fileObjects = fileManager.getJavaFileObjects(tempSourceFile);
javac.getTask(null, fileManager, null, null, null, fileObjects).call();
위와 같이 작성하면 저장한 파일을 불러와 컴파일을 수행한다.
수행 후 같은 경로 내에 .class 바이트 코드 파일이 만들어진다.
// 클래스 로드
URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{tempSourceFile.getParentFile().toURI().toURL()});
Class<?> loadedClass = Class.forName(className, true, classLoader);
만들어진 클래스 파일을 메모리에 로드해야 한다.
// 인스턴스 생성 및 메서드 호출
Object instance = loadedClass.getDeclaredConstructor().newInstance();
Method method = loadedClass.getMethod("sayHello");
// 메인 로직 메서드 실행
method.invoke(instance);
그리고 자바 리플렉션을 이용하여 해당 클래스 정보로 새 인스턴스를 만들고 해당 인스턴스의 sayHello() 메서드 정보를 가져온다.
method.invoke()를 이용하여 실제로 해당 메서드를 호출한다.
// .java 파일 삭제
tempSourceFile.delete();
// .class 파일 삭제
File classFile = new File("./", className + ".class");
classFile.delete();
fileManager.close();
마지막으로 필요한 과정이 모두 끝났으니 만들었던 파일을 삭제하고 파일 매니저를 닫아준다.
위에서 만든 내용을 테스트삼아 실행해 보았다.
@Test
void printTest() {
CodeRunService codeRunService = new CodeRunService();
try {
codeRunService.logic();
} catch (Exception e) {
e.printStackTrace();
}
}
문자열로 선언한 클래스 내의 메서드가 잘 실행되는 것을 확인할 수 있었다.
생각해 볼 점
직접 위 과정을 수행하면서 자바 기본적인 실행 과정인 .java -> .class -> 클래스 로더 -> 실행에 대해서 직접 실습해 볼 수 있었던 과정이어서 의미 있었던 시간이었다.
그리고 코드를 작성하면서 나중에 어떻게 구조를 변경할지 생각해 봤는데 역시 공통로직 내에 바뀌는 핵심로직이 있으면 AOP를... 나중에 AOP로 만들어 따로 공통로직을 빼고, 코딩 테스트를 풀면 수행 시간이 나오는데 실제 메서드 수행 시간을 구하는 처리를 따로 또 하면 될 것 같다. 마지막으로, 과정을 진행하면서 발생하는 예외가 많은데 지금은 테스트만 하려고 그냥 다 던졌지만 각각 어떻게 처리를 해줄지 고민해봐야겠다.
가장 중요한 점을 빼먹었다...
글을 작성한 후 더 생각해봤는데 new File()로 만들면 안 된다. 다시 temp 파일로 돌아갔는데 그 이유는 동시에 코드 풀이 요청이 들어오면 파일명이 겹치기 때문이다. 관련해서 변경한 내용은 다음 게시글에 작성하도록 하겠다...
'공부 > Java' 카테고리의 다른 글
[Java] Entity 객체에서 불필요한 Setter 잡아내기 (1) | 2024.11.15 |
---|---|
[Java] 자바 Reflection(리플렉션)을 이용하여 동적 개수 파라미터 받아 문자열로 된 메서드 실행하기 (1) | 2024.11.01 |
[Java] 퀵정렬 (QuickSort) 구현하기 (0) | 2024.09.24 |
[Java] 동적 프록시 (Dynamic Proxy) (0) | 2024.07.23 |
[Java][Spring] ThreadLocal 사용하기 (0) | 2024.07.13 |