하나의 요청을 처리하기 위해 하나 이상의 메소드를 호출해야 하는 경우가 있습니다. 이 때 DB에서 하나 이상의 트랜잭션이 발생합니다. 예를 들어 사용자 비밀번호를 변경하는 메소드가 있습니다.
@Override
@Transactional
public void changeUserPwd(UserVO user) {
UserVO newUser = userMapper.getUserById(user.getUserId());
String encodedPwd = PasswordEncryptor.encrypt(user.getUserPwd());
UserVO changeUser = UserVO.builder()
.accountId(newUser.getAccountId())
.userPwd(encodedPwd)
.build();
userMapper.changeUserPwd(changeUser);
}
사용자가 비밀번호 변경 요청을 하면 changeUserPwd() 메소드가 실행되고, 메소드 안의 getUserById(), changeUserPwd() 메소드가 처리됩니다. 즉 하나의 요청에 하나 이상의 메소드가 호출됩니다.
만약 두 메소드 중 하나라도 잘못된다면 어떤 일이 발생할까요? DB 제약조건 위반에 따른 에러가 발생해서 사용자에게 요청 처리 실패 메시지를 전달하거나, 아니면 두 메소드 중 하나만 실행될 수도 있습니다. 전자라면 에러 메시지에 따른 디버깅 작업을 한다고 칩시다. 후자의 경우는? 어쨌든 요청이 처리되어 사용자는 요청 성공 메시지를 받을 것입니다. 하지만 아무것도 변한 것이 없습니다.
이 문제를 사전에 방지하기 위해 하나의 요청을 하나의 트랜잭션으로 바라보고 처리하는 방식을 취합니다.
스프링 환경에서의 트랜잭션 처리에 대한 이해
먼저 트랜잭션 처리 기능을 사용하기 위해 의존성 추가 작업을 합니다.
pom.xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${org.springframework-version}</version>
</dependency>
PlatformTransactionManager 인터페이스에서 제공하는 commit, getTransaction, rollback 메소드를 구현하는 방식으로 트랜잭션이 동작합니다. 즉 이 인터페이스가 직접적으로 트랜잭션 처리에 관여한다는 의미입니다.
PlatformTransactionManager 구현체 등록은 어플리케이션이 동작하는 환경에 따라 다릅니다.(JDBC, JTA, Hibernate 등) 스프링 공식 문서에서는 JDBC 환경에서의 구현체 등록 예시를 보여줍니다.
먼저 아래와 같이 JDBC dataSource 빈을 정의합니다.
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
그리고 PlatformTransactionManager 빈 정의에 dataSource 정의를 참조로 추가합니다.
DataSourceTransactionManager 클래스는 PlatformTransactionManager 인터페이스를 구현하는 많은 클래스 중의 하나입니다. 클래스 이름에서 알 수 있듯이 단일 JDBC DataSource 연결에 대한 트랜잭션 처리를 구현합니다. dataSource 빈은 DB 연결 정보를 담고 있으며, DataSource 객체를 생성하는데 사용됩니다.
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
제가 구현한 어플리케이션에는 다음과 같이 설정했습니다.
<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
<property name="driverClassName" value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy"></property>
<property name="jdbcUrl" value="jdbc:log4jdbc:mysql://joonggo-rds-mysql.chagb5t1righ.ap-northeast-2.rds.amazonaws.com:3306/joonggo_market?characterEncoding=UTF-8&serverTimezone=Asia/Seoul"/>
<property name="username" value="joonggo_market"/>
<property name="password" value="joonggo1234"/>
</bean>
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<constructor-arg ref="hikariConfig" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
</bean>
<mybatis-spring:scan base-package="com.board.mapper" />
<context:component-scan base-package="com.board.service" />
<context:component-scan base-package="com.board.exception" />
<tx:annotation-driven/>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
맨 윗부분과 아랫부분만 확인합니다. 앞서 언급한 설정 내용 이외에 <tx:annotation-driven/> 이 한 줄 추가된 것을 확인할 수 있습니다. 이 의미는, 명시적 어노테이션을 통한 트랜잭션 처리를 하겠다는 의미입니다. Spring에서 DB 트랜잭션 처리하는 방법은 두 가지가 있는데, <tx:advice> 선언을 통한 선언적 트랜잭션 처리와 방금 언급한 명시적 어노테이션 방식이 있습니다. 선언적 트랜잭션 처리 방법은 공식 문서에서 확인 가능합니다.
저는 어노테이션을 통한 명시적 트랜잭션 처리 방식을 선택했습니다. 그래서 <tx:annotation-driven/>를 설정에 추가했습니다. 이 설정을 하면 Spring 컨텍스트에게 "나는 어노테이션 방식으로 트랜잭션을 관리한다!" 라고 말하는 것과 같습니다. 그리고 트랜잭션을 관리하는 트랜잭션 매니저에게 트랜잭션 설정에 관한 빈을 제공해야 합니다. 트랜잭션 매니저는 기본적으로 transactionManager 라는 이름을 가진 빈을 찾아서 트랜잭션 처리에 사용합니다. 저는 JDBC를 사용하고 있기 때문에 org.springframework.jdbc.datasource.DataSourceTransactionManager 클래스를 타입으로 가지는 transactionManager라는 이름의 빈을 등록했습니다.
@Transactional 어노테이션은 클래스, 메소드 두 레벨에서 사용 가능합니다. 클래스에 추가하면 클래스 내의 모든 메소드에 트랜잭션 기능이 적용됩니다. 반면 메소드에 추가하면 해당 메소드에서만 트랜잭션 기능이 작동합니다.
아래 코드는 판매중인 상품을 찜하는 기능을 처리하는 메소드 입니다.
메소드 내부에는 해당 상품의 좋아요 개수를 +1 하는 메소드, 그리고 찜한 상품에 대한 정보를 관리하는 테이블에 관련 정보를 삽입하는 두 개의 메소드가 있습니다. 이것을 하나의 트랜잭션으로 바라보고 둘 중 하나라도 에러가 발생하면 롤백처리를 해서 데이터 무결성을 확보해야 합니다. 앞서 어노테이션 방식의 트랜잭션 처리 설정을 했기 때문에 @Transactional 어노테이션만 붙이면 됩니다. 만약 오류가 발생하면 로그에 롤백 처리가 되었다고 출력될 것입니다.
@Override
@Transactional
public void likeProuct(String prdtId) {
String currentUserId = LoginUserUtils.getUserId();
String accountId = userMapper.getAccountId(currentUserId);
ProductVO addLikeProduct = ProductVO.builder()
.prdtId(prdtId)
.accountId(accountId)
.build();
productMapper.likeProuct(prdtId);
productMapper.addLikeProduct(addLikeProduct);
}
참고
'Programming > 개인 프로젝트' 카테고리의 다른 글
[중고거래장터 - Study&Refactoring] Spring Security를 활용한 로그인 처리 (0) | 2022.04.10 |
---|---|
[중고거래장터 - Study&Refactoring] locale 변경 (0) | 2022.04.07 |
[중고거래장터] Server Architecture (0) | 2022.04.03 |
[중고거래장터 - Study&Refactoring] Spring Rest Docs 적용을 통한 API 문서 작성 (0) | 2022.04.01 |
[중고거래장터 - Study&Refactoring] 캐싱 기능 적용(Spring MVC + Redis + Jedis) Java 클래스 방식 (0) | 2022.03.31 |