클린코드

[클린코드] 3장 함수

우디혜 2020. 11. 29. 01:07

작게 만들어라!

물론 사람마다 '작다'는 기준은 다르다고 생각한다. 책에 나와있는 조금 더 명료한 기준을 들자면 함수의 들여쓰기 수준은 1단 혹은 2단을 넘어가면 안된다. 그리고 if/else/while statement에 들어가는 블록은 한 줄이어야.. 한다고 나와있지만 현업(경험이 없는 지혜이지만)에서 그게 가능할까라는 의문이 든다. 과도한 캡슐화, 추상화는 오히려 독이 되지 않을까 싶어서 이 부분은 참고만 하고 넘어가야겠다. 어쨌든 책에서 말하고자 하는 바는 함수를 장황하게 쓰지 말라는 거니까.

 

한 가지만 해라!

함수는 한 가지만 잘하면 된다. 그리고 그것만 해야한다. 사실 OOP를 접하다보면 항상 듣는 말이다. 그리고 책에서 함수를 만드는 이유를 '큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서'라고 했는데 이 말을 듣고서 함수들이 공장에서 전문성을 살려 각자 맡은 일을 하고 있는 사람들 같다는 생각을 했다. 이런걸 흔히 '분업'이라고 하는데, 분업은 이미 산업화의 성장동력 중 하나였던 만큼 이미 효율성이 입증된 방식이다. 어떻게보면 좋은 코드의 조건에도 '잘 분업된 함수'가 들어가는건 당연한 일이라고 생각한다.

 

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

사실 책에서 추상화 수준이라는 단어가 나왔을 때는 알 것도 같으면서 정확한 의미를 몰랐다. 쉬운 예시로 getHtml()은 추상화 수준이 아주 높은 반면, .appned("/n") 와 같이 말 그대로 추상화가 거의 사용되지 않은 코드는 추상화 수준이 낮은 것이다. 그리고 이렇게 추상화 수준이 다른 코드를 한 함수에 섞어버리면 가독성이나 유지보수 측면에서 좋은 코드가 아니다.

추상화 수준이 맞는지 제대로 파악하기 위해서는 내려가기 규칙을 활용해도 좋다. 위에서 아래로 책을 읽듯이 함수 내부 코드를 쭉 읽어내려가면서 추상화 수준을 점검하다보면 일정하게 추상화 수준을 유지하기가 쉬워진다.

 

Switch 문

switch문은 태생적으로 앞에서 언급했던 클린코드 원칙들을 적용하기가 힘들다. 일단 여러가지 case를 고려하기 때문에 함수의 길이가 길고, 하나 이상의 작업을 수행한다. 당연히 유지보수도 어려워지고 SRP 원칙도 지키기 어려워진다. 확장성도 떨어지기 때문에 OCP 원칙에도 위배된다. 그래서 글쓴이는 Switch문을 다형적 객체를 생성할 때, 그때 단 한 번만 참아준다고 한다. 책에서 제시한 예시도 보면 switch문은 글쓴이가 꾹 참고 허용하는 다형적 객체를 생성하는 코드로 사용될 지라도 추상 팩토리에 넣어 꽁꽁 숨겨놓았다. [추상 팩토리 구현 예시]

 

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

개인적으로 네이밍은 정말정말 어려운 것같다. 책에서는 아래와 같이 서술적인 이름을 함수명으로 추천한다. 요지는 함수 이름을 보고서 다른 개발자들이 기능을 짐작할 수 있어야하고, 그 기능이 실제로 잘 수행되어야 한다는 것이다.

testableHtml() X
SetupTeardownIncluder() O
isTestable() O
includesetupAndTeardownPages() O

그리고 '길고 서술적인 이름이 짧고 어려운 이름보다 좋다'는 말에 조금 안심했다. 네이밍을 하다보면 이름이 길어질 때가 있는데 그때마다 가독성이 떨어지지 않을까라는 걱정을 한 적이 많았기 때문이다.

 

  1. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
  2. 길고 서술적인 이름이 길고 서술적인 주석보다 좋다.
  3. 함수 이름을 정할 때는 여러 단어가 쉽게 읽히는 명명법을 사용한다. (의미는 알겠는데 방법이 잘 떠오르지 않는다)
  4. 그런 다음, 여러 단어를 사용해 함수 기능을 잘 표현하는 이름을 선택한다.
  5. 이름의 일관성도 중요하기 때문에, 가급적 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다. 
    • 아래와 같은 네이밍은 코드를 처음 본 사람들도 includeTeardownPages, includeSuiteTeardownPage 등의 함수도 있을 수 있겠다는 합리적인 짐작(?)을 가능하게 하고 우리는 그 기대와 짐작에 부응해야할 필요가 있다.
      • includeSetupAndTeardownPages
      • includeSetupPages
      • includeSuiteSetupPages
      • includeSetupPage

함수 인수

함수 인수란, 함수의 argument를 말한다. 가장 최선은 인수가 없는 경우고, 1-2개까지는 OK, 3개는 되도록 지양하고, 4개면 정말정말 특수한 상황이 아니고서야 만들지 않는게 좋다. 그리고 인수가 함수의 흐름을 이해하는데 불필요한 정보(별로 중요하지 않은 세부사항)라고 느껴질 때, 즉 인수가 해당 함수와 같은 추상화 수준이 다를 때는 인스턴스 변수를 적절히 사용해서 인수를 줄여라.

 

많이 쓰는 단항 형식, 변환 함수

 

1. 인수에 질문을 던지는 경우

2. 인수를 무언가로 변환하여 반환

 

인수에 질문을 던지는 예로는 boolean fileExist("fileName"),

무언가로 변환하는 예로는 InputStream fileOpen("fileName") 에서 파일이름 정보를 실제 InputStream 타입의 파일을 리턴

 

이 이외의 경우는 되도록 단항 함수로 만들지 마라.

예를 들어, 변환 함수에서 void transform(StringBuffer out)와 같이 출력인수를 사용하는 것보다,

StringBuffer transform(StringBuffer in)이 더 좋다. 중요한 것은 변환 형태를 유지하는 것이다.

 

이항 함수

 

인수의 개수가 하나를 넘어서게 되면, 순서에도 신경을 써야한다. 그리고 보통 단항 함수보다는 조금 더 함수의 흐름을 이해하는데 시간이 걸린다. 물론 new Point(0,0)은 이항함수가 더 적절하다는 것을 통념으로 알고있기 때문에 이 경우에는 이항 함수를 사용하는 것이 적절하지만 그 이외의 경우에는 되도록이면 단항 함수로 바꾸도록 신경써야한다.

또 하나, 좋은 포인트라고 생각했던 구절, '어떤 코드든 절대로 무시하면 안된다. 무시한 코드에 오류가 숨어드니까'. 이해하기 힘든 코드일 수록 큰 흐름을 이해하기 위해 무시하게되는 코드들이 생긴다. 그리고 대개 그런 코드들을 무시한 결과로 오류가 찾아오는 경우가 많다. 오류의 가능성을 줄이기 위해서라도 유의미한 인수들 이외에는 되도록 사용하지 않는 편이 좋다고 생각한다.

 

삼항 함수

 

보통 인수가 늘어날 수록 훨씬 더 이해하기 어렵다. 순서, 무시 등으로 문제가 발생할 가능성이 더 높아진다. 그리고 책에서는 '주춤'할 가능성도 커진다고 이야기를 했는데, 함수를 한눈에 보고서 이해할 수 없는 상황을 표현하는 단어이다. 주춤하는 순간들이 많다는 것은 코드의 가독성이 떨어진다는 이야기다. 그렇기 때문에 만약 꼭 삼항 함수를 써야하는 상황이 온다면 assertEquals(message, expected, actual) 보다는 assertEquals(exected, actual, message)가 더 적절할 것이다. assertEquals() 함수를 보고 가장 먼저 떠올릴 수 있는 것은 expected와 actual일 것이기 때문이다. message를 보고 '주춤'할 수 있는 여지를 만들지 말자.

 

인수 객체

 

여러 개의 인수를 하나의 개념으로 묶어서 표현할 수 있다면 하나로 묶어서 클래스 변수로 선언하는 것도 좋은 방법이다.

Circle makeCircle(double x, double y, double radius);

Circle makeCircle(Point center, double radius);

첫 번째 방식 대신 center와 radius라는 표현을 쓸 수 있도록 인수들을 객체화시켜주는 것도 좋은 방법이다.

 

✔ 이런 방식은 혼자서 프로그래밍 했을 때는 이해하기 힘들었다. 아무래도 객체를 하나 더 만든다는게 클래스를 하나 더 만드는 일이라서 비효율적인 방식이 아닐까라는 의구심이 있었기 때문이다. 그런데 지금은 유지보수, 협업이라는 부분에 무게를 두고 보면 장기적인 관점에서는 확실히 두 번째 방법이 가져오는 장점이 많다고 생각한다.

 

동사와 키워드

 

함수 혹은 인수의 의도나 인수의 순서를 제대로 표현하기 위해서는 네이밍이 중요하다. 특히 단항함수는 함수와 인수가 '동사/명사' 쌍을 이루는 것이 좋다. 예를 들면 writeField(name). 함수 이름에 키워드를 추가하는 것도 좋다. 예를 들면 assertEquals보다 assertExpectedEqualsActual(expected, actual)이 더 좋다. 인수 순서가 확실해지기 때문이다.

 

 

부수 효과를 일으키지 마라

하나의 함수가 두 개 이상의 책임을 맡을 때, 시간적 결합(temporal coupling) 혹은 순서 종속성(order dependency)에 따른 문제가 발생할 수 있다.

 

A라는 이벤트와 B라는 이벤트가 있다고 가정해보자. 

A와 B가 하나의 함수에 있게 되면 먼저 시간적 결합이 일어나게 된다. A하면서도 B하는 상황이 발생할 경우에만 해당 함수를 실행시킬 수 있기 때문이다. A만 발생하는 상황, 혹은 B만 발생하는 상황을 처리할 수 없다. 그리고 두 가지 이벤트가 하나의 함수에 있게되면 반드시 순서가 생긴다. 때문에 A-B 순서로 함수가 실행된다면 B-A의 상황에는 쓰일 수 없게된다. (반대로 A와 B 이벤트 사이에 순서 종속성이 필요한 경우에는 하나의 함수에 두 가지의 이벤트를 동시에 넣는 것이 오히려 적절하다는 생각이 든다, 개인적인 의견으로는..)

 

출력 인수

 

출력 인수란 말 그대로 해당 함수에서 출력 목적을 위해 사용되는 인수를 말한다.

책에서 나온 예시는 이렇다.

// 1. 출력 인수 s를 사용
 StringBuffer s = new StringBuffer();
 appendFooter(s);
 
// 2. 객체지향적 방식으로 출력
 report.appendFooter()

첫 번째 방식은 StringBuffer 타입 객체인 s가 출력 인수가 되어 appendFooter안에서 실제로 출력된다. 그러나 여기서 가장 큰 문제는 s를 보고 '주춤'할 여지가 있다는 것이다.

 

두 번째 방식은 appendFooter() 함수가 StringBuffer 클래스 내부에 존재한다. 이런 경우에는 출력 인수를 사용하지 않아도 된다.

 

최대한 두 번째 방식을 지향해야한다. 즉, 출력 인수 사용을 피해야한다.

 

명령과 조회를 분리하라!

하나의 함수는 수행(객체 상태를 변경)하거나, 답하거나(객체 정보를 반환) 둘 중 하나의 일만 해야한다. 

 

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

오류 코드를 반환하기 위해 코드를 짜다보면 코드와 함수 로직이 섞이게 된다. 또 오류 코드를 반환받은 호출자는 오류 를 따로 처리해주어야한다. 따라서 오류 코드 대신 예외 처리를 통해서 기본 로직과 예외 처리 코드를 분리해주면 더 코드가 깔끔해진다.

 

Try/Catch 블록 뽑아내기

 

try/catch를 사용하면 함수 로직과 예외 처리 로직이 섞이게 되어 코드 구조에 혼란을 줄 수 있다. 때문에 try/catch 블록을 별도의 함수로 뽑아내 정상 로직과 오류 처리 로직을 분리하면 가독성, 코드 수정 측면에서 더 좋은 코드가 된다. 오류 처리도 한 가지 기능이기 때문에 오류 처리 함수는 오류 처리만 담당해야 한다.

 

반복하지 마라!

중복된 코드들은 수정 작업도 오래걸리고, 가독성도 떨어진다. 반대로 중복을 제거하면 수정이 용이해지고, 가독성이 높아진다. 클린 코드를 위한 많은 전략들이 중복을 없애기 위해 만들어졌다고 이야기할 수 있을 정도로, 반복은 악의 근원(ㅋㅋㅋ).

 

함수를 어떻게 짜죠?

1. 클린코드나 구조를 생각하지 않고 쭉 써내려간다 + 단위 테스트

2. 기능 단위로 분리, 적절한 네이밍, 중복 제거 등을 고려하여 리펙토링 + 단위 테스트

 

처음부터 좋은 코드는 나오지 않는다. 정말 다행스럽게도 나만 그런게 아니었어.

 

결론

프로그래밍의 기술은 언제나 언어 설계의 기술이다. 사람들이 코더와 프로그래머를 구분짓는 기준도 여기에 있지 않을까 싶다. '대가 프로그래머는 시스템을 프로그램이 아니라 이야기로 여긴다.'라는 말도 인상깊었다. 개인적으로 이 말을 단순히 프로그램의 결과에 집중하기 보다는 조금 더 흐름을 이어나가는 방식에 초점을 맞추라는 말로 해석했다. 프로그램은 '구현'하는 것이지만 이야기는 '풀어'나가야하는 것이기 때문이다. 작가가 이야기를 잘 풀어내어 효과적으로 전달하려면 이를 표현하는 좋은 표현이나 구조가 동반되어야 한다. 마찬가지로 프로그래머가 프로그래밍 언어를 가지고 시스템을 만들 때, 좋은 표현과 구조를 통해 하나의 논리적 흐름을 효과적으로 표현해낼 수 있어야 한다고 생각한다. 그리고 그 좋은 표현과 구조는 결국 클린코드 전략과 맞닿아있지 않을까.

 

'클린코드' 카테고리의 다른 글

[클린코드] 4장 주석  (0) 2020.12.27
[클린코드] 2장 의미있는 이름  (0) 2020.11.14
[클린코드] 추천사, 1장  (0) 2020.11.03