실습 코드 참조
moonhy7/SpringFramework: Spring Framework 실습 코드 정리 (github.com)
7.1절 트랜잭션 네임스페이스 등록
1. 스프링의 트랜잭션 설정 ( _050_BoardWeb_JDBC_Transaction )
1) 선언적 트랜잭션
- 스프링과 비교되는 EJB는 모든 비즈니스 메소드에 대한 트랜잭션 관리를 EJB 컨테이너가 자동으로 처리해준다.
- 스프링에서도 트랜잭션 처리를 컨테이너가 자동으로 처리하도록 설정할 수 있는데 이를 선언적 트랜잭션이라 함
2) 스프링의 트랜잭션 설정
- 스프링의 트랜잭션 설정에서는 XML 기반의 AOP 설정만 사용할 수 있고 어노테이션은 사용할 수 없다.
- 또한 <aop:aspect> 엘리먼트 대신 <aop:advisor> 엘리먼트를 사용해야 한다.
2. 트랜잭션 네임스페이스 등록
1) 트랜잭션 네임스페이스 등록
7.2절 트랜잭션 관리자 등록
1. PlatformTransactionManager 인터페이스
- 트랜잭션 관련 설정에서 가장 먼저 등록하는 것은 트랜잭션 관리자 클래스이다.
- 스프링은 다양한 트랜잭션 관리자를 지원하는데 어떤 기술을 이용하여 데이터베이스 연동을 처리했느냐에 따라 트랜잭션 관리자가 달라진다.
- 모든 트랜잭션 관리자는 PlatformTransactionManager 인터페이스를 구현한 클래스들이다.
- 따라서 스프링이 제공하는 모든 트랜잭션 관리자는 트랜잭션 관리에 필요한 commit(), rollback() 메소드를 가진다.
2. DataSourceTransactionManager 클래스
- 현재 우리가 가지고 있는 두 개의 DAO 클래스는 모두 JDBC를 기반으로 동작하기 떄문에 당분간 DataSourceTransactionManager 클래스를 이용하여 트랜잭션을 처리할 것이다.
- 예를 들어 JPA를 이용하여 DAO 클래스를 구현했다면 JPATransactionManager를 등록해주면 된다.
- 이제 트랜잭션을 커밋, 롤백하기 위한 객체로 DataSourceTransactionManager를 <bean> 등록해주어면 된다.
- 그 후 트랜잭션 관리자가 가지고 있는 메소드를 호출하면서 실질적인 트랜잭션 관리 기능을 제공하는 어드바이스를 등록해주면 된다.
<!-- DataSource 설정 -->
<!-- 외부 properties파일 참조 -->
<context:property-placeholder location="classpath:config/datasource.properties"/>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<!-- setter 메소드를 이용한 의존성 주입 -->
<!-- Oracle -->
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
<!-- MySQl -->
<!--
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/studydb?serverTimezone=UTC"></property>
<property name="username" value="study"></property>
<property name="password" value="study"></property>
-->
</bean>
<!-- JDBCTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- Transaction 설정 -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
7.3절 트랜잭션 어드바이스 설정
1. 트랜잭션 관리 어드바이스 설정
- 트랜잭션에선 commit이나 rollback에 어떤 어드바이스 메소드가 호출될지 모르기 때문에 어드바이스 메소드를 지정할 수 없고 aspect가 아닌 advisor로 aop 설정해야됨
- 지금까지의 AOP 관련 설정에 사용한 모든 어드바이스 클래스는 직접 구현했지만 트랜잭션 관리 기능의 어드바이스는 스프링 컨테이너가 <tx:advice> 설정을 참조하여 자동으로 생성한다.
- tx:attributes : Transaction advice 동작 시점을 설정, 포인트컷 지정없이 메소드 지정가능
- tx:method : advice 동작할 메소드 지정
- read-only : transaction이 동작하지 않음 (get으로 시작하는 메소드들에서는 트랜잭션 동작 X)
- get*으로 시작하는 메소드들을 제외한 모든 메소드들에서는 트랜잭션이 동작 O
<!-- Transaction 설정 -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- Trasaction advice 등록 -->
<!-- 트랜잭션에선 commit이나 rollback 어떤 어드바이스 메소드가 호출될지 모르기 때문에
어드바이스 메소드를 지정할 수 없고 aspect가 아닌 advisor로 aop 설정해야됨 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<!-- tx:attributes : Transaction advice 동작시점을 설정, 포인트컷 지정없이 메소드 지정가능-->
<tx:attributes>
<!--
tx:method : advice 동작할 메소드 지정
read-only : transaction이 동작하지 않음(get으로 시작하는 메소드들에서는 트랜잭션 동작x)
get*으로 시작하는 메소드들을 제외한 모든 메소드들에서는 트랜잭션이 동작o
-->
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
2. <tx:method> 속성 종류
7.4절 AOP 설정을 통한 트랜잭션 적용
1. 트랜잭션 관리 설정
- txPointcut으로 지정한 메소드가 호출될 때 txAdvice로 등록한 어드바이스가 동작하여 트랜잭션을 관리하도록 설정함
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut expression="execution(* com.springbook.biz..*(..))" id="txPointcut"/>
<aop:advisor pointcut-ref="txPointcut" advice-ref="txAdvice"/>
</aop:config>
2. 스프링 설정 파일 전체 소스
<!-- DataSource 설정 -->
<!-- 외부 properties파일 참조 -->
<context:property-placeholder location="classpath:config/datasource.properties"/>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<!-- setter 메소드를 이용한 의존성 주입 -->
<!-- Oracle -->
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
<!-- MySQl -->
<!--
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/studydb?serverTimezone=UTC"></property>
<property name="username" value="study"></property>
<property name="password" value="study"></property>
-->
</bean>
<!-- JDBCTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- Transaction 설정 -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- Trasaction advice 등록 -->
<!-- 트랜잭션에선 commit이나 rollback 어떤 어드바이스 메소드가 호출될지 모르기 때문에 어드바이스 메소드를 지정할 수 없고
aspect가 아닌 advisor로 aop 설정해야됨 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<!-- tx:attributes : Transaction advice 동작시점을 설정, 포인트컷 지정없이 메소드 지정가능-->
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut expression="execution(* com.springbook.biz..*(..))" id="txPointcut"/>
<aop:advisor pointcut-ref="txPointcut" advice-ref="txAdvice"/>
</aop:config>
3. 트랜잭션 설정 동작 순서
① 클라이언트가 BoardServiceImpl 객체의 insertBoard() 메소드 호출
② insertBoard() 메소드의 비즈니스 로직이 수행됨
③ insertBoard() 메소드 수행 중 문제 발생시 txAdvice로 등록한 어드바이스가 동작함
④ txAdvice가 참조하는 txManager의 rollback() 메소드를 호출함 (문제없이 정상적으로 수행되었다면 commit() 메소드 호출)
7.5절 트랜잭션 설정 테스트
1. BoardServiceClient 설정
- BoardServiceClient에서 명시적으로 100번 글을 등록하도록 설정
public class BoardServiceClient {
public static void main(String[] args) {
...
//3. 글 등록 테스트
BoardVO vo = new BoardVO();
vo.setSeq(100); // seq를 100으로 지정
vo.setTitle("임시 제목");
vo.setWriter("홍길동");
vo.setContent("임시 내용.........");
boardService.insertBoard(vo);
...
}
}
2. BoardServiceImpl 클래스 설정 에서 명시적으로 100번 글을 등록하도록
- BoardServiceImpl 클래스의 insertBoard() 메소드에서 boardDAOSpring의 insertBoard() 메소드를 연속으로 두 번 호출
- 첫 번째 입력은 성공하자먼 BOARD 테이블에 SEQ 컬럼이 기본키로 지정되어있으므로 두 번째 입력에서 예외 발생
- 트랜잭션은 메소드 단위로 관리되므로 발생한 예외 때문에 insertBoard() 메소드의 작업 결과는 모두 롤백 처리됨
@Service("boardService")
public class BoardServiceImpl implements BoardService{
@Autowired
// BoardDAO boardDAO;
private BoardDAOSpring boardDAO;
public void insertBoard(BoardVO vo) {
boardDAO.insertBoard(vo); // 100번 글 등록 성공
boardDAO.insertBoard(vo); //예외 발생
}
}
3. BoardDAOSpring 수정
- BoardServiceImpl 에서 BoardDAOSpring 객체의 insertBoard() 메소드를 호출하기 때문에 BoardDAOSpring의 insertBoard() 메소드와 SQL 구문을 수정한다.
- SEQ 컬럼을 자동 증가하도록 작성했던 걸 수정해서 사용자가 입력한 100번 글이 INSERT 되도록 수정한다.
//SQL 관련 명령어
//private final String BOARD_INSERT = "INSERT INTO BOARD(SEQ, TITLE, WRITER, CONTENT) "
// + "VALUES((SELECT NVL(MAX(SEQ), 0) + 1 FROM BOARD), ?, ?, ?)";
private final String BOARD_INSERT = "INSERT INTO BOARD(SEQ, TITLE, WRITER, CONTENT) "
+ "VALUES(?, ?, ?, ?)";
//CRUD 기능의 메소드 구현
//글 등록
//getJdbcTemplate() : JdbcTemplate객체를 리턴. JdbcDaoSupport 클래스에서 상속받아 사용하는 메소드
public void insertBoard(BoardVO vo) {
System.out.println("====> JDBC로 insertBoard() 기능 처리");
jdbcTemplate.update(BOARD_INSERT, vo.getSeq(), vo.getTitle(), vo.getWriter(), vo.getContent());
}
4. 실행 결과
- SEQ는 primary key라서 100이라는 같은 데이터가 들어가게 되면 무결성 에러가 발생하게 됨
5. 실행 결과 - 롤백 확인
- insertBoard() 메소드 호출 부분을 주석처리하고 다시 글 목록 출력만 실행해보기
public class BoardServiceClient {
public static void main(String[] args) {
...
//3. 글 등록 테스트
BoardVO vo = new BoardVO();
vo.setSeq(100); // seq를 100으로 지정
vo.setTitle("임시 제목");
vo.setWriter("홍길동");
vo.setContent("임시 내용.........");
// boardService.insertBoard(vo);
...
}
}
- 글 목록에 100번 글이 입력되지 않은 것을 확인
- 아예 실행이 안되서 롤백이 된 상태로 100번째 글은 등록되지않음
댓글