버스와 경찰은 무슨 관계야

버스가 위협적으로 차선 변경을 하는 바람에 아버지가 앞에 있던 택시를 받을 수 밖에 없었다. 비오는 날이라 급정거를 할 수도 핸들을 꺽을 수도 없었다. 오히려 그랬다면 어버지 목숨이 위험한 상황이었다. 물론 속도는 규정속도를 오버하지 않았다.

버스가 아버지 차량에 위협을 가한 이유는 그 사건이 발생하기 조금 전 아버지 차량이 버스가 진입해야 할 보도쪽 차선에서 주행중이었고 그게 못마땅 했는지 버스기사는 짜증스럽게 크락션을 울려 아버지 차량을 그 차선에서 밀어내버렸다.

그 뒤에… 2차선에서 주행중이던 버스가 갑자기 1차선 뒤쪽에 오던 아버지 차량을 분명히 봤는데도 불구하고 1차선으로 급차선 변경을 한다. 그리고 아버지 차량을 버스를 피해 1차선 깊숙한 곳으로 계속 주행을 하다가 정차해 있던 택시를 들이 받는다… 다행히 버스 기사와 아버지 모두 무사하고 택시 기사만 경미한 부상으로 병원에 입원하고 아버지는 경찰에 신고한다.

이때 버스는 백밀러로 아버지 차량과 택시 충돌을 게속 주시하면서 그 사고 현장을 쭉… 지나쳐간다. 아무일 없었다는 듯이…

아버지라서 내가 편드는 것 아닐까라고 생각할 수 있겠지만 이 내용은 버스기사가 인정한 내용이고 택시기사와 버스에 내장된 CCTV로 확인한 결과이다. 특히 버스에 장착된 4대의 CCTV 중 버스기사 옆에 달린 화면을 보면 버스기사의 시선까지 확인할 수 있다. 그리고 그 당시 택시기사, 출동한 경찰과 출동한 보험사 직원, 아버지 앞에서 버스기사가 화가나서 아버지 차량에 일부러 위협을 가했다는 말까지 서슴없이 했다고 한다.

자 경찰서에 왔다. 누가 가해자이고 누가 피해자라고 적었을까?

경철은 아버지를 가해자 버스와 택시를 피해자라고 적어놨다.

이게 현실이다. 버스노조는 아무도 이길 수 없다고 한다. 사고가 발생한 지점은 인천의 어느 동네인데 기억나진 않는다. 아버지는 한마디로 굉장히 열받았다. 보험사 직원은 경찰서에 들어가 따질 수가 없다고 한다. 사고 당사자가 따져야 하는데 만약에 아버지가 사고로 병원에라도 입웠했으면 억울해도 그냥 가해자로 피해보상만 해주고 끝날 수밖에 없었을 것이다. 그런데 정말 다행으로 아버지는 거의 다치지 않으셨고 경찰서에 가서 가해자와 피해자를 뒤집어 버렸다. 아버지가 진도를 급변경했으면 사고가 날 여지가 충분했고 버스가 급격하게 차선을 변경해서 사고가 난게 분명했기 때문이다.

우선 여기에 문제가 있다. 왜 경찰은 버스편을 드는가? 모르겠다. 버스노조와 경철이 모종의 관계가 있다는 생각이든다. 거의 분명하다. 그러지 않고서 그런 상황에서 아버지 차량을 가해자로 몰수는 없다.

다행히 다혈질인 아버지가 따져서 가해자 버스, 피해자 택시 그리고 아버지로 바꼈다.

그런데 경찰과 버스와의 모종의 관계는 거기서 끝나지 않는다. 조서를 꾸며 검찰로 넘기는 과정에서 경찰은 버스 기사의 고의적인 차선 변경과 도주 사실을 덮어줬으며 마치 아버지가 가해자인양 당사자 목록 순서를 조정해뒀다. 또 핵심적인 증거물인 버스와 택시의 CCTV 동영상 조차 검찰에 넘기지 않았다. 조서 내용 역시 다혈질이면서도 매우 치밀한 아버지가 열심히 발품팔아 알아낸 문서이지 그냥 가만히 있었으면 이런 사실도 모르고 검찰에 넘어갔을 것이다. 아니 이미 넘어간 문서 내용을 참조한 거니까,.. 검사는 그 내용만 보고 결정했을 것이다.

하지만… 끈질긴 아버지는 검사에게 연락을 해 이런 사실을 일일히 고했고, 검사도 그 점에 관심을 가지고 탄원서와 증거자료를 제출해 달라고 했고 난 CCTV 영상과 사고 당일 찍은 사진을 CD로 구워드렸다.

사건이 어떻게 마무리 될지 모르겠지만…인천지역 버스와 경찰의 관계는 분명히 찝찝하다. 똑바로 살기 더럽게 힘든 나라에서 경찰도 믿을 수가 없으니 정말 큰일이다. 민중의 지팡이 좋아하시네…

[자바 클래스 릴로딩 301] 톰캣, 글래스피쉬, OSGi, Tapestry 5 등 웹 배포시 클래스로더

참조: http://www.zeroturnaround.com/blog/rjc301/

자바 EE 애플리케이션

자바 EE 웹 애플리케이션을 배포할 땐 .WAR로 압축하고 톰캣 같은 서블릿 컨테이너에 배포한다. 하지만 이런 방법은 제품을 배포할 때나 쓸만하지 개발할 때는 소스 코드를 고치면 웹 브라우저에서 바로 확인하길 원한다.

재배포

서버에 애플리케이션을 배포하면 애플리케이션 당 클래스로더를 하나씩 만든다. 톰캑은 .WAR 애플리케이션 마다 StandardCOntext 클래스의 인스턴스가 WebappClassLoader를 만들어서 해당 웹 애플리케이션 클래스를 로딩한다. 사용자가 “reload” 버튼을 클릭하면 다음과 같은 일이 발생한다.

  1. StandardContext.reload() 메서드 호출
  2. 이전에 만든 WebappClassLoader를 새 인스턴스로 교체
  3. 서블릿을 참조하던 모든 레퍼런스 사라짐
  4. 새 서브릿 생성
  5. Servlet.init() 호출

tomcat-cl-reload

Sevlet.init()을 호출하면 애플리케이션 상태가 초기화 되고 수정한 클래스를 새 클래스로더에서 읽어 사용할 수 있다.

이때 문제가 바로 “초기화” 된다는 것이다. 애플리케이션을 재배포 할 때 마다 메타데이터/환결설정을 다시 읽어들이고, 캐시를 만들고, 어떤 확인 작업을 하는 등 초기화 작업하는데 시간을 쓰게 된다. 물론 애플리케이션 규모가 작다면 몇초안에 초기화 작업이 끝나버리겠지만 말이다.

핫 디플로이

웹 컨테이너는 보통 특정한 디렉토리(톰캑에서 webapp, JBoss에선 deploy)를 주기적으로 살펴보다가 새 웹 애플리케이션이 추가되거나 기존의 것이 변경되면 스캐너가 재배포를 수행한다. 톰캣의 경우 배포했던 .WAR 파일이 변경되면 StandardContext.redeploy()를 호출한다.

이때 사용자 입장에서는 아무런 부가작업 없이 변경되기 때문에 이걸 가지고 “핫 디플로이”라고 표현하고 있으며 이런 기능은 여러 애플리케이션 서버마다 각기 다른 이름으로 명하고 있다. autodeployment, rapid deployment, autopublishing, hot reload 등이 있다. 일부 컨테이너는 자신이 주시하고 있는 디렉토리가 아니라 사용자가 원하는 디렉토리를 주시할 수 있는데 이클립스에서 파일을 저장하면 재배포가 발생하게 되는 경우도 그것을 이용한 것이다. 수동으로 ‘재배포’ 버튼을 누른것과 거의 같기 때문에 코드를 변경한 결과가 곧바로 웹 브라우저에 반영되진 않는다.

여기서 주목할 또 한가지 문제는 재배포나 핫 디플로이 도중에 발생할 수 있는 “클래스로더 누수”다. 이전 글에서도 살펴봤듯이 클래스로더는 정말 쉽게 누수될 수 있고 또 금새 OutOfMemoryError를 야기할 수 있다.

Exploded 배포

웹 컨테이너의 주요 기능 중 하나로 “exploded 배포”라는 것이 있다. “unpackaged” 또는 “directory” 배포라고도 하는데 .WAR 압축 파일로 배포한느 것이 아니라 .WAR로 압축할 것과 동일한 구조를 지닌 디렉토리자체로 배포하는 것이다. (나도 이렇게 하는데ㅋㅋ)

exploded

왜 이렇게 하냐면 압축파일 만드는데 오래 걸리기도 하고 프로젝트 내부에 .WAR 압축파일 구조와 동일한 구조로 디렉토리를 만들 수 있기 때문이다. 이렇게 하면 서버에 .WAR 파일을 복사하지 않고도 디렉토리에 접근해서 직접 수정할 수 있다. 하지만~ 그렇게 한다고 해서 재배포 하지 않고 변견되진 않는다. 자바 클래스는 재배포하지 않고 다시 로딩 될 수 없다.

세션 영속화

재배포할 때 애플리케이션도 초기화 되기 때문에 세션 상태에 들어있던 정보도 초기화 된다. HTTP 세션에는 로그인 정보와 대화형 상태 정보가 들어있을 수 있다. 개발시에 이런 정보가 초기화 되면 귀찮기 때문에 대부분의 컨테이너는 이 문제를 해결하고자 자바 직렬화를 사용해서 HttpSession 맵에 들어있는 객체들을 직렬화 했다가 역질렬화 하는 형태로 세션 정보를 복구한다.

hot-deploy-session

세션 영속화는 오래전부터 대부분의 주요 컨테이너가 지원해왔다.(톰캣의 경우 Restart Persistence가 동작한다.)

OSGi

OSGi는 기본적으로 자체 클래스로더를 가지고 있는 모듈의 집합으로 볼 수 있다. 따라서 해당 클래스로더를 버리거나 새로 만들어서 모듈 자체를 새로 추가하고 없애는게 가능하다. 이때 모듈 하나를 집중해서 모면 웹 애플리케이션 하나랑 동일한 방식으로 만들어진다.

osgi

OSGi와 웹 컨테이너의 차이는 OSGi가 애플리케이션을 더 세밀하게 쪼개서 노출하고 있다는 것이다. 따라서 설계상 모듈이 단일 웹 애플리케이션보다 재배포 하는 단위가 작고 그만큼 재배포 시간이 빨라질 순 있지만 모듈을 어떻게 구성하느냐에 따라 달라질 것이다.

Tapestry 5, RIFE, Grails

테페스트리 5, RIFE, Grails 같은 최근 몇몇 웹 프레임워크는 애플리케이션 상태 관리를 직접 해준다. 애플리케이션 상태를 직렬화 하거나 어떻게든 간단하게 다시 생성할 수 있도록 만들어서 클래스로더를 버리고 난 뒤에 다시 초기화 하는 작업이 필요없다.

즉 개발자가 프레임워크가 제공하는 컴포넌트와 해당 컴포넌트의 라이프사이클을 사용하는 것이고 프레임워크가 각 컴포넌트를 초기화, 실행, 소멸시켜주는 것이다.

component

컴포넌트가 작고 클래스로더는 상세하게 나눠져 있으니까 빠르게 재배포한다는 장점이 있다. 따라서 코드가 즉각 다시 로딩 되고 개발이 유연해 진다. 하지만 여러 클래스 버전이 동시에 로딩되어 ClassCastException 같은 비호환 문제가 발생할 수도 있다.

[자바 클래스 릴로딩 201] 클래스로더 누수가 어떻게 발생하는가

참조: http://www.zeroturnaround.com/blog/rjc201/

클래스로더에서 클래스

자바에서 메모리 누수가 발생하는 경우는 보통 정리해야 할 레퍼런스가 남아버려서 그렇다. 클래스로더는 그 중에서도 매우 특별한 경우로 클래스로더가 누수되면 서버에서 애플리케이션을 몇번 재배포 할 때마다 OutOfMemoryError를 보게 될 것이다.

reloading-object

이전 글에서 살펴봤던 내용을 다시 보면, 새로운 클래스를 로딩할 때마다 이전 클래스로더는 버리고 매번 새 객체를 만들고 이전 객체 그래프를 복사해왔다.

모든 객체는 자신의 클래스를 참조하고 있고, 또 해당 클래스는 자신을 로딩한 클래스로더를 참조하고 있다. 또! 모든 클래스로더는 자신이 로딩한 모든 클래스들을 참조하고 있으며 각 클래스들은 해당 클래스 내부의 static 필드를 들고 있다.

classloader-refs

(캬.. 정말 멋진 그림이닷.)

즉..

  1. 만약에 클래스로더가 누수되면 그 클래스로더가 들고 있는 모든 클래스와 static 필드를 들고 있게 된다.(즉 GC되지 않는다.) static 필드는 보통, 캐시, 싱글톤, 애플리케이션 상태나 설정 정보로 사용한다. 직접 작성한 코드에 static 필드가 없더라도 사용중인 라이브러리에서 static 필드를 사용중일 수도 있으니 클래스로더 누수는 심각한 상황을 초래할 수 있다.
  2. 어떤 클래스로더를 누수시키려면 그 클래스로더로 로딩한 어떤 클래스의 어떤 객체에 대한 레퍼런스 하나만 남겨두는 걸로 충분하다. 객체가 아무리 무해해 보이더라도 그 객체는 분명히 자신의 클래스로더를 참조하고 있는 것이고 즉 모든 애플리케이션의 상태를 들고 있는 것이나 마찬가지다. 어느 한 곳에서라도 적절하게 정리되지 않고 레퍼런스가 남아버리면 누수가 발생한다. 특히 써드파티 라이브러리에 그런 문제가 있다면 고치기 어렵다.

메모리 누수를 만들어 보자.

이전 코드에 ILeak과 그 구현체 Leak을 추가하는데 특이한 점은 Leak 클래스가 ILeak 타입의 멤버 변수를 하나 들고 있다는 점인데.. 이 멤버변수를 사용해서 누수를 시킬겁니다.

public class Leak implements ILeak {
  private ILeak leak;

  public Leak(ILeak leak) {
    this.leak = leak;
  }
}

그리고 Example에다가 ILeak 타입의 멤버 변수를 추가하고 복사하는 와중에 leak이 누수 되도록 코딩합니다.

//Example.java

public class Example implements IExample {
  private int counter;
  private ILeak leak;
  private static final long[] cache = new long[1000000];

public ILeak leak() {
  return new Leak(leak);
}

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

이렇게요. 이러면 없어져야 할 오래된 leak이 계속 남아서 새로운 Example의 leak 변수의 leak 변수에 담겨서 유지되겠죠. 이때.. 메모리를 먹게 하려고 크기가 좀 큰 배열을 같이 만들어 뒀습니다. 여기서 주목할 건.. Leak이 누수 됐는데 왜 Example의 배열 때문에 누수가 나느냐 입니다. 왜인지는 이미 위에서 설명했듯이

classloader-leak

leak –> Leak 클래스 –> 클래스로더 –> Example –> 엄청큰 배열

2

 

정확하게 보려면 힘덤프를 뜨고 덤프 분석 툴로 보면 되는데.. 그건… 다음에~

ps: 소스 코드는 참조한 링크에 보시면 Resources에 있습니다.

[자바 클래스 릴로딩 101] 객체, 클래스, 클래스로더

참조: 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로 바뀌게 됩니다.

[GWT] GWT 2.1에 추가될 기능

참조: http://code.google.com/intl/ko-KR/webtoolkit/doc/latest/ReleaseNotes.html

GWT 2.1 M3에 다음 기능을 추가했다.

Data Presenration 위젯

방대한 데이터 집합을 다룰 때 굉장히 효율적인 뷰를 만들 수 있다. 이 위젯은 두 가지 설계상 장점을 지니고 있다. 하나는 데이터셋의 일부를 렌더링 할 수 있다. 따라서 사용자 입장에서는 뷰 초기화가 더 빨라진다. 다른 하나는 위젯이 ‘flyweight’ 패턴을 사용해서 다른 위젯의 컨테이너가 되는게 아니라 DOM에 추가될 HTML 덩어리를 만든다. 이렇게 해서 이벤트 처리 오버헤드를 줄일 수 있다.

MVP 프레임워크

MVP 프레임워크는 뒷단 데이터를 Data Presentation 위젯에 쉽게 연결해주는 애플리케이션 프레임워크다. 이 프레임워크를 사용해서 개발자는 데이터를 보여주는데만 집중하고, Acitivities와 ActivityManager가 “presenter” 역할을 맡아 자체 액션을 처리하고, RequestFactories가 모델 변경을 인식하고 전파할거다.

이런 스타일 앱을 쉽게 만들수 있도록 스프링 Roo 1.1 M1에서는 앱 컴포넌트와 GWT의 MVP 프레임워크와 연결할 때 필요한 반복적인 코드를 생성해 준다.

서버쪽 시간 추적

Speed Tracer는 성능 문제를 감지하고 고칠 수 있는 툴이라고 언급했었다. 지금까지 이런 문제 해결을 클라이언트 쪽 코드로만 제한됐었다.

Speed Tracer 1.0 M1에서는 이제 GAE와 TC 서버 DE에서 동작하는 앱의 서버쪽 타이밍 데이터를 참조할 수 있다. 이 툴을 사용해서 데이터베이스 호출과 메모리 캐시 히트, 리소스 가져오기 등에 걸리는 시간을 참조할 수 있을 뿐 아니라 서버쪽 서비스 호출에 소요되는 시간도 볼 수 있다.