336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
4월 19일 - 회사 개발방법론 때문에 고통을 받는다는 얘기를 듣고 와서 

회사에서 맡은 직무 상 개발부서에서 어려워 하는 부분을 관찰하려 노력 중이다. 돌아다니며 어려움에 대한 얘기를 들은 후에는 원인을 생각해보곤 한다. 원인을 생각하다보면 누군가 단기간 내 성과에 집착함으로써 발생한다는 결론에 이르게 될 때가 있다. 그렇다면 단기간 내 성과에 집착하는 사람이 나쁜 사람인가? 그건 아닌 것 같다. 개발자의 어려움이 있듯이 그들도 나름의 고충이 있을 것이다. 그래서 요즘은 오히려 단기간 내 성과에 집착하게 만드는 시스템에 문제가 아닌가라는 생각이 많이 든다. 왜 그렇게 하고 있을까?

4월 20일 - TDD Out-In 방식의 글 쓰기  

최근 글을 쓰다가 글을 멈추는 경험을 몇번했다. 이유는 글이 뭔가 초점이 없어 보였기 때문이다. 뭔가 생각이 있어 글을 시작하지만 쓰다 보면 이런 상황이 자주 생긴다. 이 문제를 해결할까? 그래서 생각한 것이 TDD 방식을 글 쓰기에도 적용하는 것이다. TDD는 접근방향에 따라 크게 두 가지 방식이 있다. Out-In과 In-Out이다. 그 중 Out-In은 본인이 애용하는 방법으로써, 바깥쪽의 필요한 인터페이스를 먼저 생각하고 그 인터페이스를 구현하기 위해 안에서 필요한 구성요소를 채워나가는 방법이다. 

이를 글 쓰기에 적용하면 이렇게 할 수 있을 것 같다. 먼저 글의 핵심 주제를 정한다. 핵심 주제를 설명하기 위한 소주제를 식별한다. 소주제 간의 논리적 연관관계과 흐름이 매끄러운지 확인한다. 그 다음 글을 채운다. 사실 쓰고보니 많이 알려져 있는 글 쓰기 방식인 것 같다. 이 내용을 미투데이에 짧게 올렸더니 동료 분이 완전히 반대되는 방법을 제안해주었다. 일단 생각나는 대로 쓰고 주제와 관계 없는 글은 제거한다는 것이다. 물론 완전 제거는 아니고 어딘가에 저장해두고 나중에 활용한다는 내용이었다. 괜찮은 것 같다는 생각이 들었다. 최근 양질의 글을 적정시간 안에 쓰는 훈련이 필요하다는 생각이 많이 든다.


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

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


출판 전부터 관심을 가지고 지켜보던 자바지기님의 메이븐책이 출판되었다. 원래 출판 전에는 국내에 메이븐 관련 책이 없었지만, 현재는 번역서가 나와있다. 하지만, 국내 저자가 쓴 메이븐 책으로써는 아마 최초가 아닐까 싶다. 저자와의 친분과 메이븐에 대한 관심 때문에 베타리뷰를 하고 추천평을 쓰게 되었다. 평소에 저자에 대해 본인이 가졌던 존경 때문에 아래 추천평을 쓰는 것은 큰 기쁨이 되었다. 

2006년 메이븐을 처음 사용했을 당시의 기억이 아직도 생생하다. 당시 다른 개발자가 구축해 놓은 메이븐 기반의 프로젝트를 받아 운영하게 되었다. 예전에는 앤트를 사용하고 있었고, 나는 앤트의 사용법과 동작원리에 대해 잘 이해하고 있었다. 메이븐을 써보니 빌드를 수행하기는 매우 쉬웠다. 'mvn package'를 실행하면 모든 것이 잘 작동했다. 하지만, 난 메이븐이 어떻게 동작하는지 알고 싶었고, 빌드정보가 기술된 pom.xml을 열어보았다. 잠시 후 난 당혹감을 느꼈다. 내가 기대하던 항목이 보이지 않았기 때문이다. 소스의 위치를 지정하는 곳도 보이지 않았고, 컴파일 결과가 들어갈 위치를 지정해주는 곳도 보이지 않았다. 메이븐이 내 소스의 위치를 어떻게 알고 컴파일하지? 나는 강한 의문에 휩싸였고 학습을 시작했다. 그리고 꽤 많은 시간이 지난 후에야 메이븐에 익숙해질 수 있었다. 

그 후 내게도 메이븐에 대해 문의하는 사람이 생겼다. 얘기를 들어보면 내가 처음에 그랬던 것처럼 그들도 당혹스러워하고 있었다. 동작원리를 설명해 주며 왜 메이븐이 왜 당혹감을 주는지를 곰곰이 생각해보게 되었다. 내가 얻은 결론은 메이븐에 적용 된 디자인이 직관적이지 않고 이해를 어렵게 한다는 것이었다. 위 문제를 인지한 후 메이븐을 이해하는 데 도움이 될 만한 글을 블로그에 정리하기로 했다. 하지만, 글을 쓰다 보니 설명해야 할게 많았고, 이해를 돕기 위한 다수의 예제도 필요함을 느꼈다. 이런 어려움으로 결국 나는 글을 완성하지 못했다. 그러던 어느 날 이 책의 저자인 박재성 씨가 내게 메이븐을 관련 책을 쓰겠다는 얘기를 했다. 나는 필요성에 충분히 공감했고, 많은 개발자에게 도움이 될 것으로 생각했다.

베타 리더로 책을 읽으며 색다른 즐거움이 있었다. 위키북 프로젝트를 함께 하지는 못했지만, 책을 통해 당시 저자의 진지한 고민과 역동적 감성을 어렵지 않게 느낄 수 있었기 때문이다. 사실 이런 면에서 이 책은 단순히 메이븐의 사용법만을 전달하는 책이 아니다. 오히려 문제가 많은 현실가운데 이상향을 그리고, 메이븐을 손에 든 채 이상향을 향해 한 걸음씩 전진하는 개발자의 용기 넘치는 모습을 보여준다. 독자는 일차적으로 메이븐에 대한 이해를 얻게 되겠지만, 각 장의 시작과 말미 그리고 가끔 설명 중간에 나오는 저자의 회고적인 글을 통해 이상을 가진 개발자의 고뇌와 시행착오 그리고 마침내 얻게 되는 희열을 엿볼 수 있을 것이다. 나는 이를 단순한 지식의 전달을 뛰어넘는 경험의 전달이라 생각하며, 이 책을 가장 빛나는 하는 요소라 생각한다. 만약 메이븐에 관심 있는 개발자라면 이 책의 일독을 권하고 싶다.

 

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

,
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
어느 한가한 토요일 방안에서 뒹굴고 있을 때였다. 심심해서 머리 속으로 년도별 나만의 최고의 책을 뽑아보고 있었다. 심사 결과 2008년에는 구현패턴(Implementation Patterns), 2009년에는 Growing Object Oriented Software, Guide By Tests(이하 GOOS), 2010년에는 Working Effectively With Legacy Code(이하 WEWLC)가 뽑혔다. 이 세 권의 책은 내게 큰 영향을 미쳤는데, 이 책들이 내게 어떻게 영향을 주었나라는 점을 가만히 생각해보니 재밌게도 각기 조금씩 다른 점이 있다는 것을 알게 되었다.

1. 지식의 체계화

때로 실무경험을 통해 다양한 경험적 지식이 축적되어 있는 경우가 많다. 이 때 누군가가 내가 이미 경험해 알고 있는 어떤 것을 이야기하면 쉽게 공감하고 반응할 수 있다. 그런데 이렇게 내가 경함 것을 글로 정리하려 하면 재밌는 일이 생긴다. 분명 나는 A,B,C,D 각각에 대해 모두 잘 알고 있는데, 정리하려 했더니 잘 정리가 안 되는 것이다. 나는 아직 머리 속에서 지식의 체계화가 되지않아 이런 일이 발생한다고 생각한다. 즉, 여러 지식이 머리 속에 거미줄처럼 얽혀있는 것이다. 이런 때는 글로 관련 지식을 정리하기 어려울 뿐더러, 글을 쓰더라도 형편없는 글이 되는 때가 많다. 

켄트 벡의 구현패턴은 내가 지식을 체계화 할 수 있도록 도와준 책이다. '클래스 내에서 변수에 간접적으로 접근하는 것과, 직접적으로 접근하는 것 중 무엇이 좋을까?'라고 물어보았을 때 대답을 할 수 있는 사람이 얼마나 될까? 켄트 벡은 이에 대해 매우 상세하게 설명해준다. 난 구현패턴을 읽으며 경험했지만 정리되지 않았던 지식을 체계적으로 정리하게 되었다.

2. 새로운 시야

때로 시야가 확 넓어지는 때가 있다. 예전에는 생각치도 못했던 영역을 보게 되는 것이다. 그 영역은 이미 근처에 있었지만 내가 미처 보지 못했던 곳일수도 있고, 한번도 본적 없는 장소일수도 있다. 새로운 영역을 볼 때 기존의 쌓인 지식은 빠르게 재검토 되고, 큰 기쁨과 함께 많은 도전거리가 생긴다.

GOOS를 읽으며 이렇게 새로운 시야가 열림을 경험했다. 내가 익숙하게 하던 코딩을 완전히 다른 시각에서 보게 되었다. 특히 '객체지향 소프트웨어에서 가장 중요한 부분은 객체간의 의사소통이다'에 담긴 의미를 어렴풋이 깨달은 후로, 내 코드의 디자인은 급격히 달라지게 되었다. 

구현패턴을 읽으면서도 새로운 시야가 열림을 경험했다. 어느날 켄트 벡은 프로그래밍을 할 때 본인이 결정하는 것을 인지하기 시작했고, 그 사건을 계기로 많은 구현 패턴을 식별해냈다고 한다. 이 부분을 읽으며 나 역시 프로그래밍을 할 때 습관적으로 결정하는 것이 많다는 것을 깨달았다. 이후 '내가 왜 변수명을 이렇게 짓지?', '왜 이 메서드가 여기에 있어야 하지?'에 대해 고민하기 시작했다. 이는 GOOS와 마찬가지로 기존에 가졌던 생각을 변화시키고, 행동 또한 달라지게 했다.

3. 확신

2010년 초여름 JCO 발표를 준비하고 있었다. 발표 주제는 '리팩토링과 검증'이다. 작성한 자료를 쭉 읽어보니 많은 부분이 경험적인 내용이었다. 누군지도 잘 모르는 많은 사람 앞에서 발표를 해야했기 때문에, 대가를 통해 보편적으로 널리 인정 된 내용과 상충이 되는 내용이 있지 않을까라는 걱정이 들었다.

그래서 그동안 완독하려 했지만 게으름에 미처 하지못하던 WEWLC를 빠르게 읽어 나갔다. 결국 책을 읽고 난 후 저자의 의견과 내가 그동안 생각했던 부분이 크게 다르지 않다는 것을 깨달았다. 이는 내가 그동안 경험하고 사고하며 내린 결론의 방향이 올바르다는 확신을 주었고, 결과적으로 이런 부담 없이 JCO 발표를 수행할 수 있었다.

4. 성장의 필요성 인식

처음부터 얘기하려 했던 책은 아니지만 정리하다보니 마틴 파울러의 리팩토링에 대해서도 얘기를 하고 싶다. 난 2004년 마틴 파울러의 리팩토링을 처음 읽어보았다. 당시 난 마틴 파울러가 누군지 몰랐고 책도 회사에 돌아다니던 것을 가져다가 본 것이었다. 그런데 처음 책을 읽을 때 충격을 받았다. 왜냐하면 마틴 파울러가 코드를 수정하기 전에 하는 일련의 절차가 너무 이상해 보였기 때문이다. 리팩토링에서 '변수를 final로 고친 후 재할당이 없다는 것을 확인한다.' 같은 문구를 볼 때 전혀 공감이 되지 않았다. 더 나아가 마틴 파울러라는 사람이 이렇게 작은 부분까지 신경을 쓰는 것을 보니 약간의 편집증이 있는 사람 같다는 생각을 하기도 했다. 결국 거부감이 심하게 들어 책을 덮었고, 그 후로 꽤 오랫동안 이 책을 읽지 않았다. 지금은 어떤가? 난 진심으로 마틴 파울러를 깊히 존경한다. 리팩토링에 있는 내용은 정말 뛰어난 내용이라 생각한다.

결국 2004년의 난 리팩토링을 읽을 준비가 되지 않았던 것 같다. 때로 여러 곳에서 높히 평가하는 책임에도 난 좋은 점을 못느끼기도 한다. 이런 일이 생기는 여러 이유가 있을 것이다, 그 중 하나는 아직 그 책에 있는 내용을 이해하고 받아들일만한 역량이 되지 않아서이지 않을까? 내가 리팩토링을 처음 읽었을 때처럼 말이다.

 



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

,
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
얼마 전 분산 저장소에 대한 팀 내 관심이 뜨거웠다. 때마침 실험적 시도를 할 수 있는 작은 프로젝트를 진행하게 되었는데, 프로젝트 참여자들과 논의하여 Git 도입을 시도했다. Git을 사용해보며 여러 가지 요소를 평가 중인데, 그 중 개인적으로 관심이 있는 부분은 Git이 내가 일하는 환경의 어떤 부분을 향상시켜 줄 수 있느냐이다. 좀 더 나아가 SVN을 사용하고 있는 우리 회사가 Git으로 전환해야 하는지에 대해서도 관심이 있다.

1. Git의 특징

예전에는 중앙 저장소가 있고 모든 개발자가 중앙 저장소에 자신의 작업을 커밋했다. 반면 Git은 분산 저장소를 제공한다. 따라서 중앙 저장소가 있더라도 해당 저장소를 로컬에 복사(clone)하는 순간 로컬에 나만의 저장소가 생긴다. 따라서 예전처럼 원격 저장소에 영향을 미치지 않고 로컬 내에서 브랜치를 만들고, 커밋하고, 롤백하는 일 모두 가능하다. 

만약 중앙 저장소에 내 작업을 넣고 싶으면 어떻게 할까? 예를 들어 어떤 저장소를 로컬에 복사한다. 그리고 파일 하나를 수정한다. 원격 저장소에 변경사항을 반영(push)하려하면 변경사항이 없다고 나온다. 이는 예전과는 달리 사용자와 중앙 저장소의 입장이 아닌 로컬 저장소와 원격 저장소의 입장이 되기 때문에 발생하는 일이다. 즉, 로컬 저장소에 커밋을 하지 않았기 때문에 변경이 없다고 보는 것이다. 로컬 저장소에 커밋을 한다. 그리고 다시 변경 사항을 반영해본다. 이제서야  변경사항이 원격 저장소에 적용된다. 방금 얘기한 것이 Git의 가장 기본적 흐름이다.

2. 오픈소스가 Git으로 전환하는 이유에 대한 견해

오픈소스는 소수의 커밋터(Committer)와 다수의 공헌자(Contributor)로 구성된다. 커밋터를 제외한 공헌자는 익명으로 소스를 체크아웃하고 로컬에서 작업한다. 작업이 어느정도 완료되면 패치를 만들어 커밋터에게 적용을 요청한다. 이 모델은 대부분 오픈소스에서 사용하는 개발모델이다. 그런데 문제점이 있다. 바로 공헌자는 패치를 완료하기 전까지 SCM의 이점을 전혀 못 누린다는 점이다. 저장소가 없으므로 중간에 커밋을 할 수도 없고 롤백도 할 수 없다. 당연히 브랜치도 만들 수 없다. 따라서 어떤 공헌자가 한 프로젝트에 대해 여러 패치를 동시에 작업해야 한다면 이는 기존 SVN 환경에서 쉽지 않은 일이다. 

Git을 이용하면 방금 얘기한 문제가 해결된다. 중앙 저장소에 권한이 없더라도, 로컬에서 얼마든지 SCM의 장점을 누릴 수가 있다. 작업하다 잘못되면 롤백을 할 수도 있고, 몇 개 브랜치를 만들어 여러 패치를 동시에 작업할 수도 있다. 난 이런 이유로 오픈소스진영에서는 Git을 반길 수밖에 없다고 생각한다.

3. 그럼 회사에서도 Git이 필요할까?

Git에 대해 긍정적 의견을 내비치는 사람의 근거 중 하나는 오픈소스진영이 점차 Git으로 전환하고 있다는 점이다. 오픈소스 진영은 기술적 트렌드에 민감한 편이고, 오픈소스에 먼저 적용한 기술이 시간이 흘러 대중화되는 것은 매우 자연스러운 흐름이다. 그렇다면 회사에 Git을 도입하는 것은 어떨까? 난 아래 두 가지 이유로 신중한 접근이 필요하다고 본다.

첫째 회사는 오픈소스진영과 개발상황이 다르기 때문이다. 가장 큰 차이점은 오픈소스에는 흔한 공헌자가 없다는 것이다. 팀원 모두 커밋터고, 팀은 저장소 하나를 공유하며 함께 작업한다. 따라서 수정한 것이 있으면 바로 커밋을 하면 된다. 팀원 모두 커밋터로써 SCM의 장점을 충분히 누릴 수 있다.

둘째 지속적 통합에 대한 부정적 영향을 미칠 가능성이 있기 때문이다. 여럿이서 저장소 하나를 대상으로 함께 작업하다 보면 지속적 통합이 무척 중요하다. 다시 말해 동작하는 버전을 자주 커밋하는 게 강력히 권장된다. 이를 잘 지키면 다른 동료에게 빠른 피드백을 줄 수 있고, 통합 시점(보통 QA 혹은 배포 전)에 소스가 충돌이 나 소스를 급하게 수정하는 일도 줄어든다. 그렇다면 Git은 어떨까? 난 Git은 지속적 통합이 추구하는 바에는 잘 안 맞는 것 같다고 생각한다. Git은 로컬 저장소를 제공하기 때문에 로컬에서 커밋하고 롤백하며 작업할 수 있는 좋은 토양을 제공한다. 이로 인해 자주 통합하기보다는 소스를 로컬에 오래 가지고 있는 상황이 생기지 않을까라는 우려가 든다.

4. Git에서도 지속적 통합이 가능하다?

누군가는 이렇게 얘기할 수 있다. '로컬 저장소가 제공되는 Git을 쓰자. 그리고 예전처럼 자주 커밋하자. 예전과 다를 바가 없지 않나?' 그런데 여기 약간의 걸림돌이 있다. Git은 SVN, CVS와 달리 중앙 저장소에 바로 커밋할 수 없기 때문이다. Git은 로컬 그 자체가 저장소이기 때문에 우선 로컬 저장소에 커밋을 수행한 후에 로컬 저장소를 원격 저장소에 머지(push)하는 방식으로 통합한다. SVN, CVS를 사용할 때는 단순한 파일 하나를 수정할 때 커밋을 하면 끝이었다. 하지만 Git은 로컬 저장소에 커밋하고 원격 저장소로 머지해야 한다. 즉, 두 단계가 필요한 것이다. 별것 아닌 것 같지만, 개발자가 가장 많이 수행하는 흐름이 좀 더 길어진 것이다. 물론 두 단계를 한꺼번에 수행해주는 도구를 개발하면 쉽게 해결되는 문제이다. 하지만, 도구를 쓰는 단계까지 간다면 Git을 써야 하는 이유가 많이 퇴색되는 게 아닐까?

5. 커밋터에게 로컬 저장소가 필요한가?

앞서 소개했지만, Git의 가장 큰 장점은 로컬에 나만의 저장소를 둘 수 있다는 점으로 보인다. 그런데 과연 이 특성이 현장에서 얼마나 필요할까? 예전에 가끔 로컬에서 중간 중간 커밋하고 싶다는 생각을 한 적이 있다. 하지만, 당시 내가 그런 생각을 했던 이유는 지속적 통합을 하지 않고 있었기 때문이었다. 나는 소스를 광범위하게 고치고 있었고, 다음 수정에서 무엇인가 잘못되어 예전에 작업한 부분도 없어질까 봐 두려웠다. 하지만, 지속적 통합을 실천하며 다시 이런 생각을 한적은 없었다. 항상 동작하는 버전을 자주 커밋했다. 때로 1시간에 수십회를 커밋하기도 했다. 테스트가 통과하면 바로바로 커밋하기 때문이다. 

6. Git은 머지가 편하다?

가끔 Git과 같은 분산저장소를 사용하면 머지가 편해진다는 얘기를 듣는다. SVN에서 소스충돌은 다른 개발자가 같은 라인을 수정해서 발생하기도 했고, 어떤 때는 SVN의 오판으로 발생하기도 했다. 소스충돌이 일어났을 때 소스를 정리하는 작업은 정말 어렵고 힘들다. 따라서 Git이 머지를 편하게 해준다면 이는 큰 매력이다. 하지만 Git홈페이지(http://git-scm.com/about)를 보면 여러 장점을 소개하지만 머지에 대한 언급은 없다. 어디서 이런 얘기가 나왔는지는 모르겠지만, 확인이 필요한 부분 같다.

7. 결론

Git이 제공하는 가장 주요한 특징은 분산 저장소이다. 많은 오픈소스에서 Git을 잘 쓰고 있는 것처럼, 분명히 분산 저장소라는 특징이 빛나는 상황이 있을 것이라 생각한다. 하지만, 이 점이 Git으로 전환할 만한 충분한 이유가 될까? 난 아직 잘 모르겠다. 지금까지 내가 이해한 수준에서는 전환비용을 감당하면서도 넘어가야 할 이유를 찾기 어렵기 때문이다. 오히려 가뜩이나 잘 되지 않는 지속적 통합을 더 악화시키지 않을까라는 우려가 있다. 하지만 Git이 SVN 보다 머지기능이 탁월하다는 것이 밝혀진다면, 이는 Git으로 전환해야 하는 좋은 근거가 되리라 생각한다.

8. 참고

1) C와 같은 개발 환경에서는 Git이 지속적 통합에 해가 된다기 보다는 오히려 여러 장점이 있다는 의견을 담은 글
http://hyukhur.tumblr.com/post/4126008077/git-for-more-continuous-building 

2) 덧글 중 benelog님의 반대 의견도 참고

3) benelog님의 Git 유랑기 



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 크기의 광고 코드만 넣을 수 있습니다.
코드를 작성하는 것과 글을 쓰는 것은 무척 유사하다는 생각이 들 때가 많다. 코드를 쓸 때면 관련있는 코드끼리 묶어보고 해당 코드를 글 쓸 때의 문단처럼 생각한다. 문단 사이에는 빈줄을 넣어 구분한다. 켄트벡이 구현 패턴에서 강조했던 것처럼 읽는 사람이 좀더 내 의도를 알아주기를 바라는 마음에서다. 

테스트 메서드에서는 이런 문단 개념이 더욱 쉽게 적용될 수 있는 것 같다. 왜냐하면 대부분의 테스트 메서드는 세 개의 문단을 가지고 있기 때문이다. 준비, 테스트 실행, 검증이다. 아래 DAO 통합 테스트는 이런 세 단계를 잘 보여준다,

@Test
public void 데이터베이스에_저장한_이름을_조회한다() {
Name persistedName = sut.persist(new Name("Some name"));
        
        Name result = sut.findBy(persistedName.id());        

assertThat(result, is(persistedName));
}

위 예제를 보면 

1. 이름을 저장한다. [준비]
2. 방금 저장한 이름을 가져온다. [테스트 실행]
3. 저장한 이름을 정확히 가져왔는지 검증한다. [검증]

로 문단이 구분되어 있음을 볼 수 있다. 또 다른 예제도 있다. 비슷한 구성을 확인할 수 있다.

@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();
}

그런데 문단을 나누는 것이 정말 가치가 있을까? 이 글을 읽는 사람이 어떻게 느꼈는지는 모르겠지만 만약 문단별로 띄어쓰기를 하지 않으면 방금 전 소스는 아래처럼 보인다.

@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();
}

어디까지가 테스트 데이터를 준비하는 곳이고 어떤 곳이 실행 부분인지 잘 구분이 되지 않는다. 결국 문단별 띄어쓰기가 가독성에 긍정적인 영향을 미친다는 것을 알 수 있다.

행위 주도 개발(Behavior Driven Development)에서는 '비지니스 요구사항에 따른 필요한 행위'라는 관점에서 테스트를 작성한다. 주요 의도 중 하나는 테스트를 개발자가 아닌 사람도 이해할 정도로 만들겠다는 것이다. 사실 UML 사례처럼 이런 시도가 좋은 결과를 낸 경우가 많지 않다는 점이 먼저 생각난다. 하지만 가독성이라는 관점에서는 이런 형식을 따라도 손해볼 일은 없을 것 같다. 위키피디아에 소개 된 BDD 기반 테스트 예제는 아래와 같다.

@Test
public void shouldCloseWindows() {
// Given
WindowControl control = new WindowControl("My AFrame");
AFrame frame = new AFrame();
// When
control.closeWindow();
// Then
ensureThat(!frame.isShowing());       
}


문단 관점에서 보면 앞서 소개한 예제와 동일하게 세개의 문단으로 구성되어 있다. 다만 Given, When, Then으로 문단의 소주제를 결정한 것이 특징이다. 테스트명은 테스트하고자 하는 행위를 나타내고 있다. 쭉 읽어보면 상당히 읽기도 좋고 이해도 잘 된다. 

테스트도 사람이 읽고 유지보수해야 하는 코드이다. 따라서 코드에 적용될 수 있는 내용 중 많은 부분이 그대로 적용될 수 있다. 그중 문단 나누기는 테스트의 가독성을 향상시키려고 할 때 많은 도움이 될 것이라 생각한다.

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

,
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
저는 고등학교때 부터 축구하는 것을 무척 좋아했었습니다. 당시 다니던 교회에서 예배를 드리고 오후가 되면 교회 사람들과 축구를 하는데 그게 얼마나 재밌었는지 모릅니다. 11대 11로 하는 정식 축구가 아니라 조그만 핸드볼 골대에서 소수 인원이 했지만, 너무 즐거웠습니다.

사실 저는 어렸을 때부터 몸으로 하는 모든 것을 잘 못했습니다. 수련회 같은 곳에 가서 포크 댄스라도 출 때면 동작을 따라 하지 못해 항상 애를 먹었고, 농구를 할 때도 레이업 슛을 하면 자세가 이상하다며 친구들이 놀리곤 했습니다. 축구도 마찬가지였습니다. 마음이야 잘하고 싶었지만 달리기 빼고는 달리 잘하는 게 없었습니다. 슈팅을 할 때의 모습이 이상했는지 여러 사람이 지적하고 교정해 주려 했지만 끝내 교정하지 못했습니다. 그렇지만 축구의 즐거움과 그에 대한 애정은 항상 변함없었고 열심히 또 열심히 했습니다. 그러자 어느 날부터 축구부의 형들에게 조금씩 칭찬을 받기 시작했습니다. 기술이 뛰어난 것은 아니었지만 집중하며 한 걸음이라도 더 뛰는 것이 보기 좋았나 봅니다. 성실하게 뛴다던지 위치선정이 좋다는 얘기를 들었습니다. 팀을 승리로 이끄는 최고는 아니었지만, 팀에 도움이 되고 있다고 느꼈고 기뻤습니다. 하지만 안타깝게도 2006년 발목 부상을 당하고 통증이 사라지지 않아 축구를 약 3년 정도 쉬게 되었습니다.

최근 발목의 통증이 없어지고 다시 축구가 하고 싶어 사내의 축구 동호회에 가입했습니다. 그리고 떨리는 마음으로 모임에 참석하기 시작했습니다. 그런데 몇 번 참석을 하며 제가 팀에 도움이 안 된다는 것을 조금씩 깨닫기 시작했습니다. 원래 기술이 좋은 편이 아닌데다 예전과 같은 체력과 집중력도 없어졌기 때문입니다. 더 잘해보려 안간힘을 썼지만, 현실은 그렇지 못했습니다. 함께 뛰는 친구들이 제게 비난을 하지는 않았습니다. 저도 겉으로 내색하지는 않았지만, 함께 뛰는 친구들에게 무척 미안했습니다. 그들이 가진 축구에 대한 열정만큼이나 승리를 갈망하는 눈빛을 보며 저는 도움보다는 방해가 된다고 느꼈기 때문입니다.

유쾌한 경험은 아니었지만 귀중한 경험이었던 것 같습니다. 혹시 회사에 축구장에서의 나와 비슷한 처지에 있는 개발자가 있지는 않을까라는 생각이 들었습니다. 잘하고 싶은 마음과는 달리 나는 팀에 도움이 안 된다고 생각하거나, 실수로 장애를 내서 많은 사람을 피곤하게 하는 그래서 혼자 마음 아파하는 개발자 말입니다. "만약 주위에 이런 개발자가 있다면 어떻게 그 개발자에게 도움을 줄 수 있을까?" 라는 생각이 들었습니다.

아마 제가 축구장에서 실수를 하거나 기대에 못 미쳤을 때 하지만 제 마음은 더 잘하기를 간절히 바랄 때 듣고 싶던 말을 해주면 도움되지 않을까 싶습니다.

"괜찮아요. 어쩔 수 없었어요. 다음번에는 함께 잘해봐요. 저도 도울게요." (실수했을 때 격려함)
"와! 점점 좋아지고 있어요." (열심히 해서 조금이라도 나아진 점이 보일 때 칭찬함)
"이럴 때는 이렇게 한번 해보세요." (발전할 수 있게 도움)

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

,
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
지금까지의 글에서 검증시점을 앞당김으로 인해 발생하는 이익에 대해서 소개했고 이어 구체적으로 사용할 수 있는 여러 가지 방법에 대해 소개했습니다. 하지만 글의 흐름상 언급하지 못한 부분이 있어 여기서 소개합니다. 지금까지 했던 얘기해서 크게 벗어나는 얘기는 아니고 동일한 맥락을 가지고 있기 때문에 참고하면 좋을 것 같습니다. 아래는 오류의 원인을 찾고 있는 한 개발자의 가상사례입니다.

어느 날 한 개발자가 웹 애플리케이션의 오류로그를 살펴봅니다. 그런데 데이터베이스에 나이를 저장하는 쿼리 부분에서 제약조건위배 오류가 발생하고 있습니다. 오류에 대해 자세히 살펴보니 나이 필드에 NOT NULL 제약이 걸려있음에도 NULL 값이 입력되고 있다는 내용입니다. 그런데 오류를 자세히 보니 실행경로를 보여주는 Stacktrace 정보가 없습니다. 코드를 보니 누군가가 Exception을 잡아서 그냥 한 줄 로그만 출력하게 해놓았습니다. 이런! 열악한 상황이지만 어쨌든 문제는 찾아야 하기 때문에 쿼리 부분에 문제가 있나 라는 생각을 해봅니다. 쿼리에 값을 넣어주는(Binding) 부분을 자세히 살펴봅니다. 그런데 큰 문제가 없어 보입니다. 그 다음으로는 자연스레 서비스 레이어(Service Layer)를 살펴보게 됩니다. 혹시 나이 부분에 대해 뭔가 다루는 부분이 있나 자세히 살펴봅니다. 하지만 특별한 것을 발견하지 못합니다. 마지막으로 프리젠테이션 레이어(Presentation Layer)를 살펴봅니다. 그리고 프리젠테이션 레이어에서 나이 값이 잘 들어왔는지에 대한 검사를 하고 있지 않다는 것을 발견합니다. 거의 이 부분이 원인일 것이라 생각하지만 확신할 수는 없습니다. 왜냐하면 프로그래밍에는 너무 다양한 상황이 있다는 것을 알고 있기 때문입니다. 

위의 예를 통해 저는 두 가지 문제점을 봅니다. 첫째는 개발자가 원인을 조사하기 위해 여러 파일들을 살펴보아야 했다는 점입니다. 두 번째는 시간을 들여 여러 파일을 살펴보았음에도 불구하고 명확한 결론이 아닌 거의 확실한 추정 정도의 수준에서 조사를 끝낼 수 밖에 없다는 것입니다. 여기서 위 사례에 한 가지 가정을 더해봅시다. 마지막에 개발자가 추정한 것이 실제원인이었다는 가정입니다. 이 가정 하에서 보면 위와 같이 일을 어렵게 만든 원인을 알 수 있습니다. 바로 '오류 희석'입니다.

널리 사용되거나 공식적인 용어는 아니지만 오류 희석은 시간 혹은 공간을 지남에 따라 오류가 본래의 모습을 점점 잃어버리는 것을 뜻합니다. 위 사례의 경우 근본 원인은 사용자가 나이를 입력하지 않았다는 것입니다. 하지만 문제는 프리젠테이션 레이어와 서비스 레이어를 지나 데이터베이스에 질의하는 부분에서 전혀 다른 모습으로 발생하였습니다. 그 결과로 개발자는 문제를 찾기 위해 문제가 발생한 지점까지 도달할 수 있는 모든 경로를 역추적해야 했습니다. 

그렇다면 해결책은 무엇인가요? 해결책은 무척 간단합니다. 바로 프리젠테이션 레이어에서 나이가 정확히 들어오는지를 검사하고 만약 나이가 정상적으로 들어오지 않는다면 엄격하게 예외처리를 해주면 됩니다. 사용자에게 '나이를 입력해주세요.'라는 문구를 보여줄 수도 있고, 바로 에러를 발생시켜 처리를 중지할 수도 있습니다. 이렇게 처리한다면 문제가 발생했을 때 사용자든 개발자든 문제를 정확히 알 수 있습니다. 특히 개발자가 에러로그를 보았을 경우 개발자는 소스 파일을 탐색하거나 추정을 할 필요가 없습니다. 왜냐하면 에러로그 그 자체만으로도 무슨 문제인지 너무나도 명확하게 알 수 있기 때문입니다.

위에서 얘기한 것은 빠른 실패(Fail-Fast)의 대표적인 예입니다. 빠른 실패(Fail-Fast)는 이전에 언급했던 Fedex의 1-10-100 원칙과 유사점을 가집니다. 빠른 실패의 취지는 이왕 실패할거면 빨리 실패하자는 것입니다. 왜냐하면 위의 예대로 잘못된 부분이 있음에도 실패하지 않고 시간이 지나다 보면 오류희석으로 인해 잘못된 부분을 점점 탐지하기 어려워지고 실패에 대한 처리 또한 어려워지기 때문입니다. 그래서 앞서 얘기했던 검증시점을 앞당기려면 보다 빠르게 실패하는 것이 좋습니다.

하지만 주의해야 할 점은 모든 곳에서 빠른 실패가 필요하지는 않다는 점입니다. 때로 문제나 결함을 허용하는 것이 매우 중요한 일인 경우가 있습니다. 예를 들어 포털의 메인화면을 생각해볼 수 있습니다. 포털의 메인화면은 매우 다양한 구성요소로 이루어져 있습니다. 그런데 그 중 우측 하단 쇼핑 정보를 가져오는 부분에 문제가 생겼다고 가정을 해봅시다. 이 경우 화면을 보여주기 위한 모든 처리를 중지한 후 화면을 표시하지 않는 것 보다는 그 부분을 제외하고 다른 부분은 정상적으로 보여주는 것이 현명한 판단일 것입니다.


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

,
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
어떤 부분을 수정했는데 잘못된 부분이 있습니다. 이때 잘못되었다는 피드백을 즉시 받을 수 있다면 가장 이상적인 환경일 것입니다. 지금 까지 얘기했던 테스트 환경과 CI서버를 잘 구축해놓으면 이런 이상적인 환경을 구축할 수 있고 현실화 시킬 수도 있습니다.

하지만 실전에서는 또 다른 어려운 점이 많이 생깁니다. 개인적인 경험을 되돌아보면 어떤 부분을 리팩토링 하려고 했더니 그 부분이 핵심 기능 중 하나라서 리팩토링에 따른 영향이 무척 컸던 경우가 있습니다. 영향 받는 부분을 검토하고 검증 방법이 있나 살펴보았습니다. 그런데 테스트도 없고 그 외 검증할 방법이 마땅치 않았습니다. 리팩토링에 투자할 수 있는 시간은 한정되어 있고 고민에 빠져들었습니다. 이런 때는 어떻게 해야 할까요?

이럴 때 바로 영향력 제어가 필요합니다. 영향력 제어는 리팩토링 즉 변경에 따라 발생하는 영향력을 현재 상황에서 다룰 수 있을 정도로 축소시키려는 노력입니다. 이를 위한 여러 가지 방법이 있겠지만 이어지는 글에서 본인이 실무에서 사용하여 그 효과를 느낀 방법 두 가지를 소개하려고 합니다.

1. 위임을 이용한 영향력 제거

지난 단위 테스트에 대한 글에서 인터페이스가 변경되지 않는 경우 영향력을  제거하는 방법을 소개했습니다. 하지만 인터페이스가 변경되는 경우에는 이 방법을 사용할 수 없습니다. 결국 영향 받는 부분 모두를 검증해야 하는데 현실적인 여건상 쉽지 않을 때가 많습니다. 이럴 때는 다소의 레거시(Legacy)를 인정하며 영향력 제거를 꾀하는 '위임'을 이용하여 리팩토링을 진행할 수 있습니다.

예전 리팩토링 프로젝트를 할 당시 프로젝트 전역에 걸쳐 사용되는 핵심 도메인 모델의 한 메서드를 리팩토링 해야 하는 경우가 있었습니다. 코드 내부뿐 아니라 메서드의 인터페이스도 변경해야 했습니다. 그런데 인터페이스를 수정하려고 보니 수많은 JSP에서 해당 인터페이스를 사용하고 있었습니다. 안타깝게도 당시 JSP에 대한 자동화 된 검증방법을 준비하지 못한 상황이었습니다. 그래서 수정을 하더라도 어마어마한 범위의 확인을 어떻게 할지에 대한 대책이 필요했습니다. 어떻게 하면 시간을 아끼면서 리팩토링도 할 수 있을까? 저는 많은 고민을 했고 결국 위임을 사용했습니다. 세부적인 방법은 아래와 같습니다.
 
1) 리팩토링 했을 때 최종적으로 만들고 싶은 형태의 새로운 메서드를 개발하고 그에 따른 단위 테스트를 작성한다. 이때 주의할 점은 예전 메서드의 기능을 100% 지원할 수 있는 형태로 만들어야 한다는 점이다. 즉 하위 호환성을 완벽하게 제공해야 한다. 또한 메서드 인터페이스가 기존과는 달라야 한다는 점에 유의해야 한다. 만약 인터페이스가 같다면 단위 테스트를 이용하여 수정 전/후의 동일성을 보장하여 영향력을 제어할 수 있기 때문에 굳이 위임을 사용할 필요가 없다.

2) 예전 메서드에 대한 단위 테스트를 작성한다. 이후 수정을 할 때 해당 테스트를 통해 수정 전후의 동일성이 보장될 수 있게 한다.

3) 예전 메서드의 내부 코드를 모두 지운다. 그리고 예전에 하던 것처럼 동작을 직접 구현하는 것이 아니라 1번에서 만든 새로운 메서드에게 구현을 위임하는 방법으로 코드를 작성한다. 이것이 가능한 이유는 1번에서 하위 호환성을 제공했기 때문이다.

4) 2번에서 작성한 테스트 코드를 이용하여 동일성을 보장한다. 만약 테스트가 잘 동작한다면 예전과 같게 동작하는 것이 보장된 것이다.

5) 추천하지 않는다는 의미의 @deprecated 주석을 예전 메서드에 달아 사용자들이 예전 메서드를 더는 사용하지 않도록 안내한다. 예전 메서드는 참조가 많아 부득불 하게 유지하는 것이며 가능한 한 빨리 제거하는 것이 좋다.

6) 예전 메서드에 @see 주석을 달아 대신 사용 가능한 새로운 메서드를 알려준다. 추가로 주석 부분에 새롭게 리팩토링 한 메서드가 필요한 배경을 설명하고 하위 호환성을 제공한다는 점을 밝혀두면 이후 개발자들이 더욱 신뢰성을 가지고 새롭게 리팩토링 한 메서드를 사용할 수 있기 때문에 좋다.


그림#1. 위임의 예

저는 위와 같이 작업하여 제가 리팩토링 프로젝트에서 만났던 비슷한 유형의 문제를 모두 해결할 수 있었습니다. JSP에서 해당 인터페이스를 호출하던 부분은 예전 메서드가 그대로 존재하며 '예전과 동일한 일'을 해주기 때문에 아무런 영향을 받지 않게 되었습니다. 새로운 메서드가 추가된 것 빼고는 예전과 모든 것이 동일했기 때문에 더 이상 검증에 대해 신경 쓰지 않아도 됐고 따라서 특별히 QA를 받지 않아도 됐습니다. 또한 @deprecated와 @see를 이용한 안내로 말미암아 새롭게 코드를 작성할 때는 리팩토링 된 메서드에 대한 안내를 좀 더 편하게 받을 수 있었습니다.

위임은 현실과 이상의 괴리를 채워주는 훌륭한 방법이라고 생각합니다. 즉 리팩토링을 하고 싶지만 완벽하게 할 수는 없을 때 한편으로 이상적인 코드를 추구하면서 잠시 예전의 코드도 인정하는 방법입니다. 이렇게 되면 인터페이스 변경에 따른 전반적인 코드수정을 하지 않아도 되며 리팩토링에 따른 영향력을 거의 무의미하게 만들어 검증 또한 쉬워집니다.

2. 인터페이스 변경에 따른 영향력 확산 고립

때로 인터페이스를 변경했을 때 그에 따른 영향의 파급이 계속해서 커지는 경우가 있습니다. 예를 들어 아래 그림은 DAO의 반환 타입을 기존 Map에서 모델로 바꾸었는데 BO(Business Object)에서는 DAO에서 받은 Map을 그대로 반환하는 예입니다. 그림에서 볼 수 있듯이 문제는 해당 BO가 가지고 있는 영향력이 넓다는 점입니다. 따라서 BO의 인터페이스를 수정하게 되면 많은 부분에 영향이 미치고 검증의 부담으로 다가옵니다.


그림#2. 영향력 고립의 예

이럴 때는 영향력의 확산을 막기 위해 위 그림의 빨간 선 부분에서 영향력을 고립시킬 수 있습니다. 즉 DAO는 리팩토링을 해서 Map대신 Model을 반환하게 하지만 BO에서는 예전과는 달리 DAO에서 Model을 받더라도 예전처럼 Map으로 바꿔 반환함으로써 외부에서는 변화를 모르게 하는 방법입니다.

여기서 중요한 점은 BO에 대해서 단위 테스트에 얘기했던 단위 테스트를 이용한 동일성 보장을 해주어야 한다는 점입니다. 기존에 쓴 글에서 필요한 부분을 일부 발췌했습니다.

이어 단위 테스트는 인터페이스 변경 없이 내부코드만 변경이 있을 경우 ... 내부코드 변경의 경우는 영향 받는 곳의 코드변경은 없다는 점 ... 이 점을 통해 알 수 있는 사실은 만약 우리가 변경되는 내부코드의 변경 전/후의 동일성을 확실히 보장한다면 내부코드 수정으로 말미암아 영향 받는 부분에 대해서는 더는 신경 쓰지 않아도 된다는 것 (생략)

이 방법을 적용했을 때 BO의 내부코드는 변경 되었지만 동작은 예전과 동일하다라는 것이 보장되기 때문에 해당 BO의 변화에 영향 받는 넓은 범위의 동작을 보장할 수 있게 됩니다. 영향력 고립시키기는 부분적으로 리팩토링을 할 때 원치 않게 영향력이 확산되는 것을 막아줍니다.

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

,