Spring 트랜잭션 처리 2-1. XML based Declarative

Spring의 선언적 트랜잭션 관리 기법 중에서 XML 설정 내용을 사용하는 방법을 살펴 봅니다.

다른 방법으로는 어노테이션을 사용하는 방법이 있습니다. 이것은 Spring 트랜잭션 관리가 Spring AOP(그중에서도 Autoproxing)를 사용하고 있기 때문에 Spring AOP를 구현하는 두 가지 방법과 나란히 두 가지 방법을 마련한 것 같습니다.

먼저 트랜잭션 설정을 하기 위한 네임스페이스가 필요합니다. 다음과 같은 XML 설정 탬플릿을 사용하면 됩니다.

<?xml version=”1.0″ encoding=”UTF-8″?>
<beans xmlns=”http://www.springframework.org/schema/beans”
       xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”
       xmlns:util=”http://www.springframework.org/schema/util”
       xmlns:p=”http://www.springframework.org/schema/p”
       xmlns:aop=”http://www.springframework.org/schema/aop”
       xmlns:tx=”http://www.springframework.org/schema/tx
       xsi:schemaLocation=”
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
default-autowire=”byName”>

//여기에 기타 bean 설정을 넣어 줍니다.

</beans>

그리고 일반 bean들을 등록한 뒤 “트랜잭션 Advice”로 트랜잭션 처리할 메소드의 이름과 트랜잭션 특성을 지정해 주고 “트랜잭션 Advisor”로 방금 만든 Advice와 이 Advice를 적용한 Pointcut을 묶어 줍니다.

    <bean id=”memberService” class=”com.bookbuying.member.service.MemberServiceImplDeclarativeTransactionManagement”>
        <property name=”memberDao” ref=”memberDao” />
    </bean>

    <bean id=”memberDao” class=”com.bookbuying.member.dao.MemberDaoImpleWIthSpringTransaction”>
        <property name=”sessionFactory” ref=”sessionFactory” />
    </bean>

    <tx:advice id=”txAdvice” transaction-manager=”transactionManager”>
        <tx:attributes>
            <tx:method name=”add*” propagation=”REQUIRED” />
        </tx:attributes>
    </tx:advice>

    <aop:config>
        <aop:pointcut id=”memberServiceOperation”
            expression=”execution(* com.bookbuying.member.service.MemberService.*(..))” />
        <aop:advisor advice-ref=”txAdvice”
            pointcut-ref=”memberServiceOperation” />
    </aop:config>

설정 파일의 내용은 좀 복잡해 졌지만 자바 코드는 매우 깔끔해 졌습니다. 더이상 Business Layer에 전~혀 트랜잭션 관련 코드가 들어있지 않습니다.

    private MemberDao memberDao;

    public void setMemberDao(MemberDao memberDao) {
        this.memberDao = memberDao;
    }

    public void add(Member member) throws SQLException {
        memberDao.add(member);
        //TODO 예외 발생시키기
        TransactionTestingUtil.generateExceptionMethod();
    }

Dao 구현체는 이전에 만들어둔 클래스를 사용하였습니다. 테스트 코드 역시 이전 글들에서 사용한 것을 사용했으며 제대로 동작하는 것을 확인했습니다.

Spring 트랜잭션 처리 1. Programmtic

참조 : Spring 프레임워크 워크북, 9.6. Programmatic transaction management (1)

레퍼런스와 워크북에도 나오지만 프로그래밍 적인 트랜잭션 처리 방법은 크게 두 가지가 있습니다.

1. PlatformTransactionManager, TransactionStatus, TransactionDefinition을 사용하는 방법

(1)TransactionManager를 만들 때는 사용하는 트랜잭션 자원에 따라 만드는 방법이 다릅니다. (2)그리고 TransactionManager로 부터 getTransaction을 사용하여 트랜잭션을 생성하게 되는데 이 때 TransactionStatus라는 객체를 받게 됩니다. (3)롤백과 커밋은 TransactionManager를 사용하며 이 때 Status객체를 넘겨줘야 합니다.

서비스 계층의 구현된 코드를 보면서 위의 내용을 살펴보겠습니다.

public class MemberServiceImplWithSpringTransaction implements MemberService {

    private MemberDao memberDao;

    private PlatformTransactionManager transactionManager;

    public void setMemberDao(MemberDao memberDao) {
        this.memberDao = memberDao;
    }

    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public void add(Member member) throws SQLException {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        def.setName(“멤버 추가하는 트랜잭션”);
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        TransactionStatus status = transactionManager.getTransaction(def); //(2)
        try {
            memberDao.add(member);
            //TODO 예외 발생
            TransactionTestingUtil.generateExceptionMethod();
            transactionManager.commit(status); //(3)
        }
        catch (MyException e) {
            System.out.println(def.getName() + ” 롤백합니다.”);
            transactionManager.rollback(status); //(3)
            throw e;
        }
    }

}

TransactionManager는 HibernateTransactionManager 를 사용하고 있습니다. XML 설정을 보면 다음과 같습니다.

    <bean id=”memberService” class=”com.bookbuying.member.service.MemberServiceImplWithSpringTransaction”>
        <property name=”memberDao” ref=”memberDao” />
        <property name=”transactionManager” ref=”transactionManager” />
    </bean>

    <bean id=”memberDao” class=”com.bookbuying.member.dao.MemberDaoImpleWIthSpringTransaction”>
        <property name=”sessionFactory” ref=”sessionFactory” />
    </bean>

    <bean id=”sessionFactory” class=”org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean” autowire=”no”>
        <property name=”dataSource” ref=”dataSource” />
        <property name=”annotatedClasses” ref=”annotatedClasses” />
        <property name=”hibernateProperties”>
            <props>
                <!– 지금 사용하고 있는 데이터베에스에 최적화 된 SQL을 생성하기 위해서
                      현재 데이터베이스에 해당하는 Hibernate dialect 클래스 명 –>
                <prop key=”hibernate.dialect”>${hibernate.dialect}</prop>

                <!–  콘솔 창에 사용하는 모든 SQL문을 출력할지를 선택  –>
                <prop key=”hibernate.show_sql”>true</prop>

                <!– DB Schema에 변경사항이 발생한 경우 자동으로 수정  –>
                <prop key=”hibernate.hbm2ddl.auto”>update</prop>

                <!– 성능 튜닝에 도움이 되는 유용한 통계를 제공  –>
                <prop key=” hibernate.generate_statistics”>true</prop>

                <!– 자동 커밋이 되는 것을 방지 –>
                <prop key=”hibernate.connection.autocommit”>false</prop>

                <!– “Current” Session의 Scope 설정.<jta/thread/managed/custom> –>
                <prop key=”hibernate.current_session_context_class”>thread</prop>
            </props>
        </property>
    </bean>

    <util:list id=”annotatedClasses”>
        <value>com.bookbuying.domain.Member</value>
    </util:list>

    <bean id=”transactionManager” class=”org.springframework.orm.hibernate3.HibernateTransactionManager” autowire=”no”>
        <property name=”sessionFactory” ref=”sessionFactory” />
    </bean>

주석은 친절한 하이버네이트 찬욱군이 달아 놓았습니다. Hibernate를 사용하여 DAO쪽 구현을 했습니다. 여기서 Session에 주목할 필요가 있습니다. 그건 다음 글에서 살펴보겠습니다.

public class MemberDaoImpleWIthSpringTransaction implements MemberDao{

    private SessionFactory sessionFactory;

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public void add(Member member) {
        Session session = sessionFactory.getCurrentSession();
        session.save(member);
    }
}

JDBC 기반 트랜잭션 처리 2. Business Layer에서 책임

참조 : Spring 프레임워크 워크북

이번에는 DAO 계층이 아닌 Service 계층으로 트랜잭션 책임을 다시 가져왔습니다. 따라서 서비스 계층이 이 전글에 비하면 좀 더 복잡해 졌습니다. 게다가 쌩 JDBC를 사용하고, 거기다 트랜잭션 처리도 AOP 사용해서 모듈화 하지 않았기 때문에 코드는 다음과 같이 다소 복잡합니다.

public class MemberServiceImplTransactionInBusiness implements MemberService{

    private MemberDao memberDao;

    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void setMemberDao(MemberDao memberDao) {
        this.memberDao = memberDao;
    }

    public void add(Member member) throws SQLException {

        Connection con = null;
        try {
            con = dataSource.getConnection();
            con.setAutoCommit(false);
            memberDao.add(con, member);
            //TODO 예외발생
            TransactionTestingUtil.generateSQLExceptionMethod();
            con.commit();
        }
        catch (SQLException e) {
            if(con != null)
                con.rollback();
            throw e;
        }
        finally {
            if(con != null)
                con.close();
        }
    }

}

add()메소드에서 해야할 일은 딱 두 줄(파란색)임에도 불구하고 여러 줄의 코드가 정신없이 널려있습니다. DAO 코드를 보겠습니다.

public class MemberDaoJdbcTransactionInBusiness implements MemberDao {

    public void add(Member member) {
        throw new UnsupportedOperationException();
    }

    public void add(Connection con, Member member) throws SQLException {
        StringBuilder addMemberQuery = new StringBuilder();
        addMemberQuery.append(“INSERT INTO Member(id, password, name, email) “);
        addMemberQuery.append(“VALUES (?, ?, ?, ?)”);

        try {
            PreparedStatement pstmt = con.prepareStatement(addMemberQuery.toString());
            pstmt.setString(1, “whiteship3”);
            pstmt.setString(2, “pass”);
            pstmt.setString(3, “기선”);
            pstmt.setString(4, “keesun3@email.com”);
            pstmt.executeUpdate();
        }
        catch (SQLException e) {
            throw e;
        }
    }

}

JDBC 코딩이 별로 재미도 없고 복잡합니다. 서비스 계층에서 사용하던 connection 객체를 그대로 이어 받아서 사용합니다. 그리고 만약 복잡한 SQL을 사용했을 떄 DB가 바껴버리면 그 복잡한 SQL들을 다 어떻게 관리할지 걱정이 됩니다.

테스트코드는 이전 글과 동일합니다. Spring 설정파일은 다음과 같습니다.

    <bean id=”memberService” class=”com.bookbuying.member.service.MemberServiceImplTransactionInBusiness”>
        <property name=”dataSource” ref=”dataSource” />
        <property name=”memberDao” ref=”memberDao” />
    </bean>

    <bean id=”memberDao” class=”com.bookbuying.member.dao.MemberDaoJdbcTransactionInBusiness” />

    <bean id=”dataSource” class=”com.mchange.v2.c3p0.ComboPooledDataSource”>
        <property name=”driverClass” value=”${db.driver}”/>
        <property name=”jdbcUrl” value=”${db.url}”/>
        <property name=”user” value=”${db.username}”/>
        <property name=”password” value=”${db.password}”/>
    </bean>

다음 글에서는 Spring의 Transaction 관련 주요 API들(TransactionDefinition, TransactionStatus, PlatformTransactionManager)을 사용하여 구현하겠습니다.

JDBC 기반 트랜잭션 처리 1. Persistence Layer에서 책임

참조 : Spring 프레임워크 워크북

Spring 트랜잭션 처리를 예제 코드로 살펴보기 위해 먼저 JDBC 기반의 트랜잭션 처리의 불편함을 몸소 체험할 수 있는 코드를 작성했습니다.

트랜잭션 처리는 비즈니스 계층에서 해야겠죠. 하지만 왠지 트랜잭션! 이라고 하면 DB랑 연관이 있는 것 같아서 왠지 DAO 계층에서 처리해야 될 것만 같은 느낌을 받았었습니다. 하지만 생각해 보면 DAO는 그냥 DB에 접근하는 CRUD 하는 SQL을 날릴 뿐이고 진짜 고객이 원하는 작업의 단위는 Service 계층의 메소드 단위일 것이고 그 메소드 하나에는 여러 DAO 를 사용하여 Logical Unit of Works(=transaction)을 구현할 것입니다.

Anyway.. 스프링 워크북에 나온대로 퍼시스턴스 계층에서 책임지도록 코딩해 보았습니다.

public class MemberServiceImplTransactionInPersistence implements MemberService {

    private MemberDao memberDao;

    public void setMemberDao(MemberDao memberDao) {
        this.memberDao = memberDao;
    }

    public void add(Member member) throws SQLException {
        memberDao.add(member);
    }

}

서비스 계층은 위와 같이 단순합니다. 모든 책임이 memberDao의 add에 있고 사실 여기있는 add메소드에서 memberDao.add(member); 만 호출합니다. 그 안에는 아래와 같이 다른 작업과 트랜잭션처리도 담당합니다.

public class MemberDaoJdbcTransactionInPersistence implements MemberDao {

    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void add(Member member) throws SQLException {
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = dataSource.getConnection();
            con.setAutoCommit(false);

            StringBuilder addMemberQuery = new StringBuilder();
            addMemberQuery.append(“INSERT INTO Member(id, password, name, email) “);
            addMemberQuery.append(“VALUES (?, ?, ?, ?)”);

            pstmt = con.prepareStatement(addMemberQuery.toString());
            pstmt.setString(1, “whiteship3”);
            pstmt.setString(2, “pass”);
            pstmt.setString(3, “기선”);
            pstmt.setString(4, “keesun3@email.com”);

            pstmt.executeUpdate();

            //TODO 예외 발생하는 코드
            TransactionTestingUtil.generateSQLExceptionMethod();

            con.commit();
        }
        catch (SQLException e) {
            if(con != null)
                con.rollback();
            throw e;
        }
        finally {
            if(pstmt != null)
                pstmt.close();
            if(con != null)
                con.close();
        }
    }

    public void add(Connection con, Member member) {
        throw new UnsupportedOperationException();
    }
}

와오~ DAO 열라 복잡합니다. 할 일은 파란 색 코드 두 줄입니다. 벌써 부터 이상합니다. DAO 계층이 서비스 계층 코드에서 할 일(예외 발생 부분) 까지 포함하게 됐습니다. 이건 마치 DAO와 서비스 계층이 서로 바뀐 것 같은 모양입니다.

테스트 코드는 아래와 같습니다. 예외를 던지게 해뒀기 때문에 예외가 발생했으면 제대로 동작한 것이고 예외를 안던지면 커밋된 것으로 볼 수 있습니다.

    @Test
    public void testRollBackTransaction() {
        Member member = getMember();
        try {
            memberService.add(member);
            fail();
        }
        catch (SQLException e) {
            e.printStackTrace();
        }
    }

bean 설정은 다음과 같습니다.

    <!– jdbc transaction in persistence –>
    <bean id=”memberService” class=”com.bookbuying.member.service.MemberServiceImplTransactionInPersistence”>
        <property name=”memberDao” ref=”memberDao” />
    </bean>

    <bean id=”memberDao” class=”com.bookbuying.member.dao.MemberDaoJdbcTransactionInPersistence”>
        <property name=”dataSource” ref=”dataSource” />
    </bean>

    <bean id=”propertyConfigurer” class=”org.springframework.beans.factory.config.PropertyPlaceholderConfigurer”>
        <property name=”locations”>
            <list>
                <value>classpath:database.properties</value>
            </list>
        </property>
    </bean>

    <bean id=”dataSource” class=”com.mchange.v2.c3p0.ComboPooledDataSource”>
        <property name=”driverClass” value=”${db.driver}”/>
        <property name=”jdbcUrl” value=”${db.url}”/>
        <property name=”user” value=”${db.username}”/>
        <property name=”password” value=”${db.password}”/>
    </bean>