만족은 하되 안주하지는 말자

기록해야 기억한다

프로그래밍/backend&devOps

[OOP] 객체지향 설계 5원칙 : SOLID 디자인 원칙

D36choi 2021. 5. 25. 22:58
728x90

 

로버트 C. 마틴의 '클린소프트웨어: 애자일 원칙과 패턴 그리고 실천 방법' 에서 소개됨.

SRP 단일 책임 원칙

한 클래스는 변경에 대한 이유를 하나만 가져야 한다.

여기서 말하는 책임변경을 위한 이유

즉 클래스를 변경할 때 클래스를 변경하기 위한 다른 이유를 생각할 수 있다면 이 클래스는 SRP 위반

다른 해석으로는 책임이 변경의 축이라는 것.

예시로 클래스를 토론방이라고 하자면, 한 방에서 축구,스키,하이킹,야구 등을 이야기하고 있는데 갑자기 누군가가

영화에 대해 이야기 하면서 주제가 '사람들이 좋아하는 영화, 영화 캐릭터, 명장면에 대한 토론' 을 시작한다면

이 토론방은 2개로 나누는 것이 낫다. (취미 / 영화)

즉 여기서 취미 가 변경의 축이다.

변경의 축은 변경이 일어날 때만 변경의 축이다. - 로버트 C. 마틴

즉 아직 있지 않은 변경의 축이 있다면 미리 고민하지 않고 설계 해야 불필요한 복잡성이 줄게 된다.

OCP 개방 폐쇄 원칙

소프트 웨어 아티팩트는 확장에 열려 있고 수정에는 닫혀 있어야 한다.

버트런드 마이어가 1988년 제안한 개념. 변화를 계속 겪어도 안정적인 소프트웨어 설계를 위한 방법론

예제 (ES6)

class Circle extends Shape {
    constructor(radius, point) {
        this.type = 'circle';
        // ...
    }
}
class Square extends Shape {
    constructor(radius, point) {
        this.type = 'square';
        // ...
    }
}
function drawCircle(circle) {
    // draw circle
}
function drawSquare(circle) {
    // draw square
}

function drawAllShapes(shapes) {
    shapes.forEach((shape)=> {
        if (shape.type === 'circle') {
            drawCircle(shape);
        } else if (shape.type === 'square') {
            drawSquare(shape);
        }
    });
}

Shape 를 확장한 원과 네모. 그리고 원과 네모를 그리는 두 함수가 존재.

drawAllShapes 함수는 모양의 유형을 확인 후 그림을 그린다.

이는 OCP 를 위반한 예제이다. 그 이유가 뭔지 생각해보자.

수정 없이는 drawAllShapes() 함수를 확장할 수 없다.

만약 새로운 모양의 도형 클래스가 추가된다면? 그림을 그리기 위해서

drawAllShapes 함수 또한 수정이 불가피 하다.

즉, 확장을 위해서는 수정이 동반되는 것이다.

리팩터링하면

class Circle {
    //...
    draw() {
        //draw circle
    }
}
class Square {
    //...
    draw() {
        //draw square
    }
}
function drawAllShapes(shapes) {
    shapes.forEach((shape) => {
        shape.draw();
    });
}

draw 함수를 각 유형의 모양으로 이동하고 draw 를 drawAllShapes 에서 호출하는 방식으로 바꾸면,

원과 네모 외에 다양한 기타 유형의 도형들이 추가되더라도 해당 함수는 수정되지 않는다.

즉, 확장은 가능해지고 수정은 발생하지 않는다.

하지만 실제로 닫혀있는 것은 아니라고 한다. 왜? 다른 요구사항에는 닫혀있지 않을 수 있기 때문이다.

예를 들어 그리는 순서가 도형에 따라 정해져 있다고 한다면 역시나 drawAllShapes는 도형이 추가될때마다 그리는 도형순서마다 변경이 불가피 하다.

하지만 그 요구사항은 현재 없다.

따라서 당장의 리팩터링은 시간 낭비이다.

LSP 리스코프 치환 원칙

1988냔 바버라 리스코프에 의해 창안되었다. 이 사람의 설명은 너무 나에겐 어렵기에 로버트 C. 마틴이 나중에 재설명한거로 소개한다.

서브 타입은 그것의 기본 타입으로 치환 가능해야 한다.

위반 사례 (정사각형/직사각형 문제)

rectangle 는 직사각형,square는 정사각형이다.

 

이 관계의 문제는 뭘까?

class Rectangle {
    private int width;
    private int height;

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

    public int getHeight() {
        return this.height;
    }

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

    public int getWidth() {
        return this.width;
    }

    public int calculateArea() {
        return this.width * this.height;
    }
}
class Square extends Rectangle {

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

    public void setWidth(int width) {
        this.width = width;
        this.height = width;
    }
}
public class App {
    public void resize(Rectangle rectangle) {
        rectangle.setWidth(10);
        rectnagle.setHeight(5);
        int area = rectangle.calculateArea();
        assert area == 50;
    }
}

만약 App 에 주입되는 객체가 square면 거짓이 된다.

정사각형은 결국 25 넓이가 되고, 직사각형은 50이 된다.

개발자가 의도한 바와 다른 결과가 나올 수 있다.

서브타입인 정사각형은 resize 메서드 에서 Rectangle(기본타입)을 대체할 수가 없게 된다.

하지만 App 클래스 관점에서 볼 때 Rectangle 과 모든 하위 타입은 동일하게 동작해야만 할 것이다.

상속의 관계를 제거하는 것이 이 경우 옳을 수 있겠다.

ISP 인터페이스 분리 원칙

자신이 사용하지 않는 메소드에 의존하도록 강제되어선 안된다.

좀 어려운 것 같다.

https://www.slideshare.net/neetuangelmishra/ppt-12735983

예시

클래스 종류가 아래와 같다

  • 관리자(매니저)
  • 평범한 개발자, 열일하는 개발자 - work, eat 을 근무중에 한다.
  • 로봇 - 점심시간을 가질 필요 없는 노동자인 로봇이 추가되어야 한다.

나쁜 예시

    static public interface IWorker {
        public void work();
        public void eat();
    }
    static class Worker implements IWorker {

        @Override
        public void work() {
            //work...
        }

        @Override
        public void eat() {
            //eat...
        }
    }

이렇게 되면 로봇은 인터페이스를 상속하기 애매해진다.

로봇은 먹지 않을텐데, IWorker 인터페이스에는 먹기 메서드가 불필요하게 상속되어야하기 때문이다.

 

좋은 예시

    static public interface IWorkable {
        public void work();

    }
    static public interface IEatable {
        public void eat();

    }
    static class NormalWorker implements IWorkable,IEatable {

        @Override
        public void work() {
            //working...
        }

        @Override
        public void eat() {
            //eating..
        }
    }
    static class HardWorker implements IWorkable,IEatable {

        @Override
        public void work() {
            //working much more
        }

        @Override
        public void eat() {
            //eating
        }
    }
    static class Robot implements IWorkable {
        @Override
        public void work() {
            // only working
        }
    }

인터페이스를 먹기와 일하기로 분리한다.

로봇은 불필요한 메서드까지 상속하지 않아도 되게 바뀌었다.

 

DIP 의존 관계 역전 원칙

https://softwareengineering.stackexchange.com/questions/347138/clarification-on-the-dependency-inversion-principle

이 원칙은 2가지에 초점을 둔다.

  • 상위 모듈은 하위 모듈에 의존해서는 안 된다. 둘 다 모두 추상화에 의존해야 한다.
  • 추상화는 구체적인 내용에 의존해선 안 된다. 구체적인 사항이 추상화에 의존해야 한다.

여기서 상위 수준 모듈은 비즈니스 로직을 포함하고, 하위 수준 모듈은 구현체의 세부 내용을 포함한다.

우리가 자주 쓰는 List<Integer> numbers = new ArrayList... 도 예시가 될 수 있겠다.

ArrayList<Integer> numbers = ... 로 쓰기보단 위처럼 쓰기를 권장하는 것이다.

예시

로버트 아저씨의 책에는 이런 예시가 있다고 한다.

버튼을 이용해 램프를 켜거나 끈다! 버튼은 램프에 의존하고 있다.

버튼 객체는 켜야하는 기계인 램프를 주입받아 램프를 poll 을 통해 켜거나 끈다.

근데 만약 램프 대신 다른 기계를 켜거나 꺼야한다면 Poll 메서드는 변화가 불가피해진다!

즉, Button 클래스가 구체적인 클래스인 Lamp 에 의존하게 되는 것이다. 이런 의존성의 변경사항에 의해 Button 은 불안전한 객체가 된다. 만약 Button 을 통해 Lamp 대신 Radio 를 켜거나 끄게 하고싶을때도 있을 텐데 말이다.

해결법

중간에 인터페이스를 두어 구체적인 모듈들 간의 의존성을 분리한다. (표현이 맞는지는 모르겠다)

인터페이스를 통해 상위 수준 모듈이 추상화에 의존하게 바꾼다.

static void Main(string[] args)
{
    ISwitchableDevice switchableDevice = new Lamp();
    var button = new Button(switchableDevice);
    // TODO - wiring for other classes which use button and switchableDevice
}

위처럼 인터페이스를 두게 되면 라디오든, 램프든 버튼에 추상화 객체를 주입해 버튼은 상위 수준의 추상화만 고려하면 된다.

5가지 디자인 원칙인 SOLID 를 알아봤다. 이 원칙들은 자바등의 객체지향 언어를 이용한 설계를 할 때 꼭 기억하고 명심해야 할 5계명 같은거라고 생각한다. 앞으로도 자주 훑어볼 수 있도록 해야 겠다.