[Clean Code] 3. 함수

9577 단어 CLEAN CODECLEAN CODE

들어가며

최근 RxSwift를 사용하는 프로젝트를 진행한 경험이 있는데, 최대한 Side Effect 가 없도록 함수를 설계하려고 노력을 했었다. 그리고 코드 리뷰를 진행하며, 함수를 어떤 단위로 쪼갤지... 그리고 함수 이름, 전달인지 이름에 대해 많은 이야기를 나눴는데, 클린 코드에서는 이에 대한 어떤 철학을 가지고 있을지 궁금하다.

1. 작게 만들어라!

함수를 만드는 첫 번째 규칙도, 두 번째 규칙도 "작게 만들어라!"이다. 함수를 작게 만들면, 하나의 역할만을 할 것이고, 그러면 명료해집니다. 그래서 최대한 작게 만들고, 블록이 깊어질 경우에는 함수 분리를 해줘야 합니다.

항상 함수를 설계할 때도 재사용성을 높이기 위해, 함수 네이밍에 맞는 기능만 포함시켜 명료하게 하기 위해 생각을 많이 하는 편입니다. 하지만, 복잡한 로직들이 있을 경우에는 정말 지키기 힘들었습니다. 그럴 경우에는 무리하게 함수를 쪼갤려고 하지 않고 천천히 멀리서 다시 보면서 리팩토링을 하면 더 좋은 구조로 함수가 만들어지는 것 같습니다.

2. 한 가지만 해라!

저자가 말하기로는 지난 30여년 동안 여러 가지 다양한 표현으로 개발자들에게 주어진 충고가 있다고 한다.

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

위에서도 언급했듯이 함수는 한 개의 역할만을 수행해야지 명료함을 확실하게 할 수 있습니다. 그렇다면 여기서 가장 중요한 것은 "한 가지의 역할"을 어떤 기준으로 정하는 것인가 입니다. 책에서 말하는 한 가지란 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행하는 것을 의미합니다.

음... 알아듣기 힘든데, 함수가 "한 가지"만 하는지 판단하는 방법이 하나 더 있습니다.. 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈입니다.

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

함수가 확실히 "한 가지" 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 합니다. 한 함수 내에서 추상화 수준을 섞으면 읽는 사람이 혼란스럽습니다. 또 근본 개념과 세부 사항이 섞이기 시작하면 사람들이 함수에 세부사항을 점점 더 추가하게 됩니다.

또한, 추상화 수준이 높은 함수는 위에, 추상화 수준이 낮은 함수는 아래에 작성해야 한다. 이를 내려가기 규칙이라 부른다.

4. Switch 문

Switch 문의 경우는 작게 만들기 어렵습니다. 본질적으로 switch 문은 N가지 일을 처리합니다. 자연스럽게 길어질 수 밖에 없죠. 그리고 아래 문제들이 발생합니다.

  • 함수가 길어진다.
  • 한 가지 작업만 수행하지 않는다.
  • SRP(Single Responsibility Principle) 을 위반한다.
  • OCP(Open Closed Principle)을 위반한다.

저자의 경우는 switch 문을 추상 팩토리에 꽁꽁 숨기며, 다형적 객체를 생성하는 코드 안에서만 사용한다고 합니다.

Swift 에서는 Switch 문을 enum 타입과 함께 사용하기 편하게 되어 있어, 이 부분은 Swift에서 어떻게 해석해야 될 지 모르겠네요. 다만 저자가 의도한 대로 위 문제들이 발생하는 것에 대해서는 동의합니다. 특히 enum 타입의 case가 추가될 가능성이 높은 경우에 Switch 문을 많이 사용할 경우 수정해야되는 영역이 넓어져서 불편했던 기억들이 많습니다. 상황에 맞게 사용해야되는 분기처리 구문인 것 같습니다.

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

"코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다." 라는 말이 있었습니다.

서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워집니다. 좋은 이름을 고른 후 코드를 더 좋게 재구성하는 사례도 없지 않습니다.

Swift API Guidelines 를 보면 function 네이밍 그리고 전달인자 네이밍을 정하는 것에 대해 소개를 할 때 "한 문장처럼 읽히도록 합니다." 라는 내용이 정말 정말 강조되고 있습니다. 때에 따라 다르지만 전치사를 전달인자로써 잘 사용하면 해당 메서드의 기능을 정말 잘 표현할 수 있었습니다.

6. 함수 인수

함수의 이상적 parameter 의 개수는 0개 입니다. 다음이 1개, 그 다음이 2개입니다. 가능한 3개는 피하는 게 좋습니다. 4개 이상은 특별한 이유가 있어야 합니다.

  • 인수가 많을 수록 함수를 파악하는데 시간이 더 오래 걸린다.
  • 테스트 관점에서 보면 인수는 더 어렵습니다. 갖가지 인수 조합으로 함수를 검증하는 테스트 케이스를 만들어야 하기 때문입니다.

UIKit 에서 사용되는 함수만 봐도 인수가 굉장히 많습니다. 4, 5개가 넘는 함수를 많이 봤습니다. 인수가 많을 수록 테스트를 작성하기 힘들다는 점은 동의하지만, 비동기 작업이 많아 함수형 프로그래밍 패러다임의 장점이 크게 부각되는 iOS 에서는 순수함수를 만들기 위해서 지켜내기 힘들다고 생각합니다.

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

많은 경우 시간적인 결합(temporal coupling)이나 순서 종속성(order dependency)을 초래한다고 합니다. 즉, 메서드 자체로는 문제가 없어 보이지만 메서드가 다른 메소드나 객체와 시기적으로 연관이 있거나 정해진 순서가 있는 경우 원래 메소드의 목적 이외의 영향을 줄 수 있다는 것입니다. 가장 흔하게 일어나는 경우는 어떤 메소드가 특정 cache를 clear하는 기능을 담고 있을 때 이 메소드 자체에서는 문제가 없지만 순서 문제로 다른 메소드가 뜻하지 않게 계속 빈 cache만 받을 수도 있다는 겁니다.

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

함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 합니다. 둘 다 하면 안됩니다. 둘 다 할 경우 혼란을 초래하게 됩니다. set(attribute: String, value: String) -> Bool 이라는 메서드가 있다면,

if (set(attribute: "username", value: "unclebob")

위와 같이 사용되게 됩니다. 정말 어색하죠. 이 함수가 무엇인가를 설정하는 함수인지, 확인하는 함수인지 헷갈립니다. 이런 혼란을 피하기 위해선 아래처럼 명령과 조회를 분리해야 합니다.

if attributeExists(attribute: "username") {
	setAttribute(attribute: "username", value: "unclebob")
}

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

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

오류 코드 대신 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 훨씬 깔끔하게 됩니다.

try {
	deletePage(page)
    registry.deleteReference(page.name)
    configKeys.deleteKey(page.name.makeKey)
} catch {
	logger.log(e.gotMessage())
}

"try/catch 블록은 원래 추하다"

정상 동작과 오류 처리 동작을 뒤섞는다. 그러므로 try/catch 블록을 별도 함수로 뽑아내는 편이 좋다.

아래처럼 분리하는 것이 좋다

func delete(page: Page) {
	try {
    	deletePageAndAllReferences(page: page)
    } catch {
    	logError(error)
    }
}

private func deletePageAndAllReferences(page: Page) throws {
	deletePage(with: page)
    registry.deleteReference(page.name)
    configKeys.deleteKey(page.name.makeKey())
}

private func logError(_ error: Error) {
	logger.log(error.description)
}

함수는 "한 가지 일만 해야 된다"라는 규칙을 지킨 것이라고 볼 수 있겠네요

10. 반복하지 마라

코드 길이가 늘어날 뿐 아니라 로직이 변할 경우 한 곳이 아닌, 여러 곳을 수정해야 되기 때문에 유지보수성도 좋지 않을 뿐더러, 오류가 발생할 확률도 중복되는 만큼 n배가 됩니다.

11. 함수를 어떻게 짜죠?

프로그래밍은 글을 짓는 것과 다를게 없다. 논문이나 기사를 작성할 때는 먼저 생각을 기록한 후 읽기 좋게 다듬습니다.

함수도 똑같습니다. 처음 짤 때는 길고 복잡하며, 들여쓰기 단계도 많고 중복된 루프도 많습니다. 하지만 나는 그 서투른 코드를 빠짐없이 테스트하는 단위 테스케이스를 만듭니다.

그런 다음 코드를 다듬고, 함수를 쪼개고 중복을 제거하며 지금 껏 추구하고자 했던 것을 하나씩 이루면 됩니다.

좋은 웹페이지 즐겨찾기