미소를뿌리는감자의 코딩
Clean Code 본문
함수의 인자 수
함수의 인자 개수는 중요하다. 이는, 함수의 가독성에 영향을 미친다.
인자의 개수가 4개를 넘어가면 안된다. 라고까지 말한다.
또한 예외 처리의 경우 각각 처리하는 것보단, 한 번에 처리하는 게 더 낫다. 만약 함수에서 에러가 발생해서, 이를 반환해 준다면, 호출부에서 If문으로 검사를 하게 된다. 이는 전반적인 코드의 가독성을 떨어뜨린다.
해당 함수에서 throw를 하고 호출부에서 try catch로 잡아주는 것이 차선 책이다. 더 좋은 방법은 호출부에서 에러를 처리하지 않는 것이다.
private void deletePageAndAllReferences(Page page) throws Exception {
...
}
private void logError(Exception e) {
logger.log(e.getMessage());
}
함수에서 Throws를 통해 함수 내부에서 발생한 에러들을 처리한다.
주석
"잘 달린 주석은 그 어떤 정보보다 유용하다. 경솔하고 근거 없는 주석은 코드를 이해하기 어렵게 만든다."
필자는 대부분의 주석은 필요 없는 주석이라고 말하는 것 같다. 주석은 코드와 같이 고치기 힘들 뿐더러, 코드가 수정되더라도 주석은 수정이 안될 수 있기 때문이다.
또한, 코드를 더 명확하게 적음으로써 주석의 사용을 줄일 수 있다. 따라서, 주석을 어떻게 작성할까 고민하기 보다는 코드를 어떻게 더 명료하게 작성할까 고민하는 것이 더 좋다.
그래도 필자가 인정하는 주석은 다음과 같다.
- 의도를 설명하는 주석
- 결과를 경고하는 주석 - 특정 테스트 케이스를 꺼야 하는 이유.
- TODO 주석
- 중요성을 강조하는 주석
해당 챕터를 끝까지 읽으면서 든 생각은,, 최대한 주석을 사용하지 않고, 명료하게 코드를 작성하려 노력하자.
형식 맞추기
소스 파일의 첫 부분은 고차원 개념과 알고리즘을 설명해야 한다. 이후, 마지막에는 가장 저차원 함수와 세부 내역이 나온다.
"종속 함수. 한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다."
또한 연관되어 있는 함수들을 가까이에 두는 것이 좋다. 원래 public 함수들을 위에다 배치하고, private 함수를 파일의 하단에 배치하는 경향이 있었으나, 이번 계기로 근처에 배치하도록 노력할 것이다.
또한 들여쓰기를 일관되게 사용하여 프로젝트의 가독성을 높이는 것이 좋다. 가끔씩 짧은 함수의 경우 한 줄에다가 완성하는 경우도 있지만, 필자는 그것을 권하지 않는다. 해당 함수도 명확하게 {} 괄호를 사용하여 일관된 형태로 유지하는 것을 추천한다.
오류 처리
오류 처리의 경우 null 혹은 에러를 반환 받아서 명시적으로 처리하는 것을 권유하지 않는다. 차라리 발생한 곳에서 에러를 던지는 것을 더 권유한다.
또한 wrapper 클래스가 있어서, 이를 이용한 방법에 대해서도 알게 되었다.
에러가 발생할 우려가 있는 클래스를 wrapper클래스로 감싸서, 해당 에러를 wrapper 클래스에서 처리하도록 하는 방식이다.
테스트 코드
깨끗한 코드를 작성하기 위해선, 목적 중심의 코드가 중요하다.
예를 들어 페이지를 만들기 위해서 여러 설정 파일들이 필요하고 준비과정이 필요하다면, 이를 함수로 묶어서 가독성이 더 좋도록 변경한다.
WikiPage page = makePage("PageOne");
위와 같이 makePage 함수에다가 해당 기능들을 넣어서 작동하도록 만든다.
각 테스트는 명확히 세 부분의 나눠진다.
- 테스트 자료를 만든다
- 테스트 자료를 조작
- 조작한 결과가 올바른지 확인
책을 읽으면서 나중에 적용해보고 싶은 테스크 코드 작성 방식이 있어 밑에 적어둔다.
public void testGetPageHierarchyHasRightTags() throws Exception {
givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
whenRequestIsIssued("root", "type:pages");
thenResponseShouldContain(
"<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"
);
}
신기하게도, given-when-then을 함수 명명에 사용하여 작성하고 있다. 또한 그 내부에 작동 될 로직들을 해당 함수 밑으로 넣은 것이 신기하다.
조금 걱정되는 부분은,, 이렇게 적었을 때, 추상화가 너무 많이 되는 것이 아닐까.. 하는 고민이 있다.
F.I.R.S.T
깨끗한 테스트는 다섯 가지 규칙을 따른다고 한다.
Fast
Independent
Repeatable
Self-Validating
Timely (이 부분...)
Timely의 부분에서는 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다고 한다.
테스트가 불가능하도록 실제 코드를 설계할 가능성이 있으니, 단위 테스트를 실제 코드 작성 직전에 작성해야 한다고 한다.
이 부분에 대해서는 감이 잘 안잡힌다. 테스트 -> 실제 코드 ''' 작성 과정이 머릿속에서 잘 그려지지 않는다.
시스템
시스템 파트에서는 객체 지향적인 관점을 중점으로 이야기를 한다. 컨테이너를 이용해서, 객체를 넘김으로서 독립성을 보장하려고 한다는 점도 알게 되었다.
그 중 AspectJ 관점에 관심이 생겼다.
AspectJ는 언어 차원에서 관점을 모듈화 구성으로 지원하는 자바 언어 확장이다. 라고 설명이 작성되어 있다.
뭔가 annotation을 이용해서 사용하는 방법인듯 한데, 자세히 나와있지 않아 찾아보았다.
https://jiwondev.tistory.com/152
자바 AOP의 모든 것(Spring AOP & AspectJ)
이 글은 사전지식이 없다면 읽기 어려울 수 있다. 바이트코드와 리플렉션을 모른다면 아래의 글을 꼭 읽어보도록하자. 2021.08.17 - [기본 지식/Java 기본지식] - 바이트코드 조작(리플렉션, 다이나믹
jiwondev.tistory.com
추가적으로 DSL (Domain Specific Language) 라는 부분도 알게 되었다.
창발성
클린 코드에서 자주 등장하는 패턴이기도 하나, 신기한 패턴 같은 것이 있다.
흐름에 따라 함수를 만들어서 분리하는 것이다.
public void acrueEUDDivisionVacation() {
// 지금까지 근무한 시간을 바탕으로 휴가 일수를 계산하는 코드
// ...
// 휴가 일수가 유럽연합 최소 법정 일수를 만족하는지 확인하는 코드
// ...
// 휴가 일수를 급여 대장에 적용하는 코드
// ...
}
이런 형태의 코드를 아래와 같이 분리한다.
abstract public class vacationPolicy {
public void accruevacation() {
calculateBasevacationHours();
alterForLegalMinimums();
applyToPayroll();
}
private void calculateBasevacationHours() { /// }
abstract protected void alterForLegalMinimums();
private void applytoPayroll() { /// }
}
public class USVactionPolicy extends vacationPolicy {
@Override protected void alterForLegalMinimums() {
// 미국 최소 법정 일수
}
}
public class EUVactionPolicy extends vacationPolicy {
@Override protected void alterForLegalMinimums() {
// 유럽 최소 법정 일수
}
}
이런 식으로 분리를 해서,
public class VacationService {
public void processVacation(String countryCode) {
vacationPolicy policy;
if ("US".equals(countryCode)) {
policy = new USVacationPolicy();
} else if ("EU".equals(countryCode)) {
policy = new EUVacationPolicy();
} else {
throw new IllegalArgumentException("Unsupported region");
}
policy.accruevacation(); // 공통 로직 실행 + 지역별 커스터마이징 적용
}
}
국가의 법정 일수에 따라 policy를 다르게 적용할 수 있다.
국가에 따라, 해당 국가의 extends를 사용하도록 이용할 수 있다.
동시성
이전에 pintOS 과제를 하면서 공부했던 부분이어서 비교적 쉽게 이해할 수 있었다.
동시성의 개념에 대한 설명을 시작으로, 동시성 코드를 어떤 식으로 작성하면 좋을 지에 대해 배우게 된다.
책에서 여러번 강조하던 부분은 '동시성 코드는 다른 코드와 분리하라.' 이다.
SRP 원칙과 테스트의 용이성을 위해서이다. 스레드 코드를 테스트할 때는 전적으로 스레드만 테스트해야 한다.
또한, 스레드 환경을 구축할 때는 라이브러리 또한 유의해야 한다. 스레드 환경에 안전한 컬렉션을 사용하는 것도 중요하다.
라이브러리 중 스레드를 차단하는 라이브러리도 있으므로 이를 유의해서 선택해야 한다.
점진적인 개선1
해당 파트는 코드가 너무 길어서 집중해서 이해하기 힘들었다.
처음에는 흐름에 따라 코드를 작성하였다. 따라서, 맨 위 함수가 밑에 함수를 참조, 밑에 함수가 그 밑에 함수를 참조하는 식으로 흐름이 구성되었다.
이후로 개선된 코드는 interface를 이용하는 것이었다. ArgumentMarshaler라는 인터페이스를 작성한 후, Boolean, String, Integer, Double, String에 따라 해당 인터페이스를 implements 해서 구현하는 식으로 수정되었다.
이전과 개선된 점은 책임 분리가 명확하다는 것이다. Args는 무슨 인자인지만 알고, 값을 어떻게 해석할지는 마샬러에게 책임을 위임한다.
점진적인 개선2
이번에는 Args: 1차 초안에 대해 알아보았다.
눈에 띄는 점은, String, Boolean, Integer 마다 쓰는 함수가 다르다는 점이다.
이후에는 Boolean 인수만 지원하던 코드를 살펴보게 되었다.
해당 코드는 등록된 boolean 인자면 값을 true로 설정하고 그렇지 않으면 예외 인자로 판단해서 기록한다는 점이 다르다.
이후, String과 Integer에 대한 코드만 추가했음에도 확실히 코드는 복잡해졌다.
이에 리팩토링을 하기 시작했다.
private Map<Character, ArgumentMarshaler> booleanArgs = new HashMap<Character, ArgumentMarshaler>();
// map 의 변경
private class ArgumentMarshaler {
private boolean booleanValue = false;
public void setBoolean(boolean value) {
boolean = value;
}
public boolean getBoolean() {return booleanValue;}
}
// Argument Marshaler의 정의, set get
이렇게 정의하고 각 인자에 따라 extends를 하는 식으로 구성하였다.
ArgumentMarshaler를 인터페이스로 추상화하는 것이 이번 sector의 주된 작업이었다.
이제 이를 코드에 적용하기 시작하였다.
자료형에 따라 알맞은 ArgumentMarshaler의 구현체를 반환하기 시작하였다.
private boolean isIntArg(char argChar) {
ArgumentMarshaler m = marshalers.get(argChar);
return m instanceof IntegerArgumentMarshaler;
}
이를 모든 자료형에 대해서도 수정을 해주었다.
또한 Map으로 각 자료형에 대해서 처리를 해주었다면, 이제는 기존 자료형별 Map을 하나로 통합하였다.
private Map<Character, ArgumentMarshaler>
boolean, string, int 모두 위의 Map에 저장한다.
인상 깊었던 리팩토링 중 하나는 Iterator를 사용하는 것이다.
private abstract class ArgumentMarshaler {
public abstract void set(Iterator<String> currentArgument) throws ArgsException;
}
Iterator를 통해서 현재 어디 위치까지 파악했는 지를 넘겨주지 않아도 된다.
마지막으로 모든 에러 처리를 ArgsException에서 처리하도록 만들었다. 최종 코드를 바라보면서, 현재 내 코드에서 API 이름을 수정하는 게 좋겠다고 생각이 들었지만, 이미 FE에서 구현이 완료되어 수정을 못하는 것이 마음에 아프다.
JUnit 들여다보기
Junit을 리팩토링 하는 과정을 들여다보았다.
아래와 같이 여러 조건을 검사해야 하는 경우가 있었다. 이를 함수로 빼고 이름을 명시하여 가독성을 높였다.
if (expected == null || actual == null ...)
if (shouldNotCompact())
가장 인상 깊었던 부분은 부정문보다 긍정문을 사용하고자 한다는 점이다. 그 이유는 가독성에 있다.
또한, 함수의 호출 순서를 코드적으로 강제해서, 코드를 잘못 작성할 수 없게끔 수정하였다. 이는 나 말고 다른 개발자가 실수를 할 가능성을 없애는 행동이었다.
마지막으로 length와 index 변수를 둘 다 사용하고 있었는데, 하나로 통일함으로서 의미적으로 더 잘 통하도록 수정하였다.
'코딩 이야기' 카테고리의 다른 글
[Web Server] Proxy code (2) | 2024.10.28 |
---|---|
[web server] Dealing with static contents (0) | 2024.10.28 |
[RB Tree] 삭제 (1) | 2024.10.13 |
LCS ( Longest common substring, Longest Common subsequence) (0) | 2024.09.28 |
Knap Sack (2) | 2024.09.27 |