필자는 백엔드 개발을 공부하면서 데이터베이스 연결과 트랜잭션 관리의 중요성을 깊이 느끼게 되었다.
특히 Spring에서는 DataSource와 트랜잭션이 핵심적인 역할을 하는데, 이 개념들을 제대로 이해하지 못하면 안정적인 데이터 처리가 어렵다는 것을 알게 되었다.
이번 글에서는 DataSource와 트랜잭션의 개념을 정리해보고자 한다.
DataSource란?
데이터베이스 연결을 관리하는 것은 애플리케이션에서 매우 중요한 부분이다.
DataSource는 데이터베이스 연결을 위한 표준 인터페이스로, Connection Pool을 통해 데이터베이스 연결을 효율적으로 관리한다. (이전글 참고)
데이터베이스 연결은 비용이 많이 드는 작업이기 때문에, DataSource는 미리 연결을 생성해두고 필요할 때마다 이를 재사용하는 방식으로 동작한다.
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("user");
config.setPassword("password");
return new HikariDataSource(config);
}
}
위 코드는 HikariCP를 사용하여 DataSource를 구성하는 예시이다.
HikariCP는 가벼우면서도 빠른 커넥션 풀 라이브러리로, 스프링 부트에서 기본으로 사용된다.
JDBC와 DataSource의 관계
이전 글에서 설명한 것과 같이 JDBC(Java Database Connectivity)는 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API이다.
DataSource는 JDBC의 일부로, 데이터베이스 연결을 추상화하여 제공한다고 보면된다.
JDBC를 직접 사용할 때는 매번 Connection을 생성하고 닫아야 했지만, DataSource를 사용하면 이러한 작업을 자동화할 수 있다.
@Repository
public class UserRepository {
private final DataSource dataSource;
public UserRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
public User findById(Long id) {
String sql = "SELECT * FROM users WHERE id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, id);
ResultSet rs = ps.executeQuery();
// 결과 처리
}
}
}
이 예시는 DataSource를 사용하여 데이터베이스 연결을 획득하고 쿼리를 실행하는 방법을 보여준다.
try-with-resources를 사용하여 자원을 자동으로 해제합니다.
트랜잭션의 기본 개념
트랜잭션은 데이터베이스의 상태를 변화시키는 하나의 논리적 작업 단위를 말한다.
또한 트랜잭션은 ACID 속성을 보장해야 한다!
- Atomicity: 원자성
- Consistency: 일관성
- Isolation: 격리성
- Durability: 지속성
Spring에서의 트랜잭션 관리는 어떻게 진행될까?
스프링은 선언적 트랜잭션 관리를 지원한다.
@Transactional 어노테이션을 사용하면 메소드나 클래스 레벨에서 트랜잭션 경계를 쉽게 정의할 수 있다!
@Service
public class UserService {
private final UserRepository userRepository;
@Transactional
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
Account fromAccount = userRepository.findById(fromId);
Account toAccount = userRepository.findById(toId);
fromAccount.withdraw(amount);
toAccount.deposit(amount);
userRepository.save(fromAccount);
userRepository.save(toAccount);
}
}
이 예시는 계좌 이체 로직을 트랜잭션으로 관리하는 방법을 보여준다.
모든 연산이 성공적으로 완료되거나, 오류 발생 시 모든 변경사항이 롤백되는 것을 볼 수 있다.
트랜잭션의 격리 수준
트랜잭션 격리 수준은 동시에 실행되는 트랜잭션들이 서로에게 미치는 영향을 제어한다.
Spring에서는 다음과 같은 격리 수준을 지원한다.
- READ_UNCOMMITTED: 가장 낮은 격리 수준으로, 다른 트랜잭션이 커밋하지 않은 데이터를 읽을 수 있다.
- READ_COMMITTED: 커밋된 데이터만 읽을 수 있다.
- REPEATABLE_READ: 트랜잭션이 시작될 때 읽은 데이터를 트랜잭션이 종료될 때까지 일관되게 읽을 수 있다.
- SERIALIZABLE: 가장 높은 격리 수준으로, 완벽한 격리를 제공하지만 성능이 저하될 수 있다.
@Transactional(isolation = Isolation.READ_COMMITTED)
public void processOrder(Long orderId) {
// 주문 처리 로직
}
이 코드는 READ_COMMITTED 격리 수준을 사용하여 주문을 처리하는 예시이다.
트랜잭션의 전파 속성
트랜잭션 전파는 기존 트랜잭션이 있을 때 새로운 트랜잭션을 어떻게 처리할지 정의한다.
Spring에서 제공하는 주요 전파 속성으로는
REQUIRED, REQUIRES_NEW, SUPPORTS, MANDATORY, NEVER 등이 있다.
@Transactional(propagation = Propagation.REQUIRED)
public void mainOperation() {
subOperation(); // 같은 트랜잭션 내에서 실행
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void subOperation() {
// 새로운 트랜잭션에서 실행
}
이 예시는 서로 다른 전파 속성을 가진 메소드들의 상호작용을 보여준다.
REQUIRED는 기존 트랜잭션이 있으면 그것을 사용하고, 없으면 새로 생성한다.
반대로 REQUIRES_NEW는 항상 새로운 트랜잭션을 생성한다.
'Spring' 카테고리의 다른 글
[Spring] MyBatis 개념정리 🤩 (0) | 2025.01.20 |
---|---|
[Spring] JdbcTemplate으로 데이터베이스 접근하기! (0) | 2025.01.20 |
[Spring] Controller의 Return Type과 Parameter 가이드 👍 (1) | 2025.01.20 |
[Spring] Spring Singleton Pattern과 주요 어노테이션 정리 ✅ (0) | 2025.01.20 |
[Spring] POJO, IoC, DI, AOP, Filter 핵심 개념 완벽 정리 🧹 (2) | 2025.01.19 |