트랜잭션 - 1006

Yung·2022년 10월 6일
0

Java223bitcamp

목록 보기
16/26

Controller-Server-DAO-Table

074. 트랜잭션 적용하기 I - 작업 성공의 예

074. 트랜잭션 적용하기 II - 작업 실패의 예

1.excuteUpdate() : 게시글 입력 
 - BoardDao.insert() -> PreparedStatement
2. insertSQL
 - PreparedStatement -> Connection
3. insert(요청)
 - Connection -> DBMS(4. 데이터입력 : 쓰레드 -> app_board)
 
Connection해서 생성되는 쓰레드는 같은 쓰레드를 공유한다???

첨부파일 데이터 입력 중 제약조건을 만족하지 못해 오류가 발생한다면?
제약조건 : 데이터 형, 컬럼 크기, 데이터 중복, FK 제약 등등으로 인해 
=> 첨부파일 데이터 입력은 실패한다.
그러나 이전에 입력한 게시글은 그대로 유효하다.??


Transaction(트랜잭션) : 게시글 입력과 첨부파일 입력이 하나의 작업으로 묶인 경우
트랜잭션으로 묶인 경우 한개의 작업이 실패하면 이전에 성공한 작업도 취소해야한다.
예)
계좌이체
1. 내 계좌에서 출금 | 성공 | 실패
2. 상대 계좌에 입금 | 실패 | 실패
내 계좌에서 출금한경우와 상대계좌에 입금한게 둘다 성공했을때 계좌이체가 진행되어야 한다.

074. 트랜잭션 적용하기 III - 작업 성공의 예

쿼리가 성공하면 임시데이터에 저장하고 전부다 성공했으면 commit요청을 보내 실제 데이터베이스에 저장한다.

074. 트랜잭션 적용하기 IV - 작업 실패의 예

첨부파일입력에서 오류가 발생하면 PrepredStatement에서 connection에 rollback을 요청한다.
rollback을 요청하면 임시 데이터베이스에 들어있던 데이터를 삭제한다.

074. 서비스 객체에 트랜잭션 적용하기

1단계 - 서비스 객체에 Connection 객체를 주입힌다.

  • DefaultBoardService 클래스 변경
    • 생성자 변경
// public class DefaultBoardService implements BoardService {
// BoardDao boardDao;
//
// public DefaultBoardService(BoardDao boardDao) {
// this.boardDao = boardDao;
// };

public class DefaultBoardService implements BoardService {
  Connection con;
  BoardDao boardDao;

  public DefaultBoardService(BoardDao boardDao, Connection con) {
    this.boardDao = boardDao;
    this.con = con;
  };
  • ContextLoaderListener 클래스 변경
    • Board
//ctx.setAttribute("boardService", new DefaultBoardService(boardDao));
ctx.setAttribute("boardService", new DefaultBoardService(boardDao, con));

2단계 - 게시글 입력과 변경, 삭제에 트랜잭션을 적용한다.

DefaultBoardService 클래스파일의 add() , insert(), update(), delte() 변경
select는 트랜잭션을 사용하지 않는다??

  1. add() 변경
//  @Override
//  public void add(Board board) throws Exception {
//    if (boardDao.insert(board) == 0) {
//      throw new Exception("게시글 등록 실패!");
//    }
//    boardDao.insertFiles(board);
//  }

  @Override
  public void add(Board board) throws Exception {
    con.setAutoCommit(false);
    try {
      if (boardDao.insert(board) == 0) {
        throw new Exception("게시글 등록 실패!");
      }
      boardDao.insertFiles(board);
      con.commit();
    } catch (Exception e) {
      con.rollback();
      throw e;
    } finally {
      con.setAutoCommit(true);
    }
  }
  1. update()
//  @Override
//  public boolean update(Board board) throws Exception {
//    if (boardDao.update(board) == 0) {
//      return false;
//    }
//    boardDao.insertFiles(board);
//    return true;
//  }

  @Override
  public boolean update(Board board) throws Exception {
    con.setAutoCommit(false);
    try {
      if (boardDao.update(board) == 0) {
        return false;
      }
      boardDao.insertFiles(board);
      con.commit();
      return true;

    } catch (Exception e) {
      con.rollback();
      throw e;
    } finally {
      con.setAutoCommit(true);
    }
  }
  1. detete()
//  @Override
//  public boolean delete(int no) throws Exception {
//    // 첨부파일 삭제
//    // 게시글의 첨부파일을 먼저 삭제한다.
//    boardDao.deleteFiles(no);
//    // 게시글 삭제
//    return boardDao.delete(no) > 0;
//  }

  @Override
  public boolean delete(int no) throws Exception {
    con.setAutoCommit(false);
    try {
      boardDao.deleteFiles(no);
      boolean result = boardDao.delete(no) > 0;
      con.commit();
      return result;
    } catch (Exception e) {
      con.rollback();
      throw e;
    } finally {
      con.setAutoCommit(true);
    }
  }

075. 스레드 별로 DB 커넥션 다루기

  • DB 커넥션을 공유했을 때 발생하는 문제를 이해하기
  • 클라이언트 요청을 처리할 때 별도의 커넥션을 사용하는 방법

멀티스레드,
client1, client2의 요청이 같은 Connection객체를 사용하기때문에 발생하는 문제
스레드마다 커넥션을 리턴해줄 커넥션 관리자를 만들어야한다
커넥션 관리자

075. DB커넥션으로 공유했을 때 발생하는 문제를 해결하기

DAO <>---> DatabSource(Thread 전용 DB커넥션 제공)
DataSorurce가 Connection객체를 생성해서

DAO에 있는 메소드가
DAO ---> Connection객체를 사용
insert()
insertFiles()
update()
delete()

1단계 - 스레드 전용 DB 커넥션을 제공해주는 일을 할 객체를 만든다.

  • com.bitcamp.sql.DataSource 클래스 생성
package com.bitcamp.sql;

import java.sql.Connection;
import java.sql.DriverManager;

// 스레드 전용 DB 커넥션을 제공하는 일을 한다.
public class DataSource {
  String jdbcUrl;
  String username;
  String password;

  // ThreadLocal을 이용하면 쓰레드 영역에 변수를 설정할 수 있기 때문에, 
  // 특정 쓰레드가 실행하는 모든 코드에서 그 쓰레드에 설정된 변수 값을 사용할 수 있게 된다. 

  //스레드 전용 DB 커넥션 보관소
  ThreadLocal<Connection> conStore = new ThreadLocal<>();

  public DataSource(String driver, String jdbcUrl, String username, String password)
      throws Exception {

    // JDBC Driver 클래스 로딩하기
    //  Class.forName("org.mariadb.jdbc.Driver");
    Class.forName(driver);
    this.jdbcUrl = jdbcUrl;
    this.username = username;
    this.password = password;
  }

  //  Connection con =
  //      DriverManager.getConnection("jdbc:mariadb://localhost:3306/studydb", "study", "1111");

  public Connection getConnection() throws Exception {
    Thread currThread = Thread.currentThread();
    System.out.printf("%s=> getConnection() 호출\n", currThread.getName());

    // 현재 스레드의 보관소에서 DB 커넥션 객체를 꺼낸다.
    Connection con = conStore.get();
    if (con == null) { // 현재 스레드 보관소에 커넥션 객체가 없다면, 
      DriverManager.getConnection(jdbcUrl, username, password); // 새로 생성
      conStore.set(con); // 새로 만든 DB 커넥션 객체를 다음에 다시 사용할 수 있도록 보관한다.
      System.out.printf("%s=> Connection 생성\n", currThread.getName());
    }
    return con;
  }
}

2단계 - DAO에 DataSource 객체를 주입한다.

  • com.bitcamp.board.listener.ContextLoaderListener 클래스 변경
//@WebListener
//public class ContextLoaderListener implements ServletContextListener {
//  @Override
//  public void contextInitialized(ServletContextEvent sce) {
//    System.out.println("공유 자원을 준비 중!!");
//
//    try {
//      Class.forName("org.mariadb.jdbc.Driver");
//      Connection con =
//          DriverManager.getConnection("jdbc:mariadb://localhost:3306/studydb", "study", "1111");
//
//      // 생성자에서 getServletContext()를 호출하면 오류 발생!!
//      // 왜?
//      //  - ServletContext는 ServletCOnfig 객체를 통해 꺼낼 수 가 있는데,
//      //  - ServletContext는 아직 주입되지 않은 상태이다.
//      // ServletConfig 객체가 언제 주입되는가?
//      //  - 생성자 다음에 호출되는 init()가 호출될 때 ServletConfig 객체가 주입된다.
//      // 왜? 아직 ServletContext 객체가 준비되지 않았기 때문이다.
//      // 그래서 생성자 다음에 호출되는 init()에서 getServletContext()를 호출해야 한다.
//      ServletContext ctx = sce.getServletContext();
//
//      DataSource ds = new DataSource("org.mariadb.jdbc.Driver",
//          "jdbc:mariadb://localhost:3306/studydb", "study", "1111");
//
//      BoardDao boardDao = new MariaDBBoardDao(con);
//      MemberDao memberDao = new MariaDBMemberDao(con);
//
//      ctx.setAttribute("boardService", new DefaultBoardService(boardDao, con));
//      ctx.setAttribute("memberService", new DefaultMemberService(memberDao));
//    } catch (Exception e) {
//      e.printStackTrace();
//    }
//  }
//}

// 웹 애플리케이션이 시작되었을 때 공유할 자원을 준비시키거나 해제하는 일을 한다.
@WebListener
public class ContextLoaderListener implements ServletContextListener {
  @Override
  public void contextInitialized(ServletContextEvent sce) {
    System.out.println("공유 자원을 준비 중!!");

    try {
      ServletContext ctx = sce.getServletContext();

      DataSource ds = new DataSource("org.mariadb.jdbc.Driver",
          "jdbc:mariadb://localhost:3306/studydb", "study", "1111");

      System.out.println(ds.getConnection());

      BoardDao boardDao = new MariaDBBoardDao(ds);
      MemberDao memberDao = new MariaDBMemberDao(ds);

      ctx.setAttribute("boardService", new DefaultBoardService(boardDao, ds));
      ctx.setAttribute("memberService", new DefaultMemberService(memberDao));
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

3단계 - DataSource에서 제공하는 Connection을 사용하여 데이터를 처리한다.

  • com.bitcamp.board.dao.MariaDBBoardDao 클래스 변경
  • com.bitcamp.board.dao.MariaDBMemberDao 클래스 변경
  • DefaultBoardService 클래스 변경
//public class MariaDBBoardDao implements BoardDao {
//
//  Connection con;
//
//  // DAO가 사용할 의존 객체 Connection을 생성자의 파라미터로 받는다.
//  public MariaDBBoardDao(Connection con) {
//    this.con = con;
//  }

public class MariaDBBoardDao implements BoardDao {

  DataSource ds;

  // DAO가 사용할 의존 객체 Connection을 생성자의 파라미터로 받는다.
  public MariaDBBoardDao(DataSource ds) {
    this.ds = ds;
  }


//  @Override
//  public int insert(Board board) throws Exception {
//    try (PreparedStatement pstmt = con.prepareStatement(
//        "insert into app_board(title,cont,mno) values(?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) {

  @Override
  public int insert(Board board) thimport java.sql.Connection;

public class TransactionStatus {
  Connection con;

  public TransactionStatus(Connection con) {
    this.con = con;
  }

  public Connection getConnection() {
    return this.con;
  }
}rows Exception {
    try (PreparedStatement pstmt = ds.getConnection().prepareStatement(
        "insert into app_board(title,cont,mno) values(?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) {

076. 트랜잭션 관리자 역할을 별도의 클래스로 분리하기

  • Spring 프레임워크의 트랜잭션 관리 기법을 모방해 보기
  • 왜 모방? 나중에 진짜 스프링 프레임워크를 사용할 때 이해도를 높이기 위해

1단계 - 트랜잭션 제어에 필요한 값을 담을 보관소를 만든다.

  • com.bitcamp.transaction.TransactionStatus 클래스 생성
import java.sql.Connection;

public class TransactionStatus {
  Connection con;

  public TransactionStatus(Connection con) {
    this.con = con;
  }

  public Connection getConnection() {
    return this.con;
  }
}

2단계 - 트랜잭션 관리자 역할을 수행할 클래스를 정의한다.

  • com.bitcamp.transaction.TransactionManager 클래스 생성
import java.sql.Connection;
import com.bitcamp.sql.DataSource;

public class TransactionManager {
  DataSource ds;

  public TransactionManager(DataSource ds) {
    this.ds = ds;
  }

  public TransactionStatus getTransaction() throws Exception {
    //현재 스레드에서 사용할 DB 커넥션 객체를 생성한다.
    Connection con = ds.getConnection();

    //트랜잭션을 수행할 수 있도록 수동 커밋 상태로 변경한다.
    con.setAutoCommit(false);

    return new TransactionStatus(con);
  }

  public void commit(TransactionStatus status) {
    try {
      // 트랜잭션 보관소에 저장된 DB 커넥션을 사용하여 커밋을 수행한다.
      status.get().commit();
    } catch (Exception e) {
      // 커밋하다가 발생한 예외는 무시한다.
    } finally {
      try {
        status.get().setAutoCommit(true);
      } catch (Exception e) {
        // 자동 커밋 상태로 설정하는 중에 발생한 오류는 무시한다.
      }
    }
  }

  public void rollback(TransactionStatus status) {
    try {
      // 트랜잭션 보관소에 저장된 DB 커넥션을 사용하여 롤백을 수행한다.
      status.get().rollback();
    } catch (Exception e) {
      // 롤백하다가 발생한 예외는 무시한다.
    } finally {
      try {
        status.get().setAutoCommit(true);
      } catch (Exception e) {
        // 자동 커밋 상태로 설정하는 중에 발생한 오류는 무시한다.
      }
    }
  }
}

3단계 - 서비스 객체에 DataSource 대신 트랜잭션 관리자를 주입한다.

  • com.bitcamp.board.service.DefaultBoardService 클래스 변경
public class DefaultBoardService implements BoardService {
  TransactionManager txManager;
  BoardDao boardDao;

  public DefaultBoardService(BoardDao boardDao, TransactionManager txManager) {
    this.boardDao = boardDao;
    this.txManager = txManager;
  };

  @Override
  public void add(Board board) throws Exception {
    TransactionStatus status = txManager.getTransaction();
    try {
      if (boardDao.insert(board) == 0) {
        throw new Exception("게시글 등록 실패!");
      }
      boardDao.insertFiles(board);
      txManager.commit(status);
    } catch (Exception e) {
      txManager.rollback(status);
      throw e;
    }
  }

0개의 댓글