Clean Code 핵심 정리

Clean Code는 읽기 쉽고, 이해하기 쉽고, 수정하기 쉬운 코드를 작성하기 위한 개발 원칙이다.

대표적으로 로버트 C. 마틴(Robert C. Martin)의 저서 Clean Code를 통해 널리 알려졌지만, 특정 문법이나 라이브러리를 의미하는 것은 아니다. 코드의 구조와 이름, 함수의 크기, 책임 분리, 의존성 관리처럼 소프트웨어를 유지보수하는 과정에서 발생하는 문제를 다루는 개념에 가깝다.

코드는 한 번 작성하고 끝나는 경우보다 계속 수정되는 경우가 많다. 새로운 기능을 추가하고, 버그를 수정하고, 기존 정책을 변경하다 보면 처음에는 단순했던 코드도 점점 복잡해진다.

Clean Code의 목적은 코드를 예쁘게 만드는 것이 아니다.

변경이 발생했을 때 코드를 이해하고 수정하는 비용을 낮추는 것에 가깝다.


Clean Code가 필요한 이유

코드는 컴퓨터가 실행하지만, 대부분의 개발 비용은 사람이 코드를 읽고 수정하는 과정에서 발생한다.

예를 들어 다음과 같은 코드가 있다고 하자.

if (a > 10 && b == 3 && c != nullptr)
{
    // ...
}

문법적으로 문제가 있는 코드는 아니다.

하지만 a, b, c가 무엇을 의미하는지 코드를 읽는 사람은 알기 어렵다.

반면 의미를 드러내는 이름을 사용하면 다음처럼 작성할 수 있다.

if (playerLevel > 10 &&
    selectedCharacterCount == 3 &&
    player != nullptr)
{
    // ...
}

코드 자체는 조금 길어졌지만 읽는 데 필요한 추론은 줄어든다.

Clean Code에서 말하는 가독성은 단순히 코드가 짧은 것을 의미하지 않는다.

코드를 읽었을 때 개발자가 머릿속에서 수행해야 하는 추론을 줄이는 것도 가독성의 중요한 부분이다.


좋은 이름 사용하기

Clean Code에서 가장 먼저 신경 써야 할 부분 중 하나가 이름이다.

변수, 함수, 클래스의 이름은 코드에 대한 설명 역할을 한다.

int d;

이름만 봐서는 아무것도 알 수 없다.

int elapsedDays;

이제 변수의 의미를 어느 정도 알 수 있다.

함수도 마찬가지다.

void Process();

무엇을 처리하는지 알 수 없다.

void CalculatePlayerDamage();

함수가 어떤 일을 하는지 훨씬 명확하다.

좋은 이름을 사용하면 주석이 필요해지는 상황도 줄어든다.

다만 이름을 지나치게 길게 만드는 것도 좋은 방법은 아니다.

int numberOfCurrentlyAlivePlayersInTheCurrentGameRoom;

이 정도면 이름 자체가 읽기 어려워진다.

중요한 것은 짧은 이름과 긴 이름의 선택이 아니라, 이름만 보고 역할을 추론할 수 있는가다.


함수는 하나의 책임을 갖도록 만든다

함수에 너무 많은 역할이 들어가면 코드를 이해하기 어려워진다.

예를 들어 다음과 같은 함수가 있다고 하자.

void CreatePlayer()
{
    ValidateInput();
    LoadPlayerData();
    CreateDatabaseRecord();
    SendWelcomeMessage();
    LogPlayerCreation();
}

이 함수 자체가 반드시 잘못된 것은 아니다.

문제는 CreatePlayer()가 실제로 어떤 책임까지 가지고 있는지다.

입력 검증, 데이터 로딩, DB 처리, 메시지 전송, 로깅까지 모두 직접 처리하기 시작하면 함수 하나를 수정할 때 확인해야 할 영역이 많아진다.

이를 적절한 책임 단위로 나누면 구조가 명확해진다.

void CreatePlayer()
{
    ValidatePlayer();
    PlayerData data = LoadPlayerData();
    SavePlayer(data);
    SendWelcomeMessage(data);
    LogPlayerCreation(data);
}

각 함수가 구체적인 구현을 담당하고 상위 함수는 전체 흐름을 표현한다.

이런 구조는 코드를 읽을 때도 유리하다.

플레이어 생성 -> 검증 -> 데이터 로드 -> 저장 -> 메시지 전송 -> 로그 기록

함수의 이름만 읽어도 전체 흐름을 파악할 수 있다.


함수의 크기보다 책임이 중요하다

Clean Code를 이야기할 때 흔히 “함수는 짧아야 한다”는 식으로 이해하기 쉽다.

짧다고 무조건 좋은 것은 아니다.

다음 코드가 있다고 하자.

void Update()
{
    UpdatePosition();
    UpdateAnimation();
    UpdateAI();
    UpdateInventory();
    UpdateQuest();
    UpdateNetwork();
}

함수의 줄 수는 많지 않지만 역할은 상당히 많다.

반대로 다음과 같은 함수는 조금 길더라도 하나의 명확한 책임을 수행할 수 있다.

void CalculateDamage(...)
{
    // 공격력 계산
    // 방어력 적용
    // 치명타 처리
    // 최종 피해량 보정
}

여기서는 모든 코드가 “피해량 계산”이라는 하나의 목적을 가지고 있다.

따라서 함수의 품질을 단순히 줄 수로 판단하면 안 된다.

함수가 하나의 명확한 목적을 가지고 있는지가 더 중요하다.


중복을 줄인다

같은 로직이 여러 곳에 반복되면 나중에 수정하기 어려워진다.

player.hp -= damage;
player.hp = std::max(player.hp, 0);

이 코드가 여러 시스템에 반복된다고 해보자.

체력 처리 규칙이 변경되면 모든 위치를 찾아 수정해야 한다.

void ApplyDamage(Player& player, int damage)
{
    player.hp -= damage;
    player.hp = std::max(player.hp, 0);
}

이제 체력 감소에 대한 규칙을 한 곳에서 관리할 수 있다.

다만 중복 제거를 무조건 함수로 만드는 것도 좋은 것은 아니다.

겉보기에는 비슷하지만 서로 다른 의미를 가진 코드까지 억지로 하나로 합치면 오히려 결합도가 높아질 수 있다.

따라서 DRY(Don’t Repeat Yourself)는 단순히 똑같은 코드가 보이면 무조건 합친다는 의미로 이해하면 곤란하다.

중복되는 지식과 변경 이유가 같은지를 보는 것이 더 중요하다.


주석은 코드의 부족한 부분을 보완하는 용도로 사용한다

주석 자체가 나쁜 것은 아니다.

문제는 코드가 설명해야 할 내용을 주석으로 대신하는 것이다.

// 플레이어의 체력을 0보다 작아지지 않게 한다.
player.hp = std::max(player.hp, 0);

코드만 읽어도 충분히 알 수 있다.

이런 주석은 코드와 함께 유지보수해야 하는 정보가 하나 더 생긴다.

반면 구현의 이유를 설명하는 주석은 가치가 있다.

// 서버와 클라이언트의 판정 차이를 방지하기 위해
// 피해량 계산은 서버 기준값을 사용한다.

이런 내용은 코드만 봐서는 알기 어렵다.

따라서 주석은 무엇을 하는지보다 왜 그렇게 해야 하는지를 설명하는 방향이 유용하다.


조건문도 읽기 쉽게 만든다

조건문이 복잡해지면 로직 자체보다 조건을 해석하는 데 시간이 걸린다.

if (player != nullptr &&
    player->IsAlive() &&
    player->GetLevel() >= 10 &&
    !player->IsBlocked() &&
    player->HasPermission())
{
    // ...
}

조건 자체가 틀린 것은 아니지만 의미가 한눈에 들어오지 않는다.

복잡한 조건에는 의미 있는 이름을 부여할 수 있다.

bool canEnterDungeon =
    player != nullptr &&
    player->IsAlive() &&
    player->GetLevel() >= 10 &&
    !player->IsBlocked() &&
    player->HasPermission();

if (canEnterDungeon)
{
    // ...
}

조건의 세부 구현과 실제 의도가 분리된다.

물론 모든 조건을 변수로 빼야 하는 것은 아니다.

한 줄로 충분히 이해되는 조건이라면 굳이 분리할 필요도 없다.


부작용을 줄인다

함수 이름만 보고 예상하지 못한 동작이 발생하면 코드를 이해하기 어려워진다.

예를 들어

GetPlayer()

라는 이름의 함수가 내부에서 DB에 접근하고 캐시를 변경하고 로그까지 남긴다면 호출하는 입장에서는 예상하기 어렵다.

함수의 이름과 실제 동작 사이에 차이가 커질수록 코드의 예측 가능성이 떨어진다.

특히 게임 서버나 대규모 애플리케이션에서는 이런 문제가 커진다.

UpdatePlayer() -> 플레이어 상태 변경 -> DB 저장 -> 네트워크 패킷 전송 -> 이벤트 발생 -> 로그 기록

UpdatePlayer() 하나를 호출했는데 내부에서 여러 부작용까지 수행한다면 해당 함수를 호출하는 코드가 예상하지 못한 결과를 만들어낼 가능성이 높다.

함수의 책임과 부작용을 명확하게 관리하는 것이 중요하다.


오류 처리를 코드 구조에 포함한다

오류 처리를 정상적인 로직과 섞어 놓으면 코드의 핵심 흐름이 흐려진다.

if (player == nullptr)
{
    return false;
}

if (!player->IsAlive())
{
    return false;
}

if (!player->HasPermission())
{
    return false;
}

// 실제 작업

이런 구조는 흔히 사용할 수 있다.

실패 조건을 먼저 제거하고 정상적인 흐름을 아래쪽에 배치하면 코드의 주된 동작을 읽기 쉬워진다.

오류 처리 방식 자체도 프로젝트의 규칙에 맞춰야 한다.

C++에서는 예외, 반환값, std::optional, std::expected 등의 선택지가 있고 C#에서는 예외나 Result 계열 패턴 등을 사용할 수 있다.

Clean Code가 특정 오류 처리 방식을 강제하는 것은 아니다.

중요한 것은 오류가 발생했을 때 그 사실을 숨기지 않고 코드 구조에서 명확하게 드러내는 것이다.


클래스와 모듈의 책임을 분리한다

함수 수준에서 책임을 분리했다면 더 큰 단위에서도 같은 원칙을 적용할 수 있다.

예를 들어 게임에서 플레이어 관련 클래스가

Player -> 이동 / 전투 / 인벤토리 / 퀘스트 / DB 저장 / 네트워크 전송 / 로그

모든 것을 직접 관리하기 시작하면 클래스가 비대해진다.

클래스가 커질수록 변경 이유도 많아진다.

전투 시스템을 수정했는데 플레이어 클래스가 영향을 받고, DB 구조를 변경했는데 플레이어 클래스까지 수정해야 하는 식이다.

따라서 역할을 적절히 분리할 수 있다.

Player / CombatSystem / Inventory / QuestSystem / PlayerRepository / NetworkService

다만 이것 역시 무조건 클래스를 잘게 쪼개라는 의미는 아니다.

클래스를 너무 많이 만들면 객체 간 관계가 복잡해지고 오히려 코드를 따라가기 어려워질 수 있다.

응집도는 높이고 결합도는 낮추는 방향으로 판단하는 것이 좋다.


추상화는 목적이 있어야 한다

Clean Code와 함께 SOLID를 공부하다 보면 인터페이스와 추상화를 많이 사용하게 된다.

하지만 인터페이스를 많이 만든다고 좋은 설계가 되는 것은 아니다.

예를 들어 구현체가 하나뿐이고 교체 가능성도 없는데

class IPlayerService
{
public:
    virtual void Save() = 0;
};

를 만들고

class PlayerService : public IPlayerService
{
};

로 구성하는 것이 항상 좋은 것은 아니다.

추상화는 변경 가능성이 있는 부분을 분리하거나 의존성을 제어할 필요가 있을 때 의미가 있다.

특히 다음과 같은 상황에서 유용하다.

게임 로직 -> 인터페이스 -> 실제 저장소

실제 저장소가

MySQL / Redis / Memory / Mock

등으로 바뀔 가능성이 있다면 추상화가 유용할 수 있다.

반대로 변경 가능성이 없는 단순한 코드에 추상화를 추가하면 구조만 복잡해진다.

Clean Code에서 말하는 좋은 구조는 추상화가 많은 구조가 아니라 변경에 적절하게 대응할 수 있는 구조에 가깝다.


SOLID와의 관계

Clean Code를 공부하다 보면 SOLID 원칙과 자연스럽게 연결된다.

대표적인 원칙은 다음과 같다.

S - Single Responsibility Principle / O - Open/Closed Principle / L - Liskov Substitution Principle / I - Interface Segregation Principle / D - Dependency Inversion Principle

그중 Clean Code와 직접적으로 연결해서 이해하기 쉬운 것은 SRP와 DIP다.

Single Responsibility Principle

하나의 클래스나 모듈이 여러 가지 변경 이유를 가지지 않도록 책임을 분리한다.

Player / PlayerRepository / PlayerService

처럼 역할을 분리하는 이유가 여기에 있다.

Dependency Inversion Principle

상위 수준의 정책이 구체적인 구현에 직접 의존하지 않도록 의존성을 관리한다.

GameLogic -> IPlayerRepository -> MySQLPlayerRepository / RedisPlayerRepository

게임 로직은 MySQL이라는 구체적인 기술보다 플레이어 데이터를 저장한다는 추상적인 역할에 의존할 수 있다.

다만 SOLID 역시 체크리스트처럼 기계적으로 적용하면 문제가 생긴다.

설계 원칙은 코드의 복잡성을 줄이기 위해 사용하는 도구이지, 지켜야 할 종교적 규칙이 아니다.


리팩토링과 Clean Code

Clean Code는 처음부터 완벽한 코드를 작성하는 것만을 의미하지 않는다.

오히려 실제 개발에서는 기존 코드를 계속 리팩토링하는 과정이 중요하다.

처음에는 다음처럼 작성할 수도 있다.

void Process(Player& p)
{
    // 복잡한 처리
}

기능이 완성된 뒤 코드를 다시 읽어보고

void ProcessPlayer(Player& player)
{
    ValidatePlayer(player);
    UpdateStats(player);
    SavePlayer(player);
}

처럼 역할을 분리할 수 있다.

리팩토링의 목적은 동작을 변경하지 않으면서 코드의 구조를 개선하는 것이다.

대표적인 작업으로는

이름 변경 / 함수 분리 / 중복 제거 / 조건문 단순화 / 클래스 책임 분리 / 의존성 정리 / 불필요한 코드 제거

등이 있다.

테스트가 존재하면 리팩토링하기 훨씬 편해진다.

기존 동작이 유지되는지 자동으로 검증할 수 있기 때문이다.


Clean Code에서 흔히 생기는 오해

Clean Code를 공부하면서 가장 조심해야 하는 부분도 있다.

짧은 코드가 좋은 코드다

항상 그렇지는 않다.

짧지만 의미를 이해하기 어려운 코드보다 조금 길더라도 의도가 명확한 코드가 유지보수하기 좋다.

함수는 무조건 작아야 한다

작게 만드는 것 자체가 목적이 아니다.

하나의 책임을 명확하게 표현할 수 있다면 어느 정도 길이가 있어도 문제가 되지 않는다.

클래스는 작을수록 좋다

클래스를 지나치게 분리하면 객체 간 의존성이 증가한다.

파일 수와 클래스 수가 늘어나는 것이 곧 좋은 설계는 아니다.

주석은 나쁘다

불필요한 주석이 문제다.

코드로 표현할 수 있는 내용을 주석으로 반복하는 것보다 코드만으로 표현하기 어려운 의도와 제약조건을 설명하는 것이 좋다.

디자인 패턴을 많이 사용해야 한다

디자인 패턴은 문제를 해결하기 위한 도구다.

패턴을 적용하기 위해 코드를 복잡하게 만드는 순간 목적이 뒤집힌다.


실무에서 어떻게 적용할 것인가

Clean Code를 한 번에 적용하려고 하면 오히려 부담이 커진다.

코드를 작성할 때 몇 가지 기준만 가지고 있어도 충분히 차이가 난다.

먼저 이름을 확인한다.

이 변수의 의미를 이름만 보고 알 수 있는가? / 이 함수가 무엇을 하는지 이름만 보고 알 수 있는가?

다음으로 책임을 확인한다.

이 함수가 여러 가지 일을 하고 있지는 않은가? / 이 클래스가 여러 이유로 변경되고 있지는 않은가?

그다음 중복을 본다.

같은 규칙이 여러 위치에 구현되어 있지는 않은가? / 이 로직을 변경하면 여러 파일을 수정해야 하는가?

마지막으로 의존성을 본다.

상위 로직이 구체적인 구현에 강하게 묶여 있지는 않은가? / 변경 가능성이 있는 부분이 적절하게 분리되어 있는가?

이 정도만 의식해도 코드를 보는 관점이 달라진다.


Clean Code를 보는 기준

결국 Clean Code는 특정 코딩 스타일을 의미하지 않는다.

프로젝트마다 언어도 다르고 팀의 규칙도 다르며 성능 요구사항도 다르다.

게임 클라이언트와 게임 서버의 코드 구조가 같을 필요도 없고, 임베디드 시스템과 웹 서비스의 코드 작성 방식이 같을 수도 없다.

그래서 다음과 같은 질문이 더 중요하다.

이 코드를 처음 보는 사람이 빠르게 이해할 수 있는가? / 변경이 발생했을 때 수정해야 하는 범위를 예측할 수 있는가? / 하나의 기능을 수정했을 때 예상하지 못한 곳에 영향을 주지 않는가? / 코드의 이름과 실제 동작이 일치하는가? / 불필요한 의존성과 중복이 존재하지 않는가? / 테스트하기 쉬운 구조인가?

이런 질문에 좋은 답을 내놓는 코드라면 굳이 특정 Clean Code 규칙을 모두 적용하지 않아도 충분히 좋은 코드가 될 수 있다.


정리

Clean Code의 목적은 코드를 보기 좋게 꾸미는 것이 아니다.

사람이 코드를 읽고 이해하고 변경하는 비용을 줄이는 데 있다.

이를 위해 기본적으로

의미 있는 이름 -> 명확한 함수 책임 -> 적절한 책임 분리 -> 불필요한 중복 제거 -> 예측 가능한 동작 -> 명확한 오류 처리 -> 적절한 추상화 -> 낮은 결합도와 높은 응집도 -> 지속적인 리팩토링

을 고려한다.

다만 Clean Code에는 절대적인 정답이 있는 것은 아니다.

함수를 몇 줄로 만들어야 하는지, 클래스를 어디까지 분리해야 하는지, 인터페이스를 언제 사용해야 하는지 같은 문제는 결국 코드가 해결해야 하는 문제와 변경 방향을 보고 판단해야 한다.

좋은 코드는 규칙을 많이 지킨 코드가 아니라 변경하기 쉬운 코드에 가깝다.

코드는 작성하는 순간보다 그 이후에 더 많이 읽힌다. Clean Code를 공부하는 이유도 결국 이 부분에 있다.