개발 공부/Clean Code

3. [Clean Code] 함수

위 사진은 우아한 테크코스에서 교육생들에게 내주는 과제의 조건이다. 사실 1주차 내용이기에 간단하지만 주차가 진행되면서 더 어려운 조건을 부여 받아서 진행하는 것 같다.

 

www.youtube.com/watch?v=bIeqAlmNRrA&t=1369s

 

그러면 과연 함수를 작성할 때 어떻게 작성해야 클린코드인건지 ...

 

책에서는 말하는 내용은 다음과 같다.

 

1. 작게 만들어라

함수가 길어지면 길어질수록 이해하기 힘들어진다, 그러니 짧게 만들라는 말이다.

짧게 만드는 방법은 여러가지일것이다. 테크코스처럼 intent를 최소화하고 else 문 대신 메서드를 분리해서 return을 사용하는 방식

심지어 책에서는 if문/else문, while문 등에 들어가는 블록을 한줄로 만들어야한다고 이야기한다.

 

2. 한 가지만 해라

이부분은 사실 1번의 작게 만들라는 이야기와 연결된다. 한가지만 하면 작게 만들기 보다 쉬워진다. 그런데 한가지의 정체가 무엇인가 뭘 한가지만 하라는건지에 대한 생각을 하게 한다.

책에서는 "지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한가지 작업만 한다." 라고 이야기한다.

 

3. 함수 당 추상화 수준은 하나로

한 가지만 하기 위해서 하나의 함수에서 하나의 추상화 단계만 수행하라고 했는데 그럼 추상화의 단계라는 게 무엇인가라는 의문이 들었다. 책에서는 

geHtml() 은 높고

String PagePathName = PathParser.render(pagepath); 는 중간

.append("\n"); 은 낮다

라고 표현했는데 이 차이가 무엇인가 ..

 

정의들을 찾아보면 

추상화란 중요한 특징을 찾아낸 후 간단하게 표현하는 것.

 

컴퓨터 공학에서의

추상화 계층의 아래쪽-구현 측면부터 살펴보면, 모델은 그에 해당하는 것을 구현할 때 그것이 어떤 특징들을 가지도록 만들어야 하는지에 대한 명세(specification)가 된다.

추상화 계층의 위쪽-활용 측면에서 살펴보면, 우리는 이 모델에 해당하는 것의 특징들이 어떠한지 알고 있으므로, 모델은 대상이 내부에서 어떻게 동작하는지의 세부사항을 모르더라도 다른 것을 만들 때 활용할 수 있게끔 하는 인터페이스(interface)의 역할

 

그렇다면 간단하게 이야기해서 추상화의 수준은 보고 명시적인가 아닌가의 수준정도가 아닐까

 

책에서 부가적인 설명을 내려가기 규칙을 보면 코드는 아래로 이야기처럼 읽혀야하며 한 함수 다음에는 추상화 수준이 한단계 낮은 함수가 온다고 설명한다. 점차 함수가 구체화 된다는 것으로 함수의 흐름을 따라가면 정확히 뭘하는지 명확해지는 것이다.

 

그럼 다시 책에서 수준을 말해줬던 3가지를 보자

geHtml() -> html을 넘겨준다는건가?

String PagePathName = PathParser.render(pagepath); -> 파서를 통해서 특정 페이지의 랜더 값을 문자열로 주는건가?

.append("\n"); -> Stringbuffer의 메서드로 append는 추가한다는 의미로 문자열에 줄바꿈을 추가한다.

 

append가 getHtml보다 보다 명시적이지 않은가 이런 차이를 의미하는 것으로 이해 하였습니다. 

이개념에 대해서 제가 이해한 부분이 틀릴수 있어서 다른 자료들을 찾아보시길 추천드립니다.

 

4. switch 문

이 부분은 책이 명시적으로 설명해줘서 이해하기 쉬운데 

public Money calculatePay(Employee e) throws InvalidEmployeeType {
    switch (e.type) {
        case COMMISSIONED :
        	return calculateCommissionedPay(e);
        case HOURLY :
        	return calculateHourlyPay(e);
        case SALARIED :
        	calculateSalariedPay(e);
        default :
        	throw new InvaliedEmployeeType(e.type);
    }
}

위의 코드는 4가지 정도의 문제를 가진다.

1) 함수가 길다.

2) 한 가지 작업만 수행하지 않는다.

3) SRP 위반

4) OCP 위반

새직원 유형이 추가될때마다 직원은 길어지고 코드를 수정해야한다는 문제가 있다.

 

해결은switch문은 다형적 객체를 생성하는 코드 안에서만 사용
switch문을 추상 팩토리(Abstract Factory)에 숨기고, 팩토리는 switch문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성한다.

isPayday,deliverPay,calculatePay 등과 같은 함수는 Employee 인터페이스를 거쳐 호출된다.

public abstract class Employee {
	public abstract boolean isPayday();
	public abstract Money calculatePay();
	public abstract void deliverPay(Money pay);
}

public interface EmployeeFactory {
	public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType; 
}

public class EmployeeFactoryImpl implements EmployeeFactory {
	public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
		switch (r.type) {
			case COMMISSIONED:
				return new CommissionedEmployee(r) ;
			case HOURLY:
				return new HourlyEmployee(r);
			case SALARIED:
				return new SalariedEmploye(r);
			default:
				throw new InvalidEmployeeType(r.type);
		} 
	}
} 

다만 이경우에도 새 직원 유형이 추가된다면 코드 수정은 불가피하다.

 

5. 서술적 이름을 사용해라

말그대로 이다 길어도 상관없으니 함수가 하는일을 좀더 잘 표현하도록 서술적인 이름을 사용해라.

또한 이름을 붙일 때는 일관성 있어야한다.

 

6. 함수 인수

함수의 인수는 적울수록 좋으면 3개는 되도록 피하는게 4개 이상은 사용하지 않는 것이 적절하다.

함수와 인수 사이의 추상화의 수준 차이, 코드를 읽는 사람이 현 시점에서 별로 중요하지않은 인수의 정보를 알아야한다.

인수가 많은수록 테스트 하기 힘들어진다. 

  • 단항 형식
    • 인수에 질문을 던지는 경우
      • boolean fileExists("MyFile")
      • InputStream fileOpen("MyFile")은 인수를 뭔가로 변환해 결과를 반환한다.
    • 이벤트 함수는 이벤트라는 사실이 코드에 명확히 나타나야 한다.
      • passwordAttemptFailedNtimes(int attempts)
  • 플래그 인수 (bool)
    • 플래그 인수는 추하다. 대놓고 여러 가지를 처리하는 함수라고 공표하는 셈이니까! 이럴땐 함수를 나눠야 마땅하다.
  • 이항 함수
    • 인수가 1개인 함수보다 이해하기 어렵다.
    • 2개의 인수가 한 값을 표현하는 두 요소는 괜찮다.
      • Point p = new Point(0,0)
    • 인수 간의 자연적인 순서가 있지 않으면 인위적으로 기억해야 한다.
      • assertEquals(expected, actual)
  • 삼항 함수
    • 인수가 2개인 함수보다 훨씬 더 이해하기 어렵다.
  • 인수 객체
    • 인수가 2~3개가 필요하다면 클레스 변수로 선언할 가능성을 집어본다.
    • Circle makeCircle(Point center, double radius)
  • 인수 목록 (가변 인수)
    • 가변 인수도 의미에 따라 단항, 이항, 삼항으로 취급할 수 있다. 하지만 이를 넘어서는 인수를 사용할 경우에는 문제가 있다.
  • 동사와 키워드
    • 함수의 의도와 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다.
      • write(name) : 이름이 무엇이든 쓴다.
      • writeField(name) : name이 Field라는 사실을 알 수 있다.
      • assertExpectedEqualsActual(expected, actual) : 인수의 순서를 기억할 필요가 없어진다.

 

7. 부수효과를 일으키지 마라

함수에서 한가지를 하겠다고 약속하고서 남몰래 다른 것을 하는것을 의미한다.

public class UserValidator {
	private Cryptographer cryptographer;
	public boolean checkPassword(String userName, String password) { 
		User user = UserGateway.findByName(userName);
		if (user != User.NULL) {
			String codedPhrase = user.getPhraseEncodedByPassword(); 
			String phrase = cryptographer.decrypt(codedPhrase, password); 
			if ("Valid Password".equals(phrase)) {
				Session.initialize();
				return true; 
			}
		}
		return false; 
	}
}

 

userName과 password가 vaild 한지를 체크하는 함수에서 Session.initialize()를 통해서 세션을 날려버리는 부수효과를 발생시킨다. 이름만 봐서는 세션을 초기화하는지 모르기 때문에 이름만 보고 함수를 사용하는 사용자의 세션을 의도하지않게 날리게 된다.

 

8. 명령과 조회를 분리하라

함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다. 둘 다 하면 안된다.

public boolean set(String attribute, String value);

if (set("username", "unclebob")) ...

사실 위 처럼 쓰는 사람이 있을까 싶긴하다...

if (attributeExists("username")) {
    setAttribute("username", "Luna");
    ...
}

 

9. 오류 코드보다 예외를 사용하라

명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다. 자칫하면 if 문에서 명령을 표현식으로 사용하기 쉬운 탓이다

if (deletePage(page) == E_OK) {
	if (registry.deleteReference(page.name) == E_OK) {
		if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {
			logger.log("page deleted");
		} else {
			logger.log("configKey not deleted");
		}
	} else {
		logger.log("deleteReference from registry failed"); 
	} 
} else {
	logger.log("delete failed"); return E_ERROR;
}

위 코드는 여러 단계로 중첩되는 코드를 야기한다. 오류 코드를 반환하면 호출자는 오류 코드를 바로 처리해야 하지만, 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다. try/catch불록은 코드 구조에 혼란을 일으키며 정상 동작과 오류 처리 동작을 뒤섞는다. 그러므로 try/catch 블록을 별도 함수로 뽑아내는 편이 좋다.

public void delete(Page page) {
    try {
        deletePageAndAllReferences(page);
    } catch (Exception e) {
        logError(e);
    }
}

private void deletePageAndAllReferences(Page page) throws Eception { 
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
}

private void logError(Exception e) { 
    logger.log(e.getMessage());
}

위의 delete 함수는 모든 오류를 처리하여 코드를 이해하기 쉽게 된다.

오류 처리도 한가지 작업으로 따로 메서드로 빼는 것이 올바르다.

 

10. 반복하지 마라

반복되는 알고리즘이 보인다면 제거하여 가독성을 높혀야 한다.

AOP, COP도 어떤면에서는 중복 제거 전략이라고 볼수 있다.

 

11 구조적 프로그래밍

단일 입/출구 규칙은 작은 함수에서는 별 이익을 제공하지 않는다. 함수를 작게 만들경우 return, break, continue를 여러 차례 사용해도 괜찮다.

 

'개발 공부 > Clean Code' 카테고리의 다른 글

코드 리뷰 조금 이해하기  (1) 2021.07.02
4.[Clean Code] 주석  (0) 2021.04.09
2. [Clean Code] 의미 있는 이름  (0) 2021.04.07
1. [Clean Code] 깨끗한 코드  (0) 2021.04.07
Clean Code 책 정리 시작  (0) 2021.04.06