주요글: 도커 시작하기
반응형
웹 프로그래밍에서 기본적인 로그인/아웃의 기본 구현 개념에 대해서 살펴본다.

로그인 아웃 구현-서론

웹 프로그래밍에 도움이 될만한 팁이나 테크닉 중에서 로그인, 로그아웃, 로그인 유저 리스트 구현의 실질적인 아이디어와 구현 방법, 주의점들을 프로그래머의 입장에서 단계별로 구현해 나가는 과정을 제시해보고자 한다. 즉, 완성된 코드의 해설이 아니라, 코드를 하나씩 완성해 가며 어떤 이유로 코드를 추가하거나 변경하는지 설명하는 방식으로 전개하게 된다.

참고로, 로그인 유저 리스트의 구현은 이미 "로그인 중인 접속자의 리스트 구현" 이라는 주제의 기사로 다룬 적이 있다. 그러나, 간단한 개념만을 제시하면서 자세한 개념이나 커플링 문제 등을 처리하지 않았다. 여기서는 자세한 소스 코드와 개념을 Part I, II, III로 나누어 상세히 다루기로 한다.

  • Part I - 로그인/아웃의 구현
  • Part II - HTTP 세션 타임 아웃시 로그아웃과 커플링 제거
  • Part III - 로그인 유저 리스트와 다중 접속 방지
사용하게 될 클래스들 (Part I, II, III 전체에서)

  • MemberBean - 회원 정보 클래스
  • LoginBean - 로그인 정보 인터페이스 (초반에는 클래스로 구현, 나중에는 인터페이스화 하게 된다.)
  • LoginBeanBindingEvent - 로그인 빈의 바인딩 이벤트 클래스
  • LoginBeanBindingListener - 로그인 빈의 바인딩 리스너 인터페이스
  • LoginBeanImpl - 로그인 정보 인터페이스의 구현 클래스
  • LoginUserList - 로그인 유저 리스트를 저장하는 클래스
  • MemberManager - 회원 관련 프로세스를 위한 매니저 클래스
  • DataAccessor - 회원 데이터 접근을 위한 헬퍼 클래스
로그인의 구현

로그인의 실행은 보통 서블릿 또는 JSP 페이지에서 요청하게 된다. 이 때 로그인을 요청하는 코드는 다음과 같을 것이다.

   try {
      MemberManager.getInstance().login(
            request.getParameter("id"),request.getParameter("password"));
      // 정상적으로 로그인이 이루어진 경우 실행될 코드가 들어갈 부분
   } catch (NoSuchMemberException ex) {
      // 해당하는 아이디의 사용자가 없는 경우 실행될 코드가 들어갈 부분
   } catch (InvalidPasswordException ex) {
      // 패스워드가 틀린 경우 실행될 코드가 들어갈 부분
   } catch (ServiceNotActiveException ex) {
      // 데이터베이스 연결 등의 문제로 서비스에 문제가 생긴 경우 실행될 코드가 들어갈 부분
   }

이 코드는 간단하다. MemberManager 클래스에게 login 처리를 요청하는 것으로 반환 값은 없으며 로그인에 실패하면 Exception을 발생한다. 반환 값을 이용하는 방법과 차이는 없으며, 프로그래밍 취향에 따라 반환 값으로 로그인 성공/실패 여부를 처리하도록 변경해도 상관없다.

MemberManager 클래스

MemberManager 클래스의 login() 메소드는 어떻게 구현되어야 할까. 그러나 그보다 먼저 MemberManager.getInstance() 메소드에 대해 알아보자.

MemberManager 클래스는 많은 사용자가 동시에 보낸 요청을 처리하게 된다. 따라서 요청마다 객체를 생성하는 것은 부적합하며 단 한 개의 객체가 처리해주는 것이 좋다. 그러나 MemberManager 클래스를 static 메소드와 필드로 만드는 것은 객체지향적이지 못한 문제점을 가지게 되므로 싱글톤 패턴으로 구현한다.

   public class MemberManager {
   
      private static MemberManager instance = null;
   
      protected MemberManager() {
      }
   
      public static MemberManager getInstance() {
         if (instance == null) {
            instance = new MemberManager();
         }
         return instance;
      }
   }

간단히 구현된 MemberManager 클래스는 이제 new 연산자로 생성할 수 없고 반드시 getInstance() 메소드를 통해서 객체의 레퍼런스를 얻어야 한다. 따라서 instance 라는 이름의 static 필드에 의해 유지되는 단 한 개의 객체만 존재하게 된다.

login() 메소드

이제 MemberManager 클래스에 login() 메소드를 추가한다.

   public void login(String id, String password) 
          throws NoSuchMemberException, InvalidPasswordException, ServiceNotActiveException {
      // 로그인 처리 과정을 위한 코드가 들어갈 부분, 실패시에는 해당하는 Exception 발생
   }

로그인 처리는 어떻게 할 것인가. 아이디와 패스워드를 검사하는 것만으로 로그인을 처리하게 된다. 아이디와 패스워드가 데이터베이스에 저장되어 있다면 데이터베이스로부터 아이디와 패스워드를 쿼리하는 코드가 필요하다.

checkPassword() 메소드

아이디와 패스워드를 검사하는 checkPassword() 메소드를 만들어 로그인이나 패스워드 검사가 필요한 부분에서 사용할 수 있도록한다.

   public void checkPassword(String id, String password) 
          throws NoSuchMemberException, InvalidPasswordException, ServiceNotActiveException {
      Connection connection = null;
      PreparedStatement pstmt = null;
      ResultSet rs = null;
      // 데이터베이스 connection 얻는 코드가 들어갈 부분 (생략)
      try {
      String sql = "select password from members where id=?";
         pstmt = connection.prepareStatement(sql);
         pstmt.setString(1,id);
         rs = pstmt.executeQuery();
         if (rs.next()) {
            if (password.equals(rs.getString(1))) {
            } else throw new InvalidPasswordException();
         } else throw new NoSuchMemberException();
      } catch (SQLException ex) {
         throw new ServiceNotActiveException();
      } finally {
         try { rs.close(); } catch (SQLException ex) {} finally { rs = null; }
         try { pstmt.close(); } catch (SQLException ex) {} finally { pstmt = null; }
      // 데이터베이스 connection을 닫거나 커넥션 풀로 반환하는 코드 부분 (생략)
      }
   }

checkPassword 메소드에서 데이터베이스 커넥션을 생성하는 부분은 사용하는 JDBC 드라이버나 커넥션 풀의 구현 또는 사용 여부에 따라 달라질 수 있으므로 생략한다.

DataAccessor 클래스

데이터베이스 연결과 관련된 메소드들은 MemberManager 클래스로부터 따로 떼어내서 헬퍼 클래스로 만드는 것이 좋다. 여기서는 MemberManager 클래스가 사용할 DataAccessor 클래스를 따로 만들어서 checkPassword() 메소드를 포함시킨다. 편의상 DataAccessor 클래스도 MemberManager와 마찬가지로 싱글톤 패턴으로 구현한다. (반드시 그럴 필요는 없다.)

이제 MemberManager 클래스에는 다음과 같은 필드가 추가되고 생성자에도 다음과 같은 코드가 추가된다.

   private DataAccessor accessor = null;

   protected MemberManager() {
      accessor = DataAccessor.getInstance();
   }

그리고, MemberManager 클래스의 login() 메소드의 로그인 처리과정 코드에는 이제 다음과 같은 코드가 들어간다.

   accessor.checkPassword(id, password);

checkPassword() 메소드는 로그인 실패시에 Exception을 발생시키며 login() 메소드는 Exception을 처리하지 않고 그대로 다시 login() 메소드를 호출한 코드로 던진다. 결국 처음에 나온 서블릿 또는 JSP 페이지에서 Exception 들을 처리하게 된다.

세션과 LoginBean 클래스

다음엔 로그인 상태를 세션에서 유지시키는 과정이 필요하다. 로그인 이후에 다른 웹 페이지들을 요청할 때 로그인한 상태인지 확인할 수 있는 가장 좋은 방법중에 하나가 세션을 사용하는 방법이다.

세션에 로그인 상태를 저장하기 위하여 LoginBean 클래스를 만든다. LoginBean 클래스는 다음과 같다.

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

login() 메소드의 수정

그 후에 MemberManager 클래스의 login() 메소드에 다음과 같은 코드를 추가한다.

   LoginBean login = new LoginBean(id);
   session.setAttribute(LOGIN_BEAN, login);

이제 몇가지 문제가 생겼다. session 객체는 어디서 얻는가와 LOGIN_BEAN 상수 선언이다. 상수 선언은 간단하다. MemberManager 클래스에

   private static final String LOGIN_BEAN = "JavacanLoginBean";

이라는 상수필드를 추가하면 된다.

session 객체는 요청을 처음 받은 서블릿/JSP 페이지로부터 받아와야 한다. 따라서 login() 메소드에 파라미터가 하나 추가된다. login() 메소드의 시그너쳐는 다음과 같이 바뀐다.

   public void login(HttpSession session, String id, String password)
          throws NoSuchMemberException, InvalidPasswordException, ServiceNotActiveException {
   }

이제 login() 메소드를 호출하는 서블릿/JSP 페이지에서 메소드 호출도 바뀐다.

   MemberManager.getInstance().login(
         request.getSession(true), 
         request.getParameter("id"),
         request.getParameter("password"));

이제 session의 전달에는 아무런 문제가 없어졌다.

logout() 메소드

다음은 logout() 메소드를 MemberManager 클래스에 추가한다. 웹페이지에서 로그아웃 버튼을 누르면 연결된 서블릿 또는 JSP 페이지는 로그인의 경우와 마찬가지로 다음과 같은 코드로 처리하게 된다.

   try {
      MemberManager.getInstance().logout(request.getSession(true));
      // 정상적으로 로그아웃이 이루어진 경우 실행될 코드가 들어갈 부분
   } catch (NoSuchMemberException ex) {
      // 해당하는 아이디가 이미 로그아웃 되어 있는 경우 실행될 코드가 들어갈 부분
   } catch (ServiceNotActiveException ex) {
      // 내부적으로 서비스에 문제가 생긴 경우 실행될 코드가 들어갈 부분 (이 경우는 필요없을지도 모른다.)
   }

이제 MemberManager 클래스는 logout() 메소드를 구현해야 한다.

   public void logout(HttpSession session)
          throws NoSuchMemberException, ServiceNotActiveException {
      try {
         if (session.getAttribute(LOGIN_BEAN) == null) {
            throw new NoSuchMemberException();
         } else {
            session.removeAttribute(LOGIN_BEAN);
         }
      } catch (Exception ex) {
         throw new ServiceNotActiveException();
      }
   }

로그인과 로그아웃의 처리는 이로서 간단하게 구현되었다.

Exception 클래스들

Exception 클래스들은 다음과 같이 구현된다.

   public class ServiceNotActiveException extends Exception {
   }
   
   public class NoSuchMemberException extends Exception {
   }
   
   public class InvalidPasswordException extends Exception {
   } 

모두 별도의 코드는 필요없으며 Exception 클래스를 상속하기만 하면 된다. 예외의 경우를 인식하기 위한 클래스들이기 때문이다. 나중에 필요하면 메소드나 필드를 추가하게 될 것이다.

결론

로그인과 로그아웃의 기본적인 구현 알고리즘과 개념을 살펴보았다. Part II 에서는 세션 타임 아웃으로 인한 로그아웃의 구현을 추가하고 커플링(coupling)을 제거하기 위한 방법을 제시할 것이다. 그리고 Part III 에서는 로그인 유저 리스트의 구현과 접근에 유지 방법, 그리고 다중 접속을 막는 방법 등을 제시한다.



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

+ Recent posts