주식을 주제로 한 프로젝트를 진행하던 중, 사용자가 여러 종목에 대해 자동매매를 동시에 실행할 때 동시성 문제와 정합성 문제가 발생하는 상황을 확인했습니다.
현재 Member
테이블에는 사용자의 자산 정보가, Account
테이블에는 현재 보유 중인 주식 정보가 저장되어 있습니다.
그런데 여러 스레드(또는 요청)가 동시에 주문을 넣거나 체결을 처리하게 되면, 다음과 같은 문제가 생겼습니다.
예를 들어,
if (member.getMoney() >= 주문금액 && account.getStockCnt() >= 주문수량) {
// 주문 처리
}
member.getMoney()와 account.getStockCnt()는 서로 다른 테이블에서 각각의 데이터를 조회하게 되는데,
이 과정이 동일한 트랜잭션 내에서 일관성 있게 보장되지 않으면 다음과 같은 상황이 벌어질 수 있습니다.
예를 들어, A 스레드가 두 값을 각각 조회해 조건을 만족한다고 판단한 직후, B 스레드가 먼저 주문을 체결하면서 자산 또는 주식 수량을 변경해버리는 경우입니다.
하지만 A 스레드는 여전히 "조건을 만족한다"고 착각하고 주문을 처리하게 되어서, 실제 자산보다 많은 주문이 체결되거나, 보유하지 않은 주식을 매도하는 상황이 발생할 수 있습니다.
이런 동시성 문제를 막기 위해 처음에는 @Version을 활용한 낙관적 락(Optimistic Lock)
을 사용해 문제를 해결하려고 했지만,
자동매매처럼 트래픽이 빈번한 환경에서는 충돌이 자주 발생하고 롤백이 많아지는 문제가 있었고, 결국 비관적 락(Pessimistic Lock)
으로의 전환을 검토하게 되었습니다.
낙관적 락은 말 그대로 "충돌이 거의 없을 것"이라고 낙관적으로 가정하는 락 방식입니다.
트랜잭션 간의 충돌 가능성이 낮다고 보고, 일단 데이터를 자유롭게 읽고 수정한 뒤 커밋 시점에 충돌 여부를 검사합니다.
JPA에서는 @Version
어노테이션을 사용해서 구현합니다.
이 어노테이션이 붙은 필드는 트랜잭션이 커밋될 때 자동으로 버전 체크가 수행됩니다.
데이터가 업데이트될 때마다 @Version 필드 값이 1씩 증가하는 구조입니다.
@Entity
public class Member {
@Id
private Long id;
private Long money;
@Version
private Long version;
}
UPDATE member SET money = ?, version = version + 1 WHERE id = ? AND version = ?
와 같은 쿼리를 실행
장점
단점
비관적 락은 충돌이 일어날 가능성이 높다고 비관적으로 가정하고,
데이터를 읽을 때부터 다른 트랜잭션이 접근하지 못하도록 락을 거는 방식입니다.
JPA에서는 @Lock
어노테이션과 LockModeType.PESSIMISTIC_WRITE
등을 사용합니다.
SELECT ... FOR UPDATE
쿼리가 날아가며 락이 걸림public interface MemberRepository extends JpaRepository<Member, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT m FROM Member m WHERE m.id = :id")
Optional<Member> findByIdForUpdate(@Param("id") Long id);
}
SELECT ... FOR UPDATE
쿼리가 날아가고, 해당 행은 락이 걸린 상태
장점
단점
(프로젝트 사진 추가 예정)
현재는 JPA와 비관적 락을 기반으로 구현을 시작했습니다.
이후 거래량이 증가하고 병목 현상이 발생할 경우, Redis 기반의 분산 락으로 전환할 예정입니다.
만약 더 큰 규모의 트래픽 처리나 자동 매매 로직의 고속화가 필요해진다면, Kafka 기반의 비동기 처리 구조로 확장할 예정입니다.
정합성이 중요한 로직에서는 충돌을 피할 수 있는 전략이 반드시 필요하다는 것을 느꼈습니다. 낙관적 락이 성능 면에서 유리하긴 하지만, 충돌 가능성이 높은 환경에서는 비관적 락이나 분산 락으로의 전환을 고려해야만 안정적인 시스템을 유지할 수 있다는 점을 체감했습니다.
트랜잭션 설계와 데이터베이스 특성까지 함께 고려한 전략적인 판단이 결국 사용자에게 안정적인 서비스를 제공하는 핵심임을 다시금 느낄 수 있었습니다.
https://f-lab.kr/insight/understanding-optimistic-and-pessimistic-locking
https://sabarada.tistory.com/175