소프트웨어 장인정신: 시대를 초월하는 코드의 품격

소프트웨어 개발의 세계에서 '작동하는' 코드를 작성하는 것은 단지 여정의 시작일 뿐입니다. 진정한 전문성은 현재의 요구사항을 충족시키는 것을 넘어, 미래의 불확실성 속에서도 흔들림 없이 자신의 가치를 증명하는 코드를 창조하는 데서 비롯됩니다. 시간이 흘러도 그 가독성과 유지보수 용이성을 잃지 않는 코드, 마치 잘 쓰인 문학 작품처럼 다른 개발자에게 명확한 의도를 전달하고 영감을 주는 코드. 우리는 이러한 코드를 '클린 코드(Clean Code)'라 부릅니다.

클린 코드는 단순히 코딩 스타일이나 규칙의 집합이 아닙니다. 그것은 개발자로서의 책임감과 동료에 대한 배려, 그리고 자신이 만드는 소프트웨어에 대한 깊은 애정이 담긴 철학입니다. 기술 부채(Technical Debt)라는 보이지 않는 유령이 프로젝트를 서서히 잠식하는 것을 막는 가장 강력한 방패이며, 급변하는 비즈니스 환경 속에서 소프트웨어가 유연하게 진화할 수 있도록 하는 단단한 초석입니다. 이 글은 코드를 단순한 명령어의 나열이 아닌, 살아 숨 쉬는 유기체이자 정교한 설계의 산물로 여기는 모든 개발자를 위한 심도 깊은 탐험이 될 것입니다.

우리는 이름 짓기와 같은 가장 기본적인 행위에서부터 함수와 객체의 설계, 오류 처리, 그리고 전체 시스템의 구조에 이르기까지 클린 코드를 구성하는 다양한 원칙들을 구체적인 예시와 함께 파헤쳐 볼 것입니다. 이 여정을 통해 여러분은 단순히 '더 나은' 코드를 작성하는 기술을 넘어, 소프트웨어 장인으로서 자신의 작업에 자부심을 느끼고, 지속 가능한 소프트웨어를 만드는 지혜를 얻게 될 것입니다.

1부: 모든 것의 시작, 의미 있는 이름

코드에서 이름은 단순한 식별자를 넘어섭니다. 그것은 변수, 함수, 클래스가 존재하는 이유와 수행하는 역할을 담는 그릇입니다. 이름이 명확하지 않다는 것은 코드의 의도가 불분명하다는 뜻이며, 이는 곧 버그와 오해, 그리고 유지보수 비용의 증가로 이어집니다. 좋은 이름을 짓는 것은 프로그래밍의 가장 기본적인 행위이자, 가장 심오한 기술이기도 합니다. 마치 시인이 단어 하나하나를 신중하게 고르듯, 개발자는 이름 하나에 수많은 고민과 의도를 담아야 합니다.

의도를 분명히 밝히는 이름 (Intention-Revealing Names)

이름은 '왜 존재하는가', '무엇을 하는가', '어떻게 사용되는가'에 대한 답을 담고 있어야 합니다. 주석의 도움 없이도 이름 그 자체만으로 변수의 존재 이유를, 함수의 목적을, 클래스의 역할을 설명할 수 있어야 합니다. 다음 코드를 살펴보겠습니다.


// 나쁜 예
int d; // 경과 시간(단위: 날짜)

변수 d는 그것이 무엇을 의미하는지 전혀 알려주지 못합니다. 주석이 없다면 이 변수의 용도를 파악하기 위해 코드를 역추적해야만 합니다. 이는 인지적 부하를 가중시키고 코드 해석의 속도를 현저히 떨어뜨립니다. 이 변수는 다음과 같이 개선될 수 있습니다.


// 좋은 예
int elapsedTimeInDays;
int daysSinceCreation;
int fileAgeInDays;

elapsedTimeInDays, daysSinceCreation, fileAgeInDays와 같은 이름들은 변수의 의미, 측정 단위, 그리고 맥락까지 명확하게 전달합니다. 어떤 추가 설명도 필요 없습니다. 코드를 읽는 사람은 이 변수가 '생성된 후 경과된 날짜' 혹은 '파일이 생성된 후 지난 일수' 등을 나타낸다는 사실을 즉시 파악할 수 있습니다.

함수 이름 역시 마찬가지입니다. 고객 정보를 데이터베이스에서 가져오는 함수가 있다고 가정해 봅시다.


// 나쁜 예
public List<Customer> getThem() { ... }

getThem이라는 이름은 아무런 정보도 주지 않습니다. 'them'이 무엇이며, 어디서, 어떻게 가져오는 것일까요? 좋은 이름은 이러한 질문에 대한 답을 제공합니다.


// 좋은 예
public List<Customer> fetchActiveCustomersFromDatabase() { ... }

이 이름은 '데이터베이스에서 활성화된 고객 목록을 가져온다'는 함수의 목적을 명확히 설명합니다. 이름만으로도 함수의 동작을 유추할 수 있게 되어, 코드를 처음 접하는 개발자도 쉽게 그 역할을 이해할 수 있습니다.

그릇된 정보를 피하라 (Avoid Disinformation)

이름은 의도를 명확히 드러내는 것을 넘어, 적극적으로 오해를 불러일으키지 않아야 합니다. 프로그래머는 자신이 사용하는 단어의 정확한 의미에 대해 깊이 고민해야 합니다. 예를 들어, 실제로는 `List`가 아닌 다른 자료구조(가령 `Set`이나 `Map`의 키 집합)를 반환하면서 변수명에 `List`라는 단어를 포함시키는 것은 심각한 오해를 유발합니다.


// 나쁜 예: 실제로는 Set을 사용하지만 이름은 List이다.
Account[] accountList = new Account[...]; // 배열인데 List라는 이름을 쓴다.

위 코드에서 `accountList`는 실제로는 배열(Array)이지만, 이름에는 `List`가 포함되어 있습니다. 이는 개발자가 해당 변수를 `List` 인터페이스의 메서드(add(), remove() 등)로 다루려고 시도하게 만드는, 잠재적인 버그의 원인이 됩니다. 올바른 이름은 `accounts` 또는 `accountArray`가 될 것입니다.

또한, 서로 흡사한 이름을 사용하여 미묘한 차이를 두는 것도 피해야 합니다. 예를 들어, 한 모듈 안에서 `XYZControllerForEfficientHandlingOfStrings`와 `XYZControllerForEfficientStorageOfStrings`라는 이름의 클래스가 공존한다면, 개발자는 이 둘의 차이점을 파악하기 위해 상당한 시간을 소모하게 될 것입니다. 이름의 차이가 개념의 차이를 명확하게 반영하도록 신중하게 단어를 선택해야 합니다.

의미 있게 구분하라 (Make Meaningful Distinctions)

연속된 숫자를 덧붙이거나(`a1`, `a2`, `a3`) 의미 없는 불용어(noise word)를 추가하여(`Product` vs `ProductInfo` vs `ProductData`) 이름을 구분하는 것은 아무런 정보를 제공하지 못하는 나쁜 습관입니다. 컴파일러는 이러한 구분을 허용하지만, 코드를 읽는 사람에게는 아무런 의미도 전달하지 못합니다.

만약 `Product` 클래스가 있고, 여기서 파생된 정보를 담는 또 다른 클래스가 필요하다면 `ProductInfo`나 `ProductData`와 같은 이름은 적절하지 않습니다. `Info`나 `Data`는 너무나 막연하고 포괄적인 단어여서 `Product`와 구체적으로 무엇이 다른지 알려주지 못합니다. 대신, 그 클래스가 담고 있는 정보의 구체적인 맥락을 이름에 담아야 합니다.


// 나쁜 예
class Product { ... }
class ProductData { ... } // 무엇을 위한 데이터인가?
class ProductInfo { ... } // 어떤 정보인가?

위와 같은 이름 대신, 다음과 같이 구체적인 역할을 명시하는 것이 훨씬 좋습니다.


// 좋은 예
class Product { ... }
class ProductShippingDetails { ... } // 배송과 관련된 상세 정보
class ProductFinancialReport { ... } // 재무 보고서용 정보

마찬가지로, 함수의 인수를 `source`와 `destination`으로 명명하는 것은 훌륭하지만, `argument1`과 `argument2`로 명명하는 것은 최악의 선택입니다. 이름은 그 자체로 의미의 전달자가 되어야 하며, 독자가 스스로 의미를 해석하도록 강요해서는 안 됩니다.

발음하기 쉬운 이름을 사용하라 (Use Pronounceable Names)

개발은 혼자 하는 활동이 아닙니다. 우리는 동료와 끊임없이 소통하고, 코드에 대해 토론하며, 페어 프로그래밍을 진행합니다. 만약 변수나 함수의 이름이 발음하기 어렵다면, 이러한 의사소통 과정은 매우 어색하고 비효율적으로 변합니다. 예를 들어, `genymdhms` (generate year, month, day, hour, minute, second) 같은 이름은 어떻게 발음해야 할까요? "젠와이엠디에이치엠에스"라고 말해야 할까요? 이는 마치 암호를 해독하는 것처럼 들립니다.


// 나쁜 예
Date genymdhms = new Date();

이 대신, 발음이 가능하고 의미가 명확한 이름을 사용하는 것이 좋습니다.


// 좋은 예
Date generationTimestamp = new Date();

generationTimestamp는 "제너레이션 타임스탬프"라고 명확하게 발음할 수 있으며, 그 의미 또한 즉각적으로 와닿습니다. 프로그래밍은 사회적 활동임을 기억해야 합니다. 우리의 코드는 동료와의 대화 속에서 자연스럽게 언급될 수 있어야 합니다.

검색하기 쉬운 이름을 사용하라 (Use Searchable Names)

한 글자로 된 변수 이름이나 숫자 상수는 코드베이스가 커졌을 때 심각한 문제를 야기합니다. 예를 들어, 코드 전체에서 최대 허용 작업 개수를 의미하는 숫자 `5`를 찾는다고 상상해 보십시오. 검색 결과에는 온갖 종류의 `5`가 포함되어 원하는 값을 찾기란 거의 불가능에 가깝습니다. 마찬가지로, 반복문에서 흔히 사용되는 변수 `e`를 찾으려고 하면, 'the', 'name', 'service' 등 'e'가 포함된 모든 단어가 검색될 것입니다.


// 나쁜 예
for (int i = 0; i < 34; i++) {
    s += (t[i] * 4) / 5;
}

이 코드는 맥락 없이는 거의 해독이 불가능합니다. 숫자 345는 무엇을 의미할까요? s, t는 어떤 데이터를 담고 있을까요? 이러한 '마법의 숫자(Magic Number)'와 한 글자 변수명은 검색을 불가능하게 만들고 코드의 의도를 숨깁니다.


// 좋은 예
const int WORK_DAYS_PER_MONTH = 34; // 사실 이 상수는 더 좋은 이름이 필요하다.
const int REAL_DAYS_PER_WEEK = 5;
...
int sumOfSquaredDeviations = 0;
for (int i = 0; i < WORK_DAYS_PER_MONTH; i++) {
    int dailyTaskCount = tasks[i];
    sumOfSquaredDeviations += (dailyTaskCount * 4) / REAL_DAYS_PER_WEEK;
}

WORK_DAYS_PER_MONTHREAL_DAYS_PER_WEEK와 같은 이름은 검색하기 매우 용이합니다. 'WORK_DAYS'라는 키워드로 검색하면 이 상수가 사용된 모든 곳을 쉽게 찾을 수 있습니다. 이름의 길이는 검색 가능성에 비례하는 경향이 있습니다. 짧은 이름은 로컬 변수와 같이 아주 작은 범위에서만 유효하며, 범위가 넓어질수록 이름은 더 길고 구체적이어야 합니다.


2부: 코드의 뼈대, 함수

함수는 소프트웨어를 구성하는 가장 기본적인 행동 단위입니다. 프로그램의 모든 작업은 함수를 통해 이루어집니다. 따라서 함수를 어떻게 설계하고 작성하는지는 전체 시스템의 품질과 직결됩니다. 클린 코드 철학에서 함수는 작아야 하고, 한 가지 일만 해야 하며, 이름과 완벽하게 일치하는 작업을 수행해야 합니다. 이는 마치 잘 만들어진 도구와 같습니다. 하나의 도구는 하나의 명확한 목적을 위해 정교하게 설계되며, 사용자는 그 도구의 이름만 보고도 용도를 즉시 알 수 있습니다.

첫 번째 규칙: 작게 만들어라! (First Rule: Small!)

함수를 작성하는 첫 번째 규칙은 '작게' 만드는 것입니다. 두 번째 규칙은 '더 작게' 만드는 것입니다. 그렇다면 얼마나 작아야 할까요? 100줄? 50줄? 정해진 답은 없지만, 경험적으로 좋은 함수는 10줄을 거의 넘지 않으며, 이상적으로는 5줄 미만인 경우가 많습니다. 함수가 길어진다는 것은 너무 많은 책임을 지고 있다는 명백한 신호입니다.

함수가 작아야 하는 이유는 명확합니다.

  1. 이해하기 쉽다: 한눈에 함수의 전체 로직을 파악할 수 있습니다. 스크롤을 내릴 필요가 없다는 것은 인지적 부담을 크게 줄여줍니다.
  2. 재사용하기 쉽다: 작고 단일한 목적을 가진 함수는 다양한 맥락에서 재사용될 가능성이 높습니다.
  3. 테스트하기 쉽다: 입력과 출력이 명확하고, 내부 로직이 단순한 함수는 테스트 케이스를 작성하기 훨씬 수월합니다.

if/else, while, for와 같은 제어 구조의 블록은 한 줄이어야 하며, 그 한 줄은 대부분 함수 호출이어야 합니다. 이는 함수의 들여쓰기 수준을 1단이나 2단을 넘지 않게 유지하는 데 도움을 줍니다. 들여쓰기가 깊어진다는 것 또한 함수가 너무 많은 일을 하고 있다는 증거입니다.

두 번째 규칙: 한 가지만 해라! (Do One Thing!)

이 원칙은 소프트웨어 설계의 핵심 원리 중 하나인 단일 책임 원칙(Single Responsibility Principle, SRP)의 함수 버전입니다. 함수는 '한 가지' 일만 해야 하며, 그 일을 '잘' 해야 합니다. 여기서 '한 가지'란 무엇일까요?

함수가 한 가지 일만 하는지를 판단하는 좋은 방법은, 함수 내의 모든 코드가 동일한 추상화 수준에 있는지 확인하는 것입니다. 만약 함수 내에 데이터베이스 연결을 설정하는 로직(매우 낮은 수준)과 비즈니스 규칙을 적용하여 보고서를 생성하는 로직(매우 높은 수준)이 섞여 있다면, 이 함수는 여러 가지 일을 하고 있는 것입니다.

다음은 여러 가지 일을 하는 함수의 예시입니다.


// 나쁜 예: 여러 가지 일을 하는 함수
public void processAndReportUserStatistics() {
    // 1. 데이터베이스에서 사용자 데이터를 가져온다.
    List<User> users = database.query("SELECT * FROM USERS WHERE active=1");
    
    // 2. 통계를 계산한다.
    int totalAge = 0;
    for (User user : users) {
        totalAge += user.getAge();
    }
    double averageAge = totalAge / users.size();
    
    // 3. 보고서의 형식을 만든다 (HTML).
    String report = "<html><body>";
    report += "<h1>User Statistics Report</h1>";
    report += "<p>Average Age: " + averageAge + "</p>";
    report += "</body></html>";
    
    // 4. 보고서를 파일에 쓴다.
    File file = new File("user_report.html");
    try (FileWriter writer = new FileWriter(file)) {
        writer.write(report);
    } catch (IOException e) {
        // 오류 처리...
    }
}

위 함수는 명백히 네 가지 이상의 일을 하고 있습니다. 이 함수를 리팩토링하여 각기 다른 책임을 가진 여러 개의 작은 함수로 분리해야 합니다.


// 좋은 예: 각 함수가 한 가지 일만 하도록 리팩토링
public void generateUserStatisticsReport() {
    List<User> activeUsers = fetchActiveUsers();
    UserStatistics stats = calculateStatistics(activeUsers);
    String reportContent = formatStatisticsAsHtml(stats);
    saveReportToFile("user_report.html", reportContent);
}

private List<User> fetchActiveUsers() {
    // 데이터베이스에서 사용자 데이터를 가져오는 로직
    return database.query("SELECT * FROM USERS WHERE active=1");
}

private UserStatistics calculateStatistics(List<User> users) {
    // 통계 계산 로직
    ...
    return new UserStatistics(averageAge);
}

private String formatStatisticsAsHtml(UserStatistics stats) {
    // HTML 보고서 형식 생성 로직
    ...
    return report;
}

private void saveReportToFile(String fileName, String content) {
    // 파일 저장 로직
    ...
}

리팩토링된 코드는 훨씬 더 읽기 쉽고 이해하기 편합니다. 최상위 함수인 `generateUserStatisticsReport`는 전체 작업의 흐름을 요약하는 이야기처럼 읽힙니다. "활성 사용자를 가져와서, 통계를 계산하고, HTML 형식으로 만든 다음, 파일에 저장한다." 각 단계의 구체적인 구현은 더 낮은 수준의 함수에 위임되어 있습니다. 이것이 바로 '한 가지 일만 하라' 원칙의 힘입니다.

함수 당 추상화 수준은 하나로 (One Level of Abstraction per Function)

위의 리팩토링 예시는 '함수 당 추상화 수준은 하나로' 원칙을 잘 보여줍니다. 코드를 위에서 아래로 읽어 내려갈 때, 추상화 수준이 일관되게 유지되거나 점진적으로 낮아져야 합니다. 이를 '내려가기 규칙(Step-down Rule)'이라고도 합니다.

최상위 함수는 시스템의 가장 추상적인 개념과 정책을 다루어야 하며, 세부적인 구현 내용은 포함하지 않아야 합니다. 이 함수가 호출하는 다음 단계의 함수들은 조금 더 구체적인 내용을 다룹니다. 이러한 방식으로 계속 내려가다 보면 가장 구체적인 구현(예: 문자열 조작, 파일 I/O)에 도달하게 됩니다. 높은 수준의 추상화(generateReport)와 낮은 수준의 추상화(substring, append)가 한 함수 안에 섞여 있으면 코드를 이해하는 데 큰 혼란을 줍니다.

함수 인수 (Function Arguments)

함수 인수의 개수는 적을수록 좋습니다. 이상적인 인수의 개수는 0개(niladic)입니다. 그다음은 1개(monadic), 2개(dyadic) 순입니다. 3개(triadic)부터는 특별한 이유가 없는 한 피해야 하며, 4개 이상의 인수를 가진 함수는 존재해서는 안 됩니다.

인수가 많아질수록 함수를 이해하고 테스트하기가 기하급수적으로 어려워집니다. 3개의 인수를 가진 함수는 각 인수의 조합을 모두 고려해야 하므로 테스트 케이스가 훨씬 복잡해집니다. 예를 들어, `paint(color, x, y)`라는 함수는 `paint(point, color)`보다 개념적으로 이해하기 어렵습니다. `x`와 `y`는 `Point`라는 하나의 개념으로 묶일 수 있기 때문입니다. 인수가 너무 많다고 느껴진다면, 해당 인수들을 묶어 하나의 객체로 만드는 것을 고려해야 합니다.


// 나쁜 예: 너무 많은 인수
void makeCircle(double x, double y, double radius, String color) { ... }

// 좋은 예: 인수를 객체로 묶음
class CircleConfig {
    Point center;
    double radius;
    String color;
}
void makeCircle(CircleConfig config) { ... }

특히 주의해야 할 것은 '플래그 인수(Flag Argument)'입니다. `boolean` 타입의 인수를 받아 함수의 동작을 바꾸는 것은 함수가 한 가지 이상의 일을 한다는 명백한 증거입니다. `calculate(amount, isPremium)`와 같은 함수는 `calculateRegular(amount)`와 `calculatePremium(amount)` 두 개의 함수로 분리되어야 합니다.

부수 효과를 일으키지 마라 (Have No Side Effects)

함수는 이름이 암시하는 '그 일'만 해야 합니다. 선언된 것 외에 다른 일을 해서는 안 됩니다. 예를 들어, `checkPassword(user, password)`라는 함수가 단순히 비밀번호가 유효한지만 확인해야지, 비밀번호가 틀렸다고 해서 사용자의 세션을 초기화하는 등의 부수 효과를 일으켜서는 안 됩니다. 이러한 숨겨진 동작은 예기치 않은 버그를 만들고 시스템의 동작을 예측하기 어렵게 만듭니다.

이는 명령과 조회를 분리하는 원칙(Command Query Separation, CQS)과도 관련이 깊습니다. 함수는 무언가의 상태를 변경하는 명령(command)이거나, 무언가에 대한 정보를 반환하는 조회(query)여야 하며, 두 가지를 동시에 해서는 안 됩니다. `set(username, password)`는 명령이므로 `true`나 `false`를 반환하는 대신, 성공하지 못했다면 예외를 던져야 합니다. `exists(username)`은 조회이므로 `true`나 `false`를 반환해야 합니다.


3부: 코드의 목소리, 주석

주석에 대한 클린 코드의 입장은 다소 역설적입니다. "좋은 주석을 다는 가장 좋은 방법은 주석이 필요 없는 코드를 작성하는 것이다." 코드는 그 자체로 명확하고 표현력이 풍부해야 하며, 주석은 이러한 목표를 달성하지 못했을 때 사용하는 보조 수단에 불과합니다. 주석은 필요악이며, 때로는 득보다 실이 많을 수 있습니다.

주석은 실패를 의미한다

주석을 작성하는 매 순간, 우리는 코드만으로 의도를 표현하는 데 실패했음을 인정하는 것입니다. 복잡한 로직을 설명하기 위해 장황한 주석을 다는 대신, 그 로직을 더 작고 이해하기 쉬운 여러 함수로 나누는 것이 훨씬 나은 해결책입니다. 코드는 계속해서 변경되고 발전하지만, 주석은 잊히기 쉽습니다. 코드가 변경되었음에도 불구하고 주석이 수정되지 않으면, 그 주석은 코드의 실제 동작과 다른, 오해를 불러일으키는 거짓말이 되어버립니다. 오래되고 부정확한 주석은 아예 없는 주석보다 훨씬 해롭습니다.

나쁜 주석의 유형

우리는 다음과 같은 나쁜 주석들을 코드에서 추방해야 합니다.

  • 중복되는 주석 (Redundant Comments): 코드 자체만으로도 충분히 알 수 있는 내용을 설명하는 주석입니다.
    // i를 1 증가시킨다.
    i++;
    
  • 오해의 소지가 있는 주석 (Misleading Comments): 코드의 실제 동작과 다른 내용을 담고 있는 주석입니다.
  • 의무적으로 다는 주석 (Mandated Comments): 모든 함수나 변수에 주석을 달아야 한다는 규칙 때문에 의미 없이 작성된 주석입니다. Javadoc이 좋은 예가 될 수 있습니다.
    /**
     * @param name a user name
     * @return true if the user is authenticated
     */
    public boolean authenticateUser(String name) { ... }
    
  • 주석으로 처리된 코드 (Commented-Out Code): 더 이상 사용되지 않는 코드는 주석 처리하지 말고 그냥 삭제해야 합니다. 버전 관리 시스템(Git 등)이 그 코드를 기억하고 있습니다. 주석 처리된 코드는 다른 개발자들에게 혼란을 주고, 왜 남아있는지, 중요한 코드인지 아닌지 고민하게 만듭니다.
  • 변경 이력 주석 (Journal Comments): 파일 상단에 언제, 누가, 무엇을 수정했는지 기록하는 것은 이제 버전 관리 시스템의 역할입니다.
    /*
     * 2023-10-27: (John) 버그 #123 수정
     * 2023-01-15: (Jane) 초기 버전 생성
     */
    

좋은 주석의 유형

물론 모든 주석이 나쁜 것은 아닙니다. 때로는 주석이 유용하거나 꼭 필요한 경우도 있습니다.

  • 법적인 주석 (Legal Comments): 저작권 정보나 라이선스 정보와 같이 법적인 이유로 반드시 포함해야 하는 주석입니다.
    // Copyright (C) 2023 by Acme Corporation. All rights reserved.
    
  • 의도를 설명하는 주석 (Explanation of Intent): 코드 자체만으로는 드러나지 않는, 왜 특정 방식으로 구현했는지에 대한 설계자의 의도를 설명하는 주석입니다. 예를 들어, 다른 더 명백한 해결책 대신 비직관적인 방법을 선택한 이유를 설명할 수 있습니다.
  • 결과를 경고하는 주석 (Warning of Consequences): 특정 함수를 잘못 사용했을 때 발생할 수 있는 위험을 경고하는 주석입니다. 예를 들어, '이 테스트는 실행하는 데 시간이 매우 오래 걸리니 주의'와 같은 주석은 유용합니다.
    // 중요: 이 함수는 외부 API 호출로 인해 스레드를 몇 초간 블록할 수 있습니다.
    
  • TODO 주석: '지금은 아니지만 나중에 해야 할 일'을 표시하는 주석입니다. IDE는 보통 이런 주석을 찾아 목록으로 보여주는 기능을 제공하여 유용하게 활용될 수 있습니다. 하지만 TODO가 너무 오랫동안 방치되어서는 안 됩니다.
  • 중요성을 강조하는 주석: 사소해 보이지만 중요한 영향을 미치는 코드 조각을 강조할 때 사용될 수 있습니다.
    String a = b; // 여기서의 공백 트림(trim)이 매우 중요합니다!
    

결론적으로, 주석을 작성하기 전에 항상 "이 의도를 코드로 표현할 방법은 정말 없는가?"라고 자문해야 합니다. 대부분의 경우, 더 나은 이름, 더 작은 함수, 더 명확한 구조를 통해 주석을 제거할 수 있습니다. 주석은 최후의 수단으로만 사용되어야 합니다.


4부: 시스템의 질서, 형식과 구조

코드의 형식(formatting)은 기능에 직접적인 영향을 주지 않기 때문에 중요하지 않다고 생각하기 쉽습니다. 하지만 이는 큰 착각입니다. 코드는 작성되는 시간보다 읽히는 시간이 압도적으로 많습니다. 일관되고 깔끔한 형식은 코드의 가독성을 극대화하여, 다른 개발자(혹은 미래의 나 자신)가 코드를 빠르고 정확하게 이해하도록 돕습니다. 잘 정돈된 코드는 그 자체로 신뢰감을 주며, 전문성의 표현입니다.

일관성의 중요성

형식에서 가장 중요한 것은 '일관성'입니다. 중괄호 `{}`를 어디에 둘 것인지, 들여쓰기는 스페이스를 사용할지 탭을 사용할지, 한 줄의 최대 길이는 얼마로 할 것인지 등은 논쟁의 대상이 될 수 있습니다. 하지만 어떤 스타일을 선택하든, 프로젝트 전체에 걸쳐 일관되게 적용하는 것이 중요합니다. 일관된 스타일은 독자가 형식에 신경 쓰지 않고 코드의 내용에만 집중할 수 있게 해줍니다. 다행히 오늘날에는 Prettier, ktlint, Checkstyle과 같은 자동 포맷터(auto-formatter) 도구들이 있어 팀의 코드 스타일을 강제하고 일관성을 유지하는 것이 매우 쉬워졌습니다.

수직 형식: 코드의 흐름

소스 파일은 신문 기사와 같아야 합니다. 가장 위에는 가장 중요한 개념(고수준의 추상화)이 오고, 아래로 내려갈수록 세부적인 내용이 나와야 합니다. 독자는 파일 전체를 읽지 않고도 위쪽 몇 줄만 보고 파일의 목적을 파악할 수 있어야 합니다.

  • 개념 분리: 서로 다른 개념은 빈 줄로 분리해야 합니다. 패키지 선언, import 문, 각 함수, 함수 내의 논리적 블록 사이에는 빈 줄을 넣어 시각적으로 구분하는 것이 좋습니다. 붙어있는 코드는 서로 밀접하게 관련되어 있음을 의미합니다.
  • 수직 밀도: 서로 밀접하게 연관된 코드는 가까이 붙어 있어야 합니다. 의미 없는 주석이나 불필요한 빈 줄로 인해 연관된 코드가 멀리 떨어져서는 안 됩니다.
  • 수직 거리:
    • 변수 선언: 변수는 사용되는 위치에 최대한 가깝게 선언해야 합니다. 함수의 맨 위에 모든 지역 변수를 선언하는 C 스타일은 피해야 합니다.
    • 인스턴스 변수: 클래스의 인스턴스 변수는 클래스 최상단에 모아서 선언하는 것이 일반적입니다. 이는 클래스가 어떤 상태를 가지는지 한눈에 파악하는 데 도움을 줍니다.
    • 종속 함수: 한 함수가 다른 함수를 호출한다면(호출자-피호출자 관계), 이 두 함수는 수직으로 가까이 배치되어야 합니다. 가능하다면 호출하는 함수를 호출되는 함수보다 위에 두어 자연스러운 흐름을 만듭니다.

수평 형식: 가독성의 폭

한 줄에 너무 많은 코드를 넣는 것은 가독성을 해칩니다. 대부분의 전문가는 한 줄의 길이를 80~120자 사이로 제한할 것을 권장합니다. 긴 줄은 수평 스크롤을 유발하며, 이는 코드를 읽는 흐름을 끊고 전체적인 구조를 파악하기 어렵게 만듭니다.

  • 공백과 밀도: 연산자 양옆에는 공백을 두어 각 요소를 시각적으로 분리하는 것이 좋습니다. 예를 들어, `a=b+c;`보다는 `a = b + c;`가 훨씬 읽기 편합니다. 반면, 함수 이름과 괄호 사이에는 공백을 두지 않아(`myFunction(arg)`) 함수 호출이라는 것을 명확히 나타냅니다.
  • 들여쓰기: 들여쓰기는 코드의 계층 구조를 보여주는 가장 중요한 시각적 장치입니다. 들여쓰기 규칙은 절대적으로 지켜져야 합니다. `if`, `for`, `while` 문 등의 블록이 한 줄짜리 코드라고 해서 들여쓰기를 생략하거나 중괄호를 빼는 것은 심각한 버그를 유발할 수 있으므로 절대 피해야 합니다.

5부: 견고한 설계, 객체와 자료 구조

클린 코드는 단순히 개별 함수나 변수 이름에 국한되지 않습니다. 더 큰 단위인 객체와 전체적인 자료 구조의 설계에도 깊이 관여합니다. 객체와 자료 구조를 어떻게 설계하느냐에 따라 시스템의 유연성과 확장성이 결정됩니다.

데이터 추상화의 중요성

객체는 자신의 데이터를 숨기고, 그 데이터를 다루는 함수(메서드)만을 외부에 공개해야 합니다. 이를 데이터 추상화 또는 캡슐화(Encapsulation)라고 합니다. 변수를 `public`으로 선언하는 것은 객체의 내부 구현을 그대로 노출하는 행위이며, 이는 해당 변수에 의존하는 모든 코드를 잠재적인 변경의 위험에 빠뜨립니다. 대신, 접근자(getter)와 설정자(setter)를 통해 데이터에 접근하도록 해야 합니다. 더 나아가, 단순히 데이터를 묻고 답하는 객체가 아닌, 의미 있는 행동을 수행하는 객체를 설계해야 합니다.


// 나쁜 예: 구현 노출
public class Point {
    public double x;
    public double y;
}

// 좋은 예: 추상화
public interface Point {
    double getX();
    double getY();
    void setCartesian(double x, double y);
    double getR();
    double getTheta();
    void setPolar(double r, double theta);
}

위의 좋은 예는 데이터가 직교 좌표계로 저장되는지, 극좌표계로 저장되는지 내부 구현을 숨기고 있습니다. 사용자는 인터페이스를 통해 자신이 원하는 방식으로 데이터를 조작할 수 있으며, 내부 구현이 변경되더라도 사용자 코드는 영향을 받지 않습니다.

자료 구조와 객체의 비대칭성

자료 구조와 객체는 근본적으로 다른 특성을 가집니다.

  • 자료 구조 (절차적 코드): 자료 구조는 데이터를 그대로 노출하며, 별도의 함수들이 이 데이터를 받아 처리합니다. 이런 방식은 새로운 함수를 추가하기는 쉽지만(기존 자료 구조를 건드리지 않으므로), 새로운 자료 구조를 추가하기는 어렵습니다(모든 함수를 수정해야 하므로).
  • 객체 (객체 지향 코드): 객체는 데이터를 숨기고 데이터를 처리하는 메서드를 함께 캡슐화합니다. 이런 방식은 새로운 객체(클래스)를 추가하기는 쉽지만(기존 객체에 영향을 주지 않으므로), 새로운 메서드를 추가하기는 어렵습니다(영향을 받는 모든 클래스를 수정해야 하므로).

상황에 따라 적절한 방식을 선택하는 지혜가 필요합니다. 예를 들어, 새로운 데이터 유형이 추가될 가능성보다 새로운 기능이 추가될 가능성이 훨씬 높다면 객체 지향 방식이 유리합니다. 반대의 경우라면 절차적인 방식이 더 나을 수 있습니다.

디미터 법칙 (The Law of Demeter)

디미터 법칙은 "오직 너의 친구하고만 이야기하라"는 원칙으로, 객체 간의 결합도를 낮추기 위한 중요한 설계 가이드라인입니다. 어떤 객체의 메서드는 다음과 같은 대상의 메서드만 호출해야 합니다.

  1. 객체 자신
  2. 메서드의 인수로 넘어온 객체
  3. 메서드 내부에서 생성한 객체
  4. 객체의 인스턴스 변수로 가지고 있는 객체

다음과 같은 '기차 충돌(train wreck)' 코드는 디미터 법칙을 위반한 대표적인 예입니다.


// 나쁜 예: 디미터 법칙 위반
String zipCode = user.getAddress().getCity().getZipCode();

이 코드는 `User` 객체가 `Address`의 내부 구조를, `Address` 객체가 `City`의 내부 구조를 알고 있어야만 작성할 수 있습니다. 만약 중간에 있는 `getCity()`가 `City` 객체 대신 `Region` 객체를 반환하도록 변경된다면, 이 코드는 즉시 깨지게 됩니다. 이는 여러 모듈이 서로의 내부 구현에 깊이 의존하게 만들어 시스템을 취약하게 만듭니다.

대신, 객체에게 "묻지 말고 시켜라(Tell, Don't Ask)" 원칙에 따라 필요한 작업을 직접 요청해야 합니다.


// 좋은 예: 객체에 직접 요청
String zipCode = user.getZipCodeOfCity();

// User 클래스 내부
public String getZipCodeOfCity() {
    return address.getZipCodeOfCity();
}

// Address 클래스 내부
public String getZipCodeOfCity() {
    return city.getZipCode();
}

이렇게 하면 클라이언트는 더 이상 객체들의 내부 구조를 알 필요가 없으며, 각 객체는 자신의 책임 하에 정보를 제공하거나 작업을 수행하게 됩니다. 결과적으로 모듈 간의 결합도가 낮아져 훨씬 유연하고 유지보수하기 좋은 시스템이 만들어집니다.


6부: 예상치 못한 상황에 대한 대비, 오류 처리

오류 처리는 프로그램의 정상적인 흐름만큼이나 중요하지만, 종종 부수적인 작업으로 취급되어 지저분하게 작성되기 쉽습니다. 오류 처리 로직이 비즈니스 로직과 뒤섞이면, 프로그램의 전체적인 흐름을 파악하기 매우 어려워집니다. 클린 코드는 오류 처리 로직을 주 로직으로부터 명확하게 분리하여 코드의 가독성과 안정성을 높이는 것을 목표로 합니다.

반환 코드 대신 예외를 사용하라

과거에는 오류가 발생했을 때 특별한 값(예: `-1` 또는 `null`)이나 오류 코드를 반환하는 방식이 널리 사용되었습니다. 하지만 이 방식은 심각한 문제를 야기합니다. 함수를 호출하는 쪽에서 반환된 값이 정상 값인지 오류 코드인지 매번 확인해야 하기 때문입니다. 이는 `if/else` 문이 중첩되는 지저분한 코드를 만들어내고, 만약 개발자가 이 확인을 잊어버린다면 프로그램은 예기치 않은 상태에서 계속 실행되어 더 큰 문제를 일으킬 수 있습니다.


// 나쁜 예: 오류 코드 반환
ErrorCode result = deletePage(page);
if (result == ErrorCode.OK) {
    ErrorCode result2 = deleteReferences(page);
    if (result2 == ErrorCode.OK) {
        // ...
    } else {
        // 오류 처리
    }
} else {
    // 오류 처리
}

예외(Exception)를 사용하면 이러한 문제를 해결할 수 있습니다. 오류가 발생하면 예외를 던지고(throw), 오류를 처리할 책임이 있는 상위의 `catch` 블록에서 이를 처리합니다. 이렇게 하면 정상적인 비즈니스 로직과 오류 처리 로직이 명확하게 분리됩니다.


// 좋은 예: 예외 사용
try {
    deletePage(page);
    deleteReferences(page);
    // ...
} catch (PageDeletionException e) {
    // 오류 처리
}

코드가 훨씬 깔끔하고 읽기 쉬워졌습니다. `try` 블록에는 정상적인 시나리오만 남고, 모든 예외적인 상황은 `catch` 블록에서 처리됩니다.

`try-catch-finally` 문을 먼저 작성하라

코드를 작성할 때, `try-catch-finally` 구문을 먼저 작성하는 습관을 들이는 것이 좋습니다. 이는 마치 트랜잭션과 같아서, 어떤 일이 잘못되었을 때 시스템이 어떤 상태에 있어야 하는지를 먼저 고민하게 만듭니다. 예를 들어, 파일을 열고 쓰는 작업을 한다면, 성공하든 실패하든 파일 핸들을 반드시 닫아야 합니다. `finally` 블록은 이러한 '정리' 작업을 수행하기에 완벽한 장소입니다.

확인된 예외(Checked Exception)를 남용하지 마라

Java와 같은 언어에는 확인된 예외(checked exception)라는 개념이 있습니다. 이는 메서드가 던질 수 있는 예외를 시그니처에 명시하고, 호출자가 이를 반드시 처리하도록 강제하는 기능입니다. 이론적으로는 안정성을 높이는 좋은 기능처럼 보이지만, 실제로는 깨지기 쉬운 설계를 유발하는 경우가 많습니다. 최하위 단계의 함수에서 확인된 예외를 던지면, 그 함수를 호출하는 모든 상위 함수들이 해당 예외를 처리하거나 다시 던져야 합니다. 이는 개방/폐쇄 원칙(Open/Closed Principle)을 위반하며, 하위 단계의 구현 변경이 수많은 상위 모듈의 변경을 연쇄적으로 유발하게 만듭니다.

대부분의 경우, 미확인 예외(unchecked exception, `RuntimeException`의 하위 클래스)를 사용하는 것이 더 유연하고 깔끔한 설계를 가능하게 합니다.

예외에 맥락을 제공하라

예외를 잡아서(catch) 다시 던질 때는, 실패한 작업에 대한 충분한 맥락 정보를 추가해야 합니다. 단순히 원래의 예외를 그대로 다시 던지는 것은 오류의 원인을 파악하는 데 아무런 도움이 되지 않습니다. 어떤 작업을 시도하다가 실패했는지, 어떤 값들이 사용되었는지 등의 정보를 담아 새로운 예외로 감싸서(wrapping) 던지는 것이 좋습니다.


// 나쁜 예
catch (SQLException e) {
    throw new Exception("Database error");
}

// 좋은 예
catch (SQLException e) {
    throw new UserUpdateException("Failed to update user with id " + userId, e);
}

`null`을 반환하지도, 전달하지도 마라

메서드가 `null`을 반환하면, 그 메서드를 호출하는 모든 코드는 `null` 체크를 해야 하는 부담을 안게 됩니다. 이는 `NullPointerException`이라는, 프로그래머들이 가장 흔하게 마주하는 오류의 주된 원인입니다. `null`을 반환하는 대신, 비어있는 컬렉션(empty collection)을 반환하거나, 특수 사례 객체(Special Case Object, Null Object Pattern)를 사용하는 것이 훨씬 안전하고 깔끔한 방법입니다.

마찬가지로, 메서드에 `null`을 인자로 전달하는 것도 피해야 합니다. 이는 메서드 내부에서 `null` 체크 로직을 복잡하게 만들 뿐입니다. 대부분의 경우, `null`을 전달해야 하는 상황은 애초에 설계를 재고해야 한다는 신호입니다.


결론: 장인 정신과 지속적인 개선

클린 코드는 특정 규칙의 목록을 암기하고 적용하는 것에서 끝나지 않습니다. 그것은 지속적인 학습과 실천, 그리고 동료와의 끊임없는 소통을 통해 체득되는 일종의 '장인 정신'입니다. 우리가 작성하는 모든 코드 한 줄 한 줄에 책임감을 느끼고, 더 나은 표현 방식과 더 견고한 구조를 끊임없이 고민하는 태도 그 자체가 바로 클린 코드의 핵심입니다.

오늘 우리가 탐험한 원칙들—의미 있는 이름, 작고 단일한 책임을 지는 함수, 주석의 올바른 사용, 일관된 형식, 견고한 객체 설계, 그리고 깔끔한 오류 처리—은 모두 하나의 목표를 향하고 있습니다: 바로 가독성입니다. 읽기 쉬운 코드는 이해하기 쉽고, 이해하기 쉬운 코드는 변경하기 쉬우며, 변경하기 쉬운 코드는 유지보수하기 쉽습니다. 이것이 바로 소프트웨어가 시간의 흐름 속에서 살아남고 진화할 수 있는 유일한 길입니다.

보이스카우트 규칙을 기억하십시오: "언제나 처음 왔을 때보다 캠프장을 더 깨끗하게 해놓고 떠나라." 우리 개발자들도 마찬가지입니다. 기존 코드를 수정할 때마다, 단 하나의 변수 이름을 개선하든, 긴 함수를 분리하든, 작은 리팩토링을 통해 코드를 조금이라도 더 깨끗하게 만드는 습관을 들여야 합니다. 이러한 작은 노력들이 쌓여 프로젝트 전체의 건강성을 유지하고, 기술 부채의 늪에 빠지는 것을 막아줄 것입니다.

클린 코드를 작성하는 여정은 결코 쉽지 않습니다. 그것은 마감 기한의 압박과 레거시 코드의 복잡성 속에서 끊임없이 원칙을 지키려는 의지적인 노력을 요구합니다. 하지만 그 노력의 결과물은 단순히 '작동하는 소프트웨어'를 넘어, 동료에게 존경받고 스스로에게 자부심을 느끼게 하는 '훌륭한 작품'이 될 것입니다. 코드는 사라져도, 그 안에 담긴 원칙과 장인 정신은 다음 세대의 개발자들에게 계속해서 영감을 줄 것입니다.

Post a Comment