데이터베이스의 격리 수준(Isolation Level)은 트랜잭션이 동시에 실행될 때 서로의 작업의 미치는 영향을 제어하는 중요한 개념. 격리 수준은 데이터의 일관성과 무결성을 유지하면서도 시스템의 성능을 최적화하는 데 중요한 역할. SQL 표준에서는 네 가지 주요 격리 수준을 정의하고 있으며, 각가은 특정한 동시성 문제를 방지하거나 허용
MySQL(InnoDB) : 기본적으로 Repeatable Read 격리 수준을 사용하며 MVCC(Multi-Version Concurrency Control)을 통해 팬텀 리드를 방지
PostgreSQL : 기본적으로 Read Committed를 사용하지만, Serializable 수준에서 실제로 직렬화 가능한 트랜잭션을 구현하기 위해 Serializable Snapshot Isolation(SSI)를 사용.
SQL Server : 기본적으로 Read Committed를 사용하며 필요에 따라 Snapshot Isolation을 제공하여 비반복 가능 리드를 방지할 수 있음.
신입 또는 취업 준비 중인 Java, Spring 백엔드 개발자로서 데이터베이스의 격리 수준(Isolation Level)을 이해하는 것은 중요합니다. 실습을 통해 격리 수준의 동작을 직접 확인하면 이론적으로 이해한 내용을 확실히 익힐 수 있습니다. 아래에 격리 수준 실습 방법과 관련된 예제 코드 및 주요 학습 포인트를 정리했습니다.
CREATE TABLE accounts (
id INT AUTO_INCREMENT PRIMARY KEY,
balance INT NOT NULL
);
INSERT INTO accounts (balance) VALUES (100), (200), (300);
Spring Boot 프로젝트에서 DataSource를 설정하고 트랜잭션 관리 기능을 활성화합니다.
@SpringBootApplication
@EnableTransactionManagement
public class IsolationLevelDemoApplication {
public static void main(String[] args) {
SpringApplication.run(IsolationLevelDemoApplication.class, args);
}
}
JDBC를 사용하여 트랜잭션을 제어하는 예제입니다.
@Repository
public class AccountRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
public int getBalance(int accountId) {
String query = "SELECT balance FROM accounts WHERE id = ?";
return jdbcTemplate.queryForObject(query, new Object[]{accountId}, Integer.class);
}
public void updateBalance(int accountId, int amount) {
String query = "UPDATE accounts SET balance = balance + ? WHERE id = ?";
jdbcTemplate.update(query, amount, accountId);
}
}
Spring에서는 @Transactional 어노테이션을 사용하여 트랜잭션의 격리 수준을 설정할 수 있습니다.
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void readUncommittedExample(int accountId) {
int balance = accountRepository.getBalance(accountId);
System.out.println("Read Uncommitted Balance: " + balance);
}
@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommittedExample(int accountId) {
int balance = accountRepository.getBalance(accountId);
System.out.println("Read Committed Balance: " + balance);
}
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void repeatableReadExample(int accountId) {
int balance = accountRepository.getBalance(accountId);
System.out.println("Repeatable Read Balance: " + balance);
}
@Transactional(isolation = Isolation.SERIALIZABLE)
public void serializableExample(int accountId) {
int balance = accountRepository.getBalance(accountId);
System.out.println("Serializable Balance: " + balance);
}
}
Spring Boot의 테스트를 통해 격리 수준의 동작을 실습합니다.
@SpringBootTest
public class IsolationLevelTest {
@Autowired
private AccountService accountService;
@Autowired
private AccountRepository accountRepository;
@Test
@Transactional
public void testDirtyRead() {
// Thread 1: Start a transaction and update the balance
new Thread(() -> {
accountService.readUncommittedExample(1);
}).start();
// Thread 2: Read the uncommitted value
accountService.readUncommittedExample(1);
}
}
READ_UNCOMMITTED에서 트랜잭션이 커밋되지 않은 데이터를 읽는 상황을 재현.READ_COMMITTED에서 같은 트랜잭션 내에서 동일한 데이터 읽기 결과가 변경되는 상황을 확인.REPEATABLE_READ와 SERIALIZABLE에서 삽입/삭제된 데이터가 영향을 미치는지 확인.실습을 통해 이론만 학습했을 때 놓칠 수 있는 실제 동작을 확인하고, 실무에서 자주 겪을 수 있는 문제를 효과적으로 대비할 수 있습니다.