열심히 끝까지

디바이스 융합 자바(Java) day47 - 트랜잭션(rollback(),commit()) 본문

디바이스 융합 자바(Java)기반 풀스택 개발자 양성과정(수업내용)

디바이스 융합 자바(Java) day47 - 트랜잭션(rollback(),commit())

노유림 2022. 8. 17. 17:16

[트랜잭션] - Model 파트

>> 트랜잭션이 어디에 쓰이고, 
     코드를 쓰지는 않고 블로그에 개념정리를 제대로 할 것
     어디까지 적용하는 지 기억하는 것이 좋음

개념 : 하나의 작업의 처리 "단위" (단위라는 것 중요) 
           >> 작업의 단위


ex1 )   
>> 양산형 폰게임         
++++++++++++++++++++++++++++
         결제         - 기능
         사용자테이블 보석컬럼   +5000
++++++++++++++++++++++++++++위에까지가 1 트랜잭션[하나의 단위]
         아이템 구매          - 기능
         사용자테이블 보석컬럼   -1000     => T
         사용자 인벤토리            +아이템  => F
++++++++++++++++++++++++++++위에까지가 1 트랜잭션[하나의 단위]

ex2 )
>> 카카오페이/배민페이    ->   10시에 열리는데 9시에 주문했다고 하면?
1. 본인 계좌  -30,000
2. 배민페이  +30,000
3. 배민페이  -19,000
4. 주문 완료

> 1, 2 가 하나의 트랜잭션
> 3, 4 가 하나의 트랜잭션

이때, 주문 취소가 되면?
5. 배민페이  +19,000
3, 4, 5가 하나의 트랜잭션이 되버림

>> 이때 내 계좌에는? 70,000

>> 트랜잭션이 복잡하면 복잡할수록 에러가 날 가능성 존재
     : 잘게 쪼개서 진행하는 것이 중요


==> 기능을 만들 때에는 설계할 때 잘 설계해야 함

  우리의 팀프로젝트에 접목 시킬 수 있는 것
   ex ) 
      회원탈퇴 -> 게시글이 전부 삭제되었니?(확인 작업) 
      댓글추가 -> 게시글의 댓글 수 + 1(+1 되는 것이 보장되어야 함)

DB 관리자들은 연관관계, FK

※ '서비스의 단위를 설정'할 때를 고려하는 것도 중요하지만
    DB관리자처럼 '테이블을 설계'할 때도 이 과정 중요
    
하나의 트랜잭션 범위에 있는
A    B    C 존재
B가 잘 실행이 안되면 A도 롤백 시켜줘!

rollback(); -> 트랜잭션 처리과정중에 문제가 발생했으니,
                   이전에 처리했던 기능을 다시 되돌려놓아라
                    ex ) 하나의 트랜잭션 범위에 있는
                          A    B    C 존재
                          B가 잘 실행이 안되면 A도 롤백 시켜줘!

commit(); -> 하나의 트랜잭션 단위가 잘 처리되었으니,
                   이 변경사항을 고정해서 마무리해라.[확인]


>> 다음 예시는 이해를 돕기 위한 예시
      : 잘 사용하지 않음....

MYSQL에서 잘 보이기에 MYSQL로 작업(ORACLE은 자동으로 해준다고 함)
      use db;
      show tables;

      CREATE TABLE BANK1( // 신한은행
      BID INT PRIMARY KEY,
      BNAME VARCHAR(20),
      BALANCE INT
      );

      CREATE TABLE BANK2( // 국민은행
      BID INT PRIMARY KEY,
      BNAME VARCHAR(20),
      BALANCE INT
      );   

>> 최소 가져야할 것들만 설계

      INSERT INTO BANK1 VALUES(101,'KIM',10000);
      INSERT INTO BANK2 VALUES(222,'LEE',100);



dao 기능 
1. 이 사람(계좌번호 101, 222)의 해당하는 
    회원정보를 select할수 있는 dao 메서드
2. U(pdate)에 해당하는 dao 메서드
    신한(bank1, 101) -> 국민(bank2, 222)
    "-3000"                  "+3000"   << 1 트랜잭션

 

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<jsp:useBean id="b1" class="vo.Bank1" />
<jsp:useBean id="b2" class="vo.Bank2" />
<jsp:useBean id="dao1" class="dao.Bank1DAO" />
<jsp:useBean id="dao2" class="dao.Bank2DAO" />
<%
	if(request.getMethod().equals("POST")){
		if(dao1.transfer(Integer.parseInt(request.getParameter("balance")))){
			out.print("<script>alert('성공!');</script>");
		}
		else{
			out.print("<script>alert('실패...');</script>");
		}
	}

	b1=dao1.selectOne(b1);
	b2=dao2.selectOne(b2);
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

신한: <%=b1.getBname()%> | <%=b1.getBalance()%>원 <br>
국민: <%=b2.getBname()%> | <%=b2.getBalance()%>원

<hr>

<form method="post">
	이체할 금액: <input type="number" value="0" min="0" name="balance">원
	<input type="submit" value="계좌이체">
</form>

</body>
</html>

 

 

>> V + C
     -> 분리
-------------------------
1. controller에서 html 제거
2. tranjection에 controller 부분 이동시키고 지거
3. controller에 useBean 작성 
4. view작업

 

 

>> 기초 작업

 

JDBCUtil(Oracle 버전)---------

package util;

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

public class JDBCUtil {
	static final String driverName = "oracle.jdbc.driver.OracleDriver";
	static final String url = "jdbc:oracle:thin:@localhost:1521:xe";
	static final String user = "이름 넣기";
	static final String passwd = "비밀번호 넣기";

	// conn 객체 반환
	public static Connection connect() { // Connection 확보 로직
		Connection conn = null;
		try {
			Class.forName(driverName);

			conn = DriverManager.getConnection(url, user, passwd);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return conn;
	}
	// conn 객체 받아서 연결해제
	public static void disconnect(PreparedStatement pstmt, Connection conn) {
		try {
			pstmt.close();
			conn.close();
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

 

JDBCUtil2(MySQL 버전)---------

package util;

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

public class JDBCUtil2 {
	   static final String driverName="com.mysql.cj.jdbc.Driver";
	   static final String url="jdbc:mysql://localhost:3306/데이터베이스 이름";
	   static final String user="이름 넣기";
	   static final String passwd="비밀번호 넣기";


	// conn 객체 반환
	public static Connection connect() { // Connection 확보 로직
		Connection conn = null;
		try {
			Class.forName(driverName);

			conn = DriverManager.getConnection(url, user, passwd);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return conn;
	}
	// conn 객체 받아서 연결해제
	public static void disconnect(PreparedStatement pstmt, Connection conn) {
		try {
			pstmt.close();
			conn.close();
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

 

Bank1Table(Oracle)---------

CREATE TABLE BANK1( 
      BID INT PRIMARY KEY,
      BNAME VARCHAR(20),
      BALANCE INT
);

Bank2Table(Oracle)---------

CREATE TABLE BANK2( 
      BID INT PRIMARY KEY,
      BNAME VARCHAR(20),
      BALANCE INT
);

Bank1VO---------

package vo;

public class Bank1 {
	private int bid;
	private String bname;
	private int balance;
	
	public int getBid() {
		return bid;
	}
	public void setBid(int bid) {
		this.bid = bid;
	}
	public String getBname() {
		return bname;
	}
	public void setBname(String bname) {
		this.bname = bname;
	}
	public int getBalance() {
		return balance;
	}
	public void setBalance(int balance) {
		this.balance = balance;
	}
	@Override
	public String toString() {
		return "Bank1 [bid=" + bid + ", bname=" + bname + ", balance=" + balance + "]";
	}
	
}

 

Bank2VO---------

package vo;

public class Bank2 {
	private int bid;
	private String bname;
	private int balance;
	
	public int getBid() {
		return bid;
	}
	public void setBid(int bid) {
		this.bid = bid;
	}
	public String getBname() {
		return bname;
	}
	public void setBname(String bname) {
		this.bname = bname;
	}
	public int getBalance() {
		return balance;
	}
	public void setBalance(int balance) {
		this.balance = balance;
	}
	@Override
	public String toString() {
		return "Bank2 [bid=" + bid + ", bname=" + bname + ", balance=" + balance + "]";
	}
}

 

Bank1DAO---------

package dao;

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

import util.JDBCUtil2;
import vo.Bank1;

public class Bank1DAO {
	Connection conn;
	PreparedStatement pstmt;

	final String sql_selectOne="SELECT * FROM BANK1 WHERE BID=101";
	final String sql_transfer1="UPDATE BANK1 SET BALANCE=BALANCE-? WHERE BID=101";
	final String sql_transfer2="UPDATE BANK2 SET BALANCE=BALANCE+? WHERE BID=222";

	public boolean transfer(int balance) { 
		// vo로만 받아야하나요?(유지보수가 용이하지만 자유도가 높음)
		// DAO의 메서드는 일반적으로 vo 
		// 지금처럼 강제적으로 자료형이나, 인자의 개수를 고정할 수도 있음
		conn=JDBCUtil2.connect();
		try {
			conn.setAutoCommit(false); // 트랜잭션 시작
			// 트랜잭션의 시작을 설정하는 메서드
			// 자동 commit을 해제할 수 있음(MySQL)
			
			// +++하나의 작업 단위+++
			pstmt=conn.prepareStatement(sql_transfer1); // 원래는 각 하나 당 자동 커밋
			pstmt.setInt(1, balance);
			pstmt.executeUpdate();
			
			pstmt=conn.prepareStatement(sql_transfer2); // 각 하나 당 자동 커밋
			pstmt.setInt(1, balance);
			pstmt.executeUpdate();
			// ++++++++++++++++++
			
			pstmt=conn.prepareStatement(sql_selectOne);
			ResultSet rs=pstmt.executeQuery();
			// (vo.getBid) 이런 느낌으로 써야 함
			rs.next();
			System.out.println(rs.getString("BNAME")+" | " + rs.getInt("BALANCE"));
			
			System.out.println("얍!");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			rs.close();
			
			pstmt=conn.prepareStatement(sql_selectOne);
			rs=pstmt.executeQuery();
			// (vo.getBid) 이런 느낌으로 써야 함
			rs.next();
			
			if(rs.getInt("BALANCE")<0) { // 가진 금액보다 더 많이 계좌이체를 하려고 할때, 
				conn.rollback();
				// 지금까지 했던 것들 취소해라!
				return false;
			}
			else {
				conn.commit();
				// 작업에 문제가 없으면 commit 진행
			}
			rs.close();
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return false;
		} finally {
			JDBCUtil2.disconnect(pstmt, conn);
		}		
		return true;
	}
	
	// 계좌 하나 선택========================================================
	public Bank1 selectOne(Bank1 vo) { 
		// System.out.println("BoardDAO의 로그 : "+vo); // 로그 남기기
		conn=JDBCUtil2.connect();
		try {
			Bank1 data=null;
			pstmt=conn.prepareStatement(sql_selectOne);
			// pstmt.setInt(1, vo.getBid());
			ResultSet rs=pstmt.executeQuery();
			if(rs.next()) {
				data=new Bank1();
				data.setBid(rs.getInt("BID"));
				data.setBname(rs.getString("BNAME"));
				data.setBalance(rs.getInt("BALANCE"));
				System.out.println("Bank1DAO : selectOne(): " + data);
				return data;
			}
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			JDBCUtil2.disconnect(pstmt, conn);
		}		
		return null;
	}
	
}

 

Bank2DAO---------

package dao;

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

import util.JDBCUtil2;
import vo.Bank2;

public class Bank2DAO {

	Connection conn;
	PreparedStatement pstmt;

	final String sql_selectOne="SELECT * FROM BANK2 WHERE BID=222";
	final String sql_update2="UPDATE BANK2 SET BALANCE=? WHERE BID=?";


	// 계좌 하나 선택========================================================
	public Bank2 selectOne(Bank2 vo) { 
		// System.out.println("BANK2의 로그 : "+vo); // 로그 남기기
		Bank2 data=null;
		conn=JDBCUtil2.connect();
		try {
			pstmt=conn.prepareStatement(sql_selectOne);
			// pstmt.setInt(1, vo.getBid());
			ResultSet rs=pstmt.executeQuery();
			if(rs.next()) {
				data=new Bank2();
				data.setBid(rs.getInt("BID"));
				data.setBname(rs.getString("BNAME"));
				data.setBalance(rs.getInt("BALANCE"));
				System.out.println("Bank2DAO : selectOne(): " + data);
				// 적절한 공간에 설정해 놓은 로그는 내가 어디에 문제가 있는지 캐치할 수 있음 == 로깅기법(디버깅)
				return data;
			}
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			JDBCUtil2.disconnect(pstmt, conn);
		}		
		return null;
	}
}

 

 

>> html 작업

 

index---------

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
	//초기 실행 시 index로 진행, main값 가지고 tranjection 넘어가기 위함
	response.sendRedirect("controller.jsp?action=main");
%>

 

tranjection---------

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

신한 : ${b1.bname} | ${b1.balance}원 <br>
국민 : ${b2.bname} | ${b2.balance}원

<hr>

<form method="post" action="controller.jsp">
	<input type="hidden" name="action" value="send">
	이제할 금액 : <input type="number" value="0" min="0" name="balance">원
	<input type="submit" value="계좌이체"">
</form>

</body>
</html>

 

controller---------

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<jsp:useBean id="b1" class="vo.Bank1"/>
<jsp:useBean id="b2" class="vo.Bank2"/>
<jsp:useBean id="dao1" class="dao.Bank1DAO"/>
<jsp:useBean id="dao2" class="dao.Bank2DAO"/>
<%
	String action = request.getParameter("action");
	System.out.println("로그 : " + action);
	
	if(action.equals("main")){
		b1=dao1.selectOne(b1);
		b2=dao2.selectOne(b2);
		// request,session,application
		session.setAttribute("b1", b1);
		session.setAttribute("b2", b2);
		pageContext.forward("tranjection.jsp");
		// View에서 EL식으로 출력하기 위해, JSP scope 내장객체에 setAttribute() 수행함
	}
	
	else if(action.equals("send")){
		int balance=Integer.parseInt(request.getParameter("balance"));
		System.out.println("로그 : send 들어옴/ 금액 " + balance);
		if(dao1.transfer(Integer.parseInt(request.getParameter("balance")))){
			// 성공
			out.print("<script>alert('성공!');location.href='controller.jsp?action=main';</script>");
		}else{
			// 실패
			out.print("<script>alert('실패..');location.href='controller.jsp?action=main';</script>");
		}
		
	}
%>

 

실행 결과 사진--------

금액 입력하기 전
입력하고 실행 준비
성공하면 성공 멘트 출력
이체 후 잔액표시
9000원 입력 시 실패 출력
+ 이체 되지 않고 결과 출력
실행했을 때의 로그 출력