본문 바로가기
Architecture & Engineering/Clean Software

[Clean Code] 1. 의미있는 이름과 함수

by 신숭이 2023. 2. 26.

 

Clean Code 1 . 의미 있는 이름과 함수

 

 

이직 후 생각보다 큰 규모의 프로젝트를 담당하다 보니, 학부 때는 정말 그 의미가 크게 와닿지 않았던 것들이 이제야 왜 중요한지 알게 되어가는 중이다. 알게 되어간다는 말도 오만한 표현일 것 같을 정도로 생각보다 정말 많이 중요하더라.

특히 아키텍처와 패턴을 다루기 이전, [코드] 라는 것은 개발자 간 일종의 대화로도 볼 수 있다. 코드를 명료하게 표현하지 못한다면 대화를 못하는 개발자라 할 수 있다. 때문에 먼저 클린한 코드란 무엇인가부터 공부하고자 한다. (패턴에 대해, 아키텍처에 대해 수많은 임시 저장 글들이 있지만, 클린코드부터가 역시 순서상 맞는 것 같다) 사실 책은 입사 초에 읽었지만 정리가 귀찮았는데, 프로젝트가 조금 여유가 생겨 정리하면서 복기하고자 한다. (몇 가지 최신 IDE의 도움으로 해결되어 버린 주제는 생략했다). 참고로 책은 Java를 기반으로 작성되어 있다.

똑똑한 개발자와 전문적인 개발자의 차이는 명료함의 중요성을 아는 것

 

 

 

의미 있는 이름 : 의도가 분명하게 이름을 지어라

 

변수나 함수의 이름은 길어도 좋다. 코드는 단순하게 짜는 것이지 함축하는 게 아니다.

만약 변수 옆에 주석으로 더 설명이 필요하다면 이름을 잘 짓지 못했다는 것. 이름만 잘 지어도 장문의 코드를 이해하는 데 걸리는 시간의 차이가 드라마틱하게 차이 나게 된다. 제일 당연해 보이고 쉬어 보이는 이야기지만, 이런 게 보통 하다 보면 어렵다. 어느 순간 이름을 뭐라고 해야 잘 지은 걸까 하면서 고민하는 시간이 길어지더라

 

Bad 

int d; // 경과시간

 

Good

int elapsedTimeInDay;

 

Bad

public List<int[]> getThem() {
    List<int[]> list1 = new ArrayList<int[]>();
    for (int[] x : theList)
        if (x[0] == 4)
            list1.add(x);
    return list1;
}

 

Good : 이름을 분명히 하고,  int형 배열은 cell이라고 추상화

public List<Cell> getFlaggedCells() {
    List<Cell> flaggedCells = new ArrayList<Cell>();
    for(Cell cell: gameBoard) 
        if(cell.isFlagged())
            flaggedCells.add(cell);
    return flaggedCells;
}

 

 

 

의미 있는 이름 : 불용어(Noise Word)를 사용하지 마라 

 


불용어 : 의미 없는 넘버링, 쓸데없는 추가정보(~~Info, ~~Data, ~~Type).
추가로 접두어는 구닥다리 표기법이다. 인터페이스 앞에 I 를 붙인다거나, 멤버변수에 m을 붙인다거나.
(최신 IDE의 도움으로 더 이상 필요 없는 관례들)

 

bad

void copyString(String a1, String a2);
String nameString;
Data accountInfo;

 

good

void copyString(String source, String detination);
String name;
Data account;

 

 

의미 있는 이름 : 클래스 이름은 명사나 명사구, 메서드 이름은 동사나 동사구

 

ex) 클래스 이름 : Customer, WikiPage, Manager, Processor ..

ex) 메서드 이름 : postPayment, deletePate, save ..

특히 변경자(Mutator, setter)는 setXXX(), 접근자(Acessor, getter) 는 getXXX(), 조건자(Predicate)는 isXXX 라고 붙이는게 javabean 표준이라고 한다.

생성자의 중복정의는 정적 팩토리 메서드를 사용하고 fromXXX 라고 하자.
그리고 메서드 이름은 동사구, 인자는 명사를 써서 영문법처럼 자연스럽게 보이도록 작성하자.

 

bad

Complex fulcrumPoint = new Complex(23.0);

 

good

Complex fulcrumPoint = Complex.FromRealNumber(23.0);

 

 

의미 있는 이름 : 일관성 있게 한 개념에는 한 단어만 사용하라

 

같은 기능, 같은 의미로 쓰이는데 한 프로젝트에서 get, fetch, retrieve 등 여러 가지로 부른다면 분명 혼란을 초래한다. 같은 기능을 하고 같은 의미로 쓰이면 반드시 하나로 통일해서 쓰자.

다만 일관성을 유지한다고 비슷한걸 모두 한 단어로 엮으라는 의미는 절대 아니다. 코드도 일종의 [문맥 상]이라는 말이 먹히는 곳이다.


add라는 단어가 여태껏 기존 값에 무언가 더해 새로운 값을 만드는 의미로 쓰였다고 가정할 시, list 에 새 element를 넣는 행위도 add라고 해선 안된다. 일관성이 아니라 이것은 말장난인것. list 에 데이터를 추가하는 행위는 add보단 insert 나 append가 훨씬 더 적합한 단어이다.

 

 

함수 : 짧으면 짧을수록 좋다

 

얼마나? 2~4줄이면 충분하다.(충격) 이 책에서는 함수가 짧으면 짧을 수록 좋다고 수도 없이 강조한다. 조건문이나 반복문으로 인해 발생하는 중첩구조(들여쓰기) 최대 2단을 넘지 않아야 한다. 어떻게 그렇게 함수를 짧게 작성할 수 있을까? 이 책의 3장인 [함수]는 섹터에서 계속해서 말하는 내용이 있다.

함수는 단 한 가지 추상화 수준의 일만 하고, 그 한 가지 일은 확실히 잘할 것.

 

 

 

함수 : 한 가지만 해라

 

한 가지 일이란 함수의 이름이 추상화하고 있는 한 가지 일을 수행하는 것이다. renderPageWithSetup 이라는 함수는 setup 후 render를 수행하는 [한 가지] 작업을 하는 것이다. 말장난이라고 생각할 수도 있는데, 만약 여러 가지 일을 한 가지일처럼 표현하기 위해 함수이름이 무한히 길어진다면 이상함을 느낄 것이다. (이름은 최대한 서술적인 이름을 사용하라)

한 가지 일을 하는 함수인지 확실하게 판단하는 방법은 내가 작성한 함수가 유의미한 여러 개의 함수로 쪼개질 수 있다면 지금 그 함수는 한 가지 일을 하는 함수가 아닌 것이다. "와 이건 더는 못쪼개.." 라는 생각이 들어야 한다. 책에서는 한 가지 일만 하는 함수는 함수 내 문장들이 여러 가지 섹션으로 구분되는 일이 없다고 한다. (정확히는 구분할 수 없다고 한다. 구분할 수 있다면 한 가지 일을 하는 함수가 아닌 것)

 

 

 

함수 : 함수 당 추상화 수준은 하나로 통일한다.

다음 함수는 추상화 수준이 높다.

getHtml();

 

이것은 중간정도의 추상화 수준이다.

String pagePathName = PathParser.render(pagepath);

 

다음은 추상화 수준이 아주 낮다.

.append("\n");

 

한 함수 내에 여러 추상화 수준을 혼용하지 말자. 혼용될수록 한 가지 일을 하기 어려워진다. 그리고 유지보수 단계에서 계속해서 기능이 추가될 가능성이 있다. 깨어진 창문처럼.

저자는 높은 추상화 수준이 높은 내용들로 채워진 함수를 상단에 작성하고, 아래로 갈수록 낮은 추상화 수준의 내용으로 채워진 함수를 선언해, 전체적인 문서를 작성하라고 한다. 이를 [내려가기 규칙] 이라고 하던데, 코드도 글이므로 위에서부터 내려가면서 읽는 게 자연스럽기에 그걸 지향하자고 한다.

 

 

함수 : Switch문은 다형적 객체를 생성하는 코드 안에서만 사용하자

 

다음과 같은 함수가 있다고 하자. Switch 문은 본질적으로 N가지일을 하게 만든다.

public Money caculatePay(Employee e) throws InvalidEmployeeType {
    switch(e.type) {
        case COMMISSIONED:
            return calculateCommisionedPay(e);
        case HOURLY:
            return calculateHourlyPay(e);
        case SALARIED:
            return calculateSalariedPay(e);
        default:
            throw new InvaliedEmployeeType(e.type);
    }
}

위 함수는 새 직원 유형이 추가될 때마다 더 길어지고 코드를 수정해야 한다. 이는 OOP에서 말하는 기본 원칙 중 OCP(개방-폐쇄 원칙 : 변경에는 닫혀있고, 확장에는 열려있어야)을 위반하고, 변경될 이유가 여러 개가 될 수도 있다. 이는 SRP(단일 책임 원칙 : 변경될 이유는 한 가지 여야 한다) 또한 위반하는 것.

그리고 함수가 길고 동일한 모양새의 함수를 계속해서 만들어 낼 수도 있다. 아래와 같이. 이 함수들도 switch 문을 쓸 테고 위와 동일한 이유로 함수가 수정될 가능성이 농후하다. 

isPayday(Employee e, Date date);
deliverPay(Employee e, Money moneny);
..

이는 매우 유해하다.

저자는 이를 추상팩토리를 이용해 switch문을 숨기라고 한다. 아래는 새로운 직원 유형이 추가될 때마다 해당하는 함수를 강제로 작성하되, 그 함수는 각각 switch문을 쓸 필요 없이, 자신이 할 일만 수행(해당하는 알고리즘 등)하면 된다. 따라서 위와 아래의 가장 큰 차이는 위 코드는 switch 문을 수도 없이 쓸 가능성을 잠재하고 있다면, 아래 코드는 switch문이 단 한번 쓰이고 끝난다.

public abstract class Employee {
    public abstract boolean isPayday();
    public abstract Money calculatePay();
    public abstract void deliverPay();
}

public interface EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}

public class EmployeeFactoryImpl implements EmployeeFactory {
    @Override
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
        switch (r.type) {
            case COMMISSIONED:
                return new CommissionedEmployee(r);
            case HOURLY:
                return new HourlyEmployee(r);
            case SALARIED:
                return new SalariedEmployee(r);
            default:
                throw new InvaliedEmployeeType(e.type);
        }
    }
}

 

 

 

함수 : 인수(parameter)는 없는 게 제일 좋다.

 

인수는 함수의 기능을 예상하기 어렵게 만들고, 테스트 코드를 작성할 때도 경우의 수를 기하급수적으로 증가시킨다. 그래서 없는 게 제일 좋고 많아야 1~2개 정도만 쓰는 게 좋다. 3개 이상은 아예 생각도 하지 말자. 정말 특별한 이유가 있을 때나 3개 이상 쓰고 그런 이유가 없다면 안 쓰는 게 맞다. 저자에 말에 따르면 최선은 0개, 차선은 1개이다.

 

자주 쓰이는 단항(인수 1개) 형식

질문을 던지는 유형

boolean fileExists("MyFile");

 

변환해 결과를 반환하는 유형 (반드시 결과는 반환되는 형식이 여야 한다)

InputStream fileOpen("MyFile");

 

* 플래스 인수(boolean 타입을 인자로 넘기는)는 추하다. 쓰지 말자. 플래그 인수는 대놓고 함수가 2가지 일을 한다는 것을 공표한다.

render(true);  // 쓰지말자

 

이항 형식 : 2개 이상부터는 불가피한 경우를 제외하곤 피하자.

 

다음과 같이 2개가 자연스러운 경우가 있다. 이 정도 자연스러움이 아니라면 피하자.

Point p = new Point(0,0);

 

말장난처럼 보일 수 있지만, 다음과 같은 3항 형식을 객체 인수를 이용해 2항으로 바꾸는 게 더 좋다.

Circle makeCircle(double x, double y, double radious);
Circle makeCircle(Point center, double radious);

 

다음과 같은 이항 함수가 있다면 이는 객체지향 언어가 나오면서 단항으로 변경할 수 있게 되었다.
그리고 아래와 같이 출력인수(변환되어 나오는 결과)를 인자로 넘겨주는 행위는 하지 말자.

writeField(outputStream, name);    // 이항 형식
outputStream.writeField(name); 	   // 단항 형식

 

 

함수 : 부수효과를 일으키지 마라

 

한 가지 일을 하는 척하는 이름을 가진 함수가 내부적으로 시간적 결합(Temporal Coupling)을 일으키거나 순서 종속성(Order dependency) 를 초래하는 구문을 가지고 있다면 반드시 문제를 일으킨다.

public boolean checkPassword(String userName, String password) {
    User user = UserGateway.findByName(userName);
    if (user != User.NULL) {
        String codedPhrase = user.getPhraseEncodedByPassword();
        String phrase = cryptographer.decrypt(codedPhrase, password);
        if("Valid Password".equals(phrase)) {
            Session.initialize();
            return true;
        }
    }
    return false;
}

위 함수는 Session.initialize() 가 시간적 커플링(전역적인 환경을 변화시킬 수 있는)을 일으킨다. 우선 최선은 이런 시간적 커플링을 반드시 배제해야 한다. 그리고 위 함수는 이로인해 한 가지 일을 하는 함수도 아닌게 되었다. 그리고 함수이름만으로 다른 개발자는 이 함수가 세션을 초기화하는지도 알 수 없다. 만약 굳이 저런 함수를 작성해야겠다면 함수이름은 checkPasswordAndInitializeSession 이라고 해야한다. 

 

 

함수 : 명령과 조회를 분리하라

 

다음과 같은 함수는 명령과 조회가 분리되어있지 않다.

public boolean set(String attribute, String value);

 

함수의 행위는 attribute인 속성을 찾아 값을 value로 설정한 후 성공하면 true, 실패하면 false를 반환한다.

이는 다음과 같이 명령과 조회로 분리한다.

조회 : attribute 인 속성이 있나?
명령 : attribute인 속성을 찾아 값을 value로 설정한다.

if(attributeExists("username")) {
    setAttribute("username", "unclebob");
}

 

 

함수 : 오류 코드보다 try-catch 문을 사용하라

 

오류 코드(Enum type으로 정의된) 은 명령과 조회를 분리하기 어렵게 만들고, 대량의 if문 사용 및 중첩된 조건문을 유발할 가능성이 매우 농후해진다. 이는 throw와 try-catch문으로 적절히 바꾸자. 참고로 예외처리문도 "한 가지 일"로 취급한다. 따라서 이 try-catch 블록도 뽑아내어 별도 함수로 구성하면 더 좋다. (코드 이해에도 훨씬 도움이 된다)

 

 

저자는 만악의 근원은 중복된 코드라고 말한다. 
그래서 그 중복을 최대한 배제하기 위해 다양한 패턴(Design Pattern)을 적용해야 한다고 한다.

댓글