책을 생일 선물로 받았었다. 많이 부족한 탓에 머리를 탕 탕치는 내용이 정말 많다. 다 읽고나니 가볍게 읽기엔 책에서 말하는 몸으로 체득한 생활화가 안될 것 같다. 그래서 뒤늦게라도 블로그에 정리한다. 이 책은 옮긴이 말이나 추천사부터 멋있다.
'코드에 정직하고, 코드에 상태에 관하여 동료들에게 정직하고, 무엇보다도 자기 코드에 대해서 자신에게 정직하라 ! '
서문과 추천사를 읽으면서도 인상적인 문구들이 굉장히 많았다. 이 책은 전문적인 개발자는 무엇이며, 전문적인 개발자가 어떤 마음가짐으로 개발해야 하는지를 기술하고 있다. 매일 친구들과 함께 개발 잘하는 개발자가 되고싶다고 노래를 부른다. 회사에서는 '신입이지만 빠르게 적응하고 있다. 잘하고 있다.'는 얘기를 꾸준히 듣지만, 과연 내가 전문적인 개발자가 되어가고 있는 게 맞을까라는 의구심이 들 때가 굉장히 많다. 사실 매일 하는 것 같다. 아직 한참 부족한 수준이다. 책을 읽으며 공감 가는 부분도 있고 미처 생각 못한 부분은 훨씬 많았다.
이 책에서 지향하는 방법들은 절대적인 것이 아니라 언제든지 더 나은 방법이 나올 수 있다고 말한다. 시스템을 구축할 때에 조직에서 소통을 하기 위한 기초지식을 제공하고 의논할 수 있는 토픽을 던지는 책이다. 앞으로 의식적으로 이런 원칙들을 따르고 더 나아진 방법으로 코드를 만들기 위해 생활화할 것이다. 저자의 의식의 흐름을 그대로 투영해 완결된 코드를 대상으로 처음부터 끝까지 리팩토링하는 과정을 다루고 있다. 좋은 소스를 찾아보고 어떠한 기준으로 생각으로 설계하고 구현했는지를 엿볼 수 있다는 건 정말 큰 기회라 생각한다. lean 원칙을 가장 실용적으로 적용한 책이라 하니 시작부터 기대치가 99.9가 되버렸다. 학부시절 수업을 듣던 품질 경영 용어들이 나와서 매우 반가웠다.
1장. 깨끗한 코드에 대해서 알아보자
어떤 언어를 사용하든 코드는 기계가 이해하고 실행할 정도로 정확하고 상세하고 정형화되어야 하므로 코드는 사라지지 않을 것이다. 궁극적으로 코드는 요구사항을 표현하는 언어이다. 막연히 좋은 코드를 짜야한다. 하드 코딩은 안돼! 라는 생각에서 구체적으로 기준을 세울 수 있게 설명해주는 부분이다.
나쁜 코드란?
우리 모두는 자신이 짠 쓰레기 코드를 보며 나중에 고쳐야지 생각한 경험이 있다. 르블랑의 법칙(나중은 결코 오지 않는다.)을 잊지 말자.
-
나쁜 코드는 나쁜 코드를 유혹한다. 흔히 나쁜 코드를 고치면서 오히려 더 나쁜 코드를 만든다.
-
나쁜 코드는 너무 많은 일을 하려 애쓰다가 의도가 뒤섞이고 목적이 흐려진다.
나쁜 코드의 대가
남들이 저질러놓은 쓰레기 코드로 고생한 경험. 나쁜 코드는 개발 속도를 크게 떨어뜨린다. 간단한 변경은 없다. 난해한 코드를 해독해서 더 꼬인 코드를 덧붙인다. 나쁜 코드가 쌓일수록 팀 생산성은 떨어진다.
원대한 재설계의 꿈 : 기존 시스템을 유지 보수하는 팀과 재설계를 하는 팀이 차출된다. 새 시스템이 기존 시스템의 기능을 모두 제공하고 변경도 모두 반영되야하므로 오랜 시간이 필요하다. (엄청난 비용이 발생하고 재설계를 맡았던 팀원들이 모두 떠나버리면 다시 제자리로..)
태도
코드가 너무 엉망이라 몇 시간으로 예상한 업무가 몇 주로 늘어진 경험은 아직 없다. 몇 시간으로 예상한 업무를 받은 적이 없다. 매번 이걸 내가 할 수 있을까 싶은 일이었다. 허허. 각설하고 이 케이스에서의 잘못은 전적으로 프로그래머에게 있다. 전문가답지 못하게 나쁜 코드를 짰기 때문이다. 관리자는 일정과 요구사항을 요청하는 것이 그들의 책임이다. 좋은 코드를 사수하는 일은 우리 프로그래머들의 책임이다. 나쁜 코드의 위험을 이해하지 못하는 관리자의 말을 그대로 따르는 행동은 전문가답지 못하다. 나쁜 코드는 생산성을 떨어뜨린다. 좋은 코드를 사수하면서 마감 기한을 맞추는 방법은 언제나 코드를 최대한 깨끗하게 유지하는 습관이다.
많은 사람들이 말하는 깨끗한 코드란?
-
깨끗한 코드란 보기에 즐거운 코드다.
-
깨끗한 코드는 세세한 사항까지 꼼꼼하게 처리하는 코드다. (철저한 오류 처리)
-
깨끗한 코드란 한 가지를 잘한다. 깨끗한 코드는 한 가지에 집중한다. 각 함수와 클래스와 모듈은 주변 상황에 현혹되거나 오염되지 않은 채 한길만 걷는다.
-
깨끗한 코드는 단순하고 직접적이다. 잘 쓴 문장처럼 읽힌다. (가독성)
-
결코 설계자의 의도를 숨기지 않는다. 명쾌한 추상화와 단순한 제어문으로 가득하다.
-
코드는 추측이 아니라 사실에 기반해야 한다. 반드시 필요한 내용만 담아야 한다.
-
깨끗한 코드는 작성자가 아닌 사람도 읽기 쉽고 고치기 쉽다.
-
단위 테스트 케이스와 인수 테스트 케이스가 존재한다. 모든 테스트를 통과한다.
-
깨끗한 코드에는 의미 있는 이름이 붙는다.
-
특정 목적에 달성하는 방법은 하나만 제공한다.
-
의존성은 최소이며 각 의존성을 명확히 정의한다.
-
코드는 문학적으로 표현해야 한다 (=인간이 읽기 좋은 코드를 작성해라)
-
깨끗한 코드는 언제나 누군가 주의 깊게 짰다는 느낌을 준다. (클린 코드에서 제안하는 방법 = 코드를 주의 깊게 짜는 방법)
-
세세한 사항까지 꼼꼼하게 신경 쓴 코드다.
-
중복이 없다. (중복은 아이디어를 제대로 표현하지 못한다는 증거다.)
-
시스템 내 모든 설계 아이디어를 표현한다.
-
클래스, 메소드, 함수 등을 최대한 줄인다.
-
초반부터 간단한 추상화를 고려한다.(작게 추상화하라)
-
코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 된다. (읽으면서 짐작한 대로 돌아가는 코드가 깨끗한 코드다.)
-
코드가 그 문제를 풀기 위한 언어처럼 보인다면 아름다운 코드라 불러도 된다. (언어를 단순하게 보이도록 만드는 책임이 우리에게 있다.)
개발자는 저자(author)다
새 코드를 짜면서 우리는 끊임없이 기존 코드를 읽는다. 읽기 쉬운 코드의 중요성을 보여주는 대목이다. 급하다면, 쉽게 짜려면, 읽기 쉽게 만들면 된다.
보이스카우트 규칙 = 캠프장은 처음 왔을 때보다 더 깨끗하게 해 놓고 떠나라 !!
매번 좀 더 깨끗한 코드를 만들고자 노력한다면 코드는 절대 나빠지지 않는다. 한꺼번에 많은 시간과 노력을 투자해 코드를 정리할 필요가 없다. 변수의 이름을 명확하게 만들고, 길어진 함수를 명확하게 하나의 기능을 하도록 분할하고, 약간의 중복을 제거하고, 복잡한 if문을 정리하면 충분하다.
프리퀄과 원칙
이 책에서는 다양한 설계 원칙을 산발적으로 거론한다. 나중에 시간을 내서 PPP도 읽어보자!
-
SRP(The Single Responsibility Principle) : 클래스에는 한 가지, 단 한 가지 변경 이유만 존재해야 한다.
-
OCP(The Open Closed Principle) : 클래스는 확장에 열려있어야 하며, 변경에 닫혀 있어야 한다.
-
LSP(The Liskov Substitution Principle) : 상속받은 클래스는 기초 클래스를 대체할 수 있어야 한다.
-
DIP(The Dependency Inversion Principle) : 추상화에 의존해야 하며, 구체화에 의존하면 안 된다.
-
ISP(The Interface Segregation Principle) : 클라이언트에 밀집하게 작게 쪼개진 인터페이스를 유지한다.
2장. 의미 있는 이름을 짓자
의도를 분명히 밝히라
좋은 이름을 지으려면 시간이 걸리지만 좋은 이름을 통해서 절약할 수 있는 시간이 훨씬 더 많다. 이름을 주의 깊게 살펴 더 나은 이름이 떠오르면 개선하자.
-
변수(or 함수 or 클래스)의 존재 이유는? 수행 기능은? 사용 방법은?
-
이름이 이 문제에 대해서 명확하게 답해주지 않고, 따로 주석이 필요하다면 의도를 분명히 드러내지 못한 것이다.
-
변수의 이름만 제대로 고쳐도 코드는 더욱 명확해진다. 단순히 이름만 고쳐도 함수가 하는 일을 이해하기 쉬워진다.
그릇된 정보를 피하라
프로그래머는 코드에 그릇된 단서를 남겨서는 안 된다. 서로 흡사한 이름을 사용하지 않도록 주의하자. 유사한 개념은 유사한 표기법을 사용한다. 일관성이 떨어지는 표기법은 그릇된 정보다. 십중팔구 개발자는 자동 완성 기능(이클립스에서의 ctrl + space)을 통해 상세한 주석이나 메서드 목록을 살펴보지도 않고 이름만 보고 객체를 선택한다. 책에서 이 부분을 읽고 몹시 찔렸다. API를 세세하게 읽어가며 만들 때도 있지만 대부분 이름만으로 판단했기 때문이다. 하지만 돌려 생각하면 이름만으로도 명확히 의도를 표현한 덕분에 별다른 문제없이 코딩할 수 있었던 것이다!
의미 있게 구분하라
연속된 숫자를 덧붙이거나 불용어를 추가하는 방식은 하지 마라.
이름이 달라야 한다면 의미도 달라져야 한다.
읽는 사람이 차이를 알도록 이름을 지어라. (customerInfo, customer 클래스가 있다면 이 둘의 차이점을 알 수 있겠나?)
알고리즘 공부를 하면서 네이밍에 신경을 쓰지 않고 obj, nameString 이런 식으로 작성하곤 했는데 이걸 보고도 뼈를 맞았다.
발음하기 쉬운 이름을 사용하라
토론하기도 편하고, 발달한 두뇌를 활용해야 하니까~
검색하기 쉬운 이름을 사용하라
문자 하나를 사용하는 이름과 상수는 텍스트 코드에서 쉽게 눈에 띄지 않는다. 이름 길이는 범위 크기에 비례해야 한다.
간단한 메소드에서의 로컬 변수는 한 문자를 사용 하지만, 여러 곳에서 사용하는 상수나 변수는 검색하기 편한 이름이 바람직하다.
리눅스 서버로 배포를 하고 시스템에 문제가 생길 경우 grep 명령어로 로그를 뒤지거나 문제가 되는 소스코드를 찾는 일이 발생한다. (실제로 굉장히 자주 경험했다.)
인코딩을 피하라
개발자는 문제 해결에 집중하기 바쁜데 인코딩은 불필요한 정신적 부담이다.
-
헝가리식 표기법 : 이제는 없어져야 할 구시대의 유물.
-
멤버 변수에 m_이라는 접두어를 붙일 필요가 없다.
-
인터페이스 클래스와 구현 클래스 : 인터페이스 클래스 이름과 구현 클래스 이름 중 하나를 인코딩해야 한다면 구현 클래스 이름을 택하겠다.
자신의 기억력을 자랑하지 마라
독자가 코드를 읽으면서 변수 이름을 자신이 아는 이름으로 변환해야 한다면 그 변수 이름은 바람직하지 못하다. (몇 줄 더 읽다가 이 변수가 뭐더라..? 아 카운트하는 인덱스용이지! 하면 안 된다.) 전통적으로 루프에서 반복 횟수를 세는 i, j, k는 괜찮다. 그 외 대부분에는 적절하지 않다. 전문가 프로그래머는 명료함이 최고라는 사실을 이해한다.
클래스 이름 : 클래스 이름과 객체 이름은 명사나 명사구가 적합하다.
메소드 이름 : 메소드 이름은 동사나 동사구가 적합하다. 접근자, 변경자, 조건자는 값 앞에 get, set, is를 붙인다. 생성자를 오버로딩(중복정의)할 때는 정적 팩토리 메소드를 사용하는 것이 좋다.
기발한 이름은 피하라 : 특정 문화에서만 사용하는 농담은 피하자. 의도를 분명하고 솔직하게 표현하자.
한 개념에 한 단어를 사용하라
똑같은 메소드를 가져오다는 의미를 갖는 fetch, retrieve, take, get으로 제각각 부르면 혼란스럽다. 일관성 있는 어휘를 사용하자.
메소드의 이름은 독자적이고 일관적이어야 한다. 그래야 주석을 뒤져보지 않고도 프로그래머가 올바른 메소드를 선택한다.
말장난을 하지 마라
한 단어를 두 가지 목적으로 사용하지 마라. 같은 맥락이 아닌데도 일관성을 고려해 동일한 단어를 선택하면 안 된다. 오히려 더 헷갈린다.
해법 영역에서 가져온 이름을 사용하라
코드를 읽는 사람도 프로그래머이다. 전산용어, 알고리즘명, 패턴명, 수학용어를 사용해도 된다. 기술 개념에는 기술 이름이 가장 적합한 선택이다.
문제 영역에서 가져온 이름을 사용하라
문제 영역 개념과 관련이 깊은 코드라면 문제 영역에서 이름을 가져와야 한다. 특정 도메인 지식이 필요하고 전문적인 용어가 별도로 있다면 그 용어를 사용하자.
의미 있는 맥락을 추가하라
멤버 변수만으로 의미를 파악하기 어렵다면 맥락을 개선하자. 맥락을 개선하면 함수를 분해하기 쉬워지고 알고리즘도 명확해진다.
불필요한 맥락을 없애라
의미가 분명할 경우 일반적으로 짧은 이름이 긴 이름보다 좋다.
3장. 함수는 어떻게 만들어야 하는가
함수는 작게 만들어라
-
어떤 프로그램이든 가장 기본적인 단위가 함수다.
-
if문 else문 while문에 들어가는 블록은 한 줄이어야 한다. 대게 여기서 함수를 호출한다. 바깥을 감싸는 함수가 작아질 뿐 아니라 블록 안에서 호출하는 함수 이름을 적절히 짓는다면 코드를 이해하기도 쉬워진다.
-
중첩 구조가 생길 만큼 커져선 안된다. 함수에서 들여 쓰기 수준은 1단~2단을 넘어서지 않게 하자.
함수는 한 가지만 하도록 만들어라
함수는 한 가지를 해야 한다. 그 한 가지를 잘해야 한다. 그 한 가지만을 해야 한다.
함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서이다.
함수가 한 가지만 하는지 판단하는 방법 : 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 것이다.
함수당 추상화 수준은 하나로!
함수가 한 가지 작업만 수행하도록 짜려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다. 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 특정 표현이 개념적인 내용인지, 세부적인 사항인지 알기 어렵다.
위에서 아래로 코드 읽기 : 내려가기 규칙 > 코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
switch문은 작게 만들기 어렵다. 케이스 유형이 추가될수록 길어진다. 한 가지 작업만 수행하지 않는다. SRP, OCP도 위반한다.
서술적인 이름을 사용하라 : 한 가지만 수행하는 작은 함수에 좋은 이름을 붙이자. 함수가 작고 단순할수록 서술적인 이름을 고르기도 쉽다. 서술적인 이름을 사용하면 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워지는 선순환이 반복된다.
함수 인수 : 이상적인 인수 개수는 0개(무항)이다. 인수는 개념을 이해하기 어렵게 만들기 때문이다. 최선은 입력 인수가 없는 것, 차선은 1개인 것이다.
- 플래그 인수는 극혐이라고 표현하고 있다. 함수로 부울 값을 인자로 넘기는 관례는 끔찍하다. 하지 말자.
- 인자가 많아지는 경우는 생기기 마련이다. 하지만 위험이 따른다는 사실을 잊지 말자.
- 함수와 인수는 동사와 명사로 쌍을 이뤄야 한다. 좋은 함수 이름은 역시나 필수다!
부수 효과를 일으키지 마라
함수에서 한 가지를 하겠다고 약속하고선 다른 것도 하는 것이다. 시간적인 결합이나 순서 종속성을 초래한다. 예상치 못한 수정을 발생시키니 조심하자.
일반적으로 출력 인수는 피해야 한다. 객체 지향 언어에서는 출력 인수를 사용할 필요가 거의 없다. 출력 인수로 쓰도록 설계한 변수가 this이다. 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.
명령과 조회를 분리하라 > 둘 다 하면 혼란을 초래한다.
오류 코드보다 예외를 사용하라
명령 함수에서 오류 코드를 반환하는 방식은 명령과 조회를 분리하는 규칙을 어긴다. 오류 코드를 반환하면 여러 단계로 중첩되는 코드가 야기된다.(그런 소스를 많이 봤다. 충격. 궁예인줄). 예외를 사용하면 오류 처리 코드가 분리되어 코드가 깔끔해진다.
- try catch 블록 뽑아내기. 이 블록은 정상 동작과 오류처리 동작을 섞어 더럽게 만든다. 별도 함수로 뽑아내자.
- 오류 처리도 한 가지의 작업이다. 오류만 처리하도록 작성하자.
- 오류 코드를 반환한다는 것은 어디선가 오류 코드를 정의한다는 것이다. 예외를 사용하면 새 예외는 Exception클래스를 상속받아 처리한다. 재컴파일/재배치 없이도 새 예외 클래스를 추가할 수 있다.
반복하지 마라
중복은 모든 악의 근원이다. 코드도 늘어나고 수정할게 생기면 각 각 다 찾아서 손봐야 한다.
중복을 제거할 목적으로 나온 다양한 원칙과 기법
- 관계형 데이터 베이스에서의 정규형을 통해 자료의 중복을 제거했다.
- 객체 지향 프로그래밍은 중복되는 코드를 부모 클래스로 몰아 중복을 제거했다.
- 구조적 프로그래밍, AOP, COP도 중복 제거 전략이다.
구조적 프로그래밍
구조적 프로그래밍의 목표와 규율(=모든 함수와 함수 내 모든 블록에 입구와 출구는 하나만 존재해야 한다.)은 함수가 작다면 별 이익을 제공하지 못한다. 함수가 작다면 return, break, continue를 여러 차례 사용해도 괜찮다.
함수를 어떻게 짜죠?
글짓기와 같다. 생각을 기록하고 읽기 좋게 다듬어라. 처음에는 이름은 즉흥적이고 중복되는 코드도 많고 들여 쓰기 단계도 깊다.
하지만 단위 테스트 케이스를 만들고 다듬고 중복을 없애고 순서를 바꾸고 함수를 만든다. 이때, 항상 단위 테스트를 통과해야 한다. 처음부터 가능한 사람은 없다. 수정에 수정을 거듭해서 깨끗한 코드로 만드는 것이다!
결론
길이가 짧고 이름이 명확하고 체계가 잡힌 함수를 만들자. 그리고 진짜 목표는 시스템이라는 것을 잊지 말자. 장의 첫 부분에서 보여준 해독하기 난해한 코드를 보여준다. 그리고 결론 부분에서 명확하게 짜인 클린 코드를 제공한다. 멋있다. 책을 구매하는 것을 적극 추천.. 또 추천...
4장. 주석은 항상 좋은 것이 아니다.
잘 달린 주석은 그 어떤 정보보다 유용하다. 경솔하고 근거 없는 주석은 코드를 이해하기 어렵게 만든다. 코드를 깔끔하게 정리하고 표현력을 강화해서 처음부터 주석이 필요 없는 방향이 되도록 노력하자. 부정확한 주석은 아예 없는 주석보다 훨씬 더 나쁘다.
주석은 나쁜 코드를 보완하지 못한다.
코드에 주석을 추가하는 일반적 이유는 코드 품질이 나쁘기 때문이다.
클린 코드에 가까운 것? 표현력이 풍부하고 깔끔하며 주석이 거의 없는 코드 > 복잡하고 어수선하며 주석이 많이 달린 코드
코드로 의도를 표현하라
단순 조건문을 나열하고 이 조건문이 존재하는 이유에 대해서 주석을 작성한 코드가 있다. 하지만 좋은 코드는 함수로 이를 묶어내고 명확한 함수 이름으로 표현한 것이다. 코드를 통해서 이 조건문이 왜 들어간 것인지 의도를 표현했다.
좋은 주석과 나쁜 주석
- 좋은 주석
-
정말로 좋은 주석은 주석을 달지 않을 방법을 찾아낸 주석이다.
-
법적인 주석 : 각 소스 파일 첫머리에 주석으로 들어가는 저작권, 소유권 정보는 필요하고 타당하다.
-
정보를 제공하는 주석 : 기본적인 정보를 주석으로 제공하면 편리하다. 하지만, 명확한 함수명으로 코드를 작성한다면 주석이 필요 없어진다.
-
의도를 설명하는 주석 : 주석을 통해 결정에 깔린 의도까지 설명하는 경우이다.
-
의미를 명료하게 밝히는 주석 : 모호한 인수나 반환 값이 존재하고, 이 코드를 수정할 수 없다면 의미를 명료하게 표현해주는 주석이 유용하다.
-
결과를 경고하는 주석 : 특정 테스트 케이스를 꺼야 하는 이유, 스레드 안전성에 대한 경고 등을 기재한 주석은 프로그래머의 실수를 줄여준다.
-
TODO 주석 : 떡칠해놓은 것은 안 좋지만 해야 할 일을 정리해놓는 정도로는 유용하다.
-
중요성을 강조하는 주석 : 놓치고 넘어갈 수 있는 부분에 대한 강조는 옳다.
-
공개 API에서 JavaDocs : 설명이 잘 된 공개 api는 유용하고 만족스럽다. 공개 api를 구현한다면 반드시 훌륭한 javadocs를 작성하자.
- 나쁜 주석 (대다수의 주석이 여기에 속한다.)
-
주절거리는 주석 : 마지못해 억지로 작성한 주석이다. 이해가 안돼서 다른 모듈까지 뒤져야 하는 주석은 바이트만 낭비한다.
-
같은 이야기를 중복하는 주석 : 주석이 코드보다 더 많은 정보를 제공하지 못하고 의도나 근거도 설명하지도 않는데 의미도 부정확한 주석이다.
-
오해할 여지가 있는 주석 : 주석에 담긴 잘못된 정보로 인해 다른 사람들이 오랜 시간 동안 고통받을 수 있다.
-
의무적으로 다는 주석 : 모든 함수에 javadocs를 담거나 모든 변수에 주석을 달아야 한다는 규칙은 어리석다. 오히려 코드가 헷갈리고 잘못된 정보를 제공할 수 있다.
-
이력을 기록하는 주석 : 일종의 일지 혹은 로그처럼 기록한 주석이다. 예전에는 바람직한 관례였지만 이젠 형상관리시스템이 있기에 제거하는 편이 좋다.
-
있으나 마나 한 주석 : 너무나 당연한 사실을 언급하며 새로운 정보를 제공하지 못하는 주석이다. 이거 적을 바엔 코드를 정리하는 데 힘쓰자.
-
무서운 잡음 : 때로는 javadocs도 잡음이다. 문서를 제공해야 한다는 잘못된 욕심으로 잡음을 만들지 말자.
-
함수나 변수로 표현할 수 있다면 주석을 달지 마라 : 함수와 변수로 표현이 된다면 주석을 없애고 코드를 개선하자.
-
위치를 표시하는 주석 : 일반적으로 가독성을 낮춘다. 반드시 필요할 때만 아주 드물게 사용하는 편이 좋다. ( '//////////////////////////////' 로 구분해놓은 주석)
-
닫는 괄호에 다는 주석 : 중첩이 심하고 장황한 함수라면 의미가 있을 수 있다. 하지만 작고 캡슐화된 함수에는 잡음이다. while{ //샬랴샬라 }//while-end
-
공로를 돌리거나 저자를 표시하는 주석 : 소스 관리 시스템은 누가 만든 건지 다 남으니 코드를 오염하지 말자.
-
주석으로 처리한 코드 : 밉살스러운 관행이다.(굉장히 자주 봤다.ㅎㅎ) 1960년대에 유용했다. 소스 코드 관리 시스템이 있다. 삭제하자.
-
HTML 주석 : 혐오 그 자체. 읽기 힘들다. 에디터에서도 읽기 힘들다.
-
전역 정보 : 주석을 달아야 한다면 근처에 있는 코드만 기술하자. 코드 일부에 주석을 달면서 시스템 전반의 정보를 적지 마라. (환경설정 변수로 프로퍼티 파일로 관리하면서 그 값을 가져다 쓰는 모듈에서 주석으로 기재하는 것은 어리석은 행동이다.)
-
너무 많은 정보 : 주석에다가 흥미로운 역사나 관련 없는 정보를 장황하게 쓰지 마라.
-
모호한 관계 : 주석과 주석이 설명하는 코드는 둘 사이의 관계가 명백해야 한다.
-
함수 헤더 : 짧은 함수는 긴 설명이 필요 없다. 이름을 잘 붙인 함수는 주석으로 헤더를 붙인 함수보다 훨씬 좋다.
-
비공개 코드에서 Javadocs : 공개 api도 아닌데 굳이 쓸 필요 없다. 산만해질 뿐이다.
- 마지막으로 주석이 열댓 개에서 단 두 개로 줄인 예제를 보여준다. 난 매번 다시 볼 경우를 생각해서 주석을 왕창 달아두며 공부하곤 했다. 등짝을 후들겨 맞는 기분이었다. 앞으로는 주석 없이도 명확하게 이해가 되도록 코드를 작성하는 연습을 해야겠다.
5장. 형식 맞추기
프로그래머라면 형식을 깔끔하게 맞춰 코드를 짜야한다. 형식을 맞추기 위한 규칙 정하기 > 규칙 지키기 > 필요하다면 규칙을 자동으로 적용하는 도구 활용
형식을 맞추는 목적
코드 형식은 중요하다. 융통성 없이 맹목적으로 따르면 안 된다. 의사 소통의 일환이다.
기능 구현만이 1차적인 의무라 생각하지 말자. 맨 처음 잡아놓은 구현 스타일과 가독성 수준은 유지보수 용이성과 확장성에 계속 영향을 미친다. 원래 코드는 사라지더라도 개발자의 스타일과 규율은 사라지지 않는다.
적절한 행 길이를 유지하라
각 클래스 파일들이 500줄을 넘지 않고 대부분 200줄 정도인 파일로도 커다란 시스템을 구축할 수 있다. 일반적으로 큰 파일보다 작은 파일이 이해하기 쉽다.
신문 기사처럼 작성하라
소스 코드도 신문기사처럼 이름은 간단하면서도 설명이 가능하게 짓는다. 이름만 보고도 올바른 모듈을 살펴보고 있는지를 판단할 정도로 신경써서 짓는다. 소스 파일 첫 부분은 고차원 개념과 알고리즘을 설명한다. 아래로 내려갈수록 의도를 세세하게 묘사한다. 마지막에는 가장 저차원 함수와 세부 내역이 나온다.
신문은 다양한 기사, 대부분 짧은 기사로 나온다. 신문이 날짜, 이름 등을 무작위로 뒤 섞은 긴 기사 하나만 싣는다면 아무도 읽지않을 것이라. 코드도 마찬가지로 짧고 명확하게 작성하자.
개념은 빈 행으로 분리하라
거의 모든 코드는 왼쪽에서 오른쪽으로 그리고 위에서 아래로 읽힌다. 각 행은 수식이나 절을 나타내고 일련의 행 묶음은 완결된 생각하나를 표현한다.
빈 행은 새로운 개념을 시작한다는 시각적 단서다. (= 문단마다 다른 주제가 나올 때 분리되는 것과 같은 개념.) 빈 행을 제거하면 코드 가독성이 현저하게 떨어진다.
세로 밀집도
줄바꿈이 개념을 분리한다면 세로 밀집도는 연관성을 의미한다. 서로 밀접한 코드 행은 세로로 가까이 놓여야 한다. (밀접한 개념인데 굳이 주석이나 개행을 통해 분리시키지 말자)
수직 거리
시스템이 무엇을 하는지 이해하고 싶은데, 이 조각 저 조각이 어디에 있는지 찾느라 시간과 노력을 허비한다. 서로 밀접한 개념은 세로로 가까이 둔다. 하지만 두 개념이 서로 다른 파일에 속한다면 규칙이 통하지 않는다. 타당한 근거가 없다면 밀접한 개념은 한 파일에 속해야 마땅하다.(protected 변수를 피해야 하는 이유) 같은 파일에 속할 정도로 밀접한 두 개념은 세로 거리로 연관성을 표현한다. 연관성이란 한 개념을 이해하는데 다른 개념이 중요한 정도다.
-
변수 선언 : 변수는 사용하는 위치에 최대한 가까이 선언한다. 지역 변수는 각 함수 맨 처음에 선언한다. 다소 긴 함수에서 블록 상단이나 루프 직전에 변수를 선언하는 경우도 있다.
-
인스턴스 변수 : 클래스 맨 처음에 선언한다. 변수간에 세로로 거리를 두지 않는다. 잘 설계한 클래스는 많은 클래스 메소드가 인스턴스 변수를 사용하기 때문이다. > 위치에 대한 논쟁이 분분하다. c++에서는 클래스 마지막에 선언한다는 가위 규칙이 존재한다. 자바에서는 대부분 맨 처음에 인스턴스 변수를 선언한다.
-
종속함수 : 한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다. 가능하다면 호출하는 함수는 호출되는 함수보다 먼저 배치한다.(자연스럽게 읽히게). 모듈 전체의 가독성이 높아진다. 상수를 알아야 마땅한 함수에서 실제로 사용하는 함수로 상수를 넘겨주는 방법이 좋다.
-
개념적 유사성 : 비슷한 동작을 수행하는 함수. 서로가 서로를 호출하는 관계는 부차적인 요인. 중속적인 관계가 없더라도 가까이 배치하자.
-
세로순서 : 호출되는 함수보다 호출하는 함수를 먼저 배치하자. 소스 모듈이 고차원에서부터 저차원으로 자연스럽게 내려간다.
가로 형식 맞추기
한 행은 가로로 얼마나 길어야 적당한가. 프로그래머는 명백하게 짧은 행을 선호한다. 100자나 120자도 나쁘진 않다. 하지만 그 이상은 솔직히 주의 부족이다.
예전에는 오른쪽으로 스크롤할 필요가 절대로 없게 코드를 짰다. 하지만 듀얼 모니터도 쓰고 와이드 모니터도 쓰고 폰트 크기를 줄여도 잘보이니까 200자도 한 화면에 들어간다. 하지만 저자는 길어도 100~120자를 추천한다.
-
가로 공백과 밀집도 : 가로로는 공백을 사용해 밀접한 개념과 느슨한 개념을 표현한다.
-
할당 연산자를 강조하기 위해 공백을 주면 할당문의 왼족 요소와 오른쪽 요소가 분명히 나뉜다.
-
함수이름과 이어지는 괄호사이에는 공백을 넣지 않는다. 함수와 인수는 밀접한 개념이기 때문이다.
-
연산자 우선순위를 나타낼 때에도 공백을 알맞게 넣어준다면 가독성이 높아진다.
-
-
가로 정렬 : 특정 구조를 강조하고자 가로 정렬 사용. 별로 유용하지 못하다. 코드가 엉뚱한 부분을 강조해 진짜 의도가 가려진다. 변수 유형보다는 변수 이름을 먼저 읽게 된다. 할당문의 경우 할당 연산자보다 피연산자에 눈이 간다. 정렬을 할 정도로 목록이 길다면 문제는 목록 길이지 정렬 부족이 아니다.(클래스를 쪼개야 한다는 거지 정렬을 해야하는게 아니다)
-
들여쓰기 : 소스파일은 윤곽도와 계층이 비슷하다. 범위로 이뤄진 계층을 표현하기 위해 코드를 들여쓴다. 들여쓰기한 파일은 구조가 한눈에 들어온다.
-
들여쓰기 무시하기 : 간단한 if문이나 짧은 while문, 짧은 함수에서 들여쓰기를 생략하고 한 행에 쓰고싶은 유혹이 생긴다. 나중에 보기힘드니까 그냥 들여쓰기로 명확하게 범위를 표현하자.
-
가짜범위 : 빈 while문, for문을 접하는 경우 세미콜론은 새행에다가 제대로 뜰여써서 넣어주자. 안그러면 골탕먹는다.
팀 규칙
팀에 속한다면 자신이 선호해야할 규칙은 팀 규칙이다. 팀에서 정한 코드컨벤션을 따르자. 팀은 한 가지 규칙에 합의해야 하고 모든 팀원은 그 규칙을 따르자. 그래야 소프트웨어가 일관적인 스타일을 보인다. 좋은 소프트웨어 시스템은 읽기 쉬운 문서로 이뤄진다. 스타일은 일관적이고 매끄러워야 한다. 한 소스 파일에서 봤던 형식이 다른 소스파일에도 쓰이리라는 신뢰감을 독자에게 줘야 한다.
6장. 객체와 자료구조
변수를 private로 정의하는 이유는 남들이 변수에 의존하지 않게 만들고 싶어서이다. 충동이든 변덕이든, 변수 타입이나 구현을 맘대로 바꾸고 싶어서이다. 어째서 수많은 프로그래머가 getter, setter 함수를 당연하게 public으로 설정해 private변수를 외부에 노출하는 이유는 ?
자료 추상화
자료를 세세하게 공개하기보다는 추상적인 개념으로 표현하는 것이 좋다. 인터페이스나 getter, setter만으로는 추상화가 이뤄지지 않는다. 개발자는 객체가 포함하는 자료를 표현할 가장 좋은 방법을 심각하게 고민해야 한다. 아무 생각없이 getter, setter를 추가하지말자.
자료/객체 비대칭
객체와 잘구조는 근본적으로 양분된다. 모든 것이 객체 지향으로 설계되기 보다는 단순한 자료 구조와 절차적인 코드가 가장 적합한 상황도 있다.
-
절차 지향 코드는 기존 자료구조를 변경하지 않으면서 새 함수를 추가하기 쉽다.
-
객체 지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.
-
절차 지향 코드는 새로운 자료구조를 추가하기 어렵다. 그러려면 모든 함수를 고쳐야한다.
-
객체 지향 코드는 새로운 함수를 추가하기 어렵다. 그러려면 모든 클래스를 고쳐야한다.
디미터 법칙 : 휴리스틱. 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙.
"객체안에서 메소드 호출은 자신의 메소드나 로컬 생성 객체(파라메터로 받은 객체 포함)의 메소드만 호출해야 한다."
출처: https://shiconal.tistory.com/1 [생각의 전환, 유익의 추구, 새로운 패러다임]
객체는 자료를 숨기고 함수를 공개한다. 객체는 조회 함수로 내부 구조를 공개하면 안된다. <더찾아보기>
-
기차 충돌(train wreck) : 여러 객차가 한줄로 이어진 기차처럼 보인다. 조잡. 피하는 편이 좋다.
-
객체라면 내부구조를 숨기고 자료구조라면 내부구조를 노출한다.
-
자료구조는 무조건 함수 없이 공개 변수만 포함하고 객체는 비공개 변수와 공개 함수를 포함한다면 문제는 간단해진다
-
단순한 자료구조에도 조회 함수와 설정 함수를 정의하라 요구하는 프레임워크와 표준(ex. bean)이 존재한다.
-
-
잡종 구조 : 중요한 기능을 수행하는 함수도 있고, 공개 변수나 공개 조회/설정 함수도 있다.
-
새로운 함수는 물론이고 새로운 자료구조도 추가하기 어렵다.
-
양쪽의 단점을 모아둔 구조. 되도록 피하자.
-
프로그래머가 함수나 타입을 보호할지 공개할지 확신하지 못해 어중간하게 설계한 것이다.
-
-
구조체 감추기 : 내부구조를 드러내지 않으며, 모듈에서 해당 함수는 자신이 몰라야 하는 여러 객체를 탐색할 필요가 없다.
자료 전달 객체
-
자료 전달 객체(DTO, Data Transfer Object) : 자료 구조체의 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스다.
-
데이터베이스와 통신하거나 소켓에서 받은 메세지 구문을 분석할 때 유용한 객체다.
-
가공되지 않은 DB정보를 객체로 변환시 가장 처음으로 사용하는 구조체.
-
-
일반적인 형태 bean 구조 : 빈은 private 변수를 조회/설정 함수로 조작한다. 일종의 사이비 캡슐화. 별다른 이익을 제공하지 않는다.
-
활성 레코드 : DTO의 특수한 형태다. 공개 변수가 있거나 비공개 변수에 조회/설정 함수가 있는 자료구조다.
-
대게 탐색함수(save, find)도 제공한다. 활성 레코드는 자료구조로 취급한다.
-
비즈니스 규칙을 담으면서 내부 자료를 숨기는 객체는 따로 생성한다. 내부자료는 활성 레코드의 인스턴스일 가능성이 높다.
-
결론
객체는 동작을 공개하고 자료를 숨긴다.
-> 기존 동작을 변경하지 않으면 새 객체 타입을 추가하기 쉽다. 기존 객체에 새동작을 추가하기는 어렵다.
자료구조는 별다른 동작없이 자료를 노출한다.
-> 기존 자료구조에 새 동작을 추가하기는 쉽다. 기존 함수에 새 자료구조를 추가하기는 어렵다.
직면한 문제에 최적인 해결책을 선택하자.
-
어떤 시스템을 구현할 때, 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합하다.
-
다른 경우로 새로운 동작을 추가하는 유연성이 필요하면 자료구조와 절차적인 코드가 더 적합하다.
7장. 오류 처리
오류 코드보다 예외를 사용하라
오류가 발생하면 예외를 던지면 호출자 코드가 더 깔끔해진다. 논리가 오류 처리 코드와 뒤섞이지 않는다.
Try-Catch-Finally 문 부터 작성하라
try블록에서 무슨일이 생기든지 catch블록은 프로그램 상태를 일관성 있게 유지해야 한다. 예외가 발생할 코드를 짤때에는 try-catch-finally로 시작하는 것이 낫다. try-catch구조로 범위를 정의했으므로 tdd를 사용해 필요한 나머지 논리를 추가한다.
먼저 강제로 예외를 일으키는 테스트 케이스를 작성한 후 테스트를 통과하게 코드를 작성하는 방법을 권장한다
-
try 블록의 트랜잭션 범위부터 구현하게 되므로 범위 내에서 트랜잭션 본질으 유지하기 쉬워진다.
미확인(unchecked) 예외를 사용하라
확인된 예외는 OCP를 위반한다. 메소드에서 확인된 예외를 던졌는데 catch블록이 세 단계 위에 있다면 그 사이 메소드 모두 선언부에 해당 예외를 정의해야한다. 전부 수정... 모듈과 관련된 코드가 전혀 바뀌지 않았더라도 선언부가 바뀌었으므로 모듈을 다시 빌드한 다음 배포해야 한다. (연쇄적인 수정..)
예외에 의미를 제공하라
예외를 던질 때는 전후 상황을 충분히 덧붙인다. 오류 발생 원인과 위치를 찾기 쉬워진다. 자바는 모든 예외에 호출 스택을 제공한다. 하지만 실패한 코드의 의도를 파악하려면 호출 스택만으로 부족하다. 오류 메세지에 정보(실패한 메소드명, 실패 유형 등)를 담아 예외와 함께 던진다. 애플리케이션이 로깅 기능을 사용한다면 충분한 정보를 남겨주자.
호출자를 고려해 클래스를 정의하라
오류를 분류하는 방법은 수없이 많다. 오류가 발생한 위치/오류가 발생한 컴포넌트/유형으로 분류가 가능하다.
애플리케이션에서 오류를 정의할 때 가장 중요한 기준은 오류를 잡아내는 방법이 되어야한다.
외부 API를 사용할 때는 wrapper class가 최선이다.
정상 흐름을 정의하라
특수 사례 패턴(special case pattern) : 클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식이다. 클래스나 객체가 예외적인 상황을 캡슐화해서 처리하므로 예외적인 상황을 처리할 필요가 없어진다.
null을 반환하지 마라 (오류를 유발하는 행위)
호출자에게 문제를 떠넘기는 것에 해당한다. null을 반환하고 싶은 유혹이 든다면 예외를 던지거나 특수 사례 객체를 반환하자.
null을 전달하지 마라 (인자로 null을 넣지않도록 주의하자)
대다수의 프로그래밍 언어는 호출자가 실수로 넘기는 null을 적절히 처리하는 방법이 없다. 그러므로 애초에 null을 넘기지 못하도록 금지하는 정책이 합리적이다. 이런 정책을 따르면 실수를 저지를 확률도 작아진다.
결론
깨끗한 코드는 읽기도 좋아야 하지만 안정성도 높아야한다. 둘은 상충하지 않는다. 오류 처리를 프로그램 논리와 분리하면 튼튼하고 깨끗한 코드를 만들 수 있다. 독립적인 추론이 가능해지고 코드 유지보수성도 크게 높아진다.
8장. 경계
패키지를 구매하거나 오픈소스를 이용하는 것처럼 외부 코드를 사용하는 경우가 많다. 이 소프트웨어 경계를 깔끔하게 처리하는 기법과 스킬에 대해 알아보자.
외부 코드 사용하기
Map 클래스를 사용할 때마다 캡슐화하는 것이 아니라 Map(혹은 유사한 경계 인터페이스)를 여기저기 넘지기 말자. 경계 인터페이스를 이용할때에는 이를 이용하는 클래스나 클래스 계열 밖으로 노출되지 않도록 주의하자. Map 인스턴스를 공개api의 인수로 넘기거나 반환값으로 사용하지 않는다.
경계 살피고 익히기
짐 뉴커크 > 학습테스트 : 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히자. 통제된 환경에서 API를 제대로 이해하는 지를 확인하는 것이다. 학습테스트는 API를 사용하여는 목적에 초점을 맞춘다.
-
예시 log4j 익히기
-
테스트 케이스 작성을 통해 간단한 콘솔 로거를 초기화하는 방법을 익힌다.
-
이후, 독자적인 로거 클래스로 캡슐화한다.
-
나머지 프로그램은 log4j 경계 인터페이스를 몰라도 된다.
-
학습 테스트는 공짜 이상이다
필요한 지식만 확보하는 손쉬운 방법이다. 학습 테스트는 이해도를 높여주는 정확한 실험이다. 패키지가 예상대로 도는지 검증한다.
패키지 새버전이 나올때마다 우리 코드와 호환되지 않으면 학습 테스트가 이 사실을 바로 밝혀낸다. 이런 경계 테스트가 있다면 새 버전으로 이전이 쉬워진다.
아직 존재하지 않는 코드를 사용하기
우리가 바라는 인터페이스를 구현하면 우리가 인터페이스를 전적으로 통제한다는 장점이 생긴다. (코드 가독성, 코드 의도도 분명해진다.)
ADAPTER 패턴으로 API사용을 캡슐화해 API가 바뀔 때 수정할 코드를 한 곳으로 모은다. 이와 같은 설계는 테스트도 아주 편하다.
깨끗한 경계
소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 불필요하다. 경계에 위치하는 코드는 깔끔하게 분리한다. 기대치를 정의하는 테스트 케이스도 작성한다. 이쪽 코드에서 외부 패키지를 세세하게 알 필요도 없다. 외부 패키지에 의존하지 말고 통제 가능한 우리코드에 의존하자.
새로운 클래스로 경계를 감싸거나 ADAPTER패턴 을 사용하자. 코드 가독성이 높아지고, 경계 인터페이스를 사용하는 일관성도 높아지며, 외부 패키지가 변했을 때 변경할 코드도 줄어든다.
ADAPTER패턴 관련 포스팅 https://niceman.tistory.com/141
9장. 단위 테스트
TDD. Test Driven Development. 테스트 주도 개발
애자일과 TDD 덕택에 단위 테스트를 자동화하는 프로그래머들이 많아졌고 점점 더 늘어나는 추세다. 우리 분야에 테스트를 추가하려고 급하게 서두르는 와중에 많은 프로그래머들이 제대로 된 테스트 케이스를 작성해야 한다는 중요한 사실을 놓쳐버렸다.
TDD 세 가지 법칙
이 세 가지 규칙을 따르면 개발과 테스트가 대략 30초 주기로 묶인다. 이렇게 일하면 엄청난 양의 테스트 케이스와 방대한 테스트 코드가 나온다.
-
실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
-
컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
-
현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.
깨끗한 테스트 코드 유지하기
일회용 테스트 코드를 짜오다가 새삼스레 자동화된 단위 테스트 슈트를 짜기란 쉽지 않다. 테스트를 안하느니 지저분한 테스트 코드라도 있는 편이 좋다고 판단할 수 있다.
테스트 케이스가 실패 > 지저분한 코드로 인해 실패하는 케이스를 점점 더 통과하기 어려움 > 테스트 코드 증가 > 테스트 코드 관련 유지보수 비용 증가 > 테스트 슈트를 폐기하자는 결론 > 테스트 슈트가 없으면 개발자는 자신이 수정한 코드가 제대로 도는지 확인할 방법이 없다 > 결함율이 높아지기 시작한다. > 의도하지 않은 결함 수가 많아지면 개발자는 변경을 주저한다. > 결국 파국이다. 테스트 슈트 없음 / 뒤죽박죽 코드 / 좌절한 고객 / 테스트와 관련된 노력의 실망감
테스트 코드는 실제 코드 수준으로 중요하다. 실제 코드 처럼 깨끗하게 짜야 한다.
테스트는 유연성, 유지보수성, 재사용성을 제공한다.
단위테스트가 있으면 코드에 유연성, 유지보수성, 재사용성을 제공한다. 테스트 케이스가 없으면 모든 변경이 잠정적인 버그다. 아키텍처가 아무리 유연하더라도, 설계를 아무리 잘 나눴더라도, 테스트 케이스가 없으면 개발자는 변경을 주저한다. 버그가 숨어들까 두렵기 때문이다. 테스트 케이스의 존재로 인해 안심하고 아키텍처와 설계를 개선할 수 있다.
깨끗한 테스트 코드
깨끗한 테스트 코드를 만들려면 가독성이 중요하다. 테스트 코드는 최소의 표현으로 많은 것을 나타내야 한다.
Build-operate-check 패턴
도메인에 특화된 테스트 언어
API 위에다 함수와 유틸리티를 구현한 후 그 함수와 유틸리티를 사용하므로 테스트 코드를 짜기도 읽기도 쉬워진다. 이렇게 구현한 함수와 유틸리티는 테스트 코드에서 사용하는 특수 API가 된다. > 처음부터 설계된 API가 아니라 리팩토링을 통해 간결하고 표현력이 풍부한 코드로 진화된 것이다.
이중 표준
테스트 API 코드에 적용하는 표준은 실제 코드에 적용하는 표준과 다르다.
-
단순, 간결, 표현력이 풍부해야 하지만 효율적일 필요는 없다.
-
실제환경과 테스트 환경은 요구사항이 다르기 때문이다.
-
그릇된 정보를 피해야하지만 테스트의 의미를 빠르게 전달/판단하기 위해 함축적 용어를 사용하기도 한다.
-
실제 환경에서는 메모리나 CPU 효율을 고려해야 하지만 테스트 코드의 깨끗함과는 상관이 없다.
테스트 당 assert 하나
JUnit으로 테스트 코드를 짤 때는 함수마다 assert문을 단 하나만 사용해야한다. 결론이 하나라서 코드를 이해하기 쉽고 빠르다. 하지만 테스트를 전부 분리하다보면 중복되는 코드가 많아진다. TEMPLATE METHOD 패턴을 사용하면 중복을 제거할 수 있다. 테스트 케이스 안에 assert문은 최대한 줄이도록 노력하자.
테스트 당 개념 하나
테스트 함수마다 한 개념만 테스트하라. 여러 개념을 한 함수로 몰아넣으면 읽는 사람은 모든 개념을 이해해야 한다. (쉽고 빠르게 이해하지 못함)
FIRST : 깨끗한 테스트가 따르는 5가지 규칙
-
Fast : 테스트는 빨라야 한다.
-
Independent : 각 테스트는 서로 의존하면 안 된다. 각 테스트는 독립적으로 그리고 어떤 순서로 실행해도 괜찮아야 한다.
-
Repeatable : 테스는 어떤 환경에서도 반복 가능해야 한다.
-
Self-Validating : 테스는 부울 값으로 결과를 내야한다. 성공 아니면 실패.
-
Timely : 테스트는 적시에 작성해야 한다.
결론
테스트는 실제 코드만큼 굉장히 중요한 주제다. 테스트 API를 구현해 도메인 특화 언어(DSL)를 만들자. 그러면 그만큼 테스트 코드를 짜기 쉬워진다. 테스트 코드를 깨끗하게 유지하자.
10장. 클래스
클래스 체계
클래스를 정의하는 표준 자바 관례는 아래와 같다.
1. static public 상수
2. static private 변수
3. private 인스턴스 변수
4. 변수를 모두 나열 한후 공개 함수
5. 비공개 함수는 자신을 호출하는 공개 함수 직후에 넣는다. (추상화 단계가 순차적으로 내려간다. 신문기사가 읽히는 것처럼.)
> 입사 초에 실무 소스을 분석 진행하면서 private 메소드와 public 메소드가 정렬되어 있다는 느낌을 받았었는데 이 또한 가독성을 높이는 방법이었구나.
캡슐화
변수와 유틸리티 함수는 가능한 공개하지 않는 편이 낫지만 반드시 숨겨야 한다는 법칙도 없다. 같은 패키지안에서 테스트 코드가 함수를 호출하거나 변수를 사용해야 한다면 protected로 선언하거나 패키지 전체로 공개한다(= 최후의 수단). 하지만 그 전에 private 상태로 유지할 방법을 강구하자.
클래스는 작아야 한다.
함수와 마찬가지로 클래스도 작게가 기본 규칙이다. 클래스는 다른 척도로 책임을 센다. 클래스 이름은 해당 클래스 책임을 기술해야 한다. 실제로 작명은 클래스 크기를 줄이는 첫 번째 관문이다. 클래스 이름에 Processor, Manager, Super와 같이 뭉쳐진 개념이 단어가 있다면 여러 책임을 줬다는 증거다. ㄷ
클래스 설명은 if / and / or / but 을 사용하지 않고 25 단어 내외로 가능해야 한다.
단일 책임 원칙(SRP. Single Responsibility Principle)
클래스나 모듈을 변경할 이유가 단 하나 뿐이어야 한다는 원칙이다. 책임, 즉 변경할 이유를 파악하려 애쓰다 보면 코드를 추상화하기 쉬워진다. SRP는 객체 지향 설계에서 더욱 중요한 개념이다. 이해하고 지키기 수월한 개념이기도 하다.
작은 클래스가 많은 시스템이든, 큰 클래스가 몇 개뿐인 시스템이든 총 돌아가는 부품은 그 수가 비슷하다.
= 도구 상자를 어떻게 관리하고 싶은가? 작은 서랍을 많이 두고 기능과 이름이 명확한 컴포넌트를 나눠 넣고 싶은가? 아니면 큰 서랍 몇 개를 두고 모두를 던져 넣고 싶은가?
규모가 큰 시스템은 논리가 많고 복잡하다. 이를 다루려면 체계적인 정리가 필수적이다. 유지보수 할 때에도 직접 영향이 미치는 컴포넌트만 이해해도 충분히 가능하다.
응집도
클래스는 인스턴스 변수 수가 작아야 한다. 각 클래스 메서드는 클래스 인스턴스 변수를 하나 이상 사용해야 한다. 일반적으로 메소드가 변수를 더 많이 사용할수록 메서드와 클래스는 응집도가 더 높다. 모든 인스턴스 변수를 메소드마다 사용하는 클래스는 응집도가 가장 높다.
응집도가 높다 = 클래스에 속한 변수와 메소드가 서로 의존하며 논리적인 단위로 묶인다.
-
함수를 작게, 매개변수 목록을 짧게를 유지하다보면 몇몇 메서드만이 사용하는 인스턴스 변수가 아주 많아진다(새로운 클래스로 쪼개라는 신호)
-
응집도를 유지하면 작은 클래스 여럿이 나온다.
-
큰 함수를 작은 함수로 쪼개다 보면 작은 클래스 여럿으로 쪼갤 기회가 생긴다. 이를 통해 프로그램에 체계가 잡히고 구조가 투명해진다. (Literate Programming 책 예시)
-
원래 프로그램의 정확한 동작을 검증하는 테스트 슈트 작성
-
한 번에 하나씩 수 차례에 걸쳐 조금씩 코드 수정
-
코드를 수정할 때마다 테스트를 수행해 원래 프로그램과 동일하게 동작하는지 확인
-
정리한 결과 최종 프로그램 완성
-
변경하기 쉬운 클래스
깨끗한 시스템은 클래스를 체계적으로 정리해 변경에 수반하는 위험을 낮춘다. 새 기능을 수정하거나 기존 기능을 변경할 때 건드릴 코드가 최소인 시스템 구조가 바람직하다. 이상적인 시스템이라면 새 기능을 추가할 때 시스템을 확장할 뿐 기존 코드를 변경하지는 않는다.
변경으로부터 격리
상세한 구현에 의존하는 클라이언트 클래스는 구현이 바뀌면 위험에 빠진다. 그래서 우리는 인터페이스와 추상 클래스를 사용해 구현이 미치는 영향을 격리한다.
시스템의 결합도를 낮추면 유연성과 재사용성이 높아진다.
결합도가 낮다 = 시스템 요소가 서로 잘 격리되어 있다. (각 요소를 이해하기도 더 쉽다.)
결합도를 최소로 줄이면 DIP(Dependency Inversion Principle : 상세한 구현 클래스가 아닌 추상화에 의존해야 한다)를 따르는 클래스가 된다.
11장. 시스템
TODO : 정리 예정 입니당
이 포스팅은 '클린 코드'를 읽고 정리한 내용입니다. 자세한 내용은 책 속에 ! :)
http://www.yes24.com/Product/Goods/11681152
'CS > Book' 카테고리의 다른 글
[Book] 읽기 좋은 코드가 좋은 코드다 (0) | 2020.11.29 |
---|---|
[Book] 그림으로 배우는 IT 인프라구조 (0) | 2019.12.10 |