트랜잭션(Transaction)이란 데이터의 일관성을 유지하기 위해서 데이터베이스의 상태를 변화시키는 작업의 단위를 의미한다. 즉, 트랜잭션을 사용할 어떤 로직이 있을 때, 해당 로직은 모두 수행되거나 모두 수행되지 않아야 한다. 실행 중간에 예외나 오류가 발생하면 트랜잭션을 실행하고 난 후의 작업들을 모두 rollback하여 되돌린다.
스프링에서 트랜잭션을 수행하는 방법은 여러가지가 있다. 크게는 직접 코드로 작성하는 방법과, 애너테이션을 사용하는 방법이 있다. 오늘은 두 방법에서 알아보자.
직접 코드로 작성하기
@Slf4j
@RequiredArgsConstructor
public class UserServic {
private final PlatformTransactionManager transactionManager;
private final UserRepository userRepository;
public void accountTransfer(String name) {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
findByName(name);
transactionManager.commit(status);
} catch (RuntimeException e) {
transactionManager.rollback(status);
}
}
public UserDto findByName(String name) {
return userRepository.findByName(name);
}
}
비즈니스 로직은 일반적으로 서비스 계층에서 실행되므로 트랜잭션은 서비스 계층에서 작동한다. 그래서 일단 서비스 계층을 살펴보기로 하자.
사실 위의 코드의 내용 상으로는 트랜잭션을 사용할 이유가 없다. 단순 이름으로 조회이기 때문이다. 그래도 트랜잭션이 필요한 로직이라 생각하고 트랜잭션을 적용해보자.
트랜잭션은 어떤 DB를 사용하던간에 기본적으로 동작 과정은 똑같다.
트랜잭션 시작 -> 비즈니스 로직 -> commit 또는 rollback
이 과정을 거치게 된다. 그런데 DB마다 트랜잭션을 사용하는 메서드가 다르다. 그렇기 때문에 이것 또한 추상화할 필요가 생겼고 이 이유 때문에 PlatformTransactionManager라는 인터페이스가 만들어지고 사용되게 되었다.
해당 인터페이스 변수에 MySQL이나 MariaDB 등 여러 DB의 트랙잭션 관리자 구현 객체를 할당해주어서 사용한다. 원래라면 직접 Bean으로 등록하여 해당 구현체를 주입받아서 사용하는데 스프링 부트를 이용하면 기본으로 application.properties의 설정 값을 확인해서 자동으로 Bean을 등록해주므로 직접 등록하지 않아도 작동한다. 그리고 해당 트랜잭션 관리자를 할당할 때 DataSource도 지정해 주어야 하는데 스프링 부트에서는 이 또한 HikariCP를 이용하여 HikariDataSource를 자동으로 주입해주므로 직접 등록하지 않아도 된다.
@Slf4j
@RequiredArgsConstructor
public class UserRepository {
public final DataSource dataSource;
public UserDto findByName(String name) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
String sql = "SELECT * FROM users WHERE name LIKE ?";
UserDto userDto;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name);
rs = pstmt.executeQuery();
if (rs.next()) {
userDto = UserDto.builder()
.name(rs.getString("name"))
.money(rs.getInt("money"))
.build();
}
else throw new RuntimeException();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
close(conn, pstmt, rs);
}
return userDto;
}
public void close(Connection conn, Statement stmt, ResultSet rs) {
DataSourceUtils.releaseConnection(conn, dataSource);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeResultSet(rs);
}
public Connection getConnection() {
return DataSourceUtils.getConnection(dataSource);
}
}
Repository에서는 트랜잭션 관리자에서 사용하는 Connection과 같은 객체를 얻기 위해 DataSourceUtils의 getConnection() 메서드를 이용하여 Connection을 획득한다. 사용 완료 후 자원을 반환할 때도 무작정 닫아주는 것이 아니다. 아직 트랜잭션 관련 처리가 남아 있기 때문에 releaseConnection() 메서드를 이용해서 안전하게 자원을 반환하도록 한다.
직접 코드를 작성한다면 이렇게 여러 귀찮은 과정을 거쳐야 한다. 애너테이션을 사용한다면 이러한 귀찮은 과정을 생략할 수 있다.
애너테이션 사용
@Slf4j
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional
public void accountTransfer(String name) throws RuntimeException {
findByName(name);
}
public UserDto findByName(String name) {
return userRepository.findByName(name);
}
}
위의 코드는 @Transactional 이라는 애너테이션을 사용해서 트랜잭션을 수행하는 서비스이다. 단순히 @Transactional만 붙여주었는데 트랜잭션이 실행되는 것이 뭔가 많이 생략된 것 같다. 사실은 위의 코드를 직접 작성해서 진행한 부분들을 스프링 부트에서 자동으로 수행해준다. 트랜잭션 프록시라는 것을 통해 반복되는 예외 처리 같은 부분들을 템플릿화 해서 내가 작성한 비즈니스 로직을 해당 템플릿에서 실행해주는 것이다. 그렇기 때문에 나는 비즈니스 로직만 작성해도 돼서 아주 간편하다. @Transactional 애너테이션은 클래스 단위로 붙이면 public으로 동작하는 메서드에 자동으로 작동되고, 메서드 단위로 붙이면 해당 메서드에만 작동한다.
프록시의 방법은 크게 두 가지가 있다. JDK Dynamic 프록시와 CGLIB 프록시 두 가지 방식이 있는데, JDK 동적 프록시는 인터페이스를 구현한 클래스에 대해 프록시를 생성하고, CGLIB 프록시는 클래스를 상속받아 프록시를 생성한다.
이렇게 유용한 방법이지만 주의해야할 점이 있다. AOP(Aspect-Oriented Programming)를 이용한 방법이기 때문에 프록시를 거치지 않으면 트랜잭션이 제대로 동작하지 않는다는 점이다. 직접 해당 메서드를 호출해서 사용하면 말짱 도로묵이다...
'공부 > Spring' 카테고리의 다른 글
| [Spring Web] Spring MVC 컨트롤러 요청/응답 가능한 여러가지 방법 (0) | 2024.03.21 |
|---|---|
| [Spring Web] http body 내용 가져오기 (MapStruct 안 되는 이유) (0) | 2024.03.16 |
| [Spring Web] 컨트롤러에서 String만 리턴해도 되는 이유 (0) | 2024.03.13 |
| [Spring] IoC, Bean Factory, Applicatin Context (0) | 2024.02.18 |
| [Spring] DataSource를 이용한 DB 연결 테스트 (0) | 2024.01.01 |