휴면 계정 처리 – 배치에서 온라인 시스템으로

배치(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 서비스의 도입은 성공적으로 마무리가 됐다. 굿!!

Clock6h

성공적인 사업의 확장과 MSA의 도입으로 조직과 서비스 시스템들은 수평적인 확장을 통해 안정적인 서비스를 제공한다.

가입자 증가로 휴면 처리 시간이 점차 오래 걸린다. 12시간

Clock12h

과정에서

  • 이메일 발송 기능도 클러스터링 기반의 독자적인 이메일 서비스로 새롭게 탄생한다.  – 13시간
  • 상품을 판매하기 시작했고, 결제 데이터가 개인화 데이터로 분류됐다. 마찬가지로 해당 데이터 역시 휴면화 대상 프로세스에 편입되어야 한다.  – 16시간
  • 가입자가 더 늘었다. – 20시간

Clockmh

하루안에 휴면 처리 배치가 완료되어야 하지만 이대로 두면 하루를 넘길 것이 분명하다. 계정 데이터에 대한 그룹을 세분화하고, 추가 장비들을 도입해서 병렬화를 높이는 것도 방법이긴 하지만 과도한 조회로 인해 계정 테이블에 무리를 주는 건 그닥 올바른 방법으로 보이지 않는다.

문제를 다시 정의해보자.

MSA 적용 전/후의 휴면 계정 처리 시스템이 갖는 가장 큰 차이점은 대상 계정을 추출하는 방식이다. MSA 적용 이전의 Monolithic 시스템 상황에서는 DBMS를 통해 전체 계정을 조회했다. 이후에는 직접 전체 계정을 조회하는 방식이 적용됐다. 방식의 차이가 있긴 하지만 모두 전체 계정을 조회한다는 점에는 차이가 없다. 바꿔 이야기하면 두 방식 모두 가입자가 늘어나는 상황에서는 모두 문제를 가질 수 밖에는 없게 된다.

우리가 해야할 문제를 다시 한번 짚어보자. 계정의 휴면화가 진행되는 과정을 살펴보면 1년이 되기 한 달 전에 계정의 소유주에게 알림을 주고, 1년이 경과한 시점에 휴면화를 처리한다. 즉 시간의 흐름에 따라 발생하는 이벤트다. 그리고 꼭 전체 계정을 대상으로 해야할 작업이 아니라 이 조건은 특정 계정에 관련된 이슈라고 정의할 수 있다.

문제를 이렇게 다시 정리해보니, 계정을 시간 조건에 의한 State Machine으로 간주하면 쉬운 해결 방안을 찾을 수 있다. 즉 위 조건에 따르면 다음과 같은 계정의 상태 전이가 이뤄진다.

AccountActivities

NOTIFIED 혹은 DORMANTED 상태에서 계정에 Activity가 발생했다면, 당연히 ACTIVE 상태로 변경된다. 그리고 각각의 상태 변화가 발생하는 시점에서 필요한 작업들을 해주면 된다. 예를 들어 이메일을 발송하거나 계정의 개인 정보를 분리된 데이터베이스로 옮기는 작업들이 이에 속한다.

이렇게 정리되면 계정의 상태 변화를 발생시킬 수 있는 도구만 있으면 된다. 그리고 생각외로 이런 기능을 지원해주는 Repository들을 손쉽게 찾아볼 수 있다.

그래서 이렇게 바꿨다.

휴면 계정 처리 작업에서 계정을 시간을 조건으로 한 State Machine으로 정의하고 보니, 계정 테이블을 직접 연동하는 것이 아니라 휴면 프로세스를 위한 State Machine용 repository를 만들어버리는데 훨씬 더 깔끔한 접근 방법이다. 이런 식으로 방향을 잡으니 아예 휴면 계정을 위한 별도의 마이크로 서비스를 만드는게 더 효과적이다. 그래서 다음과 같은 방식으로 휴면 계정 처리 서비스의 아키텍쳐와 도구들을 잡았다.

  1. 시간의 흐름에 따른 계정의 상태 변화를 관리하기 위해 별도의 Repository를 AWS DynamoDB를 활용하여 정의한다.
    • AWS DynamoDB의 기능 가운데 TTL 관리 기능이 존재한다.
    • DynamoDB의 TTL 기능은 단일 Entry 수준에서 TTL 값을 모두 다르게 설정할 수 있다.
    • 지정된 시간이 초과한 데이터를 AWS Lambda를 통해 추가적인 필요한 처리를 실행할 수 있다.
  2. 상태의 변화가 일어날 때 처리해야할 작업들이 좀 된다.  람다의 성격상 복잡한 Biz Logic을 구현하는 건 맞지 않다고 보기 때문에 별도 Application에서 따로 처리한다.
  3. AWS Lambda는 Expire된 항목들을 받아 계정의 상태 변화를 관리하는 어플리케이션에 전달한다.
  4. 어플리케이션은 전달된 계정의 지정된 State Transition을 관리하고, 상태 변경시에 취해져야할 기능들을 실행한다.
  5. ActivityLogger 서비스와 연동해서 계정의 활동이 발생하면 해당 계정의 상태가 항상 ACTIVE 상태가 되도록 한다.
  6. 물론 마이크로서비스를 위한 최초 상태 데이터는 계정 데이터베이스와 ActivityLogger 시스템으로부터 생성한다.

Dormant service

이와 같은 구조로 변경되면 앞서 이야기했던 배치 작업이 없어진다. 시간의 흐름에 따른 개별 계정의 상태 변화는 DynamoDB와 ActivityLogger 서비스에 의해 실시간으로 처리된다. 더 이상 배치 작업이 하루 안에 마무리 될지 가지고 조바심내지 않아도 된다.

MSA 환경에 적응할려면.

마이크로서비스 아키텍처는 서비스를 제공하는 환경을 크게 탈바꿈시켰다. 개발된 기능을 배포하는 시간을 크게 줄였을 뿐만 아니라 서비스의 독립성과 빠른 배포 주기를 기본 사상으로 가지고 있기 때문이다. 이를 충실히만 따른다면 빠른 피드백 사이클을 갖는 서비스의 개발과 유지가 가능하다.

하지만 모든 것에는 장단이 있기 마련이다. 때문에 모든 Monolithic 어플리케이션을 악의 축으로 보고, 이를 MSA화 하는 것은 상당한 위험을 내포하고 있다. 실제 변화를 꾀하기 전에 단기적으로 발생하는 손익도 반드시 고려해야한다. 이 글에서 이야기한 것처럼 예기치 않은 부작용이 발생할 수 있으며 관련된 추가 비용이 발생한다.

MSA를 효과적으로 반영하기 위해서는 상황에 맞는 적절한 도구들을 활용해야 한다. 휴면 계정용 서비스를 구현하는데는 AWS DynamoDB라는 도구를 이용해 시간에 따른 이벤트를 활용할 수 있었다. 물론 DynamoDB도 Expiration이라는 것을 정확하게 체크하지는 못한다. 해당 시간이 지난 후 4시간 안에는 지워진다지 정각에 지워지는건 아니다. 하지만 휴면 계정을 실행하는데 10분 ~ 20분의 차이는 서비스의 동작에 영향을 미치지 않는다.

마이크로서비스는 기능에 집중하기 때문에 해당 기능에 최적화된 도구를 사용할 수 있느냐 없느냐에 따라 서비스의 효율성이 좌우된다. MSA 환경에서 아키텍트는 특정 도구만 고집할 것이 아니라 다양한 도구들이 어떤 특성을 가지고 있고, 상황에 따라 유연하게 도구들이 사용될 수 있도록 가이드할 수 있어야 한다. 물론 어느 한 사람이 현재의 도구나 최신 기술들을 모두 알기란 불가능하고 또 그럴 필요도 없다. 개념이면 충분하다. 그럼 활용할 수 있는 시점에 도구들을 찾아내기만 하면 된다.

무엇보다도 중요한 점은 하나의 틀에 얽매이지 않는 자세가 중요하다. MSA가 아무리 좋다고 하지만, 본인이 있는 환경에 어울리지 않는다면 사용하지 않는 용기도 가지고 있어야 한다. 좋은 개발자 혹은 테크 리더가 보여줘야 할 태도중에는 항상 최신이 아닌 최선을 위해 최신 기술을 미룰줄도 알아야 하는게 아닐까 싶다.

– 끝 –

Thoughts after reading Start with WHY and Leaders eat last

책(Leaders eat last)에서 인간의 기본 심리를 4가지 호르몬의 동작으로 정의한다.

  • 엔돌핀(Endorphins) – 생존에 대한 두려움 혹은 살아야 한다고 느꼈을 때. 어느 글에서는 이게 좋은 호르몬이라고 이야기를 했던 것 같지만 결국 두려움이 이 호르몬을 만들어낸다.
  • 도파민(Dorphamins) – 성취. 혹은 다른 사람을 이겼다고 느꼈을 때.
  • 새로토닌(Serotonins) – 다른 사람을 돌보고, 이들을 위해 희생하고자 함. 대표적으로 리더 성격을 가진 사람들이 많이 이런 호르몬을 가진다.
  • 옥시토신(Oxytocins) – 타인에 대해 사랑을 베풀고, 그 자체에서 기쁨을 얻는다.

인간이라는 동물로써 차별성을 가지는 가장 큰 부분은 사회성이다. 조직안에 있을 때 편안함을 느끼는 이유는 생존에 대한 두려움을 덜 수 있기 때문이다. 하지만 경쟁 사회에서 우리는 개인주의를 강요받는다. 특히 이런 개인주의의 팽배는 한국의 경우, 1990년대 말 IMF 시대를 거치면서 보편화됐다. 미국의 경우에도 레이건 정부를 거치면서 이런 정책이 일반화됐다. 자본주의의 팽배가 결국 사람을 사람으로 보는 것이 아니라 물건으로 보기 시작하고 하나의 숫자로써 개인을 취급하면서 보편화되었다.

선사시대 이후로 작던 크던 조직내에 소속된 개인을 돌보는 문화가 하나의 숫자로써 여기지는 시대로 변화되었다. 사회의 가치보다는 개인의 성취 혹은 성공이 더 우선시하는 개인주의가 기본 패러다임이 되었다. 이런 변화는 단순히 자본주의 때문이라기보다는 풍요로움이 기여한 바가 크다고 할 수 있다. 1,2차 세계 대전이라는 고난의 시대를 거치면서 세상은 급속하게 풍요의 시대를 맞았다. 한국의 경우에도 한국 전쟁을 거치면서 소위 베이비부머라 불리는 세대가 나타났으며 70년대 베트남 전쟁과 한강의 기적이라는 산업적 변혁을 맞이하면서 더 이상 배고프지 않는 시대에 진입했다. 80년대 90년대를 초반을 거치면서 “개천에서 용나는” 시대를 관통했다. 당시의 부모 세대는 평생 직장이라는 울타리에서 자신의 자녀 세대들은 자신에게는 거친 기억이 없는 풍요로움을 줄 수 있었으며, 또한 “용”이 되어주었으면 하는 희망으로 자녀의 교육에 최선을 다했다. 다른 사람보다는 내가 우선이었으며, 이런 사회 풍토는 미국과 마찬가지로 경쟁 사회로의 진입을 부추켰다. 90년대 IMF 이후로 이런 기조는 더욱 더 남을 돌아보기보다는 자신의 앞을 보고 더욱 더 채찍질 하는 사회를 고착화시켰다.

이런 사회적인 풍토에 제대로 일격을 가한 대표적인 사건이 바로 엔론(Elon)사태고, 2008년 모기지 사태로 촉발된 미국 금융 위기다. 자신들만의 세상(Inner Circle)의 이익을 최우선하기 위해 장부를 조작했으며, 단일 회사 수준이 아니라 누구도 예측못했던 글로벌 위기를 촉발했으며 미국뿐만 아니라 전세계의 많은 사람들이 직업을 잃고 거리를 헤매게 만들었다. 이들 Inner Circle 그룹은 조직의 다른 사람들은 단순히 숫자에 불과했으며, 사람 혹은 동료가 아니라 수단에 불과했던 것이다. 이에 대한 반성이 미국 사회에서는 조금씩 생겨나기 시작했고, 주주우선주의가 아니라 구성원 우선주의가 서서히 자리잡고 있다. Google, Facebook을 포함한 IT 기업들(Amazon은 글쎄???)이 이런 흐름에 동참하고 있고, 가장 대표적인 기업으로 Southwest Airline과 국내에도 있는 Costco가 칭송받고 있다.

한국의 경우에는 어떨까? 기업들의 Moral Hazard로 촉발되어 베이비부머 세대 수십만을 길거리로 내몬 IMF 사태 이후, 과도한 인력 유동성을 확보한 기업들은 더욱 더 자기반성없이 성과지상주의로 개인들을 초 경쟁사회로 내몰고 있다. 이제 도구로써의 역할을 다하지 못한 기업의 40, 50대들은 언제 해고될지 모르는 두려움이 떨고 있다. 생존을 위해서는 어찌되었든 옆자리의 동료을 이겨야하고, 더 좋은 라인을 타기 위해서 사내정치에 몰두한다. 그리고 승리의 보담으로 인센티브라는 단물을 챙긴다. 부모 세대들의 10, 20, 30대 자녀들은 어떨까? 그들은 풍요와 경쟁의 한 가운데에 있다. 언제까지나 피말리는 경쟁을 버텨내는 것은 싫지만 지금까지 누려왔던 풍요로움을 포기하기는 싫다. 결국 그들의 선택지는 공무원 혹은 대기업이다. 부모 세대의 경쟁은 점진적이었다. 이에 반해 이들의 경쟁은 바늘구멍같은 좁은 자리를 향해 경쟁해야한다. 결국 경쟁은 일상회되고, 사회는 피로사회로 전환된다. 스카이캐슬은 그들만의 문제가 아니라 사회의 문제이자 시스템의 문제다.

현재의 자본주의 시대는 인센티브의 시대다. 엔돌핀과 도파민의 시대다. 회사라는 조직에 있는 사람들은 언제 짤릴지 모르는 두려움(엔돌핀)을 안고 있다. 그리고 옆자리에 앉은 동료, 팀, 혹은 다른 회사와 경쟁한다. 그들의 경쟁은 몇 개의 숫자로 정의된다. 그 숫자가 맞춰지면 인센티브(도파민)가 지급된다. 이 숫자를 달성하지 못한 사람은 언제 닥칠지 모르는 상실의 두려움에 떤다. 한국의 대표적인 리더들은 항상 숫자를 우선에 둔다. 매출 목표, 수익 목표. 그 숫자들은 출근길에 쓰러진 부하 직원과 스트레스에 회사를 떠난 직원들을 패배자로 규정시킬 수 있도록 리더에게 당위성을 부여한다. Inner Circle에 충성해야하고, Inner Circle이 지정한 숫자에 충성해야 한다. 고객은 숫자이며, 직원은 숫자를 채우는 수단에 불과하다.

우리는 왜 일하는가? 회사와 같은 인위적인 공동체에 개인은 소속의 이유를 갖는다. 공동체의 리더는 조직의 존재 이유를 대변하고, 이 가치를 실현하기 위해 노력해야 한다. 공통체의 일원인 우리는 그 가치에 동의하고 그 가치를 실현하는데 동참하기 원한다. 하지만 이상은 이상이고, 현실은 현실이다. 그리고 공동체의 가치를 설파하고, 실현시키기 위해 노력한 리더도 대부분 이상론으로 존재할 뿐 현실에서는 찾아보기 정말 어렵다. 세상을 혁신하겠다는 스티브 잡스나 세상사람들에게 컴퓨터라는 동일한 권력을 쥐어주겠다는 빌 게이츠가 이런 대표적인 리더이다. 이들이 현업에 있던 당시에 함께 일했던 애플과 마이크로소프트의 직원들은 아마도 이런 생각에 깊이 동감했을 것 같다. 하지만 지금의 이들이 떠나버린 애플이나 마이크로소스프트에 이런 생각에 동의하면서 일하는 구성원들이 얼마나 될까?

하지만 새롭게 생겨나는 대부분의 회사들이 단순히 돈을 벌기위해서 만들어지는 건 아니다. 나름대로 그들이 정의하는 공동체를 위한 목적을 가지고 있으며, 회사의 구성원들 모두 그 목적을 실현하기 위해 다들 노력하고 있다. (혹은 노력하고 있다고 믿고 싶다.) 현실론으로 단순히 돈을 벌기 위해 그 안에 있다는 것 자체는 너무 슬프지 않은가?

이 관점에서 나는 어떤 목표와 목적을 가지고 왜 아침에 일터로 향하는가? 내가 현재의 회사에 조인한 이유는 명확하다.

고객들에게 의미있는 서비스를 만들고 싶고, 그 서비스를 통해 고객들과 공감하고 싶다. 

나는 여전히 이 목표에 충실한가 라는 질문에 지금까지는 “예” 라고 답할 수 있다. 그리고 최근에 이야기된 회사의 가치 역시 이에 부합한다고 생각한다.

 

Player experience first
Dare to dream
Thrive together
Execute with excellence
Stay humble; stay hungry

 

숫자 혹은 성과를 위해 하는 것이 아닌 것은 분명하다.

개발을 포함해 모든 일이 마찬가지지만 개인 혼자서 할 수 있는 일이 아니고 팀 혹은 구성원들과 함께 하는 공동의 작업이다. 따라서 개발의 리더로써 내가 해야할 일은 명확하다. 조직의 목표와 비전을 명확하게 함께하는 동료들에게 인지시켜야 한다. 함께 일을 하는데 있어서 동감은 반드시 전제되어야 한다. 동감되지 않는 리더의 비전은 그 사람의 “꿈”에 지나지 않는다. 꿈이 아니라 현실에서 이를 실현하기 위해서는 구성원들의 지원이 필요하며, 이를 위해 동감은 반드시 필요하다.

동감을 통해 우리가 추구해야할 가치는 구성원간의 신뢰다. 공통체가 공동체로써 존재할 수 있는 가장 큰 근간은 신뢰 혹은 상대방에 대한 이해다. 신뢰가 무너지면 조직의 근간이 흔들린다. 사람의 진실성(Integrity)에 대해 의문을 품는 순간, 신뢰에는 금이 간다. 신뢰를 만들어나가는 과정은 쉽지않다. 하지만 무너지는 것은 일순간이다. 특히 요즘과 같이 카톡이나 페이스북 메신저가 일상화된 현실에서 온라인 상에서만 이뤄지는 대화는 우리가 생각하는 신뢰라는 것에 대한 착각을 불러일으키기 십상이다. 텍스트보다는 얼굴을 보고 이야기할 때 오히려 사람사이의 진실함을 파악할 수 있다. 특히나 같은 공간을 함께하는 동료라면 그 사람의 책상으로 가서 이야기를 하는 것이 훨씬 더 효과적이다. 밀레니엄 세대는 이런 오프라인 대화를 부담스러워한다고 이야기를 하지만 자연스럽게 얼굴보고 이야기할 수 있는 기회를 만들고 활용할 수 있도록 하는 것이 필요하다.

비전을 통해 나아가야 할 방향이 정해지고, 이에 대한 공감과 같이 하는 동료간의 신뢰가 바탕된다면 이제 여정을 떠날 수 있다. 이 여정에는 가장 필요한 요소는 헌신(옥시토신)과 배려(세로토닌)이다. 여정을 통해 실현해나가는 과정에서 팀과 개인은 그에 따르는 성취를 이뤄야 한다. 물론 성취의 기본 개념은 일을 완성해나가고 있다는 것이다. 하지만 다른 관점에서 성취의 또 다른 면으로 중요시하는 것은 배움이다. 과정이 항상 성공적일 수만은 없다. 여정에서 잘못된 길로 들어설 수 있으며, 이 과정에서 왔던 길을 되돌아 나와야 하는 경우도 있다. 이 과정에서 배움이 있었다면 이 또한 성취의 또 다른 면이라고 할 수 있다. 이런 경험들이 모두 모여 우리가 이루고자하는 비전을 달성할 수 있다.

성취는 개인의 성장을 위해 반드시 필요하고 이럴 때 느끼는 도파민은 값지다. 어려움을 극복하거나 새로운 배움을 통한 성취는 개인의 성장에 큰 도움이 된다. 그리고 이런 도움이 팀의 작업에 도움이 되고, 다른 사람들의 성장을 자극한다면 더할 나위없다. 하지만 성취가 성과라는 단어와 만났을 때, 그리고 그것을 개인 혼자만의 것으로 만들려고 하는 경우에는 큰 문제가 된다. 이런 사람이 많아지면 많아질 수록 “우리” 보다는 “나” 가 우선시된다. 특히나 이런 분위기를 리더가 조장하거나 방임하게 되면 구성원들은 단기 성과에만 매달린다. 시작은 도파민이었지만, 이후에는 엔돌핀이 구성원들을 지배한다.

회사 혹은 조직의 문화에 따라 이런 도파민과 엔돌핀을 무기로 구성원들을 다루는 경우가 있다. 소위 당근과 채찍이라는 이름으로 성과를 강요한다. 이러면 구성원들은 위기감을 느끼고 뭔가를 보여줘야만 스스로의 생존을 담보할 수 있다고 느끼게 된다. 이건 사람이라는 동물적인 유전자를 가지고 있기 때문이지 개개인이 잘못이 아니다. 하지만 이런 병폐가 지속되면 자본의 논리가 조직의 논리가 된다. 사람을 물질화시키고, 투입 대비 효율이라는 가치 명제가 횡횡하게 된다. 구성원이 더 이상 그 효용이 다하면 버려도 되는 재화(Commodity)가 되어 버린다.

조직의 리더가 어떤 방식으로 조직을 이끌고, 이에 구성원들이 동의하는가에 따라 그 조직의 문화가 결정된다. 자본의 논리에 따른 각자 도생의 문화를 가질 것인지 서로 뒤를 봐주는 공생의 문화를 가질 것인지가 결정된다. 조직장의 스타일이 큰 역할을 담당하겠지만, 이를 실행하기 위한 구성원들이 이에 동감하고 함께하는지도 중요한 역할이다. 결론적으로 내가 추구하는 조직은서로가 서로를 보살피는 문화를 가진 조직이다. 우리는 우리가 해야할 목표가 있고, 이걸 달성하고 성장하면서 오래 함께 할 수 있는 조직을 이루고 싶기 때문이다.

중간 리더는 상당히 어정쩡한 존재다. 모든 것을 결정할 수 있는 충분한 권한(Authority)가 있는 것이 아니고 책임을 지고 싶어도 질 수 있는 위치가 아닌 경우가 태반이다. 결정을 해야할 때 빠른 결정을 내리는 것도 좋은 자질 가운데 하나일 수 있다. 하지만 한국 조직은 수직, 계층 조직이다. 각 조직별로 나름의 사일로(Silo)를 가지고 있으며, 이 영역을 영역밖의 사람이 들어갈려고 했을 때 받는 이물감은 상당하다. 인위적으로 이런 형국을 초래하기보다는 중간 지점에서 접점을 만들고 서로 얼굴 맞대고 최선의 결론이 도출될 수 있도록 기회를 만드는 것이 중간계의 리더가 해야 할 일이 아닐까 싶다. 그럼에도 결론이 안된다면 사일로의 최상단이 서로 논의할 수 있는 관련된 정보를 제공하는 수준에서 마무리하는 것이 최선이 아닐까?

궁극적으로 구성원들 개개인이 심리적으로 Circle of Safey 안에 존재한다는 믿음이 생기도록 만들어야 한다. 준다고 해서 덥석 생겨나는 것도 아니다. 한두달 열심히 한다고 해서 생겨나는 것도 아니다.일상을 통해 체감될 수 있도록 리더와 구성원들이 서로 노력해야한다. 그 결과가 문화로 정착되야 한다. 결론은 문화다.

– 끝 –

 

개발자에게 좋은 직장 혹은 좋은 환경

직업이 뭐냐고 물어보면 “개발자”라고 서슴없이 이야기한다. 개발하는 직장인으로써 “행복하십니까?” 라고 질문한다면 나의 답은 “행복합니다.” 이다. 하지만 “행복”이라는 단어에 고민이 있다. 나는 직장인으로써 행복한 것인지 아니면 개발자로써 행복한 것인지. 혹은 둘다에서 모두 만족과 행복을 얻고 있는 것인지.

전 직장인 네이버에서 일할때도 초반에는 이런 행복이라는 단어를 이야기했다. 그때도 개발자로 시작을 했지만, 성과를 인정받고 일을 리딩하는 팀장이 됐다. 리더는 이럴 것이다라는 나의 자화상과 팀원들의 기대를 섞은 형상이 되기 위해 최선을 다했다. 결론적으로 행복하지 못했다. 팀장, 리더라는 직책을 가진 직장인으로 행복하지 못했고, 개발할 시간을 1도 갖지 못한 개발자로써 행복하지 못했다. 더욱 심각한 문제는 나만 행복하지 못한게 아니라 함께 일하는 팀원들도 행복하지 못했다. 그리고 3년 반전에 현재의 라이엇 게임즈로 이직했다.

이직 후 현재까지 개발자로써 행복하다. 물론 라이엇 게임즈에서도 개발 리더의 역할을 하고 있다. 그리고 앞서 이야기한 것처럼 행복하다. 이 나이에 실제 코딩을 하고 있고, 과정에서 미처 몰랐던 것들, 새로운 것들을 계속 배우는 즐거움이 있다. 그리고 나이먹었다고 굳이 봐주지 않는 까칠한 동료들이 있다. 나는 내 기술과 경험과 코딩으로 사용자들이 실제 사용하는 시스템을 만들고 있고, 좋은 동료들의 도움으로 발전하고 있다. 더구나 새로움에 도전한다고 뭐라하는 말도안되는 거버넌스 같은 것들이 없다. 개발자에게 이만큰 좋은 환경이 있을까? 이렇든 저렇든 코딩을 직접하는 회사에서 이만큼의 행복한 조건을 찾을 수 있을까? 내가 보기에 감히 개발자에게 최고의 환경이다.

라이엇이 좋다고는 했지만 그럼 개발자에게 행복한 직장 환경은 뭘까?

개발이라는 직군(Discipline) 관점에서 살펴보자.

  • 능력이 아닌 실제 코딩 – 어떤 직책(Role)에 있던 개발 직군에 있는 사람은 코딩을 해야한다. 중요한 점은 할줄 아는 능력이 아니다. 실제로 코딩을 해야한다. 자, 먼저 여러분들의 주변을 둘러보자. 코딩보다는 문서를 만들고 있거나, 회의를 하고 있는건 아닌가? 혹은 의미없는 이메일 놀이? 본인의 시간을 코딩하는데 쓸 수 있어야 코딩을 할 수 있다. 그리고 코드를 통해 본인의 존재감을 인정해줄 수 있는 조직 문화가 있어야 한다. 회의에서 한마디 말을 하거나 문서/이메일 쓰레드에 자신의 필력을 발휘해야만 존재감을 인정받는다는 생각을 하는 소위 관료주의적 문화가 있다. 이런 문화는 개발자가 코드로 기여하기보다는 정치질하기 쉽다. 코드짤 시간도 인정하지 않고, 회의에서 말 한마디 못한다고 갈구는 분위기이고, 본인은 개발자로 남길 원한다면 그 조직에 오래 남을 이유는 없을 것 같다.
  • 피드백 줄 수 있는 동료 – 코딩은 글쓰기랑 비슷하다. 글쓰기 능력을 높이는 좋은 방법은 2가지 다. 첫번째는 많이 써보는 것이고, 두번째는 쓴 글을 다른 사람들과 돌려 읽는 것이다. 다른 관점에서 살펴본 피드백은 좋든 안좋든 본인이 작성한 내용을 뒤돌아보게 만든다. 코딩도 마찬가지로 피드백이 있을 때 더 빠르게 발전할 수 있다. 코딩의 피드백을 가장 잘 줄 수 있는 사람은 같은 일을 하는 개발자다. 같은 일을 하고 있기 때문에 코드의 목적을 공유하고 있고, 다른 관점에서 제대로 코드의 문맥이 제대로 읽히는지 봐줄 수 있다. 개발 시간은 3개월이더라도 완성된 코드는 6개월, 1년 혹은 몇년 동안 운영된다. 과정에서 변경은 필수이고, 변경할려면 제대로 읽혀야 한다. 제대로 읽히는지 피드백을 주는 동료가 있다면 그만큼 읽힐 수 있는 코드, 품질 높은 코드를 작성할 기회가 더 많아진다. 자신보다 잘하든 못하든 상관없이 진심으로 읽고 주는 피드백은 충분히 값어치를 한다.
  • 도전 – IT 환경만큼 빠르게 변화하는 세상이 없다. 개발 환경 뿐만 아니라 사용자 환경도 마찬가지다. A 기능이면 만족하던 사용자들이 AAAAA 기능이 아니면 안된다고 하루 아침에 돌변한다. 바뀐 환경에 대응할려면, 본인이 가지고 있던 밥그릇을 버려야 할 때도 있다. 어떻게 해야할까? 개발자라면 도전해야한다. 밥그릇이 아닌 접시를 원한다면 이제 접시를 만들러 가자. 새로움에 도전하는 사람이 있다면 응원과 관심이 필요하다. 성공하든 깨지든 과정의 경험은 도전한 사람에게 값진 경험으로 남는다. 도전이 일상이 될 때 혁신이 이뤄지고, 조직은 더욱 발전한다. 개인의 도전을 장려하고, 그 과정 혹은 결과를 조직 발전의 밑거름으로 삼으려는 조직에 있어야 개발자로써 더 나아갈 수 있다.
  • 성장 – 개발이라는 분야만큼 빠르게 변하는 동네는 없다. 자고나면 새로운 기술과 언어 그리고 프레임웍이 등장한다. 한국에서는 자바(Java)랑 스프링만 해도 충분해… 라는 생각을 가진 개발자들이 현실적으로 많다. 사실 그닥 틀린 말은 아니다. 하지만 이런 마인드가 ActiveX를 욕하면서 아직도 걷어내지 못하고 있는 것이다. Frontend와 Backend의 분리, Microservice Architecture, Cloud 등등이 최신 기술이라고 이야기하던 때는 이미 1~2년 전에 지났다. 프로토콜 관점에서도 세상은 이미 HTTP/2를 향해 나아가고 있고, Serverless와 AI, Machine Learning을 현실과 어떻게 접목할 것인가를 고민하는 시점이다. Java 언어 자체도 Imperative feature보다는 lambda를 필두로 functional language 관점이 주요 발전 뱡향으로 자리잡았다. 또 9 버전 이후 module 방식의 packaging 방식의 변화를 가져오고 있다. 이것들을 종합해보면 Concurrent 환경의 안전한 코딩과 빠른 실행을 담보하기 위한 체계로 전환해야 한다고 이야기하고 있다. 그럼에도 불구하고 당신의 조직이 “이걸로도 충분해” 라고 안주하고 있나? 성장하는 조직이라면 새로움에 대한 탐색과 이를 도전적으로 적용해보고 그 값어치에 대해 논의할 수 있는 것을 장려해야 한다. 이런 논의가 없다면 고인물이 되고, 시간이 흐르면 썩어버린다. 썩은 물은 두말할 필요없이 주변을 오염시킨다.

팀장이나 아키텍트라는 타이틀을 가진 사람들이 흔히 “내가 옛날에 해봤는데 말이야…” 라는 말을 내뱉는다. 옛날 언제? CPU가 1 Core였던 호랑이 담배피던 시절에? 혹은 “오라클 DB면 다 되네..” 라고 감탄하던 시절에? 바뀐 환경과 바뀐 프로그래밍 패러다임을 이해하지 못하는 사람과 함께 일하는 건 불행이다. 넓은 식견과 경험을 변화된 환경에 최적화된 코딩으로 녹여내기 위해 노력해야한다.

우리는 이런 노력이 결실을 맺기 위해 피드백을 주고, 시간을 줄 수 있어야 한다. 이 과정을 통해 성장한 사람이 결국 조직 구성원들에 영감을 준다. 이런 한 사람 한 사람의 노력이 모여 개발자 조직의 성장을 이끈다.

개발자라는 직장인 관점에서 행복한 직장 환경은 뭘까?

Good

  • 실행 가능한 비전과 목표 – 뜬구름잡는 듯한 구호는 의미가 없다. 비전과 목표는 실행 가능한 아젠다를 도출할 수 있어야 하거나, 무엇을 실행할지에 대한 지침이 되어야 한다.
  • 공유 – 무언가를 실행할 때 기술적이든 업무적이든 정보가 필요하다. 이런 정보가 투명하게 공개되고, 원할 때 찾아볼 수 있는 형태로 존재해야 한다. 그리고 다른 사람들에게도 이런 정보가 있음을 알려줘야 한다. 그래야 혼선을 최소화할 수 있고, 빠른 업무 진행이 가능하다.
  • 성취와 축하(Celebration) – 어떤 일이 됐든 방점이 없는 일은 사람을 지치게 만든다. 반드시 우리가 이 목표를 달성했다라는 성취감을 느낄 수 있어야 한다. 이 성취감을 통해 다음번 고지를 향해 나아갈 수 있는 자신감을 얻을 수 있다. 그리고 그 성취를 모두가 함께 축해해줘야 한다. 개인의 성취보다는 우리 혹은 팀의 성취가 되어야 하고, 그 성취를 조직의 모든 사람들이 다 같이 축하해줄 수 있어야 한다. 이런 시점에 회식은 꿀맛이다.
  • 실패 – 2보 전진을 위한 1보 후퇴다. 누구나 실패할 수 있다. 하지만 실패를 통해 배운 교훈이 있고, 이 교훈이 다음번 일을 하는 발판이 되어야 한다. 가장 어이없는 건 실패를 실패 자체로 비난하는 것이다. 이러면 누구도 두려워 일을 못한다. 아마도 성공할만한 일만 골라서 하지 않을까? 실패에서 팀이 어떤 Lesson and Learn이 있었는지, 그리고 같은 실수를 두번 되풀이하지 않기 위해 어떤 방식으로 다음번 일을 준비하는지를 봐야한다.

Bad

  • 경쟁 혹은 사내 정치 – 누구를 위한 경쟁인가? 아이러니컬하게도 사람은 3명 이상만 모이면 정치질이라는 걸 하게 되있다. 성과 중심적인 직장내에서는 특히나 이런 정치질이 심각하다. 내가 남보다 잘해서 성과를 받기 보다는 남을 깍아내려 상대적인 우위를 점하기 위해 이런 짓을 하는 경우가 심심치않다. 일에 대한 값어치는 “고객, 동료”들에게 평가받아야 하지만, 의도적인 깍아내림은 조직을 좀먹는다.
  • 단기성 금전적 인센티브 – 성과에 대한 보상으로 인센티를 왕창주면 어떻게 될까? 그리고 다음번 평가시에 이만큼의 인센티브를 받지 못하게 되면 어떻게 생각할까? 그리고 왕창 인센티브를 받지 못한 다른 팀 혹은 구성원은 어떤 생각을 가지게 될까? 결국 인센티브를 받을만한 일들만 골라서 구성원들이 할려고 든다. 왕창받지 못한 사람은 저성과자라는 인식을 스스로 하게 되거나, 혹은 성과 불평등에 대해 이야기하기 시작한다. 그리고 자신만이 높은 성과를 받기 위해 인위적으로 정보를 숨기거나 협업을 말 그대로 방해한다.
  • 회의(Meeting) – 회의는 말 그대로 여러 사람이 같이 머리를 맞대고 논의하기 위한 자리이다. 그리고 그 자리에선 대부분 결론이 도출되야 한다. 하지만 본인의 존재감을 드러내기 위해 회의에 들어온다. 하지만 그닥 참여한 의미는 없다. 되려 회의 시간만 길어진다. 최악의 회의 결론은 다음번 회의를 하자라는 것이다. 이런 회의가 비일비재하다. 회의가 소집되었다면 회의록이 있어야 하고 결론이 도출되어야 한다. 그리고 관심있는 사람들에게 회의록이 공유되면 된다. 본인의 소중한 1시간을 허비할 필요는 없이, 관심있다면 회의록을 살펴보면 된다.

불행한 직장인은 누구일까? 아마도 매일 매순간을 성과에 얽매여 두려움에 사는게 아닐까 싶다. 성과라는 것을 아예 고민하지 않을 수는 없다. 개인적인 주관이지만 개발직군에 있는 직장인으로써 이런 성과에 직접적으로 연연하기보다는 본인의 성장이 성과와 자연스럽게 연결될 수 있는게 최선이 아닐까 싶다.

본인 성장을 이룬 다는건 주변의 협조없이는 불가능하다. 그리고 조직내에서 이런 협조는 조직의 문화와 직접적으로 관련되어 있다. 그리고 궁긍적으로 이런 문화가 유지되고 발전되는건 그만큼 조직장 혹은 리더의 역할이 중요하다. 다행스럽게도 라이엇게임즈 코리아에는 이를 강력하게 지원해주는 리더십이 있다. 물론 이런 리더십을 뒷배경으로 각 개인이 열심히 하는 부분들이 있기 때문에 문화가 유지 발전되는게 아닐까 싶다.

새로운 2019년이 시작되었고, 앞으로도 기술 및 개발 조직 문화가 발전될 수 있는 환경이 되도록 일조해야겠다.

Kafka broker memory leak in 0.10.x version

Kafka 클러스터를 한국 개발팀에서 운영한지도 한 2년 넘은 것 같다. 메시징 시스템이라고 하면 뭔가 대단한 것 같았는데, 실제로 시스템을 디자인하고 운영하다보니 별거 없더라는… 라고 뭉개고 싶지만 사실 숨기고 싶은 진실이 하나 있었다.

개발 과정에서는 이 문제를 찾을 수 없었는데, 운영을 하면서 나타난 문제점이 있었다. 카프카라는 메시지 큐가 실제로 Business Logic이라는 걸 처리하는게 없다. 또 저장하는 데이터의 보관 주기 역시 그닥 크지 않다. 그런데… 왜… 아무리 Heap Memory의 크기를 늘려줘도 Full GC 이후에 카프카의 Java 어플리케이션의 메모리가 반환되지 않고 CPU는 미친듯이 날뛸까? 최초에는 넘 메모리를 적게 줘서 발생한 문제점인가 싶어서 메모리를 2G부터 9G까지 꾸준히 늘려갔다. 재시작 후 OOM을 외치는 시점이 좀 늘어나긴 했지만, 변함없이 찾아온다. 별수 없이 매번  Kafka Broker Application을 내렸다가 올려야 한다. 올리면 잘 돌아간다. 하지만 매번 해야한다. 시스템은 돌아가야 하니까…

해외 Conference에서 들었던 사례들은 무시무시한 수량의 노드들을 운영하고, 아무것도 거칠것이 없었다. 하지만 내가 만든 이 간단한 물건은 뭐가 문제길래 매번 CPU 혹은 Health Check Alarm을 걸어두고 알람이 올때마다 매번 내렸다가 올리는 작업들을 해야만 하는걸까? Kafka에 데이터를 보내고 받아오는 코드들을 이리저리 살펴봤지만 그닥 큰 문제점들이 보이지는 않는다. 그럼에도 OOM의 Root Cause를 확인할 방법이 뾰족히 보이지 않는다. 결국 취할 수 있는 방법은???

별 수 있나. 하루에 한번씩 내렸다가 올리는게 가장 안전한 방법이다! 쪽팔리지만 내가 작성한 코드 수준에서 해결할 수 없는 방법이지만 운영은 되어야 하기 때문에 이 방법을 취했다.

그리고 2주 전까지는 잘 살아왔다. 2주 전쯤에 보안 강화를 위해 기존에 데이터를 받던 방법을 개선했다. 개선하면서 제대로 데이터가 들어오게 됐다. 와우~~

그런데 데이터가 많이 들어와도 너무 많이 들어온다. 정말 너무 많이 들어왔다. ㅠㅠ 하루에 한번씩 OOM을 내던 놈이 이제는 24시간을 버티질 못하네. 이런… 이제 12시간마다 한번씩 내렸다 올려야 하나?? 완전 닭짓인데, 이게 운영인가? 근데 데이터가 너무 많이 들어는데 이 데이터를 살펴봐야하지 않을까? 따지고 보니 데이터를 수집하는 시스템을 잘못 이해하는 바람에 데이터가 비정상적으로 많이 들어왔다. 이 부분을 정리하고 났더니 전반적인 데이터 처리는 줄었지만 여전히 이전에 비해서 처리하는 데이터는 확연히 많다. 여전히 24시간을 버티지는 못한다. 역시 같은 질문! 이게 운영인가?

Kafka broker application이 돌아가는 EC2 Instance를 높은 사양으로 변경도 해보고, broker configuration도 이리저리 바꿔봐도 약간의 차이는 있지만 OOM은 매번 발생한다. 이럴리가 없는데…

그래 이건 Kafka Bug다. 사용하던 버전이 0.10.1.0 이었다. 0.10.X 버전에서 발생하는 메모리 관련된 내용들을 구글링해보니 이것저것 많이 나온다. 이런 제길…  그래 버그가 있긴하구나.

(PP-1841) Kafka memory leak

Kafka 버전을 살펴보니 어느새 2.1.0 버전이다. 내가 시스템을 셋업할때는 최신 버전이 0.10.X 버전이었는데 2년 사이에 엄청 발전한 모양이다. 사이에 한번 나도 버전업을 하긴 했지만 운영에 신경을 많이 쓰지 못한 건 맞는 사실이다. 뭐 하루에 한번씩 재시작시키는 걸로 운영을 퉁쳤으니까 할말 다했다. 🙁

기존 버전으로 최신 버전인 2.1.0 버전으로 올렸다. 재시작시키는 Cron Job을 없애고 몇일 두고 봤다. 이쁜 그림을 그리면서 잘 돌아간다. 관리를 제대로 안하고 툴을 욕했던 내가 한참을 잘못했다.

이제 자잘한 말 그래도의 운영 이슈가 남아있지만, 이제는 비겁한 재시작은 없다. 2년이라는 시간이 걸리긴 했지만, 그럼에도 불구하고 제대로 시스템이 운영되기 시작하니까 마음이 한결 놓인다.

Spring 5 reactive programming ground zero

Spring framework에서도 5.X 버전부터 Reactive 방식의 프로그래밍이 가능하다. 이게 한 1년 이상 전 이야기인 것 같다. 내 입장에서 좋기는 한데 이게 그림의 떡이었다. 대부분의 Java Backend 개발을 Springboot framework을 가지고 하고 있는데, 여기에 Spring framework만 5.X 버전으로 덜렁 넣을 수 없기 때문이다. Spring 5.X 버전을 지원하기 위해 2.X 버전이 개발중에 있었지만, Milestone 버전이었고, 옆에서 Early Adopter 기질을 가진 친구가 고생하는 모습을 보니 아직은 때가 아닌 것 같았다. 아직은 스프링부트 1.X 버전이나 잘 쓰자 생각했다.

한 반년 사이에 Springboot 2.X 정식 버전이 릴리즈됐다. 하지만 바쁘다는, 2.X 버전으로 부트 버전만 변경하는게 별 의미없다는 핑계들을 가져다대며 미적댔다. 그 사이에 다른 친구들은 프로젝트에 Spring Reactive를 Streaming 방식으로 적용해서 성공적으로 마무리를 시켰다. 사람들에게 이제 Reactive의 시대니까 백엔드도 이 방식으로 개발을 해야한다고 떠들고 다녔다. 어이없게도 정작 내가 개념을 입으로만 떠들고 아는체하고 있는게 아닌가? 가장 싫어하는 짓을 내가 하고 있으면서 그걸 모른체하고 있다는게 어이없긴 하다.

출장중에 잠못드는 밤이 많고, 그 시간에 꾸역꾸역 억지로 잠을 청하느니 차라리 밀린 숙제나 해야겠다라는 심정으로 자료를 찾아봤다. 간단한 몇번의 구글링만으로도 뭔가를 시작해볼 수 있는 자료가 화면을 가득 채운다. 나만의 썰 몇 가지를 주절여보고, 검색 결과로 나온 아주 괜찮은 몇가지를 추려봤다.

Reactive Programming의 개념부터 정리해보자.

단어적인 의미를 직역하면 “반응형” 프로그래밍이다. 반응한다라는 것으로 어떤 의미일까? 주체적으로 동작하는 것이 아니라 외부 요인에 의해서 동작이 실행된다라는 것을 의미한다. Frontend 환경에서는 이런 반응형 프로그램이 ReactJS와 같은 Javascript framework의 발전과 더불어 보편적으로 채택되고 있다. UI 환경의 동작을 실행시키는 주된 요인은 사용자의 키보드 입력 혹은 마우스 클릭과 같은 이벤트와 Backend 서버에서 보낸 데이터의 “도착” 같은 것들이 대표적이다. 이런 요인들을 Browser 혹은 Framework이 Event/Promise와 같은 형태로 Trigger 시키고, 이를 Listening하는 개발자의 코드가 실행되는 모델이다.

Backend 환경은 User event와 같은 것들이 없다. 다만 IO를 중심으로 데이터가 “반응”을 촉발시키는 매체가 된다. Synchronous 환경은 직렬화된 데이터 처리를 강요한다. 반응형이 될려면 당연히 Asynchronous IO 기반의 데이터 처리가 기본이 되어야 한다. 사실 컴퓨터라는 것 자체가 Asynchronous하게 동작되는 물건인데, 개발자가 편하라고 Synchrnonous progrocessing을 지원했던 건데 세월이 지나보니 다시 과거의 개념으로 회귀한 것이다. 뭐 물론 다른 차원의 이야기이지만.

스프링의 언어 기반인 자바는 최초에 Synchronous IO만 지원했다. 그러다 Java 1.4 시점에 NIO라는 컨셉으로 Asynchronous IO를 지원하기 시작했다. 내가 Open Manager 2.0 버전을 설계하던 즈음에 이거 나온거보고 이거다 싶어서 작업을 했던 기억이 아련하다. Async IO의 장점은 데이터를 읽어들이는데 있다. Sync IO의 경우 자신이 지정한 데이터가 도착할때까지 무작정 기다려야한다. 반면에 Async는 데이터가 도착했는지를 확인하고, 도착하지 않았다면 그 사이에 다른 작업을 수행하면 된다. 원래 IO 처리 대상이 되는 socket은 Duplex Channel 방식을 지원한다. 즉 한 socket의 FD값을 알면, 두 개 이상의 쓰레드에서 읽고, 쓰기를 동시에 할 수 있다. 그리고 Async IO는 이와 같은 duplex channel 방식의 통신이 지원되는 기반에서 동작한다.

통상 이런 일련의 흐름을 개발자가 모두 코딩하기 어렵다. 그래서 필요한 것이 일종의 엔진이다. 엔진은 데이터가 도착했는지 확인하고, 도착한 데이터를 요청한 대상(Subscriber or Listener)에게 데이터를 넘겨 처리한다. 간단히 설명했지만, 이를 실제로 동작하게 하기 위해 Thread Pool, Queue, Publish and Subscribe 등이 기본적으로 갖춰져야한다.(2004년에 이걸 다 처음부터 끝까지 구현했다라는게 놀랍기는 하다.)

Spring framework 5.0 / Springboot 2.0 버전은 위와 같은 실행 모델을 지원하기 위한 전체 틀을 지원한다. 여기에는 실제 작업을 수행하는 Web Controller 수준의 작업 정의(Get/Post/Put… Mapper)와 내부의 데이터 수행을 위한 Mono/Flux와 같은 이벤트 방식의 실행 모델 및 이를 지원하기 위한 API 집합등을 포함한다. 또한 기반 웹서버와의 통합도 아주 중요하다.

Servlet이 특정 쓰레드에 의해 단독으로 처리되는 방식이 아닌 이벤트 방식으로 동작되어야 최적의 성능을 낼 수 있기 때문이다. 때문에 Netty를 쓰라고 권고하는 것이고, 톰캣은 너무 덩치가 커서 이 방식으로 전환하기에는 적절하지 않아서 동작되도록만 맞추고 최적화는 Give Up 한 것으로 보인다.

Googling…

기초 개념을 알면 이제 뭘 해야할까? 개발자라면 가끔씩 무턱대고 코드를 짜보는 것도 나쁘지는 않다. 그래도 무작정하는 것보다는 잘 파악한다음에 하는 성격을 가진 분들도 있다. 그런 분들에게 도움이 될지 모르겠지만 찾아본 자료들 가운데 Wow 했던 자료들 몇개를 링크한다.

  • Servlet or Reactive Stacks: The Choice is Yours. Oh No… The Choice is Mine! – Rossen Stoyanchev – 2017년 SpringOne 발표된 자료인데, 시류가 Reactive라고 무조건 MVC(Sync)를 버리고 Reactive 방식을 따르지 말라고 충고한다. Pivotal에서 Reactive를 밀고 있는 사람인데 약파는 말이 아니라 쓰는 사람 관점에서 이야기를 한다. Awesome!! 물론 더 다양한 팁과 사례도 소개된다. 1시간 10분 남짓 분량인데, 조곤조곤 이야기를 설명을 참 잘한다.
  • Web on Reactive Stack – 스프링쪽에서 만든 WebFlux의 How-To 문서이다. Spring MVC 모델에 대한 개념이 있다면, 이 문서를 읽어보는 것만으로도 뛰어다닐 수 있을 것이다.
  • Mono and/or Flux – Spring Reactive Programming을 하기 위해 제공되는 객체 모델은 Mono와 Flux로 나뉜다. 이름이 의미하듯이 Mono는 1회성 데이터의 Reactive model을 위해 사용되고, Flux는 Streaming 방식의 Reactive Model을 위해 사용된다. Javadoc 문서인줄 알았는데, 비동기적 데이터의 흐름이 어떻게 되는지 제공되는 method 별로 그림을 곁들여 아주 잘 설명하고 있다. 읽어보는 것만으로도 개념을 이해하는데 많은 도움이 된다.
  • Which operator – Mono/Flux를 사용해서 개발을 하더라도 Fully Reactive하게 코드를 작성하는게 만만하지는 않다. 이미 Synchronous 환경에 생각이 굳어져있어서, 특정 상황에 어떤 method를 쓰는게 좋을지 까리까리한 경우가 종종 발생한다.  이럴 때 이 문서를 참고하면 상황별로 어떻게 Reactive task를 시작하는게 좋을지 혹은 병렬로 처리된 2개의 Reactive Task를 Merge 시킬지에 대한 아이디어를 얻을 수 있다.

 

Springboot 2.X에서 Reactive project setup

오래 해보지는 않았지만, 가장 좋은 설정 조합은 WebFlux만을 사용해서 프로젝트를 셋팅하는 것이다. 이렇게 결심했다면 다음의 설정으로 처음 시작을 해보는게 좋다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>

물론 원한다면 spring-boot-starter-web artifact를 추가할 수 있다. 이려면 MVC 방식과 Reactive 방식 모두를 사용해서 코드 작업을 해볼 수 있다. 단점은 MVC 방식이 훨 쓉기 때문에 원래 할려고 했던 Reactive를 금새 포기하게 될거라는거. 이왕 할려면 맘을 독하게 먹는게 좋지 않을까? 한가지 덧붙힌다면 starter-web artifact를 사용하는 경우, 기반 어플리케이션 서버가 Tomcat 이라는 점. 위의 구글링한 결과 가운데 첫번째 동영상 링크를 봤다면 알겠지만, 톰캣은 Servlet 요청을 Synchronous 방식으로 구현했다. 돌려 말하면 요청의 시작부터 끝까지를 완전 Reactive 방식으로 처리가 안된다. WebFlux 단독으로 사용하면 Servlet 스펙을 Asynchronous하게 구현한 Netty가 기반 어플리케이션 서버가 된다. Full Async 혹은 Reactive 방식으로 구현이 가능하다. 물론 starter-web artifact를 사용하더라도 설정을 추가로 잡으면 Netty를 쓸 수 있다. 하지만 할거면 제대로 해보라는게 충고 아닌 충고다. 영역한 동물은 쉬운 길이 있으면 굳이 어려운 길을 고집하지 않고 쉬운 곳으로 방향을 잡기 마련이다.

Spring reactive programming에서 아래와 같은 2가지 실행 모델을 지원한다.

  • Mono – 단일 값에 대한 처리
  • Flux – 서로 독립적인 복수 값들을 처리. 스트리밍 방식으로 떼이터를 처리할 때 주로 사용한다. 스트리밍 방식의 개념은 대강 할지만 여기에서 썰을 풀정도는 아니라서 스킵!

설명을 위해 이제부터는 Mono를 가지고 계속 이야기를 하겠다. 그림과 같이 Mono는 Flow를 가진다. Reactive programming이란 우리는 흐르는 과정에서 실행되길 원하는 코드를 lambda를 거치게 만드는거다. That’s it. 간단히 보자면 간단하다. 약간 어려운 부분들이 있다면, 대강 아래 같은 것들이지 않을까 싶다.

  • Mono가 가지는 Value object의 immutable refernece를 갖는다. 중요한 점은 reference가 immutable이라는거지 value object의 state(or value)는 변경 가능하다라는 점이다.
  • map() 혹은 비슷한 함수를 사용해 다른 타입의 Value object를 만들었다면, 그 객체에 대한 새로운 Mono가 만들어지고, 새로운 흐름이 만들어진다. 대부분의 경우에는 하나의 흐름을 상대하기 때문에 굳이 이런저런 걱정을 할 필요는 없다. 그리고 다른 흐름이 생겼다고, 원래 있던 흐름을 어케 정리하거나 할 필요는 없다. 이게 쓰레드나 파일과 같이 시스템의 리소스를 잡아먹는 그런 어마무시한 놈들은 아니라서.
  • 여러 Mono들의 값들을 한꺼번에 다뤄야하는 경우가 있다. Mono는 흐름이지 직접적인 실행은 쓰레드 풀에 있는 언놈일지도 모르는 쓰레드가 담당한다. 기대 결과를 기다리는 Mono들이 준비가 된 상태인지를 확인하고, 그 상황에서 우리가 지정한 function이 실행되기 위해 zip() 혹은 when()과 비슷한 기능들을 활용할 수 있다.
  • Java 개발자들이 lambda를 좋아한다고 생각안해서 그런지 모르겠지만, BiFunction() 혹은 TriFunction() 같은걸 쓰는 경우가 종종 발생한다. 자바 언어가 Strong typed language이기 때문이라고 추측을 해보지만, 무식하게 생겼다라는 느낌을 지울 수 없다. Mono/Flux 클래스에서 지원하는 메소드들이 이런 것들을 많이 활용하기 때문에 어떤 방식으로 Lambda 함수를 받아서 처리하는지 사전에 알아두는게 좋다. 안그럼 좀 많이 헷갈린다.

가장 많이 등장하는 그림이 아래 그림과 같다. 흐름이 종료되지 않으면 계속 그 흐름 과정에서 객체는 살아있다. 그리고 다른 객체 타입으로 변환되어 만들어지면, 그 순간 새로운 모노가 만들어진다.

괜히 궁금해지는 부분! Mono 객체가 종료되지 않은 상태로 있다면 이 객체는 Garbage collection 대상이 되나? 이전의 테스트 상황에서 GC에 대한 부분을 구체적으로 살펴보지 않았지만 30분정도 부하 테스트에서 성능상 큰 문제가 없었던 것으로 봐서 GC 처리되는 것 같기는 하다. 최근에 하도 GC 문제 때문에 고생을 좀 해서 그런가 그래도 안전하게 할려면 안전한 종료 처리를 하는게 맞지 않을까 싶다.

이런 이해를 바탕으로 간단히 작업해본 셧다운(Shutdown) 로직 가운데 일부다.

  @GetMapping("/{game}/{id}")
  @ResponseStatus(HttpStatus.OK)
  public Mono<PlayPermission> canPlayTheGame(@PathVariable String game, @PathVariable String id) {
      return playerService.identifyPlayer(id)
                          .doOnSuccess(playerInfo -> ifFalse(gameService.isPlayableAge(game, playerInfo.calculateAge()), NotAvailableAgeResponseException.class))
                          .map((playerInfo) -> new PlayPermission(game, id, Permission.ALLOWED));
  }

PlayerService에서 구현한 identifyPlayer는 WebClient를 통해 RESTful response를 받는다.

  public Mono<PlayerAccount> identifyPlayer(String id) {
    return webClient.get().uri("/api/v1/account/" + id)
                    .retrieve()
                    .onStatus(Predicate.isEqual(HttpStatus.NOT_FOUND), response -> Mono.error(new NotExistingPlayerResponseException(puuid)))
                    .bodyToMono(KasPlayerDto.class)
                    .map(kasInfo -> buildPlayerAccount(kasInfo));
  }

So how about?

요 설정을 기반으로 했을 때 성능 테스트 결과를 얻었다.

1000 Concurrent User를 m4.xlarge(4 Core, 8G Mem) 장비를 대상으로 실행했을 때, 466 TPS를 보였다. 재미있는 건 평균 응답 시간이 1초(1044 ms)다.
딱 곧이 곧대로 보자면, 4개의 Core로 처리할 수 있는 작업이 4개라는 이야기다. 음?

물론 곧이 곧대로 세상을 보지는 않을 것이다. 해당 Thread가 IO처리를 하면, 당연히 OS는 그 쓰레드를 Context Switching시키고, 다른 쓰레드를 CPU에 올려서 일을 시킬 것이다. 즉 처리량을 늘릴려면 쓰레드를 정량적으로 늘리면 된다. 하지만 일정 개수를 초과하는 쓰레드는 Context Switching 비용만을 증가실킬 뿐 효율성의 향상을 초래하지는 못한다.

여기에서 설명한 예제는 2개의 외부 연동 포인트를 가지고 있다. 첫번째는 회원 정보 연동을 위해 External Service를 RESTful endpoint로 요청하는 구간이고, 해당 계정 사용자가 해당 시간에 시스템에 접근하는 것을 허용할지 말지를 조회하기 위한 Repository 조회다. 각 단위 연동 시간이 아래와 같다고 하자.

  • External RESTful query – 60ms
  • Repository query – 20ms

이외 부차적인 JSON Serialization/Deserialization 등등을 위해 소모되는 시간까지 고려했을때, 총 소요 시간은 계산하기 쉽게 100ms라고 가정하자. Synchronous한 방법으로 Transaction이 처리된다고 가정하면 1개 Core에서 초당 수행 가능한 건수는 10건이다. 4개 Core라고 하면 단순 계산으로 40건을 처리할 수 있다.

Business people standing with question mark on boards
헐… 근데 근데 부하 테스트 결과가 466 TPS라고? 사기아님?

사기라고 생각할 수 있지만, 위의 그림을 보면 납득이 될 것이다. 실제 어플리케이션의 쓰레드를 통해 실행되는 Code의 총 실행 시간은 20ms 밖에는 되질 않는다. 나머지 시간은 외부 시스템들(여기에서는 External Service와 Respository)에게 정보를 요청하고, 그 결과를 받는걸 기다리는 시간이다. 따라서 전체 CPU의 시간을 온전히 어플리케이션의 수행을 위해 사용한다면 50(1 core당 처리 가능한 Transaction 수) x 4 = 200개를 처리할 수 있다!

쉬운 이해를 위해 어플리케이션 자체 처리 시간을 20ms로 산정했지만, 실제 작성된 코드는 아름다운 최적화 알고리즘과 데이터 구조로 내부 처리 시간은 10ms안쪽에서 실행되기 때문에 466TPS 라는 숫자가 나올 수 있었다. 🙂

근데 MVC 방식으로 하든 Reactive 방식이든 정말 성능에 영향을 미치나? 당연히 미친다. 왜? 어떤 사람은 IO가 발생하면 쓰레드는 Context Switching되고, 다른 쓰레드가 CPU에 의해 실행되기 때문에 성능은 비슷해야하는거 아니냐고 반론은 제기할 수 있다. 틀린 말이기도 하고 올바르게 문제를 지적하기도 했다. 성능에 영향일 미치는 요인은 바로 Context Switching에 있다. Context Switching 자체도 결국 처리는 CPU에 의해 발생된다. 시스템이 CPU를 많이 잡아드시면 드실수록 사용자 프로세스(어플리케이션)이 CPU를 실제 일을 위해서 사용할 시간이 줄어든다. 바꿔말하면 열일할 수 없다.

Reactive 방식으로 열일 시킬려고 할 때 항상 주의해야할 부분이 있다. 바로 IO에 대한 처리다. IO 처리를 User code 관점에서 처리하면 안된다. 이러면 비용대비 효율성이 떨어진다. 이유는 IO에 대한 관리를 Reactive Framework에서 관리해줄 때 최고의 효과를 볼 수 있기 때문이다. User code 수준에서 IO에 대한 주도권을 가지면 IO 처리가 완료됐을 때 User code의 쓰레드가 이를 직접 제어해야한다.

이 말은 쓰레드가 위의 그림에서처럼 기다려야한다는 의미이고, Context switching을 이용해야한다는 의미다. 그렇기 때문에 Reactive Framework에서 제공하는 WebClient 혹은 Reactive한 Repository들을 사용해야 한다.

상상도이긴 하지만 실제 Reactive의 경우, 쓰레드의 Context에 의존하는게 아니라 각각의 Queue를 통해 User code의 control을 제어한다. 마찬가지로 IO에 대한 요청 역시 User code에서 이를 직접 제어하는 것이 아니라 Reactive Repository를 통해 필요한 데이터 Request를 위임한다. 그러면 IO Dispatcher 같은 놈이 데이터를 Connection Pool을 통해 실제 데이터 저장소 혹은 External Service에 전달한다. 이 과정에서 Async IO를 하기 때문에 굳이 Connection을 물고 기다리는 것이 아니라 계속 다른 요청을 전달한다. 물론 Connection Polling을 통해 특정 Connection에 응답이 도착하면 이를 관련된 Request에 Mapping하고 궁극적으로는 Mono/Flux 객체를 Reactive Queue에 넣어서 쓰레드가 어플리케이션 수준에서 다음 작업을 이어가게 한다. 이러한 이유로 RESTful Request를 처리하기 위해서는 Reactive에서는 RestTemplate이 아닌 WebClient를 사용해야 한다.

이 과정을 통해 시스템에 의한 개입을 최소화하여 어플리케이션 수준의 Performance를 극대화하는 것이 Reactive의 핵심이다.

Reactive programming에서 주의할 점들

일반적인 Sync 코딩 방법과는 달리 몇가지 부분들을 주의해야한다. 이 섹션은 앞으로 나도 작업을 하면서 보완해나갈 예정이다. 얼마나 잘 오류를 만들지에 따라 내용이 풍부해질지 아니면 빈약한 껍데기로 남을지는 모르겠다.

  • 코딩 관점에서 반드시 생각할 점은 실행되는 쓰레드를 기다리게 만들면 안된다는 것이다. 로직을 실행하기 위해 필요한 데이터를 받아야 한다. RESTful API, NoSQL, MySQL이든 이건 IO를 통해 받기 마련이다. 이 과정에 Synchronous 한 부분이 들어간다면, 연산을 해야할 쓰레드가 불필요하게 대기해야한다. 이러면 Async 방식의 효율성이 크게 저하된다.
  • 비슷한 맥락으로 Mono/Flux에서 지원하는 block() 함수도 실제 런타임 코드에서는 사용하지 말아야 한다. block() 함수는 연관된 Async 동작이 모두 완료될 때까지 현재 쓰레드를 마찬가지로 대기하게 만든다. 디버깅을 위해 한시적으로 사용하는 용도로는 가끔씩 사용할 수 있다.

Software developer vs Software engineer vs Full stack developer

O’Reilly에서 보내주는 뉴스레터 메일에 가끔 재미있는 글이 있다. 오늘자 메일에 Software 분야에 일하는 사람들의 직군 호칭에 대한 레딧 이야기가 있다. 용어만 보면 나도 가끔 뭐가 뭔지 헷갈리는데 확실히 정확한 정의는 없는 것 같다. 각 호칭들에 대한 주관적인 생각들이 댓글로 달렸다. 주관적이지만 분류도 있고, 개인 경험을 대비한 솔직한 이야기들이 솔솔하다.

여러 댓글들 가운데 맘에 드는 글은 요글이 아닐까 싶다.

Here’s are my very simplistic definitions:

Software Developer

You know one, maybe two, programming languages well enough to implement a somebody else’s design.

Software Engineer

You know how to learn any language, how to choose the right one for the problem you need to solve and can create new designs.

Full Stack Developer

You’re a Software Developer that can work on both front-end and back-end software.

In my experience, a lot of people who consider themselves software engineers lack the adaptability and competency to make good tool and design choices.

개인적으로 제일 맘에 드는 일은 developer 역할을 수행할 때다. 가장 편하게 코딩을 즐길 수 있으니까.

분류를 나누는게 과연 의미가 있을까? 종종 이런 질문이 생각든다. 우리가 평사원/대리/과장/부장 계층을 나누듯 개발 분야에서도 주니어/걍/시니어/아키텍트… 다양한 색깔의 완장을 둔다. 그리고 이걸 내세워서 경쟁하게 만든다. 치열한 경쟁의 결과물인 완장은 조직 안에 자신의 권력을 보장한다. 혹은 “보장한다” 라는 환상을 심어준다. 지내고 보면 쓸데없는 짓이고, 괜한 맘고생을 뒤짚어 쓰는 일이다.

개발자는 개발에 집중하고, 좋은 코드를 작성하고, 그 일과 과정이 즐거우면 된다. 이 일에 충성하는 사람을 잘 관찰하고 성장의 과정이 조직의 발전과 괘를 맞춰줄 수 있도록 충고를 해주는 관리자가 있다면 그 조직은 흥할 것이다.

개발자는 관리자가 되어서는 안된다. 이유는 뻔하다. 그 관리자와 다른 개발자들이 어떻게 PR 리뷰를 편하게 주고받을 수 있겠나? 동등한 관계에서 주고받는 리뷰와 피드백은 개발자 성장의 가장 큰 밑거름이다. 지식과 경험의 많고 적은 차이가 있을 뿐이지 코드를 작성하고 하나의 어플리케이션을 만든다는 점에서 같은 위치에 있어야 한다.

이렇든 저렇든 작은 조직에서 많은 걸 바랄 수는 없다. 하지만 스스로 조심 또 조심해야할 일이다.

About the react-redux and keeping the global states in the persistent manner

I’m a pretty new one in the developing the frontend app in the web. Making a user interface in the web with HTML, CSS, and JS was a very tedious work and its code writing was so much ugly because of my short knowledge. If I had tried to learn the core nature of JS in the early days, it could be one of my best languages in the development.  Unfortunately, there was no such poor language than JS when I saw its beginning and its characteristics as a programming language. It has become the dominant programming language in the development world.

In recent days, I’ve fallen in love with the ReactJS app in the web. I’d hated the UI programming with any sorts of language, from VC++ to WEB(Mostly with HTML). But the ReactJS helps me to build a humble web app with full functionalities we need. Its code looks very good by adopting the functional and asynchronous style.  Its functional coding style helps the asynchronous event handling in a straightforward way.

Redux and the management of the state

But one of the headaches we should care is the separation of the logic from the UI. The frontend and backend system development approach has helped us define a clear role and responsibility and most of the critical business logic is on the backend side. However, some fraction of codes is related with the none UIs, just like the interactions with the backend. The values returned by the backend defines what actions or interaction should happen between the frontend app and a user. We call the set of values who controls the interaction as “states“. According to the characteristics of the data, some states are meaningful only a specific page. On the other hand, some states are valuable in the overall app and need to be shared by all pages or workflows in the app. In ReactJS, we call it the first as the “local states“, the other as the “global states“.

As far as I know, one of the many reasons why the redux feature has been introduced is to cover the management of the global states. The global states are very important resources and it should be managed in a controlled way. It means we should not make it be modified by anyone because it can do. To achieve this goal, it has adopted the value modification with the state machine along with the asynchronously executing observers. Anyone can reference values of the global states via the component’s properties, which are read-only and you cannot modify it directly.

You can see the technical details in the following resources.

  • https://redux-observable.js.org/
  • https://redux-observable.js.org/docs/basics/Epics.html
  • https://redux.js.org/basics/reducers
  • Simple online code writing and running tool: http://jsbin.com/jexomi/edit?js,output
  • https://redux-observable.js.org/docs/Troubleshooting.html#rxjs-operators-are-missing-eg-typeerror-actionoftypeswitchmap-is-not-a-function

It is a pretty awesome framework for many reasons.

  • It allows you to manage the global values in the structured and controlled way via the state transitions.
  • Its architectural guide enforces the separation of the logical data manipulation from the UIs who trigger the action by a user.
  • It provides the simple global value reference via ReactJS component’s properties in a safe way.

But how about the local states?

Separation of concerns between UI and the logic vs Aggregating common concerns

Persistence

Well, the redux-observable is a good solution to handle the global state management but it doesn’t keep the last global state in the browser. If you hit the current URL in the browser’s address bar, booms!! All the states kept in your web app are reset by the default values. To keep it, we should make one of the utilities such as the local storage, cookies, and sessions. The redux-persist supports the feature, not hurting the existing code. It requires a minimal routing change and offers a way to share the information among separate pages.

  • https://www.npmjs.com/package/redux-persist#nested-persists

The “Transform” should be your consideration to keep the local data safe. The data deletion is the simplest way to achieve this goal and the following guides can offer the way.

  • https://github.com/gabceb/redux-persist-transform-expire
  • https://github.com/maxdeviant/redux-persist-transform-encrypt

It is the first summary of my javascript/react programming and I will try to keep posting articles related to it sooner or later.

독후감: Blue ocean shift, Beyond competing

예전에 이 책의 부모님 책쯤되는 “블루오션 전략(BLUE OCEAN STRATEGY)”라는 책을 읽었다. 한참이나 제품을 가지고 고민하던 시절이었고, 읽으면서 어떤 포인트에서 제품을 만들어야 할까를 많이 고민하게 했었다. 시절이 한참이나 지나서 올초 LAX 공항 서점에서 책을 고르다보니 이 책이 눈에 들어왔다.


블루오션 전략이라는 책이 출간되면서 레드오션과 블루오션이라는 두 단어가 일상화되었다. 레드오션에서 피터지게 싸워봐야 남는게 없다. 사용자들에 대한 이해를 바탕으로 그들이 진정 원하는 가치가 뭔지를 찾아라. 그 가치를 위한 제품이나 서비스를 만들고, 이걸로 새로운 시장을 만드는 것이 2000년대라는 새로운 시대에 삶아남는 방법이라고 이야기했다.

시장에 대한 새로운 접근 방법이었고, 제품이나 서비스를 만드는 입장에서 제대로 된 화두를 던졌다고 볼 수 있다. 10년이 넘은 세월이 흐른 뒤에 나온 이 책에서 저자들은 어떤 이야기를 하고 있을까?  저자들은 많은 사람들이 블루오션의 가치를 인식하지만 이를 실행하는데서 어려움을 겪는다고 한다. 아는게 다 잘 실행된다면 이 세상에 불행한 사람이 어디 있겠나?

새로운 시장을 창조한다고 이야기를 할 때, 흔히 혁신(Innovation)을 이야기한다. 혁신만이 새로운 시장을 이야기할 때 너무 강조되기 때문에 역설적으로 블루오션을 찾아내기 어렵다. 특히나 사람들은 자신이 갈망하는 것이 있다면 그것만 보게 되고 그 밖에 있는 더 큰 가치를 보지 못한다. 만약 이미 구조화된 조직내에서 이런 갈망을 추구했을 때 잃을 것들에 대한 두려움 때문에 혁신에 대한 도전을 주저하게 만든다. 결론적으로 블루오션을 하기 위해 필요하다고 많은 사람들이 이야기하는 “창조적 파괴” 라는 것에 이르지 못한다. 하지만 정말 창조적 파괴만이 이런 혁신을 달성하게 할까? 라고 저자들은 되려 질문한다. 새로운 시장을 만드는 전략적 방안으로 다음과 같은 3가지 옵션이 있다.

  • Offering a breakthrough solution for an existing industry problem
  • Redefining and solving an existing industry problem
  • Identifying and solving a brand-new problem or seizing a brand-new opportunity

블로오션을 개척한다는 것을 사람들은 너무 과하게 생각해서 뭔가를 없앤 이후에나 달성할 수 있다고 본다. 되러 이런 사고 방식이 관점의 전환을 어렵게 한다. 되려 이것보다는 현재의 문제점을 다른 관점에서 재해석하는 것이 필요하고 이를 통해 시장의 문제점을 파고들어 새로운 시장을 만들어내는 것(Redefining and solving an existing industry problem)이 좀 더 현실적이라고 충고한다.  이 때 중요한 해석의 기준이 바로 사용자의 가치를 혁신하는 가이다. 기술쪽에 있는 내 입장에서도 새로운 기술을 통한 혁신을 많이 이야기하고 듣기도 한다. 하지만 이런 시도가 과연 사용자에게 어떤 도움을 줄 수 있는가가 먼저 평가되어야 한다. 사용자들이 제품/서비스의 팬이 될 수 있다면 제대로 된 가치의 혁신을 이뤄냈다고 정의할 수 있다. 가치 혁신을 이뤄내기 위해, 즉 블루오션으로 전환할려는 사람들은 다음과 같은 마인드가 필요하다고 이야기한다.

  • Blue ocean strategists do not take industry conditions as given. Rather, they set out to reshape them in their favor.
  • Blue ocean strategists do not seek to beat the competition. Instead, they aim to make the competition irrelevant.
  • Blue ocean strategists focus on creating and capturing new demand, not fighting over existing customers.
  • Blue ocean strategists simultaneously purse differentiation and low cost. They aim to break, not make, the value-cost trade-off.

이런 것들을 추진하기 위해 가장 우선적으로 사람들이 변화와 행동의 필요성을 본인들 스스로 느껴야 한다. 역시나 일을 한다는 것의 기본은 사람이다. 같이 일하는 동료들이 이를 자각하지 못하고 시켜져 일을 한거나 두려워 주저한다면 말짱 도루묵이다. 집중할 수 있는 최소 단위로 일이나 문제를 재정의할 수 있어야 한다. 정의된 문제점들을 통해 사람들이 변화와 도전에 공감해야 한다. 물론 이런 도전의 결과로부터 자신들이 안전하다라는 것을 느낄 수 있도록 적절한 프로세스와 리더로부터의 충분한 설명도 뒤따라야 한다. 일련의 과정들은 투명하게 운영되어야 하며, 어떤 결과를 얻을 수 있는지에 대해 지속적인 대화가 이뤄져야한다. 즉 과정은 투명해야하며 결과는 공정해야 한다. ^^

그래서 다음의 절차에 맞춰 블루오션에 대한 시도를 해 볼 수 있다.

Step One – Get Started

  • Choose the right place to start your blue ocean initiative: The Pioneer-Migrator-Settler Map
  • Construct the right team for the initiative

Step Two: Understand Where You Are Now

  • Collectively build one simple picture that captures your current state of play: The Strategy Canvas
  • See and easily agree on the need for the shift

Step Three: Image Where You Could Be

  • Discover the pain points of buyers imposed by the industry: The Buyer Utility Map
  • Identify the total demand landscape you can unlock: The Three Tiers for Noncustomers

Step Four: Find How You Get There

  • Apply systematic paths to reconstruct market boundaries: The Six Paths Framework
  • Develop alternative strategic options that achieve differentiation and low cost: The Four Actions Framework (eliminate-reduce-raise-create)

Step Five: Make Your Move

  • Select your move at the blue ocean fair, conduct rapid market tests, and refine the move.
  • Finalize the move by formalizing the big-picture business model that delivers a win for both buyers and you.
  • Launch and roll out your move.

정리는 여기까지. 이 정도 내용을 적어놨으면 나중에 다시 되씹으면서 찾아볼 수는 있을 듯 하다.

글을 쓰거나 코딩을 하거나 같이 적용되는 진실이 하나 있다. 지속적으로 하지 않으면 퇴화된다는 것이다. 항상 새로운 글을 쓰거나 새로운 랭귀지, 프레임웍을 시도할 필요는 굳이 없다. 짧은 거라도 거르지 않고 계속해야한다.  마지막 글을 쓴 다음에 이 글을 올리는 기간이 이미 상당히 벌어졌다. 영어로 된 글을 단순히 옮겨적는 것에 지나지 않지만 그럼에도 글을 쓰는게 상당히 힘들어졌다.  바쁘다는건 핑계고 소주 한잔을 줄이면 글을 쓸 수 있는데 말이다.

 

Consideration in accessing API with the credential on the apache client libraries

본사 친구들이 신규 시스템을 개발하면서 기존에 연동하던 endpoint가 deprecated되고, 새로운 endpoint를 사용해야한다고 이야기해왔다. 변경될 API의 Swagger를 들어가서 죽 살펴보니 endpoint만 변경되고, 기능을 제공하는 URI에 대한 변경은 그닥 크지 않았다. curl을 가지고 테스트를 해봤다.

$ curl -X GET --header "Accept: application/json" --header "AUTHORIZATION: Basic Y29tbX****************XR5X3VzZXI=" \
"http://new.domain.com/abc/v2/username/abcd"
{"subject":"0a58c96afe1fd85ab7b9","username":"abcd","platform_code":"abc"}

잘 되네… 예전 도메인을 신규 도메인으로 변경하면 이상없겠네.

로컬 환경에서 어플리케이션의 설정을 변경하고, 실행한 다음에 어플리케이션의 Swagger 페이지로 들어가서 테스트를 해봤다.

음… 뭐지? 분명히 있는 사용자에 대한 정보를 조회했는데, 없다네? 그럴일이 없으니 console에 찍힌 Stack trace를 확인해보니 401 Unauthorized 오류가 찍혔다.

Caused by: org.springframework.web.client.HttpClientErrorException: 401 Unauthorized
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:85)
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:708)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:661)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:636)
at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:557)

401 오류가 발생했는데, 그걸 왜 사용자가 없다는 오류로 찍었는지도 잘못한 일이다. 하지만 원래 멀쩡하게 돌아가던 코드였고, curl로 동작을 미리 확인했을 때도 정상적인 결론을 줬던 건데 신박하게 401 오류라니??? 인증을 위해 Basic authorization 방식의 credential을 사용했는데, 그 정보가 그 사이에 변경됐는지도 본사 친구한테 물어봤지만 바뀐거가 없단다. 다른 짐작가는 이유가 따로 보이지 않으니 별수없이 Log Level을 Debug 수준으로 낮춘 다음에 HTTP 통신상에 어떤 메시지를 주고 받는지를 살펴봤다.

하지만 새로운 endpoint로 request를 쐈을 때에는 아래와 같은 response를 보낸 다음에 추가적인 request를 보내지 않고, 걍 실패해버린다.

2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 HTTP/1.1 401 Unauthorized
2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 Content-Encoding: gzip
2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 Content-Type: application/json
2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 Date: Tue, 17 Apr 2018 17:41:48 GMT
2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 Vary: Accept-Encoding
2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 transfer-encoding: chunked
2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 Connection: keep-alive
2018-04-18 02:41:48.517 DEBUG 93188 : Connection can be kept alive indefinitely
2018-04-18 02:41:48.517 DEBUG 93188 : Authentication required
2018-04-18 02:41:48.517 DEBUG 93188 : new.domain.com:80 requested authentication
2018-04-18 02:41:48.517 DEBUG 93188 : Response contains no authentication challenges

이상한데???

Authentication required 라고 나오는데 코드상으로는 HTTP Connection factory를 생성할 때 Basic authorization을 아래와 같이 설정을 적용해뒀는데 말이다.

@Bean
public HttpClient httpClient() throws MalformedURLException {
PoolingHttpClientConnectionManager cm = Protocols.valueOf(new URL(domain).getProtocol().toUpperCase()).factory().createManager();
cm.setMaxTotal(connectionPoolSize);
cm.setDefaultMaxPerRoute(connectionPoolSize);

BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,
new UsernamePasswordCredentials(username, password));

return HttpClients.custom()
.setConnectionManager(cm)
.setDefaultCredentialsProvider(credentialsProvider)
.build();
}

하지만 의심이 든다. 정말 요청할 때마다 Authorization header를 셋팅해서 내보내는건지. 이전 시스템과 어떤 방식으로 통신이 이뤄졌는지 궁금해서 endpoint를 이전 시스템으로 돌려서 확인을 해봤다. 정상적으로 데이터를 주고 받을 때는 아래와 같은 response를 endpoint에서 보내줬다.

2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "HTTP/1.1 401 Unauthorized[\r][\n]"
2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "Server: Apache-Coyote/1.1[\r][\n]"
2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "WWW-Authenticate: Basic realm="Spring Security Application"[\r][\n]"
2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "Set-Cookie: JSESSIONID=78AEB7B20A1F8E1EA868A68D809E73CD; Path=/gas/; HttpOnly[\r][\n]"
2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "Content-Type: application/json;charset=UTF-8[\r][\n]"
2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "Transfer-Encoding: chunked[\r][\n]"
2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "Date: Tue, 17 Apr 2018 17:38:09 GMT[\r][\n]"

이 로그가 출력된 다음에 다시 인증 헤더를 포함한 HTTP 요청이 한번 더 나간다! 이전과 이후의 로그에서 인증과 관련되어 바뀐 부분이 뭔지 두눈 부릅뜨고 살펴보니 새로 바뀐놈은 WWW-Authenticate 헤더를 주지 않는다. 이 Response Header가 어떤 역할을 하는지 살펴보니 아래와 같은 말이 나온다.

(…)The response MUST include a WWW-Authenticate header field (section 14.47) containing a challenge applicable to the requested resource.(…)

근데 이게 문제가 왜 될까?? 생각해보니 Apache에 Basic 인증을 설정하면 요런 메시지 박스가 나와서 아이디와 암호를 입력하라는 경우가 생각났다.

아하… 이 경우랑 같은거구나! 실제 인증 과정에서 이뤄지는 Protocol은 아래 그림과 같이 동작한다.

이 그림에서 볼 수 있는 것처럼 하나의 API 요청을 완성하기 위해서 2번의 HTTP Request가 필요했던 것이다. 이런걸 생각도 못하고 걍 Basic Credential을 Example에서 썼던 것처럼 쓰면 문제가 해결된다고 아무 생각없이 너무 간단히 생각했던 것 같다. 연동해야할 Endpoint가 여러 군데인 경우에는 이런 설정이 제각각이기 때문에 많이 사용하는 RestTemplate 수준에서 Header 객체를 만들어 인증 값을 설정했을 것 같다. 물론 RestTemplate을 생성하는 Bean을 두고, Authorization 값을 설정하면 되긴 했겠지만 상황상 그걸 쓸 수 없었다. 변명을 하자면 그렇다는 것이다.

제대로 짜면 이렇다.

@Bean
public HttpClient httpClient() throws MalformedURLException {
....
Credentials credential = new UsernamePasswordCredentials(username, password);
HttpRequestInterceptor interceptor = (httpRequest, httpContext) ->
httpRequest.addHeader(new BasicScheme().authenticate(credential, httpRequest, httpContext));

return HttpClients.custom()
.setConnectionManager(cm)
.addInterceptorFirst(interceptor)
.build();
}

앞선 예제처럼 Credential Provider를 두는게 아니라 interceptor를 하나 추가하고, 여기에서 crednetial 값을 걍 설정해주는 것이다. 이러면 이전처럼 2번이 아니라 한번에 인증이 처리된다.

몸이 먼저 움직이기보다는 생각이 먼저 움직여야 하는데 점점 더 마음만 급해지는 것 같다.

참고자료

  • https://stackoverflow.com/questions/17121213/java-io-ioexception-no-authentication-challenges-found?answertab=active#tab-top
  • https://stackoverflow.com/questions/2014700/preemptive-basic-authentication-with-apache-httpclient-4

– 끝 –

Spring Data JPA와 AspectJ가 함께 친 사고

Spring JPA는 데이터베이스를 사용하는데 있어서 새로운 장을 열었다. 쿼리를 직접 사용해서 데이터베이스를 엑세스하는 MyBatis의 찌질한 XML 덩어리를 코드에서 걷어냄으로써 코드 자체도 간결해지고 직관적으로 특정 Repository 및 DAO가 어떤 테이블과의 매핑 관계가 있는지를 명확하게 파악할 수 있도록 해준다. 단점으로 생각되는 부분이 여러 테이블들을 복잡한 조인 관계를 설정하는게 상당히 난감하다. 하지만 역설적으로 이런 조인 관계를 왜 설정해야하는지를 역으로 질문해볼 필요가 있다. 우리가 복잡한 코드를 최대한 단순화시키려고 노력할 때, 그 코드가 달성할려고 하는 목적과 의미를 가장 먼저 생각하는 것처럼. 더구나 마이크로서비스 아키텍처 개념에서는 한 서비스가 자신의 독립적인 Repository를 가지는 것이 원칙이라고 할 때 해당 Repository의 구조는 단순해야한다. 그렇기 때문에 Spring JPA가 더욱 더 각광을 받는 것이 아닐까 싶다.

AspectJ 역시 특정 POJO 객체의 값을 핸들링하거나 특정 동작을 대행하는데 있어서 아주 좋은 도구이다. 주로 특정 Signature 혹은 Annotation을 갖는 객체 혹은 메소드가 호출되는 경우에 일반적으로 처리해야할 작업들을 매번 명시적으로 처리하지 않아도 AspectJ를 통해 이를 공통적으로 실행시켜줄 수 있기 때문이다.

작업하는 코드에 이를 적용한 부분은 특정 DB 필드에 대한 자동 암호화이다.  값을 암호화해서 저장하고, 저장된 값을 읽어들여 평문으로 활용하는 경우가 있다. 이 경우에는 이만한 도구가 없다.

이런 이해를 바탕으로 지금까지 서비스들을 잘 만들어서 사용하고 있었다. 그런데 새로운 기능 하나를 추가하면서 이상한 문제점이 발생하기 시작했다.  암호화된 테이블에 대한 저장은 분명 한번 일어났는데, 엉뚱하게 다른 처리를 거치고 나면 암호화 필드가 저장되어야 할 필드에 엉뚱하게 평문이 저장되었다. 그전까지 암호화된 값이 잘 저장되었는데… 더 황당한 문제는 내가 명시적으로 저장하지 않은 데이터까지 덤탱이로 변경이 되버린다!!!  개별 기능을 테스트 코드를 가지고 확인했을 때는 이상이 없었는데, 개발 서버에 올리고 테스트할려니 이런 현상이 발생한다.

 

새로운 기능이 동작하는 방식을 간단히 정리하면 아래와 같다.

  1. 기존 테이블에서 2라는 키값이 존재하는 데이터를 쿼리한다. 원래 시나리오상으로는 이런 데이터가 존재하지 않아야 한다.
  2. 2라는 PK값을 가지는 데이터를 저장한다.
  3. 저장 후 fk.for.grouping 이라는 그룹 키를 가진 항목들을 로딩해서 특정 값을 계산한다. 읽어들인 걸 저장하지는 않고, 읽어들여서 계산만 한다.
  4. 계산된 결과를 신규 테이블(New table)에 저장한다.

디버깅을 통해 확인해보니 2번 단계에서는 정상적으로 AspectJ를 통해 암호화 필드가 정상적으로 저장된다. 그런데 4번 과정을 거치고나면 정말 웃기게도 평문값으로 업데이트가 이뤄진다.  그림에서 따로 적지는 않았지만, 멀쩡하게 암호화되어 있던 1번 데이터도 평문화되는게 아닌가??? 몇 번을 되짚어봐도 기존 테이블에 저장하는 로직은 없다. 뭐하는 시츄에이션이지??

JPA와 AspectJ를 사용하는데, 우리가 놓치고 있었던 점은 없었는지 곰곰히 생각해봤다. 생각해보니 JPA를 사용자 관점에서만 이해를 했지, 그 내부에서 어떻게 동작이 이뤄지는지를 잘 따져보지는 않았던 것 같다. 글 몇 개를 읽어보니 Spring의 Data JPA는 이런 방식으로 동작하는 것으로 정리된다.

재미있는 몇가지 사실들을 정리하면 아래와 같다.

  • RDBMS를 위한 Spring Data는 JPA를 Wrapping해서 그저 쓰기 좋은 형태로 Wrapping한 것이고, 실제 내부적인 동작은 JPA 자체로 동작한다.
  • JPA는 내부적으로 EntityManager를 통해 RDBMS의 데이터를 어플리케이션에서 사용할 수 있도록 관리하는 역할을 한다.
  • EntityManager는 어플리케이션의 메모리에 적제된 JPA Object를 버리지 않고, Cache의 형태로 관리한다!!!

EntityManager가 관리를 한다고?? 그럼 그 안에 있는 EntityManager는 어떤 방식으로 데이터를 관리하지?

 

JPA Entity Lifecycle

 

오호.. 문제의 원인을 이해할 수 있을 것 같다.

  • Springboot application이 위의 처리 과정 3번에서 “fk.for.grouping 이라는 그룹 키를 가진 항목들을 로딩” 과정을 수행한다.
  • 정말 읽어들이기만 했다면 문제가 없었겠지만 AspectJ를 통해서 읽어들인 객체의 암호화 필드를 Decryption했다.
  • Entity Manager에서 관리하는 객체를 변경해버렸다! 객체의 상태가 Dirty 상태가 되버렸다.
  • 4번 과정에서 신규 테이블에 데이터를 저장할 때, Entity Manager는 관리하는 데이터 객체 가운데 Dirty 객체들을 테이블과 동기화시키기 위해 저장해버렸다.
  • 하지만 이 과정은 Entity Manager 내부 과정이기 때문에 따로 AspectJ가 실행해야할 scope를 당연히 따르지 않는다.
  • 결과적으로 평문화된 값이 걍 데이터베이스에 저장되어버렸다.

따로 저장하라고 하지 않았음에도 불구하고 엉뚱하게 평문화된 값이 저장되는 원인을 찾았다.

문제를 해결하는 방법은 여러가지가 있을 수 있겠지만, 가장 간단한 방법으로 취한 건 이미 메모리상에 로드된 객체들을 깔끔하게 날려버리는 방법이다.

public interface DBRepositoryCustom {
    void clearCache();
}

public class DBRepositoryImpl implements DBRepositoryCustom {
    @PersistenceContext
    private EntityManager em;

    @Override
    @Transactional
    public void clearCache() {
        em.clear();
    }
}

위와 같이 하면 Spring Data JPA 기반으로 EntityManager를 Access할 수 있게 된다. 이걸 기존 JPA Interface와 연동하기 위해, Custom interface를 상속하도록 코드를 변경해주면 된다.

@Repository
public interface DBRepository extends CrudRepository&lt;MemberInfo, Long&gt;, DBRepositoryCustom {
    ...
}

여기에서 주의할 점은 DBRepository라는 Repository internface의 이름과 Custom, Impl이라는 Suffix rule을 반드시 지켜야 한다. 해당 규칙을 따르지 않으면 Spring에서 구현 클래스를 정상적으로 인식히지 못한다.  따라서 반드시 해당 이름 규칙을 지켜야 한다.

  • DBInterface가 JPA Repository의 이름이라면
  • DBInterfaceCustom 이라는 EntityManager 조작할 Interface의 이름을 주어야 하며
  • DBInterfaceImpl 이라는 구현 클래스를 제공해야 한다.

이렇게 해서 위의 처리 단계 3번에서 객체를 로딩한 이후에 clearCache() 라는 메소드를 호출함으로써, EntityManager 내부에서 관리하는 객체를 Clear 시키고, 우리가 원하는 동작대로 움직이게 만들었다.

하지만 이건 정답은 아닌 것 같다. 제대로 할려면 AspectJ를 좀 더 정교하게 만드는 방법일 것 같다. 그러나 결론적으로 시간이 부족하다는 핑계로 기술 부채를 하나 더 만들었다.

 

참고자료

  • https://www.javabullets.com/access-entitymanager-spring-data-jpa/
  • https://www.javabullets.com/jpa-entity-lifecycle/

 

– 끝 –