클린코드(Clean Code) 3장 (swift ver.)

kms0524·2021년 11월 15일
0

Clean Code

목록 보기
2/2

3장 함수

프로그래밍의 초창기에는 시스템을 루틴과 하위루틴으로 나누었다. 이후엔 프로그램, 하위 프로그램, 함수로 나누었다. 이제는 함수만이 살아남았다. 모든 프로그램에서 함수는 가장 기본적인 단위가 되었다. 이제 우리는 함수를 다룰 것이다.

작게 만들어라!

함수를 만드는 첫째 규칙은 ‘작게!’ 이다. 두번쨰 규칙은? ‘더 작게!’ 이다. 근거나 증거? 그런건 없다! 단, 100줄, 500줄, 1000줄이 넘어가는 함수를 작성하다보면 자연스럽게 더 작은 함수가 좋다고 확신이 들게된다. 그러면, 얼마나 작아야 하는걸까? if / else / while 안에 들어가는 블록은 한 줄이어야 가장 이상적이다. 대부분, 그곳에서 함수를 호출하게 되고, 바깥을 감싸는 함수가 작아지게 된다. 또, 이전장에서 했던것처럼 이름을 적절히 짓는다면 코드를 이해하기 더욱더 쉬워지게된다. 정리하자면, 중첩구조가 생기는 만큼, 함수가 커져서는 안된다는 말이다. 그러므로 들여쓰기 수준은 1단, 2단을 넘어서면 안된다.

한 가지만 해라!

대부분의 정리가 안된 코드는 하나의 함수안에서 여러가지를 처리한다. 이 여러가지를 처리하느라, 함수가 엄청 바쁘게 돌아가게 되는데, 정리가 된 코드는 함수는 단 한가지만 처리를 하게된다. 아래 문구는 이번 장에서 가장 중요한 문구이다.

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

여기서, ‘한 가지’ 는 무엇일까? 그 한가지는 저장된 함수 이름 아래에서 추상화 수준이 하나인 단계를 수행한다는 것이다. 단순히 다른 표현이 아닌, 의미 있는 이름으로 해당 함수에서 다른 함수를 추출할 수 있다면 그 함수는 여러가지 작업을 수행하는셈이다.

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

함수가 한 가지 작업을 확실히 수행하려면, 함수 내의 모든 문장의 추상화 수준은 동일 해야한다. 한 함수 내에서 추상화 수준을 여러가지로 섞으면, 코드를 읽는 사람이 헷갈리게된다. 그 표현이 근본 개념인지, 세부사항인지 구분하기 힘들기 때문이다. 더 나아가면, 코드를 더 작성하게 될때 세부사항이 더더욱 추가될것이다.

코드는 위에서 밑으로 읽어야 한다. 즉, 한 함수 다음에는 추상화 단계가 한 단계 낮은 함수가 와야한다. 코드를 위에서 밑으로 읽는것처럼, 함수 또한 추상화단계가 한 번에 한 단계씩 낮아져야 한다. 이것을 내려가기 규칙이라고 정의한다. 함수 하나에 추상화 수준 하나만 구현하기란 힘들다. 하지만, 모든 프로그래머들은 이 규칙을 중요하게 여겨한다.

Switch 문

switch 문은 작게 만들기 힘들다. 당연히, if/else 문이 여러번 반복되는 문장도 마찬가지이다. case 분기가 하나만 있어도 긴 편에 속한다. 또한, 한 가지 작업한 하는 switch 문도 만들기 어렵다. 왜냐면 본질적으로 switch 문은 N가지의 작업을 처리하기 때문이다. 안타깝게도, switch 문을 완전히 배제하고 코드를 작성할 수는 없다. 하지만 다다형성을 사용해 switch 문을 저차원 클래스에 숨기지 않고 절대로 반복하지 않게 하는 방법은 있다. 아래 코드를 보자

struct CardOwner {
    let name: String
    let cardType: CardType
    
    enum CardType {
        case check, credit, prepaid
    }
}

protocol Payable {
    func pay()
}

struct CheckCard: Payable {
    let owner: CardOwner
    func pay() {
        print("\(owner.name) 님이 체크카드로 결제하였습니다.")
    }
}

struct CreditCard: Payable {
    let owner: CardOwner
    func pay() {
        print("\(owner.name) 님이 신용카드로 결제하였습니다.")
    }
}

struct PrepaidCard: Payable {
    let owner: CardOwner
    func pay() {
        print("\(owner.name) 님이 선불카드로 결제하였습니다.")
    }
}

struct CardFactory {
    
    static func makeCard(for owner: CardOwner) -> Payable {
        switch owner.cardType {
            
        case .check:
            return CheckCard(owner: owner)
        case .credit:
            return CreditCard(owner: owner)
        case .prepaid:
            return PrepaidCard(owner: owner)
        }
    }
    
}


let newCard = CardFactory.makeCard(for: CardOwner(name: "홍길동", cardType: .check))
newCard.pay()

swift는 추상 팩토리를 protocol을 통해 만들 수 있다. 팩토리 메소드( NAMING GUIDE FROM THE SWFIT API GUIDELINE: Begin names of factory methods with “make”, e.g. x.makeIterator().) 안에 switch 문을 넣으면 아무에게도 보여지지가 않는다. makeCard 메소드는 protocol을 거쳐 실행된다.

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

함수의 이름을 풀어서 쓰게되면 함수가 하는 일을 조금 더 잘 표현할 수가 있다. 이는 2장에서도 설명했던것처럼, 서술적으로 이름을 작성할 수록 좋다는것의 연장선이다. “코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드(Clean Code)라 불러도 되겠다.” 이 원칙을 기억해라. 함수가 작고 단순할수록 서술적인 이름을 고르기 쉬워진다.

이름이 길어도 상관이없다. 길고 서술적인 이름이 짧고 어려운 이름보다 훨씬 좋다. 서술적인 이름을 사용하면 개발자 머릿속에도 설계가 더 뚜렷해지므로 코드를 개선하기가 쉬워진다. 좋은 이름을 고른 후 더 좋게 재구성하는 방법도 좋다.

이름을 고를땐 일관성이 중요하다. 모듈내에서의 함수의 이름은 같은 문구, 명사, 동사를 채택해야한다. 문체가 비슷할수록 생각하기 더 쉬워진다.

함수 인수

함수에서의 이상적인 인수의 갯수는 0개 이다. 그 다음은 단항이고 그 다음은 이항이다. 삼항 이상부터는 피하는것이 좋다.

인수라는 개념을 이해하는것은 어렵다. 인수가 들어간 함수와 없는 함수는 추상화의 수준이 다르다. 인수가 들어간 함수를 발견 할 때 마다 현 시점에서 별로 중요하지 않은 세부사항을 알아야 한다.

테스트하는 관점에서 보면 더 어려워 진다. 여러가지의 인수 조합으로 함수를 테스트한다고 상상해보자. 얼마나 끔찍한가! 인수가 없다면? 훨씬 간편해진다. 인수가 한두개 정도면 괜찮지만 3개가 넘어가는순간부터 테스트하기 상당히 부담스러워진다. 출력 인수는 입력 인수보다 이해하기 더 어렵다. 최선은 입력 인수가 없는 경우이며, 차선은 입력 인수가 1개인 경우이다.

함수에 인수 1개를 넘기는 이유로 가장 흔한 경우는 두 가지이다. 하나는 인수에 질문을 던지는 경우이다. 다른 하나는 인수를 뭔가로 변환해 결과를 반환하는 경우이다.

다소 드물게 사용하지만 그래도 아주 유용한 단항 함수 형식이 이벤트다. 이벤트 함수는 입력 시스셈 상태를 바꾼다. 이벤트 함수는 조심해서 사용해야한다. 이벤트라는 사실이 코드에 명확히 드러나야 하기 때문이다.

위에서 설명한 경우가 아니라면 웬만하면 단항 함수는 가급적으로 피하는것이 좋다. 변환함수에서 출력 인수를 사용하면 혼란을 일으킨다. 입력인수를 반환하는 함수라면 변환 결과는 반환값으로 돌려준다.

플래그 인수는 추하다. 함수로 boolean값을 전달하는건 최악이다. 이유는? 함수가 한꺼번에 여러가지를 처리한다고 대놓고 알려주는 셈이다. 참이면 이걸하고 거짓이면 저걸한다는 뜻이다!

인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다. 예를들어 doSometing(this: String)은 doSomething(this: String, that: Int) 보다 이해하기 쉽다. 물론, 이항 함수가 적절한 경우도 있다. var point: Point = Point(0, 0) 가 좋은 예이다. 오히려 var point: Point = Point(0) 인 경우가 더 놀라울것이다! 하지만 여기수 인수 2개는 한 값을 표현하는 두 요소이다. 두 요소에는 자연적인 순서도 있다. 하지만 위에서 적은 경우는 한 값을 표현하지도, 자연적인 순서가 있지도 않다.

이항 함수가 무조건 나쁘다는 소리는 아니다. 프로그램을 짜다보면 불가피한 경우도 있다. 다만, 그만큼 위험이 있다는 사실을 인지하고 가능하다면 단항 함수로 바꾸도록 애써야 한다.

삼항 함수는 이항 함수보다 이해하기 훨씬 어렵다. 그러니, 삼항 함수를 만들때는 신중히 고려해야한다.

인수가 2~3개 필요하다면 일부를 독자적인 클래스 변수로 선언하는 가능성을 살펴보자. 아래 예를 보자.

func makeCircle(x: Double, y: Double, radius: Double) -> Circle
func makeCircle(center: Point, radius: Double) -> Circle

위 처럼, 객체를 생성해 인수를 줄이는 방법이 있다. 눈속임이라 생각할지 모르지만, 그렇지 않다. 변수를 묶어서 넘기려면 이름을 붙여야 하므로 결국은 개념을 표현하게 된다.

때로는 인수의 개수가 가변적인 함수도 필요하다. 단, 가변 인수를 취하는 함수도 위와 마찬가지로 같은 원리가 적용된다.

함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다. 단항 함수는 함수와 인수가 동사 / 명사 쌍을 이뤄야 한다. 예를 들어, wirte(name) 은 이해하기 쉽지만, writeField(name)이 훨씬 더 이해하기 쉽다.

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

부수 효과는 거짓말이다. 함수에서 한 가지를 약속하고선 클래스 변수를 수정하거나, 함수를 넘어온 인수나 시스템 전역 변수를 수정한다. 이것들은 아주 해로운 거짓말이다. 적합한 사용자인지 확인 하는 함수가 있다고 가정하자. 이 함수는 사용자의 이름과 암호를 확인하는 기능을 가지고있다. 그런데, 이 함수안에 서버로 세션을 전달하는 기능을 가지고 있다고 생각해보자. 함수 이름으로는 “적합한 사용자인지 확인” 인데, 동시에 세션까지 전달해버린다면, 기존의 세션 정보를 지워버릴 위험이 았다. 이러한 부수효과가 시간적인 결합을 초래한다. 시간적인 결합은 혼란을 일으킨다. 특히 부수 효과로 숨겨진 경우에는 더더욱 혼란이 커진다. 만약, 시간적인 결합이 필요하다면 함수 이름에 분명히 적어놓도록 하자.

일반적으로 함수의 인수는 입력 인수로 해석된다. 예를들어, appendFooter(a)는 바닥글(Footer)에 a를 첨부 하는것일까? 아니면 a에 바닥글을 첨부하는 것일까? 함수의 선언부를 살펴보면 func appendFooter(report: String) 라고 적혀있다. 이제서야 a가 출력 인수라는 것을 알 수가 있다. 함수 선언부를 찾아보는 행위는 가독성을 떨어뜨리는 행위이다. 위 함수는 report.appendFooter(a) 라는 방식으로 고치는것이 좋다. **일반적으로 출력 인수는 피해야 한다. 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.

명령과 조회를 분리하라!

함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야한다. 둘 다 하면 안된다. 객체 상태를 변경하거나 객체 정보를 반환하거나 둘 중 하나다. 아래 예제를 보자

func set(attribute: String, value: String) -> Boolean

이 함수는 이름이 attribute인 속성을 찾아 value로 설정한 후 성공한면 ture를 반환하고 실패하며 false를 반환한다. 그래서 아래와 같은 이상한 코드가 나오게 된다.

if set(“username”, “unclebob”) …

위 코드를 독자 입장에서 읽어보자. 이게 도대체 무슨 뜻일까? username을 unclebob 으로 설정한다는것인가?? 아니면 username 이 unclebob 인지 확인하는것인가? 함수를 호출하는 코드만 봐서는 의미가 모호하다. set이라는 단어가 동사인지 형용사인지 분간하기 어렵기 때문이다.

함수를 만든 사람을 set이라는 단어를 동사로 의도했다. 하지만 if 문에 넣고보면 동사가 아닌 형용사로 읽혀진다. 위 코드는 아래와 같이 수정하는것이 올바르다.

if attributeExists(“username” {
   setAttribute(“username”, “unclebob”)
   …
}

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


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

if deletePage(page) == E_OK

위 코드는 동사/형용사 혼란을 일으키지 않는 대신 여러단계로 중첩되는 코드를 야기한다 오류코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 한다는 문제에 직면한다. 오류 코드를 사용하는 대신 try/catch를 사용하면 코드가 훨씬 깔끔하게 변한다.

try/catch 블록은 원래 추하다 코드 구조에 혼란을 일으키며 정상 동작과 오류 처리 동작을 뒤섞는다. 그러므로, try/catch 블록을 별도의 함수로 뽑아내는 방식이 좋다.

함수는 한 가지 작업만 해야한다. 오류 처리도 한 가지 작업에 속한다. 그러므로 오류를 처리하는 함수는 오류만 처리해야 마땅하다. 함수에 키워드 try가 있다면 함수는 try 문으로 시작해 catch 로 끝나야한다.

오류 코드를 반환한다는 이야기는 클래스든 열거형 변수든 어디선가 오류 코드를 정의한다는 뜻이다. 열거형 변수가 변한다면 사용하는 클래스 전부를 다시 컴파일하고 다시 배치해야한다. 오류 코드 대신 예외를 사용하면 새로 발생하는 예외는 Exception 클래스에서 파생된다. 다시 컴파일하고 배치하는 필요가 없어지는 것이다.

반복하지 마라!

하나의 알고리즘이 여러곳에서 반복되면 모양새가 조금씩 달라지기 때문에 한눈에 들어나지는 않는다. 하지만, 중복되는 알고리즘은 문제다. 코드의 길이가 늘어날뿐만아니라 알고리즘이 변하게되면 사용한 모든 곳에서 수정을 거쳐야한다. 어느 한곳이라도 빠뜨리면 오류가 발생할 확률이 증가하게 된다. 중복은 거의 모든 소프트웨어에서 악의 근원이라고 봐도 다름없다.

구조적 프로그래밍

다익스트라 구조적 프로그래밍 원칙 중 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 한다는 원칙이 있다. 즉, 함수는 return 문이 하나여야 한다는 것이다. **루프 안에서 break나 continue를 사용해선 안되며 goto는 절대로 사용해선 안된다.

구조적 프로그래밍의 목표와 규율은 공감하지만 함수가 적다면 위 규칙은 별이익을 제공하지 못한다. 함수가 아주 클 때만 상당한 이익을 제공한다. 그러므로 함수를 작게 만든다면 간혹 return, break, continue를 여러 차례 사용해도 괜찮다. 오히려 때로는 단일 입/출구 규칙보다 의도를 표현하기 쉬워진다. 단, goto문은 큰 함수에서만 의미가 있으므로 작은 함수에서는 피해야한다.

함수를 어떻게 짜죠?

소프트웨어를 짜는 행위는 여느 글짓기와 비슷하다. 논문, 기사를 작성할 때는 먼저 생각을 기록한 후 읽기 좋게 다듬는다. 초안은 대게 서툴고 어수선하므로 원하는 대로 읽힐 떄까지 말을 다듬고 문장을 고치고 문단을 정리한다.

함수를 짤 때도 마찬가지다. 처음에는 길고 복잡하다. 들여쓰기 단계도 많고 중복된 루프롣 많다. 인수 목록도 아주 길다. 이름은 즉흥적이고 코드는 중복된다. 하지만 나는 그 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스도 만든다. 그 후, 코드를 줄이고 함수를 만들고 이름을 바꾸고 중복을 제거한다. 메서드를 줄이고 순서를 바꾼다. 때로는 전체 클래스를 쪼개기도한다. 이와중에도 코드는 항상 단위 테스트를 통과한다.

결론

모든 시스템은 특정 응용 분야 시스템을 기술할 목적으로 프로그래머가 설계한 도메인 특화 언어로 만들어진다. 함수는 그 언어에서 동사이며, 클래스는 명사이다. 요구사항 문서에 나오는 명사와 동사를 클래스와 함수 후보로 고려한다는 끔찍한 옛 규칙으로 역행하자는 이야기가 아니다. 아니, 이것은 오히려 훨씬 더 오래된 진실이다. 프로그래밍의 기술은 언제나 언어 설계의 기술이다. 예전에도 그랬고 지금도 마찬가지다.

프로그래밍의 대가는 시스템을 구현할 프로그램이 아니라 풀어갈 이야기로 여긴다. 프로그래밍 언어라는 수단을 사용해 좀 더 풍부하고 좀 더 표현력이 강한 언어를 만들어 이야기를 풀어간다. 재귀라는 기교로 각 동작은 바로 그 도메인에 특화된 언어를 사용해 자신만의 이야기를 만들어간다.

이 장은 함수를 잘 만드는 기교를 소개했다. 여기서 설명한 규칙을 따른다면 길이가 짧고 이름이 좋고 채계가 잡힌 함수가 나오리라. 하지만 진짜 목표는 시스템이라는 이야기를 풀어가는 데 있다는 사실을 명심하기 바란다.

profile
iOS 코린이

0개의 댓글