Spring Boot 인터페이스 방어: 동시성 문제를 방지하는 실용적인 예
이 기사에서는 Spring Boot에서 인터페이스 동시성 문제를 피하는 방법을 살펴보겠습니다. 높은 동시성 시나리오에서 올바르게 처리되지 않으면 요청이 데이터 불일치, 리소스 경쟁 및 성능 저하로 이어질 수 있습니다. 이러한 문제를 해결하기 위해 동기화 및 잠금을 사용하는 방법을 보여주기 위해 실용적인 사례를 사용할 것입니다.
1. 간단한 Spring Boot 프로젝트 생성
먼저 간단한 Spring Boot 프로젝트를 만들고 간단한 REST 인터페이스를 추가합니다. 이 예에서는 은행 계좌 이체 작업을 시뮬레이션합니다.
Account
다음과 같은 항목 클래스를 만듭니다 .
public class Account {
private Long id;
private String owner;
private BigDecimal balance;
// 构造方法、getter 和 setter 省略
}
2. REST 인터페이스 생성
AccountController
송금을 위한 인터페이스로 호출되는 REST 컨트롤러를 만듭니다 .
@RestController
@RequestMapping("/accounts")
public class AccountController {
@Autowired
private AccountService accountService;
@PostMapping("/{fromAccountId}/transfer/{toAccountId}/{amount}")
public ResponseEntity<Void> transfer(@PathVariable Long fromAccountId,
@PathVariable Long toAccountId,
@PathVariable BigDecimal amount) {
accountService.transfer(fromAccountId, toAccountId, amount);
return ResponseEntity.ok().build();
}
}
3. 동시성 문제를 방지하기 위해 동기화 추가
AccountService
클래스 에서 우리는 transfer
한 계정에서 다른 계정으로 돈을 이체하는 데 사용되는 메서드를 구현합니다. 동시성 문제를 방지하기 위해 synchronized
키워드를 한 번에 하나의 스레드만 전송 작업을 수행할 수 있도록 합니다.
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
public synchronized void transfer(Long fromAccountId, Long toAccountId, BigDecimal amount) {
Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow();
Account toAccount = accountRepository.findById(toAccountId).orElseThrow();
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new IllegalStateException("Insufficient balance");
}
fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
toAccount.setBalance(toAccount.getBalance().add(amount));
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
}
}
위의 코드에서 synchronized
키워드를 transfer
메서드가 한 번에 하나의 스레드에서만 실행될 수 있도록 합니다. 이러한 방식으로 동시성으로 인한 데이터 불일치 및 리소스 경쟁 문제를 방지할 수 있습니다.
4. 요약
이 기사의 실습 사례를 통해 Spring Boot 인터페이스의 동시성 문제를 방지하는 방법을 배웠습니다. 동기화 및 잠금 메커니즘을 사용하면 한 번에 하나의 스레드만 중요한 작업을 수행할 수 있으므로 데이터 불일치 및 리소스 경쟁을 피할 수 있습니다. 그러나 동기화 및 잠금 메커니즘을 과도하게 사용하면 성능이 저하될 수 있으므로 적절하게 사용하십시오.
synchronized 키워드를 사용하는 것 외에도 다음과 같은 다른 동시성 제어 방법을 사용할 수 있습니다.
5. Java 동시성 라이브러리에서 잠금 사용
Java 동시성 라이브러리는 ReentrantLock과 같은 많은 고급 동시성 제어 도구를 제공합니다. ReentrantLock은 synchronized 키워드보다 뛰어난 유연성과 확장성을 제공합니다. 다음은 ReentrantLock을 사용하도록 재작성된 AccountService 클래스입니다.
@Service
public class AccountService {
private final ReentrantLock lock = new ReentrantLock();
@Autowired
private AccountRepository accountRepository;
public void transfer(Long fromAccountId, Long toAccountId, BigDecimal amount) {
lock.lock();
try {
Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow();
Account toAccount = accountRepository.findById(toAccountId).orElseThrow();
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new IllegalStateException("Insufficient balance");
}
fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
toAccount.setBalance(toAccount.getBalance().add(amount));
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
} finally {
lock.unlock();
}
}
}
6. 낙관적 잠금 사용
낙관적 잠금은 여러 스레드가 작업을 동시에 수행하도록 허용하지만 작업을 커밋하기 전에 데이터가 변경되었는지 확인하여 동시성 문제를 방지하는 전략입니다. 데이터가 변경된 경우 작업이 다시 실행됩니다. 이 전략은 쓰기 작업보다 읽기 작업이 더 자주 발생하는 시나리오에 적합합니다.
Spring Boot에서 낙관적 잠금을 사용하려면 엔티티 클래스에 버전 필드를 추가하고 필드를 @Version 주석으로 표시할 수 있습니다. 다음은 수정된 계정 클래스입니다.
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String owner;
private BigDecimal balance;
@Version
private int version;
// 构造方法、getter 和 setter 省略
}
이 예제에서 두 스레드가 동시에 동일한 Account 인스턴스를 업데이트하려고 하면 한 스레드만 변경 사항을 성공적으로 커밋하고 다른 스레드는 동시성 위반을 나타내는 OptimisticLockingFailureException을 수신합니다.
이 기사의 실습 사례를 통해 Spring Boot 인터페이스의 동시성 문제를 방지하는 방법을 배웠습니다. 실제 응용 프로그램에서 적절한 동시성 제어 전략을 선택하는 것은 데이터 일관성과 성능을 보장하는 데 중요합니다.