[Spring] Service 계층의 분리 | @Transactional

Jeini·2023년 5월 28일
0

🍃  Spring

목록 보기
21/33
post-thumbnail

💡 서비스 계층(Layer)의 분리- 비지니스 로직의 분리


✏️ 계층 분리

  • 관심사의 분리: 성격이 다른 코드, 변경돼야 하는 이유가 다른 코드는 분리해야 한다.
  • 중복 코드 제거

  • 사용자 이력(UserHistoryDao) ➡️ 비즈니스 로직
    : 데이터베이스와는 상관이 없다. 단순히 DB 테이블하고 1대1관계로 테이블을 다루는 곳이기 때문에 비즈니스 로직을 DAO에 두지 않는다. 그럼 둘 곳이 Controller밖에 없다. 하지만 Controller에 넣기에는 역할이 비즈니스 역할이 아니다. 그렇기 때문에 비즈니스 역할을 담당할 객체를 하나 더 만들어야 한다.

  • UserDao ➡️ CRUD
    : 비즈니스 로직과 하는 일이 다르기 때문에 같이 묶는것은 적합하지 않다.

✏️ UserService 추가

  • RegisterController ➡️ Presentation

  • UserService ➡️ Service & 비즈니스 로직
    : DAO를 불러서 처리
    : Controller는 UserService만 주입 받아서 사용하면 된다.
    : 트랜잭션을 사용하기에도 적합함. 예를 들어, 회원가입을 할려면 UserDao에서 insert()와 UserHistoryDao에서 insertUserHistory() 를 하나의 트랜잭션에 묶어서 테스트를 해야 한다. 둘다 통과되면 회원가입이 진행
    ➡️ Controller에서도 할 수 있지만 너무 복잡해지고 불필요한 기능들이 섞이게 됨. 그래서 별도의 service 객체를 둬서 그 기능들만 사용할 수 있게 두는 것이 적합하다.

  • UserDao & UserHistoryDao ➡️ Persistence(영속 계층)
    :영속 계층 = 데이터베이스가 영속성을 갖고 있음

💡 TransactionManager


✔️ DAO의 각 메서드는 개별 Connection을 사용
✔️ 같은 트랜잭션내에서 같은 Connection을 사용할 수 있게 관리

  • deleteUser() 를 보면, Connection을 가져온다.
    : 메서드마다 각각의 Connection을 따로 만들면서 처리한다.

  • 하나의 트랜잭션은 하나의 Connection으로만 이뤄져야 한다.
    : deleteUser() 두번 호출하면 각각의 Connection이 된다. 둘 다 커밋한 상태에서 하나의 deleteUser()가 rollback을 한다면 되돌릴 수 없게 된다. 개별 Connection에서 실행이 되기 때문이다. 이 문제를 해결하려면, 개별 Connection을 1개의 Connection으로 쓰게 해 줘야 한다. 즉, deleteUser() 는 각각의 Connection이 아닌 같은 Connection을 써줘야 한다.

  • UserDao안에서는 각각의 Connection을 사용하고 있지만 TransactionManager가 통합할 수 있게 해준다.

❗️ DAO에서 Connection을 얻거나 반환할 때 DataSourceUtils를 사용해야 한다.

  • TransactionManager를 썼는데도 Connection이 묶이지 않는다면, DataSourceUtils.getConnection(ds); 부분을 잘 확인해 봐야 한다.

📎 TransactionManager로 Transaction 적용하기

✏️ Transaction 코드로 직접 생성

  1. TransactionManager를 생성

  2. DefalutTransactionDefinition 을 이용해서 Transaction을 얻어온다.
    : 원래는 setting해줘야 하는데 defalut속성으로 처리함

  3. 트랜잭션 시작
    : 성공 or 실패

❗️ __DefalutTransactionDefinition
: 트랜잭션의 속성을 정의

✏️ TransactionManager을 빈으로 등록

  • <tx:annotation-driven> 이 있어야 @Transaction 사용 가능

💡 @Transactional


✔️ AOP를 이용한 핵심 기능과 부가 기능의 분리

  • insert() 하는 부분이 핵심 기능. 나머지는 부가 기능
    : 핵심 기능에 트랜잭션의 부가 기능을 추가해준 것.
    : 부가 기능은 바뀌지 않는다. ➡️ AOP로 자동 코드를 추가하게 해 준다.

  • AOP를 이용해서 트랜잭션의 기능을 분리한 것 = @Transactional
    : 이렇게 우리는 핵심 기능만 집중할 수 있게 되었다.

✔️ @Transaction은 클래스나 인터페이스에도 붙일 수 있음

  • 클래스에 붙으면 클래스내의 모든 메서드에 적용 가능 (인터페이스도 마찬가지)

❗️@Transactional 은 테스트 케이스에 선언시 테스트 시작전에 트랜잭션을 시작하고, 테스트 완료 후 항상 롤백을 하여, 다음 테스트에 영향을 주지 않는다.


⚙️ @Transactional 속성

  • isolation-DEFAULT
    : DB의 설정에 따른다는 의미.

⚙️ propagation 속성의 값


✔️ 트랜잭션의 경계가 어떻게 설정됨에 따라서 결과가 달라진다.

  • REQUIRED & REQUIRES_NEW 중요

  • REQUIRED_NEW
    : 트랜잭션안에 다른 트랜잭션 생성

  • NESTED
    : 트랜잭션안에 sub트랜잭션 생성 ➡️ 같은 트랜잭션안에서 처리
    : save point가 있다.

📎 REQUIRED

✔️ 트랜잭션에 기존에 있으면 새로운 트랜잭션을 만들지 않는다.

  • 하나의 트랜잭션인 것처럼 만들어진다.

  • B2에서 에러가나면 처음인 A1부터 롤백된다.

  • REQUIRED는 기본값이여서 생략해도 된다.

📎 REQUIRES_NEW

✔️ 새로운 트랜잭션을 만든다.

  • 2개의 트랜잭션이 만들어진다.

  • A2에서 예외가 뜨면 다시 A1으로 rollback되지만 B1,B2 는 취소되지 않는다. 별도의 트랜잭션이기 때문

✏️ 총 실습


📌 a1 테이블을 만들고 insert()를 하는 테스트

✏️ Repository

package kr.ac.jipark09;

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 dataSource;

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

        try {
            connection = dataSource.getConnection();
            ps = connection.prepareStatement("insert into a1 values(?, ?)");
            ps.setInt(1, key);
            ps.setInt(2, value);

            return ps.executeUpdate();

        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            close(connection, ps);
        }
    }

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

✏️ Test

package kr.ac.jipark09;

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;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"})
public class A1DaoTest {
    @Autowired
    A1Dao a1Dao;

    @Test
    public void insertTest() throws Exception {
        a1Dao.insert(1, 100); // 성공
        a1Dao.insert(1, 200); // 실패
    }
}
  • 결과는 하나는 성공했고 하나는 실패했다. 각각의 Connection을 가지고 있어서 각각의 테스트가 되어버렸다. 이것을 통합해보자.

✔️ 트랜잭션 묶기

✏️ insertTest() 수정 ➡️ 하나의 트랜잭션으로 바꿈

package kr.ac.jipark09;

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;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"})
public class A1DaoTest {
    @Autowired
    A1Dao a1Dao;
    @Autowired
    DataSource dataSource;

    @Test
    public void insertTest() throws Exception {
        // 트랜잭션을 생성
        PlatformTransactionManager tm = new DataSourceTransactionManager(dataSource);
        TransactionStatus status = tm.getTransaction(new DefaultTransactionDefinition());

        // 트랜잭션 시작
        try {
            // 하나의 트랜잭션으로 바꿈
            a1Dao.insert(1, 100);
            a1Dao.insert(1, 200);
            tm.commit(status);
        } catch (Exception e) {
            tm.rollback(status);
        } finally {

        }
    }
}

✏️ DAO 수정: DataSourceUtils 사용

package kr.ac.jipark09;

import org.apache.taglibs.standard.tag.common.sql.DataSourceUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Repository;

import javax.sql.DataSource;
import javax.xml.crypto.Data;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

@Repository
public class A1Dao {
    @Autowired
    DataSource dataSource;

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

        try {
//            connection = dataSource.getConnection();
            connection = DataSourceUtils.getConnection(dataSource);
            ps = connection.prepareStatement("insert into a1 values(?, ?)");
            ps.setInt(1, key);
            ps.setInt(2, value);

            return ps.executeUpdate();

        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
//            close(connection, ps); 닫으면 종료되니까 ㄴㄴ
            close(ps);
            // TransactionManager가 닫아야되는지 아닌지 판단한다.
            DataSourceUtils.releaseConnection(connection, dataSource);
        }
    }

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

  • 기본키가 중복이여서 실패한 것이니까 둘다 rollback돼서 아무것도 나오지 않으면 성공이다 !!

✔️ TransactionManager를 직접 생성하지 않고 주입받아서 처리

✏️ root-context-xml에 트랜잭션 매니저 빈 등록

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven/>
  • <tx:annotation-driven/>
    : @Transactional 어노테이션을 쓰기 위해 등록

✏️ Test 수정: 빈으로 등록한 DataSourceTransactionManager를 주입 ➡️ 직접 코드로 생성 ❌

package kr.ac.jipark09;

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.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import javax.sql.DataSource;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"})
public class A1DaoTest {
    @Autowired
    A1Dao a1Dao;
    @Autowired
    DataSource dataSource;
    // 빈으로 등록한 DataSourceTransactionManager를 주입
    @Autowired
    DataSourceTransactionManager tm;

    @Test
    public void insertTest() throws Exception {
        // 트랜잭션을 생성
//        PlatformTransactionManager tm = new DataSourceTransactionManager(dataSource);
        TransactionStatus status = tm.getTransaction(new DefaultTransactionDefinition());

        // 트랜잭션 시작
        try {
            // 하나의 트랜잭션으로 바꿈
            a1Dao.deleteAll();
            a1Dao.insert(1, 100);
            a1Dao.insert(2, 200);
            tm.commit(status);
        } catch (Exception e) {
            tm.rollback(status);
        } finally {

        }
    }
}

📌 비즈니스 로직 활용 (@Service)

✏️ a1을 사용하여 테이블 b1 생성

create table b1 select * from a1 where false; -- 테이블만 생성
create table b1 select * from a1; -- 테이블만 생성 & 데이터도 복사
  • 이렇게 만든 b1 테이블에 기본키도 설정해 줘야 한다.

✏️ B1Dao 생성

package kr.ac.jipark09;

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 dataSource;

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

        try {
            connection = DataSourceUtils.getConnection(dataSource);
            System.out.println(connection);
            ps = connection.prepareStatement("insert into b1 values(?, ?)");
            ps.setInt(1, key);
            ps.setInt(2, value);

            return ps.executeUpdate();

        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            close(ps);
            DataSourceUtils.releaseConnection(connection, dataSource);
        }
    }

    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 connection = dataSource.getConnection();
        String sql = "delete from a1";
        PreparedStatement ps = connection.prepareStatement(sql);
        ps.executeUpdate();
        close(ps);
    }
}

✏️ 비즈니스 로직 생성 ➡️ Service

package kr.ac.jipark09;

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

@Service
public class TxService {
    @Autowired
    A1Dao a1Dao;
    @Autowired
    B1Dao b1Dao;

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

    }
}

✏️ Test

package kr.ac.jipark09;

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;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"})
public class TxServiceTest {
    @Autowired
    TxService txService;

    @Test
    public void insertA1WithoutTest() throws Exception {
        txService.insertA1WithoutTx();
    }

}

  • 당연히 트랜잭션에 묶여 있지 않아서 다른 Connection을 가진다. 하나만 성공함.

✔️ @Transactional 주입

✏️ Service 수정: @Transactional 추가

package kr.ac.jipark09;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TxService {
    @Autowired
    A1Dao a1Dao;
    @Autowired
    B1Dao b1Dao;

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

    }
}

  • 같은 Connection을 쓰고 있는 것을 확인

❗️ @Transactional(rollbackFor = Exception.class)
: 예외가 뜨면 rollback 하라는 의미
: @Transactional 은 RuntimeException과 Error만 rollback 한다.


📌 propagation 속성의 값 활용

✔️ REQUIRED 활용

✏️ Service

package kr.ac.jipark09;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TxService {
    @Autowired
    A1Dao a1Dao;
    @Autowired
    B1Dao b1Dao;

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

    @Transactional(propagation = Propagation.REQUIRED)
    public void insertB1WithTx() throws Exception {
        b1Dao.insert(1, 100); // 성공
        b1Dao.insert(1, 200); // 실패
    }
}
  • 둘다 REQUIRED니까 하나의 트랜잭션에 묶인다.

  • 같은 트랜잭션을 쓰고 있는 것을 확인
    : a1, b1 테이블에는 아무것도 없어야 한다. (하나가 실패했기 때문)

✔️ REQUIRES_NEW 활용

❗️ 같은 클래스에 속한 메서드끼리의 호출(내부 호출)은 @Transactional이 동작하지 않는다. 그 이유는, 프록시 방식(defalut)의 AOP는 내부 호출인 경우, Advice가 적용되지 않는다. 그래서 트랜잭션이 적용되지 않는 것.
➡️ 두 메서드를 별도의 클래스로 분리하면 트랜잭션이 적용된다.

✏️ Service

package kr.ac.jipark09;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import javax.sql.DataSource;

@Service
public class TxService {
    @Autowired
    A1Dao a1Dao;
    @Autowired
    B1Dao b1Dao;
    @Autowired
    DataSource dataSource;

//    @Transactional(propagation = Propagation.REQUIRED)
//    public void inserA1WithTx() throws Exception {
//        a1Dao.insert(1, 100); // 성공
//        insertB1WithTx();
//        a1Dao.insert(2, 200); // 성공
//    }
    public void insertA1WithTx() throws Exception {
        PlatformTransactionManager tm = new DataSourceTransactionManager(dataSource);
        DefaultTransactionDefinition txd = new DefaultTransactionDefinition();
        txd.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        TransactionStatus status = tm.getTransaction(txd);

        try {
            a1Dao.insert(1, 100); // 성공
            insertB1WithTx();
            a1Dao.insert(1, 200); // 실패
            tm.commit(status);
        } catch (Exception e) {
            tm.rollback(status);
        } finally {
        }
    }

    public void insertB1WithTx() throws Exception {
        PlatformTransactionManager tm = new DataSourceTransactionManager(dataSource);
        DefaultTransactionDefinition txd = new DefaultTransactionDefinition();
        txd.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        TransactionStatus status = tm.getTransaction(txd);

        try {
            b1Dao.insert(1, 100); // 성공
            b1Dao.insert(2, 200); // 성공
            tm.commit(status);
        } catch (Exception e) {
            tm.rollback(status);
        } finally {
        }
    }

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

  • b1 테이블의 결과만 나옴

  • 묶음으로 다른 트랜잭션으로 된 것을 확인해 볼 수 있다.

Reference
: https://fastcampus.co.kr/dev_academy_nks

profile
Fill in my own colorful colors🎨

0개의 댓글