개주 훈련일지/🏋️ 전집중 호흡 훈련

비밀번호 찾기 UX 개선: 아이디 확인 → 이메일 자동 세팅 → 인증코드 발송(AJAX/Servlet)

lshfood2 2025. 12. 31. 22:32

[ 구현 배경 ]

비로그인 상태에서 비밀번호 재설정을 만들 때,

단순히 “이메일 입력 → 코드 발송”으로 끝내면

위험 포인트가 많다.

 

그래서 이번 목표는 아래 흐름을

안전하게 만드는 것이다.

  • 사용자가 아이디 입력
  • “아이디 확인” 클릭
    → 서버에서 계정 존재 여부 확인 + 이메일 자동 세팅
  • “인증번호 발송” 클릭
    → 서버가 이메일 발송 + 인증코드/만료시간(3분) 세션 저장
  • 인증 성공 후에만 새 비밀번호 입력 가능

1. 최종 구조(역할 분리)

이번 구현은 “페이지 이동(동기)”과

“데이터 처리(JSON, 비동기)”를 분리한다.

 

동기 페이지 진입

  • FindPasswordPageAction
    → findpassword.jsp 렌더링

비동기(AJAX) 3종

  • FindPasswordMemberLookupServlet
    : 아이디 존재 확인 + 이메일 조회
  • FindPasswordSendCodeServlet
    : 인증코드 발급 + 이메일 발송 + 세션 저장
  • FindPasswordVerifyCodeServlet
    : 인증코드 검증(2편에서)

최종 반영(동기)

  • FindPasswordResetAction
    : 검증 완료 상태에서만 비밀번호 UPDATE

2. 세션에 저장할 인증 상태 값

서버가 “진짜 상태”를 들고 있어야 한다

비밀번호 재설정은 클라이언트 값을 신뢰하면 안 된다.

그러므로 최소 아래 5개는 서버 세션에 저장한다.

  • findPasswordMemberId : 대상 회원 PK
  • findPasswordEmail : 발송 이메일(참고용)
  • findPasswordCode : 발급 인증코드
  • findPasswordExpireAt : 만료 시각(ms)
  • findPasswordVerified : 인증 성공 여부(Boolean)

3. (비동기 1) 아이디 확인 + 이메일 자동세팅

FindPasswordMemberLookupServlet

핵심 포인트는 2가지이다.

  • 이메일을 사용자가 입력하게 두지 않고
    서버가 DB에서 가져와 세팅한다.
  • 응답은 JSON으로 내려줘서
    JSP가 UI를 제어하게 한다.
@WebServlet("/FindPasswordMemberLookup")
public class FindPasswordMemberLookupServlet extends HttpServlet {

  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
    response.setCharacterEncoding("UTF-8");
    response.setContentType("application/json; charset=UTF-8");

    Gson gson = new Gson();
    Map<String, Object> result = new HashMap<>();

    String memberName = request.getParameter("memberName");
    if (memberName == null || memberName.trim().isEmpty()) {
      response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
      result.put("success", false);
      result.put("message", "아이디를 입력해주세요.");
      response.getWriter().print(gson.toJson(result));
      return;
    }
    memberName = memberName.trim();

    MemberDAO dao = new MemberDAO();
    MemberDTO dto = new MemberDTO();
    dto.setCondition("MEMBER_ID_EMAIL");
    dto.setMemberName(memberName);

    MemberDTO data = dao.selectOne(dto);

    result.put("success", true);
    if (data == null) {
      result.put("exists", false);
      result.put("message", "존재하지 않는 아이디입니다.");
    } else {
      result.put("exists", true);
      result.put("memberId", data.getMemberId());
      result.put("memberEmail", data.getMemberEmail());
    }

    response.getWriter().print(gson.toJson(result));
  }

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
    doPost(request, response);
  }
}

4. (비동기 2) 인증코드 발급 + 이메일 발송 + 세션 저장

FindPasswordSendCodeServlet

핵심 포인트는 아래 보안 요소이다.

→ 클라이언트가 준 email을 믿지 않는다


memberName으로 다시 DB 조회해서

서버가 이메일을 확정한다.

@WebServlet("/FindPasswordSendCode")
public class FindPasswordSendCodeServlet extends HttpServlet {

  private static final int EXPIRE_SECONDS = 180; // 3분

  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {

    response.setCharacterEncoding("UTF-8");
    response.setContentType("application/json; charset=UTF-8");

    Gson gson = new Gson();
    Map<String, Object> result = new HashMap<>();

    String memberName = request.getParameter("memberName");
    if (memberName == null || memberName.trim().isEmpty()) {
      response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
      result.put("success", false);
      result.put("message", "아이디를 입력해주세요.");
      response.getWriter().print(gson.toJson(result));
      return;
    }
    memberName = memberName.trim();

    // 1) 서버가 DB에서 이메일 조회(신뢰)
    MemberDAO dao = new MemberDAO();
    MemberDTO dto = new MemberDTO();
    dto.setCondition("MEMBER_ID_EMAIL");
    dto.setMemberName(memberName);

    MemberDTO data = dao.selectOne(dto);
    if (data == null) {
      result.put("success", false);
      result.put("message", "존재하지 않는 아이디입니다.");
      response.getWriter().print(gson.toJson(result));
      return;
    }

    String email = data.getMemberEmail();
    if (email == null || email.trim().isEmpty()) {
      result.put("success", false);
      result.put("message", "해당 계정의 이메일 정보가 없습니다.");
      response.getWriter().print(gson.toJson(result));
      return;
    }

    // 2) 코드 생성
    String code = String.valueOf(100000 + new Random().nextInt(900000));

    // 3) 이메일 발송
    try {
      EmailService emailService = new EmailService();
      emailService.sendPasswordResetCode(email, code);
    } catch (Exception e) {
      e.printStackTrace();
      result.put("success", false);
      result.put("message", "이메일 발송에 실패했습니다.");
      response.getWriter().print(gson.toJson(result));
      return;
    }

    // 4) 세션 저장(검증/최종변경에 사용)
    HttpSession session = request.getSession(true);
    session.setAttribute("findPasswordMemberId", data.getMemberId());
    session.setAttribute("findPasswordEmail", email);
    session.setAttribute("findPasswordCode", code);
    session.setAttribute("findPasswordExpireAt", System.currentTimeMillis() + (EXPIRE_SECONDS * 1000L));
    session.setAttribute("findPasswordVerified", false);

    result.put("success", true);
    result.put("expireSeconds", EXPIRE_SECONDS);
    response.getWriter().print(gson.toJson(result));
  }

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
    doPost(request, response);
  }
}

5. 프론트(JSP)에서는 “버튼만 서버에 연결”하면 된다

1편에서는 UI 전체를 길게 설명하지 않고, 핵심만 적는다.

  • “아이디 확인” 버튼
    → /FindPasswordMemberLookup
  • “인증번호 발송” 버튼
    → /FindPasswordSendCode

(인증코드 확인/최종 변경은 2편에서)