2025-08-31 12:54
-
코드 스멜은 버그는 아니지만, 코드의 구조적인 문제를 암시하여 유지보수를 어렵게 만드는 ‘나쁜 냄새’와 같습니다.
-
긴 메서드, 거대한 클래스, 중복 코드 등 다양한 유형의 코드 스멜은 리팩토링을 통해 개선할 수 있습니다.
-
코드 스멜을 꾸준히 식별하고 개선하는 습관은 기술 부채를 줄이고 소프트웨어의 장기적인 건강을 지키는 핵심입니다.
개발자의 코딩 근육을 키우는 코드 스멜 완전 정복 핸드북
소프트웨어 개발은 건축과 비슷합니다. 잘 지은 건물은 수십 년간 튼튼하게 유지되며 필요에 따라 쉽게 확장할 수 있지만, 부실하게 지은 건물은 작은 문제에도 금이 가고 수리하기 어렵습니다. 코드의 세계에서는 이러한 ‘부실 공사의 징후’를 **코드 스멜(Code Smell)**이라고 부릅니다.
코드 스멜은 당장 문제를 일으키는 버그는 아닙니다. 하지만 마치 주방에서 나는 미세한 냄새가 음식이 상하고 있다는 신호인 것처럼, 코드 스멜은 코드의 설계가 잘못되었거나 미래에 큰 문제를 일으킬 수 있다는 경고 신호입니다. 이 핸드북은 코드 스멜이 무엇인지, 왜 발생하는지, 그리고 어떻게 해결하여 더 건강하고 유연한 코드를 만들 수 있는지 안내하는 종합 가이드입니다.
1. 코드 스멜은 왜 만들어졌을까? (탄생 배경)
1990년대 후반, 켄트 벡(Kent Beck)과 마틴 파울러(Martin Fowler)와 같은 소프트웨어 장인들은 ‘리팩토링’이라는 개념을 대중화했습니다. 리팩토링은 코드의 겉으로 보이는 동작을 바꾸지 않으면서 내부 구조를 개선하는 과정입니다. 이때 그들은 “언제, 어떤 코드를 리팩토링해야 하는가?”라는 질문에 답할 필요가 있었습니다.
그 답이 바로 ‘코드 스멜’입니다. 코드 스멜은 리팩토링이 필요한 지점을 알려주는 경험적인 규칙(Heuristics)의 모음입니다. 즉, 특정 패턴의 코드가 나타나면 “음, 여기서 뭔가 냄새가 나는데? 한번 살펴봐야겠어”라고 생각하게 만드는 직관적인 신호인 셈입니다.
코드 스멜이라는 용어가 탄생하면서 개발자들은 더 이상 코드를 ‘좋다’ 또는 ‘나쁘다’처럼 막연하게 평가하지 않게 되었습니다. 대신 “이 코드는 ‘긴 메서드’ 스멜이 나네요. ‘메서드 추출’ 리팩토링이 필요해 보입니다” 와 같이 구체적이고 건설적인 논의를 할 수 있게 되었습니다. 이는 코드 리뷰와 팀의 전반적인 코드 품질 향상에 혁신적인 변화를 가져왔습니다.
2. 코드 스멜의 구조 (대표적인 유형 파헤치기)
코드 스멜은 다양한 유형으로 분류할 수 있습니다. 여기서는 가장 흔하게 발견되는 유형들을 중심으로, 그 원인과 문제점을 살펴보겠습니다.
카테고리 1: 비대해진 코드 (Bloaters)
코드가 불필요하게 커지고 비대해져서 이해하고 수정하기 어려워진 경우입니다.
스멜 이름 | 설명 | 문제점 |
---|---|---|
긴 메서드 (Long Method) | 하나의 메서드가 너무 많은 일을 처리하며 수십, 수백 줄에 달하는 경우 | 코드 파악이 어렵고, 재사용성이 떨어지며, 작은 수정도 예측 불가능한 부작용을 낳을 수 있음 |
거대한 클래스 (Large Class) | 하나의 클래스가 너무 많은 책임과 데이터를 가지는 경우 | 단일 책임 원칙(SRP) 위반. 클래스가 비대해질수록 응집도가 낮아지고 수정이 어려워짐 |
원시 타입 집착 (Primitive Obsession) | 전화번호, 금액, 단위 등 의미 있는 데이터를 단순 문자열이나 숫자로만 처리하는 경우 | 데이터에 대한 유효성 검사나 관련 로직이 여러 곳에 흩어지게 되어 코드 중복 및 오류 발생 가능성 증가 |
긴 매개변수 목록 (Long Parameter List) | 메서드가 3~4개 이상의 많은 매개변수를 받는 경우 | 메서드 호출이 복잡해지고, 매개변수 순서를 헷갈리기 쉬우며, 관련 없는 데이터가 함께 묶일 가능성 암시 |
데이터 뭉치 (Data Clumps) | 여러 곳에서 항상 똑같은 데이터 그룹이 함께 사용되는 경우 (예: 시작일, 종료일) | 데이터 간의 관계가 코드에 명시적으로 표현되지 않음. 매개변수 목록을 길게 만드는 주범 |
예시: 긴 메서드 (Long Method)
Before (냄새나는 코드):
public void processOrder(Order order) {
// 1. 주문 유효성 검사
if (order.getId() == null || order.getItems().isEmpty()) {
System.out.println("Invalid order data");
return;
}
// 2. 고객 신용도 확인
Customer customer = customerRepository.findById(order.getCustomerId());
if (customer.getCreditRating() < 500) {
System.out.println("Credit check failed");
return;
}
// 3. 재고 확인 및 차감
for (Item item : order.getItems()) {
Product product = productRepository.findById(item.getProductId());
if (product.getStock() < item.getQuantity()) {
System.out.println("Not enough stock for " + product.getName());
return;
}
product.setStock(product.getStock() - item.getQuantity());
productRepository.save(product);
}
// 4. 가격 계산
double totalPrice = 0;
for (Item item : order.getItems()) {
totalPrice += item.getPrice() * item.getQuantity();
}
if (customer.isVip()) {
totalPrice *= 0.9; // VIP 할인
}
// 5. 주문 저장
order.setTotalPrice(totalPrice);
order.setStatus("PROCESSED");
orderRepository.save(order);
System.out.println("Order processed successfully!");
}
After (리팩토링 후):
public void processOrder(Order order) {
try {
validateOrder(order);
Customer customer = checkCustomerCredit(order.getCustomerId());
updateStock(order.getItems());
calculateAndSetTotalPrice(order, customer);
saveOrderAsProcessed(order);
System.out.println("Order processed successfully!");
} catch (OrderProcessingException e) {
System.out.println(e.getMessage());
}
}
private void validateOrder(Order order) throws OrderProcessingException {
if (order.getId() == null || order.getItems().isEmpty()) {
throw new OrderProcessingException("Invalid order data");
}
}
private Customer checkCustomerCredit(Long customerId) throws OrderProcessingException {
Customer customer = customerRepository.findById(customerId);
if (customer.getCreditRating() < 500) {
throw new OrderProcessingException("Credit check failed");
}
return customer;
}
// ... updateStock, calculateAndSetTotalPrice, saveOrderAsProcessed 메서드 ...
설명: processOrder
라는 하나의 거대한 메서드를 논리적인 단위로 나누어 각각의 책임을 가진 작은 private 메서드로 추출했습니다. 코드가 훨씬 간결해지고, 각 단계의 역할을 명확하게 파악할 수 있으며, 재사용과 테스트가 용이해졌습니다.
카테고리 2: 객체 지향 원칙 위반 (Object-Oriented Abusers)
객체 지향 프로그래밍의 장점을 제대로 활용하지 못하고 절차 지향적으로 코드를 작성하는 경우입니다.
스멜 이름 | 설명 | 문제점 |
---|---|---|
Switch 문 (Switch Statements) | 특정 타입이나 조건에 따라 분기 처리를 위해 switch 문이나 여러 개의 if-else 문을 사용하는 경우 | 새로운 타입이 추가될 때마다 모든 switch 문을 찾아 수정해야 함 (OCP 위반). 다형성으로 대체하는 것이 좋음 |
임시 필드 (Temporary Field) | 특정 상황에서만 값이 설정되고 사용되는 클래스 멤버 변수 | 코드를 읽는 사람이 필드의 용도를 파악하기 어렵고, 객체의 상태가 예측 불가능해짐 |
상속 거부 (Refused Bequest) | 부모 클래스의 메서드나 데이터를 자식 클래스에서 거의 사용하지 않는 경우 | 상속 관계가 잘못 설정되었다는 신호. 불필요한 복잡성을 유발하며, 리스코프 치환 원칙(LSP)을 위반할 수 있음 |
예시: Switch 문 (Switch Statements)
Before (냄새나는 코드):
class Employee {
private String type;
// ...
public double calculatePay() {
switch (type) {
case "ENGINEER":
return monthlySalary;
case "SALESMAN":
return monthlySalary + commission;
case "MANAGER":
return monthlySalary + bonus;
default:
throw new IllegalArgumentException("Invalid employee type");
}
}
}
After (리팩토링 후):
// 전략 패턴 또는 다형성 활용
abstract class Employee {
public abstract double calculatePay();
}
class Engineer extends Employee {
@Override
public double calculatePay() {
return getMonthlySalary();
}
}
class Salesman extends Employee {
@Override
public double calculatePay() {
return getMonthlySalary() + getCommission();
}
}
// ... Manager 클래스 ...
설명: type
이라는 문자열에 의존하던 switch
문을 Employee
라는 추상 클래스와 각 직원을 나타내는 서브클래스로 대체했습니다. 이제 새로운 직원 유형(예: Intern
)이 추가되어도 기존 코드를 수정할 필요 없이 Intern
클래스만 새로 만들면 됩니다. 이는 개방-폐쇄 원칙(OCP)을 준수하는 훨씬 유연한 설계입니다.
카테고리 3: 변경을 방해하는 코드 (Change Preventers)
하나의 작은 변경이 코드의 여러 부분을 동시에 수정하도록 강요하는 경직된 구조입니다.
스멜 이름 | 설명 | 문제점 |
---|---|---|
산탄총 수술 (Shotgun Surgery) | 하나의 기능을 수정하기 위해 여러 클래스를 조금씩 수정해야 하는 경우 | 관련된 코드들이 흩어져 있어 수정할 부분을 빠뜨리기 쉽고, 변경의 영향 범위를 파악하기 어려움 |
다발성 수정 (Divergent Change) | 하나의 클래스가 여러 다른 이유로 자주 변경되는 경우 | 클래스가 너무 많은 책임을 가지고 있다는 신호 (SRP 위반). 관련 없는 기능들이 섞여 있어 코드가 복잡해짐 |
카테고리 4: 불필요한 코드 (Dispensables)
존재 이유가 없거나 코드를 더 복잡하게 만드는 불필요한 요소들입니다.
스멜 이름 | 설명 | 문제점 |
---|---|---|
주석 (Comments) | 코드가 하는 일을 설명하기 위해 장황하게 달린 주석 | 좋은 코드는 스스로를 설명해야 함. 주석은 코드가 복잡하고 나쁘다는 것을 가리기 위한 변명일 수 있음 |
중복 코드 (Duplicate Code) | 거의 동일한 코드가 여러 곳에 복사/붙여넣기 되어 있는 경우 | DRY(Don’t Repeat Yourself) 원칙 위반. 수정이 필요할 때 모든 복사본을 찾아 고쳐야 하므로 버그 발생의 온상이 됨 |
죽은 코드 (Dead Code) | 더 이상 호출되거나 사용되지 않는 변수, 매개변수, 메서드, 클래스 | 코드베이스를 불필요하게 비대하게 만들고, 혼란을 야기하며, 유지보수 비용을 증가시킴 |
카테고리 5: 결합을 유발하는 코드 (Couplers)
클래스나 모듈 간의 의존성이 너무 높아 한 부분을 수정하면 다른 부분에 예기치 않은 영향을 주는 경우입니다.
스멜 이름 | 설명 | 문제점 |
---|---|---|
기능 편애 (Feature Envy) | 한 클래스의 메서드가 자기 자신의 데이터보다 다른 클래스의 데이터를 더 많이 사용하는 경우 | 해당 메서드는 잘못된 클래스에 위치해 있다는 강력한 신호. 클래스 간의 결합도를 높임 |
지나친 친밀함 (Inappropriate Intimacy) | 두 클래스가 서로의 내부 구현에 너무 깊이 의존하는 경우 | 한 클래스의 내부를 변경하면 다른 클래스도 함께 변경해야 함. 캡슐화가 깨진 상태 |
메시지 체인 (Message Chains) | a.getB().getC().getD() 와 같이 객체 탐색 코드가 길게 이어진 경우 | 클라이언트 코드가 객체 그래프의 내부 구조에 깊숙이 의존하게 됨 (디미터 법칙 위반) |
3. 코드 스멜 사용법 (진단과 처방)
코드 스멜 핸드북을 알게 된 당신은 이제 코드의 ‘냄새’를 맡을 수 있는 능력을 갖추게 되었습니다. 그렇다면 이 능력을 어떻게 활용해야 할까요?
-
인식(Identify): 코드 리뷰, 페어 프로그래밍을 하거나 자신의 코드를 작성할 때 위에서 언급된 스멜 패턴이 보이는지 의식적으로 찾아보세요. “이 메서드가 너무 긴 것 같은데?”, “이 두 클래스는 왜 이렇게 서로를 자주 호출하지?” 와 같은 질문을 스스로에게 던지는 것이 시작입니다.
-
분석(Analyze): 모든 코드 스멜이 즉시 제거해야 할 악성 종양은 아닙니다. 때로는 성능상의 이유나 비즈니스 로직의 특성 때문에 어쩔 수 없이 발생하는 경우도 있습니다. 냄새의 원인을 분석하고, 이로 인해 발생하는 실질적인 위험(유지보수 비용 증가, 버그 발생 가능성 등)이 얼마나 큰지 판단해야 합니다.
-
리팩토링(Refactor): 위험이 크다고 판단되면, 적절한 리팩토링 기법을 적용하여 스멜을 제거합니다. 예를 들어 ‘긴 메서드’는 ‘메서드 추출(Extract Method)‘로, ‘Switch 문’은 ‘다형성으로 대체(Replace Conditional with Polymorphism)‘로 개선할 수 있습니다.
-
테스트(Test): 리팩토링은 코드의 동작을 변경하지 않는 것이 원칙입니다. 리팩토링 전후에 반드시 테스트 코드를 실행하여 기존 기능이 문제없이 동작하는지 확인해야 합니다. 테스트 코드가 없다면 리팩토링을 시작해서는 안 됩니다.
4. 심화 내용 (기술 부채와 리팩토링)
-
코드 스멜과 기술 부채(Technical Debt): 코드 스멜은 기술 부채의 가장 흔한 ‘이자’입니다. 당장 빠른 개발을 위해 코드 품질을 타협(부채 발생)하면, 나중에 코드 스멜이라는 형태로 나타나 수정과 기능 추가를 더디게 만듭니다(이자 지불). 코드 스멜을 꾸준히 제거하는 것은 기술 부채를 관리하는 가장 효과적인 방법 중 하나입니다.
-
자동화 도구의 활용: 코드의 냄새를 맡는 것은 개발자의 경험에 크게 의존하지만, 오늘날에는 이 과정을 도와주는 훌륭한 도구들이 많습니다. SonarQube, PMD, Checkstyle과 같은 정적 분석 도구나 IntelliJ, Eclipse 같은 IDE의 내장 기능을 활용하면 코드 스멜을 자동으로 감지하고 수정 제안까지 받을 수 있습니다.
결론: 건강한 코드를 위한 꾸준한 관리
코드 스멜은 죄가 아닙니다. 코드를 작성하는 과정에서 누구나 만들어낼 수 있는 자연스러운 현상입니다. 중요한 것은 그 냄새를 방치하지 않고, 원인을 파악하며 코드를 더 나은 방향으로 개선하려는 태도입니다.
이 핸드북에서 소개한 코드 스멜의 유형과 해결 방법을 꾸준히 학습하고 실제 프로젝트에 적용해 보세요. 처음에는 어색하고 시간이 더 걸리는 것처럼 느껴질 수 있지만, 이 습관이 쌓이면 코드의 품질을 보는 눈이 달라지고, 변화에 유연하게 대처할 수 있는 견고한 소프트웨어를 만드는 진정한 ‘개발 장인’으로 성장하게 될 것입니다.