Enemy of the redeployment
그 첫번째, 우리를 당황하게 하는 ClassCastException
자바기반의 웹 애플리케이션 개발자라면 Tomcat 등으로 웹 애플리케이션을 개발하는 도중에 이상한(?) ClassCastException이 발생하여 당황했던 적이 있을 것이다.
웹 애플리케이션이라면 ClassCastException을 발생시킨 코드는 십중팔구 다음과 유사할 것이다.
아마 session 객체의 attribute에 User 타입이 아닌 객체를 넣었나 보다 라고 생각할 수 있다.
그런데 모든 코드베이스를 아무리 샅샅이 뒤져봐도 결과는 뻔하다.
이러한 코드 외에는 발견할 수 없을 것이다.
게다가 위의 예외 메시지가 말해 주듯이, sessionObj 객체는 바로 User 타입이다 <footnote>ClassCastException의 에러 메시지(여기서는 com.mycompany.myweb.User)는 캐스팅하려는 인스턴스의 클래스 이름을 보여준다</footnote>. 아니 User 인스턴스를 User로 캐스팅하는 데 왜 ClassCastException이 발생한단 말인가?
이유는 바로 User 클래스를 로딩한 클래스로더ClassLoader 인스턴스가 다르기 때문이다.
이런 상황이 발생한 과정을 살펴보면 다음과 같다.
문제는 4)의 redeploy를 위해 서블릿 컨테이너가 새로운 클래스로더를 생성<fn>반드시 이런 식으로 작동하도록 정의되어 있지는 않지만 대부분의 알려진 컨테이너는 이런 식으로 작동한다</fn>하는 데 있다. redeploy를 위해 생성된 새로운 클래스로더 인스턴스가 로딩한 User 클래스는, session에 저장된 User 인스턴스의 클래스(처음 deploy할 때 생성한 클래스로더에 의해 로딩된 User)와는 더 이상 같지 않기 않다. 비록 우리 눈에는 소스코드가 변경되지 않는 한 User 클래스 자체는 같지만(컴파일된 바이트 코드도 같다), VM 입장에서는 클래스로더가 다르므로 완전히 다른 클래스로 본다. 이런 이유로 VM은 ClassCastException을 던져 버리는 것이다. 즉 컴파일된 클래스의 동일성이 런타임runtime 시점의 클래스 동일성을 보장하지 않는다.
마찬가지로, 다음의 코드가 항상 true를 출력하는 것은 아니다.
따라서 클래스로더의 생명주기보다 긴 생명주기의 객체를 주의 깊게 사용하지 않는다면 알 수 없는 버그의 원인이 될 수 있음을 위의 예가 보여 주고 있다.
- 계속 -
그 첫번째, 우리를 당황하게 하는 ClassCastException
자바기반의 웹 애플리케이션 개발자라면 Tomcat 등으로 웹 애플리케이션을 개발하는 도중에 이상한(?) ClassCastException이 발생하여 당황했던 적이 있을 것이다.
Caused by: java.lang.ClassCastException: com.mycompany.myweb.User
.............
.............
웹 애플리케이션이라면 ClassCastException을 발생시킨 코드는 십중팔구 다음과 유사할 것이다.
Object sessionObj = session.getAttribute("LoginUser");
User user = (User)sessionObj; // ClassCastException 발생
User user = (User)sessionObj; // ClassCastException 발생
아마 session 객체의 attribute에 User 타입이 아닌 객체를 넣었나 보다 라고 생각할 수 있다.
그런데 모든 코드베이스를 아무리 샅샅이 뒤져봐도 결과는 뻔하다.
User user = .... // 로그인한 User 객체를 얻음
session.setAttribute("LoginUser", user);
session.setAttribute("LoginUser", user);
이러한 코드 외에는 발견할 수 없을 것이다.
게다가 위의 예외 메시지가 말해 주듯이, sessionObj 객체는 바로 User 타입이다 <footnote>ClassCastException의 에러 메시지(여기서는 com.mycompany.myweb.User)는 캐스팅하려는 인스턴스의 클래스 이름을 보여준다</footnote>. 아니 User 인스턴스를 User로 캐스팅하는 데 왜 ClassCastException이 발생한단 말인가?
이유는 바로 User 클래스를 로딩한 클래스로더ClassLoader 인스턴스가 다르기 때문이다.
이런 상황이 발생한 과정을 살펴보면 다음과 같다.
1) 해당 애플리케이션을 서블릿 컨테이너에 deploy하고 기동함
2) 애플리케이션의 로그인 페이지에서 로그인함
4) 자바 소스코드 수정 -> 빌드 -> 컨테이너에 redeploy
5) 버그 발생했던 페이지 다시 클릭
2) 애플리케이션의 로그인 페이지에서 로그인함
2-1) 로그인 체크의 결과로 User 객체 생성
2-2) 로그인 체크의 증표로서 User 객체를 session.setAttribute()으로 session에 저장됨
3) 테스트를 하다 보니 버그 발견4) 자바 소스코드 수정 -> 빌드 -> 컨테이너에 redeploy
5) 버그 발생했던 페이지 다시 클릭
5-1) 로그인한 사용자인지 체크해야 하므로 아래 코드 실행.
5-2) User user = (User)session.getAttribute("LoginUser");
5-3) 위 코드에서 ClassCastException 발생
5-2) User user = (User)session.getAttribute("LoginUser");
5-3) 위 코드에서 ClassCastException 발생
문제는 4)의 redeploy를 위해 서블릿 컨테이너가 새로운 클래스로더를 생성<fn>반드시 이런 식으로 작동하도록 정의되어 있지는 않지만 대부분의 알려진 컨테이너는 이런 식으로 작동한다</fn>하는 데 있다. redeploy를 위해 생성된 새로운 클래스로더 인스턴스가 로딩한 User 클래스는, session에 저장된 User 인스턴스의 클래스(처음 deploy할 때 생성한 클래스로더에 의해 로딩된 User)와는 더 이상 같지 않기 않다. 비록 우리 눈에는 소스코드가 변경되지 않는 한 User 클래스 자체는 같지만(컴파일된 바이트 코드도 같다), VM 입장에서는 클래스로더가 다르므로 완전히 다른 클래스로 본다. 이런 이유로 VM은 ClassCastException을 던져 버리는 것이다. 즉 컴파일된 클래스의 동일성이 런타임runtime 시점의 클래스 동일성을 보장하지 않는다.
마찬가지로, 다음의 코드가 항상 true를 출력하는 것은 아니다.
User user = .....; // User 객체를 어디에선가 얻어옴
System.out.println(user.getClass() == User.class); // 항상 true를 출력하지 않음!
System.out.println(user.getClass() == User.class); // 항상 true를 출력하지 않음!
따라서 클래스로더의 생명주기보다 긴 생명주기의 객체를 주의 깊게 사용하지 않는다면 알 수 없는 버그의 원인이 될 수 있음을 위의 예가 보여 주고 있다.
- 계속 -

