<< 학습 목표 >>

1. 회원 정보 수정 페이지와 기능을 구현할 수 있다.

2. 동적 쿼리를 구현할 수 있다.

3. 서블릿으로 웹 페이지를 출력하는게 불편하다는걸 인지하고 있다.


드디어 지금까지 배운 것들을 활용해 회원 정보 수정 페이지와 기능을 개발할 수 있음

 

앞 Chapter의 글 ( https://codingaja.tistory.com/48 ) 에서도 언급했듯 회원 정보 수정 페이지는 로그인한 사용자의 정보를 보여줘야하므로 동적인 페이지임

 

동적인 페이지를 보여주는 방법은 알지만 "로그인한 사용자의 정보가 들어간 회원 정보 수정 페이지" 는 보여줄 수 없었음

그러나 이제 우리가 세션에 로그인한 사용자의 정보를 저장하고 꺼내는 방법을 익혔으므로 "로그인한 사용자의 정보가 들어간 회원 정보 수정 페이지"를 보여줄 수 있음

 

회원 정보 수정 페이지와 기능을 개발하자


<< 회원 정보 수정 페이지 >>

현재는 member 패키지 -> MemberUpdate 서블릿이 회원 정보 수정 페이지를 달라는 요청이 들어오면 회원 정보 수정 페이지로 포워딩 하고 있지만 이제는 해당 서블릿에서 현재 로그인한 사용자의 정보가 들어간 회원 정보 수정 페이지를 출력하면 됨

만약 로그인한 사용자의 정보를 쿠키에 저장했으면 HTML페이지에서 JS를 사용했으면 되겠지만 로그인한 사용자의 정보는 세션에 저장되어있으므로 서블릿에서 꺼내 보여줘야함

 

member 패키지 -> MemberUpdate 서블릿을 다음과 같이 수정하자

package member;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@WebServlet("/member/update")
public class MemberUpdate extends HttpServlet {
	
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		HttpSession session = request.getSession();
		
		if(session.getAttribute("member") == null) {
			response.sendRedirect("/member");
		}
		
		MemberDto loginMember = (MemberDto) session.getAttribute("member");
		String id = loginMember.getId();
		String nickname = loginMember.getNickname();
		String tel = loginMember.getTel();
		
		response.setCharacterEncoding("UTF-8");
		
		PrintWriter pw = response.getWriter();
		
		pw.print("<!DOCTYPE html>");
		pw.print("<html>");
		pw.print("<head>");
		pw.print("	<meta charset=\"UTF-8\">");
		pw.print("	<title>연습 프로젝트</title>");
		pw.print("	<link rel=\"stylesheet\" type=\"text/css\" href=\"/css/public/common.css\">");
		pw.print("	<link rel=\"stylesheet\" type=\"text/css\" href=\"/css/member/join.css\">");
		pw.print("</head>");
		pw.print("<body>");
		pw.print("	<header>&#60;&#60; 회원 정보 수정 &#62;&#62;</header>");
		pw.print("	<main>");
		pw.print("		<form action=\"/member/join\" method=\"POST\">");
		pw.print("			<fieldset>");
		pw.print("				<label for=\"id\">아이디</label>");
		pw.print("				<input type=\"text\" name=\"id\" id=\"id\" required=\"required\" dp-name=\"아이디\" readonly=\"readonly\" disabled=\"disabled\" value=\""+id+"\">");
		pw.print("			</fieldset>");
		pw.print("			<fieldset>");
		pw.print("				<div>");
		pw.print("					<label for=\"pw\">비밀번호</label>");
		pw.print("					<input type=\"password\" name=\"pw\" id=\"pw\" required=\"required\" dp-name=\"비밀번호\">");
		pw.print("				</div>");
		pw.print("				<div>");
		pw.print("					<label for=\"pwchk\">비밀번호 확인</label>");
		pw.print("					<input type=\"password\" name=\"pw\" id=\"pwchk\" required=\"required\" dp-name=\"비밀번호 확인\">");
		pw.print("				</div>");
		pw.print("			</fieldset>");
		pw.print("			<fieldset>");
		pw.print("				<label for=\"nickname\">닉네임</label>");
		pw.print("				<input type=\"text\" name=\"nickname\" id=\"nickname\" required=\"required\" dp-name=\"닉네임\" value=\""+nickname+"\">");
		pw.print("			</fieldset>");
		pw.print("			<fieldset>");
		pw.print("				<label for=\"tel\">연락처</label>");
		pw.print("				<input type=\"tel\" name=\"tel\" id=\"tel\" required=\"required\" dp-name=\"연락처\" value=\""+tel+"\">");
		pw.print("			</fieldset>");
		pw.print("			<fieldset>");
		pw.print("				<button type=\"button\" role=\"submit\">정보 수정</button>");
		pw.print("			</fieldset>");
		pw.print("		</form>");
		pw.print("	</main>");
		pw.print("</body>");
		pw.print("</html>");
	}
}


코드가 길긴하지만 조금만 집중해서 보면 어렵지 않게 이해할 수 있는 코드임

1. 로그인하지 않은 상태로 회원 정보 수정 페이지에 접근했다면 메인 페이지로 이동

2. 세션에 저장된 로그인한 사용자의 정보를 꺼낸 후 보여줘야할 데이터(아이디, 닉네임, 연락처)를 별도의 변수에 저장

3. 아이디를 화면에 보여줌

여기서 비밀번호를 보여주지 않는 이유는 보안상의 이유 보다는 암호화 때문임

비밀번호는 단방향 암호화를 했기 때문에 DB에 저장된, 사용자가 가입할 때 입력한 비밀번호, 는 절대 알 수 없음

비밀번호를 제외한 나머지는 사용자가 가입할 때 입력한 그대로 DB에 저장 되어있기 때문에 사용자에게 보여줄 수 있는 것


이제 회원 정보 수정 기능을 추가하자

시간이 흘러 기억이 흐릿하겠지만 기능(서블릿)을 만들 때는 먼저 기능에 대한 정의, 스토리보드, 인터페이스를 작성한 후 개발 해야함

 

우리는 전 Chapter의 회원 정보 수정 글 ( https://codingaja.tistory.com/48 ) 에서 이미 기능에 대한 정의, 스토리보드, 인터페이스를 작성했음

다시 한번 확인하고 오자

 

이제 기능에 대한 정의, 스토리보드, 인터페이스에 맞게 회원 정보 수정 기능을 개발하자

회원 정보 수정 기능을 개발하기 위해 필요한 것은

1. 회원 정보 수정 요청을 받을 컨트롤러

2. 회원 정보 수정을 수행할 서비스 메서드

3. DB의 member 테이블에 update 쿼리를 수행하고 결과를 받아 서비스 메서드로 전달할 DAO

 

<< 1. 회원 정보 수정 요청을 받을 컨트롤러 >>

더보기

<< 회원 정보 수정 컨트롤러 (member 패키지 -> MemberUpdate 서블릿) >>

코드가 길어 일부만 가져옴

// ...

@WebServlet("/member/update")
public class MemberUpdate extends HttpServlet {
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		HttpSession session = req.getSession();
		
		if(session.getAttribute("member") == null) {
			resp.sendRedirect("/member");
			return ;
		}
		
		MemberDto oldMember = (MemberDto) session.getAttribute("member");
		
		req.setCharacterEncoding("UTF-8");
		
		String pw = ParameterUtil.getString(req, "pw");
		String nickname = ParameterUtil.getString(req, "nickname");
		String tel = ParameterUtil.getString(req, "tel");
		
		if(pw != null && !ParameterUtil.isPw(pw)) {
			resp.setStatus(400);
			return ;
		} else if(!ParameterUtil.isNickname(nickname)) {
			resp.setStatus(400);
			return ;
		} else if(!ParameterUtil.isTel(tel)) {
			resp.setStatus(400);
			return ;
		}
		
		if(pw != null) {
			CryptoUtil cu = new CryptoUtil();
			pw = cu.onewayEncryption(pw);
		}
		
		MemberDto newMember = new MemberDto(oldMember.getId(), pw, nickname, tel);
		newMember.setIdx(oldMember.getIdx());
		
		MemberService service = new MemberService();
		
		try {
			newMember = service.updateMember(newMember, oldMember);
			
			session.setAttribute("member", newMember);
		} catch(DuplicateDataException e) {
			resp.setStatus(409);
		}
	}
    
 // ...

 

<< 코드 설명 >>

1. 회원 정보 수정은 로그인한 사용자만 사용할 수 있으므로 회원 정보 수정 기능이 본격적으로 동작하기 전 로그인 여부 확인

  이해하면서 따라오고 있다면 "회원 정보 수정 페이지에 로그인한 사용자만 들어갈 수 있는거 아닌가?" 싶겠지만 회원 정보 수정 페이지에 접속해 있다가 어떤 일이 있어 몇 시간 뒤에 정보를 수정할 수 있음

2. 클라이언트(사용자) 가 보낸 값을 꺼낸 후 검증

  비밀번호는 변경하지 않을 경우 입력하지 않으므로 비밀번호를 변경하지 않았다면 비밀번호를 보내지 않을 예정임

  닉네임과 연락처는 변경하지 않더라도 기존의 닉네임과 연락처를 보낼 예정임

  그래서 클라이언트가 보낸 값을 검증하는 if문의 조건식이 약간 다름

3. 비밀번호를 변경한다면 비밀번호를 암호화 함

4. 정보를 수정할 때 사용할 newMember 객체 생성

  회원 번호와 아이디는 변경할 수 없으므로 기존 회원 정보(oldMember)의 회원 번호와 아이디로 지정하고 나머지(비밀번호, 닉네임, 연락처)는 클라이언트가 전달한 새로운 데이터를 저장

5. 회원 정보를 수정한 후 서비스 메서드가 정보가 수정된 새로운 회원의 정보를 반환함

  메인 페이지, 회원 정보 수정 페이지에서 세션에 들어있는 로그인한 사용자의 정보를 꺼내서 사용하므로 수정된 회원 정보를 세션에 다시 저장해 해당 페이지에서 수정된 회원 정보를 사용하도록 함



 

<< 2. 회원 정보 수정을 수행할 서비스 메서드 >>

더보기

<< 회원 정보 서비스 (member패키지 -> MemberService 클래스) >>

코드가 길어 일부만 가져옴

package member;

public class MemberService {
	public MemberDto updateMember(MemberDto newMember, MemberDto oldMember) throws DuplicateDataException {
		MemberDao dao = new MemberDao();
		
		String newPw = newMember.getPw();
		String oldPw = oldMember.getPw();
		
		String newNickname = newMember.getNickname();
		String oldNickname = oldMember.getNickname();
		
		String newTel = newMember.getTel();
		String oldTel = oldMember.getTel();
		
		if(newPw != null && (oldPw.equals(newPw))) {
			newMember.setPw(null);
		} else if(newPw != null && (!oldPw.equals(newPw))) {
			oldMember.setPw(newPw);
		}
		
		if(oldNickname.equals(newNickname)) {
			newMember.setNickname(null);
		} else {
			oldMember.setNickname(newNickname);
		}
		
		if(oldTel.equals(newTel)) {
			newMember.setTel(null);
		} else {
			MemberDto selectedMember = dao.selectOneByTel(newMember.getTel());
			if(selectedMember != null) {
				throw new DuplicateDataException();
			}
			
			oldMember.setTel(newTel);
		}
		
		dao.updateMember(newMember);
		
		return oldMember;
	}
    
// ...

 

<< 코드 설명 >>

1. 수정할 회원 정보와 기존 회원 정보를 모두 꺼냄

2. 비밀번호를 변경하는데 기존의 비밀번호와 동일하게 입력했다면 DAO에서 비밀번호를 변경하지 않도록 하게 만들기 위해 수정할 회원 정보의 비밀번호를 지움

  비밀번호를 변경하는데 기존의 비밀번호와 다르다면 기존 회원 정보(oldMember)의 비밀번호를 변경할 비밀번호로 변경

  서비스 메서드의 oldMember 객체는 회원 정보를 수정한 후 컨트롤러로 반환하는 객체로 (4) 까지 동작하고 나면 기존 회원 정보가 변경된 최신 회원 정보를 갖고 있게 됨

3. 닉네임을 변경하지 않는다면 수정할 회원 정보의 닉네임을 지움

  닉네임을 변경한다면 기존 회원 정보의 닉네임을 수정할 닉네임으로 변경

4. 연락처를 변경하지 않는다면 수정할 회원 정보의 연락처를 지움

  연락처를 변경한다면 연락처 중복 여부 확인

  연락처가 중복됬다면 DuplicateDataException 예외 발생

  연락처가 중복되지 않았다면 기존 회원 정보의 연락처를 수정할 연락처로 변경

5. 회원 정보 수정

6. 변경된 최신 회원 정보를 컨트롤러로 반환

 

<< member패키지 내 DuplicateDataException 예외 클래스 추가 >>

package member;

public class DuplicateDataException extends RuntimeException {

}


 

<< 3. DB의 member 테이블에 update 쿼리를 수행하고 결과를 받아 서비스 메서드로 전달할 DAO >>

더보기

<< member 패키지 -> MemberDao 클래스 >>

코드가 길어 일부만 가져옴

// ...

public void updateMember(MemberDto member) {
	Connection conn = null;
	PreparedStatement pstmt = null;
	
	int idx = member.getIdx();
	String pw = member.getPw();
	String nickname = member.getNickname();
	String tel = member.getTel();
	
	if(pw == null && nickname == null && tel == null) {
		return ;
	}
	
	try {
		conn = DBUtil.getConnection();
		
		int count = 1;
		
		StringJoiner sql = new StringJoiner(",", "UPDATE member SET ", " WHERE idx = ?");
		if(pw != null) {
			sql.add("pw = ?");
			count++;
		}
		if(nickname != null) {
			sql.add("nickname = ?");
			count++;
		}
		if(tel != null) {
			sql.add("tel = ?");
			count++;
		}
		
		pstmt = conn.prepareStatement(sql.toString());
		
		pstmt.setInt(count--, idx);
		
		if(tel != null) {
			pstmt.setString(count--, tel);
		}
		if(nickname != null) {
			pstmt.setString(count--, nickname);
		}
		if(pw != null) {
			pstmt.setString(count, pw);
		}
		
		pstmt.executeUpdate();
	} catch (SQLException e) {
		e.printStackTrace();
	} finally {
		DBUtil.closes(conn, pstmt);
	}
}

// ...

 

<< 코드 설명 >>

1. 수정할 회원 정보의 데이터를 꺼냄

2. 서비스 메서드가 잘못 호출해 수정할 회원 정보가 비어있다면 메서드 흐름을 끊음

3. StringJoiner를 사용해 상황에 맞는 update 쿼리 생성

  StringJoiner를 모른다면 ChatGPT에 물어보기

4. 생성한 쿼리를 DB로 보내기 위한 준비

5. 준비된 쿼리에 ? 부분을 채움

6. 쿼리 보내기 & 실행

이 DAO에서 상황에 맞는 쿼리를 생성하므로 이런 쿼리를 동적 쿼리라고 함



이제 마지막으로 회원 정보 수정 페이지에서 [ 정보 수정 ] 버튼을 눌렀을 때 회원 정보 수정 컨트롤러로 요청이 들어가도록 회원 정보 수정 페이지 내 [ 정보 수정 ] 버튼에 기능을 추가하자

 

회원 정보 수정 페이지는 동적인 페이지이므로 회원 정보 수정 컨트롤러(member 패키지 -> MemberUpdate 서블릿)에서 직접 출력하고 있음

더보기

회원 정보 수정 컨트롤러(member패키지 -> MemberUpdate 서블릿) 내 doGet 메서드 수정

닫는 main 태그( </main> ) 와 닫는 body 태그 ( </body> ) 사이에 코드가 추가된 것

// ...

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
	HttpSession session = request.getSession();
	
	if(session.getAttribute("member") == null) {
		response.sendRedirect("/member");
	}
	
	MemberDto loginMember = (MemberDto) session.getAttribute("member");
	String id = loginMember.getId();
	String nickname = loginMember.getNickname();
	String tel = loginMember.getTel();
	
	response.setCharacterEncoding("UTF-8");
	
	PrintWriter pw = response.getWriter();
	
	pw.print("<!DOCTYPE html>");
	pw.print("<html>");
	pw.print("<head>");
	pw.print("	<meta charset=\"UTF-8\">");
	pw.print("	<title>연습 프로젝트</title>");
	pw.print("	<link rel=\"stylesheet\" type=\"text/css\" href=\"/css/public/common.css\">");
	pw.print("	<link rel=\"stylesheet\" type=\"text/css\" href=\"/css/member/join.css\">");
	pw.print("</head>");
	pw.print("<body>");
	pw.print("	<header>&#60;&#60; 회원 정보 수정 &#62;&#62;</header>");
	pw.print("	<main>");
	pw.print("		<form action=\"/member/update\" method=\"POST\">");
	pw.print("			<fieldset>");
	pw.print("				<label for=\"id\">아이디</label>");
	pw.print("				<input type=\"text\" name=\"id\" id=\"id\" required=\"required\" dp-name=\"아이디\" readonly=\"readonly\" disabled=\"disabled\" value=\""+id+"\">");
	pw.print("			</fieldset>");
	pw.print("			<fieldset>");
	pw.print("				<div>");
	pw.print("					<label for=\"pw\">비밀번호</label>");
	pw.print("					<input type=\"password\" name=\"pw\" id=\"pw\" required=\"required\" dp-name=\"비밀번호\">");
	pw.print("				</div>");
	pw.print("				<div>");
	pw.print("					<label for=\"pwchk\">비밀번호 확인</label>");
	pw.print("					<input type=\"password\" name=\"pw\" id=\"pwchk\" required=\"required\" dp-name=\"비밀번호 확인\">");
	pw.print("				</div>");
	pw.print("			</fieldset>");
	pw.print("			<fieldset>");
	pw.print("				<label for=\"nickname\">닉네임</label>");
	pw.print("				<input type=\"text\" name=\"nickname\" id=\"nickname\" required=\"required\" dp-name=\"닉네임\" value=\""+nickname+"\">");
	pw.print("			</fieldset>");
	pw.print("			<fieldset>");
	pw.print("				<label for=\"tel\">연락처</label>");
	pw.print("				<input type=\"tel\" name=\"tel\" id=\"tel\" required=\"required\" dp-name=\"연락처\" value=\""+tel+"\">");
	pw.print("			</fieldset>");
	pw.print("			<fieldset>");
	pw.print("				<button type=\"button\" role=\"submit\">정보 수정</button>");
	pw.print("			</fieldset>");
	pw.print("		</form>");
	pw.print("	</main>");
	
	pw.print("<script src=\"https://code.jquery.com/jquery-3.6.3.min.js\" integrity=\"sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=\" crossorigin=\"anonymous\"></script>");
	pw.print("<script>");
	pw.print("	$(\"button\").on(\"click\", function() {");
	pw.print("		let pw = $(\"#pw\").val();");
	pw.print("		let pwchk = $(\"#pwchk\").val();");
	pw.print("		let nickname = $(\"#nickname\").val();");
	pw.print("		let tel = $(\"#tel\").val();");
	
	pw.print("		let formData = {\"nickname\": nickname, \"tel\": tel};");
	
	pw.print("		if((pw.length != 0) && (pw != pwchk)) {");
	pw.print("			alert(\"비밀번호와 비밀번호 확인이 일치하지 않습니다.\");");
	pw.print("			$(\"#pwchk\").focus();");
	pw.print("			return false;");
	pw.print("		} else if(pw.length != 0) {");
	pw.print("			formData[\"pw\"] = pw;");
	pw.print("		}");
	
	pw.print("		$.ajax({");
	pw.print("			url: $(\"form\").attr(\"action\"),");
	pw.print("			type: $(\"form\").attr(\"method\"),");
	pw.print("			data: formData,");
	pw.print("			success: function() {");
	pw.print("				alert(\"회원 정보를 수정했습니다.\");");
	pw.print("			},");
	pw.print("			error: function(response) {");
	pw.print("				if(response.status == 400) {");
	pw.print("					alert(\"수정할 정보를 입력해주세요.\");");
	pw.print("				} else if(response.status == 409) {");
	pw.print("					alert(\"연락처가 중복되었습니다.\");");
	pw.print("				} else {");
	pw.print("					alert(\"서버에 문제가 생겼습니다.\\n잠시 후 다시 시도해주세요.\");");
	pw.print("				}");
	pw.print("			}");
	pw.print("		});");
	pw.print("	});");
	pw.print("</script>");
	
	pw.print("</body>");
	pw.print("</html>");
}

// ...

 

<< 코드 설명 >>

서블릿이 직접 동적인 페이지를 출력했을 때는 이와 같이 굉장히 불편함

이를 개선한 툴? 언어?가 JSP임

우리는 아직 JSP를 배우지 않았으니 어쩔 수 없이 JS를 이와 같이 추가해야함

 

추가된 JS 부분만 설명함

1. 회원 정보 수정 버튼을 클릭했을 때

2. 화면에 입력한 비밀번호 ~ 연락처까지 가져옴

3. 회원 정보 수정 컨트롤러로 보낼 데이터를 JSON으로 생성

4. 비밀번호를 변경하는데 비밀번호와 비밀번호 확인이 일치하지 않는다면 안내 문구 출력

  비밀번호를 변경하는데 비밀번호와 비밀번호 확인이 일치한다면 회원 정보 수정 컨트롤러로 보낼 데이터에 비밀번호 추가

5. 회원 정보 수정 컨트롤러로 요청을 보내고 결과를 받아 적절한 안내 문구 출력



여기까지 다소 길고 복잡하지만 회원 정보 수정 페이지와 기능을 구현했음

이제 서버를 실행시키고 회원 정보 수정 기능이 잘 동작하는지 확인하자

 

여기서 가장 불편한 부분은 서블릿이 직접 웹 페이지를 출력하면 웹 페이지에 무언가를 수정하거나 그 웹 페이지에 JS를 추가할 때 굉장히 불편하다는 점임

 

컨트롤러, 서비스, DAO 에서 이해가 안가는 부분이 있다면 그건 자바와 서블릿 복습이 부족하다는 것

728x90
LIST