슬기로운 개발생활

클린 코드(Clean Code) 요약 및 정리

by coco3o
반응형

개발 하며 내가 작성하고 있는 코드가 과연 좋은 코드인가 문득 고민하게 되었고, 좋은 코드란 무엇인가에 대한 궁금증으로 '클린 코드' 서적을 접하게 되어 읽으면서 중요한 내용들을 정리하고자 한다.
 
 

💡 의도를 분명히 밝혀라

변수, 함수, 클래스 등의 이름은 코드의 의도를 잘 표현할 수 있도록 지어야 하며,
이름만으로도 코드가 하는 일을 예측할 수 있도록 해야 한다.
의미 없는 약어나 애매한 이름을 피하고, 코드의 기능을 정확하게 반영하는 이름을 선택하는 것이 중요하다.

// 나쁜 예
boolean flag = true;

// 좋은 예
boolean isUserLoggedIn = true;

 
 

💡 조건을 캡슐화하라

조건의 의도를 분명히 밝히는 함수로 표현하자.

// 나쁜 예
//직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if ((employee. flags & HOURLY_FLAG) && (employee.age > 65))

// 좋은 예
if (employee.isEligibleForFullBenefits())

 
 

💡 객체 생성에 유의미한 이름을 사용하라

객체의 생성자를 오버로딩 하는 경우 어떤 값으로 어떻게 생성되는지 정보가 부족할 수 있다.
이런 경우, 정적 팩토리 메소드를 사용하여 인수를 설명하는 이름으로 작성하는 것이 명확하다.

// 두 번째 인자가 무엇인지 파악이 어렵다.
Student student = new Student("James", 85);

// 이름을 부여하여 두 번째 인자를 명확하게 파악할 수 있다.
Student student = Student.withGrade("James", 85);

 
 

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

서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.
- 함수가 작고 단순할수록 서술적인 이름을 작성하기 쉬워진다.
- 이름이 길어도 괜찮다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
- 이름을 붙일 때는 일관성이 있어야 한다. 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용하자.

public void includeSetupAndTeardownPages() {
// ...
}
    
public void includeSetupPages() {
// ...
}
    
public void includeSuiteSetupPage() {
// ...
}
    
public void includeSetupPage() {
// ...
}

 
 

💡 명령과 조회를 분리하라 (CQS 원칙)

함수는 뭔가를 수행하거나 뭔가를 조회하거나 하나의 역할만을 해야 한다.
두 개의 역할을 동시에 하면 혼란을 초래한다.

public boolean set(String attribute, String value);
if(set("username", "coco")) {
	...
}

위 예시 코드를 보면, "username"이 "coco"로 설정되어 있는지를 확인하는 건지 "username"을 "coco"로 설정하는 코드인지 알 수 없다.
그 이유는 위의 함수가 명령과 조회를 한 번에 처리하기 때문이다. 조회와 명령을 분리하여 다음과 같이 작성해 주는 것이 명확하다.

public boolean attributeExists(String attribute);
public boolean set(String attribute, String value);
if(attributeExists("username")) {
	set("username", "coco");
}

 
 

💡 오류 코드보단 예외를 사용하라

오류 코드를 반환하면 그에 따른 분기가 생기고, 또 분기가 필요한 경우엔 중첩되기 마련이다.

public Status deletePage(Page page) {
    if(deletePage(page) == E_OK) { // (1)
        if(registry.deleteReference(page.name) == E_OK) { // (2)
            if(configKeys.deleteKey(page.name.makeKey()) == E_OK) { // (3)
                log.info("page deleted");
                return E_OK;
            } else { // (3)
                log.error("config key not deleted"); 
            }
        } else { // (2)
            log.error("reference not deleted"); 
        }
    } else { // (1) 
        log.error("page not deleted"); 
    }
    return E_ERROR;
}

여기서 오류 코드 대신 각 함수에서 예외를 사용하면 코드를 더욱 간결하게 작성할 수 있다.

public void deletePage(Page page) {
    try {
        deletePage(page);
        registry.deleteReference(page.name);
        configKeys.deleteKey(page.name.makeKey());
    } catch (Exception e) {
        log.error(e.getMessage());
    }
}

여기서 deletePage 함수에 try-catch 블록이 생기게 되는데 아래와 같이 분리하여 deletePage 함수는 오류 처리 역할만 맡기고, deletePageAndAllReferences 함수는 페이지 제거하는 역할만 맡길 수 있다.

public void deletePage(Page page) {
    try {
        deletePageAndAllReferences(page);
    } catch (Exception e) {
        log.error(e.getMessage());
    }
}

public void deletePageAndAllReferences(Page page) throws Exception {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
}

 

 

💡 설계의 품질을 높여주는 4가지 규칙

1. 모든 테스트를 실행하라 : 테스트가 쉬운 코드를 만들다 보면 SRP를 준수하고, 낮은 결합도를 갖는 설계를 얻을 수 있다.
2. 중복을 제거하라 : 깔끔한 시스템을 만들기 위해 단 몇 줄이라도 중복을 제거해야 한다.
3. 개발자의 의도를 표현하라 : 좋은 이름, 클래스와 함수의 작은 크기, 표준 명칭, 단위 테스트 작성을 통해 이를 달성할 수 있다.
4. 클래스와 메소드의 수를 최소로 줄여라 : 클래스와 메소드를 작게 유지함으로써 시스템의 크기 역시 작게 유지할 수 있다.
 
2~4번은 리팩토링 과정에 해당한다. 모든 테스트케이스를 작성한 후에 코드를 정리하기 때문에 안전하다.
이 단계에선 소프트웨어 설계 품질을 높이는 기법이라면 무엇이든 적용해도 괜찮다.

 
 

💡 디미터 법칙

디미터의 법칙은 어떤 모듈이 호출하는 객체의 속사정을 몰라야 한다는 것이다.
객체는 자신의 내부 구조를 숨기고 함수를 통하여 기능을 공개해야 한다.
만약 객체의 내부 구조를 그대로 노출하면 다른 모듈은 객체의 내부 구조를 알게 되며 이로 인해 결합도가 높아지게 된다.
 
다음 코드는 메소드 체이닝을 통해 메소드가 반환하는 객체의 내부 구조에 접근하므로 디미터 법칙을 위반한다..

String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

위 코드는 다음과 같이 변환하는 것이 바람직하다.

Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
String outputDir = scratchDir.getAbsolutePath();

 
 

💡 테스트 코드

TDD(Test-Driven Development)는 실제 코드를 짜기 전에 단위 테스트를 먼저 작성하는 기법으로,
이를 통해 유연성, 유지보수성, 재사용성을 제공받는다. TDD의 핵심 규칙 3가지는 다음과 같다.


1. 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
2. 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
3. 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.
 
테스트 코드가 없다면 실제 코드를 변경할 때 잠정적인 버그가 발생할 수 있음을 내포하지만,
테스트 코드가 있다면 변경된 코드를 검증함으로써 이를 해결할 수 있다.
그리고 실제 코드가 변경되면 테스트 코드 역시 변경 해주어야 하는데, 이러한 이유로 테스트 코드 역시 가독성 있게 작성하는 것이 중요하며, 테스트 코드를 작성할 때에는 다음 내용을 준수하는 것이 좋다. 


1. 테스트 함수 하나당 한 개념만을 테스트하라.
2. 한 개념 당 assert문의 수를 최소화하라.
 
또한, 깨끗한 테스트 코드는 F.I.R.S.T라는 5가지 규칙을 따른다.
1. Fast : 테스트는 빠르게 동작해야 한다.
2. Independent : 각 테스트는 서로 의존해선 안되며, 독립적으로 그리고 아무 순서로 실행해도 괜찮아야 한다.
3. Repeatable : 테스트는 어떤 환경에서도 반복 가능해야 한다.
4. Self-Validating : 테스트는 성공 또는 실패로 bool 값으로 결과를 내어 검증해야 한다.
5. Timely : 테스트는 적시에 즉, 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다.
 
 

💡 변경하기 쉬운 클래스

요구사항은 수시로 변할 수 있다. 그렇기 때문에 변경하기 쉬운 클래스를 만드는 것은 중요하다.
변경하기 쉬운 클래스는 기본적으로 단일 책임 원칙을 지키며, 구현체보다는 추상체에 의존한다.

public class Sql {
    public Sql(String table, Column [] columns) 
    public String create()
    public String insert(Object [] fields)
    public String select (Column column, String pattern) 
    public String select(Criteria criteria) 
    private String valuesList(Object[] fields, final Column[] columns) 
}

위 예시 코드는 새로운 SQL문을 생성하거나 select 문에 내장된 select 문을 지원할 때, 기존 SQL문을 수정할 때 등
Sql 클래스를 고쳐야 하므로 단일 책임 원칙을 위반한다. 

abstract public class Sql {
    public Sql(String table, Column[] columns)
    abstract public String generate();
}

public class CreateSql extends Sql {
    public CreateSql(String table, Column[] columns)
    @Override public String generate()
}

public class SelectSql extends Sql {
    public SelectSql(String table, Column[] columns)
    @Override public String generate()
}

public class InsertSql extends Sql {
    public InsertSql(String table, Column[] columns, Object[] fields)
    @Override public String generate()
    private String valuesList(Object[] fields, final Column[] columns)
}

public class SelectWithCriteriaSql extends Sql {
    public SelectWithCriteriaSql(String table, Column[] columns, Criteria criteria)
    @Override public String generate()
}

public class SelectWithMatchSql extends Sql {
    public SelectWithMatchSql(String table, Column[] columns, String pattern)
    @Override public String generate()
}

변경하기 쉬운 클래스를 만들기 위해 단일 책임 원칙을 지키며 추상 클래스와 상속을 활용한 예시이다.
클래스의 역할을 더 명확하게 분리하여 유연성과 확장성이 높아진다.
그리고 새로운 요구사항이나 변경이 발생했을 때 특정 부분만 수정하거나 확장하는 작업이 더 효율적이게 된다.
 
 

💡 적절한 추상화 수준에서 이름을 선택하라

적절한 이름 선택은 코드의 가독성과 유지보수성을 크게 영향을 미치는 요소이다.
구현 세부사항을 드러내는 이름보다는 작업 대상 클래스나 함수가 위치하는 추상화 수준을 반영하는 이름을 선택하는 것이 중요하다.
구현을 드러내는 이름은 피하고, 작업 대상 클래스나 함수가 위치하는 추상화 수준을 반영하는 이름을 선택하자.
아래 인터페이스에서는  "phoneNumber"라는 구체적인 구현을 드러내고 있다.

public interface Modem {
    boolean dial(String phoneNumber);
    boolean disconnect();
    boolean send(char c);
    char recv();
    String getConnectedPhoneNumber();
}

더 나은 이름 선택 전략은 다음과 같다.

public interface Modem {
    boolean connect(String connectionLocator);
    boolean disconnect();
    boolean send(charc);
    char recv();
    String getConnectedLocator(); 
}

위 코드는 연결 대상의 이름을 "connectionLocater"로 변경하여 전화번호라는 구체적인 개념에 국한되지 않도록 하며, 이렇게 함으로써 다양한 연결 방식에도 유연하게 사용될 수 있다.
 
 

마무리 하며

이렇게 클린 코드를 읽으며 몇 가지 내용들을 정리 해보았는데, 지나쳐버린 중요한 내용들도 더 있을 것 같다.
앞으로 지속적으로 다시 읽어보며 새롭게 보이는 핵심 내용들을 이 글에 추가적으로 정리 할 예정이다.
 

반응형

블로그의 정보

슬기로운 개발생활

coco3o

활동하기