[CleanCode] -3. 함수

Young Min Sim ·2021년 4월 12일
0

CleanCode

목록 보기
3/16
post-custom-banner

함수

  • 어떤 프로그램이든 가장 기본적인 단위는 함수

💁🏻‍♂️ 의도를 분명히 표현하는 함수를 어떻게 구현할 수 있을까?

1. 작게, 더 작게 만들어라

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();
}

를 더 줄인 코드가 아래 코드.
아래 코드는 더 이상 나누기 힘듦

public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception {
    if (isTestPage(pageData))
        includeSetupAndTeardownPages(pageData, isSuite);
    return pageData.getHtml();
}
  • if 문, while 문 등에 들어가는 블록은 한 줄이어야 한다.
    블록 안에서 호출하는 함수이름을 적절히 짓는다면 코드를 이해하기도 쉬워짐

책에서는 함수를 작게 만들어야 하는 이유에 대해 구체적인 근거는 없다고 한다.
경험적으로 봤을 때 하나의 함수에 여러 가지 추상화 수준, 여러가지 의미가 있는 것보다는
하나의 의미만 존재하는 것이 함수를 이해하기 쉽게 만든다고 생각.


2. 한 가지만 해라

  • 발생하는 문제: 도대체 그 한 가지가 무엇인지? 어떻게 판단?
  • 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 하는 것
    즉, 의미를 유지하면서 더 이상 줄이기 힘들 때 까지 함수를 나눠라

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

  • 한 함수 내에 여러 추상화 수준이 존재하는 코드를 섞으면 읽는 사람이 헷갈린다.
  • 내려가기 규칙

    '코드는 위에서 아래로 이야기처럼 읽혀야 좋다'

    • 각 함수는 다음 함수를 소개, 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.
    • 동시에 각 함수는 일정한 추상화 수준을 유지한다.
...

public static String render(PageData pageData) throws Exception {
	return render(pageData, false);
}

public static String render(PageData pageData, boolean isSuite) throws Exception {
	return new SetupTeardownIncluder(pageData).render(isSuite);
}

...

4. Switch 문

switch 문은 본질적으로 (한 가지가 아닌) 여러 가지를 처리하고 작게 만들기도 어렵다.

  func calulatePay(e: Employee) -> Money {
  	switch e.type {
    	case COMMISSIONED:
        	return calulateCommissionedPay(e)
        case HOURLY:
        	return calculateHourlyPay(e)
        case SALARIED:
        	return calculateSalariedPay(e)
    }
  }
  • 위 코드의 문제
    • 새 case 를 추가할 때마다 길어진다.
    • SRP 위반
    • OCP 위반(새 직원 추가할 때마다 기존 코드를 변경)
    • 만약 동일한 구조를 가지는 함수가 무한정 존재한다면?
      ex) isPayday(e: Employee, date: Date), deliverPay(e: Employee, pay: Money)

이러한 switch 문을 완전히 피할 방법은 없다.
하지만, 이를 개선할 방법은 존재한다.

  • switch 문을 추상 팩토리에 숨기고
    팩토리는 switch 문을 이용해 적절한 파생 클래스의 인스턴스를 생성하여 반환
protocol Employee {
    func isPayday() -> Bool
    func calculatePay() -> Money
    func deliverPay(pay: Money)
}

protocol Employee {
	func makeEmployee(r: EmployeeRecord) -> Employee
}

class EmployeeFactory implements EmployeeFactory {
	func makeEmployee(r: EmployeeRecord) {
	   switch r.type {
    	     case COMMISSIONED:
        	return calulateCommissionedPay(r)
            case HOURLY:
        	return calculateHourlyPay(r)
            case SALARIED:
        	return calculateSalariedPay(r)
        }
    }
}
  • 기존에는 여러 곳에서 반복적으로 사용해야 했던 switch 문을 숨긴 후
    다른 코드에 노출하지 않고 재사용할 수 있다는 점,
  • 변경으로 인한 부작용을 최대한 줄일 수 있다는 점이 장점이라고 생각

  • 개인적으로는 모든 switch 문을 이렇게 한다기보다는
    예제와 같이 Employee 와 관련된 타입을 이용하는 switch 문이 여러 곳에서 중복되어 사용될 가능성이 보이는 경우에 위와 같이 리팩토링 하는게 맞지 않나 생각

5. 서술적인 이름을 사용하라

  • 함수 이름이 길어도 겁 먹을 필요 없다. 길고 서술적인 이름이 길고 짧고 어려운 이름보다 좋다.
  • 서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.
    좋은 이름을 고른 후 코드를 더 좋게 재구성하는 사례도 적지 않다.

6. 함수 인수

  • 함수에서 이상적인 인수의 개수는 0개(무항). 그 다음 1항, 2항.
  • 3항은 가능한 피하는 것이 좋음
  • 단항 형식 함수에서 입력 인수는 O, 출력 인수는 X
  • 일반적으로 인수를 입력으로 인식하기 때문에 출력 인수를 쓰는 것은 헷갈릴 수 있다.
    ex) appendFooter(s) 를 봤을 때 어떻게 해석할 수 있을까?
    • 무언가에 s 를 바닥글로 첨부 vs s에 바닥글을 첨부
    • 직관적으로 봤을 때 전자로 해석된다. 따라서 후자로 만들고 싶은 경우는 s.appendFooter() 가 더 적절함.

객체의 역할면에서 봤을 때도 s 에 Footer 를 추가하는건 s 의 책임이지, s 를 사용하는 상위 모듈이 맡을 책임이 아니라고 생각
재사용 측면에서도 s.appendFooter() 가 더 적절. (p.56)

  • 인수 객체
    • 인수가 2~3개 이상 필요하다면 독자적인 클래스 변수를 고려해봄직 함.
    • x, y 가 point 라는 하나의 개념이므로 묶을 수 있음
    func makeCircle(x: Double, y: Double, radius: Double)
    func makeCircle(center: Point, radius: Double)
  • 동사와 키워드
    • 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다. (== 문장처럼 읽혀야 한다)
      ex) func write(name)
      • 더 나아가서 func writeField(name) 이라고 하면 name 이 field 임을 알 수 있다.

        swift에서는 func write(field: name)


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

  • 함수에서 한 가지를 하겠다고 약속하고선 남 몰래 다른 짓 까지 하는 것 == 부수효과
  • 아래 예제의 함수 이름만 읽으면 암호를 확인하는 메서드로만 보이지만, 세션을 초기화는 역할까지 담당
  • 즉, 함수 이름만 보고 함수를 호출하면 의도치 않게 사용자의 세션 정보가 날아갈 수 있음
  • 이런 식으로 시간적인 결합, 순서에 종속성이 있는 경우
    checkPasswordAndInitializeSession 같은 방식으로 수정하여 결과를 예측할 수 있도록 해야 한다
    (물론 여전히 함수가 한 가지 일만 한다는 규칙은 위반하고 순서에 종속성이 있다는 단점이 존재)
    class UserValidator {
    	private let cryptographer: Cryptographer
        
        func checkPassword(userName: String, password: String) -> Bool {
          let user = UserGateway.findByName(userName)
          if user != nil {
              let codedPhrase = user.getPhraseEncodedByPassword()
              let phrase = cryptographer.decrypt(codedPhrase, password)
              if phrase == "Valid Password" {
                  Session.initialize()
                  return true
              }
           }
           return false
        }
    }

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

  • 함수는 뭔가를 수행(객체 상태를 변경) or 뭔가에 답하거나(객체 정보를 반환) 둘 중 하나만 해야 한다.
    둘 다 하면 혼란을 초래한다.
func set(_ attribute: String, _ value: String) -> Bool
  • 이 함수는 이름이 attribute 인 속성을 찾아 값을 value 로 설정한 후
    성공하면 true를 반환하고 실패하면 false 를 반환하는 함수인데, 이 때문에 아래와 같은 코드가 나와버림.
if set("username", "unclebob")  { }
  • 이 코드를 처음 읽는 사람 입장에선 함수 내에서 정확히 어떤 일들이 일어나는지 한 번에 추측하기 쉽지 않음.
  • 순서에 종속성이 생긴다는 문제 또한 존재.
  • 따라서 아래와 같이 명령/조회를 분리하면 문제를 해결할 수 있다.
if attributeExists("username") {
	setAttribute("username", "unclebob")
    ...
}

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

  • 명령문임과 동시에 조회까지 하는 것처럼 사용된다는 점을 지적. 그리고 코드가 복잡해짐.
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 {
    deletePage(page)
    registry.deleteReference(page.name)
    configKeys.deleteKey(page.name.makeKey())
} catch {
	// 예외 처리
}
  • try 블럭과 catch 이하 블럭을 함수로 모듈화
  • delete 함수가 예외를 처리하고 실제로 페이지를 제거하는 주요 로직을 수행하는 함수는 deletePageAndAllReferences 다.
func delete(page: Page) {
  try {
     deletePageAndAllReferences(page)
  } catch {
	// 예외 처리
  }
}
  • 오류 처리도 한 가지 작업이다.
    - 함수는 한 가지 작업만을 해야 한다. 오류 처리도 한 가지 작업에 속한다.
    • 따라서 오류를 처리하는 함수는 오류만 처리해야 마땅하다.

10. 반복하지 마라

'어쩌면 중복은 소프트웨어에서 모든 악의 근원이다'

  • 중복을 없애면 모듈의 가독성이 크게 높아진다.
  • 많은 원칙과 기법이 중복을 없애거나 제어할 목적으로 나왔다.
    ex) OOP에서는 코드를 부모 클래스로 몰아 중복을 없앤다.

중복을 없애기 위한 성급한 추상화는 주의해야 할 것


11. 함수를 어떻게 짜죠 ?

글짓기와 비슷하다.

  • 처음에는 길고 복잡하다. 인수 목록도 길고 이름도 즉흥적이고 코드도 중복된다.
  • 하지만 그 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스를 만든다.
  • 코드를 다듬고, 함수를 만들고, 중복을 제거하는 등의 리팩토링을 진행하는 중에도 코드는 항상 단위테스트를 통과한다.
  • 처음부터 완벽한 함수를 짜는 건 불가능하다.

결론

'프로그래밍 기술은 언제나 언어 설계의 기술'
대가 프로그래머는 시스템을 구현할 프로그램이 아니라 풀어갈 이야기로 여긴다.

이 장에서는 함수를 잘 만드는 기교를 소개했다. 길이가 짧고 이름이 좋고, 체계가 잡힌 함수를 위해.
하지만 진짜 목표는 시스템이라는 이야기를 풀어나가는 데 있다는 사실을 명심
함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아 떨어져야 이야기를 풀어나가기 쉬워질 것.

개인적으로 드는 생각

  • 함수명만 보고서는 정확한 의도를 알 수 없는, 나만 아는 코드를 많이 짰던 것 같다.
    이제부터 함수명과 파라미터만 봐도 의도가 좀 더 명확히 보이는 함수를 작성하기 위해 노력해야겠다.

  • 기존에 함수를 나누는 이유에 대해 중복을 제거하기 위함이라고만 생각해왔는데,
    추상화 수준을 일정케하여 의도를 명확하게 보여주고자 하는 것(가독성)도 있다는 사실을 깨달았다.

post-custom-banner

0개의 댓글