참조: http://www.zeroturnaround.com/blog/reloading-objects-classes-classloaders/

멀리서 보자면..

자바 코드 릴로딩을 이해하려면 우선 클래스와 객체 관계를 알아야 한다. 모든 자바 코드는 클래스에 들어있는 메소드와 관련있다. 간간하게 클래스를 메소드 집합으로 볼 있으며 각 메서드는 첫번째 인자로 ‘this’를 받고 있다고 생각하면 된다. 클래스는 메모리에 올라가고 유일한 식별자를 할당받는다. 자바 API에서 이 식별자는 java.lang.Class의 인스턴스로 표현되며 MyObject.class 형식으로 접근할 수 있다.

(흠.. 번역하려던게 아닌데;; 요약만 해야지)

MyObject 타입의 mo객체를 가지고 mo.method()를 호출하면 mo.getClass().getDeclaredMethod(“method”).invoke(mo) 같은 일이 벌어지는 것이다.

object

모든 Class 객체는 클래스로더와 관계를 맺고 있다.(MyObject.class.getClassLoader()) 클래스로더의 주요 역할은 클래스 스코프를 정하는 것이다. 클래스의 스코프 라는 것은 해당 클래스를 어디서는 참조할 수 있고 어디서는 참조할 수 없는지에 대한 것이다. 이런 스코프를 사용해서 같은 이름의 클래스가 여러 클래스로더에 존재할 수 있다. 이를 이용해서 다른 클래스로더로 새 버전의 클래스를 로딩할 수 있다.

reloading-object

자바에서 코드를 릴로딩할 때 주요 문제는 새버전의 클래스를 로딩할 수는 있어도 그 클래스가 완전히 다른 식별자를 가지게 되고 기존의 객체는 계속해서 이전에 존재하던 클래스를 참조하게 된다는 것이다.

MyObject 클래스의 새버전을 로딩한다고 가정해보자. 예전 버전을 MyObject_1라 하고 새 버전을 MyObject_2라 해보자. MyObject_1은 MyObject.method()가 “1”을 반환하고 MyObject_2는 MyObject.method()가 “2”를 반환한다고 가정하고 mo2가 MyObject_2라고 했을 때 다음과 같은 일이 벌어진다.

  • mo.getClass() != mo2.getClass()
  • mo.getClass().getDeclaredMethod(“method”).invoke(mo) != mo2.getClass().getDeclaredMethod(“method”).invoke(mo2)
  • mo.getClass().getDeclaredMethod(“method”).invoke(mo2)는 ClassCastException을 발생시킨다. mo와 mo2의 Class 식별자가 다르기 때문이다.

즉 이 문제를 해결하려면 mo2 인스턴스를 만든 다음에 mo의 값을 전부 복사하고 mo를 참조하고 있던 레퍼런스를 모두 mo2로 바꿔야 한다. 이 작업은 마치 전화번호를 변경했을 때 발생하는 일과 비슷하다. (정말 멋진 비유입니다. 캬..)

코딩좀 해볼까..

public interface IExample {
    String message();

    int plusPlus();

    int counter();

    IExample copy(IExample example);
}

이런 인터페이스가 있고 이 구현체는 다음과 같습니다.

public class Example implements IExample {
    private int counter;

    public String message() {
        return "Version 2";
    }

    public int plusPlus() {
        return counter++;
    }

    public int counter() {
        return counter;
    }

    public IExample copy(IExample example) {
        if (example != null)
            counter = example.counter();
        return this;
    }
}

그리고 런타임시에 클래스로더를 만들어서 Example 객체를 만들어 주는 팩토리는 다음과 같습니다.

public class ExampleFactory {
    public static IExample newInstance() {
        URLClassLoader tmp = new URLClassLoader(new URL[]{getClassPath()}) {
            @Override
            public synchronized Class<?> loadClass(String name) throws ClassNotFoundException {
                if ("sandbox.classloader.rjc01.Example".equals(name))
                    return findClass(name);
                return super.loadClass(name);
            }
        };

        try {
            return (IExample) tmp.loadClass("sandbox.classloader.rjc01.Example").newInstance();
        } catch (InstantiationException e) {
            throw new RuntimeException(e.getCause());
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    private static URL getClassPath() {
        String resName =
                ExampleFactory.class.getName().replace('.', '/') + ".class";
        String loc =
                ExampleFactory.class.getClassLoader().getResource(resName)
                        .toExternalForm();
        URL cp;
        try {
            cp = new URL(loc.substring(0, loc.length() - resName.length()));
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
        return cp;
    }
}

이제 이것들을 가지고 테스트를 할 메서드는..

public class Main {
    private static IExample example1;
    private static IExample example2;

    public static void main(String[] args) throws InterruptedException {
        example1 = ExampleFactory.newInstance();

        while (true) {
            example2 = ExampleFactory.newInstance().copy(example2);

            System.out.println("1) " +
                    example1.message() + " = " + example1.plusPlus());
            System.out.println("2) " +
                    example2.message() + " = " + example2.plusPlus());
            System.out.println();

            Thread.currentThread().sleep(3000);
        }
    }
}

이렇게 생겼습니다. 즉 맨처음에 로딩한 example1과 3초마다 새로 로딩해오는 example2의 message()와 plusPlus() 메서드 리턴값을 계속 출력하는 겁니다.

이때 위에 굵은 코드인 copy()를 호출하지 않으면 plusPlus()값이 초기화 되는 것을 볼 수 있고 위와 같이 copy()를 해줘서 기존 객체의 값을 복사해주면 똑같은 값을 찍을 수 있겠죠.

1

일단 실행하면 둘다 1)과 2) 둘다 Version 1이 찍히는데… 애플리케이션이 돌아가는 와중에 Example 소스 코드를 고쳐서 message() 메서드에서 반환하는 값을 Version 2라고 바꿔서 저장한 다음에 컴파일 해주면… 중간에 보이는 것처럼 Version 2로 바뀌게 됩니다.