우테코에 참가하는 많은 사람들이 클린코드에 대해 고민하는 모습에 배울점이 많았다.
다른 사람의 질문이 곧 나의 질문이 되기도 하는 경우가 많아 미처 인지하지 못하던 내용까지도 확인할 수 있었다.
하나의 스레드가 다양한 의견이 오가는 토론의 장이 될때면 참 보기 좋다.
정말 감사하게도 좋은 내용을 공유해주시는 분들도 많았다.
좋은 정보를 공유해주신 모든 분들께 감사하다는 말씀을...🙇♂️
더 좋은 협업을 위해 클린코드를 지향해야한다는 생각이 들었다.
나 또한 누군가에게 이러한 의견을 누군가에게 근거를 뒷받침하여 말할 수 있도록 클린코드에 대해 공부해보고자 한다.
클린코드 1장, 2장, 3장을 읽고 해당 내용을 정리하고자한다.
변수명을 지을 때는 의미있는 이름을 지어야 한다.
// 의도가 불분명하다.
int a;
// 의도를 나타ㅏ내고있다.
int daysSinceCreation;
변수명이 길어질것을 우려하거나 다른 플랫폼의 예약어를 사용하지 않도록 유의해야한다.
또한 해당 변수가 나타내는 정보가 다른 의미로 해석될 것을 우려해야한다.
List는 프로그래머의 입장에서는 자료형의 의미로 해석되기 쉽다.
accountList
라는 변수가 있을 때 이 변수는 List 자료형을 가질 것 같다는 생각으로 흘러간다는 이야기다.
하지만 사실 accountList
가 List가 아닌 다른 자료형이라면 생각의 흐름을 멈추게한다.
이 경우 accountGroup
과 같은 이름이 더 어울린다.
다음 코드를 보자. 흔히 볼 수 있는 반복문이다.
public static void copyChars(char a1[], char a2[]) {
for(int i = 0; i < a1.length; i++) {
a2[i] = a1[i];
}
}
위 코드에서 함수의 인자로 받아오는 a1, a2는 이름에서 어떠한 의도도 찾아볼 수 없다.
a1
, a2
대신 source
, destination
으로 사용할 수 있겠다.
for문 내부의 i와 같은 경우는 자주 쓰이는 관례같은 느낌으로 해당 부분까지는 어느정도 허용한다.
ex) i, j, k ...
코드는 대화의 수단이다. 더 매끄러운 대화를 위해서는 발음하기 쉬운 이름을 사용해야한다.
간혹 변수명이 길어질것을 우려해 개인의 주관이 담긴 축약형 문자를 사용할 때가 있다.
한가지 예를 들어보자.
createdTime
-> crtdtm
처럼 변수명을 사용했다고 가정해보자.
코드리뷰를 하는 입장에서 crtdtm
이라는 변수를 두고 대화를 하게 된다면 뭐라고 발음해야할까? 이 변수가 createdTime 이라고 불린다는것은 작성자만이 알고 처음 보는 사람은 긴가민가하다.
씨 알 티 디 티 엠
? 큹틈
? 발음하기 어렵다...
결론은 발음하기 쉬운 이름을 사용하도록 하자.
상수와 같은 데이터는 무수히 많은 텍스트 코드에서 눈에 띄지않는다.
7이라는 상수를 사용할 때 그 목적이 드러나 있지 않다면 이 상수를 찾는데 많은 시간을 소모하게 될 것이다.
상수가 가지는 의미를 분석해 상수를 가려내야한다.
private final int week = 7;
private final int WORK_DAYS_PER_WEEK = 7;
클래스 혹은 객체의 이름은 명사나 명사구가 적합하다.
좋은 예 : Customer
, WikiPage
, Account
, AddressParser
Manager
, Processor
, Data
, Info
등과 같은 단어는 피하고 동사는 사용하지 않는다.
메소드 이름은 동사 혹은 동사구가 적합하다.
좋은 예 : postPayment
, deletePate
, save
등
접근자, 변경자, 조건자는 javabean 표준에 따라 값 앞에 get, set, is를 붙인다.
JavaBeans(TM) Specification 1.01 Final Release
추상적인 개념 하나에 단어 하나만 선택해 이를 고수해야한다.
똑같은 목적을 가진 메소드를 클래스마다 fetch, retrieve, get 등등 부르는 명칭이 제각각이면 혼란을 겪기 마련이다.
마찬가지로 controller, manager, driver를 섞어써도 문제다.
DeviceManager와 ProtocolController는 뭐가 다를까?
위 같은 경우 Manager와 Controller 둘중 하나만 고수하여 작성해야겠다.
바로 위에서 이야기한 한 개념에 하나의 단어를 사용하자는 맥락과 비슷하다.
두 값을 더하는 기능에 대해 add라고 정의했다고 하자.
새로운 List에 값을 추가하는 행위도 add라고 지칭한다고 했을 때 어떤 객체에 대해 Something.add 라고 하면 이 기능은 두 값을 더하는 기능인가? 추가하는 기능인가? 헷갈린다.
위와 같은 경우는 add보다는 append 혹은 insert가 더 적절해 보인다.
적절한 프로그래머 용어가 없다면 문제 영역에서 이름을 가져온다.
또한 문제영역과 관련이 깊은 코드의 경우에도 문제영역에서 이름을 가져와야한다.
의미가 분명한 경우에 한해 이름에 불필요한 맥락을 추가하지 않도록 주의한다.
BankAccountAddress 클래스는 하위 메소드를 구성할 때 중복된 단어가 포함될 가능성이 높다. 이 경우 Bank 클래스에 accountAddress 인스턴스 정도로 나눌 수 있겠다.
다음 문장을 뇌리에 깊이 박아두고 시작하자.
함수는 한 가지를 해야 한다. 그 한가지를 잘 해야 한다. 그 한가지만을 해야 한다.
함수가 한가지의 기능만 수행하는지 판단하기 위해서 해당 함수에서 의미있는 이름의 다른 함수를 추출할 수 있는지 확인하는 방법이 있다.
if, else, while, for문 등에 들어가는 블록은 한줄이어야한다.
즉, 하나의 들여쓰기(indent)만 허용한다.
Switch - case문은 가능하면 최대한 지양해야하는 구조다.
객체지향 원칙에 빗대어보면 Switch문은 SRP(단일책임원칙), OCP(개방-폐쇄원칙)를 위반한다.
하나의 유형이 추가, 수정 될때마다 코드를 변경해야하며 switch문과 구조가 동일한 함수가 무한정 존재한다는 문재도 있다.
switch(RoleType r) {
case "ADMIN":
// DO SOMETHING
break;
case "USER":
// DOSOMETHING
break;
...
}
위 switch문의 조건은 아래와 같이도 쓰일 수도 있다.
isAdmin(Account a, Role r);
isUser(Account a, Role r);
함수에서 이상적인 인수의 개수는 0개, 그다음은 1개, 2개다. 3개 이상은 가능한 피하는것이 좋으며 4항이상의 다항인수는 특별한 이유가 있어도 사용하면 안된다.
최선은 인수가 없는 경우며 차선을 인수가 1개뿐인 경우라는 것을 항상 인지하도록 하자.
인수와 관련하여 짚고 넘어갈 내용이 많으니 스트레칭 한번 하고 읽어보자.
함수에 인수를 1개 넘기는 이유로 가장 흔한 경우는 두가지다.
인수에 질문을 던지는 경우
boolean fileExists("myFile")
정도가 좋은 예로 볼 수 있다.
인수를 무엇인가로 변환해 결과를 반환하는 경우
InputStream fileOpen("myFile")
의 경우 String을 InputStream으로 변환한다.
void includeSetuppageInto(StringBuffer pageText)
와 같은 경우는 피해야한다.
변환함수에서는 변환값을 결과값으로 반환해줘야한다.
인수에 질문을 던지는 경우
boolean fileExists("myFile")
정도가 좋은 예로 볼 수 있다.
인수를 무엇인가로 변환해 결과를 반환하는 경우
InputStream fileOpen("myFile")
의 경우 String을 InputStream으로 변환한다.
이벤트함수
passwordAttemptFailedNtimes(int attempts)
정도가 좋은 예로 볼 수 있다.
플래그 인수는 정말 도움이 안된다.
함수가 한꺼번에 여러가지를 처리한다고 대놓고 이야기하고 있는거나 다름없다.
String doSimething(boolean flag)
는 flag의 값에 따라 true일때는 A를 수행하고 false 일때는 B를 수행한다고 이야기하고 있는거나 다름없다.
만약 인수가 많아지는 경우 일부를 독자적인 클래스 변수로 선언할 가능성을 생각해볼 수 있다.
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
결국 x, y를 묶는 과정에서도 개념을 표현해야한다는 사실은 잊지말자.
인수 개수가 가변적인 함수도 있다.
이러한 경우 가변인수를 동등하게 취급하여 논리적으로 이항함수로 나타낼 수 있다.
대표적인 예로 System.out.printf, String.format 등이 있다.
String.format(“%s worked %.2f hours.”, name, hours);
실제 String.format 선언부를 살펴보면 Object… args
로 가변인수를 나타내는 것을 확인할 수 있다.
일반적으로 출력인수는 피해야한다.
함수에서 상태를 변경해야한다면 함수가 속해있는 객체 상태를 변경하는 방식을 택하는 것이 좋다.
appendFooter(report)
라는 함수는 다소 생소하게 생겼다.
객체지향 언어에서는 출력 인수를 사용하지 말고 this를 사용해 이러한 문제를 해결한다.
따라서 객체상태를 변경하는 방식으로 report.appendFooter()
와 같이 호출하는 방식이 좋다.
함수에 부수효과가 존재한다는 것은 거짓말을하는거나 다름없다.
예를들면 유저의 password를 확인할 때 Session이 초기화된다고 가정해보자.
이 경우 함수명이 checkPassword라면 Session 초기화에 대한 명시가 되어있지 않으므로 부수효과가 일어나고있다는 것을 알 수 있다.
이 경우 checkPasswordAndInitializeSession
이라는 이름이 더 적합하다.
다음 함수를 보자.
// 이 함수는 이름이 attribute인 속성을 찾아 값을 value로 설정한 후 성공하면 true, 실패하면 false를 반환한다.
public boolean set(String attribute, String value);
if(set("username", "junho") {
...
}
위에서 말한 부수효과를 일으킬 뿐더러 함수가 동사인지 형용사인지 구분하기도 어렵다.
setAndCheckIfExists
라고 변경하는 방식도 있겠지만 if문에 넣었을 때 역시나 어색하다. check와 set 하나 이상의 기능을 가지고 있으니 다음과 같이 분리해주는 것이 더 좋아보인다.
if (attributeExists("username") {
setAttribute("username", "junho");
...
}
실제로 사용하는 사람은 본적이 없지만 오류코드를 사용하는 사람이 있을수도 있다.
간단히 예를들어 아래와 같이 상태코드로 무언가를 관리한다고 생각해보자.
if (deleteAccount(account) == CODE_OK) {
...
} else {
...
logger.log(e.getMessage());
return CODE_ERROR;
}
얼핏 봐도 문제가 많아보인다
try/catch 구문을 사용하여 오류 처리 코드가 분리되도록 구성할 수 있다.
try {
deleteAccount(account);
...
} catch (Exception e) {
logger.log(e.getMessage());
}
하지만 Try/catch 블록은 사용을 지양해야한다.
코드 구조에 혼란을 일으키고 정상동작과 오류처리 동작을 뒤섞기 때문이다.
다음과 같이 try catch 블록을 뽑아내는 방식을 생각해볼 수 있다.
private void deleteAccount(Account account) throws Exception {
deleteAccount(account);
...
}
private void logError(Exception e) {
logger.log(e.getMessage());
}
함수는 한가지 작업만 해야한다 라는 말은 오류처리에 대해서도 예외를 두지 않는다.
때문에 기능을 분리하는것이 옳다.
하지만 enum으로 Error코드를 별도로 관리하기엔 이를 다양한 곳에서 import 받고 추가, 수정하기가 번거롭다.
이에 Exception 클래스에서 파생된 기존의 오류코드를 사용할 수도 있다.
ex) IllegalStateException
내용은 없지만 유독 중요한 제목이라 느낌표를 4개나 붙였다.
중복이 일어나면 코드의 길이 뿐만 아니라 코드의 개수만큼 변경사항, 오류에 대한 대처에 배로 신경써야하기 때문에 중복은 절대로 일어나서는 안된다.
AOP, COP 등 프로그래머들은 중복 제거를 위해 많은 노력을 하고있다.
대표적인 예로 Spring을 사용하면 AOP로 Exception Handling을 Global하게 관리할 수 있다.
1~3장까지 읽고 알게된 점을 정리해보았다.
1,2장은 클린코드에 대한 전반적인 내용인 깨끗한 코드와 의미있는 이름으로 진행되었고, 3장은 함수에 대한 내용이 주가 되었다.
아무래도 함수를 깔끔하게 구성하는 것이 프로젝트 전반적으로 가장 큰 비율을 차지하기 때문에 함수에 대한 내용이 유독 많은것 같다.
책에서는 함수를 작성하는 방법을 글짓기에 비유하고있다.
자소서를 작성할때와 마찬가지로 글을 작성할 때는 생각을 기록한 후 읽기 좋게 다듬는다.
초안은 대개 서투르고 어수선하여 원하는대로 읽힐때 까지 다듬고 문장을 고치고 문단을 정리한다.
함수를 짤때도 마찬가지다.
처음에는 길고, 복잡하며 들여쓰기도 많고, 이름도 즉흥적이고, 중복 코드도 많다.
이 서툰 코드를 테스트하는 단위테스트 케이스도 만든다.
이후 코드를 다듬고, 이름을 바꾸고, 중복을 제거하는 등 함수를 정리하며 점진적으로 보기 좋은 코드가 만들어진다.
이 과정에서 만들어둔 테스트케이스는 계속해서 통과한다.
...
당장 모든 규약을 한꺼번에 지키기보다는 점진적인 성장을 기대하는 것을 기대하며 학습을 진행하면 좋을 것 같다.
처음부터 좋은 코드를 짤 수 있는 사람은 없다는 부분은 어렴풋이 알고 있었지만 테스트 케이스라는 기반을 잡아놓음으로서 코드를 개선하면서 본래의 방향성은 잃지 않는 방법이 있다는 것에 새로운 깨달음을 얻었다.
책의 목차를 살펴보면 9장에 단위테스트에 대한 내용이 있다. 뒷 내용이 기대된다.
Clean Code(클린 코드) | 로버트 C. 마틴
프엔이지만 정말 잘 읽었습니다! 유익한 포스팅 감사합니다!