20211125 로그인

DUUUPPAAN·2021년 11월 25일
1

20211122 WAS(TOMCAT), JSP

목록 보기
4/10

·멘붕인 하루

-오늘 수업 진도는 정말 쾌속 그 자체였다. 아무래도 시험과 다운로드 및 환경설정 때문에 시간을 너무 많이 써서 진도에 속도를 낼 필요가 있어서인 것 같다. 그래도 너무 빠른 진도에 진이 빠질 수준으로 빠른 수업을 진행하셨다.

·로그인

-index.jsp파일에 로그인 화면을 띄워놓고 header부분과 navigation부분을 따로 include폴더에 빼서
<%@ include file="/include/head.jsp" %>
<%@ include file="/include/navigation.jsp" %>
이런식으로 include시켰다. 이렇게 해주는 이유는 어차피 매 페이지마다 들어갈 화면이기 때문에, 굳이 매 화면마다 작성해주는 것보다 이렇게 삽입시키는 것이 코드를 더 간결하게하고 수정도 용이하고, 재사용성을 높여주기 때문이다.

-로그인에 대한 정규식을 하지 않았고, 비어있는지만 확인하는 정도로만 jQuery를 만들었다. 다만, 한가지 전에 하지 않았던 것이 있는데, 바로 버튼을 누르는 것이 아니라 특정 input박스에서 특정 키를 눌렀을 때도 작동하도록 해보았다.

$("#userId").on("keypress", function(e){
			if(e.which == 13)//enter란 소리 : jQuery의 keypress의 which를 찾아보면 됨. http://b1ix.net/170 참고하자.
			{
				//submit할 함수
				fn_loginCheck(); //최종 submit 함수
			}
		});

여기서 e.which == 13은 enter키를 가리킨다. 숫자마다 가리키는 키가 있지만, 13번 말고는 딱히 쓰이지 않을 것 같은 느낌이 있다. 링크로 가면 각각의 값을 알 수 있게 주석으로 적어놓았다. 참고로 keypress말고 keyup과 keydown도 있는데, 계속 기능을 유지하기 위해서는(즉, 1회성이 아니기 위해서는) keypress를 쓰는 것이 바람직하다.

-AJAX통신으로 로그인한 아이디와 비밀번호의 값을 넘길 것이다. 그리고 타입은 JSON으로 속성과 값이 하나의 쌍으로 되어있는 구조의 타입이라고 보면 된다. 특히 수업에서는 제이쿼리의 AJAX통신을 사용했다.

$.ajax({
			type: "POST", 
			url: "/loginProcAjax.jsp",
			data: 
			{
				userId: $("#userId").val(),
				userPwd: $("#userPwd").val()
			},
			//data는 JSON방식이여서 속성과 값을 쌍으로
			// 서버에서 받을 때 속성의 이름을 똑같이 받아야 함.
			datatype: "JSON",
			//돌아왔을 때 성공 실패를 구분해야 함.
			success: function(obj) 	//여기서 성공여부는 서버에서의 성공이지 아이디와 비밀번호가 일치한다는 것이 아님
			{
				if(!icia.common.isEmpty(obj))// obj가 공백이 아님
				{
					icia.common.log(obj);
					
					var data = JSON.parse(obj);
					//JSON의 obj을 자를 것이라는 뜻. JSON의 짝꿍들을 짤라줌
					
					//data를 받았는데, 그 중에서 "flag"인 값을 받음 
					//data의 flag 값이 없을 경우 인자 값 3번째가 -500을 세팅한다.
					var flag = icia.common.objectValue(data, "flag", -500);
					
					//flag가 0인 경우를 성공으로
					if(flag == 0)
					{
						alert("로그인 성공!!");
					}else //0이 아니면 오류인 경우
					{
						if(flag == -1) //-1인 경우는 비밀번호가 다름
						{
							alert("비밀번호가 올바르지 않습니다.")	
							$("#userPwd").focus();
						}
						else if(flag == -2) //-2는 블랙리스트나, 정지한 회원, 휴면계정 등등 사용이 정지된 아이디
						{
							alert("사용이 정지된 아이디입니다.");
							$("#userId").focus();
						}
						else if(flag == -3) //아이디가 존재하지 않음.
						{
							alert("아이디와 일치하는 사용자 정보가 없습니다.");
							$("#userId").focus();
						}
						else if(flag == -100) //사용자 페이지에서 아이디와 비밀번호가 아닌 것을 보냈음
						{
							alert("파라미터 값이 올바르지 않습니다.");	
						}
						else //서버에선 왔는데 flag값이 이상한 경우가 있음
						{
							alert("오류가 발생했습니다.");
							//뭔지 모르지만 오류났으니까 아이디 다시 입력해.
							$("#userId").focus();
						}
					}
				}
			},
			//응답이 종료되고 나서 실행되는 complete.
			complete: function(data)
			{
				icia.common.log(data);
			},
			//얘는 고정, 실패의 경우
			//프로그램 상의 오류가 아니라 서버 자체의 오류인 경우임.
			error: function(xhr, status, error)
			{
				icia.common.log(error);
				alert("로그인 에러!");
			}
		});

위에 잘 보면 해당 로그인의 결과를 loginProcAjax.jsp로 넘긴다. 여기서 AJAX통신을 왜 하는지에 대해 간단하게 설명하자면, 기존처럼 보내는 방식은 보낸 후에 결과가 리턴될 때까지 멈춰서 대기해야하는 동기방식이었지만, AJAX통신을 사용하면, 비동기 방식으로 내 로그인에 대한 처리는 loginProcAjax.jsp에서 해주고, 나는 현재 보여지는 화면에서 계속 다른 것을 할 수 있게 되는 것이다. 따라서 loginProcAjax.jsp는 따로 보여주는 화면이 없다. 단순히 넘겨받은 값을 DB의 값과 비교해서 결과인 flag값을 넘겨줄 뿐이다. 이 때 넘겨주는 flag값은 각각 index.jsp에서 정의한다.

-이제 DB에서 사용할 user의 아이디와 비밀번호 등의 정보를 담는 TBL_USER 테이블을 만들고, 원래같으면 회원가입을 해서 인서트해야겠지만, 우선은 직접 인서트해서 아이디가 test이고 비밀번호가 1234인 유저를 하나 만든다.

--비밀번호, 아이디, 
CREATE TABLE TBL_USER
(
    USER_ID VARCHAR2(20) NOT NULL,
    USER_PWD VARCHAR2(20) NULL,
    USER_NAME VARCHAR2(30) NULL,
    USER_EMAIL VARCHAR2(50) NULL,
    STATUS CHAR(1) NULL,
    REG_DATE DATE NULL
);

--뭔지 알지 위한 커멘트
COMMENT ON COLUMN TBL_USER.USER_ID IS '사용자 아이디';
COMMENT ON COLUMN TBL_USER.USER_PWD IS '비밀번호';
COMMENT ON COLUMN TBL_USER.USER_NAME IS '사용자명';
COMMENT ON COLUMN TBL_USER.USER_EMAIL IS '사용자 이메일';
COMMENT ON COLUMN TBL_USER.STATUS IS '사용여부(Y:사용, N:정지)';
COMMENT ON COLUMN TBL_USER.REG_DATE IS '등록일';

--인덱스 생성
CREATE UNIQUE INDEX XPK_USER ON TBL_USER(USER_ID ASC);

--값은 대소문자 구분함
--가입하면 무조건 Y를 줄 것임. 가입한 시점의 SYSDATE를 넣어주면 됨.
INSERT INTO TBL_USER VALUES('test', '1234', '테스트1', 'test@icia.co.kr', 'Y', SYSDATE);
COMMIT;

--아이디를 조회할 때, 존재하는지 여부는 COUNT를 사용하면 됨.
--CNT는 변수명임.
SELECT COUNT(USER_ID) AS CNT
  FROM TBL_USER
 WHERE USER_ID = 'test';
 
--보통 비밀번호만 가져오는 쿼리는 만들지 않음.
--그냥 일단 전부 가져옴.
--사용자 아이디 정보 조회
SELECT
    USER_ID,
    NVL(USER_PWD, '') USER_PWD,
    NVL(USER_NAME, '') USER_NAME,
    NVL(USER_EMAIL, '') USER_EMAIL,
    NVL(STATUS, '') STATUS,
    NVL(TO_CHAR(REG_DATE, 'YYYY.MM.DD HH24:MI:SS'), '') AS REG_DATE
FROM
    TBL_USER
WHERE USER_ID = 'test';

-아래쪽의 쿼리문은 나중에 데이터를 조회하기 위해 미리 써본 쿼리문이다.

이제 해당 테이블에 맞춰서 .java파일을 하나 만들어줘야 한다. 단, 이 때 DB의 정보가 노출되지 않도록 데이터를 캡슐화해야 하기 때문에 private 접근지정자를 설정하고 getter와 setter를 만든다.

package com.icia.web.model;

import java.io.Serializable;

public class User implements Serializable {
	
	//서버랑 싱크를 맞추기 위한 용도
	private static final long serialVersionUID = 1L;
	
	private String userId;
	private String userPwd;
	private String userName;
	private String userEmail;
	private String status; //상태 Y는 사용, N은 정지
	private String regDate;
	
	public User() 
	{
		//각각의 값들을 초기화 함. String type이니까 초기값을 ""으로 넣음.
		userId = "";
		userPwd = "";
		userName = "";
		userEmail = "";
		status = "";
		regDate = "";
	}
	
	//getter와 setter.
	public String getUserId() {
		return userId;
	}

	public void setUserId(String userId) {
		this.userId = userId;
	}

	public String getUserPwd() {
		return userPwd;
	}

	public void setUserPwd(String userPwd) {
		this.userPwd = userPwd;
	}

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	public String getUserEmail() {
		return userEmail;
	}

	public void setUserEmail(String userEmail) {
		this.userEmail = userEmail;
	}

	public String getStatus() {
		return status;
	}

	public void setStatus(String status) {
		this.status = status;
	}

	public String getRegDate() {
		return regDate;
	}

	public void setRegDate(String regDate) {
		this.regDate = regDate;
	}

	
}

-쿼리문에 대한 .java를 만들기 전에 DB와 연동하는 자바 코드를 작성해야 한다.

package com.icia.web.db;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public final class DBManager {
	//상수를 쓸 것이기 때문에 final 붙여줌.
	//얘가 커넥션 풀이 될 것.
	
	//jdbc 드라이버 명
	private static final String driverClassName = "oracle.jdbc.OracleDriver";
	
	//jdbc url
	private static final String jdbcUrl = "jdbc:oracle:thin:@localhost:1521:xe";
	
	//오라클 로그인 계정
	private static final String userName = "c##iciauser";
	private static final String password = "1234";
	
	//생성자
	private DBManager() 
	{
		
	}
	
	//데이터베이스 연결 객체 얻기
	public static Connection getConnection() 
	{
		Connection conn = null;
		
		try 
		{
			Class.forName(driverClassName);
			conn = DriverManager.getConnection(jdbcUrl, userName, password);
		}
		catch(ClassNotFoundException e) 
		{
			//콘솔로그에 찍는 것
			System.out.println("[DBManager]getConnection ClassNotFoundException");
		}
		catch(SQLException e) 
		{
			System.out.println("[DBManager]getConnection SQLException");
		}
		return conn;
	}

	//resultSet 객체 닫기
	//DB를 닫을 때, 맥시멈 세 가지를 닫아야 함.
	//DB에서 select를 했을 때, 여러 컬럼을 리턴함. 그때 오는 것이 ResultSet임.
	//닫는 순서는 ResultSet(select일 때만 닫음), PreparedStatement(status일 때만 닫음. *를 전부 불러옴.), Connection 순서
	public static void close(ResultSet rs) 
	{
		//메소드 오버로딩 하는 것.
		close(rs, null, null);
	}
	
	//PreparedStatement 객체 닫기
	public static void close(PreparedStatement pstmt) 
	{
		close(null, pstmt, null);
	}
	
	//Connection 객체 닫기.
	public static void close(Connection conn) 
	{
		close(null, null, conn);
	}
	
	//하나만 넘어오는 경우도 있지만, 두 개가 같이 넘어오는 경우가 당연히 있음.
	//그래서 두 개짜리 다 해야됨.
	//ResultSet, PreparedStatement 객체 닫기
	public static void close(ResultSet rs, PreparedStatement pstmt) 
	{
		close(rs, pstmt, null);
	}
	
	//PreparedStatement, Connection 객체 닫기, insert update delete의 경우
	public static void close(PreparedStatement pstmt, Connection conn) 
	{
		close(null, pstmt, conn);
	}
	
	
	//데이터베이스 객체 닫기
	public static void close(ResultSet rs, PreparedStatement pstmt, Connection conn) 
	{
		if(rs != null) 
		{
			try 
			{
				rs.close(); 
				//예외처리를 이쪽으로 넘겼기 때문에 try catch구문으로 구현해야 함.
			}catch(SQLException e) 
			{
				System.out.println("[DBManager]close ResultSet SQLException");
			}
		}
		
		if(pstmt != null) 
		{
			try 
			{
				pstmt.close();				
			}catch(SQLException e) 
			{
				System.out.println("[DBManager]close PreparedStatement SQLException");
			}
		}
		
		if(conn != null) 
		{
			try 
			{
				conn.close();
			}catch(SQLException e) 
			{
				System.out.println("[DBManager]close Connection SQLException");
			}
		}
	}
	
	//commit 모드 변경(auto냐, 직접 해야 커밋이 되냐)
	//auto인 경우 수동으로 바꾸고, 수동인 경우 오토로 바꾸는 용도
	//flag는 auto냐 아니냐 
	public static void setAutoCommit(Connection conn, boolean flag)
	{
		if(conn != null) 
		{
			try 
			{
				if(conn.getAutoCommit() != flag) 
				{
					conn.setAutoCommit(flag);
				}
			}
			catch(SQLException e) 
			{
				System.out.println("[DBManager]setAutoCommit SQLException");
			}
		}
	}
	
	//rollback
	public static void rollback(Connection conn) 
	{
		if(conn != null) 
		{
			try 
			{
				conn.rollback();
			}
			catch(SQLException e) 
			{
				System.out.println("[DBManager]rollback SQLException");
			}
		}
	}
	
	
}

-여기서 중요한 점은, DB와 연결한 후 반드시 최대 3가지를 close해줘야 하는데 각각, ResultSet, PreparedStatement, Connection이다(순서도 이 순서로 닫아줘야 하며, null값이 오는 상황들을 전부 오버로딩해줘야 한다.). ResultSet은 SQL문으로 얻어온 컬럼의 값들을 테이블의 형태로 가지고 있는 객체이다. 따라서 서버로부터 데이터를 얻어올 때 반드시 사용해야 하는 개체이다. PreparedStatement는 ResultSet이라는 결과를 리턴하는 쿼리를 실행할 수 있는 객체이다.(.executeQuery()메서드를 통해서) 단, 매 실행시마다 컴파일을 해야하는 Statement와는 다르게 Statement를 상속받은 PreparedStatement는 ?를 통해서 쿼리문에 인자를 사용할 수 있다. 그래서 매번 컴파일을 진행하지 않더라도 ?의 부분을 변경하여서 해당 객체를 재사용할 수 있다. 단, 반드시 Connection 객체를 통해 연결해서 사용해야 한다.

-이제 UserDao라는 클래스를 만들어서 SQL문을 넣어준다.

package com.icia.web.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

import com.icia.web.db.DBManager;
import com.icia.web.model.User;

//모든 sql문은 dao 패키지에 저장
public class UserDao {
	
	public UserDao() 
	{
		
	}
	
	//사용자 아이디 체크
	public int userIdSelectCount(String userId) 
	{
		int count = 0;
		Connection conn = null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		//얜 또 뭘까. 아마도 sql문을 받는 객체라고 생각하면 되겠지?....
		StringBuilder sql = new StringBuilder();
		
		//괄호 안에 공백 넣기 필수
		sql.append("SELECT COUNT(USER_ID) AS CNT ");
		sql.append("  FROM TBL_USER ");
		sql.append(" WHERE USER_ID = ? ");
		//?에는 값을 담아서 날릴 것임. 라운딩 변수라고 함. 찾아보기
		
		try 
		{
			conn = DBManager.getConnection();
			//쿼리문 얻어올 때는 toString으로 변환
			pstmt = conn.prepareStatement(sql.toString());
			
			//인덱스 값과 받은 userId를 받음
			pstmt.setString(1, userId);
			
			//executeQuery에 인수를 안넣는 이유는 이미 pstmt에 담겨 있음.
			rs = pstmt.executeQuery();
			
			//rs는 처음에 아무값도 안가리키다가, next를 하면 1번째 값을 가리킴
			if(rs.next()) 
			{
				count = rs.getInt("CNT");
				
			}	
		}
		catch(Exception e) 
		{
			System.out.println("[UserDao]userIdSelectCount Exception");
		}
		finally 
		{
			DBManager.close(rs, pstmt, conn);
		}
		
		return count;
	}
	
	//사용자 조회
	//User 객체를 리턴하겠다. 사용자 정보 전체를 리턴하겠다.
	public User userSelect(String userId) 
	{
		User user = null;
		
		int count = 0; //얘는 존재여부를 확인하기 위한 메소드.
		
		Connection conn = null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		StringBuilder sql = new StringBuilder();
		
		sql.append("SELECT ");
		sql.append("	USER_ID, ");
		sql.append("	NVL(USER_PWD, '') USER_PWD, ");
		sql.append("	NVL(USER_NAME, '') USER_NAME, ");
		sql.append("	NVL(USER_EMAIL, '') USER_EMAIL, ");
		sql.append("	NVL(STATUS, '') STATUS, ");
		sql.append("	NVL(TO_CHAR(REG_DATE, 'YYYY.MM.DD HH24:MI:SS'), '') AS REG_DATE ");
		sql.append("	FROM ");
		sql.append("	TBL_USER ");
		sql.append("	WHERE USER_ID = ? ");
	
		try 
		{
			conn = DBManager.getConnection();
			//쿼리문 얻어올 때는 toString으로 변환
			pstmt = conn.prepareStatement(sql.toString());
			
			//인덱스 값과 받은 userId를 받음
			pstmt.setString(1, userId);
			
			//executeQuery에 인수를 안넣는 이유는 이미 pstmt에 담겨 있음.
			rs = pstmt.executeQuery();
			
			//rs는 처음에 아무값도 안가리키다가, next를 하면 1번째 값을 가리킴
			if(rs.next()) 
			{
				user = new User();
				//원래는 null체크 다 해야함. 원래 공통모듈 적용해서 null값을 제외해야 함.
				user.setUserId(rs.getString("USER_ID"));
				user.setUserPwd(rs.getString("USER_PWD"));
				user.setUserName(rs.getString("USER_NAME"));
				user.setUserEmail(rs.getString("USER_EMAIL"));
				user.setStatus(rs.getString("STATUS"));
				user.setRegDate(rs.getString("REG_DATE"));
			}	
		}
		catch(Exception e) 
		{
			System.out.println("[UserDao]userSelect Exception");
		}
		finally 
		{
			DBManager.close(rs, pstmt, conn);
		}
	
		return user;
	}
	
}

-이제 해당 DB의 사용자 정보와 입력한 값이 일치하는지를 판단하는 loginProcAjax.jsp를 작성한다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="com.icia.web.model.User" %>
<%@ page import="com.icia.web.dao.UserDao" %>

<%
   //아이디 비밀번호를 입력하면, UserDao와 User객체를 이용해서 결과값만 던져줌.  index.jsp랑 User와 UserDao를 연결해주는 것.
	//객체 임포트 시킴. 원래 로그인할 때 쿠키를 이용할 것인데, 오늘은 빼기.
	String userId = request.getParameter("userId");
	String userPwd = request.getParameter("userPwd");
	//null 체크 하나도 안한 것, 내일 추가해줄 것!
	
	//정상적일 때, 파라미터를 받았을 때
	UserDao userDao = new UserDao();
	
	//User객체에 UserDao의 쿼리문 날린 결과를 가져옴.
	User user = userDao.userSelect(userId); //userDao의 userSelect의 리턴값이 User임.
	
	if(user != null) //유저 정보가 있다는 소리
	{
		//상태가 휴먼이나 정지된 게정이 아닌지 확인
		if(user.getStatus().equals("Y"))
		{
			//아이디와 비밀번호 따로하는 이유는 어디가 잘못되었는지 모르기 때문.
			if(user.getUserPwd().equals(userPwd))
			{
				//입력한 비밀번호와 db의 비밀번호가 같냐?
				//넘겨받을 때, AJAX통신의 JSON방식으로 받았기 때문에 넘길 때도 그렇게 넘겨야함.
				response.getWriter().write("{\"flag\":0}");				
			}
			else
			{
				//비밀번호 불일치
				response.getWriter().write("{\"flag\":-1}");
			}
		}
		else
		{
			//정지된 사용자
			response.getWriter().write("{\"flag\":-2}");
		}
	}
	else
	{
		//index.jsp에 flag값을 -100으로 보내주는 것.
		response.getWriter().write("{\"flag\":-3}");
	}
%>

이런식으로 하면 로그인 성공여부를 알 수 있음. 단 로그인 시 DB에서 인서트한 아이디와 비밀번호로만 성공할 수 있음

·굉장히 복잡

-사실 지금 이렇게 하나하나 뜯어보니까 막 못따라갈 수준은 아니고, 할 수 있겠다 싶은데, 수업당시에는 정말 멘붕에 멘붕에 멘붕이었다. 도대체 모르는 객체와 메소드들이 너무 많이 나와서 이걸 정말 쓸 수 있을까 하는 걱정거리만 한가득이었다. 그래서 많이 어려웠지만 이렇게 시간을 내서 복습을 하니 그래도 내일 따라갈 수 있겠다는 생각이 든다. 내일은 더 복잡해진다는데...그래도 오늘구조까지는 이해했으니까 파이팅하자!!

profile
비전공자란 이름으로 새로운 길을 가려 하는 신입

0개의 댓글