개요
모의 주식 거래 사이트를 개발하면서 WebSocket 으로 실시간 가격을 받아오고 있다. 이때 시장가 주문이 들어온다면 해당 실시간 가격으로 거래를 처리해야하기에 실시간 데이터를 기록하고 불러오는 기능이 필요했다.
@Component
public class PriceHolder {
private BigDecimal currentPrice = BigDecimal.ZERO;
public void updatePrice(BigDecimal price) {
currentPrice = price;
}
public BigDecimal getCurrentPrice() {
return currentPrice;
}
}
그래서 PriceHolder
라는 객체를 생성하고 여기에서 매번 ws 응답마다 가격을 저장하고, 주문 처리 시 가격을 가져와서 사용하였다.
하지만 이러한 형태는 동시성 제어가 전혀 고려되어있지 않다. 물론 시장가 주문이 주문자가 가격을 확인하지 않는 주문이지만 금융권에서는 철저한 동시성 제어가 필요할 것이다. 엄밀히 살피자면 멀티 스레드로 동작한다면 스레드 간 가시성이 전혀 보장되지 않기에 같은 시점에서도 다른 값을 읽어갈 수도 있다는 큰 문제를 가지고 있다.
가시성 문제
![](https://blog.kakaocdn.net/dn/bIehPo/btsKDSCnCTC/fZ86aCUdsM1dHZfk4ve36k/img.png)
보통의 메모리 구조는 위와 같다. 이때 L1, L2, L3 캐시 등이 존재하는데, 메인 메모리에 변수의 값이 갱신되더라도 캐시 값이 갱신되지 않는다면 정합성 문제가 발생할 수 있다.
위에서 말한 멀티 스레드에서의 정합성 문제는 위 같은 메모리 구조 때문에 발생한다.
1. Volatile
그럼 가시성 문제는 어떻게 해결할 수 있을까? 메모리 구조를 보면 알겠듯이 변수의 값을 가져올 때 메인 메모리에서 가져오면 스레드간 문제가 없다!
이를 위한 키워드가 volatile
이다!
@Component
public class PriceHolder {
private volatile BigDecimal currentPrice = BigDecimal.ZERO;
public void updatePrice(BigDecimal price) {
currentPrice = price;
}
public BigDecimal getCurrentPrice() {
return currentPrice;
}
}
단순히 변수에 volatile
키워드만 붙어있다. 해당 키워드는 최적화되지 않는다! 변수를 메인 메모리에 저장하고 읽을 때마다 CPU 캐시가 아닌 메모리에서 직접 읽기 때문에 스레드 간 가시성이 보장된다.
하지만 키워드 하나로 동시성이 보장되지는 않는다.
2. Synchronized
@Component
public class PriceHolder {
private BigDecimal currentPrice = BigDecimal.ZERO;
private final Object lock = new Object();
public void updatePrice(BigDecimal newPrice) {
synchronized (lock) {
this.currentPrice = newPrice;
}
}
public BigDecimal getCurrentPrice() {
synchronized (lock) {
return currentPrice;
}
}
}
가장 간단한 동시성 처리 방법이다. 해당 키워드가 붙는다면 해당 메서드나 블록에는 한 번에 하나의 스레드만 진입할 수 있기에 동시성 문제가 해결된다. 하지만 병렬 처리가 안되기에 속도가 상당히 느려지는 문제가 있다. 금융에서 주문 처리의 속도도 매우 중요하기에 병렬 처리가 필수적이라고 할 수 있다.
추가로 메서드 레벨의 동기화보다는 명시적 락 객체 사용이 더 안전하다고 한다. 메서드 레벨의 동기화는 외부에서 락을 획득할 수도 있고 상속 시 영향이 발생하기에 명시적 락 사용을 참고하자.
3. Concurrent 패키지
마지막으로 Concurrent 패키지 타입을 쓰는 방법이 있다.
import java.util.concurrent.atomic.AtomicReference;
@Component
@Slf4j
public class PriceHolder {
private final AtomicReference<BigDecimal> currentPrice = new AtomicReference<>(BigDecimal.ZERO);
public void updatePrice(BigDecimal newPrice) {
currentPrice.set(newPrice);
}
public BigDecimal getCurrentPrice() {
return currentPrice.get();
}
}
대표적으로는 ConcurrentHashMap
이 있다. 이 패키지에 있는 타입들은 현재 스레드에서 사용되고 있는 값들이 메인 메모리에 있는 값들과 같은 지 비교하고 불일치한다면 해당 값을 가져와서 사용하는 CAS 알고리즘을 사용한다. 때문에 Dirty Read 등을 방지하면서 원자성을 보장할 수 있다.
Synchronized 와 달리 병렬성을 해치지 않으면서 동시성을 보장하기에 더 좋은 성능을 가진다.
이를 적용하는 프로젝트에서는 주식별 실시간 가격이 필요했고 이를 위해 Map 형태를 구상하였기에 ConcurrentHashMap<String, BigDecimal> 을 적용시켰다.
BigDecimal 같은 경우는 연산 시간은 오래걸리지만 정합성이 중요한 금융 시스템에서 사용해야할 타입이라 생각했다.
'Programming > JAVA' 카테고리의 다른 글
부동소수점? BigDecimal 을 통한 정확한 계산 (7) | 2024.11.11 |
---|---|
[JAVA] 정적 파일 위치 설정 및 src/main/resource 경로 ClassLoader 로 읽기 및 Java Reflection 살펴보기 (0) | 2024.07.11 |
[JAVA] JVM 구조 총정리 (0) | 2024.07.10 |
[JAVA] Spring 없이 웹 서버 구축! (4) 라우팅을 위한 Mapper (0) | 2024.07.09 |
[JAVA] Spring 없이 웹 서버 구축! (3) HTTP Request, Response 객체 (0) | 2024.07.09 |