[클린코드] 함수의 추상화 수준, 그리고 Switch 문

Jake·2022년 2월 21일
0

책 읽기

목록 보기
1/5
post-thumbnail

[3장] 함수 중에서...

1. 작게 만들어라!

  • 얼마나 작게? → 가로 150자, 세로 20줄

    가로 150자를 넘어서는 안 된다. 함수는 100줄을 넘어서는 안 된다. 아니 20줄도 길다

  • 중첩 구조가 생길만큼 함수가 커져서는 안 된다.
    들여쓰기 수준은 1단이나 2단을 넘어서면 안 된다.

2. 한 가지만 해라!

함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다

  • 추상화 수준
    //목록 3-2, from p.42
    public static String renderPageWithSetupsAndTeardowns{
    	PageData pageData, boolean isSuite
    ) throws Exception {
    	boolean isTestPage = pageData.hasAttribute("Test");
    	if (isTestPage) {
    		WikiPage testPage = pageData.getWikiPage();
    		StringBuffer newPageContent = new StringBuffer();
    		includeSetupPages(testPage, newPageContent, isSuite);
    		newPageContent.append(pageData.getContent());
    		includeTeardownPages(testPage, newPageContent, isSuite);
    		pageData.setContent(newPageContent.toString());
    	}
    	return pageData.getHtml();
    }
    //목록 3-3, from p.43
    public static String renderPageWithSetupsAndTeardowns{
    	PageData pageData, boolean isSuite) throws Exception {
    	if (isTestPage(pageData))
    		includeSetupAndTeardownPages(pageData, isSuite);
    	return pageData.getHtml();
    }

    지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.
    ...(중략)
    목록 3-2도 추상화 수준이 둘이다. 그래서 목록 3-3으로 축소가 가능했다. 하지만 의미를 유지하면서 목록 3-3을 더 이상 줄이기란 불가능하다.

    • 목록 3-2과 3-3의 차이점은 if문 내부입니다.
    • renderPageWithSetupsAndTeardowns() 메서드는 다음과 같은 기능을 하는데
      1. 페이지가 테스트 페이지인지 확인 한 후
      2. 테스트 페이지라면 SetupPage와 TeardownPage를 넣습니다.
      3. if문과 관계 없이, 페이지를 html로 렌더링하여 반환합니다.
    • 따라서 2번에 해당하는 내용을 본 메서드인 renderPAgeWithSetupsAndTeardown에서 구현하는 것은 책에서 이어 서술하는 함수 당 추상화 수준은 하나로! 라는 내용에 위배됩니다. 목록 3-2는 마치 다음과 같은 것이죠
      1. 페이지가 테스트 페이지인지 확인 한 후

      2. 테스트 페이지라면

        1. create testPAge
        2. create newPageContent
        3. includeSetUpPages
        4. append
        5. includeTeardownPages
        6. setContent
      3. if문과 관계 없이, 페이지를 html로 렌더링하여 반환합니다.

        이렇게 보면, a ~ f에 해당하는 내용을 함수로 추상화해야 한다는 것을 확실히 느낄 수 있습니다.

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

  • 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다.
  • 내려가기 규칙 : 위에서 아래로 코드 읽기
    • 코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
    • 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.
    • 핵심은 짧으면서도 ‘한 가지'만 하는 함수다!

4. Switch 문

  • switch 문은 작게 만들기 어렵다.
  • switch 문은 본질적으로 N가지를 처리하기 때문에, 한 가지 작업만 하도록 만들기 어렵다.
  • 따라서 다형성을 적극 이용하여 반복을 저차원 클래스에 숨겨야 한다.
  • 다음 코드에는 크게 네가지 문제점이 있다
public Money calculatePay(Employee e) throws InvalidEmployeeType{
	switch (e.type) {
		case COMISSIONED:
			return calculateComissionedPay(e);
		case HOURLY:
			return calculateHourlyPay(e);
		case SALARIED:
			return calculateSalariedPay(e);
		default:
			throw new InvalidEmployeeType(e.type);
	}
}
  • 무슨 문제가 있는 것일까요?
    1. 함수가 길다
    → 20줄이 넘어가는 수준은 아니지만, 짧을 수록 좋다는 책의 맥락에서 보면 길다고도 볼 수 있을 듯 합니다.

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

    → case 안에 이미 세 개의 다른 메서드가 포함된 것을 확인할 수 있습니다.

    → 이 부분은 switch 문의 태생이 그렇기 때문에 어쩔 수 없는 것이 아니냐고 하실 수도 있습니다. 저자도 이 부분에 대해서 인지하고 있기에, 책의 이어지는 페이지에서 다음과 같이 말합니다.

    *일반적으로 나는 switch 문을 단 한 번만 참아준다. 다형성 객체를 생성하는 코드 안에서다.
    ...(중략)
    물론 불가피한 상황도 생긴다. 나 자신도 이 규칙을 위반한 경험이 여러 번 있다.

    저자까지 이렇게 말 할 정도면, 정말 어쩔 수 없는 부분이긴 한 것 같습니다 :(

    3. SRP를 위반한다. 코드를 변경할 이유가 여럿이기 때문이다.

    → 조금 어렵습니다. 코드를 변경할 이유가 왜 여럿일까요?

    → 만약 계산 할 때, 다른 여러 조건을 고려해야 하게 된다면 어떻게 될까요? 예를 들어 Employee의 근무 일수를 따져서 계산해야 할 수가 있습니다. 이런 경우에 위 코드에서는 calculatePay() 메서드의 변경이 불가피하겠죠.

    → 코드가 calculateXXXPay() 라는 구체적 메서드에 의존하고 있기 때문에 이러한 현상이 발생합니다. 이는 이어지는 OCP 관련 내용과도 연관이 있습니다.

    4. OCP를 위반한다. 새 직원 유형을 추가할 때마다 코드를 변경하기 때문이다

    → 직관적으로 이해 가능한 문제입니다. 동시에, 해결할 수 없는 문제이기도 합니다.

    5. 가장 심각한 문제는, 위 함수와 구조가 동일한 함수가 무한정 존재한다는 것.

    → isPayday(Employee e, Date date), deliverPay(Employee e, Money pay) 등의 함수를 써야 하는 케이스가 발생한다면, 이 함수들 내에 동일한 switch 문을 작성해야 합니다. 이는 개발자에게 악몽과도 같은 일이죠...ㅎ;

  • 해결책? 추상화와 다형성!

    • calculatePay() 함수의 가장 큰 문제점은 switch 문을 통해 직원 유형을 파악한 뒤, 구체적인 메서드를 실행한다는 점입니다. 이에 따라 변화에 유연하게 대응하지 못하는 코드가 되는 것입니다.

    • 이럴 때 Java 진영에서는 추상화를 통해 문제를 해결합니다.

      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 InvaludEmployeeType;
      }
      
      //
      
      public class EmployeeFactoryImpl implements EmployeeFactory {
      	public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
      		switch (r.type) {
      			case COMISSIONED:
      				return new ComissionedEmployee(r);
      			case HOURLY:
      				return new HourlyEmployee(r);
      			case SALARIED:
      				return SalariedEmployee(r);
      			default:
      				throw new InvalidEmployeeType(e.type);
      		}
    • 이렇게 하면, makeEmployee를 통해 생성받은 객체에서 isPayday(), calculatedPay() 등의 메서드를 실행하면 다형성으로 인해 실제 파생 클래스의 함수가 실행된다!

profile
Java/Spring Back-End Developer

0개의 댓글