1. Transaction이란?
데이터 무결성 보장을 위해서 격리된 operation을 지원하는 기술.
ACID라는 속성을 만족해야 하기에 기술의 근본 자체는 pessimistic하지만,
현대의 RDBMS에서는 MVCC을 기반으로 고립성 수준에 따라 optimistic하면서도 pessimistic한 동시성 제어를 구현한다.
2. JDBC와 DB의 Connection
트랜잭션을 얘기하기 전에 우선 먼저 Java App에서 DB로의 연결을 어떻게 하는지 살펴봐야 한다.
JPA를 쓰던, Spring data JPA를 쓰던 그 어떤 것을 쓰던지 DB와의 연결이 없다면
이러한 트랜잭션의 논의가 불가능하기 때문이다.
공부를 하다보니 이 연결이 트랜잭션과도 관련이 있기 때문에 자세히 얘기해보고자 한다.
Java Application과 DB의 연결은 두 가지 방식으로 진행한다.
물리적 접속 방식과 논리적 접속 방식 두 가지 이며, 이 두 가지 방식 모두 JDBC driver를 통해 이루어진다.
- JDBC driver는 사용하게 될 DBMS별로 다르게 구현된다.
- 그렇기에 build.gradle에는 사용하게 될 RDBMS의 JDBC Driver의 의존성 설치를 진행해주어야 한다.
- ex) postgresQL의 경우, runtimeOnly 'org.postgresql:postgresql' 를 추가해주어야 한다.
- 이를 설치해야 이후 사용할 DriverManager와 DataSource의 구현체를 사용할 수 있다.
- 이후 접속을 위해서 setting을 해주어야 하는데,
이때 필요한 것은 해당 DB의 url, DB에서 사용할 계정의 username, password 이다. - 이를 application.properties 혹은 application.yml에 환경변수로 등록해두고
실제 사용은 spring bean의 @Value 어노테이션을 통해 해당 필드에 등록이 가능하다.
- DB와의 연결은 공통적으로 connection ➡️ Statement ➡️ ResultSet의 순서로 이루어진다. (JDBC API 기준)
- 여기서 connection의 방식, 트랜잭션의 방식에 따라 변화가 있고
추상화 정도, 고수준화에 따라 생략되는 게 있을 수 있다. 그렇지만 기본적인 저 틀을 벗어나지는 않는다. - JDBC driver의 모든 SQL 처리를 위한 과정에는 반드시 SQLException을 처리하도록 되어있다.
SQLException은 대표적인 checked exception이기에, 이를 처리하지 않을 경우 컴파일 에러가 발생한다.
3. JDBC를 통한 접속 방식
- 물리적 접속
- DriverManager를 통해 진행된다.
- DB 접속이 필요할 때 마다 이를 통해 Connection을 생성하고, 쿼리를 수행한 후 종료한다.
- 매 연결마다 TCP/IP 소켓 연결을 통해 위 과정을 반복하는데, 연결 관리비용이 크고, 성능상 문제가 있을 수 있다.
public class UserJdbcApiDao {
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
public User findById(int userId) throws SQLException {
Connection connection = null; // 1
Statement statement = null; // 2
ResultSet resultSet = null; // 3
try {
connection = DriverManager.getConnection( // 1
url, username, password
);
...
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "유저 정보가 존재하지 않습니다 - id : " + userId);
} catch (SQLException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "자원에 대한 접근에 문제가 있습니다.");
} finally {
// 자원반납
if (resultSet != null) resultSet.close(); // 1
if (statement != null) statement.close(); // 2
if (connection != null) connection.close(); // 3
}
}
}
- 논리적 접속
- DataSource를 통해 진행된다.
- DB에 미리 접속해서 이를 통한 Connection Pool을 생성한다.
- DB 접속이 필요할 때 마다 DataSource의 Connection Pool 내 connection 중 한 개를 가져와
이를 통해 쿼리를 수행하고, 이를 connection pool에 반환한다.
- 참고로, DataSource 구현체에 따라 자체 connection pool 이 없는 경우도 있기에 driver를 잘 확인해야한다.
- Spring Boot 2+ 부터 DataSource 표준은 HikariCP이다.
- connection의 timeout과 connection에 유지할 최소의 커넥션 수, 최대 커넥션 수를 제한할 수 있다.
- 단, 성능을 고려할 때 tomcat의 Thread pool의 갯수보다는 적게 설정해야 한다.
connection이 남게될 경우, 리소스 낭비
- 단, 성능을 고려할 때 tomcat의 Thread pool의 갯수보다는 적게 설정해야 한다.
논리적 접속의 경우, 아래와 같이 HikariDataSource를 사용할 것을 명시한 후
여기에서 getConnection()을 통해 connection pool에서 connection을 받아 진행한다.
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setUsername(username);
config.setPassword(password);
config.setDriverClassName(driver);
HikariDataSource hikariDataSource = new HikariDataSource(config);
Connection connection = hikariDataSource.getConnection();
4. JDBC API를 통한 DB 조작의 추상화
위에서 사용한 Connection, Statement와 ResultSet 모두 JDBC API를 사용한 추상화 방식의 예이다.
물리적 접속을 할 것인지, 논리적 접속을 할 것인지를 고려했다면 (Connection)
접속후에 어떻게 쿼리를 작성하고, 이를 수행해서 (Statement)
어디에 데이터를 받을 것인지 (ResultSet) 를 추상화한 것이다.
이를 통해서 쿼리를 수행하고, 쿼리를 통해 얻어온 데이터를 서비스로 반환하는 로직은 아래와 같이 구현된다.
connection = dataSource.getConnection(); // 1
statement = connection.createStatement(); // 2
resultSet = statement.executeQuery( // 3
"SELECT * FROM \"user\""
);
List<User> results = new ArrayList();
while (resultSet.next()) {
results.add(
new User(
resultSet.getInt("id"),
resultSet.getString("name"),
resultSet.getInt("age"),
resultSet.getString("job"),
resultSet.getString("specialty"),
resultSet.getTimestamp("created_at")
.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime()
)
);
}
return results;
5. JDBC API를 통한 Transaction과 auto commit
위의 코드를 보면 어디에도 transaction에 대한 직접적인 정의나 사용하겠다는 명시가 없다.
그러나 위와 같이 실행해도 변경사항이 알아서 커밋되고, DB의 출력결과를 보면 제대로 입력이 됐음을 확인할 수 있다.
그게 당연한 것이 Jdbc API의 Connection 인터페이스는 트랜잭션의 정의를 하지 않고도 알아서 처리할 수 있도록
트랜잭션 처리를 위한 Auto-Commit 모드를 제공하고 있다.
그렇기에 굳이 실행하지 않아도, 하나의 쿼리 당 하나의 트랜잭션이 계속해서 commit이 되고 있다.
이러한 Auto-Commit은 Connection단위로 진행이 된다.
해당하는 connection을 열어주고, 이 connection에 대해 connection.setAutoCommit(false)로 해준다면,
여기서 connection.commit()을 해줘야만 변경 사항에 대해 업데이트 하는 커밋이 진행된다.
우선, 아래와 같은 코드에서는 auto commit의 설정이 따로 되어있지 않기에,
쿼리수행에 대한 커밋이 계속해서 발생해 총 3번의 트랜잭션에 3번의 커밋이 일어난다.
public void save(String message, Long userId) throws SQLException {
Connection connection = null;
PreparedStatement statement = null;
try {
connection = dataSource.getConnection();
statement = connection.prepareStatement(
"INSERT INTO \"message\" (user_id, message, created_at) VALUES (?, ?, now())");
statement.setLong(1, userId);
statement.setString(2, message);
int updatedRowCount = statement.executeUpdate();
if (updatedRowCount != 1) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
"서버에 문제가 있습니다.");
}
statement = connection.prepareStatement(
"INSERT INTO \"message\" (user_id, message, created_at) VALUES (?, ?, now())");
statement.setLong(1, userId);
statement.setString(2, message + "2트");
updatedRowCount = statement.executeUpdate();
if (updatedRowCount != 1) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
"서버에 문제가 있습니다.");
}
statement = connection.prepareStatement(
"INSERT INTO \"message\" (user_id, message, created_at) VALUES (?, ?, now())");
statement.setLong(1, userId);
statement.setString(2, message + "3트");
updatedRowCount = statement.executeUpdate();
if (updatedRowCount != 1) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
"서버에 문제가 있습니다.");
}
} catch (SQLException e) {
log.warn(e.getMessage(), e);
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "서버에 문제가 있습니다.");
} finally {
if (statement != null) {
statement.close();
}
if (connection != null) {
connection.close();
}
}
}
그리고 각각의 트랜잭션이 rollback될 경우,
전체 connection에 대해 rollback이 적용되도록 (전체 메서드의 트랜잭션이 rollback되도록)
단일한 commit으로 트랜잭션을 동기화 하는 것을 TransactionSynchronization이라고 한다.
앞으로의 transaction과 관련된 얘기는 트랜잭션, 그리고 트랜잭션 동기화의 방식,
그리고 이에 대한 추상화 방식과 고수준 인터페이스화에 대한 얘기가 될 것이다.
이에 대한 예시로 TransactionSynchronizationManager와 PlatformTransactionManager가 있다.
https://www.baeldung.com/java-jdbc-auto-commit
'연재작 > Database' 카테고리의 다른 글
Persistence Context의 Entity 상태 관리, Life-Cycle (1) | 2024.11.28 |
---|---|
Transaction Deep Dive (5) (0) | 2024.11.28 |
Transaction Deep Dive (4) (0) | 2024.11.26 |
Transaction Deep Dive (3) (0) | 2024.11.23 |
Transaction Deep Dive (1) (0) | 2024.11.20 |