기존 스레드 로컬 변수를 활용해 다중 WAS 환경에서 세션정보의 일관성을 확보하고자 하였지만 접근 방향이 맞지 않았고, 멀티 스레드 환경에서 주문관리의 무결성을 보장하기 위한 방안으로 사용하고자 하였으나 이 역시 스레드 로컬 변수로는 한계점이 많았다.
최종적으로 동시주문제어는 redis를 그대로 활용하되, 주문차액 산출 시 스레드로컬 변수를 활용하는 것이 가장 적합하다 판단하였고 활용하였다.
이에 대해 고민하고 방안을 구성하였던 과정을 기록한다.
기존 주문관리의 동시성 제어는 redis의 pub/sub 방식으로 여러 스레드가 한 주문정보에 동시에 접근하였을때 일관성있는 주문처리를 진행할 수 있도록 설정하였다.
이번 스레드로컬 변수의 경우 여러가지로 기존 redis 방안과 함께 운용하면서 동시주문제어를 효율적으로 할 수 있다는 판단과 협의하에 진행하였다.
스레드로컬 변수를 활용한 방안 역시 서비스 호출시점에서 스레드로컬 변수를 일관성 있는 동기화된 값을 활용한다는 점, 추가 환경구성이 필요없다는 점에서 적합하다는 생각이 들었다.
별도의 synchronized와 같은 추가적인 조치없이 주문금액만을 매개변수로 사용하여, 비교적 간단하게 구현할 수 있다는 점도 채택이유이다.
스레드로컬변수를 관리할 수 있는 ThreadLocalConfigurer 클래스를 제작하여 모든 업무에서 사용할 수 있도록 하였다.
특히 기존 DTO와는 달리, 스레드로컬변수를 저장/삭제하고 스레드로컬변수의 제너릭 타입인 엔티티를 생성하는 모든 과정을 해당 클래스에서 일괄 진행할 수 있도록 하였다.
더불어 기존 get/set와는 달리 빌더패턴을 적용하여 로직의 간결성을 유도할 수 있도록 적합한 return type을 가지도록 구성하였다.
public class ThreadLocalConfigurer extends AbstractClass(
private String ordrNo;
private ThreadLocalEntity threadLocalEntity;
ThreadLocal<ThreadLocalEntity> threadLocalStore = Threadlocal<>();
new public ThreadLocalEntity getThreadLocalEntity(){
return this.threadLocalEntity;
}
public ThreadLocalEntity getThreadLocalEntity(BigDecimal amount, ThreadLocalEntity threadLocalEntity){
return threadLocalEntity.setAmount(amount).getThreadLocalEntity();
}
public void setAmtStore(ThreadLocalEntity threadLocalEntity){
threadLocalStore.set(threadLocalEntity);
}
public ThreadLocalEntity getAmtStore(){
return threadLocalStore.get();
}
public void removeNameStore() {
threadLocalStore.remove();
}
스레드로컬 변수를 저장할 타입인 엔티티가 필요하였기에 별도로 구성하였다.
biz-common에 구성하였는데, 빈생성을 위해 @Component를 필수로 기입해주어야 하였다.
엔티티에는 생성자와 함께, 필드변수에 대한 get/set 메소드를 지정해주었으며, set메소드에서는 엔티티를 return해주어 연쇄사용이 가능하도록 구성해주었다.
더불어 엔티티 정보를 불러오는 엔티티전용 get 메소드까지 구성해주었는데, 생성자를 통해서만 엔티티의 주문금액을 초기화하여 구성할 수 있도록 하였다.
누적 주문금액의 일관성 저해를 방지하기위한 핵심적인 요소는 스레드로컬 엔티티에 저장된 주문금액이다.
public class ThreadLocalEntity extends AbstractClass(
private BigDecimal amount;
ThreadLocalEntity(BigDecimal amount){
this.amount = amount;
}
public BigDecimal getamount() {
return this.amount;
}
public ThreadLocalEntity setAmount(BigDecimal amount){
this.amount = amount;
return this;
}
public ThreadLocalEntity getThreadLocalEntity(){
return this;
}
}
서비스를 진행하면서 다건 주문건을 처리하면서 누적 주문금액의 누적 차액을 계산하며, 스레드로컬에는 남은 주문금액을 저장한다.
이때 스레드 로컬 변수에 남은 주문금액이 더이상 없을 경우 사용자에게 예외처리를 진행하여 주문을 진행할 수 없다는 예외처리를 진행한다.
스레드마다 주문금액을 고유한 값으로 누적하여 차감하거나 증액할 수 있기에 무결성을 확보할 수 있으며, 나아가 채번오류역시 로그인한 사용자마다 고유한 채번값을 가질 수 있으므로 서비스 호출 시점부터 채번, 종료까지 주문상태의 무결성을 최대한 보장할 수 있는 방안을 최종적으로 구성하였다.
추가적으로 Early Return pattern 이용하여 각 validate의 목적성을 잘 표현할 수 있도록 로직을 구성하였다.
/*
** 스레드로컬 변수를 사용한 핵심 부분만 기재
*/
public int Service(ArrayList<ServiceDTO> request) {
//values
String emplNo = Securityutil.getCurrentuserDetail(SecurityuserDetail.class).getEmpINo();
BigDecimal amount = request.getAmount();
ThreadlocalConfigurer threadLocalConfigurer = new ThreadLocalConfigurer();
...주문 validate
//서비스를 호출하는 시점에 최초 주문금액을 저장하기 위한 스레드로컬 생성
ThreadLocalEntity threadlocalEntity = new ThreadLocalEntity(amount);
threadLocalConfigurer.setAmtStore(ThreadLocalEntity.setAmount(amount));
/*
** 서비스 실행시점에 스레드로컬변수를 저장하되,
** 동일 사용자와 주문번호에 대한 스레드로컬변수가 null이 아니라면
** 동일한 사용자가 동시주문하는 상태이므로 Exception 발생
*/
for(ServiceDTO ordrItem : request){
//스레드 로컬에 저장된 주문차액
BigDecimal amount = threadLocalConfigurer.getAmtStore.getAmount();
// 주문차액 누적이 0보다 클 경우에만 주문 가능
if(amount - ordrItem.getPrice() < 0)
throw new KidbException();
....주문
//주문 진행할 경우 남은 차액을 스레드로컬 변수에 저장 threadLocalConfigurer.setAmtStore(ThreadLocalEntity.setAmount(amount - ordrItem.getPrice()));
//서비스 종료시점에 스레드로컬변수 삭제
threadLocalConfigurer.removeNameStore();
}
스레드 로컬에 저장된 최종 차액은 별도 전산처리하는데 또 활용하도록 하였다.
스레드로컬 - https://velog.io/@semi-cloud/%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B3%A0%EA%B8%89%ED%8E%B81-ThreadLocal
스레드로컬변수는 반드시 remove가 필요하다, 서로 다른 독립성을 가지지만 remove를 하지않을 경우 공유할 수 있다, 이 경우 무결성 저해 - https://www.inflearn.com/community/questions/1312200/threadlocal%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%96%88%EB%8A%94%EB%8D%B0-%EC%99%9C-%EC%97%AC%EB%9F%AC-%EC%8A%A4%EB%A0%88%EB%93%9C%EC%97%90%EC%84%9C-%EC%A0%91%EA%B7%BC%EC%9D%B4-%EA%B0%80%EB%8A%A5%ED%95%9C%EA%B1%B8%EA%B9%8C%EC%9A%94?srsltid=AfmBOoqKUPje7mVsIPadkjuUdSIHhMT6nNcR-J7Cm3nFOsDMJs-ZnlBP