개발중인 BaaS(Banking as a Service) api 서비스들에서, 체계적으로 포맷이 정해진 리퀘스트 고유 아이디를 채번하는 기능이 필요하게 되었다(로그에서 각 리퀘스트의 히스토리 추적을 용이하게 하기위해)
사내에서 요구하는 양식에 맞는 각 리퀘스트 고유 아이디를 할당하는 과정에서, 각 리퀘스트를 담당하는 스레드들이 범용적으로 동시에 사용할 수 있는, thread-safe이자 concurrent를 보장하는 시퀀스 로직이 필요하게 되었다
다중 스레드 간 유니크를 보장할 수 있는 시퀀스 로직을 포함한 아이디 채번 로직을 만든다
할당하고자 하는 리퀘스트 아이디 포맷에 따라 달라지겠지만,
당행의 BaaS 프로젝트에서 채택한 고유 리퀘스트 아이디 포맷은 다음과 같다
총 30자: yyyyMMddHHmmssSSS(17) + API(3) + xxxxx(pod_name) + 00001(~99999 시퀀스)
똑같은 시각(년월일시분초밀리초)일때 같은 파드 내부로 들어온 리퀘스트에 대해서는 리퀘스트 아이디가 각자 고유하도록 시퀀스 할당을 구현한다
이런 유니크 시퀀스에 대해서는 java.util.concurrent.atomic 패키지에서 제공하는 Atomic 시리즈를 활용한다
public class IDGenerator {
// 시퀀스를 제외한 나머지 아이디 자리 수 생성 로직
private static final AtomicInteger sequence = new AtomicInteger(1);
public static String generate() {
if(마지막으로 채번했던 당시 ymdhms와 지금 ymdhms가 다르다면) sequence.set(1);
return new StringBuilder()
.append(시퀀스를 제외한 아이디 세그먼트)
.append(String.format("%05d", sequence.getAndIncrement()))
.toString();
}
}
AtomicInteger.getAndIncrement()는 i++ 와 같이 값 반환 후 아톡이 내부에서 관리하고 있는 현재 숫자에 대해 +1 연산을 수행한다
해당 메소드는 concurrent이며 thread-safe라서 스레드간 유니크한 시퀀스 할당이 가능하다
단순하게 생각했을때는 아토믹을 사용하지 않고, 학생때 생각하던 것 처럼 synchronized 블럭으로 접근할 수도 있다
public class IDGenerator {
// 시퀀스를 제외한 나머지 아이디 자리 수 생성 로직
private static int sequence = 1;
public synchronized static String getID() {
if(마지막으로 채번했던 당시 ymdhms와 지금 ymdhms가 다르다면) sequence = 1;
return new StringBuilder()
.append(시퀀스를 제외한 아이디 세그먼트)
.append(String.format("%05d", sequence++))
.toString();
}
}
그러나, (하나의 리퀘스트가 리스폰스를 받기까지 어떤 논리 순서가 보장되어야 하는) 멀티쓰레드 환경의 동기화가 필요한 작업이 아니라면, synchronized를 사용하는 것은 싱글 쓰레드 환경에서 최대한 지양하는 것이 좋다.
즉, 쉽게 말하면, synchronzied는 절대 필요한게 아니라면 반드시 안써야 되는게 맞다.
이유는 단순하다. synchronized 블록을 사용하게 되면, 스레드 간 concurrent를 보장하기 위해 현재 진행중인 스레드가 작업을 끝날때까지 임계영역에 다른 스레드가 접근하지 못하기 때문이다
비록 이번에 싱크로나이즈 블록을 사용하는 영역의 연산이 단순하여 오버헤드가 크리티컬하진 않겠지만, 습관 자체를 바꿔놓을 필요가 있는것이다
톰캣 또한 리퀘스트 당 하나의 스레드가 담당하니 리퀘스트가 동시 들어왔을때 고유 시퀀스 채번만 보장해줄수 있다면 synchronized는 안쓰는게 맞는 것 같다