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 크기의 광고 코드만 넣을 수 있습니다.

최근 메일, 전화, 소셜 네트워크를 통해 동료 개발자의 퇴사소식이 들린다. 마음이 무척 심란하고, 특히 몇년동안 사내에서 얼굴을 봐온 동료의 퇴사 소식에는 진한 아쉬움이 느껴진다. 이런 와중 누군가 내게 묻는다. 너는 힘들다고 하며 왜 여기에 계속 있냐고 말이다. 또 누군가는 내게 조언한다. 너는 타성에 젖어 한 자리에 머물러 있음으로 인해 개발자로써 경쟁력을 잃어가고 있다고 말이다. 그 외에도 많은 얘기가 있다. 그런데, 이런 얘기는 모두 개발자로써 하는 고민과 맞닿아 있다는 생각이 든다. 그래서 나는 최근 경험한 대화를 기초로 해 개발자로 자주 고민하고 되는 주제인 이직과 경쟁력에 대해 가지고 있는 생각을 정리해보고자 한다. 


1. 이직 혹은 이동에 대한 생각


내가 생각하는 이상적인 이직은 내가 하고 싶은 일이 있는데 현재의 환경에서는 할 수 없어 부득불 이직을 하는 것이다. 반대로 가급적 피하고자 하는 이직은 현재의 일이 너무 싫어서 이를 피해 새로운 환경으로 이직하는 것이다. 그동안의 개발자 경력 중 이직을 많이 해보진 않았지만 비슷한 점이 많은 팀 이동은 많이 해보았다. 지금 생각나는 것만 해도 약 3~4 차례 팀 이동이 있었다. 나는 그때마다 파랑새를 꿈꾸었다. '여기는 정말 아닌 것 같다. 봐봐. 저쪽팀은 사람이 너무 좋아보여. 정평이 난 인품 봐봐. 환경도 굉장히 자유롭네? 그래, 결심했다. 여기서 구질구질질하게 스트레스 받으며 내 인생을 허비하지 말고 가보자.


결과는 어땠을까? 안타깝게도 파랑새를 찾은 경험은 없다. 막상 이동을 해보면 외부에서 보이던 장점이 허구였던 적도 있고, 실제로 장점이 있더라도 그 장점을 덮을만한 단점이 더 많은 때도 있었다. 그럴때면 내 잘못된 선택에 괴로웠고, 다시 다음 파랑새를 찾아보곤 했다. 그런데 파랑새를 찾는 경험이 반복되며 이상한 느낌이 들기 시작했다. 침착하게 상황을 되돌아보며 자연스레 "혹시 내가 잘못된 게 아닐까?" 라는 생각을 하게 되었다. 왜냐하면 아무리 좋아보이는 곳을 가도 나는 만족하지 못했으며, 계속해서 어려움이 있었기 때문이다. 따라서 환경이 아닌 내게 초점을 맞춰 생각을 해보기 시작했다.


예전에는 어떤 어려움이 생기면 대게 외부 환경 중 하나를 비난했다. 예를 들어 내 자신과 관리자와의 업무 관계에서 어려움이 생기면 관리자를 비난했던 것이다. 대게 나는 관리자가 잘못했다고 믿었고 관리자가 변화하길 바랬다. 그러나, 이제는 관리자를 지금 그대로 인정해보았다. '관리자는 저럴만한 사정이 있다. 그런데, 난 어려움이 있다. 어려움을 해결하기 위해 '내가 할 수 있는 일'은 무엇인가?' 이런 생각의 전환 후 많은 것이 달라졌다. 문제가 있을 때 내가 할 수 있는 것을 고민하기 시작했던 것이다. 물론 쉬운 일은 아니었다. 하지만, 최소한 어려움이 생겼을 때 비난 하는 것보다는 훨씬 낫다는 생각이 들었다.


위와 같이 태도를 바꾼 후 내 자신이 긍정적 방향으로 바뀌고 있음을 강하게 느꼈다. 예전이라면 불평불만 할 상황에서 오히려 더욱 고민하며 전진할 수 있게 된 것이다. 점점 더 단련되고 성숙해지는 느낌을 받았다. 하지만, 안타깝게도 내 개인적 경험에서 이런 시도는 좋은 결실을 맺지 못했다. 열심히 해보았지만 언제부터인가 결과는 안 보이고 내 자신만 너무 지쳐갔다. 내 노력만으로는 한계가 느껴졌던 것이다. 이렇게 되자 다시 생각을 하게 되었다. 환경을 탓하며 파랑새를 꿈꾸던 시절이 극단이었다면, 환경을 제외하고 내 자신에만 초점을 맞추는 것도 또 다른 극단이라는 생각이 든다.


예로 박지성 선수가 있다. 이 선수는 환경에 대해 불만을 터트리지 않는다. 오히려 감독, 팀 상황 등을 그대로 인정하고 이런 환경에서 본인이 어떻게 해야할지 고민한다. 예로 감독이 선발출전을 시키지 않을 때 어떤 선수는 감독에게 불만을 터트린다. 하지만, 이 선수는 언젠가 올 기회를 준비하며 더욱 몸상태를 끌어올리는 데 집중한다. 그런데, 이 선수가 일년 전 팀을 옮긴 후 본인은 특별한 변화가 없음에도 부진한 상태이다. 아마 팀에 영향을 받았을 것이다. 한편 부진하던 선수가 팀을 바꾼 후 모두가 놀랄만한 결과를 보여주는 때도 잦다. 이런 사례들를 볼 때 환경과 개인의 조화가 정말 중요하다는 생각이 든다.


앞서 하고 싶은 일을 찾아 떠나는 이상적인 이직에 대해 언급했다. 그러나, 실제 현실에서는 어려움을 겪기 때문에 이직을 생각하는 때가 잦은 것 같다. 실제로 많은 동료들이 후자에 가까운 방식으로 떠나갔다. 몇 개월 뒤 동료들을 만나 대화를 나눠보면 내간 느낀 것처럼 역시 파랑새는 없었다. 새로운 곳에서 새로운 어려움이 생기고 그에 따라 새로운 국면에서 씨름하게 될 뿐이다. 따라서 이직이라는 것은 파랑새를 찾는 방법으로 보지 않는 게 좋을 것 같다. 그것보다는 본인이 현재 환경에서 최선을 다해 어려움을 헤쳐나가 보았음에도 도무지 만족스럽지 않을 때 나에게 좀더 잘 어울리는 환경을 찾는 방법으로 보면 어떨까 싶다. 좋고, 나쁜 환경이 아닌 본인에게 어울리는 환경 말이다.


2. 개발자 경쟁력에 대한 생각


난 개발자는 계속해서 발전해야 한다고 생각한다. 계속 발전해야 개발이 계속 즐거울 것이기 때문이다. 지난 몇년간 개발을 하며 느낀 점이 있다. 바로 처음에는 경이롭고 신선한 개발적 경험이 시간이 지나 숙련도가 높아질수록 단순 노동처럼 느껴진다는 것이다. 발전을 할수록 여러 가지 즐거움이 많아진다. 적절한 곳에 새로운 기술을 도입하며 즐거움이 생길 수도 있고, 오랫동안 사용해온 기술이라도 전혀 다른 시각으로 접근해 사용해보다 즐거움이 생길 수도 있다. TDD를 통해 색다른 즐거움이 생길수도 있고, 가독성 높은 코드를 위한 노력을 하며 일상의 업무에 예기치 않은 기쁨과 보람이 생길 수도 있다.


하지만, 위에서 이직에 대해 얘기한 것처럼 환경으로 인한 영향력은 피하기 어려운 편이라고 생각한다. 예로 내 상황을 생각해본다. 나는 지난 몇년 간 일상적인 업무가 일상적인 업무가 되지 않도록 노력해왔다. 그럼에도 돌아보면 나는 내가 일하는 주요 환경이라 할 수 있는 '포털 서비스 개발'이라는 환경을 뛰어넘지 못했던 것 같다. 예를 들어 난 게임에서 필요한 그래픽 프로그래밍에도 관심이 있지만, 지난 몇년 간 이런쪽의 자료를 거의 보지 못했다. 왜냐하면 내 고민의 대부분은 '포털 서비스 개발'에 속해있었고, 이곳에서 생기는 주제만 가지고 고민하고 또 고민해봐도 해결되지 않는 문제가 많았기 때문이다.


이런 상황을 보며 누군가가 해준 조언인 '타성으로 인한 경쟁력 저하'에 대해 다시 고민해본다. 정말 나는 한곳에 오래 있음으로 인해 타성이 생기고 경쟁력이 저하되고 있는걸까?  벤처 회사에 가서 게임 개발을 하게 되면 문제가 해결될까? 나로써는 아직 경험해보지 못한 미지의 영역이라 뭐라 말하기가 어렵다. 한번 상상해본다. 난 3D 클라이언트를 맡아 생전 처음으로 그래픽 프로그래밍을 하게 된다. 아는 것이 없어 너무 어렵다. 열심히 공부하고 또 공부하며 코드를 조금씩 작성해 나간다. 어려움이 많지만 즐거움도 많다. 그래픽 분야에 대해 많이 알아가며 발전을 하고 있는게 강하게 느껴진다.


그런데, 그동안 내가 쭉 고민해왔던 포털 서비스 개발은 어떻게 되는 것일까? 이 쪽도 발전이 있을까? 어쩌면 그래픽 프로그래밍이라는 완전히 새로운 환경이 기존에 고민했단 포털 서비스 개발에 대해 영감을 주어 모든 분야에 걸쳐 더욱더 발전하는 계기가 될 수도 있을 것 같다. 하지만, 이는 이상적인 상황이고 현실적으로는 포털 서비스 개발에 대한 고민을 잃어버린 채 그래픽 분야에 대한 발전만 있을 확률이 높을 것 같다. 이렇게 된다고 가정하면 난 개발자로써 더 발전한 것일까? 만약 그냥 포털 서비스 개발에 대해 계속 고민하며 그쪽 분야에서 더 많은 경험과 지식을 갖게 되면 그것도 발전한 거 아닌가? 잘 모르겠고 확신할 수 없다. 너무 많은 상황이 있을 것 같다.


사실 내가 겪는 확실한 문제는 점점 개발의 즐거움이 적어진다는 점이다. 즐거움이 적어짐과 동시에 발전도 더뎌지고 있다. 즉, 경쟁력이 저하되고 있는 것이다. 이는 발전을 원하는 개발자로에겐 매우 치명적인 문제라고 생각한다. 원인이 무엇일까? 누군가 조언한 것처럼 타성 때문일까? 예전에는 분명히 내 개인의 노력을 통해 어느정도 개발의 재미가 생기곤 했었다. 그런데, 지금에 이르러선 이런 재미가 많이 없어졌다. 개발에 대한 열의가 떨어져서 일까? 그렇다면 열의는 왜 떨어질까? 나이가 듦에 따라 자연스레 떨어지는 것이라면 어쩔 수가 없다. 하지만, 어떤 요인으로 인해 떨어지는 것이라면 그 원인을 찾아서 개선해 볼 수 있을 것 같다. 


결국 분명한 것이 있고 분명하지 않은 것이 있다. 분명한 것은 내가 개발자로써 해결해야 할 문제가 있다는 것이다. 분명하지 않은 것은 이런 문제가 발생하는 원인이다. 확신할 수 없기에 무엇인가 결론을 내리기가 어렵다. 하지만, 문제가 있다는 것은 분명하기에 뭐라도 시도하는 편이 좋을 것 같다. 이 문제를 좀더 탐색하기 위해 탁상공론 하기 보다는 내 자신을 과감히 새로운 환경에 던져 보는 것도 좋을 것 같다. 그후 불안한 마음과 낯섬을 느끼며 새롭게 시작하는 내 자신을 관찰해 본다면 이에 대한 답에 가까워 질 수 있을 것 같다. 이 문제는 꽤 오래된 문제이기 때문에 어떤 시도든 적극적으로 해보는 게 좋겠다는 생각이 든다.


3. 정리


글을 쓰다보니 각 주제는 참 복잡하다고 느껴진다. 예전 페이스북에 너무 많은 통찰은 결정을 방해한다는 글을 쓴적이 있다. 그런데, 나 역시 같은 상황을 겪고 있는 것 같다. 어렸을 때 자바를 공부하게 된 계기가 있다. 당시 C++과 자바 사이에서 어떤 것에 시간을 더 투자해 공부할지 고민했었다. 나는 지식도 경험도 없었다. 그래서 그냥 '더 마음이 내켰던' 자바를 공부하기로 결정했다. 지금 생각해보면 인생에 있어 굉장히 중요한 문제였음에도 빠르게 결정 했었다. 이 결정은 지금까지 아무 문제가 없었고, 오히려 매우 잘한 결정이라 생각하고 있다. 어쩌면 위 문제도 직관을 믿고 과감히 행동함으로써 해결해야하는 종류의 문제일지도 모르겠다.


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

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

예전에 회사의 이사님 중 한 분이 "좋은(Good) 개발자를 넘어 탁월한(Great) 개발자가 되려면 무엇이 필요한가?"라는 주제로 에세이를 제출하라고 한적이 있었다. 당시 나도 해당 주제에 대해 고민한적이 많아 6가지 정도 주제로 간단히 에세이를 제출했는데, 당시 짧게 써 내긴 했지만 그때 미처 담지 못했던 생각이 더 있어 블로그에 정리해볼까 한다. 아래는 당시에 이사님께 제출한 글 중 일부이며, 이 글의 요약본이기도 하다.

3. 의사소통을 잘하지 못하는 개발자를 배려해주시면 좋겠습니다.


기술적인 열정을 가지고 있으며 기술력이 좋은 개발자는 이상하게 의사소통 능력이 떨어지는 경우가 많습니다. 따라서 회사에 필요한 가치를 많이 만들고 있으면서도 그런 것을 잘 드러내지 못합니다. 반면 의사소통에 보다 신경을 쓰는 개발자는 상대적으로 덜한 가치를 만들면서도 그것을 매우 잘 드러내고 그에 따른 보상을 받습니다. 이때 의사소통 능력이 부족한 개발자는 상대적 박탈감을 느끼게 됩니다. 왜냐하면, 본인이 더 많은 가치를 만들었다고 생각하기 때문입니다.


의사소통 능력이 부족하다는 것이 자랑할 바는 아니지만, 회사에서 이런 개발자들이 상대적 박탈감을 가지지 않도록 특별히 배려해준다면 좋을 것 같습니다. 어떤 면에서 순수하다고 볼 수 있는 이런 사람들이 탄탄히 기술적 기반을 가지고 의사소통 능력도 향상시키며 성장하면 회사에 매우 큰 도움이 될 것이라 생각합니다.


1. 개발자의 의사소통 능력이란?


가끔 개발자의 의사소통 능력이 무엇일까에 대해 생각해보곤 한다. 개발자에게 있어 의사소통 능력이란 무엇일까? 엄밀한 정의는 어려울테고, 단순히 내가 일하면서 느낀 것 위주로 생각해보면 세 가지 정도 예시를 들 수 있을 것 같다. 첫째는 개발자 간에 특정 주제를 논의하거나 공유하는 것이다. 이런 종류의 의사소통은 본인과 동료가 함께 잘하고 함께 발전할 수 있게 한다는 점에서 유익하다고 생각한다. 둘째는 기획자 등 협력하는 부서의 동료와 원활하게 일하는 것이다. 예로 기획자의 말을 잘 이해하는 것, 기획서를 잘 읽는 것, 기술적인 이슈를 기획자에게 이해시키는 것 등이 있을 것 같다. 이러한 능력은 함께 좋은 제품을 만들어가는 데 무척 중요하다고 본다. 셋째는 회사의 방향을 고려하는 능력이다. 이를 의사소통 능력이라고 얘기해야 하는지 약간 의문이 들지만, 회사의 핵심 목표를 이해하고 그에 걸맞게 사고하고 판단하며 일한다는 관점에서 보면 기획자가 쓴 기획서를 잘 읽는 것과 큰 차이가 없을 것 같다. 


내가 그동안 만나 함께 일해왔던 동료들을 머리속에 떠올려 본다. 의사소통 능력과 개발 실력이 모두 뛰어난 개발자가 많았으면 좋았겠지만 이런 유형의 개발자는 무척 드물었다. 반면 개발 능력에 강점을 보이는 개발자(의사소통 능력은 떨어지는)와, 의사소통 능력에 강점을 보이는 개발자(개발 능력은 떨어지는)는 종종 볼 수 있었다. 이 두 유형의 개발자는 현실에서 자주 비교가 되었고 이로인한 여러 사건이 있었다.


2. 내가 보고 느낀 현실


그동안 내가 봐왔던 대부분의 조직에서는 의사소통 능력이 뛰어난 개발자가 더 인정받고 더 빨리 승진했다고 생각한다. 반면 개발 능력이 뛰어난 개발자는 비교적 인정받지 못했으며 저평가를 받을 때도 잦았다. 흥미로운 점은 대게 관리자는 의사소통 능력이 뛰어난 개발자를 높히 평가했지만, 동료들은 개발 능력이 뛰어난 개발자를 더 높히 평가했다는 점이다. 또한, 의사소통이 뛰어난 개발자는 자의가 아님에도 종종 다른 개발자의 노력을 빼앗는 듯한 상황을 연출하기도 했다. 적극적으로 업무에 관한 의사소통을 하다보니 본인이 하지 않거나 관여만 했던 일임에도 불구하고 본인이 주도해서 이뤄낸 것으로 다른 사람이 오해하게 만들었기 때문이었다. 이 때 개발만 묵묵히 해왔던 개발자는 어느순간 본인이 재주넘는 곰과 같은 신세인 것 같다는 몹시 쓰라린 느낌을 받곤 했다. 이는 큰 문제였다. 왜냐하면, 이런 느낌을 받은 개발자가 급격히 의욕이 꺽이기도 했기 때문이다.


이런 현실을 보며 스스로 많은 고민을 해보았다. 왜 의사소통을 잘하는 개발자가 개발을 잘하는 개발자보다 대게 더 인정받을까? 내가 일하는 곳은 서비스를 운영하는 곳이었기 때문에 철저히 서비스 관점에서 생각해보았다. 두 개발자 유형 중 서비스에 누가 더 기여하고 있는가? 하지만, 냉정하고 정직하게 생각해볼 때 대답하기는 무척 어려웠다. 왜냐하면 제품을 만드는 데는 적절한 의사소통과 개발 모두 중요했기 때문이다. 


3. 의사소통 능력이 부족한 개발자에 대한 배려


의사소통 능력이 좋은 개발자는 어쨌든 본인이 손해보지 않게 어떤 상황이든 잘 헤쳐나갈 수 있는 능력이 있다고 생각한다. 문제는 의사소통 능력이 부족한 개발자이다. 묵묵히 일하며 많은 가치를 만들면서도 그런 것들을 잘 알리지 못하고 결국 화려한 개발자 뒤에 가려지고 손해 볼 때가 있기 때문이다. 손해를 보아도 그런 것들을 얘기해서 해결하는 데 익숙하지 않기 때문에 또 다시 손해를 보기도 하는 것 같다. 나는 그래서 이런 개발자에겐 특별한 배려가 필요하다고 생각한다. 아래는 이런 종류의 배려가 있으면 어떨까라는 부분을 정리해본 것이다.


* 본인이 잘 드러내지 못해도 관리자나 동료가 그가 팀에서 하는 역할과 기여하는 바를 구체적으로 알아주면 좋겠다.

대게 의사소통 능력이 떨어지는 개발자는 본인이 하는 일을 잘 드러내지 못하는 것 같다. 따라서 많은 가치를 만듬에도 불구하고 주위(특히 관리자)에서는 그 사람이 무슨 가치를 만드는지 잘 모를 때가 있다. 가까운 곳에서 일하는 동료는 이런 개발자를 높이 평가하지만, 현실적으로 여러모로 손해를 볼 때가 잦은 것 같다. 만약 주위에서 이런 개발자의 가치를 제대로 알아준다면, 얼마나 좋을까?


* 본인이 개발 역량 중심으로 발전해나가도 팀에 충분히 기여할 수 있다는 믿음을 주면 좋겠다.

다른 사람들은 개발도 어느정도 하고 의사소통도 잘하는 것 같은데, 난 맨날 기술적인 내용만 공부를 하고 있어도 될까? 사회인으로써 직장인으로써 필요한 다른 것도 많이 배워야 하지 않을까? 라는 의문이 들 수 있을 것 같다. 만약 본인이 개발 역량을 쌓는 것이 명확하게 팀에 도움이 된다는 것을 알게되고 실제로도 그렇다면 좋을 것 같다. 


물론 기술적인 내용 외 하는 고민이 나쁘다는 것은 아니다. 다만, 이런 것을 고민하다 오히려 개발 역량보다는 다른 것에 집중하게 될 수 있는 것이 문제라고 생각한다.


* 일을 할 때 기술적인 책임과 권한을 주면 좋겠다.

개발 능력이 뛰어난 개발자가 본인의 일에 대한 기술적인 책임과 권한을 갖는 것은 굉장한 기쁨일 것 같다. 어쩌면 이것 하나만으로도 충분히 만족스러워 할 수도 있을 것 같다.


* 본인이 공정하게 대우 받고 있다고 느끼게 해주면 좋겠다.

위에서 나온 예처럼 본인이 재주넘는 곰처럼 누군가에게 이용당하고 있다고 느끼면 의욕이 많이 떨어질 것 같다. 주위에 의사소통 능력이 뛰어난 개발자가 있어도 의사소통을 통해 만드는 가치만큼 본인이 개발을 통해 많은 가치를 만든다면 공정하게 대우 받을 수 있다는 믿음이 있으면 좋을 것 같다.


* 의사소통을 비롯해 떨어지는 능력을 향상시킬 수 있게 도와주면 좋겠다.

때때로 개발에만 너무 집중하다보니 본인이 어떤 능력이 부족한지 잘 모를수도 있을 것 같다. 따라서 본인이 개선해야 할 점을 정확하게 알려주면 좋을 것 같다. 다만 기본적으로 가지고 있는 강점은 계속해서 발전시키는 것을 전제로 하면 좋겠다. 위에서 얘기했듯이 다른 것에 더 많은 신경을 쓰다 오히려 본인의 강점을 잃어버릴 수도 있다고 생각하기 때문이다.


4. 맺음말


의사소통 능력은 떨어지지만 개발에 특별한 강점이 있는 개발자에게 이상적인 회사란 어떤 모습일까 상상해본다. 말투가 어눌한 개발자가 있다. 그런데 그 사람이 몇몇 기술적 회의에서는 날카로운 모습을 보여준다. 다른 사람은 그가 하는 기술적인 견해에 귀를 귀울이며, 혹 뭔가 정리가 안 되어 말을 제대로 못해도 더 많은 것을 차분히 얘기할 수 있도록 배려해주고 도와준다. 이렇게 개발적 강점을 살리고 인정해주는 문화와 환경을 갖춘 회사가 있다면 이런 개발자에게는 이상적인 회사가 아닐까 생각해본다.


나는 기술적 강점이 있는 개발자가 의사소통 등 약점을 보완하려 애쓰기 보다는 본인의 강점을 더욱 강화시키는 것이 좋지 않을까 생각한다. 어떻게 보면 개발에 재능이 있는 것인데, 본인의 재능을 더욱 강화시키는 게 약점을 강화시키는 것보다 훨씬 성취가 빠를테고 결국 처음 부분 이사님이 말씀하셨던 탁월한(Great) 개발자가 되는데도 더 도움이 될 것 같기 때문이다. 물론 약점을 아예 보완하지 말자는 얘기는 아니다. 다만 개발 능력이 중심이 되어야 한다는 뜻이다. 예를 들어 기술적으로 계속해서 성장했으면 세계적인 개발자가 될만한 소양을 갖춘 사람이 있었다고 가정해보자. 그런데, 이 사람이 어느날 무슨 생각을 했는지 개발보다는 의사소통 능력에 중점적으로 힘쓰기 시작했다. 결국 사회적으로 출세했지만, 개발 능력은 크게 발전하지 않았다. 만약 이런 일이 실제로 있다면 어떨까? 개발자의 시각에서는 너무 아쉬운 일 아닐까?


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

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

회사에서 테스트 지원을 하다보면 많은 개발자가 이미 만들어 놓은 테스트를 유지보수 하는데 꽤 많은 시간을 사용하고 있음을 볼 수 있다. 그 중에서도 실패한 테스트를 고치는 일이 잦다. 만약 어떤 결함이 존재할 때, 테스트가 그 결함으로 인해 실패하는 것은 매우 고무적인 일이다. 왜냐하면 테스트가 제 할 몫을 한 것이기 때문이다. 하지만 리팩토링 등으로 테스트 대상(이하 SUT, System Under Test)의 내부구현이 바뀜으로 결함유무와 관계없이 테스트가 실패하는 것은 안타까운 일이다. 이런 현상이 자주 발생하면 개발자는 테스트를 짐처럼 여기게 될 것이고, 나중에는 테스트가 정말로 가치있는지에 대한 고민을 시작할 것이다.

그렇다면 위와 같은 일이 왜 생길까? 이유는 의존성 때문이다. 테스트는 자연스레 SUT에 의존하는 부분이 생기고 이 의존성이 위 문제를 발생시킨다. 아래 테스트 코드를 보자.

@Test
public void nameShouldBeModified() throws Exception {
    final Integer id = 1;
    Name name = new Name(id, "Min");

    Name result = O.process(name);

    assertThat(result, is(new Name(id, "Min-modified")));
}
#1. Spring Batch  관련 상태기반 테스트

Spring Batch 기반의 배치 코드 중 일부분을 테스트 하는 예제이다. 위 코드 중 process의 파라메터가 바뀌기라도 하면 이 테스트는 수정되어야 한다. 따라서 process 하는 부분이 SUT와의 의존성이다. 그런데  한편 위 코드는 의존성 문제 관점에서 보면 이상적인 테스트이기도 하다. 왜냐하면 현재 SUT 중 테스트 하려는 메서드에만 의존성을 가지고 있는데 이 이상 의존성을 줄일수는 없기 때문이다. 사실 이 글에서 얘기하려는 것 중 많은 부분은 Mock 라이브러리의 사용으로 생기는 현상들에 대해서이다.

아래는 서비스를 ROS 상태로 중지시키는 시나리오를 테스트 하는 코드이다. 코드 중 'readOnlyStop'과 'readOnlyNottifer'는 'target.stop' 메서드 내부에서 사용하는 객체이다.

@Test
public void serviceShouldBeStopped_WhenNeedingReadOnlyStop() {
    when(readOnlyStop.stopNow()).thenReturn(success);
    when(readOnlyNotifier.relatedServices()).thenReturn(createRelatedServices());
    when(readOnlyNotifier.notifyToRelatedServices(any(StopMessage.class))).
        thenReturn(success);

    target.stop(readOnlyStopType, "Read Only Stop");

    verify(readOnlyStop).stopNow();
}
#2. 의존성이 많은 행위기반 테스트

위 테스트는 많은 개발자가 즐겨쓰는 Mock 라이브러리인 Mockito를 이용하여 행위검증을 하는 예이다. 위 테스트 코드를 보면 테스트가 메서드 내부에 있는 동작에 지나치게 의존하고 있는 것을 볼 수 있다. 좀더 정확하게 표현하자면 위 5줄 모두가 의존성이다. 따라서 SUT의 메서드가 리팩토링이 된다면 위 테스트는 십중팔구 컴파일 오류가 나거나 실패할 것이고, 개발자는 결국 테스트 코드를 수정해야 할 것이다.

켄트벡은 "Where, Oh Where to Test?" 라는 글에서 이 문제를 세련스럽게 Stability 라는 단어로 표현했다. Stability 문제는 두 가지 경우를 포함한다. 첫째는 테스트가 성공했는데 결함이 있는 경우이고, 둘째는 테스트가 실패했는데 결함이 없는 경우이다. 둘의 공통점은 테스트가 잘못된 피드백을 주었고 결과로 불필요한 낭비가 생겼다는 것이다. 앞서 리팩토링 후 테스트 코드의 컴파일이 실패하거나 테스트가 실패하는 경우를 언급했다. 나는 이 경우가 테스트가 실패했음에도 실제로는 결함이 아닌 경우라 생각한다. 왜냐하면 결함 여부와 관계없이 단지 구현이 바뀜으로 인해 테스트가 실패했기 때문이다. 

지금까지 얘기한 문제는 단순해보일지도 모른다. 하지만 유지보수에 많은 부담을 주는 부분이다. 그럼 어떻게 하면 Stability 문제를 해결할 수 있을까? 본인은 두 가지 방향이 있다고 생각한다.

2. 구현에 의존적인 테스트를 만들지 말자

위에서 소개한 두 예제의 차이점은 첫째 예제가 구현에 의존적인 부분이 없는 반면, 둘째 예제는 구현에 의존적인 부분이 많다는 점이다. 첫째 예제는 상태검증이라 부르고, 둘째 예제는 행위검증이라 부른다. 그렇다면 항상 첫째 예제처럼 테스트를 작성하면 되는 것일까? 안타깝게도 불가능하다. 많은 경우 테스트를 하려면 부득불 둘째 예제처럼 작성해야만 한다. 아래 코드를 보자.

public void stop(StopType stopType, String reason) {
    validate(stopType, reason);

    if (stopType == StopType.COMPLETE_STOP) {
        stopNowQuietly(completeStop, whenFailedMessageIs(""));
    } else if (stopType == StopType.READONLY_STOP) {
        stopNowQuietly(readOnlyStop, whenFailedMessageIs("ROS"));
        readOnlyNotifier.notifyIfThereAreRelatedSerivcesUsing(createMessageBy(reason)); 
    }
}
#3. 어떤 서비스의 점검을 시작하는 메서드

위 코드는 가상의 시나리오를 구현하는 코드로써 점검에 관련한 일을 한다. 하는 일은 간단한데 파라메터에 따라 일반점검 혹은 읽기가 가능한 점검 중 하나를 수행한다. 읽기가 가능한 점검 시에는 관련 된 서비스에 알림을 한다. 위 코드를 테스트해야 한다고 가정해보자. 특히 관련된 서비스에게 알림을 잘 했는지를 보장하는 것이 테스트의 가장 중요한 목적이라면 어떻게 테스트 해야 할까? 많은 경우 아래처럼 행위검증을 사용할 수 밖에 없다.

...
verify(readOnlyNotifier).notifyIfThereAreRelatedSerivcesUsing(..);
...
#4. 3번 코드를 대상으로 알림을 했는지 여부를 검증하는 코드

위 테스트는 해당 메서드가 정확히 실행되었는지를 보장해준다. 하지만 최종적으로 달성하려는 바는 같음에도 리팩토링 등을 통해 readOnlyNotifier가 아닌 다른 객체를 사용한다면 테스트의 실패와 개발자의 수정으로 이어질 것이다.

이럴 때 사용할 수 있는 대안은 통합 테스트이다. 통합 테스트를 이용하면 메서드를 실제로 실행시킬 수 있으며 어떻게(How)가 아닌 무엇(What)에 집중할 수 있는 기회를 얻는다. 다시 말해 특정 목적을 달성하려고 구현한 방법이 아닌 달성 결과를 기반으로 검증할 수 있게 된다. 그렇다면 위 예는 통합 테스트에서 어떻게 검증할 수 있을까? 메서드 호출 후 남는 전송결과로그를 살펴보면 어떨까? 전송로그결과 파일 안에 방금 전송한 로그가 존재하는 것을 확인하는 것이다. 이렇게 하면 테스트는 예전처럼 행위검증이 아닌 상태검증을 하게 된다. 그 결과로 내부구현에 대한 의존성이 사라진다. 상태검증으로 변경 된 코드는 아래와 같다.

...
verify(readOnlyNotifier).notifyIfThereAreRelatedSerivcesUsing(..);
assertThat(logInFile, is(existing()));
...
#5. 4번의 검증 코드를 상태 기반 검증으로 변환

하지만 통합 테스트는 단점이 있다. 첫째는 실행시간의 증가이다. 위 예의 경우 실제로 로깅을 하고 관련 서비스에 메세지를 보내려면 아무래도 단위 테스트보다는 실행시간이 오래 걸리게 된다. 테스트의 실행시간이 오래 걸리는 것은 개발자가 개발 시 받는 피드백 속도를 둔화시키고 결국 생산성 저하로 이어질 수 있다. 둘째는 오류가 났을 때 어디에 문제가 생겼는지 알기 어렵다는 점이다. 통합 테스트의 특성상 많은 영역이 실행될 것이고 그 중 한 군데라도 결함이 생기면 테스트는 실패할 것이다. 하지만 이 때 결함이 어떤 부분에 생겼는지 명확하지 않다는 것이 문제다. 단위 테스트가 잘 고립되어 있을 때 테스트가 실패하면 문제점은 테스트 실행 대상 내에 있다고 단정할 수 있는 것과는 대조적이다. 마지막 문제는 SUT가 아닌 다른 곳이 의존성이 생길 수 있다는 점이다. 위 예에서는 로그파일에 대한 추가 의존성이 생겼다.

사실 위와 같은 단점 때문에 통합 테스트와 행위 기반의 단위 테스트를 비교하다보면 의존성 문제를 감안하더라도 행위검증을 선택하는 경우도 많다. 이 경우에는 아래 방향을 따르면 좋다.

3. 구현에 의존적인 테스트를 만들어야 한다면 최대한 의존하는 부분을 줄이며 테스트를 만들자

A) Mockito 사용

Mockito는 최근 들어 많은 사랑을 받고 있는 듯 하다. 사내에 EasyMock,  JMock을 사용하는 개발팀도 있지만 최근에는 많은 개발팀이 Mockito를 선택하고 있다. Mockito에는 다른 경쟁 라이브러리와 비교되는 특징이 몇 가지 있다. 그 중 의존성 관점에서 주목할 만한 점은 Mockito 홈페이지에 있는 "Verify what you want"라는 인상적인 문구와 함께 소개 된 특징이다. 이런 Mockito의 특징을 보려면 다른 라이브러리와 비교하는 것이 좋을 것 같아 JMock과 비교하는 예제를 준비했다.

public void moveArticle(Integer articleId) {
     Article article = articleDAO.findBy(articleId);

     if (article.movable()) {
         articleDAO.updateArticle(article);
         articleDAO.updateAttachedFiles(article);
         articleDAO.updateAttachedPics(article);
     }
}
#6. 아티클을 이동하는 메서드

위 코드는 실제 프로젝트에서 가져와 약간의 변형을 가한 코드이다. 하는 일을 간단히 설명하자면 아티클을 찾은 후 아티클의 이동이 가능하다면 이동을 하는 예이다. 테스트에 경험이 있는 사람은 금방 알 수 있지만 행위검증이 필요한 경우이다. 이 코드를 보면 크게 두 가지 흐름이 있음을 알 수 있다. 아티클이 이동 가능한 경우와 이동이 불가능한 경우이다. 이동이 가능할 때는 아티클 정보를 업데이트 하지만, 불가능하면 아무것도 하지 않는다. JMock을 이용하여 이 두 가지 흐름 중 이동 가능한 경우를 테스트 해보았다.

@Test
public void articleShouldBeMoved() {
    context.checking(new Expectations() {
        {
            oneOf(articleDAO).findBy(articleId);
            will(returnValue(article));
            oneOf(article).movable();
            will(returnValue(Boolean.TRUE));

            oneOf(articleDAO).updateArticle(article);
            oneOf(articleDAO).updateAttachedFiles(article);
            oneOf(articleDAO).updateAttachedPics(article);
        }
    });

    sut.moveArticle(articleId);

    context.assertIsSatisfied();
}
#7. JMock을 이용하여 아티클을 이동하는 메서드를 테스트

JMock은 Expectations를 이용하여 Stubbing과 Verifying을 한다. 위에서 보면 위가 Stubbing 하는 부분이고 아래가 Verifying하는 부분이다. 이 코드를 읽어보면 'moveArticle'을 호출한 후 관련된 모든 메서드가 잘 실행되었는지 검증을 한다는 것을 확인할 수 있다. 이후 만약 개발자가 코드를 수정하다 실수로 'updateAttachedFiles'를 수행 안 하기라도 하면 바로 오류가 날 것이다. JMock을 이용하여 엄격한 검증을 하고 있기 때문이다. Expectations안에 있는 코드는 내부의 수행되어야 하는 모든 행위를 그대로 표현하고 있다. 하지만 이게 좋은 것일까? 

JMock에 대한 관심에서 잠시 벗어나 이 얘기를 처음 시작했던 의존성 문제를 생각해보자. 위 코드를 보면 바로 알수 있듯이 의존성 투성이다. articleDAO가 내부에서 하는 모든 행위가 기록되어 있다. 의존성 관점에서는 별로 탐탁치 않은 부분이다. 그렇다면 테스트를 하지 말자는 것인가? 그것은 아니다. 다만 본인은 위 테스트를 조금 느슨하게 할 수 있다고 생각한다. 예를 들어 테스트의 목적을 잘 생각해보면 중요한 부분은 메서드 내부의 movable에 따른 분기로 볼 수 있다. 이동이 가능할 때는 'updateArticle', 'updateAttachedFiles', 'updateAttachedPics'가 있는 코드블락으로 진입해야 한다. 그렇다면 코드블락으로 진입하는지 여부만 테스트를 하면 어떨까? 좀더 구체적으로 'updateArticle'이 호출된다면 그 아래도 당연히 호출 될 테니 아래와 같이 코드를 고치면 좋을 것 같다.

@Test
public void articleShouldBeMoved() {
    context.checking(new Expectations() {
        {
            oneOf(articleDAO).findBy(articleId);
            will(returnValue(article));
            oneOf(article).movable();
            will(returnValue(Boolean.TRUE));

            oneOf(articleDAO).updateArticle(article);
            oneOf(articleDAO).updateAttachedFiles(article);
            oneOf(articleDAO).updateAttachedPics(article);
        }
    });

    sut.moveArticle(articleId);

    context.assertIsSatisfied();
}
#8. JMock의 엄격한 검증으로 인해 실패하는 테스트

하지만 테스트를 돌려보면 오류가 나는 것을 확인할 수 있다. 오류 내용은 'updateAttachedFiles', 'updateAttachedPics'가 실제로 호출 되었는데 테스트에는 이런 부분에 대한 언급이 없었다는 점을 지적한다.  JMock은 기본적으로 이렇게 엄격한 검증을 수행한다. 이어 Mockito의 코드를 보자.

@Test
public void articleShouldBeMoved() {
    when(articleDAO.findBy(articleId)).thenReturn(article);
    when(article.movable()).thenReturn(Boolean.TRUE);

    sut.moveArticle(articleId);

    verify(articleDAO).updateArticle(article);
}
#9. Mockito를 이용하여 아티클을 이동하는 메서드를 테스트

위 코드를 보면 방금 전 얘기했던 느슨한 검증에 관한 아이디어가 그대로 적용되어 있음을 볼 수 있다. 'moveArticle'을 한 후 'updateArticle'이 호출되었는지만 검사하고 있다. 결과로 테스트가 SUT에 의존하던 코드가 줄었다.  

B) Mockcito의 느슨한 검증 활용

Mockito의 느슨한 검증을 활용할 수 있는 예가 하나 더 있다. 아래 코드를 보자.

@Test
public void serviceShouldBeStopped_WhenNeedingReadOnlyStop() {
        when(readOnlyStop.stopNow()).thenReturn(success);

        target.stop(readOnlyStopType, "Read Only Stop");

        verify(readOnlyStop).stopNow();
}
#10. 내부구현에 의존하는 코드가 2회 반복해서 등장

위 테스트의 진한 부분을 보면 'readOnlyStop.stopNow'이 첫줄과 마지막 줄에 반복해서 등장하는 것을 확인할 수 있다. 혹시 그 중 하나는 제거할 수 있지 않을까? 이 경우 SUT의 코드가 어떻게 되었느야에 따라 의존성을 좀더 제거할 수도 있다. 제거가 가능한지 확인하기 위해 아래 SUT의 코드를 보자.

if (readOnlyStop.stopNow()) {
    ...
} else {
    throw new FailedStopException("Cannot stop the service.");
}
#11. 방금 전 테스트가 테스트 하는 대상

위 코드는 앞에서 소개 된 점검 예제의 한 부분이다. 살펴보면 'readOnlyStop.stopNow'가 True를 반환하지 않으면 예외가 발생되게 되어있다. 이 경우 verify를 이용하여 readOnlyStop의 stopNow가 호출되었는지를 검사하지 않아도 해당 메서드가 제대로 호출되지 않으면 예외가 발생할 것이다. 다시 말해 앞서 소개한 'when(readOnlyStop.stopNow()).thenReturn(success);'가 제대로 수행되지 않으면 False가 반환 될 것이고 예외가 발생하여 테스트는 실패할 것이다. 따라서 verify부분을 제거해도 된다는 결론에 이르게 된다. 이는 대부분의 Mock 라이브러리와 마찬가지로 Mockito는 Mock 객체에 Stubbing이 되어있지 않으면 False나 Null 등을 반환하게 되어있기 때문에 가능한 일이다. 결국 테스트 코드는 아래와 같이 변경될 수 있다.

@Test
public void serviceShouldBeStopped_WhenNeedingReadOnlyStop() {
        when(readOnlyStop.stopNow()).thenReturn(success);

        target.stop(readOnlyStopType, "Read Only Stop");
        verify(readOnlyStop).stopNow();
}
#12. verify 부분이 제거되어 보다 의존성이 줄어든 테스트

이렇게 Mockito를 잘 활용하면 의존성을 완화할 수 있다. 다만 주의할 점은 Mockito를 이용하여 느슨한 검증을 할 때 테스트 목적이 훼손되지 않도록 조심해야 한다는 것이다. 예를 들어 느슨한 검증을 하다 제대로 된 검증을 하지 못하는 테스트를 만들수도 있다. 만약 테스트를 만들었는데 조금 의아스럽다면 뮤테이션 테스트를 이용하여 테스트를 검증해보는 것이 좋다.

C) Tell, Don`t Ask 원칙

이 원칙은 Law of Demeter 원칙으로 많이 알려졌다. 또 최소지식의 원칙이라 부르기도 한다. 이 원칙을 내 표현으로 설명하면 다른 객체와 의사소통 할 때 지나치게 자세히 얘기하지 말고 가능한 간결하게 얘기하자는 것이다. 아래 코드를 보자.

public void stop(Integer type, String reason) {
    ...

    if (readOnlyNotifier.relatedServices().size() > 0) {        
        if (readOnlyNotifier.notifyToRelatedServices(stopMessage) == false) {
            throw new FailedReadOnlyNotificationException();
        }
    }
}
#13.  readOnlyNotifier에게 과다하게 의존적인 코드

'...'으로 생략 된 부분을 제외한 나머지 코드 부분의 테스트를 만든다고 가정해보자. 어떻게 하면 될까? 우선 실행흐름을 보자. 잘 보면 위 코드만 가지고도 테스트 해야 할 흐름이 세 가지나 있다. 그중 관련 서비스가 한 개 이상 있어 알림을 해야하는 흐름을 테스트 한다고 생각해보자. Mockito를 이용하여 아래와 같이 테스트 코드를 작성했다. 앞서 Mockito를 소개하며 언급했던 느슨한 검증이 가능한 특징으로 인해 verify 부분이 없는 것에 유의한다.

@Test
public void stopEventShouldBeNotifiedToRelatedServices() {
    when(readOnlyNotifier.relatedServices()).thenReturn(createRelatedServices());
    when(readOnlyNotifier.notifyToRelatedServices(any(StopMessage.class))).
        thenReturn(success);

    target.stop(readOnlyStopType, "Stop For Testing");
}
#14. 위 코드에 대한 테스트 코드

이 테스트의 의존성을 더 줄일 수 있을까? Tell, Don`t Ask 원칙을 도입하면 가능하다. 위 SUT의 문제점은 'stop' 메서드에서 'readOnlyNotifier에게 관련된 서비스가 있는지 묻고' 난 후 그 결과에 따라 메세지를 보낸다는 점이다. 묻고 보내기 때문에 두 클래스 간의 접점이 두 군데가 된다. 그런데 여기에 Tell, Don`t Ask 원칙을 적용하면 물어보지 않고 단순히 요청을 말하게 된다. 접점을 한 군데로 줄일 수 있다. 이를 위해 SUT의 코드를 아래와 같이 수정하고, 기존 코드는 readOnlyNotifier로 이동시킨다.

public void stop(Integer type, String reason) {
    ...
    readOnlyNotifier.notifyIfThereAreRelatedServices(stopMessage);
}

public class ReadOnlyNotifier {
    ...
    public void notifyIfThereAreRelatedServices(StopMessage stopMessage) {
        if (this.relatedServices().size() > 0) {        
            if (this.notifyToRelatedServices(stopMessage) == false) {
                throw new FailedReadOnlyNotificationException();
            }
        }
    }
    ...
}
#15. Tell, Don`t Ask 원칙의 적용

예전과 같이 묻지 않고 그냥 'readOnlyNotifier'에게 일을 맡기고 있다. 즉, 해당 코드가 하던 일이 'readOnlyNotifier'로 이동 된 것이다. 결과로 코드가 매우 간결해졌고 커플링도 줄어들었다. 이렇게 커플링이 줄어들면 이후 리팩토링 할 때도 서로간의 간섭이 별로 없어 좋다. 최종적으로 테스트 코드는 아래처럼 의존성이 더 줄어든다. 

@Test
public void stopEventShouldBeNotifiedToRelatedServices() {
    when(readOnlyNotifier.relatedServices()).thenReturn(createRelatedServices());
    when(readOnlyNotifier.notifyToRelatedServices(any(StopMessage.class))).
        thenReturn(success);
    when(readOnlyNotifier.notifyIfThereAreRelatedServices(any(StopMessage.class))).
        thenReturn(success);

    target.stop(readOnlyStopType, "Stop For Testing");
}
#16. Tell, Don`t Ask 원칙의 적용으로 인해 처음과 비교해 의존성이 줄어든 테스트 


4. 요약

결함유무와 관계없이 실패하는 테스트는 유지보수에 부담을 더한다. 이 부담은 테스트가 증가 할수록 심화될 수 있기 때문에 가볍게 볼 수 없는 부분이다. 사실 테스트가 SUT에 의존성을 갖는 것은 매우 자연스러운 일이다. 하지만 이 문제를 해결하지 않으면 유지보수 부담을 줄일 수 없다. 따라서 테스트를 작성할 때 가능한 SUT에 의존성하지 않도록 노력해야 한다. 이를 위해 앞서 소개한 통합 테스트, Mockito, Tell, Don`t Ask 원칙을 활용할 수 있다.

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

,
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
수개월 전 나와 많은 소통을 해왔던 동료 개발자가 퇴사했다. 퇴사한 이유는 여러 가지가 있겠지만 그 중 하나는 본인이 가진 이상을 회사에서 펼칠 수 없다고 판단해서였다. 퇴사를 지켜보며 마음이 편치 않았고, 한편으로 왜 본인의 이상을 펼치지 못했을까라는 의문이 들었다.

이 의문에 대해 내가 나름대로 내린 결론은 간단했다. 더 많은 영향력을 가진 사람과 생각이 틀려서이다. 어떤 사람은 A가 이상향이라 생각하고, 어떤 사람은 B가 이상향이라 생각한다. 또 어떤 사람은 이상향이란 없다고 생각하고, 또 다른 어떤 사람은 영리하게 모든 상황을 잘 활용하여 개인의 만족을 추구한다.

비슷한 생각을 가진 사람들이 모여 뜨거운 가슴으로 얘기한다. "이 점이 문제점이다. 이래서는 안 된다." 하지만 현실은 변하지 않고 그들의 이상은 거기서만 맴돌 뿐이다. 왜냐하면 그들은 영향력이 없기 때문이다. 이상만 있고 영향력이 없는 것은 이런 이유로 몹시 슬프고 괴로운 일이다.

나는 이상이 있는 개발자라면 반드시 영향력을 확보해야 한다고 생각한다. 본인이 바라보는 이상향을 주위에 설득하거나 권고할 수 있는 영향력 말이다. 높은 자리로 올라가는 것일수도 있고, 커뮤니티의 존경을 받는 것일수도 있다. 명확한 것은 이상을 실현하고 싶은 개발자는 앞서 영향력이 필요하다는 것이다.

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

,
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
* 특정인을 비난하자는 의도가 있는 글은 아니며, 단지 마음에 있는 생각을 솔직히 적은 글일 뿐임을 밝힙니다.

처음에는 단지 개발하는 게 재미있었습니다. 제가 무언가를 만들어내는 것에 대해 큰 기쁨과 자부심을 느꼈습니다. 개발하는 일은 정말 굉장한 일이라 생각했습니다. 그래서 저는 개발자가 되었습니다. 그런데 개발자로써 몇년을 살다보니 새로운 것이 보이기 시작했습니다. 바로 돈과 명예로 바꿔 부를 수 있는 성공이였습니다. 그리고 성공을 하게 도와줄 수 있는 강력한 도구인 정치라는 것도 알게 되었습니다.

주위를 가만히 살펴보았습니다. 그러자 개발 하는 데 모든 힘을 쏟는 것이 어리석은 일로 여겨졌습니다. 오히려 약간의 정치를 하는 것이 내게 더 유익할 것이라는 생각이 들었습니다. 극단적으로는 정치에 주로 힘을 쏟으며 약간의 개발을 하는게 성공하는 지름길이 아닌가라는 생각도 들었습니다. 실제로도 개발만 열심히 하는 사람보다는 정치를 잘 하는 사람들이 더 인정 받고 승승장구 하는 것으로 보였습니다.

이런 현실 속에서 제 마음에 갈등이 일어났습니다. 내 마음에서 옳다고 생각하는 개발자가 될 것인가 아니면 이러한 소신을 버리고 성공을 좇는 개발자가 될 것인가? 이것은 제 자신과의 치열한 싸움이였습니다. 그리고 저는 흔들리는 제 마음을 정리하기 위해 아래와 같은 '개발자 지침'을 생각해보았습니다.
개발자 지침

  하나.  개발하는 즐거움과 학습하는 자세를 잃지 않는다.
  둘.     다른 개발자들을 존중하고 사랑한다.
  셋.     불완전한 내 자신을 잊지 않으며 보잘것 없는 능력을 과신하지 않는다.
  넷.     내가 한 것 이상으로 혹은 할 수 있는 것 이상으로 나를 과대포장하지 않는다.
  다섯.  목표를 높게 잡고, 꾸준히 정진한다.
저는 앞으로 제게 남은 개발자로써의 인생에 있어 스스로 세운 위 지침을 준수하며 살고자 합니다. 이 글을 읽으시는 다른 분들도 소신을 가지고 추구하고 계신 지침이 있으시다면 함께 나누어보았으면 좋겠습니다.

2008/06/25 ~ 2008/07/23

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

,
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
개발자는 '생산성'이라는 단어를 무척이나 즐겨 사용합니다. 또한 '생산성 향상'이라는 말도 매우 좋아하는 것 같습니다. 누군가가 "이러이러한 방법으로 생산성을 향상시켰습니다"라고 감격에 차 말하면 개발자는 그 사람에게 진심으로 박수를 보냅니다.

그런데 이 생산성 향상이라는 것을 개발자 중심의 시각에서 생각해보면 어떨까요? 아래는 관련해서 제가 써본 글입니다.
자바를 이용하여 개발한다는 것은 일반적으로 JVM 위에서 대다수의 일들이 이루어진다는 것을 뜻합니다. JVM은 운영체제로부터 개발자를 분리시켜 줍니다. 예를 들어 JVM으로 인해 개발자는 운영체제 원리에 대해 깊히 알 필요가 없어졌습니다. JVM과 JVM을 한번더 감싸고 있는 Java API가 개발자가 내린 명령을 대신 실행해줍니다. 소프트웨어 아키텍쳐적인 시각으로 보았을 때 JVM의 이런 개념은 멋진 개념일 것입니다.

이러한 JVM의 멋진 개념은 여러 가지면에서 소프트웨어 산업계에 긍정적인 기여를 했다고 생각합니다. JVM으로 인해 '높은 이식성'을 가지는 웹 애플리케이션이나 GUI 클라이언트와 같은 소프트웨어를 쉽게 작성할 수 있게 되었고, 개발자들은 운영체제에 대한 깊은 지식 없이도 견고한 애플리케이션을 만들 수 있게 되었습니다. 위 두 가지 유익으로 인해 개발 생산성이 높아진 것은 두말할 나위가 없습니다.

그럼 이것이 개발자들에게 좋은 일인가요? 제가 볼 땐 아닙니다. 이것은 개발자들보다는 소프트웨어 산업과 기업에 좋은 일입니다.

예를 들어 A라는 기업의 개발팀에서 이식성이 좋지 않은 언어B을 이용하여 개발한 GUI 애플리케이션을 유닉스와, 맥, 윈도우에 배포하고 있었다고 가정해봅시다. 언어B을 이용해서 개발할 때는 운영체계의 특징에 따라 포팅 작업이 필요했습니다. 따라서 주요 기능을 개발한 후에 릴리즈를 하기 위해 포팅 작업에 시간을 쏟곤 했습니다.

그런데 어느날 자바가 나왔습니다. A기업의 CTO는 자바를 면밀히 검토했습니다. 그리고 자바는 이식성이 B언어에 비해 훨씬 좋기 때문에 그 동안 포팅 작업이 소요되던 개발 시간을 단축시킬 것이라 판단했습니다. 그는 곧 CEO에게 제안을 하여 허락을 받았고, 그의 지휘 아래 개발팀은 모든 것을 자바 기반으로 바꾸기 시작했습니다.

1년뒤 A기업의 CEO는 자바 도입으로 몇가지 희생도 있었지만 어쨋든 개발 생산성이 향상된 것을 알 수 있었습니다. 갈 길 바쁜 기업으로써는 좋은 일이였지요.

그렇다면 개발자들은 어떻게 되었을까요?

개발자들은 1년전과 다름없이 바쁘게 일하고 야근을 했습니다. 단지 차이라면 예전에 포팅 작업을 했을 시간에 다른 일을 하게 된 것입니다. 예, 아쉽게도 아무것도 달라지지 않았습니다.
또한 포팅 작업을 하며 운영체제간에 미묘한 차이점에 대한 경험 쌓는 것을 즐거워 하던 개발자A씨는 퇴사를 하려고 준비하고 있습니다. 이제는 자신이 회사에서 경험할 수 있었던 기술적 도전과제가 사라졌다고 느꼈기 때문입니다.


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

,