Burninghering's Blog
article thumbnail

컨트롤러에서 DB를 직접 관여하지 않고,

UserDAO (Repository)를 통해 DB를 조작하였다.

 

@Controller - Presentation Layer

@Repository - Persistence Layer(영속계층), Data access Layer

 

컨트롤러는 DB 조작 없이 본연의 역할에 집중하도록 관심사를 분리한 것이다.

 

사용자의 이력을 다루게 되었다

컨트롤러에서 Dao를 주입한다

 

회원가입 할 때 사용자의 이력까지 다루는 것은 비즈니스 로직이다. (컨트롤러 내, DAO 내에도 위치하면 X)

비즈니스 로직을 담당할 객체를 하나 더 만들어야 한다.

 

비즈니스로직의 메소드명은 업무 용어와 같다.

(UserDao의 메소드는 SQL적)

 

트랜잭션을 비즈니스 계층에 적용한다

회원 가입, 유저 이력까지 확인하는 작업 두 가지가 묶여서 진행되어야하기 때문이다.

 

@Controller

@Service

@Repository

이 세 개 모두 @Component 어노테이션을 포함하고 있기 때문에

컴포넌트 스캔으로 자동 스캔이 다 된다.

메소드마다 하나의 커넥션을 만들어서 DB에 접근한다.

트랜잭션은 한 개의 커넥션에서 이루어지는데,

만약 DeleteUser를 두 번 호출했다면, 각 두 번의 커넥션이 만들어진다.

그렇다면 두 개를 TX으로 묶을 수 없다.

그렇다면 DeleteUser가 하나의 커넥션을 쓸 수 있도록 한다.

 

그러므로 필요한 것이 트랜잭션 매니저이다.

같은 트랜잭션에 있는 명령들은 같은 커넥션을 쓰게 해주는것이 매니저의 역할이다.

위 상자는 트랜잭션 매니저를 직접 만드는 방법,

아래 상자는 트랜잭션 매니저를 빈으로 등록하는 방법

@Transactional은 AOP를 이용하여 핵심 기능과 부가기능을 분리한다.

 

AOP : 자동 코드 추가

 

try 안의 코드 두줄이 핵심 코드

위 아래의 코드가 부가 기능

 

핵심코드와 부가 기능이 섞여있는데,

이를 AOP를 이용하여 분리한 것이 오른쪽 상자이다.

 

위 아래로 있던 부가기능이 사라졌다.

우리는 핵심 로직에만 집중하면 된다.

 

@Transactional만 사용하면 AOP가 부가기능을 자동으로 넣어준다.

그렇게 되면 오른쪽 상자가 위, 아래 부가기능이 추가된 채로 작동이 된다.


table a1을 만들어주었다.

이 a1에 접근하기 위한 Dao를 만들어준다.

package com.fastcampus.ch3;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

@Repository
public class A1Dao {
    
    @Autowired
    DataSource ds;
    
    public int insert(int key, int value) throws Exception{
        Connection conn = null;
        PreparedStatement pstmt = null;

        try {
            conn = ds.getConnection();
            pstmt = conn.prepareStatement("insert into a1 values(?,?)");
            pstmt.setInt(1,key);
            pstmt.setInt(2,value);

            return pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();;
        } finally {
            close(conn,pstmt);
        }
        return 0;
    }
    
    private void close(AutoCloseable... acs) {
        for(AutoCloseable ac :acs)
            try { if(ac!=null) ac.close(); } catch(Exception e) { e.printStackTrace(); }
    }
}

 

테스트 코드를 만들어준다.

package com.fastcampus.ch3;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.junit.Assert.*;
@RunWith(SpringJUnit4ClassRunner.class) //ac를 자동으로 만들어줌
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"}) //xml 경로 지정해주기
public class A1DaoTest {

    @Autowired
    A1Dao a1Dao;

    @Test
    public void insertTest() throws Exception{
        a1Dao.insert(1,100);
        a1Dao.insert(2,200);
    }

}

두 개 전부 다 성공할 때만 데이터가 들어가게 하고 싶다.

둘 중에 하나 실패하면 들어가지 못하게 하고 싶다.

이때 해주면 될 것이 트랜잭션이다.

 

@RunWith(SpringJUnit4ClassRunner.class) //ac를 자동으로 만들어줌
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"}) //xml 경로 지정해주기
public class A1DaoTest {

    @Autowired
    A1Dao a1Dao;

    @Autowired
    DataSource ds;

    @Test
    public void insertTest() throws Exception{
        //TxManager를 생성
        PlatformTransactionManager tm = new DataSourceTransactionManager(ds);
        TransactionStatus status=tm.getTransaction(new DefaultTransactionDefinition());
        //Tx 시작
        try {


            a1Dao.insert(1,100);
            a1Dao.insert(1,200);
            tm.commit(status); //성공하면 커밋
        } catch (Exception e) {
            e.printStackTrace();
            tm.rollback(status); //예외가 발생하면 롤백
        } finally {
        }
    }

}

 

트랜잭션 매니저를 추가해줌으로써 A1Dao 수정

@Repository
public class A1Dao {

    @Autowired
    DataSource ds;

    public int insert(int key, int value) throws Exception{
        Connection conn = null;
        PreparedStatement pstmt = null;

        try {
//            conn = ds.getConnection();
            conn= DataSourceUtils.getConnection(ds); 
            
            pstmt = conn.prepareStatement("insert into a1 values(?,?)");
            pstmt.setInt(1,key);
            pstmt.setInt(2,value);

            return pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();;
        } finally {
//            close(conn,pstmt); //커넥션을 닫으면 트랜잭션이 종료되므로 
            close(pstmt); //pstmt만 닫는다
            DataSourceUtils.releaseConnection(conn,ds); //트랜잭션매니저가 close할지 말지 알아서 판단
        }
        return 0;
    }

 

delete가 되지 않아서 에러가 발생하므로

A1Dao.java에 

deleteAll()메소드 추가

public void deleteAll() throws Exception{
    Connection conn = DataSourceUtils.getConnection(ds); //deletAll은 Tx와 별개로 동작하므로, Connection conn = ds.getConnection();으로 해야한다
    String sql="delete from a1";
    PreparedStatement pstmt = conn.prepareStatement(sql);
    pstmt.executeUpdate();
    close(pstmt);
}
package com.fastcampus.ch3;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import javax.sql.DataSource;

import static org.junit.Assert.*;
@RunWith(SpringJUnit4ClassRunner.class) //ac를 자동으로 만들어줌
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"}) //xml 경로 지정해주기
public class A1DaoTest {

    @Autowired
    A1Dao a1Dao;

    @Autowired
    DataSource ds;

    @Test
    public void insertTest() throws Exception{
        //TxManager를 생성
        PlatformTransactionManager tm = new DataSourceTransactionManager(ds);
        TransactionStatus status=tm.getTransaction(new DefaultTransactionDefinition());
        //Tx 시작
        try {
            a1Dao.deleteAll();
            a1Dao.insert(1,100);
            a1Dao.insert(2,200);
            tm.commit(status); //성공하면 커밋
        } catch (Exception e) {
            e.printStackTrace();
            tm.rollback(status); //예외가 발생하면 롤백
        } finally {
        }
    }

}


@Repository
public class A1Dao {

    @Autowired
    DataSource ds;

    public int insert(int key, int value) throws Exception{
        Connection conn = null;
        PreparedStatement pstmt = null;

        try {
//            conn = ds.getConnection();
            conn= DataSourceUtils.getConnection(ds);

            pstmt = conn.prepareStatement("insert into a1 values(?,?)");
            pstmt.setInt(1,key);
            pstmt.setInt(2,value);

            return pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
            throw e; 
        } finally {
//            close(conn,pstmt); //커넥션을 닫으면 트랜잭션이 종료되므로
            close(pstmt); //pstmt만 닫는다
            DataSourceUtils.releaseConnection(conn,ds); //트랜잭션매니저가 close할지 말지 알아서 판단
        }

    }

 

A1Dao에서

예외처리를 하면서 catch구문에서 예외가 발생하면 예외를 다 먹어버리도록 작성했다.

그래서 

throw e;

코드를 추가해주었다 (예외를 다시 던져준다)

 

A1Dao에서 예외 처리를 다 해버리므로

A1DaoTest에서 예외가 발생하지 않는것이다 ㅋ.ㅋ

 

 

같은 트랜잭션으로 묶여있기 때문에,

(1,100)이 성공하더라도 (1,200)은 들어가지 않기 때문에 

예외처리로 롤 백이 되는 것이다


트랜잭션 매니저를 직접 생성하지 않고

주입해서 실습해본다

 

root-context.xml에 추가

	<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"/>
	</bean>
	<tx:annotation-driven/>

트랜잭션 매니저를 빈으로 등록했다!

 

 

데이터트랜잭션매니저를 주입하고

트랜잭션 매니저를 생성하는 부분을 주석처리했다.

@RunWith(SpringJUnit4ClassRunner.class) //ac를 자동으로 만들어줌
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"}) //xml 경로 지정해주기
public class A1DaoTest {

    @Autowired
    A1Dao a1Dao;

    @Autowired
    DataSource ds;

    @Autowired
    DataSourceTransactionManager tm;

    @Test
    public void insertTest() throws Exception{
        //TxManager를 생성
//        PlatformTransactionManager tm = new DataSourceTransactionManager(ds);
        TransactionStatus status=tm.getTransaction(new DefaultTransactionDefinition());
        //Tx 시작
        try {
            a1Dao.deleteAll();
            a1Dao.insert(1,100);
            a1Dao.insert(2,200);
            tm.commit(status); //성공하면 커밋
        } catch (Exception e) {
            e.printStackTrace();
            tm.rollback(status); //예외가 발생하면 롤백
        } finally {
        }
    }

}

 

새로운 테이블 b1 하나 만듦

create table b1 select * from a1 where false; #테이블만 생성
create table b1 select * from a1; #테이블도 생성하고 데이터도 복사

 

A1Dao를 복사해서 B1Dao를 만들었다.

package com.fastcampus.ch3;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Repository;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

@Repository
public class B1Dao {

    @Autowired
    DataSource ds;

    public int insert(int key, int value) throws Exception{
        Connection conn = null;
        PreparedStatement pstmt = null;

        try {
//            conn = ds.getConnection();
            conn= DataSourceUtils.getConnection(ds);

            pstmt = conn.prepareStatement("insert into b1 values(?,?)");
            pstmt.setInt(1,key);
            pstmt.setInt(2,value);

            return pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
            throw e;
        } finally {
//            close(conn,pstmt); //커넥션을 닫으면 트랜잭션이 종료되므로
            close(pstmt); //pstmt만 닫는다
            DataSourceUtils.releaseConnection(conn,ds); //트랜잭션매니저가 close할지 말지 알아서 판단
        }

    }

    private void close(AutoCloseable... acs) {
        for(AutoCloseable ac :acs)
            try { if(ac!=null) ac.close(); } catch(Exception e) { e.printStackTrace(); }
    }

    public void deleteAll() throws Exception{
        Connection conn = DataSourceUtils.getConnection(ds); //deletAll은 Tx와 별개로 동작하므로, Connection conn = ds.getConnection();으로 해야한다
        String sql="delete from a1";
        PreparedStatement pstmt = conn.prepareStatement(sql);
        pstmt.executeUpdate();
        close(pstmt);
    }
}

 

트랜잭션 서비스를 만들어보자

@Service
public class TxService {

    @Autowired
    A1Dao a1Dao;

    @Autowired
    B1Dao b1Dao;

    public void insertA1WithoutTx() throws Exception{
        a1Dao.insert(1,100);
        a1Dao.insert(1,200);
    }
}

 

위 코드에 대한 테스트를 만들어보자

@RunWith(SpringJUnit4ClassRunner.class) //ac를 자동으로 만들어줌
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"}) //xml 경로 지정해주기
public class TxServiceTest {
    @Autowired
    TxService txService;
    
    @Test
    public void insertA1WithoutTxTest() throws Exception{
        txService.insertA1WithoutTx();
    }

}

 

테스트를 돌리면,

커넥션이 서로 다르기때문에

트랜잭션이 제대로 안된다.

 

충돌 났어도 하나는 제대로 성공한다.


이번엔 WithTx를 한번 만들어보자.

@Transactional 어노테이션을 붙이면,

AOP가 위아래로 (부가 코드를) 주입을 해준다.

@Service
public class TxService {

    @Autowired
    A1Dao a1Dao;

    @Autowired
    B1Dao b1Dao;

    public void insertA1WithoutTx() throws Exception{
        a1Dao.insert(1,100);
        a1Dao.insert(1,200);
    }

//    @Transactional(rollbackFor = Exception.class) //이 어노테이션이 있어야 Exception과 그 자손들이 롤백이 된다.
    @Transactional //RuntimeException, Error만 rollback한다.
    public void insertA1WithTxFail() throws Exception{
        a1Dao.insert(1,100); //성공
        a1Dao.insert(1,200); //실패
    }

    @Transactional
    public void insertA1WithTxSuccess() throws Exception{
        a1Dao.insert(1,100);
        a1Dao.insert(2,200);
    }
}
@RunWith(SpringJUnit4ClassRunner.class) //ac를 자동으로 만들어줌
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"}) //xml 경로 지정해주기
public class TxServiceTest {
    @Autowired
    TxService txService;

    @Test
    public void insertA1WithoutTxTest() throws Exception{
        txService.insertA1WithTxFail();
    }

}

 

isolation의 default는 DB의 설정을 따른다는 것

 

rollbackFor 와 noRollbackFor는 반대이다

 

 

propgation 설명!

Requires_new는 TX안에 다른 TX

Nested는 TX안에 SUB TX - 같은 TX

위 상자가 아래 상자를 호출한다

 

REQUIRED는 TX가 기존에 있으면 새로 TX를 만들지 않는다.

만약 B2에서 에러가 나면 TX1 시작 부분으로 롤백이 된다.

그러므로 TX의 경계가 무척 중요하다.

REQUIRES_NEW는 새로운 TX가 필요하다는 것이다.

B1시작 전 새로운 트랜잭션2가 시작되기 때문에,

 

두개의 트랜잭션이 있는것이다.

만약 B2에서 예외가 발생한다면

TX2시작쪽으로 롤백하는 것이다.

 

만약 A2에서 에러가 생겼다면

TX1시작쪽으로 롤백한다.

작업 B1,B2까지 취소되지 않고 커밋이 된다.(별도의 트랜잭션이기 때문에..)


실습!

@Service
public class TxService {

    @Autowired
    A1Dao a1Dao;

    @Autowired
    B1Dao b1Dao;

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) //하나의 트랜잭션 안에 묶임
    public void insertA1WithTx() throws Exception{
        a1Dao.insert(1,100); //성공
        insertB1WithTx();
        a1Dao.insert(2,100); //성공
    }

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void insertB1WithTx() throws Exception {
        b1Dao.insert(1,100); //성공
        b1Dao.insert(1,200); //실패
    }
@RunWith(SpringJUnit4ClassRunner.class) //ac를 자동으로 만들어줌
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"}) //xml 경로 지정해주기
public class TxServiceTest {
    @Autowired
    TxService txService;

    @Test
    public void insertA1WithoutTxTest() throws Exception{
        txService.insertA1WithTx();
    }

}

 

a1, b1 둘 다 아무런 데이터가 들어가지 않는다


@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) //하나의 트랜잭션 안에 묶임
public void insertA1WithTx() throws Exception{
    a1Dao.insert(1,100); //성공
    insertB1WithTx();
    a1Dao.insert(1,100); //실패
}

@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void insertB1WithTx() throws Exception {
    b1Dao.insert(1,100); //성공
    b1Dao.insert(1,200); //성공
}

이번에도 a1, b1 둘 다 아무런 데이터가 들어가지 않는다

 

두개 다 하나의 트랜잭션에 묶여버렸기 때문에


A1 -> 트랜잭션 경계선 B1 B2 트랜잭션 경계선 -> A2

@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) //하나의 트랜잭션 안에 묶임
public void insertA1WithTx() throws Exception{
    a1Dao.insert(1,100); //성공
    insertB1WithTx();
    a1Dao.insert(2,200); //성공
}

@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void insertB1WithTx() throws Exception {
    b1Dao.insert(1,100); //성공
    b1Dao.insert(1,200); //실패
}

 

a1에만 데이터가 들어간다


@Transactional이 동작하지 않는 이유는 같은 클래스에 속한 메서드끼리의 호출(내부 호출)이기 때문

프록시방식(디폴트)의 aop는 내부호출인 경우, advice가 적용되지 않음.

그래서 tx가 적용되지 않는 것임.

두 메서드를 별도의 클래스로 분리하면 tx가 적용됨.

근본적인 해결은 프록시 방식이 아닌 다른 방식을 사용해야함.

'패캠 챌린지' 카테고리의 다른 글

MaBatis로 DAO작성하기  (0) 2023.05.11
MyBatis  (0) 2023.04.28
AOP  (0) 2023.04.04
Transaction, Commit, Rollback  (0) 2023.04.03
DAO의 작성과 적용  (0) 2023.03.30
profile

Burninghering's Blog

@개발자 김혜린

안녕하세요! 반갑습니다.