본문 바로가기
나의 공부

객체지향 설계 원칙 SOLID

by 이숴 2023. 4. 27.
반응형

SOLID 원칙은 유명한 로버트 C. 마틴이라는 분깨서 정리하신 5가지 원칙입니다. 객체 지향을 설계할때 지켜야하는 점들을 말하는데요. 하나씩 알아보겠습니다.

 

SRP 단일 책임 원칙 (SIngle responsibility Principle)

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

 

여기서 말하는 책임은 한가지 이유만으로 동작을 해야한다는 의미입니다. 

 

어떠한 코드를 변경할때 이것을 계기로 여러 코드를 바꿔야 한다면 바꾸게 된 클래스는 여러 책임을 가지고 있다는 것이 되므로, 단일 책임 원칙을 지키고 있지 못한 것이죠.

 

예시를 통해 알아보겠습니다.

// 잘못된 예시
class User {
    public void create() { ... } // DB에 새 사용자를 생성하는 책임
    public void delete() { ... } // DB에서 사용자를 삭제하는 책임
    public void sendEmail() { ... } // 이메일을 보내는 책임
}

// 올바른 예시
class UserCreator {
    public void create() { ... } // DB에 새 사용자를 생성하는 책임
}

class UserDeleter {
    public void delete() { ... } // DB에서 사용자를 삭제하는 책임
}

class EmailSender {
    public void sendEmail() { ... } // 이메일을 보내는 책임
}

위와같은 잘못된 예시에서는 user라는 클래스를 변경되었을때 create, delete 등과 같은 메소드들에도 변경이 발생하게 됩니다. 그럴 경우에는 올바른 예시와 같이 분할하여 최소한의 책임만을 가지게 하는 것이죠.

 

OCP 개방-폐쇄 원칙 (Open/closed principle)

"소프트웨어의 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야한다."

 

기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있도록 설계해야 하는 것을 의미합니다.

이는 소프트웨어 엔티티(클래스, 모듈, 함수 등)가 확장 가능하고 수정 가능하지 않아야 함을 얘기하죠.

 

예시를 통해 알아보겠습니다.

// 잘못된 예시
public class PaymentProcessor {
    public void pay(String paymentMethod) {
        if (paymentMethod.equals("creditcard")) {
            // 신용카드 결제 처리 로직
        } else if (paymentMethod.equals("paypal")) {
            // Paypal 결제 처리 로직
        } else if (paymentMethod.equals("alipay")) {
            // AliPay 결제 처리 로직
        }
    }
}

// 올바른 예시
public interface PaymentMethod {
    void pay();
}

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

public class PaypalPayment implements PaymentMethod {
    public void pay() {
        // Paypal 결제 처리 로직
    }
}

public class AlipayPayment implements PaymentMethod {
    public void pay() {
        // AliPay 결제 처리 로직
    }
}

public class PaymentProcessor {
    public void pay(PaymentMethod paymentMethod) {
        paymentMethod.pay();
    }
}

위의 예시와 같이 바로 일일히 한번에 계산을 하게되면 이외에 로직에도 문제가 발생하게 됩니다. 그래서 구현과 실제 계산을 분리하여 구성하게 되는 것이죠.

 

LSP 리스코프 치환 원칙 (Liskov substitution principle)

"프로그램의 객체는 프로그램의 정확성을 깨트리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다."

위 문장의 의미는 살짝 어려웠는데요. 정리하자면, 서브 타입은 언제나 기반 타입으로 대체할 수 있어야 한다. 이는 하위 클래스가 상위 클래스의 역할을 수행할 수 있어야 함을 의미합니다.

 

자동차라는 인터페이스의 엑셀이라는 기능은 앞으로 간다는 로직의 기능을 가지고 있는데, 뒤로 가게 구현하면 위반한다는 것입니다. 느리더라도 앞으로만 가면 되는거죠.

 

예시를 통해 봅시다.

public interface Shape {
    int getArea();
}

public class Rectangle implements Shape {
    protected int width;
    protected int height;
    public void setWidth(int width) { this.width = width; }
    public void setHeight(int height) { this.height = height; }
    public int getWidth() { return width; }
    public int getHeight() { return height; }
    public int getArea() { return width * height; }
}

public class Square implements Shape {
    protected int length;
    public void setLength(int length) { this.length = length; }
    public int getArea() { return length * length; }
}

위와 같이 일단 같은 인터페이스를, 다른 클래스 공유하지만, 실행되는 기능은 동일하다는 것입니다.

 

ISP 인터페이스 분리 원칙 (Interface segregation principle)

"특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다"

이 것은 인터페이스 하나로 정리해두는 것이 안좋다는 것입니다.

 

여러가의 인터페이스로 분리해야 대체 가능성이 높아지고, 그 기능이 명확해지니까요.

 

예시를 보겠습니다.

// 잘못된 예시
public interface Worker {
    void work();
    void manage();
}

public class Manager implements Worker {
    public void work() {
        // 일하는 로직
    }
    public void manage() {
        // 관리하는 로직
    }
}

// 올바른 예시
public interface Worker {
    void work();
}

public interface Eatable {
    void eat();
}

public class Employee implements Worker {
    public void work() {
        // 일하는 로직
    }
}

public class Chef implements Worker, Eatable {
    public void work() {
        // 요리하는 로직
    }
    public void eat() {
        // 식사하는 로직
    }
}

위와같이 인터페이스를 분리하여 대체 가능성을 높이겠다는 것입니다.

 

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

"프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.” 의존성 주입은 이 원칙 을 따르는 방법 중 하나다."

 

쉽게 말하자면 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 것입니다.

 

예시를 보겠습니다.

// 잘못된 예시
public class Car {
    private GasEngine engine = new GasEngine();
    public void start() {
        engine.on();
    }
    public void stop() {
        engine.off();
    }
}

public class GasEngine {
    public void on() {
        // 가솔린 엔진을 켜는 로직
    }
    public void off() {
        // 가솔린 엔진을 끄는 로직
    }
}

// 올바른 예시
public interface Engine {
    void on();
    void off();
}

public class Car {
    private Engine engine;
    public Car(Engine engine) {
        this.engine = engine;
    }
    public void start() {
        engine.on();
    }
    public void stop() {
        engine.off();
    }
}

public class GasEngine implements Engine {
    public void on() {
        // 가솔린 엔진을 켜는 로직
    }
    public void off() {
        // 가솔린 엔진을 끄는 로직
    }
}

public class ElectricEngine implements Engine {
    public void on() {
        // 전기 엔진을 켜는 로직
    }
    public void off() {
        // 전기 엔진을 끄는 로직
    }
}

보시다시피 클래스에 의존하는 것이 아닌 인터페이스로 의존하여 설계되어 있습니다. 이렇게 해야만 구현체를 유연하게 변경할 수 있기 때문이죠.

 

실제 현업에서는 이렇게 모든 원칙을 맞춰서 개발하기에는 힘들겠지만, 이 원칙을 맞춰서 개발하는 개발자가 된다면, 정말 성장을 많이 할 수 있는 개발자가 될 수 있을 것 같습니다.

반응형

댓글