[Spring] 스프링 트랜잭션 처리 (@Transactional)
2023. 10. 8.
반응형

 

 

이번 포스팅은 트랜잭션과 그를 처리하는 방법에 대해서 다뤄보겠습니다.

 

트랙잭션이란 무엇일까요?

두 개 이상의 쿼리를 한 작업으로 실행해야 할 때 사용하는 것입니다.

여러 쿼리를 논리적으로 하나의 작업으로 묶어주고, 한 트랜잭션으로 묶은 쿼리 중 하나라도 실패하면 전체 쿼리를 실패로 간주하고 실패 이전에 실행한 쿼리를 취소합니다.

 

예를 들어 회원 이메일의 유효성을 검증하는 로직이 있다고 가정하겠습니다.

로직을 처리하기 위해 인증이 된 상태에서 회원 테이블에 회원의 이메일을 update 하고 인증여부를 insert 하는 두 쿼리를 돌려야 합니다.

그런데 update 쿼리만 성공하고 insert 쿼리가 실패했다면 데이터의 원자성이 깨지게 됩니다.

 

이를 방지하기 위해 트랜잭션이라는 단위를 사용해서 update가 성공하더라도 insert가 실패하면 update도 다시 원상태로 되돌리게 됩니다. 이를 롤백이라 부르고, DB에 실제 결과를 반영하는 것을 커밋이라고 부릅니다.

트랜잭션을 시작하면 트랜잭션을 커밋하거나 롤백할 때까지 실행한 쿼리들이 하나의 작업 단위가 됩니다.

 

 

 

반응형

 

 

 

 

 

스프링에서 제공하는 @Transactional을 사용하면 트랜잭션 범위를 쉽게 지정할 수 있습니다.

 

ChangePasswordService.java

 

@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);
}

 

이처럼 사용하고 싶은 메소드에 어노테이션을 붙여만 주면 됩니다.

이렇게 하면 selectByEmail()과 changepassword()그리고 update()의 쿼리들이 하나의 트랜잭션으로 묶이게 됩니다.

 

@Transactional을 사용하기 위해서는 스프링에서 설정이 필요합니다.

 

 

 

 

반응형

 

 

 

 

AppCtx.java

 

@Configuration
@EnableTransactionManagement
public class AppCtx {
	@Bean
	public PlatformTransactionManager transactionManager() {
		DataSourceTransactionManager tm = new DataSourceTransactionManager();
		tm.setDataSource(dataSource());
		return tm;
	}
}

 

PlatformTransactionManager는 스프링이 제공하는 트랜잭션 매니저 인터페이스입니다.

스프링은 구현기술에 상관없이 동일한 방식으로 트랜잭션을 처리하기 위해 이 인터페이스를 사용합니다.

dataSource 프로퍼티를 이용해서 트랜잭션 연동에 사용할 DataSource를 지정해 줍니다.

 

@EnableTransactionManagement은 @Transactional 어노테이션이 붙은 메소드를 트랜잭션 범위에서 실행하는 기능을 활성화합니다. 등록된 PlatformTransactionManager 빈을 사용해 트랜잭션에 적용합니다.

 

 

 

 

반응형

 

 

 

 

트랜잭션이 제대로 작동하는지 확인을 하기 위해 로그 메시지를 찍어보겠습니다.

이를 위해 Logback를 사용해 보겠습니다.

그러기 위해서는 Logback 모듈을 의존성 추가해야 합니다.

 

Maven ver

 

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.9</version>
</dependency>

 

 

Gradle ver

 

 

implementation 'org.slf4j:slf4j-api:2.0.9'

 

 

그리고 src/main/resources 경로에 logback.xml 파일을 추가하도록 하겠습니다.

 

 

logback.xml 

 

<?xml version="1.0" encoding="UTF-8"?>

<configuration>
	<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
			<pattern>%d %5p %c{2} - %m%n</pattern>
		</encoder>
	</appender>
	<root level="INFO">
		<appender-ref ref="stdout" />
	</root>

	<logger name="org.springframework.jdbc" level="DEBUG" />
</configuration>

 

 

이를 통해 JDBC 관련 모듈에서 출력하는 로그 메시지를 상세하게 DEBUG 레벨로 보기 위한 설정입니다.

 

DEBUG o.s.j.d.DataSourceTransactionManager - Initiating transaction rollback
DEBUG o.s.j.d.DataSourceTransactionManager - Rolling back JDBC transaction on Connection

 

 

이 설정을 통해 Main 클래스를 실행하면 로그가 찍히는 것을 확인할 수 있습니다.

 

 

 

 

 

 

반응형

 

 

 

 

 

이 트랜잭션의 시작, 커밋, 롤백... 등 이런 것들은 누가 어떻게 처리하는 것일까요?

트랜잭션 역시 AOP, 즉 공통 기능 중 하나입니다. AOP는 프록시를 통해 구현되기 때문에 트랜잭션 역시 프록시를 통해서 이루어집니다.

스프링은 @Transactional 어노테이션을 사용하면, 이를 적용하기 위해 @EnableTransactionManagement를 사용하면 스프링은 @Transactional 어노테이션이 적용된 빈 객체를 찾아 프록시 객체를 생성합니다.

 

쉽게 설명하자면 비밀번호를 바꿀 때 DB 정보를 수정하려 할 때(트랜잭션을 수행하려 할 때),

스프링은 트랜잭션 기능을 적용한  프록시 객체를 생성합니다. 이때 getBean()을 실행하면 이 프록시 객체가 리턴됩니다.

이 프록시 객체는 @Transactional 어노테이션이 붙은 메소드를 호출하면 PlatformTransactionManager를 사용해서 트랜잭션을 시작합니다. 그 후 실제 객체 메소드를 호출하고 성공적으로 실행되면 트랜잭션을 커밋합니다.

 

@Transactional -> PlatformTransactionManager로 트랜잭션 시작(프록시 객체로) -> (프록시 객체가 아닌) 실제 메소드 호출 -> (성공이라면) 트랜잭션 커밋 

 

이때 메소드를 실행하고 호출하는 과정에서 RuntimeException(실패) 일 때 트랜잭션을 롤백합니다.

JdbcTemplate은 DB 연동 과정에 문제가 있으면 DataAccessException을 발생하는데 이것 역시 RuntineException을 상속받고 있기 때문에 이 경우에도 트랜잭션이 롤백하게 됩니다.

 

하지만 SQLException은 RuntimeException을 상속하고 있지 않으므로 이 경우에 트랜잭션을 롤백하고 싶다면 @Transactional의 rollbackFor 옵션을 사용해야 합니다.

 

ex) @Transactional(rollbackFor = SQLException.class)

 

@Transactional -> PlatformTransactionManager로 트랜잭션 시작(프록시 객체로) -> (프록시 객체가 아닌) 실제 메소드 호출 -> (RuntineException, 실패라면) 트랜잭션 롤백

 

 

 

 

반응형

 

 

 

 

그렇다면 @Transactional의 주요 속성에 대해 살펴보겠습니다.

 

속성 타입 설명
value String 트랜잭션을 관리할 때 사용할 PlatformTransactionManager 빈의 이름을 지정한다.
기본값은 " "이다.
propagation Propagation 트랜잭션의 전파 타입을 지정한다.
기본값은 Propagation.REQUIRED이다.
isolation Isolation 트랜잭션 격리 레벨을 지정한다.
기본값은 Isolation.DEFAULT이다.
timeout int 트랜잭션 제한 시간을 지정한다.
기본값은 -1로 이 경우 데이터베이스의 타임아웃 시간을 사용한다. 초단위로 지정한다.

 

@Transactional의 Propagation은 트랜잭션의 전파 타입을 지정하는 타입입니다.

그렇다면 트랜잭션의 전파는 무엇일까요?

 

메소드에 @Transactional이 붙으면 메소드가 실행될때 하나의 트랜잭션이 시작됩니다.

하지만 @Transactional이 붙은 A 메소드 내에서 역시나 @Transactional이 붙은 B 메소드를 실행시키면 어떻게 될까요?

Propagation.REQUIRED인 경우에는 A 메소드에서 생성한 하나의 트랜잭션에서 B 메소드까지 같이 포함해서 실행시킵니다.

 

하지만 Propagation.REQUIRES_NEW인 경우에는 항상 새로운 트랜잭션을 만들기 때문에 A의 트랜잭션, B의 트랜잭션 즉, A 메소드를 실행시키면 두 개의 트랜잭션이 실행되게 됩니다.

 

그럼 만약 B 메소드에 @Transactional이 붙지 않았다면 어떻게 될까요?

JdbcTemplate 클래스 덕분에 진행 중인 트랜잭션이 있다면 그 트랜잭션 범위 내에서 쿼리를 실행하게 됩니다.

 

 

 

 

반응형

 

 

 

 

@EnableTransactionManagement의 주요 속성에 대해서도 살펴 보겠습니다.

 

속성 설명
proxyTargetClass 프록시를 이용해서 프록시를 생성할지 여부를 지정한다.
기본값은 false로서 인터페이스를 이용해서 프록시를 생성한다.
order AOP 적용 순서를 지정한다.
기본값은 가장 낮은 우선순위에 해당하는 int의 최댓값이다.

 

 

처: 초보 웹 개발자를 위한 스프링 5 프로그래밍 입문

반응형
myoskin