단일 쿼리(DAO)들을 하나의 서비스로 묶어주는 것을 말한다. 그래서 보통 Service Layer에 생성한다. 예를 들어 A의 통장💰에서 B의 통장💸으로 5천원을 송금한다고 가정해보자. A의 통장💰 -> B의 통장💸으로 5천원이 송금되는 일련의 과정을 하나의 서비스로 본다. 하지만 이것을 구현할 때에는 단일 쿼리로는 불가능할 것이다.
예를 들면,
1) A의 통장💰의 잔액 조회 (SELECT)
2) 잔액이 5000원보다 크면, A의 통장💰에서 5000원 차감 (UPDATE)
3) B의 통장💸에 5000원 증가 (UPDATE)
4) 증가 후 B의 통장💸의 잔액 조회 (SELECT)
라는 과정을 거쳐야되는 것이다.
만약 이때 3번의 과정이 실패되었다고 생각해보자. 트랜잭션을 사용하지 않을 경우 A의 통장에서 5000원이 차감되었지만 B의 통장에는 5000원이 증가되지 않는 말도 안되는 일이 발생 되는 것이다!
트랜잭션을 사용하면, 3번이 실패했을시 모든 과정을 ROLLBACK
한다. A의 통장에서 5000원을 차감하는 과정까지 모두 취소 되는 것이다.
먼저 샘플DB를 생성해보자.
📌 parent 테이블
CREATE TABLE `parent` (
`pid` INT(11) NOT NULL AUTO_INCREMENT,
`info1` VARCHAR(50) NOT NULL COLLATE 'utf8_general_ci',
PRIMARY KEY (`pid`) USING BTREE
)
📌 child 테이블
CREATE TABLE `child` (
`pid` INT(11) NOT NULL,
`info2` VARCHAR(50) NOT NULL DEFAULT '' COLLATE 'utf8_general_ci',
PRIMARY KEY (`pid`) USING BTREE,
CONSTRAINT `FK__parent` FOREIGN KEY (`pid`) REFERENCES `m1`.`parent` (`pid`) ON UPDATE NO ACTION ON DELETE NO ACTION
)
parent의 기본키인 p_id가 child 테이블의 외래키로 적용되어있다.
그리고 그 다음에 ParentDao와 ChildDao를 생성해보았다.
📌 ParentDao
package model;
import java.sql.*;
public class ParentDao {
public int insert(String info1, Connection conn) throws SQLException {
int pId = -1; // 안전하게 초기값을 -1로 주는 것도 좋다
// Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
String sql = "INSERT INTO parent(info_1) VALUES(?)";
try {
// Class.forName(driver);
// conn = DriverManager.getConnection(url,dbid,dbpw);
stmt = conn.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
stmt.setString(1, info1);
int row = stmt.executeUpdate();
rs = stmt.getGeneratedKeys(); // 키 값 가져오기
if(row == 1) { // insert 성공시
if(rs.next()) {
pId = rs.getInt(1);
}
}
} finally {
try {
rs.close();
stmt.close();
// conn.close();
} catch(Exception e2) {
e2.printStackTrace();
}
}
return pId;
}
}
📌ChildDao
package model;
import java.sql.*;
public class ChildDao {
public int insert(int pId, String info2, Connection conn) throws SQLException {
int row = 0;
// Connection conn = null;
PreparedStatement stmt = null;
String sql = "INSERT INTO child(info_2) VALUES(?) WHERE p_id = ?";
try {
// Class.forName(driver);
// conn = DriverManager.getConnection(url,dbid,dbpw);
stmt = conn.prepareStatement(sql);
stmt.setString(1, info2);
stmt.setInt(2, pId);
row = stmt.executeUpdate();
} finally {
try {
stmt.close();
// conn.close();
} catch(Exception e2) {
e2.printStackTrace();
}
}
return row;
}
}
child 테이블의 p_id가 외래키이기 때문에 ChildDao.insert()는 p_id가 필요하다. 즉, ParendDao.insert()와 같이 수행되어야 한다. ParendDao.insert()가 실패하여 리턴값인 pId가 정상적으로 반환되지 않는다면, 그 다음 단계인 ChildDao.insert()도 실행되지 않을 것이다.
즉, 이 경우 또한 트랜잭션을 이용하면 좋을 것이다. 또한, 두 DAO가 Connection 객체를 공유 하여 하나의 트랜잭션당 한번씩만 생성되고 종료하게 함으로써, 자원효율성의 측면에서도 더 좋다!
그래서 만들어진 DAO를 확인해보면, Connection 객체를 두 DAO가 공유하기 위해 메소드의 매개값으로 Connection을 추가로 받고, 매번 실행되던 Connecion 생성과 종료가 주석처리된 것을 확인할 수 있다.
📌 ParentService
package service;
import java.sql.*;
import model.*;
public class ParentService {
// Controller에서 info1, info2 매개값으로
public void add(String info1, String info2) {
Connection conn = null;
String driver = "org.mariadb.jdbc.Driver";
String url = "jdbc:mariadb://127.0.0.1:3306/m1";
String dbid = "****";
String dbpw = "****";
int row = 0;
try {
Class.forName(driver);
conn = DriverManager.getConnection(url,dbid,dbpw);
// ParentDao.insert() & ChildDao.insert() 호출이 하나의 트랜잭션으로..
conn.setAutoCommit(false); // 자동 커밋 해제
// 트랙잭션은 단일 쿼리들을 하나의 서비스로 묶어주는 역할을 한다
// 어떤 DAO 작업이 실패했을 경우 그 전의 작업까지 모두 rollback 하여 취소시킬 수 있다
// rollback하기 위해서는 자동 커밋을 해제해야한다!
ParentDao parentDao = new ParentDao();
int pId = parentDao.insert(info1, conn);
ChildDao childDao = new ChildDao();
row = childDao.insert(pId, info2, conn);
conn.commit(); // 성공 시 커밋
} catch(Exception e) {
e.printStackTrace();
try {
conn.rollback(); // 실패 시 롤백
} catch(Exception e1) {
e1.printStackTrace();
}
} finally {
try {
conn.close();
} catch(SQLException e2) {
e2.printStackTrace();
}
}
}
}
이제 위에 처럼 Service Layer를 생성한다. 트랜잭션을 관리할 하나의 공간이라고 생각하면 된다. 똑같이 try, catch 절을 사용하여 예외 처리를 해준다. 이때, 주의할 점은 conn.setAutoCommit(false);
과 conn.rollback();
, conn.commit();
이다.
위에서 트랜잭션을 쓰는 이유는 중간 과정에서 실패시 ROLLBACK하기 위해서라고 설명했다. 즉, 내가 원할 때에 롤백하기 위해서는 자동 커밋을 반드시 먼저 해제해야 한다. 그리고 try,catch 절을 이용하여 정상 수행시 conn.commit();
, 예외 발생시 conn.rollback();
하고 모든 과정이 끝난 후(finally) conn.close();
로 커넥션을 종료한다.
위와 같은 과정들은 크게 보면 다음과 같이 이루어진다.
Controller에서 Service를 호출한다. 그리고 Service에서 트랜잭션대로 DAO를 호출한다.
즉, 요청 -> Controller -> Service -> DAO으로 이루어지고, 응답 또한 역순으로 이루어진다.
세션에 대해서는 우리 모두 알고 있다. 세션은 일정기간이 지나면 종료되고 또는 로그아웃 시에도 사라진다. 만약 브라우저마다 특정한 값을 저장하고 싶다면 어떨까? 사용자가 세션이 사라지거나 종료된 후에 다시 방문했을 경우에도, 그 사용자를 알아보고 저장한 값에 따라 다른 view를 보여주고 싶다면?
이럴 때 쿠키를 사용할 수 있다. 쿠키는 사용자의 브라우저에 저장되는 정보이다. 예를 들면 로그인 시 아이디 저장에 체크했을 경우 브라우저 종료 후 다음날 해당 사이트에 접속해도 아이디 정보는 남아있을 것이다. 세션이 아닌 쿠키에 저장했기 때문이다.
쿠키와 세션, 그리고 위에서 배운 트랜잭션으로 아이디 저장 기능을 포함하고 있는 로그인 기능을 구현해보자.
📌 login.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<h1>login</h1>
<form action="${pageContext.request.contextPath}/login" method="post">
<div>
아이디 : <input type="text" name="id" value="${loginId}">
</div>
<div>
비밀번호 : <input type="password" name="pw">
</div>
<div>
<input type="checkbox" name="idSave" value="y">ID저장
</div>
<div>
<button type="submit">로그인</button>
</div>
</form>
</body>
</html>
ID저장에 체크시, idSave로 값이 넘어가게 된다.
📌 LoginDao
package model;
import java.sql.*;
import vo.*;
// LoginService 에서 호출 받는다
public class LoginDao {
// 로그인 // service에서 (트랜잭션에서) 생성한 Connection을 매개변수로 받음으로써 하나의 커넥션을 공유할 수 있다
public Member selectMemberById(Member paramMember, Connection conn) {
Member returnMember = null;
// Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
String sql = "SELECT member_id memberId, member_pw memberPw FROM member WHERE member_id=? AND member_pw = PASSWORD(?)";
try {
// Class.forName(driver);
// conn = DriverManager.getConnection(url,dbid,dbpw);
stmt = conn.prepareStatement(sql);
stmt.setString(1, paramMember.getMemberId());
stmt.setString(2, paramMember.getMemberPw());
rs = stmt.executeQuery();
if(rs.next()) {
returnMember = new Member();
returnMember.setMemberId(rs.getString("memberId"));
returnMember.setMemberPw(rs.getString("memberPw"));
}
} catch(Exception e1) {
e1.printStackTrace();
} finally {
try {
rs.close();
stmt.close();
// conn.close(); // 하나의 트랜잭션에서 모든 DAO가 수행된 뒤에 커넥션을 종료해야하므로 커넥션 종료도 service에서 이루어져야할 것이다
} catch(Exception e2) {
e2.printStackTrace();
}
}
return returnMember;
}
}
login하는 DAO를 마찬가지로 Connection을 생략하는 대신에, 매개값으로 넣어주어 작성한다.
📌 LoginService
package service;
import java.sql.*;
import model.*;
import vo.*;
// LoginController 에서 호출 받는다
public class LoginService {
public Member login(Member paramMember) {
Member member = null;
// 하나의 트랜잭션을 관리하기 위해 DAO 마다 각각의 Connection을 생성하는 것이 아닌,
// Service Layer에서 하나의 Connection을 생성한다
Connection conn = null;
String driver = "org.mariadb.jdbc.Driver";
String url = "jdbc:mariadb://127.0.0.1:3306/cash";
String dbid = "****";
String dbpw = "****";
try {
Class.forName(driver);
conn = DriverManager.getConnection(url,dbid,dbpw);
conn.setAutoCommit(false); // 트랜잭션 관리를 위해 자동 커밋 해제
// 트랙잭션은 단일 쿼리들을 하나의 서비스로 묶어주는 역할을 한다
// 어떤 DAO 작업이 실패했을 경우 그 전의 작업까지 모두 rollback 하여 취소시킬 수 있다
// rollback하기 위해서는 자동 커밋을 해제해야한다!
// DAO 호출
LoginDao loginDao = new LoginDao();
member = loginDao.selectMemberById(paramMember, conn);
conn.commit(); // 성공 시 커밋
} catch(Exception e) { // 실패 시
e.printStackTrace();
try {
conn.rollback(); // 롤백
} catch(Exception e1) {
e1.printStackTrace();
}
} finally { // 성공 유무 관계없이 무조건 실행되는 구문
try {
conn.close(); // 생성한 커넥션 종료
} catch(SQLException e2) {
e2.printStackTrace();
}
}
return member;
}
}
마찬가지로 작성해준다. 해당 경우에는 DAO는 하나만 호출되지만 추후 유지보수를 위해 트랜잭션을 이용하는 형식으로 작성했다.
📌 LoginController
package controller;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import service.LoginService;
import vo.*;
@WebServlet("/login")
public class LoginController extends HttpServlet {
// 로그인 폼
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
RequestDispatcher rd = request.getRequestDispatcher("/WEB-INF/view/login.jsp");
// 이전 로그인 시 쿠키에 저장한 아이디가 있다면(loginId로 저장), form에서 보여주어야 하므로 request 속성에 저장한다
Cookie[] cookies = request.getCookies(); // 쿠키 정보를 가져오는 메소드 // 쿠키타입의 배열로 반환
if(cookies != null) { // 쿠키에 값이 존재한다면
for(Cookie c : cookies) { // 배열이므로 반복문 시작
if(c.getName().equals("loginId") == true) { // 쿠키에 loginId라는 키가 존재하면 // getName메소드 사용
request.setAttribute("loginId", c.getValue()); // request에 저장 // getValue메소드 사용
}
}
}
// getName과 getValue는 모두 String 타입으로 리턴됨!
rd.forward(request, response);
}
// 로그인 액션
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// request 값 받기
String id = request.getParameter("id");
String pw = request.getParameter("pw");
// 객체에 값 저장
Member paramMember = new Member(id, pw, null, null);
// 모델값 구하기 // 호출 순서
// Controller -> Service -> Dao
LoginService loginService = new LoginService();
Member member = loginService.login(paramMember);
// 로그인 결과에 따라 분기
if(member == null) { // 로그인 실패 시
System.out.println("로그인 실패!");
response.sendRedirect(request.getContextPath() + "/login");
// -> redirect 보다는 forward가 좋다. 실패 메세지와 기존 입력 데이터를 form에 넘기기 더 좋기 때문이다!
return;
}
// 로그인 성공 시
// 로그인 정보를 세션에 저장 + 로그인 아이디를 쿠키에도 저장! (만료기간 설정 가능) -> "loginId"로 쿠키에 저장
// 로그인 성공페이지(home.jsp)로 redirect
// 세션에 저장
HttpSession session = request.getSession();
session.setAttribute("loginMember", member);
System.out.println("로그인 성공");
// + 아이디 저장을 선택했다면 아이디값을 쿠키에 저장
if(request.getParameter("idSave") != null) {
Cookie loginIdCookie = new Cookie("loginId", id); // 쿠키는 꼭 매개변수(키String,값String)를 선언해주어야함
// loginIdCookie.setMaxAge(60*60*24); // 쿠키 유효기간을 설정 // 초단위
response.addCookie(loginIdCookie);
}
response.sendRedirect(request.getContextPath() + "/home");
}
}
doPost에서 호출 과정을 보면, 위에서 말했듯이 Controller -> Service -> Dao 순서로 호출된 것을 볼 수 있다.
LoginService loginService = new LoginService();
Member member = loginService.login(paramMember);
해당 실습에는 쿠키 관련 메소드들이 많이 사용되었다. 다음을 참고해보자.
쿠키 관련 메소드
1. getCookies() : 쿠키의 값을 Cookie 배열(Cookie[])로 리턴한다
2. getName() : 쿠키의 이름을 가져온다 (String으로 리턴)
3. getValue() : 쿠키에 설정된 값을 가져온다 (String으로 리턴)
4. setMaxAge(int) : 쿠키의 유효기간을 설정한다
5. setValue(String value) : 쿠키의 값을 설정한다
6. getMaxAge() : 쿠키의 만료 기간을 가져온다