Search

[독서 후기] 클린 코드

Created
2022/10/08
Tags
OOP
Book
Review
Clean
Code
지금으로부터 3년 전인 2019년에는 코드를 잘 짜고 싶은 마음에 클린 코드를 구매했었다.
구매 당시에 클린 코드를 읽었을 때는 내용이 너무 와닿지 않아서 완독에 실패했다. 아마 클린 코드에서 표현하고자 하는 것들을 너무 모르고서 코딩한 것이 컸다는 것일거고, 그만큼 아무 생각없이 코드를 짜서 그랬을 것이다.
이 때의 실패 이후로 클린 코드를 다시 완독하려는 시도를 2020년도에 한 적이 있었는데, 이마저도 실패했다.
그리고 시간이 많이 흘렀고, 서비스를 운영해보면서 느낀 점들과 불편한 점들, 불안한 점들을 통해 마침내 2022년엔 완독할 수 있었다.
이번만큼은 이전 시도와는 달리 책의 도입부부터 어마어마하게 공감이 많이 되었다. 프로그래밍이란 무엇이고, 깔끔한 코드란 무엇일까에 대해 먼저 생각을 정리하고, 개요를 읽었을 때의 그 공감은 아직도 선명하다.
특히 개요에서의 내용을 각 챕터마다 자세히 풀어낼 때, 내가 경험한 것들도 많아서 리뷰로 너무 좋았고, 일부는 내가 경험해보지 못한 것들이라 신선해서 좋았다.
완독하고 나서야 왜 클린 코드는 경험이 받쳐주지 않으면 읽기 힘든 책인지 몸소 깨달을 수 있었다.
객체 지향의 사실과 오해에서 내가 코드를 짤 때 집중해야 하는 요소를 깨닫고, 클린 코드를 통해 객체 지향에서의 초점으로 더 나은 코드를 작성하는 법을 공감할 수 있게 된 것에 감사하고 있다. 거창한 것을 깨달은 것 같지만 그렇지는 않은데, 내가 앞으로 작성할 코드들에 이런 능숙함이 더 묻어났으면 좋겠다.
나도 비야네 스트룹스트룹이 말한 것 같은 목적이 분명하면서도 우아한 코드를 작성할 수 있는 개발자가 되고 싶다. 아래에 내가 완독하면서 집중했던 요소들을 조금 풀어봤다.

1) 객체 지향

1. 객체는 행동에 집중하고, 절차는 데이터에 집중하자

몇 프로젝트를 진행하면서 객체 지향을 하려고 굉장히 많이 애썼다. 그럼에도 객체 지향이라고 하기엔 모호한 결과물이 나왔고, 코드가 잘못 나온 것 같은 느낌은 있는데 이유를 찾을 수 없었다.
책을 통해 문제점을 인식한 것은 아니지만 SQL 위주의 데이터 중심적인 구조로 프로젝트를 설계한데 문제가 있었다. 객체 지향의 프로그래밍과 SQL이 지향하는 정규화 구조에는 목적이 다르기 때문에 당연히 데이터 중심적인 구조로 프로그래밍에 임하면 객체 지향이 잘 될리가 없었다.
그리고 이 책을 읽었을 때 데이터에 집중한 내 코드가 절차 지향이라는 것을 깨닫고, 절차적인 설계를 하면서 객체 지향을 하려니 안 되었구나라고 명확하게 인식할 수 있었다.

2. 객체 지향의 캡슐화

객체 지향의 큰 특징 중 하나인 캡슐화의 목적이 어느 법칙에 근거한 것인지 알 수 있었다. 디미터 법칙으로부터 유래된 캡슐화는 남에게 속사정을 드러내지 않는 것에 있다.
그리고 객체 지향은 행동에 집중하는 것이기 때문에 getter의 사용으로 간접적으로라도 자료를 드러내는 것도 주의가 필요하다는 것을 알 수 있었다. 물론 이러한 관점이 자바 빈 프로퍼티와 상충되긴 하지만, 어떤 관점에서 이를 주장한 것인지 충분히 납득할 수 있었다.
내 생각 역시도 형식에 갇힌 메서드보다는 필요에 의해 목적이 분명한 메서드가 좋다는 생각을 많이 했다.

3. 객체 지향의 추상화

추상화가 필요한 이유는 겉으로 드러나는 요소들을 프로그래밍 상에서 객체 지향의 목적에 따라 행위에 집중하도록 만들기 위함임을 알 수 있었다.
이렇게 추상화 된 객체는 인터페이스로 나타나면서 OCP를 준수하기 용이한 형태의 코드가 나옴을 깨달을 수 있었다.

2) Format

1. 주석 정리

재잘 거리는 주석
오해의 여지가 충만한 주석
코드와 중복된 내용의 주석
의무감에 작성하는 주석
이력 관리를 위한 주석
있으나마나한 주석
위 요소들은 클린 코드에서 밝힌 지양해야할 주석들이었다. 나는 주석을 애용하는 프로그래머였다. 그런 입장에서 완독 전에 위 내용을 접했을 때는 공감하지 못했다.
하지만 서비스를 운영하면서 혹시나 모를 상황에 대비해 주석을 열심히 라인 바이 라인으로 달았던 내 상황은 오히려 걸림돌에 걸리는 상황이 되었다.
주석을 작성할 것이라면 정말 최고의 주석을 달든가, 그렇지 않으면 변수와 함수 이름을 잘 작성하는 것이 가장 좋은 방법이라는 말에 공감한다.
코드를 유지 보수하면서 지난 주석들을 걷어낸 적이 있었는데, 그 때 주석을 찬찬히 읽어보니 정말 말도 안 되게 작성한 것들을 볼 수 있었다. 최대한 3자의 시선으로 많이 보려고 했는데, 정말이지 가관이 아닐 수 없었다.
놀랍게도 이력 관리를 위한 주석을 제외하고선 모두 해당되었다는 사실이 조금은 부끄러웠다. 주석을 작성하더라도 이렇게 못 쓸 수가 있나 싶었는데, 차후에 주석이 꼭 필요하다면 주석의 목적을 잘 살릴 수 있도록 작성하고 싶다.

2. 코드 스타일

Norminette와 같은 린터를 사용하면서 처음에는 린터를 사용하는 목적을 알 수 없었고, 그저 귀찮은 존재로만 인식하고 있었다.
하지만 린터를 사용해가면서, 그리고 여러 사람과 협업을 해보면서 린터의 중요성을 명확히 인지할 수 있었다.
클린 코드에서 제안한 가로 밀집도와 세로 밀집도는 120자와 40줄 내외였다. 사람에 따라 다를 수 있지만 이 정도의 밀집도가 사람이 봤을 때 가장 무난하다는 것이었다. 내 경우엔 100자와 50줄 정도로 잡고 있었는데, 다시 한 번 더 내가 잡은 기준에 대해서도 유한 기준으로 협의해볼 수 있겠다는 생각을 했던 것 같다.
추가로 연산자 관점에서의 가독성에서도 알게 된 것이 있었는데, Golang에서의 연산자 정렬을 살펴보면 곱셈과 나눗셈만 이상하게 붙여서 정렬하는 것을 볼 수 있었다.
처음에는 별 신경도 안 쓰고 있었는데, 2*3 + 4/5와 같은 형식이 연산에 이용되는 한 묶음을 편하게 보기 위함이라는 것을 알 수 있었다. 별 것 아니지만 Golang에서의 포맷팅이 얼추 근거가 있는 것이었구나 라고 느끼기도 했다.
그리고 코드 간의 수직 거리는 세로 밀집도에 영향을 준다는 것을 알았고, 이 때문에 종속 함수가 어떤 순서로 스크립트에 작성되어야 하는지 스스로 기준을 정할 수 있었다.
이와 비슷하게 변수와 인스턴스의 정의 순서에도 고민해볼 수 있었으며, 가위 규칙이라는 것도 찾아볼 수 있었다.

3) Exception

에러 핸들링의 경우 Flag 혹은 Return 값으로 처리하게 된다. 이에 따라 매 순간마다 해당 값을 검증하는 식의 로직이 구성된다.
깨끗한 코드라 함은 오류가 발생하지 않는 것이 아니라 오류를 정확히 명세하고 처리하는데 있다.
오류를 잘 처리하는 것은 단순히 오류를 잘 확인하는 것이 아니라, 오류 처리가 적절한 곳에 위치하고 주요 로직을 방해하지 않는 곳에 있는지 역시 포함된다.
기존의 에러 핸들링은 이런 요소들을 만족시키기 어렵다. 하지만 Exception은 그렇지 않다.
Checked Exception 보다는 C++의 throw() 혹은 noexcept 처럼 Unchecked Exception을 이용하는 것이 좋다는 것을 사례를 통해 확인할 수 있었다.
이는 Dynamic Throw가 없어진 이유를 글을 통해서만 이해하고 있던 것을 명확히 느낄 수 있었다.
OCP를 준수할 수 있었야하는데 Checked Exception을 이용하게 되면 코드 변경 시 이를 어길 가능성이 높아지고, 그렇게 바뀐 명세는 해당 코드를 의존하고 있는 곳에서 특히 치명적이기 때문에 가급적이면 Checked Exception으로 운용하는 것은 지양해야함을 알 수 있었다.
물론 예외적으로 Exception이 명확하게 한정된 라이브러리 개발같은 경우에는 Checked Exception을 이용할 수는 있다는 것으로 확인했다.
하위 Layer에서의 반복적인 Exception의 처리는 Catch 구문을 늘리게 한다. 따라서 해당 Exception들을 별도로 감싸서 하나의 Exception 객체로 운용하며 행동을 축약할 수도 있다는 것을 알았다.
Golang에서는 Exception과 비슷한 panic이라는 개념이 있지만, 에러 핸들링을 이용한다.
기존에 C를 하다가 C++로 넘어오면서 에러 핸들링이라는 개념이 없어지고, Exception을 이용한 것에는 이유가 있다고 생각했었다. 그리고 대부분의 언어가 Exception을 이용하는데는 비슷한 이유일 것이라고 생각했다.
이럼에도 불구하고 비교적 최근에 나온 Golang에서는 에러 핸들링을 쓰는 것이, Exception에 해당하는 챕터를 읽고서는 의문 투성이가 되긴 했다.
덕분에 많이 비교하려고 찾아봤다. Try-Catch와 같은 형태는 에러 핸들링 로직을 메인 로직과 분리하여, 코드의 집중도를 높여줄 수 있다는 장점이 있다. 하지만 Try-Catch는 결고 값싼 연산이 아니라고 한다. 이를 통해 유추해보건데, Golang에서는 에러 핸들링 로직과 메인 로직의 분리보다는 즉각적인 에러 처리와 그 여부를 확인하는 것이 더 중요하다고 본 것이 아닐까 싶었다. 그리고 이러지 않아도 되는 부분들은 panic으로 보완하면서 Exception 개념을 이용할 수 있기는 하도록 만든 것이 아닐까 했다.

4) 테스트

테스트 역시 주석처럼 있을 거면 아주 좋게 있든가 그렇지 않으면 아예 없는 것이 낫다는 일화가 굉장히 흥미로웠다.
어정쩡하고 지저분한 테스트 코드는 오히려 코드를 좀먹고 결과적으로 없는 것과 비슷한 결과를 낳는다는 일화도 볼 수 있었다.
모 아니면 도 식의 테스트 코드에서, 그럼에도 테스트 코드는 있는 것이 무조건 맞다의 결론에 58000% 공감한다.
일부 칼럼에서는 TDD의 단점을 지적하기도 했지만, TDD를 진행하면서 테스트 코드를 작성하기 쉬운 코드를 만들어가는 능력은 굉장히 중요할 것이라는 생각도 했고, 이상적으로 테스트 코드 작성 시간 대비 운영 코드 작성 시간이 서로 비등비등하게 나올 수 있도록 연습을 많이 해봐야겠다고 느꼈다.
단순히 인풋 대비 아웃풋이 올바른지 보다는 함수 기능의 세세한 곳까지 제어 단위로 검증할 수 있는 테스트 코드를 작성해야, 서비스가 무너지지 않는다는 것도 몸소 느꼈다보니 아무래도 클린 코드에서 말하는 것들이 엄청 와닿았던 것 같다.
테스트 코드는 운영 코드처럼 사용하는 것이 아니다보니 조금은 유연한 기준으로 코드를 작성할 수 있다는 것을 알게 되었다. 그럼에도 중요한 점은 어떤 것을 테스트 하려는지 그 의도와 환경과 검증이 명확히 보여야 한다는 것이었다.
테스트 항목에는 given, when, then의 구조를 이용하는 Build-Operate-Check 패턴으로 작성하는 것이 목적을 파악하는데 좋다는 것을 알 수 있다.
테스트 항목 1개에 1개의 assert가 들어가야 한다는 주장은 나쁘지 않은 개념이라고 생각지만, 실질적으로는 그러기가 쉽지 않으니 여럿을 쓸 수도 있다고 생각하기도 했다. 다만 1개의 테스트의 항목에는 assert가 적을수록 좋은 것 같긴하다.