이번 포스팅에선 클린 코드 3장 함수에 대해서 알아보겠습니다.
3장에서는 함수를 어떻게 하면 클린하게 작성할 수 있는지 소개합니다.
1. 작게 만들어라
가장 중요한 규칙입니다. 작게 만들어라!
얼마나 작아야 작게 만든 걸까? 라는 생각을 맨날 했는데
켄트 벡이 예전에 짠 소스엔 2~4줄이 넘어가는 메소드가 없었다고 합니다.
(실무에서 저렇게 해보려고 했는데 잘 안 되더라구요...ㅎㅎ..)
if, else, while 등에 들어가는 블록은 1줄이어야 한다.
결론 : 작으면 작을수록 좋다.
2. 한 가지만 해라!
2장에서도 나왔던 내용입니다.
함수는 한 가지만 해야 하고 그걸 잘 해야 한다.
그럼 한 가지만 하는지 어떻게 판단할 수 있을까?
1) 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행하는가?
2) 함수 내 로직을 의미있는 이름으로 다른 함수를 추출할 수 있는가?
3) 함수 내 섹션이 나눠지는가?
3. 함수 당 추상화 수준은 하나로!
함수 내 모든 문장의 추상화 수준이 동일해야 한다.
한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈립니다.
그럼 어떻게 추상화 수준이 동일하게 작성할 수 있을까?
내려가기 규칙
한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.
함수를 타고 들어가서 나오는 함수는 추상화 수준이 한 단계 낮은 함수여야 한다.
내려가기 규칙을 적용해서 추상화 수준이 하나인 함수를 작성하자
4. 서술적인 이름을 사용하라!
함수 이름은 서술적으로 작성하는 게 좋다.
서술적이고 이해가 쉬운 긴 이름 > 짧고 난해한 이름, 서술적인 주석
예) testOrder() -> isOrderable(), hasAvailableProduct()
5. 적절한 함수 인자를 사용하라!
적합한 인자 수 : 0개 > 1개 > 2개 > 3개 > 4개 이상
1) 단항 함수 (1개)
좋은 단항 함수)
- 질문을 던지는 경우 : boolean fileExists("MyFile")
- 인자를 변환해 리턴하는 경우(변환 함수) : InputStream fileOpen("MyFile")
위에 경우가 아니면 단항 함수는 가급적으로 쓰지말자
나쁜 단항 함수)
- 변환 함수에서 출력 인수 사용 지양 : void includeSetupPageInto(StringBuffer pageTest)
- 입력 인수를 변환하는 변환하는 함수라면 변환 결과는 반환값으로 돌려준다 :
void transform(StringBuffer out) 보다 StringBuffer transform(StringBuffer in)가 좋다.
플래그 인자)
플래그 인자는 boolean 타입 인자를 말한다. 예) render(true)
사용을 지양해야 하는 이유 :
- 플래그 인자로 true면 a, false b를 처리하니깐 대놓고 a, b 2가지 일을 하기 때문
- 호출 코드만 보면 헷갈린다
해결법 : true, false 로직을 적합한 함수로 추출
2) 이항 함수 (2개)
당연히 이항 함수는 단항 함수보다 이해하기가 어렵다.
다들 아시는 assertEquals(expected, actual) 메소드만 봐도 expected가 먼저인지 actual가 먼저인지 엄청 헷갈린다..
가능하면 단항 함수로 변경시키려고 노력해야 한다.
당연한 얘기지만 무조건 이항 함수가 나쁘다는 건 아니다.
좋은 이항 함수)
Point p = new Point(0, 0)
직교 좌표계는 일반적으로 x, y를 갖기 때문에 위와 같이 쓰면 좋다.
나쁜 이항 함수)
writeField(outputStream, name)
writeField(outputStream, name)은 outputStream에 name을 쓴다는 건지, name에 outputStream에 있는 값을 넣는 다는 건지 정확히 알 수가 없다. 물론 전자겠지만.. 헷갈릴 수 있다.
outputStream.writeField(name)로 변경하면 정확하게 보자마자 이해가 간다.
3) 삼항 함수 (3개), 다항 함수 (4개 이상)
삼항, 다항은 최대한 신중하게 생각해봐야 한다.
이항 함수보다 배는 이해하기 어려워지기 때문이다.
삼항, 다항을 해소 방법)
항상 적용되는 건 아니다.
- 인자 객체
Circle makeCircle(double x, double y, double radius) -> Circle makeCircle(Point center, double radius)
x, y를 Pointer 객체로 만들어서 인자로 받으면 좀 더 이해가 쉽다.
- 인자 목록
String.format("%s 함수의 %s 인자를 줄여볼게요")
위 함수를 호출하려면 인자를 (String method, String arg) 라고 써야 하는데 이를
(String... args)로 한번에 받을 수 있다.
6. 부수 효과를 일으키지 마라!
public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPasswd(String id, String passwd) {
User user = UserGateway.findById(id);
if (user != null) {
String codedPhrase = user.getPhraseEncodedByPasswd();
String phrase = cryptographer.decrypt(codePhrase, passwd);
if ("Valid Passwd".equals(phrase)) {
Session.init();
return true;
}
}
return false;
}
}
checkPasswd라는 이름만 봐도 비밀번호를 확인 해주는 함수로 보인다.
하지만 내부 로직을 보면 부수효과를 일으키는 로직이 존재한다.
if ("Valid Passwd".equals(phrase)) {
Session.init();
return true;
}
위와 같은 부수효과는 함수 이름만 보면 알 수가 없다.
함수 이름을 checkPasswd -> checkPasswdAndInitSession 라고 변경해주자.
그럼 더 명확해져 이해하기가 쉽다.
출력 인수)
이항 함수 - 나쁜 이항 함수에서 설명한 내용입니다.
appendFooters(s)를 보면 s를 footer에 추가하는지 s에 footer를 추가하는지 알 수가 없다. 함수 선언부를 보기 전까진...
위처럼 쓰지말고 report.appendFooter() 형식으로 이해하기 쉽게 쓰자.
7. 명령과 조회를 분리하라!
public boolean set(String attribute, String value);
위 함수는 attribute에 value를 set해서 성공하면 true, 실패하면 false를 리턴하는 함수다.
if (set("username", "effortguy"))...
set 함수를 사용하면 위와 같은 코드가 발생하는데 set을 하는 함수인지? set이 되어있다는 함수 인지? 구분이 힘들다.
if (existsAttribute("username")) {
setAttribute("username", "effortguy");
}
위처럼 명령 (setAttribute) 과 조회 (existsAttribute)를 분리하니 확실히 보기 좋아졌다.
8. 오류 코드보다 예외를 사용하라!
if (deletePage(page) == E_OK)
위처럼 사용하면 "명령과 조회를 분리하라" 규칙을 위반한다.
그리고 여러 단계로 중첩되는 코드를 야기한다.
if (deletePage(page) == E_OK) {
if (registry.deleteRegistry(page.name) == E_OK) {
logger.log("Registry deleted");
} else {
logger.log("delete Registry failed");
}
} else {
logger.log("delete failed")
return E_ERROR;
}
위 같이 에러 코드를 사용하면 이상하게 생긴 모양의 중첩문이 나온다.
바로 예외를 사용하게 수정해보자.
try {
deletePage(page);
registry.deleteRegistry(page.name);
} catch (Exception e) {
logger.log(e.getMessage());
}
예외를 사용하게 수정하니 아주 깔끔해졌다.
한 단계 더 나아가서 try, catch 블록에 있는 로직을 함수로 빼냈다.
public void delete(Page page) {
try {
deletePageAndRegistry(page);
} catch(Exception e) {
logError(e);
}
}
private void deletePageAndRegistry(Page page) throw Exception {
deletePage(page);
registry.deleteRegistry(page.name);
}
private void logError(Exception e) {
logger.log(e.getMessage());
}
아주 깔끔해졌다.
여기서 중요하게 알고 넘어가야 할 것은 오류도 한 가지 작업이다 라는 것이다.
한 가지 작업만 잘 하자는 규칙을 잘 생각해보자.
9. 반복하지 말아라!
이건 뭐 너무나 당연한 소리다. 중복을 최소화 하라.
10. 함수를 어떻게 짜죠?
백지에 함수를 짜는데 당연 처음부터 잘 짤 수는 없다. 물론 잘 짜는 사람들도 있다.
로직을 짠다 - 테스트 코드를 짠다 - 리팩토링을 한다
작가처럼 글을 쓰고 계속 리팩토링을 하다보면 어느새 좋은 함수가 작성되어 있는 걸 볼 수 있다.
개발할 때 엄청 중요한 함수를 잘 짜는 법에 대해 알아봤습니다.
위에 있는 규칙들을 모든 곳에 적용할 수 는 없지만 항상 생각하면서 개발해야겠습니다.
'Clean Code' 카테고리의 다른 글
[클린 코드] 6장 - 객체와 자료 구조 (Objects and Data Structures) (0) | 2022.03.06 |
---|---|
[클린 코드] 5장 - 형식 맞추기 (Formatting) (0) | 2022.03.01 |
[클린 코드] 4장 - 주석 (Comments) (0) | 2022.03.01 |
[클린 코드] 2장 - 의미 있는 이름 (Meaningful Names) (0) | 2022.02.27 |
[클린 코드] 1장 - 깨끗한 코드 (Clean Code) (0) | 2022.02.27 |
댓글