'경험담'에 해당하는 글 2건

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

동료들과 대화를 하다보면 오버엔지니어링이라는 단어가 종종 나온다. 주로 안 좋은 문맥에서 이 단어를 사용하게 된다. 전임자가 어떤 코드부분을 오버엔지니어링 해놓아서 이해하기가 어려웠다고도 하고, 더 나아가서는 시스템 자체가 오버엔지니어링 되어 있어 유지보수에 큰 어려움을 겪는다고 얘기하기도 한다. 오버엔지니어링이 미치는 부정적인 영향아 많아 보인다. 그런데, 여러 관점에서 생각해보니 오버엔지니어링은 단순히 부정적으로 보기에는 아깝고, 여러모로 생각할만한 꺼리가 있다고 생각한다. 그래서 이번 글에서 오버엔지니어링에 대한 생각을 정리해보려고 한다. 


1. 오버엔지니어링? 엔지니어링?


오버엔지니어링에 대한 여러 얘기를 나누기 전에 오버엔지니어링에 대한 간단한 정리가 필요할 것 같다. 위키피디아 등의 정의와 더불어 개발자들이 보통 인식하는 오버엔지니어링은 다음과 같다.


현재 필요한 것 보다 더 과하게 제품을 디자인 하는 것이다. 즉, 제품을 더 견고하게 만들거나, 더 복잡하게 만드는 것이다. 핵심개념만을 담아 최대한 단순하게 만들자는 최소주의와는 대비되는 개념이다. 예를 들어 실제로는 만 명의 사용자가 쓰는 시스템이 내부적으로는 1억의 사용자에 맞춰 설계 개발되어 있고, 이로인해 100만의 사용자 규모에는 필요없는 구성요소들이 들어가 있을 때 오버엔지니어링 되어 있다고 얘기한다. 보통 오버엔지니어링 되어 있으면 이후 제품을 운영 할 때 어려움을 줄 때도 많다. 단순한 구조에서는 간단히 할 수 있는 일을 더 복잡하게 해야 하는 등이 일이 발생하기 때문이다.


추가로 앞으로의 글에서 자주 사용할 '엔지니어링'이라는 표현에 대해서도 정리를 해보고 싶다. 나는 기능구현을 위해 꼭 필요한 것은 아니지만 안정성이나 확장성을 위해 부가적으로 하는 일을 '엔지니어링 한다'고 표현한다. 예를 들어 안정성을 위해 입력값을 검사하는 코드를 넣는 것이나 향후 확장성을 위해 디자인 패턴을 적용하는 것 등이 있다. 이러한 활동이 과다해지는 것을 사람들이 대게 '오버엔지니어링 한다'라고 표현하는 것을 볼 때 뜻이 적절하다고 본다. 하지만, 다른 사람이 동일하게 표현하는 것을 한번도 못 보아서 확신은 없다. 


2. 오버엔지니어링이 시작되는 이유


오버엔지니어링의 정의만 보면 이상할 수도 있다. 왜 필요보다 과하게 만드는 걸까? 내 생각에 개발자가 오버엔지니어링을 하게 되는 계기는 이렇다. 보통 초보 개발자 시절에는 오버엔지니어링을 하지 않는다. 아니 못한다. 아무래도 개발 실력이 부족할 때는 다른 것에 신경 쓸 여력이 없기 때문이다. 따라서 일단은 단순히 잘 동작하는 제품을 만드는 데 집중하고 집중한다. 얼마 후 본인이 만든 잘 동작하는 제품을 릴리즈한다. 릴리즈 후 정신없이 고객문의가 들어온다. 다양한 고객문의를 보며 안정성, 예외처리 등 고려하지 못한 부분이 많다는 것을 깨닫는다. 이런 문제를 해결해가며 잘 동작하는 기능 말고도 중요한 게 있다는 것을 깨닫는다.


다음 제품부터는 적절히 엔지니어링을 하기 시작한다. 예전과는 달리 입력 값이 잘못 들어왔을 때를 대비한 처리를 해놓기도 하고, 소스 구조도 단순히 동작하는 것을 넘어 나중에 고치기 쉽게 만들기도 한다. 이런 과정에서 초보 시절 단순히 기능구현만을 신경썼을 때 보다 소스량이 다소 늘어나게 된다. 보통 이정도 수준까지는 오버엔지니어링이라고 부르지 않는다. 오히려 코드 리뷰 등을 할 때 이런 부분에 신경 쓴 것에 긍정적인 평가를 받을 때가 잦다. 이렇게 긍정적인 평가를 받다보면 아무래도 이런 부분을 지속적으로 신경쓰게 된다. 더 고려해야 할 것이 없는지 비판적인 시각으로 제품을 살펴본다. 


경험이 많아짐에 따라 단순히 입력 값이 잘못 들어왔을 때를 대비해 몇줄의 소스 코드를 넣던 수준에서, 시스템 구조 수준에 대비를 하기에 이른다. 예로 왠지 미래에 생길 것 같은 다양한 보안 요구사항을 고려하여 복잡하지만 유연한 보안 관련 프레임웍을 미리 도입하기도 하고, 당장은 불편하지만 차후 확장성이 좋은 방향으로 저장소 구조를 설계하기도 한다. 이렇게 하는 이유는 잘못된 입력 값을 처리하던 이유와 같다. 미래에 있을법한 일에 미리 대비하여 제품의 안정성 등을 확보하자는 것이다. 이정도 수준에 이르면 종종 주변 동료에게 오버엔지니어링이 아니냐는 도전을 받기도 한다.


3. 엔지니어링은 성공해야 하는 투자


그렇자면 오버엔지니어링이 나쁜 것인가? 예로 저장소 구조에 대한 가상사례를 가지고 생각해본다. 단순히 개발해도 약 500만명의 사용자까지는 문제 없는데, 1억명 까지 사용자가 늘어나는 상황을 고려해 구조를 설계 및 구현해놓았다. 제품을 릴리즈 하고 서비스를 한다. 그런데, 초기 사용자는 10만을 넘지 않는다. 1년이 지난다. 서비스 사용자는 여전히 100만 정도이다. 당분간 더 늘 것 같지도 않다. 이 때 저장소 구조에 대해 이미 해둔 엔지니어링을 어떻게 평가해야 할까? 나는 부정적으로 본다. 왜냐하면, 1억명의 사용자를 고려하여 설계 및 구현하는 비용을 사용했는데 실제로는 사용자가 1억명이 되지 않았기 때문이다. 이렇게 될꺼라면 굳이 엔지니어링을 하지 않아도 됐다.


정반대의 상황도 생각해본다. 제품을 릴리즈 했다. 그런데, 사용자가 폭팔적으로 늘어난다. 릴리즈 한 달 만에 사용자가 1,000만명이 넘었다. 다행히 미리 해둔 엔지니어링 덕분에 사용자의 제품 사용에 큰 문제가 없다. 만약 미리 엔지니어링을 해두지 않았다면 어땠을까? 개발자는 급하게 구조를 변경해야했을 것이고, 작업기간 동안 사용자는 접속불안이나, 응답속도저하 등의 문제를 겪었을테다. 게다가 생각해보니 초기에 2주 정도를 투자해서 준비했기 망정이지, 이미 릴리즈해서 서비스 중인 제품으로 엔지니어링 했더라면 훨씬 많은 시간이 필요했음이 분명하다. 어쩌면 4주 이상이 걸렸을 지도 모르겠다. 작업 과정에서 장애도 많이 났을테고 말이다.


이런 맥락에서 볼 때 엔지니어링은 투자라고 생각한다. 마치 유망주에 투자를 하듯이 미래에 생길 것 같은 일에 투자를 하는 것이다. 위 가상의 예에서 볼 수 있듯이 실패하면 엔지니어링에 대한 투자는 아무것도 아니게 된다. 반면 성공하면 매우 긍정적인 결과가 있다. 문제는 경험 상 이런 투자가 성공으로 이어지는 때가 적다는 것이다. 정확히 측정 해보지 않았지만, 내가 봐온 많은 투자가 실패했다. 게다가 흥미로운 점은 엔지니어링의 규모가 크면 클수록 실패할 확률이 더 높다는 것이다. 특히 저장소 확장성 설계 같은 규모가 큰 엔지니어링은 이후에 보면 사용자가 적어 필요없을 때가 잦았고, 개발 뿐 아니라 운영에도 부담을 주곤 했다.


4. 적정엔지니어링


이런 이유로 가급적 성공할만한 투자만 하자는 것은 이견이 없을 것 같다. 그리고 이를 잘 판단할 수 있는 것은 개발자이다. 그런데, 현실에서 개발자는 의외로 성공하기 어려운 투자를 자주 감행한다. 이는 여러 원인이 있다고 본다. 첫째는 책임 문제이다. 예로 사용자가 폭팔적으로 증가했을 때 기술적 준비가 안 되어 있다면 개발자는 타격을 받는다. 둘째는 개발자의 욕심이다. 개발자는 대게 기술에 대한 욕심이 있는데, 엔지니어링은 이런 욕심을 충족시키는 좋은 방법 중 하나다. 셋째는 개발자가 엔지니어링을 당연하게 생각하기 때문이다. 어떤 개발자는 엔지니어링은 투자이고 선택할 수 있는 영역이라고 판단한다. 그런데, 어떤 개발자는 엔지니어링은 무조건 해야 하는 것으로 인식한다.


위 세 가지 중 개발자 책임 문제는 까다로운 문제 같다. 개발자들끼리의 회의에서 엔지니어링을 가지고 논쟁이 벌어지다 가끔 나오는 말이 있다. "그럼 나중에 장애나면 누가 책임질껀데?" 이런 말이 나오면, 그냥 모두 맘 편하게 오버엔지니어링을 하기도 한다. 합리적으로 판단해 엔지니어링을 하지 않았는데 이후 사용자가 폭팔적으로 증가해 제품의 서비스가 멈추기라도 할 때 개발자에게 책임을 묻는 사태가 일어날까 걱정하는 것이다. 또한, 이럴 때 기술적 이해가 부족한 외부부서는 개발자를 무능하게 볼 수 있고 이는 힘을 빠지게 한다. 나중에 급히 준비하는 과정도 개발자에겐 너무 힘들다. 당장 서비스에 문제가 생기면, 퇴근하기도 힘들어지기 때문이다.


두 번째 개발자의 욕심 또한 생각해볼만한 문제다. 경험상 실력이 좋은 개발자일수록 엔지니어링에 욕심이 있는 것 같다. 기술적 지식이 많다보니 제품을 설계할 때 많은 고민을 한다. 고민을 통해 인지하게 된 제품의 기술적 약점을 보완하고 싶은 마음이 자연스레 생긴다. 약점이 뻔히 보이는 데 아무것도 안 하고 넘어가는 것은 개발자에게는 힘든 문제일수도 있다. 한편 신기술을 극단적으로 선호하는 개발자도 있다. 이런 개발자는 나름의 근거를 가지고 새로나온 프레임웍 등의 신기술을 도입한다. 하지만, 가만히 살펴보면 도입근거가 미약할 때가 많고, 외부 시선으로 보기에는 오버엔지니어링으로 판단하게 될 때도 있다.


그래서 나는 개발자가 현재 비즈니스 상황에 따라 적절한 엔지니어링 수준을 결정하는 능력이 중요하다고 생각한다. 이를 위해서는 세 가지 정도 꼭 필요한 능력이 있다고 생각하는데 첫째가 용기다. 용기가 필요한 이유는 엔지니어링 결정으로 인해 최악의 경우 다소 억울한 비난을 받을수도 있기 때문이다. 이런 부분을 감안하고도 옳은 판단을 내릴 수 있어야 한다고 본다. 둘째는 비지니스에 대한 이해이다. 예로 확장성에 대한 엔지니어링 결정을 하려면, 어떤 데이터가 많이 증가할지 예측이 가능해야 한다. 이런 부분은 기술력보다는 비즈니스 이해가 판단에 더 많은 영향을 미친다고 생각한다. 마지막으로 당연히 기술력이 필요하다.


그런데, 초반 제품을 개발할 때 위에서 얘기한 적정엔지니어링 관점으로 준비를 했다고 가정하면, 이후 제품 운영이 더 어려울 수도 있다. 왜냐하면, 경험상 미리 준비하는 것보다 운영 중에 준비하는 것이 훨씬 어렵기 때문이다. 따라서, 적정엔지니어링을 적용해 제품을 개발했다면 비즈니스 상황 및 시스템 모니터링을 통해 필요한 엔지니어링을 지속적으로 확보해가는 능력이 필요하다고 본다. 또한, 이 때도 적정엔지니어링을 유지하는 것이 좋다고 본다. 운영 중 확장성 문제가 생겼을 때 한번에 투자하기 보다는, 조금씩 투자하며 현재 상황에 맞게 전진할 수 있다고 보기 때문이다. 아래는 이런 노력을 하며 실제 있었던 경험담이다. 다만, 많은 것이 생략 된 대략적인 내용이다.


최초 상황


- MySQL Master 1대 /Slave 1대

- Master는 Insert/Update/Delete 쓰기 요청을 받음, Slave는 Select 요청을 받음

- Master의 데이터는 Slave로 실시간 복제 됨

- 릴리즈 한지 약 2년 정도 된 제품


첫 번째 엔지니어링


- Slave에 복잡한 쿼리가 많아 Slow Query가 늘어나며 부담이 생김

- 추가 Slave를 투입하여 Slave를 2대로 만듬

- Round Robin 방식으로 Slave#1, Slave#2가 번갈아 가며 요청을 처리하게 함

- 결과로 Slow Query가 줄고 부담이 경감 됨

- 위 작업에 2일 정도 소모


두 번째 엔지니어링(준비했었으나 실제 제휴가 진행되지 않아 계획만 세움)


- 제휴로 인해 약 10배 사용자가 예상 됨, 곧바로 샤딩을 할수도 있지만 비용을 고려하여 다시 한번 조금만 엔지니어링 하기로 함

- Master는 N개로 늘릴 수 없기 때문에 Scale-Up(하드웨어 업그레이드) 방식으로 확장

- Slave는 필요한 만큼 확장하고 마찬가지로 Round Robin 방식으로 서비스, 단 복제지연을 고려해야 함

- 이후 모니터링 중 복제지연이 생기면 필요한 튜닝을 진행

- 만약 사용자가 10배 그 이상이 될 소지가 보이면 비즈니스/기술 부서에서 최소한 1달 전에 인지하여 준비


설명


위 경험담을 보면 보통 사용하는 샤딩이 안 되어 있는 상황이다. 아마 초기에 이로 인한 여러 이득이 있었을 것으로 판단한다. 그 후 성능 문제가 발생했지만, 아무래도 큰 작업인 샤딩을 진행하지는 않는다. 다만, 가까운 미래에 필요할 정도로만 엔지니어링을 한다. 이후 10배 사용자가 필요한 상황이 되었지만, 이때도 어떻게든 구조를 많이 변경하지 않고 엔지니어링을 해본다. 10배 이상이 되면 구조적인 한계가 있기 때문에 샤딩을 진행한다. 다만, 샤딩은 시간이 필요하기 때문에 모니터링 및 판단을 잘 해서 최소한 1달 전에 알 수 있도록 준비한다.


5. 적정엔지니어링이 개발자에게 미치는 영향

적정엔지니어링은 비즈니스 관점에서 긍정적으로 볼 수 있을 것 같다. 미리 기술에 투자하지 않고 필요할 때마다 조금씩 점진적으로 해나가기 때문에 빠르게 제품을 릴리즈 하는데 도움이 되기 때문이다. 특히 스타트업처럼 시간적 여유가 부족하다면 더욱 빛을 발할 것 같다. 게다가, 데이터베이스 샤딩을 하지 않는 사례를 보면 제품 개발 시점 뿐 아니라 운영 시점에 긍정적 영향을 주기도 한다. 그런데, 개발자에게는 어떨까? 부정적인 점이 있다고 본다. 적정엔지니어링의 필요성을 느낀 후 반년 넘게 실천을 해보았다. 처음에는 내가 성숙한 개발자가 된 것 같아 기분이 좋았다. 그런데, 시간이 갈수록 뭔가 문제가 있다는 생각이 들었다.


자연스레 예전 모습을 기억해보았다. 예전에는 프로젝트 중 10~20%정도는 기술적으로 도전하곤 했었다. 도전이라는 것은 어떤 면에서는 적정엔지니어링을 하지 않았다는 뜻이다. 투자 관점의 합리적인 엔지니어링보다는 단순히 기술적 도전을 위한 엔지니어링을 시도했다. 이런 도전을 통해 재미를 느꼈고 새로운 것을 배웠다. 그런데, 적정엔지니어링에 몰입하고 난 후에는 항상 비즈니스를 고려한 최적의 결정만을 하려 노력했다. 자연스레 예전 습관이었던 10~20% 기술적 도전은 사라졌다. 이로인해 단기간에 내가 할 수 있는 최선의 결과를 냈는지는 모르겠지만, 장기적으로는 재미를 잃어가고 한편으로 지쳐갔다.


나는 적정엔지니어링은 성숙한 개발자라면 꼭 해야한다고 생각해 실천했었고, 지금도 이 생각은 변함없다. 그럼에도 한편에서 개발자는 항상 도전하는 게 반드시 필요하다고 생각한다. 특히 개인적 시간에 따로 도전하는 것도 좋지만, 업무 영역에서 많이 도전해야 한다고 생각한다. 경험상 업무적 도전이 훨씬 현실적이고 배우는 게 많기 때문이다. 따라서 적정엔지니어링과 오버엔지니어링 모두 조화롭게 추구하는 게 좋다고 생각한다. 적정엔지니어링을 너무 심하게 하는 것도, 오버엔지니어링을 너무 심하게 하는 것도 금물이라 본다. 프로젝트 위험도나 본인의 상황에 따라 틀리겠지만 적절한 비율은 8:2 혹은 9:1 정도가 아닐까 싶다. 물론 많은 편이 적정엔지니어링이다.[1]


6. 요약


사람들은 누군가 불필요한 엔지니어링을 한다고 판단할 때 오버엔지니어링이라 부르며 부정적으로 생각한다. 엔지니어링은 어떤 면에서 미래를 위한 투자로 볼 수 있다. 이런 투자가 과열될 때 오버엔지니어링이 되기 쉬운 것 같다. 그래서 개발자는 오버엔지니어링을 하기보다는 적절한 수준의 엔지니어링을 뜻하는 적정엔지니어링을 추구할 필요가 있다고 본다. 하지만, 적정엔지니어링만 추구하다보면 개발의 재미가 없어지고 발전도 더뎌지는 것 같다. 따라서 장기적인 관점에서 개발자는 어느정도의 오버엔지니어링을 통해 본인의 재미와 발전을 추구하는 것이 좋다고 생각한다.


[1] 단순히 프로젝트를 하는 것만으로도 많이 배우는 상황에 있는 개발자에게는 해당하지 않는 내용이다.


* 이어지는 글 '최소엔지니어링에 대한 생각' : http://wave.ivorypen.com/minslovey/5



WRITTEN BY
차민창
르세상스 엔지니어가 사회를 이끌어나가는 상상을 하며!

,
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

약 한달 전 새로운 팀으로 옮겼다. 동료들이 반갑게 맞아주며 약 2주 간은 천천히 소스코드도 읽어보고 기존 업무를 파악하라고 한다. 사실 기존 조직에서 여러모로 지쳐있는 상태에서 왔기 때문에 좋은 기회라고 생각했다. 천천히 소스코드도 읽고 그동안 했던 생각도 정리해보며 하루 하루를 나름 알차게 보냈다. 그런데, 얼마 지나지 않은 금요일에 갑작스레 일을 맡게 되었다.


1. 첫 번째 시도


우리 서비스는 RabbitMQ를 사용하고 있다. 대부분의 서비스는 장비 중 한 대에 문제가 생길 때도 정상적인 서비스를 하기 위해 최소한 장비 2대 이상을 클러스터링 해서 사용하는데, 우리 서비스도 L4 스위치와 RabbitMQ 2.3.1 자체의 클러스터링 기능을 이용해 이런 부분을 염두에 두고 있었다. L4 스위치는 클라이언트(Queue 구조에서 Publisher)에서 도메인명으로 MQ 서버에 접근했을 때 연결(Connection)을 분산시키는 역할을 하고 있었고, RabbitMQ 자체의 클러스터링은 두 독립적인 노드가 마치 하나의 MQ 인스턴스처럼 동작하게 하는 역할을 하고 있었다. 그런데, 어느날 시스템 팀에서 메모리 불량으로 MQ 1번 장비가 언제 문제가 생길지 모르니 가급적 신속히 교체 작업을 진행하자고 했다. 옆 자리 과장님은 RabbitMQ가 클러스터링 되어 있음을 알았기 때문에 바로 장비 1대를 중단했다. 별 문제가 없을 것이라 생각했지만 혹시나 해서 MQ를 사용하는 클라이언트의 로그를 보았다. 그런데, 놀랍게도 MQ 관련 오류 메세지가 수도 없이 올라오고 있었다. 과장님은 놀라 바로 중단했던 장비를 즉시 재기동 시켰다. 좀 살펴보던 과장님은 머리가 아팠는지 옆 자리에 있던 내게 도움을 부탁했다. 나도 사실 RabbitMQ를 한번도 써보지 못햇다. 하지만, 어쨌든 함께 문제를 살펴보기 시작했다.


2. 두 번째 시도


RabbitMQ에 대한 지식이 별로 없었음에도 신속히 교체를 해야했기 때문에 내부 동작을 파악하기 위한 충분한 시간이 부족하다 생각했다. 따라서 내부 이해에 근거한 접근보다는 블랙박스 식의 접근으로 문제를 해결하려 시도했다. 다시 말해 RabbitMQ 테스트 장비에서 여러 동작을 해보며 드러나는 결과를 관찰한 것이다. 목적은 문제 없이 장비 한대를 중단하는 것이기 때문에 어떻게 하면 서비스에 지장없이 장비를 중단할 수 있을지 여러 가지 방식으로 테스트 해보았다. 테스트를 하며 알아낸 사실은 MQ 1번 장비(마스터)를 중단하면 선언 된 Queue 등이 사라지며 문제가 생기지만, MQ 2번 장비(슬래이브)를 중단하면 아무 문제도 생기지 않는다는 점이었다. 이 결과를 확인하고 몇몇 문서를 참고한 후 '마스터가 Queue 등에 대한 메타 정보를 갖고 있기 때문에 마스터만 중단되지 않으면 서비스가 된다'라는 전제를 갖게 되었다. 일반적인 마스터-슬래이브 구조에서 마스터는 메타 정보를 관리하고 이에 따라 SPOF(Single Point Of Failure)가 되기 때문에 이 전제는 보편성이 있다고 생각했다. 이 전제에 따라 아래와 같은 교체 계획을 세웠다.


1) MQ2(슬래이브)를 중단하고 L4에서 제거한다.

이때 MQ2에 연결되어 있는 클라이언트는 연결이 끊긴 후 재연결을 시도할 것이고, 이 때 L4에서는 MQ2가 중단된 것을 인지하고 자연스레 MQ1로 연결을 해줄 것이다. MQ1로 연결이 완료되면 MQ1이 혼자 서비스한다.


2) MQ1과 MQ2의 클러스터 관계를 해제한다. 

이로인해 더이상 MQ1과 MQ2는 관계가 없게 된다.


3) MQ2의 클러스터링 설정을 초기화하고 MQ2를 마스터로 변경한다. 또한 Queue 선언 등을 MQ1과 동일하게 해 마스터로써 서비스 할 수 있게 준비시킨다. 즉, 새로운 마스터를 만드는 것이다.


4) MQ2를 다시 L4에 투입한다.


5) MQ1을 L4에서 제거하고 MQ1의 RabbitMQ를 중단한다.

이 때 1번과 같은 이유(재연결, L4)로 자연스레 MQ2로 재연결이 된다.


6) 메모리 교체가 끝나면 MQ1을 다시 MQ2의 슬래이브로 편입시킨다.


위 시나리오를 기반으로 베타환경에서 테스트를 시작했다. 혹시나 해서 여러 번 테스트를 했는데 테스트가 매우 잘 되었다. 어느정도 확신이 생겼고 리얼환경에 적용하기로 했다. 그런데 리얼환경에서 위 시나리오 중 1번 부분을 적용하는 순간 선언 된 Queue을 찾을 수 없다는 오류가 무수히 발생하며 다시 롤백을 해야했다. 


왜 그런것일까? 다시 원인을 살펴보았다. 이런저런 부분을 상세히 살피다보니 원인은 Queue가 선언되는 노드의 위치에 있었다. RabbitMQ에는 Queue를 선언 할 때 해당 Queue가 선언 될 노드를 지정할 수 있다. 리얼환경을 상세히 살펴보니 Queue선언 중 일부는 MQ1에 선언되있었고, 나머지는 MQ2에 선언되어 있었다. 이로인해 MQ2를 중단하자 MQ2에 선언 되있던 Queue선언을 찾을 수 없다는 오류가 발생한 것이다. 그렇다면 베타에서는 왜 문제가 없었지? 살펴보니 베타환경에는 모든 Queue가 MQ1번에 선언되어 있었다. 결국 현상만 보고 판단해 세운 "마스터가 중단되지 않으면 서비스가 가능하다"라는 전제는 완전히 잘못되었던 것이다.



# 그림1. 클러스터가 중단되면 Queue도 없어지는 현상


또 한번의 교체 실패로 머리가 아파왔다. 마음이 급했지만 이 상황을 어떻게 해결할지 침착하게 생각해보았다. 조금 방법을 찾아보면 현재 상황에서 어떻게든 교체를 할 수 있을 것 같았다. 하지만, 간단히 보였던 일이 커진 이유를 생각하자 생각이 바뀌었다. 즉, 근본적인 문제는 클러스터링이 제대로 동작하지 않는다는 점이었다. 이번에는 어떻게든 상황을 넘어갈 수 있지만, 이후 또 비슷한 일이 생겼을 때 지금처럼 일이 어려워 질 것이라 생각했다. 그래서 차라리 이번 기회에 클러스터링을 제대로 구성해보자 마음먹었다.


3. 세 번째 시도


결국 시스템팀에 상황을 얘기하고 잠시 교체를 미루기로 했다. 이번에는 클러스터 노드 중 하나가 중단되도 잘 동작하는 진짜 클러스터링 환경을 구성하기로 마음먹었다. 찾아보니 RabbitMQ 2.6부터 HA(High Availability)가 강화되었음을 확인했다. 


The RabbitMQ team is pleased to announce the release of RabbitMQ 2.6.0. The highlight of this release is the introduction of active-active HA, with queues getting replicated across nodes in a cluster. (생략)

http://www.rabbitmq.com/news.html


HA 관련 문서를 상세히 읽다보니 두 번째 시도에서 겪었던 문제도 이미 언급이 되어 있었다. 기존에도 읽었던 부분이지만 급하게 읽다보니 미처 인지하지 못했던 부분이었다.


You could use a cluster of RabbitMQ nodes to construct your RabbitMQ broker. This will be resilient to the loss of individual nodes in terms of the overall availability of service, but some important caveats apply: whilst exchanges and bindings survive the loss of individual nodes, queues and their messages do not.


To solve these various problems, we have developed active/active high availability for queues. This works by allowing queues to be mirrored on other nodes within a RabbitMQ cluster. The result is that should one node of a cluster fail, the queue can automatically switch to one of the mirrors and continue to operate, with no unavailability of service.

http://www.rabbitmq.com/ha.html


RabbitMQ에서는 위에서 언급 된 HA가 고려 된 Queue를 Mirrored Queue라고 부르는데 단어 그대로 Queue를 여러 노드에 걸쳐 Mirroring 해주는 기능이다. 따라서 한 노드가 중단 되어도 Queue 선언과 Queue에 담긴 메세지가 다른 노드에 이미 복사되어있기 때문에 문제 없이 서비스가 가능하다. 혹시 모르니 테스트 클러스터 환경을 구성해 RabbitMQ 2.6 이상의 최신버전을 설치하고 다양한 방법으로 노드를 중단해보며 여러 테스트를 해보았다. 마스터 노드든 슬래이드 노드든 중단되도 Queue 선언이나 Queue 안에 들어있는 메세지가 잘 보존되고 잘 처리됨을 확인했다. 이 결과에 따라 아래와 같은 교체 계획을 다시 세웠다.


1) 새로운 MQ 장비 2대를 준비하고 해당 장비에 대한 새로운 도메인(L4 바인딩 포함)도 준비한다.

2) MQ 장비에 Mirrored Queue를 제공하는 RabbitMQ 최신버전을 설치하고 클러스터링 한다.

3) 베타환경에서 신규 MQ 도메인으로 연결해 테스트를 진행한다.

4) MQ를 참조하는 모든 프로젝트에서 기존 MQ 도메인을 신규 MQ 도메인으로 바꾸어 배포한다.

5) 기존 MQ 클러스터는 제거한다.


결국 위 계획은 성공했다. 처음 예상했던 것보다 훨씬 많은 시간을 소모했지만 서비스를 중단하지 않고 MQ 클러스터를 교체한 것이다. 현재 위와 같은 구성으로 바뀐지 약 한달이 넘었지만 큰 문제 없이 잘 동작하고 있다. 앞으로는 위와 같이 급하게 장비를 교체 할 일이 생겨도 쉽게 할 수 있을 것이라 기대하고 있다.

# 그림2. 클러스터가 중단되었음에도 Mirrored Queue 때문에 Queue가 유지 됨


4. 관련 설정 및 몇 가지 이야기

 

고가용성을 보장하려면 RabbitMQ의 2.6 이상의 버전을 사용하는 게 좋고, Queue는 선언 시 반드시 HA 설정(Mirrored Queue)을 해줘야 한다. 


4-1) Spring-AMQP(Spring-Rabbit)


아래와 같이 Queue를 선언할 때 x-ha-policy를 all로 주면 된다. 


<rabbit:queue name="hello.string">

<rabbit:queue-arguments>

        <entry key="x-ha-policy" value="all" />

</rabbit:queue-arguments>

</rabbit:queue>


<rabbit:queue name="hello.object">

<rabbit:queue-arguments>

<entry key="x-ha-policy" value="all" />

</rabbit:queue-arguments>

</rabbit:queue>


혹은 코드에서 Queue를 선언한다면 아래와 같이 Parameter를 추가해주면 된다.


Map<String, Object> args = new HashMap<String, Object>();

args.put("x-ha-policy", "all");

channel.queueDeclare("myqueue", false, false, false, args);


4-2) RabbitMQ Management Web UI


Queue를 선언할 때 Mirror 필드의 값을 "Across All Nodes"로 선택해주면 된다.


4-3) Mirrored Queue는 Replication이 아님


문서에 HA가 등장하고 요즘에는 Replication을 기본적으로 지원하는 NoSQL류를 많이 살펴보다보니 Mirrored Queue가 Replication 처럼 동작한다고 착각했었다. 내가 이해한 Mirrored Queue와 Replication의 차이점은 이렇다. 


노드 A가 있고 아직 처리되지 않은 메세지가 10개 있다고 가정해보자. 이때 노드 B가 새롭게 클러스터에 참여한다. 잠시 후 노드 A가 예기치 않게 중단된다. 그럼 이 때 노드 B에 메세지가 있을까? 만약 Replication 이고 노드 B가 참여한 후 노드간에 Replication이 이뤄질 시간이 있었다면 노드 B에는 10개의 메세지가 보존되어 있을 것이다. 하지만, Mirrored Queue는 메세지가 없을 것이다. 왜냐하면 Mirrored Queue는 메세지가 들어올 때 전 노드에 복사(Mirroring) 하는 방식이기 때문이다. 위 10개 메세지가 들어올 당시 노드 B는 클러스터에 없었기 때문에 메세지는 존재하지 않는다.


4-4) 무손실 배포


이번에 새로운 클러스터를 도입하며 고민해본 부분은 어떻게 하면 무정지/무손실로 배포를 할지였다. 우리쪽에 MQ를 이용하는 곳을 생각해 보면 일반적인 MQ 모델처럼 Publisher와 Consumer가 있다. 만약 Publisher와 Consumer를 신규 도메인을 보도록 변경해 그냥 동시 배포한다면, 기존 MQ 클러스터에 쌓여있는 메세지는 더 이상 처리가 안 될 수 있다. 이런 이유로 배포 시 아래와 같은 단계로 배포를 진행했다.


1. Publisher 배포

앞으로 새롭게 유입되는 메세지는 신규 MQ 클러스터에 쌓인다.


2. 기존 MQ 클러스터의 메세지가 모두 처리되었음을 확인

이 시점에서 신규 MQ 클러스터에는 메세지가 쌓이고 있을 것이다.


3. Consumer 배포

Consumer를 배포하고 신규 MQ 클러스터에 쌓인 메세지를 처리하게 한다.


배포 중 새롭게 유입 된 메세지는 약간의 처리 지연 시간이 생기겠지만 메세지 손실 없이 배포가 가능하다.



WRITTEN BY
차민창
르세상스 엔지니어가 사회를 이끌어나가는 상상을 하며!

,