백엔드

객체지향 설계 원칙 (SOLID) 개념과 적용 방법

Newbie Developer 2025. 2. 17. 11:24

 

객체지향 프로그래밍(Object-Oriented Programming, OOP)은 유지보수성과 확장성이 뛰어난 소프트웨어를 개발하는 데 중요한 패러다임입니다. 이를 효과적으로 활용하기 위해서는 객체지향 설계 원칙(SOLID) 을 이해하고 적용하는 것이 필수적입니다.

이번 글에서는 객체지향 설계 원칙의 핵심 개념과 실제 프로젝트에서 이를 적용하는 방법을 알아보겠습니다.


1. 객체지향 설계 원칙이란?

객체지향 설계 원칙은 소프트웨어를 설계할 때 결합도(Coupling)를 낮추고 응집도(Cohesion)를 높여 유지보수성과 확장성을 개선하는 것을 목표로 합니다. 대표적인 원칙으로 SOLID 원칙이 있으며, 이는 다섯 가지의 핵심 원칙으로 구성됩니다.

SOLID 원칙

SOLID는 다섯 가지 객체지향 설계 원칙의 약어입니다.

  • S : 단일 책임 원칙 (Single Responsibility Principle, SRP)
  • O : 개방-폐쇄 원칙 (Open/Closed Principle, OCP)
  • L : 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
  • I : 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
  • D : 의존 역전 원칙 (Dependency Inversion Principle, DIP)

이제 각 원칙을 자세히 살펴보겠습니다.


2. SOLID 원칙 상세 설명

1) 단일 책임 원칙 (SRP: Single Responsibility Principle)

"클래스는 단 하나의 책임만 가져야 한다."

하나의 클래스가 여러 가지 역할을 수행하면 유지보수성이 떨어지고, 변경 시 영향을 받는 범위가 커집니다. 따라서, 클래스는 한 가지 책임(변경 이유)을 가져야 합니다.

✅ 적용 방법

  • 하나의 클래스는 하나의 역할만 수행하도록 분리합니다.
  • 기능이 많아지는 경우 별도의 클래스로 분리하여 관심사를 분리합니다.

❌ 잘못된 코드 (SRP 위반)

class Report {
    public String generateReport() {
        return "Report Data";
    }

    public void saveToFile(String filename) {
        System.out.println("Saving report to " + filename);
    }
}

➡️ Report 클래스가 리포트 생성과 파일 저장 두 가지 책임을 가지고 있습니다.

 

✅ 개선된 코드 (SRP 적용)

class Report {
    public String generateReport() {
        return "Report Data";
    }
}

class FileSaver {
    public void saveToFile(String data, String filename) {
        System.out.println("Saving report to " + filename);
    }
}

➡️ 리포트 생성파일 저장을 각각의 클래스로 분리하여 단일 책임 원칙(SRP) 을 준수했습니다.


2) 개방-폐쇄 원칙 (OCP: Open/Closed Principle)

"코드는 확장에는 열려 있어야 하지만, 변경에는 닫혀 있어야 한다."

❌ 잘못된 코드 (OCP 위반)

class Payment {
    public void pay(String method, double amount) {
        if ("credit".equals(method)) {
            System.out.println("Paid with Credit Card");
        } else if ("paypal".equals(method)) {
            System.out.println("Paid with PayPal");
        }
    }
}

➡️ 새로운 결제 방법이 추가될 때마다 pay() 메서드를 수정해야 합니다.

✅ 개선된 코드 (OCP 적용)

interface PaymentMethod {
    void pay(double amount);
}

class CreditCard implements PaymentMethod {
    public void pay(double amount) {
        System.out.println("Paid with Credit Card");
    }
}

class PayPal implements PaymentMethod {
    public void pay(double amount) {
        System.out.println("Paid with PayPal");
    }
}

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

➡️ 인터페이스를 활용하여 결제 방식을 확장 가능하게 만들었습니다. 새로운 결제 수단이 추가되어도 기존 코드를 변경할 필요가 없습니다.


3) 리스코프 치환 원칙 (LSP: Liskov Substitution Principle)

"자식 클래스는 부모 클래스를 대체할 수 있어야 한다."

❌ 잘못된 코드 (LSP 위반)

class Bird {
    public void fly() {
        System.out.println("Flying...");
    }
}

class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguins cannot fly");
    }
}

➡️ 펭귄은 날 수 없기 때문에 fly() 메서드를 호출하면 예외가 발생합니다. 이는 LSP를 위반한 것입니다.

✅ 개선된 코드 (LSP 적용)

interface Bird {
    void move();
}

class Sparrow implements Bird {
    public void move() {
        System.out.println("Flying...");
    }
}

class Penguin implements Bird {
    public void move() {
        System.out.println("Swimming...");
    }
}

➡️ Bird 인터페이스를 활용하여 모든 새가 이동할 수 있도록 일반화했습니다.


4) 인터페이스 분리 원칙 (ISP: Interface Segregation Principle)

"클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다."

❌ 잘못된 코드 (ISP 위반)

interface Worker {
    void work();
    void eat();
}

class Robot implements Worker {
    @Override
    public void work() {
        System.out.println("Robot working...");
    }

    @Override
    public void eat() {
        throw new UnsupportedOperationException("Robots don't eat");
    }
}

➡️ Robot은 eat() 메서드가 필요 없지만, 인터페이스를 구현해야 합니다.

✅ 개선된 코드 (ISP 적용)

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class HumanWorker implements Workable, Eatable {
    public void work() {
        System.out.println("Human working...");
    }

    public void eat() {
        System.out.println("Human eating...");
    }
}

class RobotWorker implements Workable {
    public void work() {
        System.out.println("Robot working...");
    }
}

➡️ 필요한 기능만 인터페이스로 분리하여 불필요한 의존성을 제거했습니다.


5) 의존 역전 원칙 (DIP: Dependency Inversion Principle)

"상위 모듈은 하위 모듈에 의존하면 안 되며, 둘 다 추상화에 의존해야 한다."

❌ 잘못된 코드 (DIP 위반)

class EmailSender {
    public void send(String message) {
        System.out.println("Sending Email: " + message);
    }
}

class Notification {
    private EmailSender emailSender = new EmailSender();

    public void notifyUser(String message) {
        emailSender.send(message);
    }
}

➡️ Notification 클래스가 EmailSender에 직접 의존하고 있어 변경이 어렵습니다.

✅ 개선된 코드 (DIP 적용)

interface MessageSender {
    void send(String message);
}

class EmailSender implements MessageSender {
    public void send(String message) {
        System.out.println("Sending Email: " + message);
    }
}

class Notification {
    private final MessageSender messageSender;

    public Notification(MessageSender messageSender) {
        this.messageSender = messageSender;
    }

    public void notifyUser(String message) {
        messageSender.send(message);
    }
}

➡️ Notification이 구체적인 구현이 아닌 인터페이스(MessageSender)에 의존하도록 변경하여 유연성을 높였습니다.


결론

SOLID 원칙을 준수하면 유지보수성이 높고 확장성이 뛰어난 객체지향 설계가 가능합니다.

원칙 핵심 개념
SRP 하나의 클래스는 하나의 책임만 가져야 한다.
OCP 기존 코드를 변경하지 않고 기능을 확장할 수 있어야 한다.
LSP 자식 클래스는 부모 클래스를 대체할 수 있어야 한다.
ISP 클라이언트가 필요하지 않은 메서드에 의존하지 않아야 한다.
DIP 구체적인 구현이 아니라 추상화에 의존해야 한다.

이 원칙들을 적용하면 클린 코드유지보수성이 뛰어난 소프트웨어를 만들 수 있습니다! 🚀