트랜잭션
트랜잭션은 두 개 이상의 쿼리를 한 작업으로 실행해야 할 때 사용하는 것이 트랜잭션(transaction)입니다.
트랜잭션은 여러 쿼리를 논리적으로 하나의 작업으로 묶어줍니다. 한 트랜잭션으로 묶인 쿼리 중 하나라도 실패한다면 전체 쿼리를 실패로 간주하고 실패 이전에 실행한 쿼리를 취소합니다.
롤백 (rollback) : 쿼리 실행 결과를 취소하고 DB를 기존 상태로 되돌리는 것
커밋 (commit) : 트랜잭션에 묶인 모든 쿼리가 성공해서 쿼리 결과를 DB에 실제로 반영하는 것
트랜잭션을 시작하면 트랜잭션을 커밋하거나 롤백할 때까지 실행한 쿼리들이 하나의 작업 단위가 됩니다.
JDBC는 Connection의 setAutoCommit(false)를 이용해서 트랜잭션을 시작하고 commit()과 rollback()을 이용해서 트랜잭션을 반영(커밋)하거나 취소(롤백)합니다.
Connection conn = null;
try {
conn = DriverManager.getConnection(jdbcUrl, user, pw);
conn.setAutoCommit(false); //트랜잭션 범위 시작
... 쿼리 실행
conn.commit(); // 트랜잭션 범위 종료 : 커밋
} catch (SQLException ex){
if(conn != null)
// 트랜잭션 범위 종료 : 롤백
try { conn.rollback(); } catch (SQLException e) {]
} finally {
if (conn != null)
try { conn.close(); } catch (SQLException e) {}
}
위와 같은 방식은 코드로 직접 트랜잭션 범위를 관리하기 때문에 개발자가 트랜잭션을 커밋하는 코드나 롤백하는 코드를 누락하기 쉽습니다. 게다가 구조적인 중복이 반복되는 문제도 있습니다.
스프링이 제공하는 트랜잭션 기능을 사용하면 중복이 없는 매우 간단한 코드로 트랜잭션 범위를 지정할 수 있습니다.
@Transactional을 이용한 트랜잭션 처리
스프링이 제공하는 @Transactional 을 사용하면 트랜잭션 범위를 매우 쉽게 지정할 수 있습니다.
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void changePassword(String email, String oldPwd, String newPwd){
Member member = memberDao.selectByEmail(email);
if(member == null)
throw new MemberNotFoundException();
member.changePassword(oldPwd, newPwd);
memberDao.update(member);
}
스프링은 @Transactional 어노테이션이 붙은 changePassword() 메서드를 동일한 트랜잭션 범위에서 실행합니다. 따라서 memberDao.selectByEmail()에서 실행하는 쿼리와 member.changePassword()에서 실행하는 쿼리는 한 트랜잭션에 묶입니다.
@Transactional 어노테이션이 제대로 동작하려면 두 가지 내용을 스프링 설정에 추가해야 합니다.
- 플랫폼 트랜잭션 매니저(PlatformTransactionManager) 빈 설정
- @Transactional 어노테이션 활성화 지정
설정 예
@Configuration
@EnableTransactionManagement
public class AppCtx {
@Bean(destroyMethod = "close")
public DataSource dataSource() {
DataSource ds = new DataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost/spring5fs?characterEncoding=utf8&useSSL=false");
ds.setUsername("spring5");
ds.setPassword("spring5");
ds.setInitialSize(2);
ds.setMaxActive(10);
ds.setTestWhileIdle(true);
ds.setMinEvictableIdleTimeMillis(60000 * 3);
ds.setTimeBetweenEvictionRunsMillis(10 * 1000);
return ds;
}
@Bean
public PlatformTransactionManager transactionManager() {
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource());
return tm;
}
}
PlatformTransactionManager는 스프링이 제공하는 트랜잭션 매니저 인터페이스입니다. 스프링은 구현기술에 상관없이 동일한 방식으로 트랜잭션을 처리하기 위해 이 인터페이스를 사용합니다. JDBC는 DataSourceTransactionManager 클래스를 PlatformTransactionManager로 사용합니다. 위 설정에 보듯, dataSource 프로퍼티를 이용해 트랜잭션 연동에 사용할 DataSource를 지정합니다.
@EnableTransactionManagement 애노테이션은 @Transactional 애노테이션이 붙은 메서드를 트랜잭션 범위에서 실행하는 기능을 활성화합니다. 등록된 PlatformTransactionManager 빈을 사용해서 트랜잭션을 적용합니다.
트랜잭션 처리를 위한 설정을 완료하면 트랜잭션 범위에서 실행하고 싶은 스프링 빈 객체 메서드에 @Transactional 애노테이션을 붙이면 됩니다. 예를 들어, 비밀번호를 변경하는 chagnePassword 메서드에 붙이면 됩니다.
package spring;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
public class ChangePasswordService {
@Autowired
private MemberDao memberDao;
@Transactional
public void changePassword(String email, String oldPwd, String newPwd) {
Member member = memberDao.selectByEmail(email);
if(member == null) {
throw new MemberNotFoundException();
}
member.changePassword(oldPwd, newPwd);
memberDao.update(member);
}
public void setMemberDao(MemberDao memberDao) {
this.memberDao = memberDao;
}
}
실제로 트랜잭션 범위에서 실행되는지 확인을 해봅시다. 그러기 위해 ChangePasswordService 클래스를 빈으로 추가해야 합니다.
@Bean
public ChangePasswordService changePwdSvc() {
System.out.println("changePwdSvc() call...");
ChangePasswordService pwdSvc = new ChangePasswordService();
pwdSvc.setMemberDao(memberDao());
return pwdSvc;
}
메인 클래스
package main;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import config.AppCtx;
import spring.ChangePasswordService;
import spring.MemberNotFoundException;
import spring.WrongIdPasswordException;
public class MainForCPS {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);
ChangePasswordService cps = ctx.getBean("changePwdSvc", ChangePasswordService.class);
try {
cps.changePassword("madvirus@madvirus.net", "1234", "1111");
System.out.println("암호 변경");
} catch (MemberNotFoundException e) {
System.out.println("회원 데이터 미존재");
} catch (WrongIdPasswordException e) {
System.out.println("암호가 올바르지 않음.");
}
ctx.close();
}
}
메인 클래스의 메인 메서드를 실행하면 실제로 트랜잭션이 시작되고 커밋되는지 알 수 없습니다. 이를 확인하는 방법은 스프링이 출력하는 로그 메시지를 보는 것입니다. 트랜잭션과 관련 로그 메시지를 추가로 출력하기 위해 Logback를 사용해 봅시다.
스프링 5 버전은 자체 로깅 모듈인 spring-jcl을 사용합니다. 이 로깅 모듈은 직접 로그를 남기지 않고 다른 로깅 모듈을 사용해서 로그를 남깁니다. 예를 들어 클래스 패스에 Logback이 존재하면 Logback을 이용해서 로그를 남기고 Log4j2가 존재하면 Log4j2를 이용해서 로그를 남깁니다. 따라서 사용할 로깅 모듈만 클래스 패스에 추가해 주면 됩니다.
pom.xml이나 build.gradle 파일에 Logback 모듈을 추가해 봅시다. 메이븐의 경우 아래와 같이 추가해 줍니다.
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
클래스 패스에 Logback 설정 파일을 위치시켜야 하므로 프로젝트 폴더 하위에 src/main/resources 폴더도 추가해야 합니다.
그다음 이클립스에서 인식하도록 프로젝트를 업데이트해줍시다. [maven]-[update project]
메인 클래스를 실행하면 다음과 같은 로그를 볼 수 있습니다.
2024-02-28 22:15:48,925 DEBUG o.s.j.d.DataSourceTransactionManager - Acquired Connection [ProxyConnection [PooledConnection [com.mysql.jdbc.JDBC4 Connection@738 dc9 b]]] for JDBC transaction
2024-02-28 22:15:48,927 DEBUG o.s.j.d.DataSourceTransactionManager - Switching JDBC Connection [ProxyConnection[PooledConnection[com.mysql.jdbc.JDBC4Connection@738dc9b]]] to manual commit
2024-02-28 22:15:48,959 DEBUG o.s.j.c.JdbcTemplate - Executing prepared SQL query
2024-02-28 22:15:48,959 DEBUG o.s.j.c.JdbcTemplate - Executing prepared SQL statement [select * from MEMBER where EMAIL = ?]
2024-02-28 22:15:49,018 DEBUG o.s.j.c.JdbcTemplate - Executing prepared SQL update
2024-02-28 22:15:49,019 DEBUG o.s.j.c.JdbcTemplate - Executing prepared SQL statement [update MEMBER set NAME = ?, PASSWORD = ? where EMAIL = ?]
2024-02-28 22:15:49,020 DEBUG o.s.j.c.JdbcTemplate - SQL update affected 1 rows
2024-02-28 22:15:49,020 DEBUG o.s.j.d.DataSourceTransactionManager - Initiating transaction commit
2024-02-28 22:15:49,020 DEBUG o.s.j.d.DataSourceTransactionManager - Committing JDBC transaction on Connection [ProxyConnection[PooledConnection[com.mysql.jdbc.JDBC4Connection@738dc9b]]]
2024-02-28 22:15:49,023 DEBUG o.s.j.d.DataSourceTransactionManager - Releasing JDBC Connection [ProxyConnection[PooledConnection[com.mysql.jdbc.JDBC4Connection@738dc9b]]] after transaction
2024-02-28 22:15:49,023 DEBUG o.s.j.d.DataSourceUtils - Returning JDBC Connection to DataSource
암호 변경
2024-02-28 22:15:49,023 INFO o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@3c756e4d: startup date [Wed Feb 28 22:15:47 KST 2024]; root of context hierarchy
위 로그와 같이 트랜잭션을 시작하고 커밋한다는 로그를 확인할 수 있습니다.
그렇다면 DB에 있는 암호와 맞지 않는다면 WrongIdPasswordException 이 발생해야 할 텐데, 진짜 발생하는지, 비밀번호를 일부러 1234 대신 3333으로 입력해 봅시다.
cps.changePassword("madvirus@madvirus.net", "3333", "1111");
2024-02-28 22:19:16,193 DEBUG o.s.j.d.DataSourceTransactionManager - Acquired Connection [ProxyConnection[PooledConnection[com.mysql.jdbc.JDBC4Connection@66beb02d]]] for JDBC transaction
2024-02-28 22:19:16,196 DEBUG o.s.j.d.DataSourceTransactionManager - Switching JDBC Connection [ProxyConnection[PooledConnection[com.mysql.jdbc.JDBC4Connection@66beb02d]]] to manual commit
2024-02-28 22:19:16,209 DEBUG o.s.j.c.JdbcTemplate - Executing prepared SQL query
2024-02-28 22:19:16,209 DEBUG o.s.j.c.JdbcTemplate - Executing prepared SQL statement [select * from MEMBER where EMAIL = ?]
2024-02-28 22:19:16,235 DEBUG o.s.j.d.DataSourceTransactionManager - Initiating transaction rollback
2024-02-28 22:19:16,235 DEBUG o.s.j.d.DataSourceTransactionManager - Rolling back JDBC transaction on Connection [ProxyConnection[PooledConnection[com.mysql.jdbc.JDBC4Connection@66beb02d]]]
2024-02-28 22:19:16,236 DEBUG o.s.j.d.DataSourceTransactionManager - Releasing JDBC Connection [ProxyConnection[PooledConnection[com.mysql.jdbc.JDBC4Connection@66beb02d]]] after transaction
2024-02-28 22:19:16,236 DEBUG o.s.j.d.DataSourceUtils - Returning JDBC Connection to DataSource
암호가 올바르지 않음.
2024-02-28 22:19:16,236 INFO o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@1d131e1b: startup date [Wed Feb 28 22:19:15 KST 2024]; root of context hierarchy
WrongIdPasswordException이 발생하여 "암호가 올바르지 않음"이라는 메시지를 출력하였고, 트랜잭션을 롤백했다는 로그 메시지가 찍혔습니다.
다음 시간에는 이런 트랜잭션 동작을 누가 시작하고, 커밋하고, 롤백하는 것인지에 대해 알아보겠습니다.
감사합니다.
'Spring' 카테고리의 다른 글
[Spring] @Transactional 적용 메서드 롤백 처리 (0) | 2024.03.04 |
---|---|
[Spring] @Transacational과 프록시 (1) | 2024.03.01 |
[Spring] Spring DB - 익셉션 변환처리 (0) | 2024.02.27 |
[Spring] Spring DB 연동 과정에서 발생하는 Exception (0) | 2024.02.26 |
[Spring] Spring DB - 쿼리 실행 테스트 (1) | 2024.02.25 |