배치(Batch)라는 작업은 주기적으로 실행되는 작업을 말한다. 다루는 데이터가 적은 경우는 별 걱정이 없다. 하지만 다룰 데이터가 많다면 과연 이 작업이 정해진 시간안에 끝날지 걱정하게 된다. 배치 작업은 대량의 데이터에 대한 문제도 있지만, 한 주기안에 그 일이 끝나야한다는 시간적인 제약도 존재하는 문제기도 하다.
서비스와 이를 뒷받침하는 시스템은 계속 진화한다. 그리고 데이터와 시간에 대한 최적화도 진화에 맞춰 지속되어야 한다. 최근의 시스템은 MSA(Microservice Architecture)를 따라 보편적으로 개발한다. 그리고 기존의 시스템들도 MSA화 하기 위한 방향으로 변경을 진행한다. 내가 있는 라이엇게임즈 개발팀에서도 시스템을 개편하거나 신규 시스템들은 모두 MSA를 따라 개발 작업을 진행한다.
MSA 방식으로 구성된 시스템은 Monolithic 방식으로 구성된 시스템보다는 내부 Component간 연동이 느릴 수밖에 없다. 어플리케이션 내부의 함수 콜이 작은 어플리케이션간의 RESTful API 호출로 바뀌었기 때문에 당연히 느릴 수밖에 없다. 각각의 컴포넌트는 독립적이고 자율적인 형태로 바뀌었지만, 이에 대한 반대 급부로 컴포넌트간의 연동 속도 저하가 단점이 될 수 밖에는 없다. 배치 작업 가운데 여러 외부 컴포넌트를 연동하는 경우가 많다면 MSA를 따르면서 전반적인 시스템의 최적화가 난관에 봉착하게 마련이다. 생각보다 한 주기의 시간안에 작업을 끝내는게 심각하게 어려운 작업이라는 것을 새롭게 알 수 있게 될 수 도 있다..
이 글에서는 팀이 MSA 환경에서 어떻게 배치 시스템의 속도 문제를 사고의 전환으로 해결했는지 소개한다.
휴면 계정이란?
휴면 계정은 개인 정보 보호 조치 가운데 하나다. 계정이 1년 동안 아무런 활동도 없으면, 해당 계정과 관련된 개인 정보를 라이브 시스템에서 없애고 이를 라이브 시스템과 분리된 별도의 공간에 저장하는 조치다. 유럽에서 GDPR이 작년(2017)에 실행되면서 개인 정보 보호 조치를 강화했지만, 한국에서는 이미 2015년에 이 조치를 실행했다. 이런 경우들을 두고보면 개인 정보를 다루는 법률적인 측면에서 한국이 되려 다른 나라보다 선두에 있음에 틀림없다. 꼭 이런 조치를 실행하고, 시스템을 만들어야 하는가에 대한 논의가 있긴하다. 하지만 불필요한 개인 정보를 굳이 라이브 시스템에 둘 이유는 없다. 필요없다면 지우는게 맞다. 이런 측면에서 옳은 정책임에는 틀림없다. 다만 이를 어떻게 구현하고 실행할지 그 방법론이 문제가 될 수 있을지도 모르겠다.
휴면 계정 대상자는 휴면 조치를 취하기 전 1개월내에 최소 1회 이상, 계정 소유주에게 휴면 조치가 취해진다는 사실을 알려야 한다. 그럼에도 불구하고 추가적인 활동이 없다면 최종 활동이 있었던 시점으로부터 1년이 되는 날에 계정에 대한 휴면 조치를 실행한다.
일반적으로는 이렇게 합니다.
1년 동안 아무런 활동도 없는 계정들을 추출하기 위해서 어떤 방식을 사용하나? 가장 쉽고 일반적으로 생각할 수 있는 방안은 매일 전체 Active 계정들(ACCOUNT 테이블)을 추출한 다음에 해당 계정들의 최종 Activity Date를 확인하는 방법이다.
안목있는 아키텍트가 있다면, 최종 활동에 대한 정보를 하나의 테이블(RECENT_ACTIVITY)에 모아둘 것이다. 그리고 모든 데이터가 하나의 데이터베이스에 있는 구조라면 이 문제를 가장 손쉽게 해결할 수 있다. SQL 쿼리 한방으로.
SELECT * FROM ACCOUNT, RECENT_ACTIVITY WHERE ACCOUNT.id = RECENT_ACTIVITY.account_id AND RECENT_ACTIVITY.activity_datetime < now() - 365;
휴면 계정으로 추출된 계정 정보들을 휴면 계정화하고, 이를 계정 테이블에서 삭제한다. 정말 깔끔하다. 휴면 계정화 1개월 전에 보내야하는 통보 기능도 비슷한 쿼리로 이메일 주소를 추출해서 처리할 수 있다.
SELECT * FROM ACCOUNT, RECENT_ACTIVITY WHERE ACCOUNT.id = RECENT_ACTIVITY.account_id AND RECENT_ACTIVITY.activity_datetime < now() - 335;
원래 있던 쿼리의 365를 335로 간단히 바꾸면 통보를 위한 대상자도 손쉽게 추출할 수 있다.
MSA가 대세라구요!?
MSA를 서비스에 적용하면서 이제 역할에 따른 DBMS를 별도로 분리한다. MSA의 기본 원칙 가운데 하나는 하나의 마이크로서비스가 자신의 데이터를 서비스를 위해 정의한 저장소에 관리하고, 해당 데이터가 필요한 다른 서비스들은 API를 통해 참조하는 것을 원칙으로 한다.
이제 최종 활동에 대한 관리 기능이 별개의 서비스(ActivityLogger)로 분리된다. 잦은 로깅으로 전체 서비스에 영향을 주던 민폐를 걷어내고, 온전히 로깅에 대한 역할을 담당하는 마이크로서비스로 거듭난다. 계정의 최종 활동일을 계정 테이블에 담고자하는 노력이 있긴 했지만, 계정 테이블은 많은 서비스들에서 기본적으로 참조하는 데이터 영역이고, 잦은 업데이트는 과도한 IO 부하를 일으키기 때문에 ActivityLogger 서비스의 API를 통해 참조해서 처리하기로 한다.
ElasticSearch를 통해 구현된 최종 활동 API는 빠른 응답 속도를 보장한다. 이를 사용하기로 했기 때문에 어쩔 수 없이 전체 계정들을 모두 로딩 후 API를 호출해서 최종 Activity Date를 구하고, 1년이 경과한 계정을 찾는다. 하지만 백만 건이 넘는 계정에 모두 로딩해서 처리하는 건 많은 소요 시간을 필요로 한다. 계정 아이디에 따라 이를 N개의 그룹으로 나누고, 각 그룹을 로딩해서 동시에 처리하도록 병렬 쓰레딩을 도입한다.
어차피 전체 계정들을 모두 로딩해야했기 때문에 이메일 통보 기능과 휴면화 기능을 2개의 배치 작업에서 1개의 배치 작업으로 합친다.
전체 데이터 셋과 유사한 환경을 구성하고, 테스트를 해보니 6시간이면 배치 작업이 성공적으로 완료됐다. 이제 라이브 서비스에서 실제로 배치를 실행할 시점이다.
이라, 이건 아닌데
테스트 환경과 달리 라이브 환경은 계정 테이블에 대한 다양한 조회와 변경이 존재하기 때문에 6시간 보다 완료 시간이 2시간 더 걸렸다. 하지만 24시간내에 작업이 완료됐기 때문에 ActivityLogger 서비스의 도입은 성공적으로 마무리가 됐다. 굿!!
성공적인 사업의 확장과 MSA의 도입으로 조직과 서비스 시스템들은 수평적인 확장을 통해 안정적인 서비스를 제공한다.
가입자 증가로 휴면 처리 시간이 점차 오래 걸린다. 12시간
과정에서
- 이메일 발송 기능도 클러스터링 기반의 독자적인 이메일 서비스로 새롭게 탄생한다. – 13시간
- 상품을 판매하기 시작했고, 결제 데이터가 개인화 데이터로 분류됐다. 마찬가지로 해당 데이터 역시 휴면화 대상 프로세스에 편입되어야 한다. – 16시간
- 가입자가 더 늘었다. – 20시간
하루안에 휴면 처리 배치가 완료되어야 하지만 이대로 두면 하루를 넘길 것이 분명하다. 계정 데이터에 대한 그룹을 세분화하고, 추가 장비들을 도입해서 병렬화를 높이는 것도 방법이긴 하지만 과도한 조회로 인해 계정 테이블에 무리를 주는 건 그닥 올바른 방법으로 보이지 않는다.
문제를 다시 정의해보자.
MSA 적용 전/후의 휴면 계정 처리 시스템이 갖는 가장 큰 차이점은 대상 계정을 추출하는 방식이다. MSA 적용 이전의 Monolithic 시스템 상황에서는 DBMS를 통해 전체 계정을 조회했다. 이후에는 직접 전체 계정을 조회하는 방식이 적용됐다. 방식의 차이가 있긴 하지만 모두 전체 계정을 조회한다는 점에는 차이가 없다. 바꿔 이야기하면 두 방식 모두 가입자가 늘어나는 상황에서는 모두 문제를 가질 수 밖에는 없게 된다.
우리가 해야할 문제를 다시 한번 짚어보자. 계정의 휴면화가 진행되는 과정을 살펴보면 1년이 되기 한 달 전에 계정의 소유주에게 알림을 주고, 1년이 경과한 시점에 휴면화를 처리한다. 즉 시간의 흐름에 따라 발생하는 이벤트다. 그리고 꼭 전체 계정을 대상으로 해야할 작업이 아니라 이 조건은 특정 계정에 관련된 이슈라고 정의할 수 있다.
문제를 이렇게 다시 정리해보니, 계정을 시간 조건에 의한 State Machine으로 간주하면 쉬운 해결 방안을 찾을 수 있다. 즉 위 조건에 따르면 다음과 같은 계정의 상태 전이가 이뤄진다.
NOTIFIED 혹은 DORMANTED 상태에서 계정에 Activity가 발생했다면, 당연히 ACTIVE 상태로 변경된다. 그리고 각각의 상태 변화가 발생하는 시점에서 필요한 작업들을 해주면 된다. 예를 들어 이메일을 발송하거나 계정의 개인 정보를 분리된 데이터베이스로 옮기는 작업들이 이에 속한다.
이렇게 정리되면 계정의 상태 변화를 발생시킬 수 있는 도구만 있으면 된다. 그리고 생각외로 이런 기능을 지원해주는 Repository들을 손쉽게 찾아볼 수 있다.
그래서 이렇게 바꿨다.
휴면 계정 처리 작업에서 계정을 시간을 조건으로 한 State Machine으로 정의하고 보니, 계정 테이블을 직접 연동하는 것이 아니라 휴면 프로세스를 위한 State Machine용 repository를 만들어버리는데 훨씬 더 깔끔한 접근 방법이다. 이런 식으로 방향을 잡으니 아예 휴면 계정을 위한 별도의 마이크로 서비스를 만드는게 더 효과적이다. 그래서 다음과 같은 방식으로 휴면 계정 처리 서비스의 아키텍쳐와 도구들을 잡았다.
- 시간의 흐름에 따른 계정의 상태 변화를 관리하기 위해 별도의 Repository를 AWS DynamoDB를 활용하여 정의한다.
- AWS DynamoDB의 기능 가운데 TTL 관리 기능이 존재한다.
- DynamoDB의 TTL 기능은 단일 Entry 수준에서 TTL 값을 모두 다르게 설정할 수 있다.
- 지정된 시간이 초과한 데이터를 AWS Lambda를 통해 추가적인 필요한 처리를 실행할 수 있다.
- 상태의 변화가 일어날 때 처리해야할 작업들이 좀 된다. 람다의 성격상 복잡한 Biz Logic을 구현하는 건 맞지 않다고 보기 때문에 별도 Application에서 따로 처리한다.
- AWS Lambda는 Expire된 항목들을 받아 계정의 상태 변화를 관리하는 어플리케이션에 전달한다.
- 어플리케이션은 전달된 계정의 지정된 State Transition을 관리하고, 상태 변경시에 취해져야할 기능들을 실행한다.
- ActivityLogger 서비스와 연동해서 계정의 활동이 발생하면 해당 계정의 상태가 항상 ACTIVE 상태가 되도록 한다.
- 물론 마이크로서비스를 위한 최초 상태 데이터는 계정 데이터베이스와 ActivityLogger 시스템으로부터 생성한다.
이와 같은 구조로 변경되면 앞서 이야기했던 배치 작업이 없어진다. 시간의 흐름에 따른 개별 계정의 상태 변화는 DynamoDB와 ActivityLogger 서비스에 의해 실시간으로 처리된다. 더 이상 배치 작업이 하루 안에 마무리 될지 가지고 조바심내지 않아도 된다.
MSA 환경에 적응할려면.
마이크로서비스 아키텍처는 서비스를 제공하는 환경을 크게 탈바꿈시켰다. 개발된 기능을 배포하는 시간을 크게 줄였을 뿐만 아니라 서비스의 독립성과 빠른 배포 주기를 기본 사상으로 가지고 있기 때문이다. 이를 충실히만 따른다면 빠른 피드백 사이클을 갖는 서비스의 개발과 유지가 가능하다.
하지만 모든 것에는 장단이 있기 마련이다. 때문에 모든 Monolithic 어플리케이션을 악의 축으로 보고, 이를 MSA화 하는 것은 상당한 위험을 내포하고 있다. 실제 변화를 꾀하기 전에 단기적으로 발생하는 손익도 반드시 고려해야한다. 이 글에서 이야기한 것처럼 예기치 않은 부작용이 발생할 수 있으며 관련된 추가 비용이 발생한다.
MSA를 효과적으로 반영하기 위해서는 상황에 맞는 적절한 도구들을 활용해야 한다. 휴면 계정용 서비스를 구현하는데는 AWS DynamoDB라는 도구를 이용해 시간에 따른 이벤트를 활용할 수 있었다. 물론 DynamoDB도 Expiration이라는 것을 정확하게 체크하지는 못한다. 해당 시간이 지난 후 4시간 안에는 지워진다지 정각에 지워지는건 아니다. 하지만 휴면 계정을 실행하는데 10분 ~ 20분의 차이는 서비스의 동작에 영향을 미치지 않는다.
마이크로서비스는 기능에 집중하기 때문에 해당 기능에 최적화된 도구를 사용할 수 있느냐 없느냐에 따라 서비스의 효율성이 좌우된다. MSA 환경에서 아키텍트는 특정 도구만 고집할 것이 아니라 다양한 도구들이 어떤 특성을 가지고 있고, 상황에 따라 유연하게 도구들이 사용될 수 있도록 가이드할 수 있어야 한다. 물론 어느 한 사람이 현재의 도구나 최신 기술들을 모두 알기란 불가능하고 또 그럴 필요도 없다. 개념이면 충분하다. 그럼 활용할 수 있는 시점에 도구들을 찾아내기만 하면 된다.
무엇보다도 중요한 점은 하나의 틀에 얽매이지 않는 자세가 중요하다. MSA가 아무리 좋다고 하지만, 본인이 있는 환경에 어울리지 않는다면 사용하지 않는 용기도 가지고 있어야 한다. 좋은 개발자 혹은 테크 리더가 보여줘야 할 태도중에는 항상 최신이 아닌 최선을 위해 최신 기술을 미룰줄도 알아야 하는게 아닐까 싶다.
– 끝 –
‘무엇보다도 중요한 점은 하나의 틀에 얽매이지 않는 자세가 중요하다’ 라는 점이 가장 공감가네요
좋은 글 포스팅해주셔서 감사합니다!!
개발자에게 사고의 자유로움이 최고의 선물 아닐까 싶네요. 좋은 글인지 어떤지는 모르지만 좋은 자극이 되었으면 해요~
사고의 자유로움이 나오려면 그에 앞서 서비스 대상에 특징을 꿰뚫고, 그에따른 알맞은 정의가 필요한 것 같아요. 이를 성공적으로 구현하신것 같아요! 저에겐 어려운 글이었지만 잘 읽었습니다. ( _ _ )