DB 연동 중 자주 발생할 수 있는 Spring의 Exception
org.springframework.jdbc.CannotGetJdbcConnectionException
잘못된 DataSource 설정으로 인해 DB 연결 정보가 올바르지 않거나 DB가 실행되지 않아서 DB 서버에 연결할 수 없는 경우 해당 Exception이 발생한다.org.springframework.jdbc.BadSqlGrammerException
Spring에서 제공하는org.springframework.dao.DataAccessException을 상속한 Exception 클래스로, JdbcTemplate의 쿼리 실행 메서드에서 잘못된 sql 문법을 사용하여 쿼리를 수행했을 때 발생한다.
JdbcTemplate에서는 DB 연동을 위해 JDBC API를 사용할 때 잘못된 sql 문법 사용으로 SQLException이 발생한다면 이를 데이터 연결상의 문제가 있을 때 발생하는 DataAccessException으로 바꾼다. 그리고 JdbcTemplate에서는 이 Exception을 그 하위 클래스인 BadSqlGrammerException으로 다시 바꾼다.
여기에서 주목해야 할 점은 JdbcTemplate의 메서드에서 사용한 SQL 쿼리에서 오류가 발생할 경우 이 오류 그대로 SQLException이 발생하는 대신 Spring에서 제공하는 Exception인 DataAccessException으로 변환된다는 점이다. Spring이 이와 같은 Exception 변환을 수행하는 이유는 어떤 DB 연동 기술(JDBC, JPA, Hibernate 등)을 사용하는지에 관계없이 동일한 코드로 Exception을 처리할 수 있게 하는 것에 있다.
트랜잭션 관련 주요 용어 정리
- 트랜잭션(Transaction)
: 여러 개의 쿼리로 이루어진, 논리적으로 설정된 하나의 작업 단위로, 보통 Commit/Rollback 이전까지 실행했던 쿼리들이 1개의 트랜잭션으로 묶인다.- 롤백(Rollback)
: 1개의 트랜잭션으로 묶인 쿼리 중 1개라도 실패할 경우, 전체 쿼리를 실패로 간주하고 실패하기 전에 실행했던 모든 쿼리를 취소하는 작업- 커밋(Commit)
: 1개의 트랜잭션으로 묶인 모든 쿼리가 성공했을 때, 쿼리의 결과를 DB에 실제로 반영하는 작업
트랜잭션 관련 주요 어노테이션 정리
@Transactional
이 어노테이션이 사용된 Spring Bean 객체의 메서드는 트랜잭션 범위에서 실행할 수 있다.@EnableTransactionManagement
Spring 설정 클래스에 사용하여@Transactional가 붙은 메서드를 트랜잭션 범위에서 실행하는 기능을 활성화한다.
Spring에서의 트랜잭션 처리는 트랜잭션 범위에서 실행하고자 하는 메서드에게 @Transactional 어노테이션을 사용하고, @Configuration 클래스에서는 @EnableTransactionManagement 어노테이션과 PlatformTransactionManager 타입 Bean 메서드를 추가하는 방식으로 이루어진다.
PlatformTransactionManager 인터페이스
구현 기술에 관계없이 동일한 방식으로 트랜잭션을 처리하기 위해 Spring이 제공하는 인터페이스이다. JDBC에서는 이 인터페이스를 구현한 DataSourceTransactionManager 클래스의 setter로 DataSource Bean을 주입하는 방식을 통해 트랜잭션 연동에 사용할 DataSource를 지정한다.
실제 실행 코드
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
.. 코드 생략
@Configuration
@EnableTransactionManagement
public class AppCtx {
@Bean(destroyMethod = "close")
public DataSource dataSource() {
DataSource ds = new DataSource();
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost/spring5fs?characterEncoding=utf8");
... 코드 생략
return ds;
}
/* PlatformTransactionManager : 트랜잭션 처리 기능 제공 */
@Bean
public PlatformTransactionManager transactionManager() {
DataSourceTransactionManager tm = new DataSourceTransactionManager();
/* 트랜잭션 연동에 사용할 DataSource 지정, setter 주입 방식 사용 */
tm.setDataSource(dataSource());
return tm;
}
@Bean
public MemberDao memberDao() {
return new MemberDao(dataSource());
}
... 코드 생략
}
package main;
import config.AppCtx;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import spring.ChangePasswordService;
import spring.MemberNotFoundException;
import spring.WrongIdPasswordException;
public class MainForCPS {
public static void main(String[] args) {
/* 1. 트랜잭션 처리를 위한 ChangePasswordService 타입 프록시 객체 생성 */
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);
ChangePasswordService cps = ctx.getBean("changePwdSvc", ChangePasswordService.class);
/* 2. 대상 객체에서 쿼리 시작 시 트랜잭션 처리 수행
3. 쿼리 실행 완료 시 commit, 쿼리 실행 중 Exception 발생 시 rollback */
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();
}
}
실행 결과
Spring에서의 트랜잭션 처리 기능은 여러 Bean 객체에 공통으로 적용된다. 이는 곧 Spring에서 @Transactional 어노테이션을 사용한 트랜잭션 처리를 수행할 때 내부적으로 프록시를 통해 구현되는 AOP를 사용한다는 것을 의미한다.
실제로 @Transactional 어노테이션을 적용하기 위해 @EnableTransactionManagement를 사용하면 Spring은 @Transactional이 적용된 Bean 객체를 찾아 해당 Bean의 타입 또는 Bean이 상속한 인터페이스 타입의 프록시 객체를 생성한다.
이렇게 생성된 프록시 객체에서는 @Transactional이 붙은 메서드가 호출되면 PlatformTransactionManager를 사용하여 트랜잭션을 시작하고, 실제 객체에서의 쿼리 실행 결과에 따라 Commit/Rollback을 수행한다.
@Transactional을 처리하는 프록시 객체는 별도의 설정을 수행하지 않은 경우 RuntimeException과 그 하위 Exception이 발생했을 때 Rollback을 수행한다.
하지만 @Transactional의 다음과 같은 속성들을 활용하면 지정된 Exception에 대해서도 Rollback을 수행하거나, 수행하지 않을 수 있다.
@Transactional(rollbackFor = {Exception1.class, ...})
속성값으로 지정한 Exception(들)이 발생했을 때 Rollback을 수행한다. 이를 통해RuntimeException을 상속하지 않는 Exception에 대해서도 Rollback을 수행할 수 있다.@Transactional(noRollbackFor = {Exception1.class, ...})
속성값으로 지정한 Exception(들)이 발생하더라도 Rollback을 수행하지 않는다. 이를 통해RuntimeException과 그 하위 Exception이 발생하더라도 Rollback 대신 Commit을 수행할 수 있다.
@Transactional의 propagation 속성의 기본값은 Propagation.REQUIRED이다. 이 값은 메서드를 실행할 때 트랜잭션이 필요하다는 것을 의미한다. 만약 이미 진행 중인 트랜잭션이 존재한다면 기존의 트랜잭션을 그대로 사용하고, 진행 중인 트랜잭션이 없다면 새로운 트랜잭션을 생성한다.
만약 @Transactional 어노테이션이 적용된 메서드가 실행되는 도중에 @Transactional 어노테이션이 없는 메서드를 호출했다면 이는 기존에 진행 중인 트랜잭션이 존재하는 상황이 된다.
따라서@Transactional 어노테이션이 없는 메서드는 현재 진행 중인 트랜잭션 범위에서 쿼리를 실행하게 되므로, @Transactional이 붙은 메서드 내부에서 실행되는 모든 쿼리는 하나의 트랜잭션 범위에서 실행된다. 이는 곧 내부에서 새로 호출된 메서드로 기존에 진행 중인 트랜잭션이 '전파'되는 것으로 볼 수 있다.