본문 바로가기
05_Server (04. JSP 프로그래밍 구현)

[1-3] 회원서비스 로그인, 로그아웃

by moca7 2024. 9. 7.

 

 


ㅁ 회원서비스

- 회원서비스 개발 세팅         <- 이전에 여기까지 했음.

 

- 회원서비스_로그인            <- 여기부터 시작해서
- 회원서비스_로그아웃           <- 여기까지 내용이다.
- 회원서비스_회원가입페이지로이동

 

- 회원서비스_회원가입요청
- 회원서비스_마이페이지요청
- 회원서비스_정보변경요청
- 회원서비스_비번변경요청
- 회원서비스_회원탈퇴요청

 

 

 

 

 http://localhost:8888/web/

- 메인페이지를 들어가본다.

- 그런데 404 에러 뜸. 보니까 프로젝트 세팅에서는 context root가 web으로 변경되어 있는데,

이클립스 하단 Servers에서 서버 더블클릭하고 module 탭에서 보면 root가 webApp으로 되어 있음.

그래서 http://localhost:8888/webApp을 입력해야 들어가진다.

 

 

- edit 눌러서 /web으로 수정하고 서버 재시작 했는데도 여전히 안 됨.

- 서버 멈추고, project - clean 하고, 서버 재시작하고 하니까 됨.

 

 

 

 

 

 

- 메인페이지는 index.jsp가 로드되고 있는 것이다.

이 index.jsp에 header.jsp와 footer.jsp를 include해놔서 이렇게 요소들이 보여진다.

- 주로 header.jsp에서 기능을 계속 요청할 것이다.

header에 로그인 폼도 있고 다른 메뉴로 이동할 수 있는 메뉴바도 있다.

 

- 우상단 로그인 폼쪽은 로그인 성공 후에는 누가 로그인이 됐는지, 그리고 마이페이지로 이동을 요청할 수 있는 요소로 바뀌어야 한다.

이 자리에 보여질 두 가지 경우를 다 화면구현해 놨었다.

 

 

 

 

 

ㅁ 회원서비스 기능구현 진행을 위해 패키지와 클래스를 만들어놨었다.

 

 

- src/main/java는 소스폴더다.

- 여기에 com.br.web이라는 패키지가 있고, 그 뒤부터 네번째 레벨로 member, notice, board로 나뉜다.

- db 패키지도 따로 뒀고 db 패키지 안에는 config와 mappers 패키지를 뒀었다. 

 

 

 

 

- JDBCTemplate 클래스도 구성을 했고, db.config 패키지에 driver.properties 파일도 만들어 놨었다.

어떤 db에 접속할건지, 그 환경설정 관련한 내용이었다.

- JDBCTeplate 클래스에서 db 연결해주는 메소드로 getConnection() 메소드를 뒀었다. 

이 때 webapp/WEB-INF/classes/db/config 안에 동기화된 driver.properties 파일을 읽어들여야 한다. 

 

 

 

 

- dao 클래스에 프로퍼티스 객체를 인스턴스 변수(인스턴스 필드)로 선언한다.

 

- src/main/java의 db.mappers 패키지에 member-mapper.xml 파일을 만들어 놨고

이 안에 key-value 세트로 실행할 쿼리문들을 작성할 예정이다.

 

- dao 측에서 쿼리문이 담긴 xml 파일을 읽어들여야 하기 때문에 마찬가지로 실제 읽어들이는 파일은 

webapp/WEB-INF/classes/db/mappers 안에 동기화된 member-mapper.xml 파일을 읽어들여야 한다.

 

- prop이라는 인스턴스 변수에 key-value 세트들이 담기게 되고, 그걸 이제 앞으로 만들 메소드 측에서 사용하면 된다.

 

 

 

 

 

- dao의 메소드는 service에서 호출한다. 그래서 MemberService 클래스도 만들었다.

MemberService에 이제 각각의 기능별 메소드가 만들어질 예정이다. 

- 각 기능에 맞는 쿼리를 service에서 dao 메소드를 호출시켜서 실행시킬 예정이다.

 

 

 

 

- 한 회원에 대한 데이터를 담을 VO 객체인 Member 클래스도 만들었다.

 

 

 

 

 

 

ㅁ 기능별 메소드 구현 + sql 쿼리문 작성

- 왼쪽에 MemberService 클래스를 띄우고, 오른쪽에 쿼리문을 작성할 member-mapper.xml을 띄운다.

- 그 외에 header.jsp와 MemberDao.java도 켜놓는다.

 

- 원래 개발할 때 기능 분석 끝나고, 화면 설계 끝나고, db도 다 구축되었으면

본격적인 기능구현 하기전에 화면 설계된 거 or 화면 구현된 걸 보면서

각각의 페이지상에 필요한 쿼리같은 걸 미리 다 작성해 둘 수가 있다.

- 앞으로 실행시켜야할 쿼리를 쭉 작성 해두고 쿼리에 문제가 없는지 쿼리 테스트도 진행을 해야만 한다.

- 그런데 아직은 이 페이지에서 요청별로 어떤 쿼리가 필요한지 예상이 불가능할 테니,

지금은 기능구현을 하면서 필요한 쿼리를 작성한다.

나중에 익숙해지면 미리 쿼리도 생각해보세요. 

 

 

 

 

 

- MemberService 클래스에서는 빈번하게 멤버 dao 객체가 필요하기 때문에

그때마다 new MemberDao(); 하지 않고 인스턴스 변수로 MemberDao 객체를 생성한다.

 

 

 

 

 

ㅁ header.jsp를 살펴본다.

 

 

- 앞으로 현재 url 요청시, 절대경로 방식으로 포트번호 바로 뒤에 붙는 url을 작성할 예정이다.

그때마다 contextPath를 알아와야 한다.

- "/web"이라는 것을 알고 있지만 이 context path는 언제든지 변경될 수 있기 때문에

현재 context path를 동적으로 알아내는 request.getContextPath() 메소드를 사용해야 한다.

 

- 정적인 요소에 대한 경로를 작성할 때(로고 이미지)도 항상 context path를 제시해서 절대 경로 방식으로 작성해야 파일을 찾기가 훨씬 수월하다.

- contextPath가 곧 webapp 폴더를 가리킨다. 

 

 

 

 

- header 태그는 지금 크게 3가지 영역으로 되어 있다.

로고 이미지가 있는 div가 있고, 가운데 div는 비워놨고, 세번째 div는 로그인 폼을 구성해놨다.

 

 

 

 

 

- 어떤 페이지에 있든 로고 이미지를 클릭하면 메인 페이지로 올 수 있게끔 하려면, 

a 태그에 href로 요청할 url을 context path로 작성하면 된다.

- context path로 작성하면, "/web"이 오고, 이러면 index.jsp 페이지가 로드된다.

- 앞으로 요청 처리 이후에 index.jsp 페이지가 띄워지게끔 하려면 url로 위와 같이 context path를 요청하면 된다.

 

 

 

 

 

- 현재 이 페이지가 로드되는 시점이 로그인 전일 수도 있고, 로그인 후일 수도 있다.

로그인 전 상태라면 case1에 작성한 로그인 form 요소가 보여지게 하고,

로그인 후 상태라면 case2에 작성한 로그인 후 요소가 보여지게 할 예정이다.

- 로그인 후에는 현재 로그인한 회원의 이름이 보여져야 한다. 

 

 

 

 

 

ㅁ header.jsp에 로그인 기능을 구현한다.

 

 

- case1에 로그인 요청을 할 수 있는 form 요소가 있고, form 요소 내에 submit 타입의 버튼으로 로그인 버튼이 있다.

- 아이디와 비밀번호 입력란에 required 속성을 추가한다. 

- 아이디와 비밀번호 텍스트상자에 입력된 값을 로그인 요청시 서버에 전송시켜야 한다.

웹과 통신을 할 때는 항상 key-value 세트로 데이터가 움직인다.

- key 값을 부여해야 value 값이 넘어가므로 input 요소에 name 속성도 추가한다. 

 

 

- 비밀번호가 노출되면 안되므로 이런 보안을 중시하는 요청은 post 방식으로 요청한다. 

method="post"로 하면 데이터가 url에 노출되지 않는다.

 

- 요청할 url은 action=""에 작성하면 된다. 항상 서블릿을 요청한다고 보면 된다. 

단순한 페이지 요청도 다 서블릿으로 요청한다.

 

- 서블릿의 url mapping 값을 context path 뒤에 작성한다. 

항상 절대경로 방식으로 요청할 url을 작성하는 것이 좋다.

 

- 변수 contextPath에 "/web"이 담겨 있다. 

- 회원서비스는 url mapping값의 패턴을 항상 뒤에 ".me"로 끝나게 작성하기로 한다.

서비스별로 url 패턴을 구분지어주는 것이 좋다.

보통 . 하고 뒤에 패턴을 붙인다.

 

- <form action="<%= contextPath %>/login.me" method="post">

/login.me 라는 url mapping 값을 가지는 서블릿을 호출하기로 한다. 

 

 

 

 

 

com.br.web.member.controller "/login.me"라는 url mapping 값을 가지는 서블릿 클래스를 만든다.

- 서블릿 클래스는 MVC 패턴 중 컨트롤러 역할이다.

- src/main/java의 com.br.web.member.controller 패키지 우클릭 - new - Servlet으로 서블릿을 만든다.

 

 

 

 

- 서블릿 클래스명의 시작은 대문자로 작성한다.

파일들이 많은데 어떤게 클래스고 어떤게 jsp 파일인지 구분할 수 있어야 한다.

- jsp 파일명은 소문자로 시작하게 작성한다.

 

- 바로 finish 하고 나중에 url mapping 값을 수정해도 되지만, next 하고 url mapping 값을 수정한다.

 

 

 

 

 

- 클래스명을 따서 "/MemberLoginController"로 되어 있는 url mapping 값을 "/login.me"로 바꾼다.

- 슬래시가 누락되면 안 된다.

슬래시 없이 url mapping 값을 쓰면 서버 스타트시 오류가 난다.

 

 

 

 

 

- 이렇게 "/login.me"라는 url mapping 값을 가지는 서블릿 클래스가 만들어진다.

- 위의 탭들을 MVC 흐름대로 정리한다.

header.jsp의 로그인 form에서 로그인 요청시, "/login.me"라는 url이 요청되고,

그때 실행되는 서블릿 클래스가 MemberLoginController이다.

- controller 클래스에서 service => dao로 데이터를 넘겨서 db에 쿼리문을 실행하고 결과를 돌려받는다.

 

 

 

 

 

- 요청이 get 방식이면 doGet() 메소드가, post 방식이면 doPost() 메소드가 서블릿에서 자동으로 실행된다.

 

- 그런데 어차피 doPost() 메소드에서도 doGet() 메소드를 호출하고 있다.

이클립스에서는 개발자 편의를 위해서 get 방식이든 post 방식이든 doGet() 메소드에서 작업할 수 있게 해놨다.

- doPost() 메소드에 작성된 구문은 그대로 두고,

doGet() 메소드에 작성된 구문을 다 지우고 post 방식임에도 그냥 doGet() 메소드에 작성한다.

 

 

 

 

- 컨트롤러에서는 항상 두 가지만 기억하면 된다.

요청에 관한 것과 응답에 관한 것을 작성하면 된다.

 

- 요청에 관한 것은 요청하면서 전달한 값을 뽑아내고, JDBC 과정을 통해서 쿼리를 실행 후 결과를 받으면 된다.

- 요청에 전달값이 없다면 굳이 값을 뽑을 필요 없다.

- 만약 단순한 페이지 이동이라면 그때는 쿼리를 실행할 필요도 없다.

 

- 지금은 아이디와 비밀번호 값이 전달된다.

요청시 전달값이 있다는 소리는 그 값을 뽑아서 어딘가에 사용을 한다는 소리다.

 

 

 

 

 

- request.getParameter(String) 혹은 request.getPrameterValues(String) 메소드로 값을 뽑으면 된다.

key 값을 제시해서 value 값을 뽑을 수 있다. 이때 무조건 문자열로 반환된다.

그래서 이 메소드 호출시 반환된 값을 String 타입의 변수에 담아둔다.

 

- 이때 변수명은 무조건 key값과 동일할 필요는 없고 임의로 의미를 부여해서 만들면 된다.

getParameter(Stirng)메소드에 제시하는 key 값은 input 요소의 name 속성값과 같아야 한다.

 

- post 방식 요청일 경우, 한글 데이터면 setCharacterEncoding("UTF-8") 메소드를 이용해서 utf-8로 인코딩 해야 한다.

여기서는 아이디와 비밀번호에 한글이 없어서 생략했다.

 

 

 

 

 

- 이제 이 아이디와 비밀번호가 유효한 회원이 맞는지를 확인해야 한다.

회원 data는 db에 저장되어 있다. 그러면 db에 이 아이디와 비밀번호를 가지고 있는 회원이 있는지 조회해야 한다.

여기서 실행할 쿼리는 select이다.

- service로 아이디와 비밀번호를 넘기면서 조회하러 간다.

MemberService() 객체를 생성해서 loginMember()라는 메소드를 호출한다. (아직은 이런 메소드가 없다)

 

 

 

 

 

ㅁ MemberService 클래스에 loginMember라는 메소드를 만든다.

- com/br/web/member/model/service 패키지의 MemberService 클래스에 loginMember라는 메소드를 만든다.

두 개의 String 타입의 변수를 받을 매개변수도 만든다.

- 뭘 반환할지는 아직 모르겠으면 반환형은 void로 일단 둬 놓는다.

 

 

 

 

- 이제 select문을 실행하기 위해 dao로 넘어가야 한다.

그런데 dao측에 jdbc 전체 구문을 작성하기에는 너무 길어지니 JDBCTemplate 클래스에 static 메소드로 선언해 두었다.

 

 

 

 

 

 

- db와 접속된 Connection 객체를 JDBCTemplate 클래스의 메소드를 호출해서 얻어내고 변수에 담는다.

 

- 그런데 매번 "JDBCTemplate."을 작성하기 귀찮아서 import static문으로

앞으로 MemberService 클래스에서는 JDBCTemplate 클래스 안에 있는 모든 static 메소드를 호출한다고 선언해둘 수 있다.

 

import static com.br.web.common.template.JDBCTemplate.getConnection; 을 선언해 두면

앞으로 JDBCTemplate.getConnection();이 아닌 getConnection();이 가능하다.

- 그러면 전에 쓴 import com.br.web.common.template.JDBCTemplate; 는 필요가 없으니 지운다..

 

 

 

 

 

 


- 이제 select 문을 실행하러 넘어간다.

dao측 메소드를 호출할 건데 Connection 객체와 userId, userPwd를 다 전달한다.

- 이때 아직 어떤 값이 넘어올지 모르겠으면 그냥 이대로 둬 놓는다.

 

 

 

 

 

 

ㅁ MemberDao 클래스에 loginMember 메소드를 만든다.

 

 
package com.br.web.member.model.dao;

import static com.br.web.common.template.JDBCTemplate.close;

import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;

import com.br.web.member.model.vo.Member;
 

public class MemberDao {
 
   
    private Properties prop = new Properties();
 
   
    public MemberDao() {
       
        String filePath = MemberDao.class.getResource("/db/mappers/member-mapper.xml").getPath();
       
        try {
            prop.loadFromXML(new FileInputStream(filePath));
        } catch (IOException e) {
            e.printStackTrace();
        }
       
    }
 
   
    public Member loginMember(Connection conn, String userId, String userPwd) {
 
        // select문 => ResultSet (한 행, 한 회원) => Member객체
        Member m = null;
        PreparedStatement pstmt = null;
        ResultSet rset = null;
 
        String sql = prop.getProperty("loginMember");
       
        try {
            pstmt = conn.prepareStatement(sql); // 미완성된 sql문
            pstmt.setString(1, userId);
            pstmt.setString(2, userPwd);
            rset = pstmt.executeQuery();
           
            if(rset.next()) {
                m = new Member(rset.getInt("USER_NO")
                             , rset.getString("user_id")
                             , rset.getString("user_pwd")
                             , rset.getString("user_name")
                             , rset.getString("phone")
                             , rset.getString("email")
                             , rset.getString("address")
                             , rset.getString("interest")
                             , rset.getDate("enroll_date")
                             , rset.getDate("modify_date")
                             , rset.getString("status"));
            }
           
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(rset);
            close(pstmt);
        }
 
       
        return m;
       
    }
   
}
 

 

- Connection 객체, userId, userPwd를 전달받기 위해 매개변수 3개를 둔다.

 

- 전달받은 아이디와 비밀번호를 가지고 select문을 실행시킨다. 

결과는 ResultSet에 담겨서 돌아오는데 아이디는 중복이 안되므로 조회 결과는 하나(한 행, 한 회원)다.

아니면 아이디와 비밀번호를 잘못 입력한 경우 조회가 안 되니 rset은 비어있고 m도 null일 것이다.

 

- 최종 결과는 Member 객체의 각 필드에 담는다.

vo객체의 용도는 데이터를 담아서 전송시키는 것이다.

 

 

 

 

 

ㅁ member-mapper.xml에 쿼리를 작성한다.

 

 

- entry의 key값은 그냥 메소드명과 동일하게 준다. 괜히 오타날 수 있으니 붙여넣기 한다.

- 쿼리문은 대문자든 소문자든 상관없다.

- 참고로 sql developer를 안 켜놔도 db는 현재 이 pc에 다 구축이 되어있기 때문에 작동 잘 된다.

 

- sql developer를 켜서 Server 계정의 Member 테이블의 열 정보 탭에서 COLUMN_NAME을 싹 복사해 와서 컴마만 붙여준다.

- 다른사람도 잘 알아볼 수 있게 들여쓰기 중구난방으로 하지 말고, 컴마도 뒤가 아닌 앞에 쓴다.

보통 이렇게 많이 쓴다. 한 줄 단위 주석처리하기도 쉽다.

- FROM 다음 테이블명은 붙여써도 되는데 여러 테이블을 조인하는 경우 테이블들도 컬럼처럼 작성하는 경우가 있어서 한 줄 단위로 주석처리하기 쉽게끔 이렇게 작성하기도 한다.

- 이렇게 작성하면 SELECT 절, FORM 절, WHERE 절을 정확히 구분해서 볼 수 있다.

 

- USER_ID와 USER_PWD에는 뭐가 들어올지 모르므로 "?"를 써 놓는다.

- 조건을 여러개 쓸 때도 한 줄씩 내려서 표현해주는 것이 좋다.

- 탈퇴회원은 조회안되게끔 한다. STATUS 컬럼이 'R'이면 탈퇴회원이다.

탈퇴회원도 일정 기간 동안은 데이터를 가지고 있는다.

 

- 쿼리 마지막에는 절대 세미콜론(;)이 찍혀있어서는 안 된다.

 

 

 

 

 

- 이렇게 이 쿼리가 어떤 패키지의 어떤 클래스의 어떤 메소드 측에서 실행되는 쿼리인지 주석을 쓰기도 한다.

- INSERT, UPDATE, DELETE도 다 마찬가지다.

내가 어떤 명령어를 쓸지 작성하고 그 다음줄부터 작성해주시는게 좋다.

 

 

 

 

ㅁ MemberDao 클래스에 loginMember 메소드로 돌아온다.

 

 
package com.br.web.member.model.dao;

import static com.br.web.common.template.JDBCTemplate.close;

import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;

import com.br.web.member.model.vo.Member;
 

public class MemberDao {
 
   
    private Properties prop = new Properties();
 
   
    public MemberDao() {
       
        String filePath = MemberDao.class.getResource("/db/mappers/member-mapper.xml").getPath();
       
        try {
            prop.loadFromXML(new FileInputStream(filePath));
        } catch (IOException e) {
            e.printStackTrace();
        }
       
    }
 
   
    public Member loginMember(Connection conn, String userId, String userPwd) {
 
        // select문 => ResultSet (한 행, 한 회원) => Member객체
        Member m = null;
        PreparedStatement pstmt = null;
        ResultSet rset = null;
 
        String sql = prop.getProperty("loginMember");
       
        try {
            pstmt = conn.prepareStatement(sql); // 미완성된 sql문
            pstmt.setString(1, userId);
            pstmt.setString(2, userPwd);
            rset = pstmt.executeQuery();
           
            if(rset.next()) {
                m = new Member(rset.getInt("USER_NO")
                             , rset.getString("user_id")
                             , rset.getString("user_pwd")
                             , rset.getString("user_name")
                             , rset.getString("phone")
                             , rset.getString("email")
                             , rset.getString("address")
                             , rset.getString("interest")
                             , rset.getDate("enroll_date")
                             , rset.getDate("modify_date")
                             , rset.getString("status"));
            }
           
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(rset);
            close(pstmt);
        }
       
        return m;
       
    }
   

}
 

 

 

- 최종 조회 결과는 Member 객체에 담아서 반환한다.

- 최종 조회 결과를 담을 변수를 null로 선언만 해둬야지 new로 생성해 놓으면 안 된다.

조회결과가 있는지 없는지를 비교할 때 null인지 null이 아닌지로 비교해야 하기 때문이다. 

조회결과가 있을 때 vo 객체를 생성해야 한다.

 

- import할 때 Member 클래스가 많은데 우리가 만든 Member 객체로 한다.

- 미완성된 쿼리를 실행할 때는 PreparedStatement를 사용해야 한다.

- select 문은 조회 결과를 ResultSet으로 받는다.

 

- MemberDao 클래스가 생성되면서 생성자로 "/db/mappers/member-mapper.xml" 파일에 기술되어 있는

key-value 세트가 prop이라는 전역 변수에 읽어들여져 있다.

- 실행할 sql문을 prop에서 꺼내어 String 타입의 변수에 담는다. 

 

 

- 필요한 변수들과 sql문 세팅이 끝났으면 pstmt 생성부터 진행하면 된다.

conn.prepareStatement(sql) 메소드 호출시 내가 실행할 sql문을 담은 채로 pstmt 객체를 생성한다.

- sql문이 물음표가 2개 있는 미완성된 sql문이다.

그러면 실행하기 전에 pstmt.setString(1, userId); 등으로 완성을 시켜야 한다.

- 그리고 pstmt.executeQuery() 메소드로 sql문을 실행한다.

조회 결과는 ResultSet으로 반환된다.

 

 

- 조회가 됐을 수도 있고 안됐을 수도 있는데 뭐가 됐든 커서를 움직여야만 한다. 

움직였을 때 데이터가 있다면 생성을 진행한다. 한번만 진행하면 돼서 while은 필요 없다. 

- rset에는 각각의 컬럼에 대한 값이 담겨있다.

그걸 우리는 뽑아서 Member 객체의 각 필드에 담는다.

m이라는 Member 타입 변수는 선언만 되어있고 현재 생성되어 있지 않다.

그래서 Member 객체에 데이터를 담고 싶으면 생성을 먼저 해야한다. 매개변수가 다 있는 생성자로 생성한다.

- rset.getXXX() 메소드에 인자로 주는 값은 db의 컬럼명이다.

대문자로 쓰든 소문자로 쓰든 상관없다.

 


- 이렇게 한 행에 대한 각각의 컬럼값들을 뽑아서 Member 객체의 각 필드에 담았다.

물론 조회 결과가 있을 때만 진행되는 내용이다.

조회 결과가 없으면 m은 null인 상태 그대로다.

 

- 그리도 다 쓴 객체들을 생성된 역순으로 반납한다. 

import static com.br.web.common.template.JDBCTemplate.close; 문을 여기에도 상단에 써준다.

이러면 이제 close() 메소드 호출시 JDBCTemplate 클래스의 close() 메소드가 실행된다.

- Connection 객체는 service에 돌아가서 또 사용될 수도 있으니 service에서 반납을 진행한다.

 

 

 

 

 

ㅁ MemberService 클래스의 loginMember 메소드를 마저 완성시킨다.

 

 
package com.br.web.member.model.service;

import static com.br.web.common.template.JDBCTemplate.close;
import static com.br.web.common.template.JDBCTemplate.commit;
import static com.br.web.common.template.JDBCTemplate.getConnection;
import static com.br.web.common.template.JDBCTemplate.rollback;

import java.sql.Connection;
import java.util.Map;

import com.br.web.member.model.dao.MemberDao;
import com.br.web.member.model.vo.Member;

public class MemberService {
   
    private MemberDao mDao = new MemberDao();
 
   
    public Member loginMember(String userId, String userPwd) {
 
        Connection conn = getConnection();
        Member loginUser = mDao.loginMember(conn, userId, userPwd);
 
        close(conn);
        return loginUser;
    }

}
 

 

- mDao.loginMember(conn, userId, userPwd) 메소드를 호출하면 조회된 데이터가 담겨있는 Member 객체가 반환된다.

- 조회를 한번 실행하고 왔다. 그런데 여기서 또 다른 쿼리가 실행될 수도 있다.

그러면 여기서 또 dao 메소드가 호출된다. 그때는 또 Connection 객체를 사용해야하니 service에서 반납을 진행하는 것이다.

 

- 더이상 실행할 쿼리가 없으면 Connection 객체를 반납하면 된다.

import static com.br.web.common.template.JDBCTemplate.close; 문을 여기에도 써준다.

그럼 close();만으로 메소드 호출이 가능하다.

- service에서도 최종적으로 loginUser라는 Member 객체를 반환한다.

 

 

 

 

ㅁ MemberLoginController 클래스의 doGet() 메소드로 돌아온다.

 

 

- loginUser라는 Member 객체 타입의 변수를 만들어서 결과값을 담는다.

null이냐 아니냐로 로그인 성공인지 실패인지를 판별할 수 있다. 각각의 경우에 대한 응답을 해주면 된다.

 

 

 

 

  
요청 처리 후 특정 데이터를 담을 수 있는 객체


1. 이 객체들은 "JSP 내장객체"로 JSP에서 바로 사용 가능하다.
2. 주로 응답페이지에서 사용할 데이터를 담음
3. 어떤 객체에 응답 데이터를 담냐에 따라 담은 데이터를 꺼내서 사용할 수 있는 범위가 다르다.

 

 


4. 종류

- 사실 session, request 이 두 객체만 거의 쓴다.

 


(1) ServletContext application
ㄴ 컨텍스트 - 애플리케이션 하나당 한 개 존재 (즉, 애플리케이션에 유지할 데이터 담기)
ㄴ 애플리케이션 종료 전까지 데이터 사용 가능함 (범위가 가장 넓음)


(2) HttpSession session *
ㄴ 세션 - 브라우저 하나당 한 개 존재 (즉, 브라우저에 유지할 데이터 담기)
ㄴ 브라우저 종료 및 서버 종료 전까지 데이터 사용 가능함


(3) HttpServletRequest request *
ㄴ 요청 - 하나의 요청당 한 개 존재. 이 객체는 서블릿이 실행될 때 생성돼서 넘어온다. 서블릿 당 1개의 request 객체가 존재한다.

ㄴ (즉, 해당 요청에 대한 응답페이지에 유지할 데이터 담기)

응답페이지에서만 필요한 데이터는 request에 담는다.
 forward하면서 같이 전달된 request는 이동한 Servlet 및 JSP에서만 사용 가능함.


(4) PageContext page
ㄴ 페이지 - 한 페이지당 한 개 존재 (즉, 해당 페이지에 유지시킬 데이터 담기)

ㄴ jsp와 연관있다. 사실 서블릿 쪽에서는 접근할 수 없다.

ㄴ 해당 페이지에서 유지시킬 데이터를 그 페이지에서 담는 객체다.

ㄴ 해당 jsp에서 담고 해당 jsp에서만 사용 가능함

 

 



5. 공통 메소드

- key-value 세트로 담고, key값을 제시해서 그에 해당하는 value값을 꺼낸다.


1) 데이터 담기       :  .setAttribute(String, Object)
2) 데이터 꺼내기    :  .getAttribute(String )   => Object타입으로 반환 (캐스팅 필요)
3) 데이터 제거하기 :  .removeAttribute(String )

 

 

 

 

 

- 아래는 MemberLoginController 클래스이다.

 
package com.br.web.member.controller;

import java.io.IOException;

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;

import com.br.web.member.model.service.MemberService;
import com.br.web.member.model.vo.Member;

 
@WebServlet("/login.me")
public class MemberLoginController extends HttpServlet {
 
    private static final long serialVersionUID = 1L;
 
 
    public MemberLoginController() {
        super();
    }

 
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
       
        // 1. 요청 ([요청시 전달값 뽑기] => [JDBC과정을 통해 쿼리실행 후 결과받기])
        String userId = request.getParameter("userId");
        String userPwd = request.getParameter("userPwd");
       
        Member loginUser = new MemberService().loginMember(userId, userPwd); // null | 생성된객체    
 
       
        // 2. 응답
 
        if(loginUser == null) {
 
            /*
            로그인 실패
              ㄴ 응답페이지 : 에러페이지 (/web/views/common/errorPage.jsp)
              ㄴ 응답데이터 : "로그인 실패" 메세지 (해당 응답페이지 에서만 필요)
             */
 
            // request에 응답데이터 담기
            request.setAttribute("msg", "로그인 실패");        
 
            // request를 전달하면서 응답페이지로 forward(이동)
            request.getRequestDispatcher("/views/common/errorPage.jsp").forward(request, response);
           
        }else {
 
             /*
             로그인 성공
               ㄴ 응답페이지 : 메인페이지 (/web/index.jsp)
               ㄴ 응답데이터 : 조회된 데이터들이 담겨있는 Member객체 (브라우저 종료 전까지 계속 필요함)
             */
 
            // session에 응답데이터 담기
            HttpSession session = request.getSession(); // jsp와 달리 Servlet에서는 HttpSession 객체를 얻어와서 사용
            session.setAttribute("loginUser", loginUser);
           
            // 이미 메인페이지를 응답하는 URL로 redirect (url 재요청)
            response.sendRedirect(request.getContextPath()); // "/web"
           
        }
       
       
    }

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

}
 

 

 

- 응답은 돌려받은 결과를 가지고 판단하면 된다.

- 로그인 실패시 에러 페이지를 띄운다.

에러 페이지는 로그인 실패뿐 아니라, 회원가입 실패, 회원 정보변경 실패시에도 재사용한다.

- 에러페이지는 아직 제작을 하진 않았지만 /web/views/common/errorPage.jsp 로 만들 예정이다.

 

- 에러 페이지에 어떤 서비스 실패인지 출력할 예정이다.

이 페이지에서 필요한 응답 데이터는 "로그인 실패" 메세지이다. "로그인 실패"라는 응답 데이터가 필요하다.

- 응답데이터가 어느 범위에서 필요한지를 생각해야 한다.

"로그인 실패" 메세지는 딱 이 해당 응답 페이지에서만 필요하다. request에 응답 데이터를 담으면 된다.

- "msg"라는 key 값으로 "로그인 실패"라는 value 값을 담았다.(String, Object로 담았다)

 

 

 

- 로그인 성공시 응답페이지는 메인페이지("/web/index.jsp")이다.

로그인 성공시 응답할 데이터는, 로그인에 성공한 순간 로그인한 회원 데이터가 쭉 사용된다.

로그인 성공 후 다른 페이지를 가도 현재 로그인한 회원 정보가 계속 필요하다.

- 로그인 성공시 필요한 응답데이터는 조회된 데이터들이 담겨있는 Member 객체다.

이 페이지 뿐만 아니라 다른 페이지 넘어가서도 쭉 이 로그인한 회원의 데이터가 계속 필요하다.

이 브라우저를 통해서 이 사이트를 이용하는 과정 중에 계속 필요하다.

 

 

- 응답 데이터들을 어딘가에 담아 놓아야 응답 페이지에서 쓰거나 브라우저 내에서 계속 사용할 수 있다.

request에 담았던 적이 있긴 한데 다른 객체에도 담을 수가 있다. 

- session에 담긴 데이터는 브라우저가 종료되기 전까지 혹은 서버가 종료되기 전까지 쭉  유지가 된다.

그래서 각 페이지들에서 조건문 등에 쓸 수 있다. 

- 로그인 성공시 응답데이터로 조회된 데이터들이 담겨있는 Member 객체가 넘어간다.

session으로 Member 객체를 넘기면 응답 페이지뿐만 아니라 다른 페이지에서도 쭉 사용이 가능하다.

 

 

 

- 로그인 성공시 메인페이지를 띄우고 싶으면 /web/index.jsp할 필요 없이 /web만 해도 된다. 

그냥 contextPath로 url 재요청(redirect)하면 메인페이지가 띄워진다.

그런데 "/web"이라는 context path도 언제 바뀔지 모르므로 "/web" 이렇게 정적으로 작성하면 안되고,

request.getContextPath() 메소드로 동적으로 알아내야 한다.

 

- redirect하는 경우(url 재요청)에는 request 객체가 전달되지 않는다.

그래서 request에 데이터를 담아봤자 소용이 없다. 여기선 session에 담았다.

- jsp에서는 session이 내장객체라서 바로 쓸 수 있는데, 서블릿에서는 request.getSession();으로 얻어서 써야 한다.

 

 

 

 

- 이제부터 요청에 있어서 응답할 때 응답페이지를 뭐로 띄울 거고, 그때 필요한 응답데이터는 뭔지를 생각해야 한다.

그게 어느 범위까지 사용될 데이터인지 잘 판단해서 그에 맞는 객체에 담아야 한다.

- 응답페이지를 띄우는 방법이 두 가지가 있는 것이다. forward와 redirect.

forward로 이동해서 띄워주거나 아니면 이미 이 페이지를 요청하는 url이 존재한다면 그 url을 재요청하는 redirect 방식이 있다.

 

 

 

 

ㅁ webApp/src/main/webapp/views/common 폴더에 errorPage.jsp 만들기

 

 
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

  <div class="container p-3">

    <!-- Header, Nav start -->
    <%@ include file="/views/common/header.jsp" %>
    <!-- Header, Nav end -->

    <!-- Section start -->
    <section class="row m-3" style="min-height: 500px">

      <div class="container border p-5 m-4 rounded">
        <h2 class="m-4" style="color: orangered;"><%= request.getAttribute("msg") %></h2>
      </div>

    </section>
    <!-- Section end -->

    <!-- Footer start -->
    <%@ include file="/views/common/footer.jsp" %>
    <!-- Footer end -->

  </div>
 

</body>
</html>
 

 

 

- 에러 페이지도 header와 footer가 있었으면 해서 header와 footer를 include 했다. index.jsp에 가서 복사해왔다.

 

- 이 에러 페이지에서 쓸 데이터로 request에 "msg"라는 key값으로 "로그인 실패"가 담긴채로 이동했다.

- request로부터 데이터를 꺼내 쓰려면 자바코드를 실행해야 한다. 표현식을 사용한다. 

 

- request는 jsp 내장객체이기 때문에 jsp에서 request 객체 생성 없이 곧바로 사용가능하다.

 

- JSP의 <%= ... %> 표현식은 내부적으로 toString() 메서드를 호출하여 값을 문자열로 변환합니다.

그래서 Object로 value값이 담겼지만 꺼낼 때 (String)으로 형변환하는 과정이 없었다.

 

 

 

 

ㅁ header.jsp 수정하기

 

 
<%@ page import="com.br.web.member.model.vo.Member" %>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
   
<!-- Bootstrap 사용을 위한 CDN -->
<!-- ------------------------- -->
   
<style>
  header{height: 150px}
  header a{color:black;}
</style>

<%
  String contextPath = request.getContextPath(); // "/web"
 
  Member loginUser = (Member)session.getAttribute("loginUser");
  // 해당 구문이 실행되는 시점
  // 로그인 요청 전 페이지 로드시 : null
  // 로그인 성공 후 페이지 로드시 : 조회된 데이터가 담겨있는 Member객체
%>

<header class="row m-3">
  <div class="col-3 d-flex justify-content-center align-items-center">
    <a href="<%=contextPath%>"><img src="<%= contextPath %>/assets/image/goodee_logo.png"></a>
  </div>
  <div class="col-6"></div>
  <div class="col-3 d-flex justify-content-center align-items-center">

    <% if(loginUser == null) { %>
    <!-- case1. 로그인전 -->
    <form action="<%= contextPath %>/login.me" method="post">
      <table>
        <tr>
          <th>ID</th>
          <td><input type="text" name="userId" class="form-control form-control-sm" placeholder="Enter Your ID" required></td>
        </tr>
        <tr>
          <th>PWD</th>
          <td><input type="password" name="userPwd" class="form-control form-control-sm" placeholder="Enter Your PWD" required></td>
        </tr>
        <tr>
          <td colspan="2" align="center">
            <button type="submit" class="btn btn-secondary btn-sm">로그인</button>
            <button type="button" class="btn btn-secondary btn-sm">회원가입</button>
          </td>
        </tr>
      </table>
    </form>
    <% }else { %>
    <!-- case2. 로그인후 -->
    <div>
      <b><%= loginUser.getUserName() %></b>님 환영합니다. <br><br>

      <a href="<%=contextPath%>/myinfo.me">마이페이지</a>
      <a href="<%=contextPath%>/logout.me">로그아웃</a>
    </div>
    <% } %>

  </div>
</header>
<nav class="navbar m-3 navbar-expand-sm bg-dark navbar-dark d-flex justify-content-center">
  <ul class="navbar-nav">
    <li class="nav-item">
      <a class="nav-link" href="<%=contextPath%>">Home</a>
    </li>
    <li class="nav-item">
      <a class="nav-link" href="<%= contextPath %>/list.no">공지사항</a>
    </li>
    <li class="nav-item">
      <a class="nav-link" href="#">일반게시판</a>
    </li>
    <li class="nav-item">
      <a class="nav-link" href="#">사진게시판</a>
    </li>
  </ul>
</nav>
 

 

 

- session 객체로부터 getAttribute로 응답데이터를 꺼낸다.

Object 타입으로 반환되므로 (Member)로 형변환하고 Member 타입 변수에 담는다.

 

- 서블릿과 달리 jsp에서는 session이 jsp 내장객체이므로 바로 접근 가능하다.

 

- Member는 타 패키지에 있는 클래스다보니 import해야 한다. import 페이지 지시어를 최상단에 작성한다.

 

- "<b>홍길동</b>님 환영합니다."를 위와 같이 표현식을 사용하여 수정했다.

화면 구현 단계에서는 정적으로 아무렇게나 써놓고, 기능 구현 단계에서 실제 데이터가 보여지게끔 바꾸면 된다.

 

 

 

 

 

ㅁ 이렇게 로그인 기능을 완성시켰고 테스트를 하기 전에, db와 연동이 되기 때문에 ojdbc6 라이브러리가 연동되어 있어야만 한다.

 

 

- ojdbc6 라이브러리를 src/main/webapp/WEB-INF/lib 폴더에 둔다.

- jar 파일은 자바 클래스들이 모여 있는 압축 형식이다. zip같은 압축 파일이다.

아이콘은 다르게 보일 수 있는데 상관없다. 무시. pc에 압축 프로그램(알집, 반디집 등)이 있으면 그럼.

- 프로젝트 우클릭 - 자바 빌드 패스에서 등록은 안했음. 이렇게 lib 폴더에 가져다만 두었다.

 

 

 

 

ㅁ 로그인 기능 테스트

 



- 로그인에 실패하면 forward하므로 최초 로그인 요청을 하면서 호출했던 servlet의 url mapping값인 /login.me가 보여진다.

이동한 에러페이지의 url이 보여지지 않는다.

 

 

 

 

 

- 로그인 성공시에는 /web으로 redirect해서 index.jsp(메인페이지)를 띄우므로 url도 변경된다.

 

- 만약 오류가 중첩해서 발생한다면, 최초 오류를 봐야 한다.

 

 

 

 

ㅁ 로그아웃 기능

 

 

- 로그인 후에 보여질 요소에 로그아웃 링크가 있다. 얘도 서블릿을 요청한다.

 

 

 

- "/logout.me"라는 url mapping 값을 가지는 서블릿 클래스를 호출한다.

 

 

 

 

 

 

ㅁ src/main/java의 com.br.web.member.controller 패키지에 MemberLogoutController 서블릿 클래스를 만든다.

 

 
package com.br.web.member.controller;

import java.io.IOException;

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("/logout.me")
public class MemberLogoutController extends HttpServlet {
 
    private static final long serialVersionUID = 1L;
       

    public MemberLogoutController() {
        super();
    }


    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
       
        // 로그아웃 : 별도의 쿼리 실행 없이 그냥 session에 담긴 loginUser 지우고 다시 메인페이지로 이동
//      HttpSession session = request.getSession();        (서블릿이라 세션을 먼저 얻고)
//      session.removeAttribute("loginUser");
       
        // 세션 무효화 == 만료 == 싹 비우기
//      session.invalidate(); (2줄을 한줄로 줄일 수 있다)
        request.getSession().invalidate();
       
        // 응답페이지 : 메인페이지
        response.sendRedirect(request.getContextPath());
       
   
    }


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

}
 

 

 

- 로그아웃 요청은 요청과 함께 전달되는 데이터가 없다.

- 로그아웃 로직은 별도로 실행할 쿼리가 없다. service, dao갈 필요가 없다.

 

- 응답페이지는 바로 메인페이지로 돌아갈 건데 그때, 로그인 form이 보여지고자 한다면 loginUser라는 변수가 null이어야 한다.

- session.removeAttribute("loginUser"); 

이렇게 "loginUser"라는 key 값에 대한 Member 객체를 날려도 된다.

 

 

- 그런데 로그인한 회원 정보가 loginUser 말고 다른 곳에도 더 담겨있을 수 있다. session에 다른 정보도 더 담겨있을 수 있다.

깔끔하게 session을 싹 비우는게 좋다.

- session을 싹 비우려면 session 무효화를 하면 된다. session 무효화는 session을 만료시키는 개념이다.

- session.invalidate();

하면 session 안에 담겨있는 게 깔끔히 비워진다. 

 

- 그 이후에 다시 메인페이지가 뜨게끔 redirect하면 된다. 

 

 

- 자바 코드를 수정했으니 서버를 리스타트하고 다시 메인페이지를 띄운다.
서버가 종료됐다가 재실행되면 session이 무효화돼서 로그인이 풀린 상태다.

- 기존에 담긴 회원 객체는 날라갔으니 다시 로그인부터 하고, 로그아웃 기능을 테스트한다.

- 로그아웃을 클릭하는 순간 session이 무효화되고 다시 index.jsp가 로드된다.

loginUser == null이기 때문에 로그인 요청을 할 수 있는 form이 보여진다.