카테고리 없음

프로젝트 진행 ③ : 메인 페이지로 리다이렉트, 아이디 찾기 기능 구현 (메일 API)

OhPro 2022. 7. 4. 00:35
반응형

목차

  • 시작하며
  • 포워딩 + 리다이렉트
  • 아이디 찾기 기능 구현
  • 아쉬운 점(개선하고 싶은 점)
  • 마치며

 


 

시작하며

저번 포스팅부터 현재까지 가장 큰 추가/변화라고 하면 아이디 찾기 기능 구현이 아닐까 싶다. 물론 아쉬운 점도 분명히 있다. 이게 일정을 짜고 진행하는건 좋은데 쫒기는 기분이 들기도 한다. 그래서 기능을 우선으로 구현한 후에도 계속 리펙토링이나 구조 개선이 일어날 것같다!

 


 

포워딩 + 리다이렉트

만약 첫 포스팅을 봤다면 기존에 내가 스프링 프레임워크를 사용하지 않으면서 디스패쳐 서블릿을 어떻게 만들었는지 알 것이다. 기존에는 RequestDispatcher Class를 Request에서 획득하여 포워딩하는 방식으로 페이지 응답을 했었다.

 

그런데 포워딩을 사용하면 브라우저 상단에 마지막 url 요청이 "/login"과 같은 형태로 남게된다. 이 때 새로고침을 하게되면 어떻게 될까? 혹은 뒤로가기를 누르면 어떻게 되지? 와 같은 생각이 들었다.

 

보통 페이지를 이동할 때는 크게 포워딩이나 리다이렉팅 둘 중 하나를 한다. 그런데 일반 요청같은 경우엔 성능같은 점을 고려하여 포워딩을 해주지만, 로그인과 같은 뭔가 권한을 획득한다던가 글을 작성함으로서 중요한 액션이 일어나는 경우에는 리다이렉팅을 하는 것이 바람직하다는 것을 구글링을 통해 알아냈다.

 

그러면 필연적으로 리다이렉팅과 포워딩을 선택하여 적당하게 사용할 수 있어야 한다는 이야기가 된다. 리다이렉팅 하는 방법 자체는 어렵지 않았다. response.sendRedirect("리다이렉팅할 주소") 와 같이 사용하면 되기 때문이다.

 

좀 많이 고민했던 부분은 '어떤 방식으로 포워딩과 리다이렉팅을 선택할 것인가?' 였다.

 

 

내가 선택한 방법은 컨트롤러에서 뷰 이름만 받지말고 뷰 이름 + 뷰에 대한 정보를 담는 클래스도 함께 반환받는 것이었다. 그런데 항상 클래스를 반환해야 하면 간단한 페이지 요청같은 경우에 불합리(?)한 상황이 발생할 수 있으므로 컨트롤러가 뷰 이름만 String 형태로 반환해도 되고 뷰 클래스를 반환해도 되는 구조를 만들어야 겠다고 생각했다.

 

그래서 이 구조를 만들려고 하다보니 스프링 프레임워크에 ModelandView객체를 떠올렸고 ModelandView객체를 구현하려고 했었다. 그런데 request, response외에 세션(session)에 접근해야 할 수도 있고 ModelandView를 구현하다보면 필연적으로 항시 request의 attribute에 정보를 넣었다 뺐다를 해야한다. 

 

사실 비용이 그렇게 크게 들진 않을 수도 있다. 하지만 현재 HttpServletRequest와 같은 웹 객체와 굳이 분리시켜야할 마땅한 이유는 없기 때문에 ModelandView가 아닌 ViewInfo라는 이름으로 아래처럼 클래스를 만들었다.

(스프링은 그걸 시도하고 있는걸로 알고있다.)

 

public class ViewInfo {
    String viewName = "";
    boolean redirectRequire = false;

    public ViewInfo() {

    }

    public ViewInfo(String viewName) {
        this.viewName = viewName;
    }

    public void setRedirectRequired() {
        this.redirectRequire = true;
    }

    public void setNotRedirectRequired(){
        this.redirectRequire = false;
    }

    public boolean isRedirectRequire() {
        return redirectRequire;
    }

    public String getViewName() {
        return viewName;
    }

    public void setViewName(String viewName) {
        this.viewName = viewName;
    }

}

 

그리고 ControllerTemplete의 executeGetRequest, executePostRequest 메서드에서 반환받은 뷰에 대한 정보가 String인지 ViewInfo인지 체크해서 둘 다 아니라면 판별할 수 있는 근거가 없으므로 "pageNotFound"를 반환해주고 둘 중 하나였다면 ViewInfo로 바꿔주는 작업을 템플릿(TempleteMethod)에 추가했다. 당연히 리턴 타입은 IController의 execute부터 ViewInfo를 반환하도록 수정했다.

 

    @Override
    public ViewInfo execute(HttpServletRequest req, HttpServletResponse resp) {
        this.req = req;
        this.resp = resp;

        Object object = null;
        ViewInfo viewInfo = null;

        switch (req.getMethod()) {
            case "GET" : {
                object = executeGetRequest();
                break;
            }

            case "POST" : {
                object = executePostRequest();
                break;
            }
        }

        if (object instanceof String)
            viewInfo = new ViewInfo((String) object);
        else if (object instanceof ViewInfo)
            viewInfo = (ViewInfo) object;
        else
            viewInfo = new ViewInfo("pageNotFound");

        return viewInfo;
    }

 

그리고 디스패쳐 서블릿에 아래처럼 리다이렉팅 여부를 묻는 코드를 추가했다.

 

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        IController controller = null;
        controller = contollerMapping.getHandler(req.getRequestURI());
        String uri = req.getRequestURI();

        ViewInfo viewInfo = controller.execute(req, resp);
        String viewName = viewResolver.getViewName(viewInfo.getViewName());

        if (viewInfo.isRedirectRequire()) {
            resp.sendRedirect(viewInfo.getViewName());
            return;
        }
        
        RequestDispatcher requestDispatcher = req.getRequestDispatcher(viewName);
        requestDispatcher.forward(req, resp);
    }

 

이렇게 리다이렉팅이 되면 사용자가 마지막으로 "/login"을 요청했더라도 페이지 응답이 된 후에는 정상적으로 "/main"처럼 url이 변하게 된다.

 

 


 

아이디 찾기 기능 구현

내가 처음 생각했던 아이디 찾기 과정은 다음과 같다.

 

아이디 찾기 과정

 

이 과정을 따르려면 이메일 인증 기능을 구현해야 하는데, 이메일을 송신하는 데에는 javax.mail 라이브러리를 사용했다. 메일 api를 구현하는 방법은 이 글을 참고하면 좋을 것이다. 내가 직접 구글링 + 디버깅 + 고뇌를 하면서 알아낸 정보들이다.. 참고 소스도 있으니 괜찮을 거라고 생각한다.

 

처음엔 단순히 메일 api를 '쓰기만 하면 되겠지' 라는 생각으로 자료를 찾아보고 예제 소스를 찾아봤었으나, 도통 파라미터들도 그렇고 와닿지가 않았다. 이러다간 그냥 누군가 해놓은걸 베끼는 것과 같을 것 같아서 공식 문서나 파라미터의 의미, 각종 용어들의 정의와 유래등을 찾아보면서 개념을 익혔다. 덕분에 다음에 쓰게 되더라도 어렵지 않게 바로 써먹을 수 있을 것 같다. (^^)

 

아이디 찾기 기능을 순서대로 나열하면 아래와 같다.

  1. 이메일 정보를 입력 폼에 입력하고 인증번호 받기 버튼을 클릭한다.
  2. url 요청을 받은 디스패쳐 서블릿이 IdCertificationController에게 일을 위임한다.
  3. IdCertificationController는 요청을 처리하기 위해 입력받은 이메일 정보를 FindAccountService객체에게 넘겨준다.
  4. FindAccountService는 accountDao를 사용하여 메일 정보가 있는지 체크한 후 메일이 유효하면 해당 주소로 메일을 보낸다.
  5. 유저가 자신의 이메일을 확인하여 인증번호를 알아낸 후 입력 폼에 입력하고 인증버튼을 클릭한다.
  6. url 요청을 받은 디스패쳐 서블릿이 이번에는 url-request 매핑 정보대로 FindIdController에게 일을 위임한다.
  7. FindIdController는 사용자로부터 입력받은 인증번호와 이전에 생성했던 난수를 비교하고 일치한다면 FindAccountService를 호출하고FindAccountService는accountDao를 사용하여 이메일 정보를 매개로 아이디를 알아낸다.
  8. FindIdController는 알아낸 아이디를 request.setAttribute()를 사용하여 저장시킨 후에 저장되어있던 인증번호와 이메일 정보를 삭제하고 디스패쳐 서블릿에게 뷰 정보를 반환해준다.

 

위에서 보면 알겠지만 메일로 인증번호 받기(요청)하면서 이메일 정보가 계정 정보 테이블에 존재하는지 체크하고 인증번호 확인(요청)을 받았을 때 이전에 받았던 이메일 정보와 첫 요청에서 생성된 인증번호를 기억하고 있어야 했다.

 

그래서 생각한 방법이 두 가지 있는데 첫 번째는 DB에 저장하고 있다가 가져오는 것이고, 두 번째는 세션에 저장해두고 가져오는 것이다. 

 

DB에 저장
VS
세션에 저장

 

지금 생각으로는 세션에 저장하는게 더 간단할 것이라고 생각하는데, 간단하고 보안에 취약하면 그것도 문제다.. 그래도 세션은 서버에 저장되는 값이니 괜찮지 않을까 싶다..!! 아무튼 일단은 세션에 저장하려고 한다.

(괜찮으시다면 댓글로 팁좀 주십셔....ㅠㅠ)

 

여기서 세션에 저장되는 정보는 2가지로 사용자가 처음에 입력한 이메일 주소와 인증번호인데 세션 어트리뷰트에 따로 저장해두는 것보단 하나의 객체로 저장해두는게 괜찮다고 생각해서 아래와 같이 만들었다.

 

public class IdCertificationInfo {

    String certificationValue = "";
    String emailAddress = "";

    public IdCertificationInfo() {}

    public IdCertificationInfo(String certificationValue, String emailAddress) {
        this.certificationValue = certificationValue;
        this.emailAddress = emailAddress;
    }

    public String getCertificationValue() {
        return certificationValue;
    }

    public void setCertificationValue(String certificationValue) {
        this.certificationValue = certificationValue;
    }

    public String getEmailAddress() {
        return emailAddress;
    }

    public void setEmailAddress(String emailAddress) {
        this.emailAddress = emailAddress;
    }

}

 

이렇게 만든 인증용 클래스를 FindAccountService Class에서 아래와 같이 사용해준다.

 

public class FindAccountService {

    String certificationValue = "";

    public IdCertificationInfo sendMailForFindId(String email){
        if (getAccountIdFromEmail(email).equals(""))
            return null;

        String certificationValue = getRandomValue();

        MailUtil mailUtil = new MailUtil();
        mailUtil.setTitle("[비하인드] 요청하신 인증번를 알려드립니다.\n");

        StringBuffer sb = new StringBuffer();
        sb.append("<h3 style='color:black'> 요청하신 인증번호를 알려드립니다.</h3>\n");
        sb.append("<h3 style='color:black'>아래의 인증번호를 인증번호 입력 창에 입력해 주세요.</h3>\n\n");
        sb.append("<h2 style='color:red'>" + certificationValue + "</h2>");
        sb.append("\n\n<h3 style='color:black'>감사합니다.</h3>");
        String messageText = sb.toString();

        mailUtil.setContentText(messageText);

        mailUtil.sendMail(email);

        return new IdCertificationInfo(certificationValue, email);
    }

    public String getRandomValue(){
        String value = Integer.toString((int)(Math.random() * 1000000));
        setCertificationValue(value);
        return value;
    }

    public void setCertificationValue(String certificationValue) {
        this.certificationValue = certificationValue;
    }

    public String getCertificationValue() {
        return certificationValue;
    }

    public String getAccountIdFromEmail(String email) {
        AccountDao accountDao = new AccountDao();
        String userId = accountDao.getAccountIdFromEmail(email);
        if (userId.equals(""))
            return "";
        return userId;
    }
}

 

위를 보면 알겠지만 대충 FindAccountService가 하는 일은 크게 3가지로 아래와 같다.

 

  • 인증번호 생성 (6자리 난수)
  • dao를 사용하여 계정 정보 획득
  • 존재하는 유저에게 이메일 전송

 

FindAccountService를 FindIdController와 IdCertificationController, 추후에는 비밀번호 관련 컨트롤러에서도 사용하게 될 것이다.

 

다른 컨트롤러 소스들은 더보기를 클릭하면 볼 수 있게 해두겠다.

더보기

IdCertificationController

public class IdCertificationController extends ControllerTemplete {
    @Override
    public Object executeGetRequest() {
        return "findId";
    }

    @Override
    public Object executePostRequest() {
        String email = (String) req.getParameter("email");
        String domain = (String) req.getParameter("domain");
        String emailAddr = email + "@" + domain;

        FindAccountService findAccountService = new FindAccountService();
        IdCertificationInfo certificationInfo = findAccountService.sendMailForFindId(emailAddr);
        if (certificationInfo.equals(null)) {
            req.setAttribute("userIdMessage", "이메일 정보가 잘못되었습니다 !!");
            return "findId";
        }

        req.setAttribute("userIdMessage", "이메일을 확인해주세요 !!");

        HttpSession httpSession = req.getSession();
        httpSession.setAttribute("idCertificationInfo", certificationInfo);
        return "findId";
    }
}

 

FindIdController

public class FindIdController extends ControllerTemplete {

    @Override
    public Object executeGetRequest() {
        HttpSession httpSession = req.getSession();
        httpSession.setAttribute("idCertificationInfo", null);
        return "findId";
    }

    @Override
    public Object executePostRequest() {
        String inputValue = (String) req.getParameter("certificationInput");

        HttpSession httpSession = req.getSession();
        IdCertificationInfo certificationInfo =
                (IdCertificationInfo) httpSession.getAttribute("idCertificationInfo");

        if (!inputValue.equals(certificationInfo.getCertificationValue())){
            req.setAttribute("userIdMessage", "인증번호가 잘못되었습니다 !!");
            return "findId";
        }

        FindAccountService findAccountService = new FindAccountService();
        String accountId = findAccountService.getAccountIdFromEmail(certificationInfo.getEmailAddress());
        httpSession.setAttribute("idCertificationInfo", null);

        req.setAttribute("userIdMessage", "당신의 아이디는 '" + accountId + "' 입니다 !!");

        return "findId";
    }
}

 

조금 부연 설명을 하면 사용자가 인증 중간에 뒤로가기를 클릭하고 다시 들어온 경우, get 메서드로 페이지를 요청할텐데 이럴 때 세션에 만약에라도 저장되어있는 인증 객체가 있다면 초기화하는 구문을 넣었다.

 

이제 이메일을 넣고 인증번호 받기를 클릭하면

 

아이디 찾기 인증메일 발송 직후

 

아래처럼 유저 메일함에 메일이 날라온다.

 

유저 메일함 예시

 

이 인증번호를 아래처럼 입력하고 인증을 누르게 되면!!

 

아이디 찾기 완료

 

아이디를 찾을 수 있다.

 


 

마치며

정말 많이 고민했던 포스팅이 아니었나 싶다. 왜냐면 리다이렉팅과 포워딩 구조도 생각해야 했고, 메일 api 사용법을 익히면서 페이지 시나리오까지 생각해야 했기 때문이다. 물론 생각했다기보단 더 나은 구조로 플로우 차트와 IA에 부합되게 만들려고 하다보니 이렇게 된 것 같다.

열심히 해야겠다..

 

감사합니다!!

 

반응형