카테고리 없음

코드 스멜: 객체지향 오용의 징후들 🚩

블로글러 2025. 3. 22. 22:22

객체지향 프로그래밍을 하다 보면 어떤 코드가 '이상하게' 느껴질 때가 있지 않으신가요? 코드가 기능은 하지만 뭔가 불편함이 느껴지는 그런 순간이요. 이런 느낌은 마치 냉장고에서 나는 이상한 냄새와 같습니다. 냄새가 난다고 해서 음식이 완전히 상한 것은 아니지만, 곧 문제가 생길 수 있다는 신호입니다. 🧪

프로그래밍에서 이런 '냄새'를 코드 스멜(Code Smell)이라고 부르며, 오늘은 그 중에서도 객체지향 프로그래밍 원칙을 제대로 적용하지 못해 발생하는 '객체지향 오용(Object-Orientation Abusers)'에 대해 알아보겠습니다.

왜 필요한가?

객체지향 오용 패턴을 인식하는 것이 중요한 이유는 다음과 같습니다:

  1. 유지보수성 향상: 이러한 패턴을 제거하면 코드를 더 쉽게 이해하고 수정할 수 있습니다.
  2. 확장성 개선: 객체지향 원칙을 올바르게 적용하면 새로운 기능을 추가하기가 더 쉬워집니다.
  3. 오류 감소: 잘 설계된 객체지향 코드는 버그가 생길 가능성이 적습니다.
  4. 테스트 용이성: 올바른 객체지향 설계는 단위 테스트를 더 쉽게 작성할 수 있게 합니다.

기본 원리

객체지향 오용의 주요 유형들을 살펴볼까요?

1. 스위치 문 남용 (Switch Statements) 🔄

복잡한 switch 문이나 여러 개의 if 문이 반복적으로 나타나는 경우입니다. 이는 객체지향의 다형성(polymorphism) 원칙을 활용하지 않고 있다는 신호입니다.

// 나쁜 예
public double calculatePay(Employee employee) {
    switch (employee.getType()) {
        case ENGINEER:
            return employee.getBaseSalary() * 1.2;
        case MANAGER:
            return employee.getBaseSalary() * 1.5;
        case DIRECTOR:
            return employee.getBaseSalary() * 2.0;
        default:
            return employee.getBaseSalary();
    }
}

이 코드의 문제점은 새로운 직원 유형이 추가될 때마다 switch 문을 수정해야 한다는 것입니다. 이는 개방-폐쇄 원칙(Open-Closed Principle)을 위반합니다.

리팩토링 방법:
다형성을 활용하여 각 하위 클래스가 자신의 계산 방식을 구현하도록 합니다.

// 개선된 예
public abstract class Employee {
    protected double baseSalary;

    public abstract double calculatePay();
}

public class Engineer extends Employee {
    @Override
    public double calculatePay() {
        return baseSalary * 1.2;
    }
}

public class Manager extends Employee {
    @Override
    public double calculatePay() {
        return baseSalary * 1.5;
    }
}

2. 임시 필드 (Temporary Field) 🔍

객체의 필드가 특정 상황에서만 값이 채워지고, 그 외에는 비어있는 경우입니다. 이런 코드는 이해하기 어렵고 예측할 수 없는 동작을 유발할 수 있습니다.

// 나쁜 예
public class Order {
    private Customer customer;
    private List<Item> items;
    private double discountRate; // 특정 고객에게만 적용됨
    private Gift gift; // 특정 금액 이상 주문시에만 사용됨

    public void process() {
        // discountRate와 gift는 대부분의 경우 null
        if (customer.isVIP()) {
            discountRate = 0.1;
        }

        if (calculateTotal() > 10000) {
            gift = new Gift();
        }

        // 코드 계속...
    }
}

리팩토링 방법:

  1. 클래스 추출(Extract Class): 관련 필드와 메서드를 새로운 클래스로 분리
  2. Null 객체 패턴(Null Object Pattern): null 대신 기본 동작을 하는 객체 사용
// 개선된 예
public class Order {
    private Customer customer;
    private List<Item> items;
    private DiscountStrategy discountStrategy;
    private GiftStrategy giftStrategy;

    public Order(Customer customer) {
        this.customer = customer;
        this.discountStrategy = customer.isVIP() ? 
            new VIPDiscountStrategy() : new NoDiscountStrategy();
        this.giftStrategy = new GiftStrategyFactory().createFor(this);
    }

    public void process() {
        double discount = discountStrategy.calculateDiscount(this);
        Gift gift = giftStrategy.getGift();
        // 코드 계속...
    }
}

3. 상속 거부 (Refused Bequest) 🚫

하위 클래스가 상위 클래스에서 상속받은 메서드나 속성의 일부만 사용하는 경우입니다. 이는 상속 관계가 적절하지 않다는 신호일 수 있습니다.

// 나쁜 예
public class Bird {
    public void fly() {
        // 날기 구현
    }

    public void makeSound() {
        // 소리내기 구현
    }
}

public class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("펭귄은 날 수 없습니다!");
    }
}

펭귄이 Bird 클래스의 fly() 메서드를 거부하고 있습니다. 이는 "is-a" 관계가 적절하지 않다는 신호입니다.

리팩토링 방법:

  1. 합성 사용(Composition over Inheritance): 상속 대신 합성 관계 사용
  2. 인터페이스 추출(Extract Interface): 공통 동작을 인터페이스로 분리
// 개선된 예
public interface Bird {
    void makeSound();
}

public interface FlyingBird extends Bird {
    void fly();
}

public class Sparrow implements FlyingBird {
    @Override
    public void fly() {
        // 날기 구현
    }

    @Override
    public void makeSound() {
        // 소리내기 구현
    }
}

public class Penguin implements Bird {
    @Override
    public void makeSound() {
        // 소리내기 구현
    }
}

4. 다른 인터페이스를 가진 유사 클래스 (Alternative Classes with Different Interfaces) 🔄

동일한 기능을 수행하지만 서로 다른 메서드명과 인터페이스를 가진 클래스들이 존재하는 경우입니다.

// 나쁜 예
public class EmailSender {
    public void sendMessage(String to, String subject, String body) {
        // 이메일 전송 구현
    }
}

public class SMSNotifier {
    public void sendSMS(String phoneNumber, String message) {
        // SMS 전송 구현
    }
}

리팩토링 방법:

  1. 메서드 이름 변경(Rename Method): 일관된 이름으로 메서드 변경
  2. 인터페이스 추출(Extract Interface): 공통 동작을 인터페이스로 분리
// 개선된 예
public interface MessageSender {
    void send(String destination, String content);
}

public class EmailSender implements MessageSender {
    @Override
    public void send(String emailAddress, String content) {
        // 이메일 전송 구현
    }
}

public class SMSSender implements MessageSender {
    @Override
    public void send(String phoneNumber, String content) {
        // SMS 전송 구현
    }
}

실제 예제

한 온라인 쇼핑몰 시스템을 생각해봅시다. 이 시스템은 다양한 결제 방법을 지원해야 합니다.

스위치 문 남용의 실제 예제

// 나쁜 설계
public class PaymentProcessor {
    public void processPayment(String paymentMethod, double amount) {
        switch (paymentMethod) {
            case "CREDIT_CARD":
                System.out.println("신용카드로 " + amount + "원 결제 처리");
                break;
            case "BANK_TRANSFER":
                System.out.println("계좌이체로 " + amount + "원 결제 처리");
                break;
            case "MOBILE_PAYMENT":
                System.out.println("모바일 결제로 " + amount + "원 결제 처리");
                break;
            default:
                throw new IllegalArgumentException("지원하지 않는 결제 방법입니다.");
        }
    }
}

이 코드에서는 새로운 결제 방법이 추가될 때마다 switch 문을 수정해야 합니다. 이는 개방-폐쇄 원칙을 위반합니다.

다형성을 활용한 개선

// 개선된 설계
public interface PaymentMethod {
    void processPayment(double amount);
}

public class CreditCardPayment implements PaymentMethod {
    @Override
    public void processPayment(double amount) {
        System.out.println("신용카드로 " + amount + "원 결제 처리");
    }
}

public class BankTransferPayment implements PaymentMethod {
    @Override
    public void processPayment(double amount) {
        System.out.println("계좌이체로 " + amount + "원 결제 처리");
    }
}

public class MobilePayment implements PaymentMethod {
    @Override
    public void processPayment(double amount) {
        System.out.println("모바일 결제로 " + amount + "원 결제 처리");
    }
}

public class PaymentProcessor {
    public void processPayment(PaymentMethod paymentMethod, double amount) {
        paymentMethod.processPayment(amount);
    }
}

다음은 표로 정리한 객체지향 오용 패턴과 해결 방법입니다:

코드 스멜 증상 해결 방법
스위치 문 남용 복잡한 조건문이 반복적으로 나타남 다형성 활용, 상태/전략 패턴 사용
임시 필드 특정 상황에서만 사용되는 필드 클래스 추출, Null 객체 패턴
상속 거부 상속받은 메서드/속성의 일부만 사용 합성 관계 사용, 인터페이스 추출
다른 인터페이스를 가진 유사 클래스 유사한 기능의 클래스들이 다른 인터페이스 사용 메서드 이름 통일, 인터페이스 추출

주의사항 및 팁 💡

⚠️ 이것만은 주의하세요!

  1. 과도한 리팩토링 지양하기

    • 작은 프로젝트에서는 때로는 복잡한 리팩토링보다 단순한 해결책이 더 효율적일 수 있습니다.
    • 모든 코드 스멜을 무조건 제거해야 한다는 강박관념은 금물입니다.
  2. 상속 관계 신중하게 설계하기

    • "is-a" 관계가 확실할 때만 상속을 사용하세요.
    • 확신이 없다면 합성(composition)을 고려하세요.
  3. 인터페이스 일관성 유지하기

    • 비슷한 기능을 하는 클래스들은 동일한 인터페이스를 가져야 합니다.
    • 메서드 이름과 파라미터를 일관되게 유지하세요.

💡 꿀팁

  • 리팩토링은 작은 단계로 진행하고, 각 단계마다 테스트를 실행하세요.
  • 디자인 패턴을 적절히 활용하면 코드 스멜을 효과적으로 제거할 수 있습니다.
  • 정기적인 코드 리뷰를 통해 코드 스멜을 조기에 발견하세요.
  • 테스트 주도 개발(TDD)을 실천하면 코드 스멜이 발생할 가능성이 줄어듭니다.

마치며

지금까지 객체지향 오용의 네 가지 주요 코드 스멜에 대해 알아보았습니다. 이러한 패턴을 인식하고 적절한 리팩토링 기법을 적용하면 코드의 유지보수성과 확장성을 크게 향상시킬 수 있습니다. 처음에는 어려울 수 있지만, 경험이 쌓일수록 자연스럽게 더 나은 설계를 할 수 있게 될 것입니다. 🌱

혹시 객체지향 프로그래밍에서 겪고 계신 특별한 문제나 더 알고 싶은 내용이 있으시면 언제든지 물어보세요!

참고 자료 🔖


#코드스멜 #객체지향프로그래밍 #리팩토링 #디자인패턴

728x90
반응형