클린코드(clean code)를 읽고 요점 정리 및 코딩의 방향성 잡기
시작
클린 코드. 말그대로 깔끔한 코드를 작성하기 위해 개발자가 알아야 할 사항들을 집필한 교과서 같은 책이었습니다. 다만 우리나라 저자가 아닌 점, 최초 발행일이 2008년인 점을 감안하여 맹목적으로 따르는 것이 아닌 옳은 방향을 잡는 나침반과 같은 용도로 삼아야겠다 생각했습니다.
회고
클린 코드의 옮긴이는 서문에서부터 시간에 쫓겨 나쁜 코드를 작성한 자신을 후회한다고 반성하였습니다. 1회독을 한 저도 그와 같이 반성을 하게 되었습니다. 이 책은 그런 책입니다. 개발에 대한 꿈을 가지고 공부한 뒤 입사를 하였지만 시간이 흐르며 일정에 치이고 현실과 타협한다는 자기합리화로 편한 길을 찾느라 망가진 코드에 대해 생각하게 만듭니다. 비슷해 보이는 기능을 찾아 제대로 검토하지 않고 복사 붙혀넣기를 하고 그저 돌아가기만 하면 장땡이라는 생각이 어느새 몸을 지배했습니다. 시간이 지나 코드를 돌아보았을 때, 잘 돌아가는 기능 속 붙혀진 수많은 if문, 불필요한 주석과 비대해진 메소드들을 심심찮게 확인할 수 있었고 지금부터라도 저자가 제시하는 방향으로 나아가리라 다짐했습니다.
결국 가장 빨리가는 방법은 언제나 깨끗한 코드를 유지하는 것입니다.
한가지 기능을 수행하는 메소드
깨끗한 코드란 무엇인가? 그것은 '보기에 즐거운' 코드입니다. 어떤 코드가 보기 즐거운 코드일까요?
한가지의 기능을 온전히 수행하는 것입니다.
위 문장을 보며 객체지향원칙의 5가지 중 하나인 SRP를 떠올렸습니다. 어떤 기능을 변경해야 할 이유는 딱 한가지 뿐이어야 합니다. 책임이 많아질수록 영향이 커지게 되고 많은 수정이 필요하고 이는 유지보수의 어려움으로 이어집니다.
코드 1-1.
public Response login(LoginDto loginDto) throws Exception{
String userId = loginDto.userId();
String userPw = loginDto.userPw();
UserVo userInfo = findUserInfoByUserId(userId);
String encUserPw = userInfo.userPw();
passwordMatch(userPw, encUserPw);
TokenVo tokenInfo = generateTokenInfo(loginDto);
LoginVo loginInfo = new LoginVo(userInfo, tokenInfo);
return new Response("success", MessageUtil.LOGIN_SUCCESS, loginInfo);
}
토이프로젝트의 코드 일부분을 가져왔습니다. 로그인 기능엔 수많은 부가기능이 필요로 합니다. 데이터베이스에서 해당 데이터가 존재하는지, 패스워드 암호화, 토큰 생성 등이 있습니다. 이뿐만 아니라 다른 기능들도 추가될 수 있습니다. 그런데 그 모든 기능들을 로그인이라는 이름의 메소드에 모두 넣는다면 이는 하나의 메소드가 여러가지의 책임을 지게 합니다. 즉 SRP원칙을 위반하는 것입니다. 그래서 부가 기능들은 다른 메소드들로 정의하였습니다. 하지만 위의 코드가 옳은지는 확신할 수 없습니다. 하나의 기능 마다 모두 세세하게 쪼개서 분리하는게 정말 유지보수를 편하게 할까? 라는 의문점이 남았기 때문입니다. 프로젝트 규모가 커지면 수백개의 메소드가 생성될 것이고 하위 메소드로 일일히 들어가서 확인하는 작업 또한 큰 일이 될 것입니다. 요점은 '모두' 분리하는게 아니라 '어떻게' 분리하는 것입니다. 완벽한 별개의 기능을 분리하는 것은 맞다고 생각하나 서로가 의존하게 되는 메소드의 경우는 한번쯤 고려해봐야 합니다. 또한 반복되는 기능은 클래스로 묶어 재사용성에 대한 고려도 동반되어야 합니다.
좋은 이름을 지어라.
여기서 좋은 이름은 함수명, 변수명, 클래스명 등을 말합니다. 이전에 저는 좋은 이름을 짓는데 많은 시간을 투자하지 않았습니다. 모두 영어로 이루어져 있는 특성 상 이름에 상대적으로 무게를 두지 않았습니다. 그러나 이 책은 좋은 이름의 중요성에 대해 설명하고 있습니다. 좋은 이름을 짓는데 고려되는 조건은 다음과 같습니다.
- 명확한 의미를 포함한다.
이름은 함수가, 클래스가 동작하는 기능에 대해 명확히 설명해야 합니다. 철자 수가 길어져도 되니까 명확해야 합니다. 오히려 검색하기 쉬운 긴 이름이 낫다고 합니다.
- 그릇된 정보는 피하라.
당연하지만 이름에 잘못된 정보를 담아서는 안됩니다. 예로 여러 계정을 담는 데이터를 accountList로 지었습니다. 그런데 위 데이터의 자료형이 List가 아닌 다른 형태라면? 개발자가 해당 변수명을 지은 의도는 알겠으나 이는 혼동의 여지를 주게 됩니다.
- 발음하기 쉬워야 합니다.
단어의 약어만을 사용하여 변수명을 구성하는 것은 지양해야 합니다. generateYearMonthDay를 gnrtYMD와 같은 약어로 지으면 발음하기 어렵습니다. 코드 리뷰를 하거나 자신의 코드를 설명해야 할 때 난감할 수 있습니다.
- 클래스와 객체는 명사, 메소드는 동사
employee.getInfo() 와 같은 식이 좋은 예입니다. 클래스와 해당 클래스의 어떤 메소드를 사용하고 이것이 어떤 기능을 호출하는지 명확하게 이해가 가능합니다.
- 일관적이어야 한다.
어떤 변수명은 user_info, 어떤 변수명은 userInfo는 일관적이지 않습니다. 일관성있는 어휘는 보기가 편합니다.
- 맥락을 추가해야 합니다.
firstName, lastName, zipCode, city, state 들은 같이 보면 주소에 대한 변수명임을 알 수 있지만 따로 보면 헷갈릴 수 있다. 클래스를 추가하여 묶거나 addrFirstName, addrLastName등으로 맥락을 추가하면 보기가 편하다. 반대로 불필요한 맥락이 있다면 제거하는 것도 필요하다.
조금의 의견을 덧붙히자면 명확한 긴 이름의 의미가 실제로 의미가 있는지는 잘 모르겠습니다. 영어를 자유자재로 구사한다면 모르겠지만 당장 저만 생각해도 명확한 의미의 긴 이름은 해석하기도 전에 눈이 피로한 느낌입니다. 영어 회화와는 다른 느낌입니다. 회화는 제스쳐와 대화의 맥락을 이해하면 어느정도 의사소통이 되지만 코드는 '완벽하게' 이해해야 합니다. '~를 바꾼다, ~로 바꾼다' 와 같이 해석을 한글자만 바꿔도 의미가 완전히 달라집니다. 프로젝트를 같이하는 팀원의 의견도 중요합니다. 제 생각은 변수명은 간단하게, 주의해야할 사항이 있다면 주석을 달자, 입니다.
함수
코드는 이야기처럼 위에서 아래로 읽어내려가며 해석해야 합니다. 메소드는 가능한 짧아야 하며 호출된 메소드는 해당 메소드의 다음에 순서대로 위치해야 합니다.
코드 1-2.
public UserVo findUserInfoByUserId(String usreId) throws Exception {
...
}
public void passwordMatch(String userPassword, String encUserPassword) throws Exception {
...
}
public TokenVo generateTokenInfo(LoginDto loginDto) throws Exception {
...
}
코드 1-1에서 호출한 메소드들이 순서대로 다음과 같이 이어진다면 유지보수하는 입장에서 더 편합니다.
또, 함수의 인수는 가능한 짧아야 합니다. 이상적 개수는 0개, 그리고 1,2,3개 순이고 4개 이상은 특별한 이유가 없으면 쓰지 않습니다. 관련된 변수들을 맵이나 리스트 객체로 넘기는 것도 방법일 것입니다.
플래그 인수는 넘기지 말아야 합니다. true면 이걸하고 false면 저걸 한다는 SRP를 깨트린다고 공표하는 의미입니다.
또, 오류 코드는 예외 처리하는 것이 깔끔합니다. 토이프로젝트에서 유효성 검증에 대한 글로벌 예외처리를 고민하였습니다.
마지막으로 마스터 프로그래머는 코드를 기능을 구현하는 것이 아닌 이야기를 풀어나가는 것이라고 합니다. 다른 개발자들이 이야기를 차례대로 읽으면서 편하게 이해할 수 있는 코드를 짜는 것이 중요합니다.
주석을 줄여라.
공교롭게도 이 의견의 절반은 동의하지 못하였습니다. 하지만 많은 반성 또한 하게 만드는 의견이었습니다. 이전에는 무조건적으로 자세한 주석이 유지보수하기 쉽다고 생각하였습니다. 중요한 기능은 어떤 로직으로 동작하는지, 유의사항은 어떤 것이 있는지에 대한 것들을 주석으로 작성한 적도 있습니다. 하지만 주석은 유지보수가 늘어나고 버전이 올라갈수록 발목을 잡습니다. 코드를 수정하면 그것을 설명하는 주석도 바꿔주어야 하고 바꿔주지 않으면 결국 쓸모없는 주석이 되어 자리를 차지하는 쓰레기가 될 뿐입니다. 의무적인 주석도 불필요합니다. 저자가 누구고, 어떤 버전이고, 뭘 수정했고, 이런 것들은 버전 관리 프로그램을 통해 모두 확인할 수 있습니다.
영어를 사용하는 원어민이라면 저자의 의견을 따라 주석을 최소화할 수 있겠습니다만 저는 한국인이고 영어의 의미를 파악하는데 버퍼링 걸리듯 시간이 소요되는것이 현실입니다. 앞서 얘기했듯 유의사항이나 중요사항은 한글 주석을 다는게 더 좋습니다. 어쩌면 메소드 전부 한글로 해석하는 주석을 다는게 보는 입장에서는 편할지도 모르겠습니다. 하지만 가장 좋은 방법은 중요한 부분만 주석을 추가하여 개발자에게 상기시키는 것이 아닐까 싶습니다. 물론 프로젝트 진행 시 팀원과 상의하여 조율이 가능합니다.
그 외 TODO 주석이나, 경고하는 주석은 좋은 주석입니다. TODO는 개발완료 시 삭제할 수 있고 경고 주석은 유지보수 시 좋은 참고가 될 수 있습니다.
저를 관통한 문장은 나쁜 코드를 주석으로 무마시키지 말라, 였습니다. 제가 봐도 알아보기 힘들 것 같은 코드를 주석으로 설명했던 과거가 생각나며 주석을 달지 않고 좋은 코드로 수정해야겠다는 다짐을 했습니다.
디미터 법칙
모듈은 자기가 조작하는 객체의 속사정을 몰라야 한다는 법칙입니다. SOLID원칙 중 마지막인 의존성 역전이 떠올랐습니다. 객체의 구현과 역할을 분리하면 구현 부분에서 어떤 기능이 추가해도 역할 부분은 수정이 필요하지 않습니다.
+-------------------+ +--------------------+
| High-level | | Low-level |
| Module | | Module |
+---------+---------+ +---------+----------+
| |
| |
V V
+---------+---------------------------+----------+
| Abstraction (Interface) |
+-------------------------------------------------+
고수준 모듈이 저수준 모듈을 의존하지 않고 추상화된 인터페이스에 둘 다 의존하고 있습니다. 이 경우 각 모듈의 결합도가 낮아집니다. 저수준 모듈에서 객체를 추가하고 그 기능을 추가하여도 고수준 모듈에서는 추가한 부분을 알 필요가 없습니다. '나는 너의 어떤 기능을 사용할 건데 그게 어디에 있는지는 알 바가 아니다.' 상태입니다.
테스트는 중요하다.
현업에서도 테스트 코드 한 줄 짜본 적 없던 저에게 TDD란 꽤나 생소한 개념이었습니다. 그래서 제 의견을 덧붙히기가 굉장히 애매했습니다. 사실 테스트 코드 없이 개발을 했기 때문에 이게 그렇게 중요한가에 대해서도 의문이 있습니다. 물론 공부를 할수록 중요성에 대해 체감할 수 있었습니다. 결함을 줄이고 코드를 지속적으로 깨끗하게 관리해야 하는 서비스 회사 입장에서는 필수적으로 작성해야 하지 않을까 생각하였습니다.
TDD법칙은 총 3가지로 구분합니다.
- 실패하는 단위 테스트를 작성할때 까지 실제 코드를 작성하지 않는다.
- 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
- 현재 실패하느 테스트를 통과할 정도로만 실제 코드를 작성한다.
중심을 테스트 코드에 두고 실제 코드를 작성하는 것입니다. 어떤 기능을 생각할 때 그 기능의 실패 조건을 생각하고 테스트 코드를 작성한 뒤 그 테스트를 통과할 정도로만 실제 코드를 작성합니다. 당연히 실제 코드만큼 테스트 코드도 중요합니다. 테스트 코드는 작성 전 명확한 테스트 슈트 정의가 필요합니다. 어떤 부분을 테스트할건지 확실히 알아야 코드도 작성할 수 있기 때문입니다. 모호한 테스트 슈트는 결함성을 높입니다.
테스트 코드는 함수마다 한 개념만 테스트해야 합니다. 놀랍게도 다음과 같이 테스트 코드를 작성하면 SRP원칙을 준수하여 코드를 작성할 수 있습니다. 기능 단위로 메소드를 정의할 수 있기 때문입니다.
테스트의 FIRST 법칙이 있습니다.
- FAST : 테스트는 빨라야합니다. 테스트 코드는 기능을 하나 수정해도 실행하기 때문에 자주 실행해야 좋은데 오래 걸리면 실행하기 쉽지 않습니다.
- INDEPENDENT : 독립적이어야 합니다. 한 테스트가 다음 테스트를 준비해서는 안됩니다. 기능, 개념별로 나누는 것이 중요합니다.
- REPEATABLE : 어떤 환경에서도 반복 가능해야 합니다. 네트워크가 없는 노트북에서도 실행 가능해야 합니다.
- SELF-VALIDATING : 성공 아니면 실패로 반환되야 합니다. 스스로 평가해야 합니다.
- TIMELY : 단위 테스트는 실제 코드를 작성하기 전에 작성되어야 합니다. 막상 기능을 만들어놓고 보니 테스트 하기 어려운 메소드임을 발견할 수 있습니다.
클래스는 확장에 개방적이고 수정에 폐쇄적이다.
재직 당시 회의에서 몇번 나왔던 주제가 있습니다. 스프링 부트 기반 웹 프로젝트 진행 시 화면 단위로 쪼갤 것인가, 기능 단위로 쪼갤 것인가? 결과는 항상 화면 단위로 구성하였습니다. 당시에는 별 생각이 없었으나 곰곰히 생각해볼만한 주제였습니다. 저자 로버트 마틴은 클래스는 확장에 개방적이고 수정에 폐쇄되는게 맞다고 하였습니다. 이는 SOLID원칙 중 하나인 OCP(개방-폐쇄의 원칙)이기도 합니다. 그러나 화면 단위로 개발할 경우 OCP원칙은 위배될 여지가 많습니다. 예를 들어, 로그인 화면에서 정책을 확인하는 API가 추가될 경우 Login 관련 클래스에 추가될 것입니다. 이는 기존 코드를 건드릴 여지가 있습니다. 그러나 기능 단위일 경우 별도의 클래스로 관리하기 때문에 기존 코드를 건드릴 필요가 없습니다.
그렇다고 기존 프로젝트의 진행 방식이 틀렸다고 생각하지는 않습니다. 기획 단계에서 이미 명확하게 화면이 구분되고 하위 기능들이 많지 않다면 화면 단위로 나눴을때의 이점이 존재합니다. 기능 단위로 기획을 재정의할 필요가 없고 해당 화면의 기능을 찾아가기가 더 용이합니다. 또한 진척상황을 정리하기도 편합니다. 클라이언트에게 이정도 완성됐습니다! 하고 보여주기가 명확합니다.
그러나 프로젝트의 크기가 커질수록, 화면의 수가 적고 그 안의 기능들이 많을 경우 화면 단위의 분리는 단점이 더 부각됩니다. 한 클래스에 메소드의 개수가 늘어납니다. 인스턴스 변수도 늘어날 수 있고 이 경우 유지보수성의 저하로 이어집니다. 확장성이 좋지 않으므로 새로운 기능 추가 시 위험 요소가 늘어납니다.
때에 따라서 양쪽 방식을 모두 사용하는 하이브리드 방식이 고려되기도 합니다. 이는 프로젝트의 특성을 파악하여 접근 방식을 달리 해야하리라 생각합니다.
소프트웨어 품질을 높이는 법
켄트 벡이 제시한 설계 규칙이 있습니다. 이 단계만 따른다면 모두가 좋은 품질의 소프트웨어를 개발할 수 있습니다.
- 모든 테스트를 실행한다.
설계한 의도대로 돌아가는 프로그램을 만드려면 내가 의도한 테스트는 모두 통과해야 마땅합니다. 테스트가 불가능하면 검증도 불가능합니다. 테스트를 신경쓰면 크기가 작고 목적을 하나만 수행하는 클래스가 탄생합니다. 당연히 낮은 결합도와 높은 응집도가 탄생하고 객체지향 프로그래밍의 의도한 대로 됩니다. 설계 품질이 올라가는 것입니다.
- 리팩터링
개발한 코드는 점진적으로 리팩토링합니다. 수정을 하며 기존 기능을 깨뜨리면 어쩌지 하는 걱정을 할 필요가 없습니다. 테스트 코드가 있습니다. 리팩토링은 기본적으로 높은 응집도, 낮은 결합도, 관심사의 분리, 함수와 클래스 소형화, 더 나은 이름 선택, 중복 제거 등이 있습니다.
이는 수학 공식처럼 적용하여 정답을 도출하지 않습니다. 끊임없이 고민하고 수정해야 합니다. 수정한 코드가 이후에 수정을 거듭하며 원래의 것으로 되돌아갈수도 있습니다. 이는 개발자의 숙명이기도 한듯 합니다.
한 메소드에 if문은 한개만.
중첩 if문을 남발하는건 매우 좋지 않은 습관이기도 합니다. 메소드, 관심사의 분리로 대체할 수 있습니다. 저도 두개이상 쓰지 않도록 노력하고 있습니다. 또한 긍정문이 이해하기 좋습니다. if(!isEmpty()) 보다는 if(isNotEmpty()) 가 낫습니다.
마치며
약 3년간의 개발자의 길을 걸으며 지나온 길을 되돌아볼 기회를 준 좋은 책이라 생각합니다. 블로그에 소개한 내용 외에도 동시성, 스레드에 관한 부분이나 다른 주의사항들, 실제 코드를 리팩토링하는 부분이 있습니다. 이는 작가가 지향하는 클린 코드가 무엇인지 더 자세히 알 수 있습니다. 그래서 신입은 물론 경력 개발자 분들도 읽으면 많은 도움이 되리라 생각합니다. 이미 알고있는 정보는 다시 되새김질 할 수 있고 미처 생각하지 못했던 부분도 존재할 것입니다. 당장 이해가 가지 않는 부분도 있을 수 있습니다. 저도 아직 책의 내용을 100프로 이해하지는 못했습니다. 동시성에 대한 부분은 이론적으로 접한 적 있는 용어일지라도 이것이 실제 소프트웨어에서 어떤 직, 간접적인 영향을 끼치는지 공부가 더 필요합니다. 그러나 모르는 정보일지라도 눈에 익히고 한번이라도 머리로 생각하는 것이 후에 큰 도움이 될 것입니다. 새로운 정보는 한번 본다고 바로 외워지는게 아닌 반복 숙달하는 과정이 필수적이니까요.
메모
인스턴스 변수로 선언한 것을 가져올 때 별도의 메소드로 선언하여 가져오면 변별력이 생겨 좋을 듯 하다.
기존 존재하던 클래스에 동작을 추가하는데 유용 = 자료 구조체(DTO)
새로운 클래스에 기능을 추가하는데 유용 = 객체(Service, ServiceImpl)
- 잘 구분하여 최적의 해결책 찾기
나쁜 주식
- 부적절한 정보 : 다른 시스템에 저장할 수 있는 정보
- 쓸모 없는 주석 : 오래된, 엉뚱한, 잘못된 주석
- 중복된 주석 : 코드만 봐도 아는데 구구절절히 설명한 주석 예) i++; // i 증가
- 성의없는 주석 : 당연한 소리 하는 주석
- 주석으로 처리된 코드