객체 지향 프로그래밍 - SOLID

SOLID란?

  SOLID는 객체 지향 프로그래밍 및 설계에서 시간이 지나도 소프트웨어의 유지 보수 및 확장이 용이하게하고, 이해하기 쉬운 소프트웨어를 만들고자 할 때 적용하는 5가지 원칙이다.

 

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

 - 단일 책임 원칙은 클래스가 하나의 책임만 가져야 한다. 클래스가 변경될 이유가 줄어들어 시스템이 더 유연해지고 변경에 대한 영향이 적어지며, 유지보수가 용이해진다.

 

예시) 아래의 코드를 보면 하나의 클래스에 보고서 생성, 인쇄, 저장하는 로직이 하나의 클래스에 포함되어있다. 

class Report {
    void generateReport() {
        // 보고서 생성 로직
    }

    void printReport() {
        // 보고서 인쇄 로직
    }

    void saveReport() {
        // 보고서를 데이터베이스에 저장하는 로직
    }
}

 

이를 개선하기 위해서 아래의 코드처럼 각 책임을 별도의 클래스로 분리할 수 있다.

class ReportGenerator {
    void generateReport() {
        // 보고서 생성 로직
    }
}

class ReportPrinter {
    void printReport() {
        // 보고서 인쇄 로직
    }
}

class ReportSaver {
    void saveReport() {
        // 보고서를 데이터베이스에 저장하는 로직
    }
}

 

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

 - 소프트웨어 엔티티는 확장에 열려 있지만 변경에는 닫혀있어야한다. 추상화와 다형성을 활용하여 기존의 코드를 변경하지 않으면서도 시스템의 기능을 확장할 수 있어야 한다.

 

예시) 아래의 소스코드는 결제 시스템에서 다양한 결제 방식을 처리하는 클래스를 구현한 코드이다. 새로운 결제 방식이 추가될 때마다 아래의 클래스를 수정해야 하므로 OCP에 위배된다.

class PaymentProcessor {
    void processCreditCardPayment() {
        // 신용카드 결제 처리
    }

    void processPayPalPayment() {
        // PayPal 결제 처리
    }
}

 

그래서 인터페이스를 정의하고 각 결제 방식을 이를 구현한 클래스로 분리한다.

interface PaymentMethod {
    void processPayment();
}

class CreditCardPayment implements PaymentMethod {
    public void processPayment() {
        // 신용카드 결제 처리
    }
}

class PayPalPayment implements PaymentMethod {
    public void processPayment() {
        // PayPal 결제 처리
    }
}

class PaymentProcessor {
    void processPayment(PaymentMethod paymentMethod) {
        paymentMethod.processPayment();
    }
}

 

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

 - 서브타입은 언제나 베이스 타입으로 교체될 수 있어야 한다. 상속을 사용할 때, 자식 클래스가 부모 클래스의 역할을 완전히 대체할 수 있어야하고, 이를 통해 소프트웨어의 정확성을 유지할 수 있다.

 

예시) 'Birt' 클래스와 이를 상속받는 'Duck', 'Ostrich' 클래스가 있다고 가정할때, 'Ostrich' 클래스는 'fly' 메소드를 적절하게 사용할 수 없다.

class Bird {
    void fly() {
        // 날다
    }
}

class Duck extends Bird {
    // Duck에 맞는 fly 구현
}

class Ostrich extends Bird {
    // 타조는 날 수 없으므로 fly 구현이 문제가 됨
}

 

이를 개선하기 위해 해당 새를 날 수 있는 새와 없는 새로 분리한다.

class Bird {
}

class FlyingBird extends Bird {
    void fly() {
        // 날다
    }
}

class Duck extends FlyingBird {
    // Duck에 맞는 fly 구현
}

class Ostrich extends Bird {
    // 타조는 날 수 없으므로 FlyingBird를 상속받지 않음
}

 

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

 - 클라이언트는 사용하지 않는 메소드에 의존하면 안된다. 큰 인터페이스를 작고 구체적인 여러 개의 인터페이스로 분리하는 것을 권장한다. 인터페이스가 명확해지고, 대체 가능성이 높아진다.(분리를 통해 연관된 인터페이스의 변경이 해당 클라이언트에 영향을 주지 않는다.)

 

예시) 여러 기능을 제공하는 'SmartDevice' 인터페이스가 있다. 만약 'Camera' 클래스가 'SmartDevice'인터페이스 구현 시 불필요한 'call', 'browseInternet'와 같은 메소드도 구현해야 한다.

interface SmartDevice {
    void call();
    void browseInternet();
    void takePhoto();
}

 

이를 해결하기 위해 각 기능에 대한 별도의 인터페이스를 만드는 것이 좋다.

interface Phone {
    void call();
}

interface InternetBrowser {
    void browseInternet();
}

interface Camera {
    void takePhoto();
}

class SmartPhone implements Phone, InternetBrowser, Camera {
    public void call() {
        // 전화 기능 구현
    }

    public void browseInternet() {
        // 인터넷 검색 기능 구현
    }

    public void takePhoto() {
        // 사진 촬영 기능 구현
    }
}

 

5. DIP(Dependency inversion principle) - 의존관계 역전 원칙

 - 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야한다. 추상화는 세부 사항에 의존해서는 안되고 세부 사항은 추상화에 의존해야 한다. 구체적인 구현이 아닌 인터페이스를 통한 의존 관계 설정을 권장한다. 

 고수준 모듈은 비즈니스 규칙이나 로직을 처리하는 부분, 저수준 모듈은 데이터 저장, 네트워크 통신 같은 세부적인 작업을 처리하는 부분을 말한다.

 더욱 쉽게 설명하면 램프(고수준 모듈)와 전기 콘센트(저수준 모듈)가 있을 때, 램프를 콘센트에 맞게 설계해야 한다. 근데 콘센트가 바뀌면 램프 설계를 다시해야하는데, 이때 어댑터(추상화)를 사용하면 콘센트가 바뀌어도 어댑터만 교체해주면 램프를 다시 설계하지 않아도 된다.

 

예시) 'LightBulb' 클래스와 이를 사용하는 'ElectricPowerSwitch' 클래스가 있을 때,  'ElectricPowerSwitch' 'LightBulb' 에 강하게 의존하고 있다. 

class LightBulb {
    void turnOn() {
        // 전구 켜기
    }

    void turnOff() {
        // 전구 끄기
    }
}

class ElectricPowerSwitch {
    private LightBulb lightBulb;

    ElectricPowerSwitch(LightBulb lightBulb) {
        this.lightBulb = lightBulb;
    }

    void press() {
        // 전구의 상태 변경
    }
}

 

이를 개선하기 위해 스위치가 조작할 수 있는 모든 장치에 대한 인터페이스를 정의하고, 'ElectricPowerSwitch'가 이 인터페이스에 의존하도록 변경한다.

interface Switchable {
    void turnOn();
    void turnOff();
}

class LightBulb implements Switchable {
    public void turnOn() {
        // 전구 켜기
    }

    public void turnOff() {
        // 전구 끄기
    }
}

class ElectricPowerSwitch {
    private Switchable device;

    ElectricPowerSwitch(Switchable device) {
        this.device = device;
    }

    void press() {
        // 장치의 상태 변경
    }
}