주요글: 도커 시작하기
반응형
세션 타임 아웃으로 인한 로그아웃의 구현과 클래스간의 커플링 제거에 대해 알아본다.

세션 타임 아웃과 로그아웃

웹 프로토콜인 HTTP 프로토콜이 텔넷, FTP 등과는 달리 연결 상태를 유지하지 않으므로, 사용자가 로그아웃 버튼을 눌러서 반드시 로그아웃을 확실히 해주지 않고 일방적으로 접속을 끊으면 서버는 로그아웃을 확인할 수 없다. 즉, 사용자가 로그아웃하지 않고 브라우저를 종료해서 접속을 끝내고 다른 일을 하거나 컴퓨터를 꺼도 전혀 알 수가 없다.

보통 이런 상황을 극복하기 위하여 대부분의 웹서비스는 시간제한이라는 방법을 사용한다. 사용자로부터 일정시간동안 접속이 없으면 로그아웃한 것으로 간주하고 자동으로 세션을 종료하는 것이다. 대부분의 JSP/서블릿 컨테이너는 세션 타임 아웃 기능이 있으므로, 정해진 시간이 지나면 해당하는 세션이 종료된 것으로 간주되어 세션 객체를 제거하게 된다.

바로 이때, 세션 객체를 제거하기 전에, 세션에 연결된 모든 attribute 객체들에 대한 참조를 제거하게 된다. 쉽게 말하면, 세션에 setAttribute() 메소드로 추가된 객체들이 removeAttribute() 메소드가 호출되어 제거된다는 것이다. 모든 attribute 객체들에 대한 참조가 제거된 후에 비로소 세션 객체가 제거된다. 여기서 세션 타임 아웃을 로그아웃에 이용할 수 있는 방법이 나온다.

HttpSessionBindingListener 인터페이스

javax.servlet.http 패키지에는 HttpSessionBindingListener 인터페이스가 존재한다. HttpSessionBindingListener 인터페이스에는 두개의 메소드, 즉, valueBound() 메소드와 valueUnbound() 메소드가 있다.

세션 객체에 setAttribute() 메소드로 어떤 객체가 추가될 때. 추가되는 객체가 HttpSessionBindingListener 인터페이스를 구현하였을 경우, 이 객체의 valueBound() 메소드를 자동으로 호출해준다. HttpSessionBindingListener 인터페이스를 구현하지 않았을 경우는 당연히 valueBound() 메소드를 호출할 수 없으므로 아무 메소드도 자동으로 호출하지 않는다.

같은 방법으로 removeAttribute() 메소드로 어떤 객체가 세션 객체로부터 제거될 때, 객체가 HttpSessionBindingListener 인터페이스를 구현하였을 경우에만 이 객체의 valueUnbound() 메소드를 자동으로 호출해준다. 그렇다면 자동으로 호출되는 이 메소드를 이용하여 세션 타임 아웃으로 인한 로그아웃을 구현하여 본다.

LoginBean 클래스의 HttpSessionBindingListener 인터페이스 구현

로그인 또는 로그아웃을 할 때 세션 객체에 추가되거나 제거되는 객체가 어느 것일까. 바로 LoginBean 객체이다. 따라서 이 LoginBean 객체가 HttpSessionBindingListener 인터페이스를 구현하여야 한다.

   public class LoginBean implements HttpSessionBindingListener {
   
      private String id = null;
   
      public LoginBean(String id) {
         this.id = id;
      }
   
      public String getId() {
         return id;
      }

      public void valueBound(HttpSessionBindingEvent event) {
         // 세션 객체에 바로 이 객체(this)가 추가될 때 자동으로 호출되는 메소드.
      }

      public void valueUnbound(HttpSessionBindingEvent event) {
         // 세션 객체로부터 바로 이 객체(this)가 제거될 때 자동으로 호출되는 메소드.
      }
   }

그러나, 지금까지의 이유로는 객체가 추가되거나 제거될 때 특별히 수행할 메소드가 없다. 왜냐하면 아직까지 구현된 logout() 메소드는 단지 세션 객체로부터 LoginBean 객체를 제거하는 것이 전부이기 때문이다. 위의 LoginBean 클래스의 valueUnbound() 메소드는 이미 타임아웃으로 인하여 자동으로 호출되든 아니면 logout() 메소드에서 직접 제거함으로써 호출되든 LoginBean 객체가 제거되기 때문에 호출되는 것이다. 즉, 타임아웃으로 세션이 제거되면 로그아웃 버튼을 누른 것과 동일한 효과가 발행한다는 뜻이다. 이건 일부러 HttpSessionBindingListener 인터페이스를 구현하지 않아도 마찬가지이다.

여기서 잠시 코드를 멈추고 생각을 할 필요가 있다. 로그인 또는 로그아웃이 기계적인 세션 유지 이외에 다른 할 일이 있을까? 바로 이 부분에서 앞으로 코드를 어떻게 구현할지 달라지게 된다. 할 일이 없다면 더 이상 고생할 필요가 없다. 그러나, 현재 접속 중인 사용자들을 알고 싶다거나, 로그인과 로그아웃한 사용자의 아이디와 시간을 기록하고 싶다면 이야기가 달라진다.

LoginBeanBindingListener 인터페이스와 LoginBeanBindingEvent 클래스

이제 LoginBean 클래스는 한번 더 변신하게 된다. 스스로 이벤트를 발생하는 클래스가 된다. 마치 Button 클래스가 리스너로 등록된 클래스들에게 버튼이 눌리면 이벤트 클래스를 발생시켜 메소드를 호출해 주는 것과 같다. 이제 LoginBeanBindingListener 인터페이스와 LoginBeanBindingEvent 클래스의 코드는 다음과 같다.

   public interface LoginBeanBindingListener {

      public void loginPerformed(LoginBeanBindingEvent event);
      public void logoutPerformed(LoginBeanBindingEvent event);

   }

   public class LoginBeanBindingEvent {

      private LoginBean login = null;

      public LoginBeanBindingEvent(LoginBean login) {
         this.login = login;
      }
   
      public LoginBean getLoginBean() {
         return login;
      }
   }

LoginBeanBindingListener 인터페이스는 자바의 ActionListener 인터페이스와 같은 다른 리스너들과 별로 다를 것이 없다. LoginBeanBindingEvent 클래스는 코드에서 보이는 것처럼 간단히 LoginBean 객체를 전달하는 이벤트 클래스일 뿐이다. 너무 간단해 보일지 모르지만 중요한 것은 프로그램의 구조이다.

LoginBean 클래스의 수정

LoginBeanBindingListener 인터페이스를 리스너로 등록할 수 있는 LoginBean 클래스의 변한 모습을 살펴보자.

   public class LoginBean implements HttpSessionBindingListener {
   
      private LoginBeanBindingListener listener = null;
      private String id = null;
   
      public LoginBean(String id, LoginBeanBindingListener listener) {
         this.id = id;
         this.listener = listener;
      }
   
      public String getId() {
         return id;
      }

      public void valueBound(HttpSessionBindingEvent event) {
         if (listener != null) 
            listener.loginPerformed(new LoginBeanBindingEvent(this));
      }

      public void valueUnbound(HttpSessionBindingEvent event) {
         if (listener != null) 
             listener.logoutPerformed(new LoginBeanBindingEvent(this));
      }
   }

LoginBean 클래스는 생성할 때 리스너를 등록받는다. 그리고 LoginBean 객체가 세션에서 추가되면 리스너의 loginPerformed() 메소드를 호출해주고, 세션에서 제거되면 리스너의 logoutPerformed() 메소드를 호출해준다. 리스너를 등록하거나 제거하는 메소드를 필요하면 추가해도 좋다. 이 경우 간단히 addLoginBeanBindingListener() 메소드와 removeLoginBeanBindingListener() 메소드를 추가하면 된다. 그러나 이럴 경우 몇가지 기준을 정해야 한다. 리스너가 두개 이상 추가되면 두개를 차례대로 호출해주는 멀티캐스트 방식을 사용할 것이지, 마지막 리스너만 등록되는 단일 리스너 방식을 적용할 것이지 정해야 한다. 여기서는 간단히 단일 리스너 방식을 사용하고 객체 생성시에 생성자를 통해 리스너를 등록하기로 하자.

LoginBean 클래스의 생성자가 변경되었으므로 MemberManager 클래스의 login() 메소드에서 new LoginBean() 으로 LoginBean 객체를 생성하는 부분도 변경되어야 한다. 파라미터가 변경되어야 하기 때문이다. LoginBeanBindingListener 인터페이스를 구현한 객체를 파라미터로 LoginBean 객체를 생성해야 하는데 이 문제는 어떻게 해결할 것인가. 이 부분은 MemberManager 클래스의 LoginBeanBindingListener 인터페이스 구현과 함께 해결된다.

MemberManager 클래스의 LoginBeanBindingListener 인터페이스 구현

이제 LoginBean 클래스의 준비가 되었으니 현재 접속 중인 사용자들을 알고 싶다거나, 로그인과 로그아웃한 사용자의 아이디와 시간을 기록하고 싶다면 어떤 클래스가 담당해야 할 것인가를 생각해보자. 아마도 독립적인 클래스를 만들어야 할 것이다. 그렇다면 점점 더 복잡해지는 이 구조를 단순하고 통일된 구조로 만들 수 없을까? 바로 MemberManager 클래스에서 해답을 얻을 수 있다. MemberManager 클래스는 로그인/로그아웃에 관련된 모든 행위(behavior)를 담당하는 매니저 클래스이다. 따라서 행위에 관련된 것들은 매니저 클래스에게 일임한다.

로그인/로그아웃한 시간 기록이나 접속중인 사용자 리스트와 같은 부가적으로 처리하기 위한 행위를 담당하기 위해서 MemberManager 클래스는 LoginBeanBindingListener 인터페이스를 구현하기만 하면 된다.

   public class MemberManager implements LoginBeanBindingListener {

      // 지금까지 MemberManager 클래스에 추가된 코드들은 수정된 부분이 없으므로 생략.

      public void loginPerformed(LoginBeanBindingEvent event) {
         // 여기에 로그인과 관련되어 부가적으로 처리할 코드들이 들어간다.
      }
   
      public void logoutPerformed(LoginBeanBindingEvent event) {
         // 여기에 로그아웃과 관련되어 부가적으로 처리할 코드들이 들어간다.
      }
   }

이제 위에 구현된 loginPerformed() 메소드와 logoutPerformed() 메소드를 이용하여 부가적인 행위를 처리하는 코드를 추가하면 세션 타임 아웃과 같은 문제가 발생하지 않고 통합된 관리를 할 수 있게 된다.

그리고 login() 메소드에서 LoginBean 객체를 생성하는 코드 부분은 다음과 같이 변경하면 LoginBean 객체를 생성할 때 간단히 LoginBeanBindingListener 인터페이스를 구현한 객체를 전달할 수 있다.

   원래 코드 :
   LoginBean login = new LoginBean(id);

   변경된 코드 :
   LoginBean login = new LoginBean(id, this);

클래스 커플링 문제와 해결

지금까지 작성된 코드에 단 한가지 문제점이 발생한다. 바로 클래스 커플링 문제이다. 클래스 커플링이란 간단히 말하면 클래스간에 상호참조가 발생한다는 것으로, 마치 닭과 달걀에 관한 문제와 같다.

어느 부분에서 커플링이 발생할까. LoginBean 클래스에서 출발하면 LoginBean 클래스를 구현하려면 HttpSessionBindingListener 인터페이스와 HttpSessionBindingEvent 클래스의 존재를 알고 있어야 한다. 그런데, HttpSessionBindingEvent 클래스는 다시 LoginBean 클래스의 존재를 알고 있어야 한다. 여기서 커플링이 발생한다. 커플링을 제거하는 가장 간단한 방법은 인터페이스를 사용하는 것이다.

LoginBean 클래스는 모든 클래스에서 참조되는 기본적인 상태 클래스이다. 따라서 LoginBean 클래스를 인터페이스로 추상화하여 커플링을 제거한다. 간단히 LoginBean 클래스를 인터페이스로 변환하고, 기존의 LoginBean 클래스는 LoginBeanImpl 클래스로 변경한다.

새로운 LoginBean 인터페이스와 LoginBeanImpl 클래스

새로운 LoginBean 인터페이스의 코드는 다음과 같다. 아직까지는 특별한 메소드를 정의할 이유가 없지만 나중에 사용될 메소드를 일단 한 개만 정의해두자. 필요하면 나중에 추가하면 된다.

   public interface LoginBean {

      public String getId();

   }

그리고 기존의 LoginBean 클래스는 이름을 변경하여 LoginBeanImpl 클래스로 작성한다. 파일 이름, 클래스 이름, 생성자 이름 등을 바꾸어주면 간단히 변경할 수 있다. 그리고 LoginBean 인터페이스를 구현하는 것을 추가한다. LoginBeanImpl 클래스는 다음과 같다.

   public class LoginBeanImpl implements LoginBean, HttpSessionBindingListener {

      // 생성자가 클래스 이름과 같이 바뀐다는 것 이외에는 수정되는 부분이 없다.
      // LoginBean 인터페이스의 getId() 메소드는 처음부터 구현되어 있었다.

   }

그리고 마지막으로 하나 더 수정하여야 한다. LoginBean 인터페이스는 클래스가 아니라서 new 연산자로 생성할 수 없으므로 MemberManager 클래스의 login() 메소드에서 new LoginBean() 부분은 new LoginBeanImpl() 코드로 변경되어야 한다. MemberManager 클래스의 login() 메소드는 이제 다음과 같다.

   public void login(HttpSession session, String id, String password)
         throws NoSuchMemberException, InvalidPasswordException, ServiceNotActiveException {
      accessor.checkPassword(id, password);
      LoginBean login = new LoginBeanImpl(id, this);
      session.setAttribute(LOGIN_BEAN, login);
   }

결론

Part II 에서는 세션 타임 아웃으로 인한 로그아웃의 구현과 클래스간의 커플링 제거에 대해 알아보았다. 이러한 과정에서 객체지향 프로그래밍에서 개념에 따른 코드의 추가와 변경이 어떻게 이루어지는가를 제시하였다. Part III 에서는 접속중인 사용자들의 리스트를 알기 위한 로그인 유저 리스트를 구현해보고, 다중 접속을 막는 방법에 관해서도 알아본다.



본 글의 저작권은 이동훈에 있으며 저작권자의 허락없이 온라인/오프라인으로 본 글을 유보/복사하는 것을 금합니다.

+ Recent posts