"읽기 좋은 코드가 좋은 코드다" 라는 책을 통해 사내 스터디를 진행하게 되었다. 원래는 클린코드를 진행 할 예정이였으나 클린코드를 읽기전 한번 이 책을 읽어보면 좋겠다는 차장님의 추천을 받아 읽게되었다. 이 책은 "The Art Of Readable Code" 즉, 더 나은 코드를 작성하는 간단하고 실전적인 테크닉이라고 책을 소개한다. 모든 내용을 정리하고 리뷰할 수는 없으나 핵심적인 내용들만을 정리한다. 개발에 입문하였다면 이 책을 한번씩은 꼭 읽어보길 바란다(예제는 c언어와 자바스크립트로 구성되어있다) 출처: O'REILLY, 한빛미디어
코드는 이해하기 쉬워야 한다
"코드는 이해하기 쉬워야 한다" 라는 주제가 이 책의 첫번째 목차이다. 내가 짠 코드를 누군가 볼때에 이해하기 쉬워야 내 코드를 분석하는데 들이는 시간 낭비를 줄일 수 있고, 가장 중요한건 6개월 후의 내가 봤을때에 이 코드를 쉽게 알아볼 수 있는가? 라는것이 핵심적인 부분이다. 우리는 단순하게 한 줄, 두 줄의 코드가 더 좋다는 생각을 할 때가 있다. 길게 늘어질 코드를 줄일 수 있기 때문이다. 그러나 무분별 하게 코드를 줄인다면 가독성이 떨어지는 즉, 누군가 알아보기 힘들고 6개월 뒤의 내가 못알아볼 수 있는 문제가 생길수 있다.
간단한 예시
첫 번째 코드
int minAge = 10;
int maxAge = 20;
int userAge = minAge < maxAge && minNum != maxNum ? (minNum * 5) : (maxAge * 2)
두 번째 코드
int minAge = 10;
int maxAge = 20;
int userAge;
if(minAge < maxAge && minNum != maxNum){
userAge = minAge * 5;
}else{
userAge = maxAge * 2;
}
위의 두 코드들은 같은 결과를 낸다. minAge가 maxAge 보다 작으면서 둘의 값이 같지 않다면 (minNum * 5)를 반환하고 그 반대라면 (maxAge * 2) 를 반환하는 것이다. 둘의 코드를 비교해봤을때 어떤것이 더 보기좋은 코드일까? 첫 번째 코드는 분명 간결하지만, 오히려 두 번째 코드가 더 친숙하게 느껴진다. 어떤 측면이 더 중요한 것일까...?
책에서는 이러한 코드에 대한 연구를 수행한 끝에, 가독성과 관련한 가장 중요한 통계적 사실이 있음을 알게 되었고 그것을 '가독성의 기본 정리' 라고 부른다고 한다. 핵심 포인트는 코드는 다른 사람이 이해하는 데 들이는 시간을 최소화하는 방식으로 작성되어야 한다. 이다.
내가 짠 코드를 평범한 동료가 읽고 이해하는데 걸리는 시간이 '이해를 위한 시간' 이며, 우리가 최소화 해야하는 값이다. 여기서 말하는 '이해'란 매우 높은 기준을 적용하고 있다. 어떤 사람이 우리의 코드를 완전히 이해한다는 것은 그가 우리의 코드를 자유롭게 수정하고, 버그를 짚어내고, 수정된 내용이 우리가 작성한 다른 부분의 코드와 어떻게 상호작용하는지 알 수 있어야 한다는 사실을 의미한다.
그래서 결론은 무엇일까? 적은 분량으로 코드를 작성하는 것이 좋은 목표이긴 하지만, 이해를 위한 시간을 최소화 하는게 더 좋은 목표라고 한다.
이름에 정보 담기
- 특정한 단어 고르기
- 보편적인 이름 피하기
- 추상적인 이름 대신 구체적인 이름 사용하기
- 접두사 혹은 접미사로 이름에 추가적인 정보 덧붙이기
- 이름이 얼마나 길어져도 좋은지 결정하기
- 추가적인 정보를 담을 수 있게 이름 구성하기
변수의 이름은 작은 설명문이다. 모든걸 담을수는 없지만 좋은 변수명을 생각하고 담으려고 노력해야한다. 이 책의 모든 내용을설명할 수는 없고 간단히 몇가지만 예시로 들어 설명해보겠다.
보편적인 이름 피하기✔
코드를 짜다보면 흔하게 사용하는 이름들이 있다. tmp, 루프반복자(for문)의 i, j 등... 흔하게 사용하는 이 이름들을 사용하는 것은 "내 머리로는 이름을 생각해낼 수 없어요" 라고 고백하면서 책임을 회피하는 증거에 불과하다. 이렇게 무의미한 이름이 아니라, 개체의 값이나 목적을 정확하게 설명하는 이름을 골라야 한다.
물론 이 이름들을 사용하지 말라는 것은 아니다. 사용할 곳에 알맞게 의미있게 사용되어야 한다는 것이다. 한번 예시를 통해 알아보자.
간단한 예시 (tmp: Temporary(일시적인, 임시적인))
첫 번째 코드
public User createUser(String name, int age){
User tmp = new User();
tmp.setName(name);
tmp.setAge(age);
return tmp;
}
첫 번째 코드를 살펴보면 간단하게 유저를 생성해주는 메소드이다. 이 과정이 짧긴 하지만 tmp 변수의 주된 목적은 임시적인 저장소 역할로 국한되지 않았다. 이럴때에는 userinfo 같은 이름이 더 적절할 것이다.
두 번째 코드
if(right < left){
tmp = right;
right = left;
left = tmp;
}
두 번째 코드의 경우 tmp 라는 이름이 완벽하게 작용한다. 이름 그대로 임시적인 저장소 역할고 제한되어있다. 이때 tmp 라는 이름은 코드를 읽는 사람에게 변수가 임시저장소 이외에 다른 용도가 없다는 사실을 잘 전달한다. tmp는 다른 함수로 전달되거나, 값이 다시 초기화되거나, 여러 차례 반복적으로 사용되는 변수가 아니다.
간단한 예시 (for 문)
첫 번째 코드
for (int i = 0; i < clubs.size(); i++){
for (int j = 0; j < clubs[i].members.size(); j++){
for (int k = 0; k < users.size(); k++){
if (clubs[i].members[k] == users[j]){
}
}
}
}
위의 코드를 보면 흔히들 사용하는 i, j, k 를 반복문의 인덱스로 사용하였다. 이러한 이름들은 보편적이지만 "나는 반복자다" 라는 의미를 충분히 전달한다. 그러나 위의 if 구문에는 오류가 있고 members[]와 users[] 는 잘못된 인덱스를 사용하고 있다. 바로 알 수 있겠는가...? 인덱스가 정확히 사용되었다면 if문은 아래의 코드와 같아야 한다.
if (clubs[i].members[j] == users[k]){}
위와같이 i, j, k 를 인덱스로 사용한다면 무엇이 잘못됬는지 한눈에 알아보기 힘들다는 문제가 있다. 물론 예시와 같이 3중 반복문을 사용하는 일은 흔하지 않을 것이다. 사용되었다면 데이터 저장 구조가 잘못되었는지, 또는 더 나은 방법이 있는지 찾아야 할것이다.
그렇다면 이럴때에는 또는 반복문을 사용할때에는 인덱스의 이름을 어떻게하는게 좋을까..? 이럴 때는 더 명확한 의미를 드러내는 이름을 사용하면 도움이 된다. club_i, members_i, users_i 와 같이 말이다. 혹은 더 간결하게 ci, mi, ui 같은 이름을 사용하는 편이 좋은 선택이다. 변경된 코드를 살펴보자
for (int ci = 0; ci < clubs.size(); ci++){
for (int mi = 0; mi < clubs[ci].members.size(); mi++){
for (int ui = 0; ui < users.size(); ui++){
if (clubs[ci].members[mi] == users[ui]){
}
}
}
}
훨씬 가독성이 높고 좋은 코드가 되었다.
추가적인 정보를 이름에 추가하기✔
앞서 말했듯이 변수의 이름은 작은 설명문이다. 충분한 공간은 아니지만, 이름안에 끼워 넣은 추가 정보는 변수가 눈에 보일 때마다 전달된다. 단위를 포함하는 값들을 통해 어떤 방법으로 더 좋은 변수명을 만들 수 있는지 살펴보자
간단한 예시 (단위를 포함하는 값들)
첫 번째 코드
Long start = new Date().getTime();
Long elapsed = new Date().getTime() - start;
log.info("Load time was: " + elapsed + " seconds");
첫 번째 코드는 사실 특별한 오류를 발생시키는 코드는 아니다. 그러나 getTime() 이 초(second) 단위가 아니라 밀리초(millisecond) 단위를 반환하기 때문에 잘못된 결과를 출력한다. 이럴때에는 변수에 _ms 를 추가하여 단위를 명시해주면 모든 문제가 명확해진다.
두 번째 코드
Long start_ms = new Date().getTime();
Long elapsed_ms = new Date().getTime() - start_ms;
log.info("Load time was: " + elapsed_ms / 1000 + " seconds");
프로그래밍에는 시간뿐만 아니라 다른 단위도 자주 사용된다. 확실하게 구별해주도록 하자.
이름은 얼마나 길어야 하는가?✔
우리는 코드를 짤때 이름에 많은 정보를 담으면 좋겠지만 이름이 지나치게 길면 안 된다는 제한이 암묵적으로 존재한다. 그 누구도 다음과 같이 이름을 사용하지 않을것이다. ThatConvertsImageFilesAndVideoFilesServiceClass 이름이 길면 길수록 기억하기도 어렵고, 다음 줄로 코드가 넘어갈 수 있을 정도로 화면을 차지한다. 반면 너무 길어서는 안된다는 조언을 맹목적으로 받아들인 나머지 오직 단어 하나 혹은 문자 하나로 된 이름을 사용할지도 모른다.
길어도 안되고, 짧아도 안되고 변수는 정확한 용도에 따라 그때그때 판단해야 한다. 다행히도 판단에 도움이 될 만한 조언이 있다.
좁은 범위에서는 짧은 이름이 괜찮다.
if (true){
HashMap<String, Integer> m = new HashMap<>();
m.put("map", 5);
log.info("keyMap: "+m.get("map"));
}
다음과 같은 좁은 범위에서만 사용되는 변수의 이름에 많은 정보를 담을 필요가 없다. 즉, 변수의 타입이 무엇인지, 초기값이 무엇인지, 그것이 어떻게 사라지는지 등과 같은 변수가 담고 있는 모든 정보가 쉽게 한눈에 보이므로 짧은 이름을 사용해도 상관없다. 그러나 m이 전역변수로 사용된다면 어떨까? m의 타입이나 목적이 드러나지 않으므로 코드의 가독성이 떨어질것이다. 즉, 좁은 범위에서는 많은 정보를 담을 필요가 없으나 전역변수로 사용될때에는 확실하게 변수명에 의미를 담아주자.
약어와 축약형
때때로 우리는 짧은 이름을 사용하기 위해 약어나 축약형을 사용한다. 예를들어 BackEndDeveloper 대신 BEDeveloper 라는 이름을 사용할 수 있을것이다. 그러나 이러한 축약은 나중에 혼란을 가져올지 모른다. 책 저자의 경험에 의하면 프로젝트에 국한된 의미를 가진 약어 사용은 좋은 생각이 아니라고 한다고 한다. 이런 이름은 프로젝트에 새로 합류한 사람에게 비밀스럽고 위협적인 모습으로 다가온다. 시간이 흐르고나서는 이름을 만들어낸 장본인에게조차 비밀스럽고 위협적인 모습을 지니게 된다.
따라서 가장 좋은방법은 팀에 새로 합류한 사람이 이름이 의미하는 바를 이해할 수 있는것이다. 예를 들어 프로그래머들 사이에서 흔히 사용하는 String 을 대신하는 str 또는 document 를 대신하는 doc 는 FormatStr() 이라는 함수를 보면 바로 의미하는 바를 알 수 있을것이다. 그러나 BEDeveloper의 의미는 이해할 수 없다.
불필요한 단어 제거하기
예를 들면 ConvertToString() 이라는 이름대신 ToString() 이라고 짧게 써도 실질적인 정보는 사라지지 않는다.
오해할 수 없는 이름들
이름을 지을때에는 "본인이 지은 이름을 다른사람들이 다른 의미로 해석할 수 있을까?" 라는 질문을 던져보며 철저하게 확인해보아야 한다.
results = filter(yearList, 2022);
위의 코드를보자. filter 메소드를 통해 무엇을 하는지 한눈에 이해가 되는가? 2022년만을 고른다는 것인지, 제거한다는 것인지 알 수 없다. 이럴때에는 results 라는 이름보다는 고를때에는 selectYear, 제거할때에는 excludeYear 이 훨씬 더 나은 이름이 될것이다.
한계를 설정하는 이름에는 한계를 표기해라.✔
ITEM_IN_CARTS = 10;
위의 상수명을 한번 살펴보자. 위의 ITEM_IN_CART 를 봤을때에 CART에 담을수 있는 ITEM의 갯수가 최소 10 이라는걸까, 최대 10이라는 걸까? 한눈에 알수가 없다. 이럴때에는 다음과 같이 이름에 한계를 드러나게 해주자
MAX_ITEMS_IN_CART = 10;
불리언 변수에 이름붙이기.✔
Boolean readPassword = true;
위의 코드는 읽은 방법에 따라서 두 가지 상반된 해석이 가능하다.
- 패스워드를 읽을 필요가 있다.
- 패스워드가 이미 읽혔다.
이 경우에는 read 라는 단어를 사용하지 않는 것이 최선이다. 대신 userIsAuthenticated(사용자가 인증되었습니다) 라는 단어를 사용할 수 있다.
미학
왠 미학...? 나는 미학이라는 제목을 보고 미학이 무슨상관이지 라는 생각을 했었는데 뒷장의 내용들을 보자마다 아...~ 라는 생각을 했다. 보기좋은 코드가 곧 읽기좋은 코드이다.
다음과 같은 코드를 보자. 자 어떤 생각이 드는가?
HashMap<String, String> jsonAdams = new HashMap<>();
jsonAdams.put("name", "Mr.jsonAdams paris G Middle Smooth Names");
jsonAdams.put("age", "25");
jsonAdams.put("ssn", "97012314231423")
HashMap<String, String> GDragon = new HashMap<>();
GDragon.put("name", "Mr.GDragon Sun Static Void Main");
GDragon.put("age", "27");
GDragon.put("ssn", "87012314231423");
HashMap<String, String> parisMillie = new HashMap<>();
parisMillie.put("name", "Mr.parisMillie HashMaps Cool Guys");
parisMillie.put("age", "28");
parisMillie.put("ssn", "77012314231423");
HashMap<String, String> testAdams = new HashMap<>();
testAdams.put("name", "testAdams");
testAdams.put("age", "30");
testAdams.put("ssn", "67012314231423");
assertEquals(jsonAdams.get("name"), "Mr.jsonAdams paris G Middle Smooth Names");
assertEquals(GDragon.get("name"), "Mr.GDragon Sun Static Void Main");
assertEquals(parisMillie.get("name"), "Mr.parisMillie HashMaps Cool Guys");
assertEquals(testAdams.get("name"), "testAdams");
아.... 정말 지저분한 코드이다. 누가보라고 짠 코드야? 라는 생각이 들것이다.(개선되는 모습을 보여주기 위해 더 지저분한 코드를 짜고싶었지만 어떻게해야 더 지저분해질지 감이안왔다) 위와같은 코드는 미학적으로 만족스럽지 못한 코드이다. 어떤줄은 이름에 따라 너무 길고 또한, 아름답지 않고 일관성 있는 패턴도 결여되어있다. 위의 코드를 총 2번에 걸쳐 개선할건데 첫번째로는 메소드를 사용하여 위의 코드를 한번 개선하여보자.
메소드를 활용하여 불규칙성 및 중복코드 제거하기✔
private HashMap<String, String> createUser(String name, String age, String ssn){
HashMap<String, String> userInfo = new HashMap<>();
userInfo.put("name", name);
userInfo.put("age", age);
userInfo.put("ssn", ssn);
return userInfo;
}
HashMap<String, String> jsonAdams =
createUser("Mr.jsonAdams paris G Middle Smooth Names", "35", "957488371837283");
HashMap<String, String> GDragon =
createUser("Mr.GDragon Sun Static Void Main", "", "");
HashMap<String, String> parisMillie =
createUser("Mr.parisMillie HashMaps Cool Guys", "", "957488371837283");
HashMap<String, String> testAdams =
createUser("testAdams", "35", "");
assertEquals(jsonAdams.get("name"), "Mr.jsonAdams paris G Middle Smooth Names");
assertEquals(GDragon.get("name"), "Mr.GDragon Sun Static Void Main");
assertEquals(parisMillie.get("name"), "Mr.parisMillie HashMaps Cool Guys");
assertEquals(testAdams.get("name"), "testAdams");
createUser 메소드를 만들어 한번 개선한 코드이다. 중복되는 코드를 없애서 코드를 더욱 간결하게 하였고, 해당 메소드를 사용하여 또 다른곳에서 createUser을 사용할 수 있기때문에 새로운 테스트도 쉬워졌다. 나는 사실 여기까지면 꽤나 만족하는 코드라고 생각한다. 하지만 책에서는 도움이 된다면 코드의 열을 맞추는것도 추천한다.
도움이 된다면 코드의 열을 맞춰라✔
HashMap<String, String> jsonAdams =
createUser("Mr.jsonAdams paris G Middle Smooth Names", "35 ", "957488371837283");
HashMap<String, String> GDragon =
createUser("Mr.GDragon Sun Static Void Main ", "" , "");
HashMap<String, String> parisMillie =
createUser("Mr.parisMillie HashMaps Cool Guys ", "" , "957488371837283");
HashMap<String, String> testAdams =
createUser("testAdams ", "35 ", "");
assertEquals(jsonAdams.get("name"), "Mr.jsonAdams paris G Middle Smooth Names");
assertEquals(GDragon.get("name"), "Mr.GDragon Sun Static Void Main");
assertEquals(parisMillie.get("name"), "Mr.parisMillie HashMaps Cool Guys");
assertEquals(testAdams.get("name"), "testAdams");
다음과 같이 코드의 열을 맞춘다면 createUser() 메소드에 주어지는 파라미터가 무엇인지 더욱더 쉽게 구별할 수 있다.
의미 있는 순서를 선택하고 일관성 있게 사용하라✔
우리가 코드를 나열할때 또는 변수를 선언할때에 순서에 아무런 의미를 두지 않고 선언할때가 많다. 그러나 변수에도 순서를 두는것이 중요하다. 가장 중요한것에서 가장 덜 중요한 순서로 변수를 나열하고, 알파벳 순서대로 나열하여 일관성있는 방식으로 나열해야한다고 한다.
코드를 문단으로 쪼개라✔
private monsterFamily(){
family = monster.family();
familyNumber = family.getFamilyNumber();
bigHeightMonster = family.getBigHeightMonster();
smallHeightMonster = family.getsmallHeightMonster();
nonMonster = family.getPerson();
nonMonsterName = nonMonster.getPersonName();
log.info(family);
log.info(familyNumber);
log.info(bigHeightMonster);
log.info(smallHeightMonster);
log.info(nonMonster);
log.info(nonMonsterName);
}
위의 코드는 뚜렷하게 드러나지는 않지만 구별되는 여러 단계를 거치고있다. 이런 코드들을 여러 문단으로 쪼개고 각 문단별로 주석처리를 해주면 다른 누군가 또는 미래의 내가 알아보기 쉬울 것이다.
private monsterFamily(){
// 몬스터 패밀리의 총 가족인원수를 가져온다.
family = monster.family();
familyNumber = family.getFamilyNumber();
// 몬스터 패밀리중 키가 큰 몬스터와 작은 몬스터를 가져온다.
bigHeightMonster = family.getBigHeightMonster();
smallHeightMonster = family.getsmallHeightMonster();
// 몬스터 패밀리중 사람을 대려와 이름을 확인한다.
nonMonster = family.getPerson();
nonMonsterName = nonMonster.getPersonName();
//각 결과들을 출력한다.
log.info(family);
log.info(familyNumber);
log.info(bigHeightMonster);
log.info(smallHeightMonster);
log.info(nonMonster);
log.info(nonMonsterName);
}
코드를 문단별로 정리하니 가독성이 훨씬 높아졌다. (예시를 위한 코드입니다)
개인적인 스타일 대 일관성✔
코드를 짜다보면 사람마다 스타일이 다르고 팀마다 스타일이 다르다. 여러 강의와 다른사람들이 짠 코드들을 보다보면 개개인의 코드 스타일이 다르다는걸 쉽게 알수있다.
public class Person{
};
위의 코드와 같이 짜는 사람이 있는 반면 아래의 코드와 같이 짜는 사람이 있다.
public class Person
{
};
누구의 코드 스타일이 옳다고는 하지 못한다. 다만 일관성이 가장 중요하다. 한 프로젝트 내에서 모두가 다른 스타일로 코드를 짠다면... 생각만 해도 끔찍하다. 적어도 같은 프로젝트 내에서는 그 프로젝트의 코드 스타일을 따르자.
주석에 담아야하는 대상
주석의 목적은 코드를 읽은 사람이 코드를 작성한 사람만큼 코드를 잘 이해하게 돕는 데 있다. 이때에 어떤 주석을 달아야 가치가 있을지 살펴보자
코드에서 빠르게 유추할 수 있는 내용은 주석으로 달지 말라.✔
//앞에오는 내용의 뒤에 공백을 기준으로 새로운 내용을 합쳐준다.
String name = "name".join(" ","Json private");
기술적으로 볼때 이 코드는 이미 코드가 무슨일을할지 다 설명하고 있다. 주석에 새로운 정보라고 할 것이 없는것이다.
나쁜 이름에 주석을 달지말고 이름을 고쳐라.✔
이름으로 메소드 혹은 변수의 의미를 모르겠을때에는 주석으로 때울 생각을 하지말고 먼저 이름을 고치고 이후에 주석으로 상세한 설명을 덧붙여 주자.
자신의 생각을 기록하라.✔
자신이 코드를 짜며 생각했던 것들을, 또 시험했던 것들을 적어놓는 것이다. 예를들어 이런 방식으로 코드를 짰을때에 어떠한 방식보다 속도가 ??% 정도 빠르다. 이렇게 짰을때에 어떠한 더 나은 결과를 나타낸다 등... 자신이 경험했던것들을 적어놓아 다른사람이 코드를 최적화하느라 시간을 허비하지 않게 도와준다.''
코드에 있는 결함을 설명하라.✔
코드를 짜다보면 프로젝트의 일정상 시간이 촉박할 때가 있고, 그 과정가운데 버그를 갖게 될 요소가 생길 수 밖에 없다. 이러한 문제들이 있을때에는 몇가지 주석으로 문제를 표시해둘 수 있다.
표시 | 보통의 의미 |
TODO: | 아직 하지 않은 일 |
FIXME: | 오동작을 일으킨다고 알려진 코드 |
HACK: | 아름답지 않은 해결책 |
XXX: | 위험! 여기 큰 문제가 있다 |
TextMate | ESC |
팀마다 이러한 표시들을 사용하는지, 언제 사용하는지 다를 수 있으나 자신의 생각을 담은 이러한 주석을 남기는것을 당연하게 생각해야 한다는 것이다. 주석은 코드의 질이나 상태 그리고 추후 개선 방법들을 제시하는 길이될수 있기 때문이다.
주석 남기기를 두려워 하지 마라✔
많은 프로그래머들이 주석을 남기는 것을 달가워하지 않는다고 한다. 좋은 주석을 달기 위해서 고민하는 시간들을 아깝게 생각하기 때문이라고 한다. 그러나 주석을 자주 남길수록 자신의 생각의 질이 향상되어 궁극적으로는 주석을 달 일이 많아지지 않을것이며, 꾸준히 달아둬야 나중에 한꺼번에 많은 분량의 주석을 달아야하는 일을 피할 수 있다.
명확하고 간결한 주석 달기
코드의 의도를 명시하라✔
간혹 주석을 달다보면 코드가 수행하는 동작을 그대로 설명하는 데 그치는 경우가 있다. 예를들어 // 리스트를 역순으로 반복한다. 라던지. 이러한 주석들도 괜찮긴하나 다음과 같이 달아주면 더욱더 좋을거라고 상각한다. // 몬스터를 키가 큰 값에서 낮은값 순으로 나타내준다.
이름을 가진 함수 파라미터 주석✔
이름을 가진 함수 파라미터 주석이란... 다음과 같은것이다.
위의 사진과같이 파라미터에 들어갈 값들에 어떤값들이 들어가야 하는지 주석을 달아주는 것이다. 그러나 요즘은 친절하게도 IntelliJ의 기능인지 자바에서 제공해주는 기능인지는 모르겠으나 다음 사진과같이 알아서 표기해준다.
읽기 쉽게 흐름제어 만들기
우리가 코드를 짤때에 if 문, for문 등 조건,루프, 흐름을 통제하는 코드가 없다면 매우 읽기 편할것이다. 그러나 빠질수 없는 요소이기에 코드에 존재하는 흐름제어를 최대한 읽기 쉽게만드는 방법을 알아보자.
조건문 인수의 순서✔
if (length >= 20){}
if (20 <= length){}
위 코드중 어떤것이 더 읽기 쉬울까? 아마 대부분의 개발자들은 첫번째 코드가 읽기 쉽다고 느낄것이다. 이와 관련하여 발견된 유용한 규칙이 있다고 한다.
왼쪽 | 오른쪽 |
값이 더 유동적인 질문을 받는 표현 | 더 고정적인 값으로. 비교대상으로 사용되는 표현 |
이러한 규칙은 다음 한국말과도 일치한다. "초록색입니다. 풋사과는" 라고 말하는것은 뭔가 부자연스럽다. 하지만 "풋사과는 초록색입니다." 라고 말하는것은 자연스럽다. 이것과 같은 맥락이다.
if/else 블록의 순서✔
if/else 문의 블록의 순서를 생각해본적이 있는가? 사실 나는 한번도 생각해본적이 없으나 두가지중 어느 한 쪽을 선택해야 하는 몇가지 규칙이 있다고 한다.
if (apple == greenApple){
} else {
}
위의 코드를 다음과 같이 바꿀 수 있다.
if (apple != greenApple){
} else {
}
- 부정이 아닌 긍정을 다루어라. 즉 if(!apple) 이 아닌 if(apple) 을 선호하자. 생각해보면 if 문을 사용할때에 if(!apple) 과도 같은 방식으로 되어있다면 !에 대해서 무엇을 부정한건지 한번쯤 살펴보게 된다.
- 간단한 것을 먼저 처리하라
- 더 흥미롭고, 확실한 것을 먼저 다루어라
라는 규칙이 있다. 자신이 짠 if/else 문이 올바른 순서대로 작성되었는지 잘 확인하고 중복된 검사나 잘못된 검사를 피하자. 다들 if/else 문을 사용하며 한번씩 경험했었을 것이다. 순서만 잘 배치해도 더 간결해질 수 있는 상황이 있음을
삼항 연산자를 이용하는 조건문 표현✔
int minAge = 10;
int maxAge = 20;
int userAge = minAge < maxAge && minNum != maxNum ? (minNum * 5) : (maxAge * 2)
글의 시작점에서 만났던 코드이다. 분명 한줄로 모든걸 끝낼수 있으나 이 삼항 연산자는 두개의 값을 선택하는것에서 끝나는 것이 아닌 결과값에 계산까지 들어간 복잡한 문제이다. 이러한 코드는 if/else 문을 사용하는 것이 더욱 보기 편하다.
int minAge = 10;
int maxAge = 20;
int userAge;
if(minAge < maxAge && minNum != maxNum){
userAge = minAge * 5;
}else{
userAge = maxAge * 2;
}
기본적으로 if/else 를 이용하길 바란다. 삼항연산자는 매우 간단할때에만 사용하길 추천한다.
중첩을 최소화 하라✔
우리는 코드를 작성하다보면 if 문안에 if문 그리고 그안에 if문을 한번더 작성하는 사태를 초보시절에 한번쯤은 경험하게 된다. 코드의 중첩이 심할수록 이해하기 어려워지는 문제가 있고 위에서 어떠한 조건들을 지나서 왔는지 파악하기 힘들어진다. 다음은 사과가 잘 익었다면 잘 익은 맛있는 사과를 리턴해주는 메소드입니다.
if (apple.getName() != "apple"){
if (apple.getSize() > greenApple.getSize()){
if (apple.getAppleStatus() == "ripeApples"){
return apple;
}
}else{
return apple.setSize(greenApple.getSize() + 1);
}
}else{
return "사과가 아닙니다.";
}
다음과 같은 중첩되는 코드가 있다. 한눈에 알아보기 힘든 아주 잘못된 코드이다.(중첩이라는 상황을 위해 일부러 더욱더 평소에는 볼수조차 없는 지저분한 코드를 만들었다) 이런 코드들은 함수의 중간에서 반환하여 중첩을 최소화 해주자.
if (apple.getName() != "apple"){
return "사과가 아닙니다.";
}
if (apple.getSize() < greenApple.getSize()){
return apple.setSize(greenApple.getSize() + 1);
}
if (apple.getAppleStatus() == "ripeApples")){
return apple;
}
실패에 대한 경우들을 먼저 처리하고 다음 상황들을 정리할 수 있다. 코드가 훨씬 보기 좋아졌다 별거 아닌 내용인데 코드를 중첩시킨다면 정말 알아보기 힘든 코드가 되는 문제를 볼 수 있다.
변수와 가독성
이 책에서는 변수를 엉터리로 사용하면 코드를 이해하기 힘들고 어려워진다고 말하며, 특히 3가지 문제가 있다고 한다.
- 변수의 수가 많을수록 기억하고 다루기 더 어려워진다.
- 변수의 범위가 넓어질수록 기억하고 다루는 시간이 더 길어진다.
- 변수값이 자주 바뀔수록 현재값을 기억하고 다루기가 더 어려워진다.
변수 제거하기✔
String appleStatus = apple.getStatus();
fruitsStatus.setApple(appleStatus);
다음 코드는 사과의 상태를 과일상태에 set 해주는 상황이다. 그런데 이때에 appleStatus 변수가 꼭 필요할까?
fruitsStatus.setApple(apple.getStatus());
전혀 appleStatus 변수가 필요하지 않으며 다음과 같이 한줄로 끝날수 있다. 물론 한 메소드 안에서 여러곳에서 중복되어 사용된다면 어딘가에 담아두는것이 좋다 그러나 그럴필요가 없다면 굳이 변수로 선언하지 않아도 된다.
변수의 범위를 좁혀라✔
우리는 변수의 범위를 최대한 좁혀야 할 필요가 있다. 개발을 하다보면 전역변수를 자제하라는 말을 한번씩은 들어봤을 것이다. 전역변수는 어디에서 어떻게 사용되는지 일일이 확인하기 어렵고, 지역변수의 이름과 중복이 되어 nameSpace가 더러워질 수 있다. 또한 어떠한 코드를 실행하다 지역 변수를 변경할 때 실수로 전역 변수를 변경하여 문제가 발생할 수 있다.
값을 한번만 할당하는 변수를 선호하라✔
이게 무슨말일까...? 간단하게 상수를 선호하라 라는 말이다. 변수의 값이 변하는 곳이 많을 수록 현재값을 추측하기가 어려워지는 문제가 있다. 그러나 상수로 할당해준다면 값이 변할리 없기에 개발자에게 추가적인 생각을 요구하지 않는다.
@PostMapping("/create-monster")
public CreateMonsterDto.Response createMonster(
@Valid @RequestBody final CreateMonsterDto.Request request
){
return mMakerService.createMonster(request);
}
다음과 같은 방식으로 request 로 받은 몬스터의 생성 정보는 final 로 선언하여 변경될 수 없게 만들어준다. 이후에는 값이 변할 일이 없기에 개발자는 추가적인 생각없이 편하게 작업을 할 수 있다.
한번에 하나씩
한 번에 하나의 작업만 수행하게 코드를 구성해야 한다. 왜냐하면 한번에 여러가지 일을 수행하는 코드는 이해하기가 어렵고, 코드 블록 하나에서 객체를 초기화하고, 업데이트하는 등 여러가지 작업이 뒤섞이면 코드를 이해하기 힘들것이다.
자바언어를 기반으로 생각해보자 우리는 Controller, Service 등... 각 레이어 별로 역할을 구분한다. 그리고 메소드를 만들때에 getDetailMonster() 라는 몬스터의 상세정보를 가져오는 기능을 만들면 만들었지 MonsterCreateAndUpdate() 라는 한번에 여러가지 일을 하는 메소드를 만들지는 않는다. 메소드당 하나의 작업을 수행하게 함으로써 함수 하나에서 행하는 일들을 줄이고 기능을 나눠 재활용성을 높이는 것이다.
테스트 친화적 개발
우리는 테스트에 익숙해져야 할 필요가 있다. 먼저 테스트를 함으로써 우리에 코드를 한번 더 생각할 수 있고, 앞으로 일어날 혹시모를 에러들을 예방할 수 있다. 테스트코드는 추후 유지보수 하기 쉽게 만들어야하며 복잡하게 만든다면 기능이 변경되어 테스트가 필요할때에 코드를 보기 싫은 상황이 올수 있다.
마무리
이 책을 읽으며 주니어 개발자인 글쓴이는 많은 도움이 되었다. 항상 코드를 짜며 변수명을 어떻게 지어야 좋을지 고민했고, 길이를 길게 해도될지에 대한 고민을 했었는데 고민에 대한 가닥을 어느정도 잡을수 있었고, 앞으로 더욱더 생각하는 개발을 할 초석을 만들어주었다.
이 책을 읽기 전까지는 개발하며 코드를 줄이는게 무조건 좋다는 생각에 삼항문을 열심히 사용하고, 반복문에 i, j 를 아무런 생각없이 신나게 사용했었는데 반성하는 시간을 갖게되었다. 이래서 책을 읽고 공부를 꾸준히 해야하나보다.
'Read Book > 짧은 내용의 책' 카테고리의 다른 글
그림으로 공부하는 IT 인프라 구조(리뷰 및 주요 개념 정리) (0) | 2022.06.20 |
---|