본문으로 건너뛰기

1. 코드 스멜이란 무엇인가​

  • 코드 스멜(Code Smell)은 Martin Fowler가 1999년 저서 "Refactoring: Improving the Design of Existing Code"에서 처음 체계화한 개념입니다.
  • 코드가 기능적으로는 정상 작동하지만, 설계나 구조상 문제가 있어 향후 유지보수나 확장에 어려움을 줄 수 있는 징후를 의미합니다.
  • 코드 스멜은 버그나 오류가 아니라 "개선이 필요한 신호"로, 리팩토링을 통해 해결할 수 있습니다.
정보

코드 스멜은 "냄새"라는 표현을 사용해서 직관적으로 "뭔가 문제가 있다"는 느낌을 주지만, 즉시 수정해야 하는 것은 아닙니다. 상황에 따라 우선순위를 정해 점진적으로 개선하는 것이 좋습니다.

1.1 코드 스멜의 중요성​

  • 유지보수성 향상: 문제가 되는 코드 패턴을 조기에 발견하여 수정 비용을 줄입니다.
  • 가독성 개선: 이해하기 어려운 코드 구조를 개선하여 팀의 생산성을 높입니다.
  • 버그 예방: 복잡하고 결합도가 높은 코드에서 발생할 수 있는 버그를 사전에 방지합니다.
  • 확장성 확보: 새로운 요구사항에 유연하게 대응할 수 있는 코드 구조를 만듭니다.

1.2 코드 스멜과 기술 부채​

  • 코드 스멜은 기술 부채(Technical Debt)의 구체적인 징후라고 볼 수 있습니다.
  • 단기적으로는 문제없이 작동하지만, 장기적으로는 개발 속도를 늦추고 유지보수 비용을 증가시킵니다.
  • 적절한 리팩토링을 통해 이러한 부채를 줄여나가는 것이 중요합니다.

2. Martin Fowler의 코드 스멜 분류 체계​

  • Martin Fowler는 코드 스멜을 다음 5가지 주요 카테고리로 분류했습니다
    • Bloaters (비대한 코드)
    • Object-Orientation Abusers (객체지향 남용자)
    • Change Preventers (변경 방해자)
    • Dispensables (불필요한 것들)
    • Couplers (결합자)

3. Bloaters (비대한 코드)​

  • 비대한 코드는 크기가 비정상적으로 커서 다루기 어려운 코드를 의미합니다.
  • 시간이 지나면서 점진적으로 커져서 관리가 어려워진 코드들이 이 범주에 속합니다.

3.1 Long Method (긴 메서드)​

  • 레퍼런스
  • 징후와 증상
    • 메서드가 너무 많은 코드 라인을 포함하고 있습니다.
    • 일반적으로 10줄보다 긴 메서드는 문제를 제기하기 시작해야 합니다.
  • 문제의 원인
    • 메서드에는 항상 무언가가 추가되지만 제거되는 것은 없습니다.
    • 기존 메서드에 추가하는 것보다 새로운 메서드를 만드는 것이 더 어려운 경우가 많습니다.
    • 이는 또 다른 줄이 추가되고 또 다른 줄이 추가되어 스파게티 코드의 엉킴을 만들어냅니다.
  • 성능
    • 많은 사람들이 주장하는 것처럼 메서드 수의 증가가 성능에 해를 끼칠까요? 거의 모든 경우에 그 영향은 너무 미미해서 걱정할 가치가 없습니다.

3.1.1 해결 방법​

  • 경험상, 메서드 내부의 무언가에 주석을 달고 싶다면, 그 코드를 가져와서 새로운 메서드에 넣어야 합니다.
  • 설명이 필요하다면 한 줄이라도 별도의 메서드로 분리될 수 있고 분리되어야 합니다.
  • 메서드 본문의 길이를 줄이려면 Extract Method를 사용하세요.
    • Extract Method: 긴 메서드의 일부분을 별도의 메서드로 분리하는 기법입니다.
  • 지역 변수와 매개변수가 메서드 추출을 방해한다면, Replace Temp with Query, Introduce Parameter Object 또는 Preserve Whole Object를 사용하세요.
    • Replace Temp with Query: 임시 변수를 제거하고, 해당 값을 반환하는 메서드로 대체하는 기법입니다.
    • Introduce Parameter Object: 여러 개의 매개변수를 하나의 객체로 묶어서 전달하는 기법입니다.
    • Preserve Whole Object: 객체의 여러 속성을 개별적으로 전달하는 대신 객체 전체를 전달하는 기법입니다.
  • 이전의 방법들이 도움이 되지 않는다면, Replace Method with Method Object를 통해 전체 메서드를 별도의 객체로 이동시키는 것을 시도해보세요.
    • Replace Method with Method Object: 복잡한 메서드를 별도의 클래스로 추출하여 여러 메서드로 분해하는 기법
  • 조건 연산자와 루프는 코드가 별도의 메서드로 이동될 수 있다는 좋은 단서입니다.
    • 조건문의 경우 Decompose Conditional을 사용하세요.
    • 루프가 방해가 된다면 Extract Method를 시도해보세요.
    • Decompose Conditional: 복잡한 조건문을 별도의 메서드로 분해하는 기법

3.1.2 이점​

  • 모든 종류의 객체지향 코드 중에서 짧은 메서드를 가진 클래스가 가장 오래 살아남습니다.
  • 메서드나 함수가 길어질수록 이해하고 유지보수하기가 더 어려워집니다.
  • 또한, 긴 메서드는 원치 않는 중복 코드를 숨기기에 완벽한 장소를 제공합니다.

3.2 Large Class (거대한 클래스)​

  • 특징: 하나의 클래스가 너무 많은 책임을 가지고 있는 경우입니다.
  • 문제점: 단일 책임 원칙(SRP)을 위반하며, 클래스의 응집도가 낮아집니다.
  • 해결방법: Extract Class나 Extract Subclass를 통해 책임을 분리합니다.
  • 실무 예시: UserService가 사용자 관리뿐만 아니라 이메일 발송, 파일 업로드, 알림 등 모든 기능을 처리하는 경우입니다.

3.3 Long Parameter List (긴 매개변수 목록)​

  • 특징: 메서드의 매개변수가 4개 이상으로 많은 경우입니다.
  • 문제점: 메서드 호출이 복잡해지고, 매개변수 순서를 기억하기 어려워집니다.
  • 해결방법: Parameter Object 패턴을 사용해 관련 매개변수들을 객체로 묶습니다.
  • 실무 예시: 보고서 생성 메서드에 시작일, 종료일, 지역, 카테고리, 포맷 등 10개 이상의 매개변수가 있는 경우입니다.

3.4 Data Clumps (데이터 덩어리)​

  • 특징: 항상 함께 나타나는 데이터 그룹이 여러 곳에서 반복되는 경우입니다.
  • 문제점: 코드 중복이 발생하고, 관련 데이터의 일관성을 보장하기 어려워집니다.
  • 해결방법: Extract Class를 통해 관련 데이터를 하나의 클래스로 묶습니다.
  • 실무 예시: 주소 정보(거리, 도시, 우편번호, 국가)가 Customer, Supplier, Order 클래스에 각각 개별 필드로 존재하는 경우입니다.

3.5 Primitive Obsession (기본형 집착)​

  • 징후와 증상
    • 간단한 작업에 작은 객체 대신 원시 타입을 사용 (통화, 범위, 전화번호용 특수 문자열 등)
    • 정보 코딩을 위한 상수 사용 (관리자 권한을 가진 사용자를 참조하기 위한 USER_ADMIN_ROLE = 1과 같은 상수)
    • 데이터 배열에서 사용할 필드명으로 문자열 상수 사용

3.5.1 해결 방법​

  • 다양한 원시 타입 필드가 많다면, 그 중 일부를 논리적으로 그룹화하여 자체 클래스로 만드는 것이 가능할 수 있습니다.
    • 더 좋게는, 이 데이터와 관련된 동작도 클래스로 이동시키세요.
    • 이 작업을 위해 Replace Data Value with Object를 시도해보세요.
  • 원시 타입 필드의 값이 메서드 매개변수에서 사용된다면, Introduce Parameter Object 또는 Preserve Whole Object를 사용하세요.
  • 복잡한 데이터가 변수에 코딩되어 있을 때는, Replace Type Code with Class, Replace Type Code with Subclasses 또는 Replace Type Code with State/Strategy를 사용하세요.
  • 변수 중에 배열이 있다면, Replace Array with Object를 사용하세요.

3.5.2 이점​

  • 원시 타입 대신 객체를 사용함으로써 코드가 더 유연해집니다.
  • 코드의 이해성과 구성이 향상됩니다. 특정 데이터에 대한 연산이 흩어져 있지 않고 같은 장소에 있습니다.
  • 이 모든 이상한 상수들과 왜 그것들이 배열에 있는지에 대한 이유를 더 이상 추측할 필요가 없습니다.
  • 중복 코드를 더 쉽게 찾을 수 있습니다.

4. Object-Orientation Abusers (객체지향 남용자)​

객체지향 원칙을 잘못 적용하거나 무시하는 코드들입니다. 객체지향의 이점을 제대로 활용하지 못하는 경우가 많습니다.

4.1 Switch Statements (스위치 문)​

  • 특징: 타입에 따른 분기 처리가 여러 곳에 흩어져 있는 경우입니다.
  • 문제점: 새로운 타입이 추가될 때마다 모든 switch문을 수정해야 합니다.
  • 해결방법: Replace Type Code with Polymorphism을 통해 다형성을 활용합니다.
  • 실무 예시: 결제 타입에 따라 처리 로직, 수수료 계산, 설명 생성이 각각 다른 곳에 switch문으로 구현된 경우입니다.

4.2 Temporary Field (임시 필드)​

  • 특징: 특정 상황에서만 사용되는 인스턴스 변수가 있는 경우입니다.
  • 문제점: 객체의 상태가 불명확해지고, 필드의 용도를 이해하기 어려워집니다.
  • 해결방법: Extract Class를 통해 관련 필드들을 별도 객체로 분리합니다.
  • 실무 예시: Calculator 클래스에서 복잡한 계산을 할 때만 사용되는 중간 결과 저장용 필드들이 있는 경우입니다.

4.3 Refused Bequest (거부된 유산)​

  • 특징: 하위 클래스가 상위 클래스의 메서드나 속성을 사용하지 않거나 재정의해서 무효화하는 경우입니다.
  • 문제점: 상속 관계가 부적절하게 설계되어 리스코프 치환 원칙을 위반합니다.
  • 해결방법: Replace Inheritance with Delegation이나 상속 구조를 재설계합니다.
  • 실무 예시: Bird 클래스를 상속받은 Penguin 클래스에서 fly() 메서드를 예외를 던지도록 재정의하는 경우입니다.

4.4 Alternative Classes with Different Interfaces (다른 인터페이스를 가진 대안 클래스들)​

  • 특징: 같은 일을 하지만 메서드 시그니처가 다른 클래스들이 있는 경우입니다.
  • 문제점: 클라이언트 코드에서 일관된 방식으로 사용할 수 없습니다.
  • 해결방법: Rename Method나 Extract Superclass를 통해 인터페이스를 통일합니다.
  • 실무 예시: XMLParser와 JsonParser가 각각 parseXml(), parseJson() 메서드를 가지는 대신 공통의 parse() 메서드를 가져야 하는 경우입니다.

5. Change Preventers (변경 방해자)​

코드 변경을 어렵게 만드는 구조적 문제들입니다. 한 곳을 수정하기 위해 여러 곳을 동시에 수정해야 하는 경우가 많습니다.

5.1 Divergent Change (확산적 변경)​

  • 특징: 하나의 클래스가 여러 가지 이유로 자주 변경되는 경우입니다.
  • 문제점: 단일 책임 원칙을 위반하며, 변경 시 예상치 못한 부작용이 발생할 수 있습니다.
  • 해결방법: Extract Class를 통해 변경 이유별로 클래스를 분리합니다.
  • 실무 예시: User 클래스가 사용자 정보 변경, 권한 관리 변경, 알림 설정 변경 등 다양한 이유로 수정되는 경우입니다.

5.2 Shotgun Surgery (산탄총 수술)​

  • 특징: 하나의 변경사항을 적용하기 위해 여러 클래스를 동시에 수정해야 하는 경우입니다.
  • 문제점: 변경 시 놓치는 부분이 생길 수 있고, 실수할 가능성이 높아집니다.
  • 해결방법: Move Method나 Move Field를 통해 관련 기능을 한 곳으로 모읍니다.
  • 실무 예시: 데이터베이스 스키마 변경 시 Repository, Service, DTO, Controller 등 여러 레이어의 클래스들을 모두 수정해야 하는 경우입니다.

5.3 Parallel Inheritance Hierarchies (평행 상속 계층)​

  • 특징: 한 클래스의 서브클래스를 만들 때마다 다른 클래스의 서브클래스도 만들어야 하는 경우입니다.
  • 문제점: 코드 중복이 발생하고 계층 구조가 복잡해집니다.
  • 해결방법: Move Method와 Move Field를 사용해 중복을 제거하고 구조를 단순화합니다.
  • 실무 예시: Employee 계층(Manager, Developer, Designer)과 EmployeeReport 계층(ManagerReport, DeveloperReport, DesignerReport)이 항상 함께 확장되는 경우입니다.

6. Dispensables (불필요한 것들)​

  • 코드에서 제거해도 되거나 오히려 제거하는 것이 좋은 요소들입니다.
  • 코드를 더 깔끔하고 이해하기 쉽게 만듭니다.

6.1 Comments (주석)​

  • 특징: 코드의 의도를 설명하기 위한 과도한 주석이 있는 경우입니다.
  • 문제점: 코드가 변경되어도 주석은 업데이트되지 않아 혼란을 야기할 수 있습니다.
  • 해결방법: Extract Method나 Rename Method를 통해 코드 자체가 의도를 명확히 드러내도록 합니다.
  • 실무 예시: 복잡한 비즈니스 로직에 대한 긴 주석 대신 메서드명과 변수명을 명확하게 작성하는 것이 좋습니다.
경고

모든 주석이 나쁜 것은 아닙니다. API 문서, 복잡한 알고리즘 설명, 비즈니스 규칙에 대한 주석은 여전히 유용합니다.

6.2 Duplicate Code (중복 코드)​

  • 특징: 동일하거나 유사한 코드가 여러 곳에 존재하는 경우입니다.
  • 문제점: 수정 시 모든 중복 부분을 찾아서 변경해야 하고, 일부를 놓칠 위험이 있습니다.
  • 해결방법: Extract Method, Pull Up Method, Form Template Method 등을 사용해 중복을 제거합니다.
  • 실무 예시: 여러 컨트롤러에서 동일한 검증 로직이나 예외 처리 로직이 반복되는 경우입니다.

6.3 Lazy Class (게으른 클래스)​

  • 특징: 충분한 기능을 수행하지 않아 존재 가치가 의문스러운 클래스입니다.
  • 문제점: 불필요한 복잡성을 추가하고 코드 이해를 어렵게 만듭니다.
  • 해결방법: Inline Class나 Collapse Hierarchy를 통해 클래스를 제거하거나 통합합니다.
  • 실무 예시: 단순히 다른 클래스의 메서드를 호출하기만 하는 Wrapper 클래스나 필드만 있고 로직이 없는 DTO 클래스입니다.

6.4 Dead Code (죽은 코드)​

  • 특징: 더 이상 사용되지 않는 코드입니다.
  • 문제점: 코드베이스를 불필요하게 복잡하게 만들고 유지보수 부담을 증가시킵니다.
  • 해결방법: 사용되지 않는 코드를 완전히 제거합니다.
  • 실무 예시: 리팩토링 과정에서 남겨진 사용되지 않는 메서드나 더 이상 호출되지 않는 API 엔드포인트입니다.

6.5 Data Class (데이터 클래스)​

  • 레퍼런스
  • 징후와 증상
    • 데이터 클래스는 필드와 이에 접근하기 위한 기본적인 메서드들(getter와 setter)만을 포함하는 클래스를 의미합니다.
    • 이런 클래스들은 단순히 다른 클래스들이 사용하는 데이터의 컨테이너 역할만 합니다.
    • 이러한 클래스들은 추가적인 기능을 포함하지 않으며, 자신이 소유한 데이터에 대해 독립적으로 작업을 수행할 수 없습니다.
    • Data Class의 문제는 "비즈니스 로직이 있어야 할 곳에 데이터만 있는 경우"를 의미합니다.
      • 데이터 전송이나 값 표현이 목적이라면 데이터만 있어도 괜찮지만, 도메인 개념을 표현하는 객체라면 해당 데이터에 대한 행동도 함께 가져야 함을 의미합니다.
  • 문제의 원인
    • 데이터 클래스는 객체지향 프로그래밍의 이점을 활용하지 못하고, 단순히 데이터를 전달하는 용도로만 사용됩니다.
      • 이러한 클래스들은 비즈니스 로직을 포함하지 않으며, 다른 클래스에 의존하여 작업을 수행합니다.

6.5.1 해결 방법​

  • 클래스가 public 필드를 포함하고 있다면, 필드 캡슐화(Encapsulate Field)를 사용하여 직접 접근을 차단하고 getter와 setter를 통해서만 접근하도록 요구하세요.
    • 하지만 getter와 setter를 사용한다고 해서 진정한 캡술화를 달성한 것은 아닙니다.
    • 대신, Move Method를 사용하여 클래스에 비즈니스 로직을 추가하세요.
  • 컬렉션(배열 등)에 저장된 데이터에 대해서는 컬렉션 캡슐화(Encapsulate Collection)를 사용하세요.
  • 해당 클래스를 사용하는 클라이언트 코드를 검토하세요. 그 안에서 데이터 클래스 자체에 위치하는 것이 더 좋을 기능을 찾을 수 있을 것입니다.
    • 이런 경우라면, 메서드 이동(Move Method)과 메서드 추출(Extract Method)을 사용하여 이 기능을 데이터 클래스로 마이그레이션하세요.
  • 클래스가 잘 설계된 메서드들로 채워진 후에는, 클래스 데이터에 대해 지나치게 광범위한 접근을 제공하는 기존의 데이터 접근 메서드들을 제거하고 싶을 것입니다.
    • 이를 위해 설정 메서드 제거(Remove Setting Method)와 메서드 숨기기(Hide Method)가 도움이 될 수 있습니다.

6.5 Speculative Generality (추측에 의한 일반화)​

  • 특징: 미래에 필요할 것 같다는 추측으로 만든 불필요한 추상화입니다.
  • 문제점: YAGNI(You Aren't Gonna Need It) 원칙을 위반하며 불필요한 복잡성을 추가합니다.
  • 해결방법: Collapse Hierarchy, Inline Class, Remove Parameter 등을 통해 과도한 일반화를 제거합니다.
  • 실무 예시: 현재는 하나의 구현체만 있는데 미래 확장을 위해 만든 복잡한 인터페이스 계층입니다.

7. Couplers (결합자)​

클래스 간의 과도한 결합을 나타내는 코드 스멜들입니다. 높은 결합도는 코드 변경을 어렵게 만들고 재사용성을 떨어뜨립니다.

7.1 Feature Envy (기능에 대한 욕심)​

  • 징후와 증상
    • 메서드가 자신의 데이터보다 다른 객체의 데이터에 더 많이 접근하는 경우입니다.
  • 문제의 원인
    • 필드들이 데이터 클래스로 이동된 후에 발생할 수 있습니다.
    • 이런 경우라면 해당 데이터를 사용하는 연산들도 함께 이 클래스로 이동시키는 것을 고려해야 합니다.

7.1.1 해결 방법​

  • 기본 원칙으로, 함께 변경되는 것들은 같은 곳에 두어야 합니다.
    • 일반적으로 데이터와 그 데이터를 사용하는 함수들은 함께 변경됩니다
  • 메서드 전체가 다른 클래스 데이터만 사용하는 경우 Move Method를 사용하여 해당 클래스로 메서드를 이동시키세요.
  • 메서드 일부분만 다른 클래스 데이터를 사용하는 경우 Extract Method를 사용하여 해당 부분을 별도의 메서드로 추출하고, 그 메서드를 호출하는 클래스로 이동시킵니다.

7.2 Inappropriate Intimacy (부적절한 친밀성)​

  • 특징: 두 클래스가 서로의 private 영역에 과도하게 접근하는 경우입니다.
  • 문제점: 클래스 간의 결합도가 높아져 독립적인 변경이 어려워집니다.
  • 해결방법: Move Method, Move Field, Extract Class 등을 통해 결합도를 낮춥니다.
  • 실무 예시: Order와 OrderItem 클래스가 서로의 내부 구현에 과도하게 의존하는 경우입니다.

7.3 Message Chains (메시지 체인)​

  • 특징: 클라이언트가 객체의 연쇄적인 호출을 통해 필요한 정보를 얻는 경우입니다.
  • 문제점: Law of Demeter를 위반하며, 중간 객체 구조 변경 시 클라이언트 코드도 수정해야 합니다.
  • 해결방법: Hide Delegate를 통해 중간 단계를 숨기거나 Extract Method로 체인을 캡슐화합니다.
  • 실무 예시: customer.getAddress().getCity().getName() 같은 긴 체인 호출입니다.

7.4 Middle Man (중개자)​

  • 특징: 클래스가 단순히 다른 클래스로 작업을 위임하기만 하는 경우입니다.
  • 문제점: 불필요한 간접성을 추가하여 코드를 복잡하게 만듭니다.
  • 해결방법: Remove Middle Man을 통해 중개자를 제거하고 직접 통신하도록 합니다.
  • 실무 예시: Department 클래스가 Manager 클래스의 메서드를 그대로 위임만 하는 경우입니다.

참고​