- 한 소스 파일에 여러 언어를 사용한다 (G1): 이상적으로는 소스 파일 하나에 하나의 언어만 사용하는 것이 좋습니다. 현실적으로 여러 언어(XML, HTML, YAML, Javadoc 등)가 혼용될 수 있지만, 그 수와 범위를 최소화하도록 노력해야 합니다. 여러 언어가 섞이면 혼란스럽고 조잡해 보일 수 있습니다.
- 당연한 동작을 구현하지 않는다 (G2): 최소 놀람의 원칙(The Principle of Least Surprise)에 따라, 함수나 클래스는 사용자가 기대하는 대로 동작해야 합니다. 당연한 동작이 구현되지 않으면, 사용자는 함수 이름을 신뢰할 수 없어 코드를 일일이 확인하게 됩니다. 예를 들어,
DayDate.StringToDay("Monday")
는 Day.MONDAY
를 반환하고, 일반적인 약어("Mon")나 대소문자 구분 없이 동작하리라 기대합니다.
- 경계를 올바로 처리하지 않는다 (G3): 직관에만 의존하지 말고, 모든 경계 조건을 찾아 테스트 케이스를 작성해야 합니다. 경계 조건, 예외 상황 등은 알고리즘 실패의 주요 원인이 됩니다. 부지런히 모든 경우를 고려하고 증명해야 합니다.
- 안전 절차 무시 (G4): 컴파일러 경고를 끄거나 실패하는 테스트를 무시하는 것은 위험합니다. 안전 절차를 무시하면(예: 체르노빌 사고) 심각한 문제를 초래할 수 있습니다.
serialVersionUID
직접 제어 등은 위험성을 인지하고 신중해야 합니다.
- 중복 (G5): 코드 중복은 추상화의 기회입니다. 중복된 코드는 하위 루틴이나 별도 클래스로 분리하여 DRY(Don't Repeat Yourself) 원칙을 지켜야 합니다. 켄트 벡은 이를 "한 번, 단 한 번만(Once, and only once)" 규칙으로 강조했습니다.
- 똑같은 코드 반복: 간단한 함수로 교체합니다.
switch/case
또는 if/else
반복: 다형성으로 대체합니다.
- 유사한 알고리즘, 다른 코드: Template Method 패턴이나 Strategy 패턴으로 제거합니다.
많은 디자인 패턴은 중복 제거를 목표로 합니다. 중복을 발견하면 반드시 제거해야 합니다.
- 추상화 수준이 올바르지 못하다 (G6): 추상화는 저수준 상세 개념과 고수준 일반 개념을 분리하는 것입니다. 기초 클래스에는 고수준 개념만, 파생 클래스에는 저수준 구현 세부사항(상수, 변수, 유틸리티 함수 등)을 두어야 합니다. 기초 클래스는 구현 정보에 의존하지 않아야 합니다. 고수준과 저수준 개념을 섞으면 안 됩니다.
public interface Stack {
Object pop() throws EmptyException;
void push(Object o) throws FullException;
double percentFull(); // 추상화 수준이 맞지 않음. BoundedStack 같은 파생 인터페이스에 있어야 함.
class EmptyException extends Exception {}
class FullException extends Exception {}
}
- 기초 클래스가 파생 클래스에 의존한다 (G7): 기초 클래스는 파생 클래스를 몰라야 독립성이 보장됩니다. 일반적으로 기초 클래스가 파생 클래스를 직접 사용하는 것은 문제가 있다는 신호입니다. 예외적으로 파생 클래스 수가 고정된 FSM(Finite State Machine) 구현 등에서는 기초 클래스가 파생 클래스를 선택하는 경우가 있지만, 이는 매우 밀접하게 연관된 경우입니다.
- 과도한 정보 (G8): 잘 설계된 모듈은 작고 명확한 인터페이스를 가집니다. 인터페이스에 불필요하게 많은 함수를 노출하지 않아야 결합도(coupling)가 낮아집니다. 클래스의 메서드, 함수가 아는 변수, 클래스 인스턴스 변수의 수는 적을수록 좋습니다. 정보(자료, 유틸리티 함수, 상수 등)를 최대한 숨겨 인터페이스를 작고 엄격하게 관리해야 합니다.
- 죽은 코드 (G9): 실행되지 않는 코드(불가능한 조건의
if
문, throw
없는 try
의 catch
블록, 호출되지 않는 함수 등)는 즉시 시스템에서 제거해야 합니다. 죽은 코드는 시간이 지남에 따라 유지보수를 어렵게 만들고 오류의 원인이 될 수 있습니다.
- 수직 분리 (G10): 변수와 함수는 사용되는 위치에 최대한 가깝게 정의해야 합니다. 지역 변수는 첫 사용 직전에 선언하고, 비공개(private) 함수는 첫 호출 직후에 정의하여 코드 가독성을 높입니다.
- 일관성 부족 (G11): 유사한 개념은 일관된 방식으로 구현해야 합니다. 특정 변수명(
response
)이나 함수 작명 규칙(processVerificationRequest
)을 정했다면, 시스템 전반에 걸쳐 동일하게 적용해야 합니다. 이는 최소 놀람의 원칙에도 부합하며, 다른 개발자들이 코드를 이해하고 유지보수하기 쉽게 만듭니다. 일관성 있는 코드는 그 자체로 규칙이 되어 유지됩니다.
- 잡동사니 (G12): 사용되지 않는 변수, 호출되지 않는 함수, 정보 없는 주석 등은 코드를 복잡하게만 만듭니다. 비어있는 기본 생성자처럼 불필요한 요소는 제거하여 소스 파일을 항상 깔끔하게 유지해야 합니다.
- 인위적 결합 (G13): 서로 관련 없는 개념들을 불필요하게 같은 곳에 배치하지 말아야 합니다.
enum
처럼 특정 클래스에 속할 이유가 없는 요소를 무심코 특정 클래스 안에 넣으면, 해당 enum
을 사용하는 코드가 불필요하게 그 클래스를 알아야 하는 의존성이 생깁니다. 함수, 상수, 변수를 선언할 때는 항상 올바른 위치를 고민해야 합니다.
- 기능 욕심 (G14): 클래스 메서드는 자신의 클래스 멤버(변수, 함수)에 주로 관심을 가져야 합니다. 다른 클래스의 내부 정보를 가져와(getter/setter 사용) 그 객체를 직접 조작하는 것은 '기능 욕심'이며, 클래스 간 결합도를 높입니다. 가능하면 피하는 것이 좋지만, 때로는 불가피한 경우도 있습니다(예: 보고서 클래스가 데이터 객체 정보를 가져와 보고서 형식으로 만드는 경우).
// 기능 욕심 예시: calculateWeeklyPay 메서드가 HourlyEmployee 객체 정보를 과도하게 사용
public class HourlyPayCalculator {
public Money calculateWeeklyPay(HourlyEmployee e) {
int tenthRate = e.getTenthRate().getPennies();
int tenthsWorked = e.getTenthsWorked();
// ... 계산 로직 ...
return new Money(straightPay + overtimePay);
}
}
// 허용될 수 있는 예외: reportHours 메서드가 HourlyEmployee 정보를 사용하지만,
// 보고서 형식을 HourlyEmployee 클래스가 알 필요는 없음
public class HourlyEmployeeReport {
private HourlyEmployee employee;
// ...
String reportHours() {
return String.format(
"Name: %s\tHours:%d.%1d\n",
employee.getName(), employee.getTenthsWorked()/10, employee.getTenthsWorked()%10
);
}
}
- 선택자 인수 (G15): 함수의 동작을 제어하기 위해
boolean
, enum
, int
등의 인수를 사용하는 것은 좋지 않습니다. 이는 함수가 여러 기능을 하나로 합쳐 놓았다는 증거이며, 코드를 이해하기 어렵게 만듭니다. 인수로 동작을 선택하는 대신, 각 동작에 맞는 별도의 함수를 만드는 것이 좋습니다.
// 나쁜 예: boolean 인수로 초과 근무 수당 계산 여부 결정
public int calculateWeeklyPay(boolean overtime) { ... }
// 좋은 예: 각 기능별 함수로 분리
public int straightPay() { ... }
public int overTimePay() { ... }
- 모호한 의도 (G16): 코드는 의도를 명확히 드러내야 합니다. 한 줄에 길게 쓴 수식, 의미 없는 약어나 헝가리식 표기법, 매직 넘버 등은 코드의 의도를 파악하기 어렵게 만듭니다.
// 나쁜 예: 변수 이름이 모호하고 수식이 한 줄로 길게 작성됨
public int m_otCalc() {
return iThsWkd * iThsRte + (int) Math.round(0.5 * iThsRte * Math.max(0, iThsWkd - 400));
}
- 잘못 지운 책임 (G17): 코드는 가장 적절하고 직관적인 위치에 배치되어야 합니다. 단순히 개발자에게 편한 위치가 아니라, 해당 기능의 책임 소재를 고려해야 합니다. 예를 들어, 총 근무 시간을 계산하는 로직은 보고서 출력 함수보다는 근무 시간 입력 처리 부분에 두는 것이 더 적절할 수 있으며, 함수 이름(
computeRunningTotalOfHours
)에 이를 반영해야 합니다. 상수의 위치 등도 기능적 책임에 맞게 결정해야 합니다.
- 부적절한 static 함수 (G18): 재정의(override)될 가능성이 있거나, 특정 객체의 상태와 관련된 함수는
static
으로 선언하면 안 됩니다. Math.max()
처럼 특정 인스턴스와 무관하고 재정의 가능성이 없는 함수는 static
이 적합합니다. 하지만 특정 계산 로직(예: HourlyPayCalculator.calculatePay
)처럼 다양한 계산 방식(일반, 초과근무 등)으로 확장될 수 있다면, 인스턴스 메서드로 만드는 것이 좋습니다. 일반적으로 인스턴스 함수가 static
함수보다 선호됩니다.
- 서술적 변수 (G19): 복잡한 계산은 중간 결과를 담는 서술적인 이름의 변수를 사용하여 여러 단계로 나누는 것이 좋습니다. 이는 코드의 가독성을 크게 향상시키는 효과적인 방법입니다. 서술적인 변수 이름은 많을수록 좋습니다.
// 좋은 예: key, value 변수를 사용해 의도를 명확히 함
Matcher match = headerPattern.matcher(line);
if (match.find()) {
String key = match.group(1);
String value = match.group(2);
headers.put(key.toLowerCase(), value);
}
- 이름과 기능이 일치하는 함수 (G20): 함수 이름은 실제 기능을 명확하게 반영해야 합니다. 이름만 보고 기능을 오해할 수 있다면(예:
date.add(5)
가 5일, 5주, 5시간 중 무엇을 더하는지, 원본 객체를 변경하는지 새 객체를 반환하는지 불명확), 더 좋은 이름으로 바꾸거나 기능을 명확하게 분리해야 합니다. (예: addDaysTo
, increaseByDays
, daysLater
, daysSince
)
- 알고리즘을 이해하라 (G21): 코드를 작성하고 테스트를 통과시키는 것만으로는 부족합니다. 작성한 알고리즘이 왜 올바른지 스스로 확실히 이해해야 합니다. 이를 위해 함수를 최대한 깔끔하고 명확하게 재구성하여 기능이 명백히 드러나도록 하는 것이 좋습니다. 알고리즘을 제대로 이해하지 못한 상태에서 작성된 코드는 종종 문제가 됩니다.
- 논리적 의존성은 물리적으로 드러내라 (G22): 한 모듈이 다른 모듈에 논리적으로 의존한다면(예: 특정 상수를 다른 클래스가 알고 있다고 가정), 이를 명시적인 방법 호출 등으로 물리적 의존성으로 바꿔야 합니다. 의존하는 정보를 명시적으로 요청하는 것이 좋습니다. 예를 들어,
HourlyReporter
가 페이지 크기(PAGE_SIZE
)를 알아야 한다면, 해당 정보를 가진 HourlyReportFormatter
에 getMaxPageSize()
메서드를 만들어 호출하는 방식이 좋습니다.
- If/Else 혹은 Switch/Case 문보다 다형성을 사용하라 (G23):
switch
문은 조건에 따라 다른 동작을 수행할 때 손쉬운 방법이지만, 새로운 유형이 추가될 때마다 수정이 필요합니다(OCP 위반). 특히 동일한 조건을 확인하는 switch
문이 여러 곳에 있다면 다형성을 활용하여 대체하는 것을 우선 고려해야 합니다. 객체 유형에 따라 동작이 달라지는 경우, 다형성이 더 유연하고 확장 가능한 해결책입니다.
- 표준 표기법을 따르라 (G24): 팀 내에서 합의된 **구현 표준(코딩 컨벤션)**을 정의하고 일관성 있게 따라야 합니다. 변수/클래스/메서드 이름 규칙, 괄호 위치 등 세부적인 스타일은 중요하지 않지만, 모두가 동일한 표준을 따르는 것이 중요합니다. 이는 코드의 일관성을 유지하고 가독성을 높입니다. 표준은 별도 문서보다는 코드 자체로 충분히 설명될 수 있어야 합니다.
- 매직 숫자는 명명된 상수로 교체하라 (G25): 코드 내에 의미를 알 수 없는 숫자(매직 넘버)나 문자열 토큰을 직접 사용하지 말고, 의미 있는 이름의 상수로 대체해야 합니다. 단,
Math.PI
나 WORK_HOURS_PER_DAY = 8
처럼 의미가 명확하고 자명한 경우는 예외적으로 숫자 리터럴을 사용해도 괜찮습니다. 중요한 것은 의미가 불분명한 값을 상수로 만들어 가독성을 높이는 것입니다.
// 애매한 경우: 상수가 꼭 필요한가?
double milesWalked = feetWalked / 5280.0; // FEET_PER_MILE
int dailyPay = hourlyRate * 8; // WORK_HOURS_PER_DAY
double circumference = radius * Math.PI * 2; // TWO?
// 명확히 필요한 경우: 7777의 의미가 불분명
assertEquals(7777, Employee.find("john Doe").employeeNumber());
// 상수로 변경
assertEquals(HOURLY_EMPLOYEE_ID, Employee.find("john Doe").employeeNumber());
- 정확하라 (G26): 코드 내에서의 모든 결정(변수 타입, 알고리즘 선택, 예외 처리 방식 등)은 명확한 이유와 근거를 가지고 정확하게 내려져야 합니다. 모호함이나 부정확함은 의견 충돌이나 단순히 게으름의 결과일 수 있으며, 이는 제거해야 할 대상입니다.
- 관례보다 구조를 사용하라 (G27): 설계상의 제약을 강제할 때는 단순한 명명 관례보다는 구조적인 방법(예: 추상 메서드 강제)을 사용하는 것이 더 강력합니다.
switch/case
문을 올바르게 사용하도록 관례를 정하는 것보다, 추상 메서드를 포함한 기초 클래스를 만들어 파생 클래스가 반드시 해당 메서드를 구현하도록 강제하는 것이 더 효과적입니다.
- 조건을 캡슐화하라 (G28): 복잡한 조건 논리는 의도를 명확히 드러내는 함수로 캡슐화하는 것이 좋습니다.
boolean
논리를 그대로 노출하는 것보다 이해하기 쉽습니다.
// 좋은 예: 조건의 의도를 함수 이름으로 표현
if (shouldBeDeleted(timer))
// 나쁜 예: 조건 논리가 그대로 노출됨
if (timer.hasExpired() && !timer.isReccurent())
- 부정 조건은 피하라 (G29): 부정 조건(
!
)은 긍정 조건보다 이해하기 어렵습니다. 가능하다면 긍정 조건으로 표현하는 것이 좋습니다.
// 좋은 예: 긍정 조건 사용
if (buffer.shouldCompact())
// 나쁜 예: 부정 조건 사용
if (!buffer.shouldCompact())
- 함수는 한 가지만 해야 한다 (G30): 함수는 단 하나의 명확한 책임만 가져야 합니다. 여러 단계의 작업을 한 함수 내에 순차적으로 나열하는 대신, 각 단계를 별도의 작은 함수로 분리해야 합니다. 이는 함수의 재사용성을 높이고 이해하기 쉽게 만듭니다. (Single Responsibility Principle)
// 나쁜 예: 여러 작업을 한 함수에서 처리
public void pay() {
for (Employee e: employees) { // 1. 직원 목록 순회
if (e.isPayday()) { // 2. 월급 지급일 확인
Money pay = e.calculatePay();
e.deliverPay(pay); // 3. 월급 계산 및 지급
}
}
}
// 좋은 예: 각 작업을 별도 함수로 분리
public void pay() {
for (Employee e: employees) {
payIfNecessary(e);
}
}
private void payIfNecessary(Employee e) {
if (e.isPayday()) {
calculateAndDeliverPay(e);
}
}
private void calculateAndDeliverPay(Employee e) {
Money pay = e.calculatePay();
e.deliverPay(pay); // deliverPay 인자 수정 필요해 보임 (원본 예시 따름)
}
- 숨겨진 시간적인 결합 (G31): 함수 호출 순서가 중요한 경우(시간적 결합), 이를 숨기지 말고 명확하게 드러내야 합니다. 한 함수의 결과를 다음 함수의 인수로 전달하는 방식으로 순서를 강제할 수 있습니다.
// 나쁜 예: 호출 순서가 중요하지만 숨겨져 있음 (오류 발생 가능)
public class MoogDiver {
Gradient gradient; List splines;
public void dive(String reason) {
saturateGradient(); // 먼저 호출되어야 함
reticulateSplines(); // 다음에 호출되어야 함
diveForMoog(reason);
}
}
// 좋은 예: 함수의 반환값을 다음 함수의 인수로 사용하여 시간적 결합을 명시적으로 만듦
public class MoogDiver {
Gradient gradient; List splines;
public void dive(String reason) {
Gradient gradient = saturateGradient();
List splines = reticulateSplines(gradient); // gradient 결과 필요
diveForMoog(splines, reason); // splines 결과 필요
}
}
- 일관성을 유지해라 (G32): 코드 구조를 설계할 때는 명확한 이유를 가지고 일관성 있게 만들어야 합니다. 시스템 전반에 걸쳐 일관된 구조와 스타일을 유지하면, 다른 개발자들이 그 규칙을 따르고 코드를 예측 가능하게 유지하는 데 도움이 됩니다. (G11과 유사)
- 경계 조건을 캡슐화하라 (G33): 반복적으로 사용되는 경계 조건 로직은 별도의 변수나 함수로 캡슐화하여 한 곳에서 관리하는 것이 좋습니다. 이렇게 하면 실수를 줄이고 코드를 명확하게 만들 수 있습니다.
// 나쁜 예: level + 1 이 반복 사용됨
if (level + 1 < tags.length) {
parts = new Parse(body, tags, level + 1, offset + endTag);
body = null;
}
// 좋은 예: 경계 조건(level + 1)을 변수로 캡슐화
int nextLevel = level + 1;
if (nextLevel < tags.length) {
parts = new Parse(body, tags, nextLevel, offset + endTag);
body = null;
}
- 함수는 추상화 수준을 한 단계만 내려가야 한다 (G34): 함수 내의 모든 문장은 동일한 추상화 수준을 가져야 하며, 이는 함수 이름이 나타내는 작업보다 단 한 단계 낮은 수준이어야 합니다. 고수준 개념과 저수준 세부사항이 한 함수 안에 섞여 있으면 안 됩니다.
- 설정 정보는 최상위 단계에 둬라 (G35): 기본값이나 설정 관련 상수 등 시스템의 고수준 정책과 관련된 정보는 저수준 함수 내에 숨기지 말고, 시스템의 최상위 추상화 단계에 배치해야 합니다. 필요하다면 고수준 함수에서 저수준 함수로 인수를 통해 전달합니다.
- 추이적 탐색을 피하라 (G36): 한 모듈은 자신이 직접 상호작용하는 모듈만 알아야 합니다. A가 B를 사용하고 B가 C를 사용할 때, A가 C를 직접 알아서는 안 됩니다 (
a.getB().getC().doSomething()
같은 형태는 피해야 함). 이를 디미터 법칙(Law of Demeter) 또는 "부끄럼 타는 코드 작성(Write Shy Code)"이라고 합니다. myCollaborator.doSomething()
처럼 직접적인 협력자에게만 메시지를 보내야 합니다.