본문 바로가기

[클린 코드] 요약 및 정리

by hseong 2023. 5. 19.

0. 서론


스프링에 대해 배우고 혼자서 프로젝트를 계속 진행하면서 변수, 메서드 이름이나 구조에 대한 고민이 많이 부족했습니다. ‘다른 사람들이 나의 코드를 읽기 쉽게 하려면 어떻게 해야할까?’ 라는 고민 아래 우선 대표적인 개발 서적인 로버트 C. 마틴의 클린 코드를 읽고 좋은 코드에 대해 생각하고, 이를 작성하기 위한 방법이 어떤 것들이 있는지 정리해보았습니다.

 

1. 클린 코드란 무엇인가


프로그램이 작동하는 것을 넘어 깨끗하고 체계적인 코드, 지속적인 개선이 가능한 코드가 좋은 코드이다.

 

2. 클린 코드를 위한 방법들


1) 의미 있는 이름

모든 변수, 메서드, 클래스는 의미 있는 이름을 가져야 한다. 내가 작성한 코드를 다른 개발자들이 명확히 파악할 수 있도록 충분히 고민하고 작성해야 한다.

  • 이름은 명료하고, 일관성 있게 지어야 한다.
  • 만일 해당 문제를 해결하기 위해 디자인 패턴을 적용하였으면 해당 패턴의 이름을 가져와 사용하는 것도 좋다.
  • 이름이 의미 있는 맥락을 가지도록 충분히 고민하여야 한다.

 

2) 함수

함수는 하나의 행위만 수행하도록, 충분히 작게 만들어야 한다. 그리고 이렇게 만든 함수는 위에서 아래로, 마치 이야기를 읽듯이 전개되어야 한다.

  • 서술적인 이름을 통해 함수가 어떤 기능을 하는지 파악할 수 있도록 하라.
  • 함수 인수는 적으면 적을수록 좋다. 만일 3개가 넘어간다면 함수가 너무 많은 행위를 수행하고 있지는 않은지 고민해야 한다.
  • 함수에 플래그 인수 즉, boolean 값을 넘기지 마라. 이는 함수가 여러 가지를 처리한다는 의미이다.
  • 함수는 객체의 상태를 변경하거나, 객체 정보를 반환하거나 둘 중 하나만 수행해야 한다.

 

3) 주석

주석은 나쁜 코드를 보완하지 못한다. 만일 내가 작성한 코드가 나쁜 코드라면 새로 짜는게 올바르다. 항상 코드만으로 의도를 표현할 수 있도록 고민하는 것에 우선 순위를 두어야 한다.

좋은 주석의 몇가지 예

  • 정보를 제공하는 주석
// kk:mm:ss EEE, MMM dd, yyyy 형식이다.
Patter timeMatcher = Pattern.compile("\\\\d*:\\\\d*:\\\\d* \\\\w*, \\\\w* \\\\d*, \\\\d*");
  • 의도를 설명하는 주석
public int compareTo(Object o)
{
    if(o instanceof WikiPagePath)
    {
        wikiPagePath p = (WikiPagePath) o;
        String compressedName = StringUtil.join(names, "");
        String compressedArgumentName = StringUtil.join(p.names, "");
        return compressedName.compareTo(compressedArgumentName);
    }
    return 1; // 옳은 유형이므로 정렬 순위가 더 높다.
}
  • 의미를 명확하게 밝히는 주석
assertTrue(a.compareTo(a) == 0);	//a == a
assertTrue(a.compareTo(b) != 0);	//a != b
assertTrue(a.compareTo(b) == -1);	//a < b
  • 결과를 경고하는 주석
public static SimpleDateFormat makeStandardHttpDateFormat()
{
    // SimpleDateFormat은 스레드 안전하지 못하다.
    // 따라서 각 인스턴스를 독립적으로 생성해야 한다.
    SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
    df.setTimeZone(TimeZone.getTimeZone("GMT"));
    return df;
}
  • 중요성을 강조하는 주석
String listItemContent = match.group(3).trim();
// trim은 정말 중요하다. trim 함수는 문자열에서 시작 공백을 제거한다.
// 문자열에 시작 공백이 있으면 다른 문자열로 인식되기 때문이다.

 

5) 형식 맞추기

코드 형식은 의사소통의 일환이다. 오늘 구현한 코드의 가독성은 앞으로 바뀔 코드의 품질에 지대한 영향을 미친다.

적절한 행 길이를 유지하라

  • 각 파일의 크기가 200줄 정도만 되도 커다란 시스템을 구축할 수 있다.

신문 기사처럼 작성하라

  • 이름만 보고도 모듈의 역할에 대해 명확히 파악할 수 있도록 신경써야 한다.

개념은 빈 행으로 분리하라

  • 일련의 행 묶음은 완결된 생각 하나를 표현한다.

세로 밀집도

  • 서로 밀접한 코드 행은 가까이 놓여야 한다.
  • 연관성이 깊은 두 개념이 멀리 떨어져 있으면 소스 파일 여기 저기를 뒤져야 한다.

세로 순서

  • 함수 호출 종속성은 아래 방향으로 유지한다.
  • 중요한 개념을 먼저 표한하고 세세한 개념은 나중에 표현한다.

들여쓰기

  • 간단한 if 문, 짧은 함수에서도 항상 범위를 제대로 표시하라

팀 규칙

  • 팀은 한 가지 규칙에 합의하고, 모든 팀원은 규칙을 따라야 한다.

 

6) 객체와 자료 구조

디미터 법칙

  • 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다.
  • 객체가 자신이 몰라야 하는 여러 객체를 탐색하면서 내부 구조를 드러낸다면 이를 위한 메서드를 정의하는 것이 더 옳다.
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

 

7) 오류 처리

깨끗한 코드는 읽기 좋을 뿐만 아니라 안정성도 높아야 한다. 오류 처리를 논리와 분리해 독자적인 사안으로 고려하면 튼튼하고 깨끗한 코드를 작성할 수 있다.

외부 API를 사용한다면 이를 감싸라

  • 외부 API를 감싸면 외부 라이브러리와 프로그램 사이에서 의존성이 크게 줄어든다. 나중에 다른 라이브러리로 갈아타도 비용이 적을 뿐만 아니라 테스트하기도 쉬워진다.
LocalPort port = new LocalPort(12);
try {
	port.open();
} catch (ProtDeviceFailure e) {
	reportPortError(e);
	logger.log(e.getMessage(), e);
} finally {
	...
}

public class LocalPort {
	private ACMEPort innerPort;

	public LocalPort(int portNumber) {
		innterPort = new ACMEPort(portNumber);
	}

	public void open() {
		try {
			innerPort.open();
		} catch (DeviceResponseException e) {
			reportPortError(e);
			throw new PortDeviceFailure(e);
		} catch (ATM1212UnlockedException e) {
			reportPortError(e);
			throw new PortDeviceFailure(e);
		} catch (GMXError e) {
			reportPortError(e);
			throw new PortDeviceFailure(e);
		} finally {
			...
		}
	}

null을 반환하지 마라

  • null을 반환하는 코드는 일거리를 늘릴 뿐만 아니라 호출자에게 문제를 떠넘긴다.
  • 누구 하나라도 null 체크를 빼먹는다면 애플리케이션은 통제 불능에 빠질 수 있다.
  • 메서드에서 null을 반환하고 싶은 유혹이 든다면 예외를 던지거나 특수 사례 객체를 반환한다.

null을 전달하지 마라

  • 정상적인 인수로 null을 기대하는 API가 아니라면 메서드로 null을 전달하는 코드는 최대한 피한다.

 

8) 경계

통제하지 못하는 외부 코드를 이용할 때는 향후 변경 비용이 커지지 않도록 주의해야 한다. 통제 불가능한 외부 코드에 의존하기 보다는 통제 가능한 우리 코드에 의존해야 한다. 새로운 클래스를 생성해 경계를 감싸거나 어댑터 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자.

학습 테스트

  • 우리가 사용할 외부 코드는 테스트하고 사용하는 편이 바람직하다.
  • 곧바로 외부 코드를 호출하기보다는 간단한 테스트 케이스를 작성해 외부 코드를 익히는 학습 테스트의 시간을 갖도록 하자.
  • 이러한 학습 테스트를 사용한다면 외부 패키지의 버전이 변해도 새로운 버전으로 이전하기 쉬워진다. 학습 테스트를 돌려보고 이전과 동일하다면 그대로 사용하면 되고, 다르다면 새로운 테스트를 작성하면 된다.

아직 존재하지 않는 코드 사용하기

  • 구체적인 API를 모른다면 경계를 파악하고 인터페이스를 작성한다.
  • 우리만의 인터페이스를 구현한다면 우리가 직접코드를 통제할 수 있다. 코드 가독성, 의도가 명확해지고 테스트 또한 적절한 클래스를 정의하여 수월하게 진행할 수 있다.
  • API가 정의된 이후에는 어댑터 패턴으로 API 사용을 캡슐화하고 API 변경 시 수정할 코드를 한곳으로 모으면 된다.

 

9) 단위 테스트

충분한 테스트 케이스가 있다면 코드 개선에 대한 두려움을 가질 필요 없다. 리팩토링을 수행하고 테스트를 통과하지 못한다면 원래대로 되돌리면 그만이다.

TDD 법칙

  1. 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
  2. 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
  3. 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

깨끗한 테스트 코드 유지하기

  • 테스트 코드가 지저분할수록 변경하기 어려워진다.
  • 실제 코드가 진화하면 테스트 코드도 변해야 한다.
  • 테스트 케이스가 없으면 모든 변경이 잠정적인 버그이다.
  • 실제 코드를 점검하는 자동화된 단위 테스트 슈트는 설계와 아키텍처를 최대한 깨끗하게 보존하는 열쇠다.

깨끗한 테스트 코드

  • 무엇보다도 가독성이 중요하다.
  • 테스트는 세 부분으로 나누어질 수 있다.
    1. 테스트 자료를 만든다.
    2. 테스트 자료를 조작한다.
    3. 조작한 결과가 올바른지 확인한다.
  • 잡다하고 세세한 코드를 없애고 진짜 필요한 자료 유형과 함수만 사용하라.

도메인에 특화된 테스트 언어

  • API 위에 함수와 유틸리티를 구현하여 테스트에 특화된 특수 API를 만들자.
  • 잡다한 사항으로 범벅되더라도 리팩토링을 통해 점진적으로 진화시켜나가면 된다.

테스트 당 개념 하나

  • assert 문이 단 하나인 함수는 결론이 하나라서 코드를 이해하기 쉽고 빠르다.
  • 다만 중복되는 코드가 많아질 수 있기 때문에 테스트 당 하나의 개념을 테스트 한다고 생각하고 assert문은 최대한 줄이겠다는 지침은 훌륭할 수 있다.
  • assert문이 여럿인건 문제되지 않으나 여러 개념을 테스트하는 건 문제이다.

F.I.R.S.T

  • 빠르게(Fast)
    • 테스트는 빨라야 한다.
    • 자주 돌리지 않으면 초반에 문제를 찾아내 고치지 못한다.
  • 독립적으로(Independent)
    • 각 테스트는 서로 의존하면 안 된다.
    • 한 테스트가 다음 테스트의 실행 환경을 준비해서는 안 된다.
    • 테스트가 서로 의존하면 하나가 실패했을 때, 나머지도 연달아 실패하고 후반의 문제를 찾아내기 어려워진다.
  • 반복가능하게(Repeatable)
    • 테스트는 어떤 환경에섣 반복 가능해야 한다.
    • 테스트가 돌아기지 않는 환경이 하나라도 있다면 테스트가 실패한 이유를 둘러댈 변명이 생긴다.
  • 자가검증하는(Self-Validation)
    • 테스트는 불 값으로 결과를 내야 한다. 성공 아니면 실패다.
    • 테스트가 스스로 성공, 실패 결과를 가능하지 않는다면 주관적 판단과 지루한 수작업 평가가 필요하다.
  • 적시에(Timely)
    • 테스트는 적시에 작성해야 한다.
    • 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.
    • 실제 코드를 구현한 다음에 테스트 코드를 만들면 실제 코드가 테스트하기 어렵다는 사실을 발견할지도 모른다.
    • 테스트가 불가능하도록 실제 코드를 설계할지도 모른다.

 

10) 클래스

클래스는 작아야 한다. 클래스의 이름은 해당 클래스의 책임을 기술하며 적절한 이름이 떠오르지 않는다면 이는 클래스의 책임이 너무 많다는 뜻이다.

단일 책임 원칙(Single Responsibility Principle, SRP)

  • 클래스는 책임, 변경할 이유가 단 하나여야 한다.
  • 책임을 파악하려다 애쓰다 보면 코드를 추상화학도 쉬워진다.
  • 작동하는 소프트웨어라는 관심사를 해결했다면 깨끗하고 체계적인 소프트웨어라는 다음 관심사로 전환해야 한다. 만능 클래스 하나를 단일 책임 클래스 여럿으로 분리해야 한다.
  • 작은 클래스는 각자 맡은 책임이 하나이며, 변경할 이유가 하나이며, 다른 작은 클래스와 협력해 시스템에 필요한 동작을 수행한다.

응집도(Cohension)

  • 클래스는 인스턴스 변수 수가 작아야 한다.
  • 각 클래스 메서든느 클래스 인스턴스 변수를 하나 이상 사용해야 한다.
    • 메서드가 변수를 더 많이 사용할수록 메서드와 클래스는 응집도가 더 높다.
  • 응집도가 높다는 말은 클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 의미이다.
  • 만일 몇몇 함수가 몇몇 변수만 사용한다면, 서로 사용하는 변수들이 달라 응집도가 낮다면 더욱 작은 클래스 여럿으로 분리할 수 있다.

변경하기 쉬운 클래스

  • 어떤 변경이든 클래스에 손대면 다른 코드를 망가뜨릴 잠정적인 위험이 존재한다. 테스트도 처음부터 다시 해야한다.
  • 미래에 변경할 가능성이 있는 코드라면, 기존 코드를 변경하지 않고 시스템을 확장 가능한 코드가 이상적이다.
  • 구현에 의존하는 클라이언트 클래스는 구현이 바뀌면 위험에 빠진다. 그렇기에 인터페이스와 추상 클래스를 사용해 구현이 미치는 영향을 최소화한다.
  • 상세한 구현에 의존한다면 테스트 코드의 작성 역시 어렵다.
    • 외부 API의 값이 계속해서 변경된다면 테스트 코드의 결과를 예측할 수 없다.
  • 따라서 내부 클래스는 인터페이스에 의존하고 상세한 구현은 알지 못하게 한다. 구체적인 것보다는 추상적인 것에 의존하라. 그렇게 하면 테스트 코드 역시 외부 API를 흉내내는 테스트용 클래스를 작성해 테스트 작성 역시 수월해질 것이다.

 

11) 창발성

모든 테스트를 실행하라

  • 테스트 케이스가 많을수록 개발자는 테스트가 쉽게 코드를 작성한다. 철저한 테스트가 가능한 시스템을 만들면 더 나은 설계를 얻을 수 있다.
  • 테스트 케이스를 많이 작성할수록 개발자는 DIP와 같은 원칙을 적용하고 의존성 주입, 인터페이스, 추상화 등과 같은 도구를 사용해 결합도를 낮춘다. 따라서 설계 품질은 더욱 높아진다.

리팩토링

  • 테스트 케이스를 모두 작성했다면 점진적으로 코드를 리팩토링 해나간다.
  • 충분한 테스트 케이스가 있다면 코드를 정리하며 시스템이 깨질 거정을 하지 않아도 된다. 코드를 정리하고 테스트 케이스로 검증을 수행하면 된다.

중복을 제거하라

  • 중복은 추가 작업, 추가 위험, 불필요한 복잡도를 뜻한다.
  • 깔끔한 시스템을 만들려면 단 몇 줄이라도 중복을 제거하겠다는 의지가 필요하다.

표현하라

  • 코드는 개발자의 의도를 명확히 표현해야 한다. 코드를 명확하게 짤수록 다른 사람이 이해하기 쉬워지고 유지보수 비용이 감소한다.
    1. 좋은 이름을 선택한다.
    2. 함수와 클래스 크기를 가능한 줄인다.
    3. VISITOR와 같은 표준 패턴을 사용해 구현한다면 클래스 이름에 패턴 이름을 넣어준다. 그러면 다른 개발자가 클래스 설계 의도를 이해하기 쉬워진다.
    4. 단위 테스트 케이스를 꼼꼼히 작성한다. 테스트 케이스는 ‘예제로 보여주는 문서’다. 잘 만든 테스트 케이슬르 읽어보면 클래스 기능이 한눈에 들어온다.
  • 표현력을 높이는 가장 중요한 방법은 노력이다.
  • 함수와 클래스에 조금 더 시간을 투자하자. 더 나은 이름을 선택하고, 큰 함수를 작은 함수 여럿으로 나누고, 자신의 작품에 조금 더 주의를 기울이자.

클래스와 메서드 수를 가능한 줄여라.

  • 단, 테스트 케이스를 만들고, 중복을 제거하고, 의도를 표현하는 작업이 우선시 되어야 한다.