객체지향설계의 5원칙 SOLID

kmb·2023년 5월 24일
0

스프링

목록 보기
9/9
post-thumbnail

2000년대 초반에 로버트 C. 마틴이 객체지향언어를 이용해서 객체지향프로그램을 올바르게 설계하기 위해 5가지의 기본원칙으로 제시한 것을 마이클 페더스가 두문자어로 소개한것이 SOLID 원칙.

이는 응집도를 높이고, 결합도를 낮추는 고전 원칙을 객체지향의 관점에서 재정립한 것.


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

어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다. - 로버트 C. 마틴

하나의 클래스는 하나의 책임만 가져야 한다. 이는 애플리케이션 모듈 전반에서 높은 유지 보수성 및 캡슐화를 유지할 수 있다.

 

  • 나쁜 예시
public class Person {

    public static void cook() {

        System.out.println("음식을 만듭니다.");
    }

    public static void shoot() {

        System.out.println("총을 쏩니다.");
    }

    public static void drive() {

        System.out.println("운전을 합니다");
    }
}

Person이라는 클래스에 3가지의 책임이 주어져 있음.

 

  • 개선된 예시
public class Person {

    public static void cook() {

        System.out.println("음식을 만듭니다.");
    }
}

public class Driver {

    public static void drive() {

        System.out.println("운전을 합니다");
    }
}

public class Soldier {

    public static void shoot() {

        System.out.println("총을 쏩니다.");
    }
}

하나의 클래스마다 하나의 책임을 부여함으로써 SRP를 준수.


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

소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서 열려있어야 하지만 변경에 대해서는 닫혀 있어야 한다. - 로버트 C. 마틴

다른 개발자가 작업을 수행하기 위해 반드시 수정해야 하는 제약 사항을 클래스에 포함해서는 안되고 다른 개발자가 클래스를 확장해서 원하는 작업을 할 수 있도록 해야 한다.

즉 자신의 확장에는 열려있고, 주변의 변화에 대해서는 닫혀 있어야 한다.

 

  • 나쁜 예시
public class ShinhanCard {

    public void swipeCard() {

        System.out.println("신한카드로 카드를 긁습니다.");
    }
}

public class Person {

    private ShinhanCard card;

    public Person(ShinhanCard card) {

        this.card = card;
    }

    public void pay() {
        System.out.println("결제를 시도합니다.");
        card.swipeCard();
        System.out.println("결제가 완료되었습니다.");
    }
}

Person 클래스는 ShinhanCard 클래스를 직접 필드로 받음으로써 이후에 기능을 추가하기 위해 다른 카드 클래스를 추가할 때마다 Person 클래스는 필드로 다른 카드 클래스를 직접 받아야 하므로 수정에 관해 닫혀있지 않다.

 

  • 개선된 예시
public interface Card {

    void swipeCard();
}

public class NongHyupCard implements Card {

    @Override
    public void swipeCard() {

        System.out.println("농협카드로 카드를 긁는다");
    }
}

public class WooriCard implements Card {

    @Override
    public void swipeCard() {
        System.out.println("우리카드로 카드를 긁는다");
    }
}

public class Person {

    private Card card;

    public Person(Card card) {

        this.card = card;
    }

    public void pay() {
        System.out.println("결제를 시도합니다.");
        card.swipeCard();
        System.out.println("결제가 완료되었습니다.");
    }
}

public class Main {

    public static void main(String[] args) {

        Card nongHyupCard = new NongHyupCard();
        Person person1 = new Person(nongHyupCard);
        person1.pay();  // 결재를 시도합니다.  농협카드로 카드를 긁는다.  결제가 완료되었습니다.

        Card wooriCard = new WooriCard();
        Person person2 = new Person(wooriCard);
        person2.pay();  // 결재를 시도합니다.  우리카드로 카드를 긁는다.  결제가 완료되었습니다.
    }
}

Card라는 인터페이스를 활용하여 각 카드 클래스마다 메서드를 Overriding하여 구현. 즉 확장에 열려있다.

또한 새로운 카드 클래스가 추가되더라도 Person 클래스는 필드로 Card 인터페이스를 설정되었기 때문에 수정에 관하여 닫혀있다.


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

서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다. - 로버트 C. 마틴

즉 서브클래스의 객체가 슈퍼클래스의 객체와 반드시 같은 방식으로 동작해야 한다.

따라서 모든 파생된 클래스 혹은 서브클래스는 아무 문제 없이 그들의 슈퍼클래스를 대체할 수 있어야 한다.

애초에 2개의 클래스가 문제가 있는 상속 관계라면 인터페이스를 통해 각각 별개의 동작을 정의해준다.

 

  • 나쁜 예시
public class Rectangle {

    int width;
    int height;

    public int getArea() {

        return width * height;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }
}

public class Square extends Rectangle{

    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    @Override
    public void setHeight(int height) {
        super.setHeight(height);
        super.setWidth(height);
    }
}

public class Main {

    public static void main(String[] args) {

        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(10);
        rectangle.setHeight(5);

        Rectangle square = new Square();
        square.setWidth(10);
        square.setHeight(5);

        System.out.println("rectangle area = " + rectangle.getArea());  // rectangle area = 50
        System.out.println("square area = " + square.getArea());        // square area = 25

    }
}

서브클래스인 Square클래스는 슈퍼클래스인 Rectangle클래스를 대체할 수 없다. 정사각형 클래스가 직사각형 클래스를 상속 받는다는 것 자체가 문제가 있다. 따라서 상속이 아닌 인터페이스를 활용하여 별개의 타입으로 구현해야 한다.

 

  • 개선된 예시
public interface Shape {

    int getArea();
}

public class Rectangle implements Shape {

    int width;
    int height;

    @Override
    public int getArea() {

        return width * height;
    }

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }
}

public class Square implements Shape {

    int length;

    @Override
    public int getArea() {

        return length * length;
    }

    public Square(int length) {
        this.length = length;
    }
}

public class Main {

    public static void main(String[] args) {

        Shape rectangle = new Rectangle(10, 20);
        Shape square = new Square(10);

        System.out.println("rectangle area = " + rectangle.getArea());  // rectangle area = 50
        System.out.println("square area = " + square.getArea());        // square area = 25
    }
}

Shape라는 인터페이스를 활용하여 Rectangle 클래스와 Square 클래스에게 면적을 구하는 공통의 메서드를 구현하도록 강제하여 별개의 타입으로 구현.


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

클라이언트는 자신이 사용하지 않는 메서드에 의존 관게를 맺으면 안 된다. - 로버트 C. 마틴

따라서 사용하지 않는 메서드를 강제로 구현하지 않도록 1개의 인터페이스를 2개 이상의 인터페이스로 분할한다.

 

  • 나쁜 예시
public interface DataManager {

    void load();  // 데이터 불러오기

    void save();  // 데이터 저장하기

    void delete();  // 데이터 삭제하기
}

public class DataService implements DataManager{

    @Override
    public void load() {  // 데이터 불러오기

    }
    @Override
    public void save() {  // 데이터 저장하기

    }
    @Override
    public void delete() {  // 데이터 삭제하기

    }
}

DataService는 void load( ), void save( ) 만을 구현해야 하는 서비스라면 delete( ) 메서드는 필요가 없지만 강제로 구현해야 하는 문제가 발생한다.

 

  • 개선된 예시
public interface DataDeleter {

    void delete();  // 데이터 삭제하기
}

public interface DataLoader {

    void load();  // 데이터 불러오기
}

public interface DataSaver {

    void save();  // 데이터 저장하기
}

1개의 인터페이스를 3개로 분할하여 역할을 분리.


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

추상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다. - 로버트 C. 마틴

즉 모든 구상 모듈은 추상적인 내용만 노출해야 한다.

따라서 구현클래스에 의존하지 말고 인터페이스에만 의존하라는 뜻.

 

  • 나쁜 예시
public class AlramA {

    public String beep() {
        return "aaaaaaaa!";
    }
}

public class AlramB {

    public String beep() {
        return "bbbbbbb!";
    }
}

public class AlramService {

    private AlramA alramA;
    private AlramB alramB;

    public String beep(String company) {
        if(company.equals("A")) {
            return alramA.beep();
        } else {
            return alramB.beep();
        }
    }
}

AlramService 클래스는 AlramA, AlramB 클래스를 직접 필드로 받음으로써
계층이 바뀌거나 추가될 때마다 AlramService 클래스의 코드를 수정해야 하므로 구체화에 의존하게 되는 문제가 발생.

 

  • 개선된 예시
public interface Alram {

    String beep();
}

public class AlramA implements Alram{

    @Override
    public String beep() {

        return "aaaaaaaa!";
    }
}

public class AlramB implements Alram{

    @Override
    public String beep() {

        return "bbbbbbb!";
    }
}

public class AlramService {

    private Alram alram;

    public AlramService(Alram alram) {
        this.alram = alram;
    }

    public String beep() {
        return alram.beep();
    }
}

추상 타입인 인터페이스나 추상 클래스를 사용하여 추상화에 의존하도록 적용.

 

출처

  • (책) 스프링 입문을 위한 자바 객체 지향의 원리와 이해
  • (책) 자바 코딩 인터뷰 완벽 가이드
profile
꾸준하게

0개의 댓글