사다리 타기
이번 미션은 우리가 흔히 알고 있는 "사다리 타기"를 구현하는 미션이었습니다. 평소에 친구들과 사다리 타기를 할 때는, 너무 간단한 게임이고 어려운 규칙이 있는 것도 아니어서 쉬울 수도 있겠다는 생각을 했습니다. (물론 쉽진 않았습니다.)
1단계 - 사다리 생성
3일 안에 완성해야하는 미션의 첫 단계는 사다리를 요구사항대로 생성하는 일이었습니다. 이번 미션의 페어였던 테바가 졸업식에 참석해야하는 이슈로 2일 만에 끝내야 했기에 조금 서둘렀습니다. 토론도 속전속결, 드라이버와 네비게이터의 역할 전환도 서로의 git
저장소에 push
하고 pull
해오는 방법을 이용해 빠르게 진행했습니다. 테바와 저의 개발 스타일이 비슷했던 것도 큰 도움이 되었습니다.
다행히 1단계 미션은 이틀 만에 완료했고, 테바는 큰 걱정 없이 졸업식에 다녀올 수 있었습니다. 물론, 저희가 개발을 엄청 잘해서 빨리 끝냈던 건 아니고, 이렇게 속도를 낼 수 있었던 이유는 다름 아닌 이번 미션의 핵심 주제 TDD(테스트 주도 개발) 덕분이었습니다. TDD 사이클을 적극적으로 활용해 미션을 진행했더니 자동차 경주 미션 때와는 다르게 코드를 신뢰할 수 있게 되었고, 혹시나 발생할 오류를 걱정하는 것과 같은 불필요한 고민을 줄일 수 있었습니다.
TDD에 대한 자세한 설명은 추후 정리해서 포스팅할 예정입니다.
1단계는 빠르게 완료했던 것만큼, 지금 돌아봐도 크게 어려웠던 점은 없었던 것 같습니다. 다만, 미션 중에 테바와 함께 고민했던 부분과 코치의 피드백은 조금 공유하고 싶습니다.
저와 테바는 View
와 Domain
의 의존관계를 끊어내기 위해 DTO를 사용하기로 합의했고, 어떤 객체에게 DTO 생성의 책임을 부여해야할지 토론하고 있었습니다. 테바는 Controller
역할을 하는 객체에게, 저는 도메인이 직접 생성하는 쪽이 낫다고 생각했습니다. 쉽사리 의견은 좁혀지지 않았고, 저희는 코치에게 어느 쪽이 더 나은지 의견을 듣고자 코치룸으로 찾아갔습니다.
하지만 코치들은 명쾌한 해답을 내심 바라고 있던 저희의 기대와는 다르게 오히려 더 고민할만한 질문을 던져주셨습니다. DTO를 사용하는 이유를 명확하게 설명할 수 있는지, Domain
에 View
를 위한 기능이 존재하는게 아니라, View
가 Domain
의 기능을 활용하는 것 뿐이라고 생각하면 안되는 건지 등, 정말 다양한 관점의 질문을 주셨고, 저흰 나름 저희의 답을 드렸지만 번번히 반박 당했습니다.
약 20분 가량의 코치와의 토론(?)끝에 내린 결론은 굳이 DTO를 사용할 이유를 설명하지 못했으니, DTO 사용을 포기하고 기능을 더 명확하게 분리해서 View
와 Domain
사이의 의존관계를 끊어내자는 것이었습니다. 다행히, 만족할만한 코드를 작성해 앞서 말했던 것처럼 1단계를 빠르고, 무사히 마칠 수 있었습니다.
개인적으로 DTO의 사용 이유와 View
와 Domain
사이의 연관관계를 끊어내야하는 이유를 설명하자면,
-
DTO는 객체를 직접 의존하지 않고도 객체의 상태(값)를 전달하거나, 전달 받도록 할 수 있는 "정류장"의 역할로 사용할 수 있다고 생각합니다. 굳이 외부에서 필요로하지 않는 상태나 메소드를 노출하지 않고도 서로 다른 두 객체가 협력할 수 있다는 것 자체로 충분히 활용할만한 가치가 있습니다.
-
View
와Domain
의 의존관계를 끊어내야 하는 이유는 객체지향 프로그래밍의 원칙 중 하나인 개방-폐쇄 원칙(OCP)를 준수하기 위함과 동시에, 코드를 수정할 때, 그 파급효과가 최소화 되기 때문입니다.View
는 사용자나 클라이언트의 의견에 따라 자주 변경될 가능성이 높은데,Domain
과 강한 연관관계를 맺고 있다면 개발자가 사소한View
수정에도 읽어야하는 코드가 너무 길고, 실수하기 쉬워집니다.
2단계 - 사다리 게임 실행
짧지만 강렬했던 1단계가 리뷰어 미르의 merge
로 종료되면서, 본격적인 사다리 타기를 구현하는 2단계 미션을 시작했습니다. 워낙 1단계가 빨리 끝나서 다소 여유로울 것이라는 제 생각과 다르게 2단계에서는 신경써야 할 부분이 훨씬 많았습니다.
사다리를 그리는 것만 잘되면 OK였던 1단계와 다르게 사다리를 타고 내려오는 연산을 어떻게 해야할지부터 상당한 고민거리였습니다. 이중 for
문을 사용하기에는 프로그래밍 요구사항의 indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다. 때문에 적합하지 않았고, 성능 상으로도 그닥 좋지 않을 것 같았습니다. (물론 성능을 신경쓰라는 요구사항은 없었습니다.)
그때 마침 다른 크루인 폭포와 대화하면서 폭포가 떠올린 알고리즘을 들을 수 있었고, 저는 이를 활용해 사다리를 타고 내려오는 연산을 구현했습니다. 제 마음에도 들고, 코드도 그리 복잡하지 않았기 때문에 저는 좋은 코드를 작성했다고 생각했지만, 다른 러쉬나 테바는 오히려 너무 복잡하다는 의견을 제시했습니다.
알고리즘을 이해하는 것부터 조금 어렵고, 성능 개선에서 드라마틱한 효과가 있는 것도 아닌데 너무 다른 동료 개발자들의 능률을 떨어뜨리는게 아니냐는 의견이었습니다. 그러나 저는 이 정도의 알고리즘은 개발자라면 누구나 이해할 수 있고, 성능 개선은 하지 않는 것보단 조금이나마 하는게 낫지 않겠냐는 생각을 가지고 있었습니다.
이미 상당 부분 구현을 완료한 상황이라 저는 크루들의 의견을 반영하기 보다, 리뷰어인 미르의 피드백을 받아보기로 했습니다. 미르는 알고리즘 자체는 이해하기 어렵지 않고, 오히려 애매한 변수나 메소드 이름 때문에 이해하기 어렵다는 피드백을 주셨습니다.
물론, 알고리즘을 활용하는 방법이 더 좋다는 건 아니었습니다. 당연하겠지만 저희는 다른 개발자들과 협업하면서 살아갑니다. 제가 작성한 코드는 저보다도 다른 동료들이 더 많이 읽게 된다는 미르의 피드백에, 저는 "남이 읽기 좋은 코드"를 저만의 좋은 코드의 기준으로 삼아야겠다는 생각을 했습니다. 개발자로서의 좌우명이 "나를 위해 남을 돕는 개발자가 되자."인 만큼, 다른 동료 개발자들이 읽기 좋은 코드를 작성하고자 노력하려 합니다.
이외에도 매개변수에 final
을 붙인 이유, NPE
를 방지하기 위한 방법들, 경계값 테스트, Enum의 values()
를 활용하기 등, 다양한 피드백을 받았고, 이를 바탕으로 코드를 개선한 끝에, 2단계 미션도 무사히 마칠 수 있었습니다.
생각해볼 것들
이번 주 미션에서 학습한 내용에 대한 우아한테크코스의 질문과, 제 답변을 간략하게 정리해봤습니다.
내가 TDD, 리팩토링을 하는 이유
저는 TDD와 리팩토링을 더 빠르고, 믿을 수 있게 코드를 작성하고자 활용합니다. 결국 테스트를 작성한다는 것은 제가 설계한 기능과 객체들의 동작을 검증하는 것이고, 리팩토링 역시 코드를 더 깔끔하고 읽기 쉽게 하기 위해 진행합니다.
또한, 실패하는 단위 테스트를 작성하고, 성공하는 로직을 구현하고, 리팩토링을 통해 코드를 깔끔하게 만드는 TDD 과정(TDD Cycle)에서, 개발자는 필요한 모든 기능을 위한 테스트를 작성하게 됩니다. 이는 결국, 테스트 코드가 프로그램의 기능 명세서이자 모든 기능이 정상 동작한다는 보증서가 된다는 방증입니다. 테스트가 주도하는 개발, TDD를 올바르게 활용하고 있다면, 테스트가 작성되지 않은 그 어떤 기능도 먼저 구현되어서는 안됩니다.
개발자는 컴퓨터가 아닌 사람이기 때문에 누구나 실수할 수 있습니다. 그러나, 개발자의 실수가 때로는 치명적인 프로그램 오류나 회사의 금전적 피해로 이어질 수 있습니다. 따라서, 돈을 벌어다주는 개발자(회사가 원하는 인재...)가 되기 위해선 테스트를 잘 작성해서 "내 코드는 믿을만합니다!"라고 당당하게 말할 수 있어야 합니다. 그래서 저는 TDD와 리팩토링을 합니다.
더 빠르고, 꼼꼼하고, 믿을 수 있게 코드를 작성하고자.
기존에 구현하는 방식과 TDD로 코드를 구현할 때의 차이는
기존의 방식과는 다르게 TDD로 코드를 구현할 때는, 내가 구현한 코드에 대한 확실한 신뢰감을 얻을 수 있었습니다. 이미 성공한 테스트의 기능들은 다시 살펴보지 않아도 정상 작동한다고 가정하고 다른 기능의 테스트를 작성하고, 구현하다보니 속도도 훨씬 빨랐습니다. 지금 당장 무엇을 해야하고, 다음엔 무엇을 해야할지 명확한 방향성도 알 수 있었습니다.
리팩토링에서의 어려움과 어려움을 줄이기 위한 시도들
리팩토링하면서 겪었던 어려움의 대부분은 테스트가 통과된 기존의 기능들을 수정해야할 때 발생했습니다. 도메인 설계 과정에서의 실수로, 새 기능을 추가하는 과정에서 기존 코드에 수정해야하는 일이 생기면, 덩달아 테스트도 깨져서 실패하기도 하고, 프로그램이 아예 돌아가지 않기도 했습니다.
이러한 어려움을 줄이기 위해 저는 객체의 역할과 책임을 더 작게 나누고 명확하게 구분하려고 노력했습니다. 결국 TDD와 리팩토링은 기능에 대한 보증을 해줄 뿐, 설계에 대한 책임은 개발자인 저에게 있습니다. 제가 처음부터 설계를 잘못하면 아무리 테스트를 열심히 작성하고 예쁘게 리팩토링해도 결국 코드를 수정해야하는 순간이 올 수 밖에 없습니다.
신뢰할 수 있는 코드를 작성하기 위해 TDD를 하고, 리팩토링을 하는데, 기초부터 잘못됐다면 과연 믿을 수 있는 코드일까요?
개발을 잘한다의 기준?
개발을 잘한다의 기준은 동료 개발자들에게 얼마나 많은 신뢰와 지지를 얻고 있는가에 달렸다고 생각합니다. 물론 절대적인 기준으로 많은 지식을 가지고 있고(본인이 작성한 논문이나 저서가 있다던지), 대회 수상 이력이나 여러 자격증을 내세울 순 있겠지만, 동료 개발자들이 함께 일하기 꺼려한다면 결국 프로그래밍은 잘할 수 있어도 개발은 잘한다고 보기 어렵다고 생각합니다.
보통 개발은 혼자서 하는 것이 아니라, 회사나 동아리 등의 단체에서 함께 하는 것이 일반적입니다. 특히, 회사에서는 수많은 개발자들이 서로 협력하며 하나의 목표를 향해 함께 나아가고 있습니다. 어느 팀에서는 더 나은 사용자 경험을 위해, 또 다른 어느 팀은 더 나은 서비스 성능을 위해, 혹은 정확한 금액 계산을 위해 "함께" 노력하고 있습니다. 더 많은 경험과 지식을 바탕으로 다른 개발자들을 도와 공통의 목표를 달성하는 개발자는 결국 동료 개발자들이 "저 사람은 개발을 참 잘해."와 같이 실력이 뛰어나다고 인정해줄 것입니다.
적어도 저는 그런 개발자를 "개발 잘하는 개발자"라고 생각하고, 그걸 목표로 하고 있습니다.
내가 작성한 코드가 만족스러운가?
물론, 전혀 만족스럽지 않습니다. 😢 아직 부족한 실력 탓인지 처음 TDD를 해봤던 탓인지 여러모로 완벽하게 요구사항을 준수하지 못한 것 같아 너무 아쉽습니다. 리뷰어의 질문에 제대로 답하지 못했던 것부터, 코드 전반적으로 읽기 쉬웠나 한다면 별로 그렇지도 않았던 것 같습니다.
리뷰어였던 미르의 피드백처럼, 메소드나 변수의 이름이 한번에 그 의미를 파악하기 어려웠고, 객체들의 역할과 책임을 제대로 분리하지 못해 중복되거나 위치가 애매한 기능들도 꽤 있었습니다. 그래도 TDD는 꽤 만족스럽게 이루어졌던 것 같아 다행인 것 같습니다. 다음 미션에서는 제가 생각하는 클린 코드의 기준인 "남이 읽기 쉬운 코드"를 위해 조금 더 노력해봐야겠습니다.
공부할 개념들
- TDD
- DTO
- 경계값 테스트
- 좋은 테스트를 위한 원칙들
- CORRECT
- Right-BICEP
- FIRST
- Enum
- 불변 객체