putlfAbsent()의 한계와 변경감지 개선

putlfAbsent()의 한계와 변경감지 개선

1. 들어가며

실시간 모니터링 시스템에서는 특정 값에 대한 계산식의 메타 데이터를 여러 서비스 간에 동기화해야하는 경우가 많습니다. 이번 프로젝트에서도 계산식이 생성되거나 변경될 때 이벤트를 발행하여 다른 서비스에 변경 사항을 전달하는 기능이 필요했습니다.

초기 구현에서는 동일한 계산식이 중복 생성되지 않도록 ConcurrentHashMapputIfAbsent()를 사용하였습니다. 조건문에 putIfAbsent()를 넣어서 통과한 경우에만 계산식 전달 이벤트를 발행하게 구현하였습니다. 계산식은 일반적으로 한번 생성되면 변경되지 않는다고 판단했기 때문에, 최초 등록만 처리하면 충분하다고 생각했습니다.

하지만 다양한 운영 시나리오를 검토하는 과정에서 예상하지 못한 문제를 발견하였습니다. 일부 계산식은 초기에 설정된 값들의 구성 뿐만 아니라 사용 가능한 센서 신호 구성에 따라 계산식 자체가 변경될 수 있었고, 기존 구조에서는 이러한 변경을 감지할 수 없었습니다.

예를 들어 특정 계산식은 A라는 신호만 존재할 경우 다음과 같이 생성됩니다.

 Value = A + 1.01

하지만 운영 중에 B라는 신호의 true/false 여부에 따라서 계산식은 다음과 같이 변경될 수 있습니다.

 Value = A - 1.01

즉, 동일한 계산식이라고 할 지라도 사용 가능한 신호 구성에 따라 계산식 자체가 달라질 수 있는 것입니다.

문제는 putIfAbsent()가 이미 등록된 key에 대해서는 아무 작업도 수행하지 않는다는 점입니다. 따라서 계산식이 변경되더라도 기존 값은 그대로 유지되며, 변경 이벤트 역시 발행되지 않게 됩니다.

결과적으로 다른 서비스에서는 최신 계산식을 전달받지 못하게 되고, 서비스 간 메타데이터의 불일치가 발생할 수 있습니다.

이번 글에서는 초기 설계에서 putIfAbsent()를 선택한 이유와 그 한계, 그리고 replace()를 활용하여 변경 감지와 이벤트 동기화를 함께 처리하도록 개선한 과정을 정리해보고자 합니다.

2. 초기 설계 - putIfAbsent()를 활용한 중복 생성 방지

우선 설계를 하기에 앞서 프로젝트에서 사용되는 계산식들을 확인해보았습니다. 초기 설계를 할 때 계산식들을 살펴보니, 전부 초기 설정값들을 조건으로 하는 계산식이었습니다. 또한 이렇게 한번 생성한 계산식은 초기 설정값이 바뀌지 않는 한 운영 중 변경되지 않는다고 판단하였습니다. 그래서 계산식은 최초 한 번만 저장하면 된다고 생각했습니다.

계산식이 처음 생성되는 시점에 메모리에 저장하고, 동시에 다른 서비스로 계산식 정보를 전달하기 위한 이벤트를 발행하는 구조를 설계하였습니다.

또한 해당 기능은 여러 스레드에서 동시에 접근할 수 있는 환경에서 동작해야 했기 때문에, 계산식 정보를 저장하는 캐시로 ConcurrentHashMap을 사용하였습니다. 초기 구현은 매우 단순했습니다.

image1.png

putIfAbsent()는 지정한 Key가 존재하지 않을 경우에만 값을 저장하고, 이미 존재하는 경우에는 기존 값을 반환합니다. 따라서 최초 계산식이 생성되는 경우에만 이벤트를 발행할 수 있었고, 동일한 계산식에 대한 중복 이벤트 발생도 자연스럽게 방지할 수 있었습니다.

당시에는 계산식이 한번 생성되면 변경되지 않는다고 판단하였습니다. 왜냐하면, 초기 설정값은 거의 바뀌는 일이 없는 고정된 값이었습니다. 혹여나 바뀌더라도, 서비스를 재기동해서 메모리를 초기화하면 새로운 계산식을 다시 생성할 수 있다고 생각했습니다.

3. 새로운 계산식 요구사항 검토 과정에서 발견한 문제

초기 구현을 마친 이후, 다른 계산식 기능에 대한 요구사항을 검토하는 회의에 참석하게 되었습니다. 해당 기능은 이번 개발 건과는 별개의 계산식 기능이었지만, 요구사항을 검토하던 중 기존 설계에도 영향을 줄 수 있는 부분을 발견하게 되었습니다.

회의에서 논의된 일부 계산식은 단순히 초기 설정값만을 사용하는 것이 아니라, 센서 신호의 존재 여부 혹은 신호의 값에 따라 계산식 자체가 달라질 수 있었습니다.

예를 들어 특정 센서 신호가 존재하지 않는 경우에는 A만을 이용하여 계산을 수행하지만, 해당 센서가 정상적으로 수집되기 시작하면 A와 B를 함께 사용하는 계산식으로 변경되어야 했습니다. 혹은 B라는 신호의 값이 true/efalse 여부에 따라 계산식이 변경되어야 했습니다.

이 요구사항을 검토하면서 현재 구현된 putIfAbsent() 기반 구조를 다시 살펴보게 되었습니다. 그리고 기존 구조에서는 이미 생성된 계산식이 변경되더라도 이를 감지할 수 없다는 사실을 발견하게 되었습니다.

4. 계산식 변경 감지를 위한 요구사항 재정의

기존 구조는 계산식 최초 생성 시 이벤트 발행이라는 요구사항만 만족하였습니다. 하지만 새로운 요구사항에 발맞추기 위해서는 이것만으로는 충분하지 않았습니다. 계산식이 변경될 수 있다면 최초 생성뿐만 아니라, 변경 또한 감지할 수 있어야 했습니다.

정리해보니 다음과 같은 조건을 만족하는 새로운 구조가 필요했습니다.

1. 계산식 최초 생성 시 이벤트 발행

2. 계산식 변경 시 이벤트 발행

3. 계산식이 동일한 경우 이벤트 미발행 

4. 다중 스레드 환경에서도 안전하게 동작

putIfAbsent()는 최초 생성만 처리할 수 있었습니다. 이미 등록된 Key에 대해서는 새로운 계산식이 들어오더라도 비교하거나 변경 여부를 확인할 수 없었습니다. 따라서 계산식의 변경을 감지할 수 있는 별도의 구조가 필요했습니다.

5. ConcurrentHashMap의 replace()를 활용한 변경 감지

최초에는 단순히 기존 값을 조회한 후 비교하여 변경 여부를 판단하는 방법도 고려하였습니다.

image2.png

하지만 이 방식은 다중 스레드 환경에서 안전하지 않았습니다. 예를 들어 두 개의 스레드가 동시에 동일한 계산식 정보를 수정하려고 시도할 경우, 두 스레드 모두 기존 값을 조회한 뒤 변경되었다고 판단할 수 있습니다. 이 경우 동일한 계산식 변경에 대해 이벤트가 중복 발행될 가능성이 존재합니다.

그래서, 이 문제를 해결하기 위해 ConcurrentHashMap에서 제공하는 replace() 메서드를 활용하였습니다.

image3.png

replace(key, oldValue, newValue)는 현재 Map에 저장된 값이 oldValue와 동일한 경우에만 newValue로 교체합니다. 즉, 다음과 같은 방식으로 동작합니다.

image4.png

만약 다른 스레드가 먼저 값을 변경하였다면 현재 Map의 값은 이미 oldValue가 아니게 됩니다.이 경우 replace()false를 반환하며 값 교체를 수행하지 않습니다. 따라서 실제로 계산식 변경에 성공한 스레드만 이벤트를 발행할 수 있게 됩니다. 최종적으로는 replace()의 반환값을 이용하여 이벤트 발행 여부를 결정하였습니다.

이를 통해 계산식 변경이 발생한 경우에만 이벤트를 발행할 수 있었으며, 동시에 다중 스레드 환경에서 발생할 수 있는 중복 이벤트 문제 또한 방지할 수 있었습니다.

6. 마치며

이번 기능을 개발하면서 처음에는 단순히 계산식의 중복 생성을 방지하는 것에만 초점을 맞추었습니다. 초기 요구사항만 고려했을 때는 putIfAbsent()를 이용한 구조만으로도 충분해보였습니다. 실제로 동일한 계산식에 대한 중복 저장과 중복 이벤트 발행을 효과적으로 방지할 수 있었기 때문입니다.

하지만 새로운 계산식 요구사항을 검토하는 과정에서 계산식이 운영 중 변경될 수 있다는 가능성을 발견하였고, 기존 설계가 이러한 상황을 처리하지 못한다는 점을 확인할 수 있었습니다. 이를 해결하기 위해 계산식의 최초 생성뿐만 아니라 변경 여부까지 감지할 수 있도록 구조를 개선하였으며, ConcurrentHashMapreplace()를 활용하여 다중 스레드 환경에서도 안전하게 동작하도록 구현하였습니다.

이번 경험을 통해 현재 요구사항만 만족하는 설계가 아니라 앞으로 발생할 수 있는 시나리오까지 고려하는 것이 중요하다는 점을 다시 한번 느낄 수 있었습니다. 또한 단순히 기능을 구현하는 것에서 끝나는 것이 아니라, 다양한 시나리오를 검토하고 설계의 한계를 발견하여 개선하는 과정 역시 개발의 중요한 부분이라는 것을 배울 수 있었습니다.

Yang

Site footer