[Architect] SOLID 원칙에 대한 정리

in #kr-dev5 years ago (edited)

OOP 프로그래밍을 기반으로 코드를 짤 때,

기본이 되는 원칙인 SOLID에 대해 복습하는 차원에서,
SOLID 원칙을 정리해보고자 합니다.

저의 개인적인 이해를 포함해서 작성하였습니다.

SOLID 원칙의 의의


SOLID 원칙의 의의는 추상성을 어떻게 이해하느냐에 있습니다.

추상성은 객체지향의 특성 중 상속에 기반이 있다고 생각합니다.

java, c# 에서는 interface고 C++ 같은 곳에서는 상속일 것입니다.

디자인 패턴처럼 구현 자체에는 큰 차이가 없지만,
개념적으로는 접근해야 되는 부분이라고 생각합니다.

추상성의 사용을 제대로 이해하고 사용하기 위한 차원에서
SOLID 원칙을 이해해야 한다고 생각합니다.

이제 SOLID의 각 항목에 대해 알아보겠습니다.

Single responsibility principle


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

가장 기본이 되는 원칙이자,

이것만 잘 지켜도 코드 개선에 큰 영향을 준다고 생각합니다.

코드를 구현하다보면 하나의 클래스에서 여러 기능이 섞이게 되는데,

이때는 클래스를 분화해서 기능을 분리해주어야 합니다.

예를들어 Adapter 클래스에서는 Collection Model을 View로 전환시켜주는 일만 수행해야 합니다.

Logic에서 Model을 다운로드 받는 과정등이 포함되면 위 원칙에 위배됩니다.

Open / closed principle


클래스는 수정에는 닫혀있고, 추가에는 열려있어야 한다.

프로그램에 추가기능을 구현하게 되었을 때, 기존 클래스를 수정하지 않고,

추상성을 이용하여, 이를 상속받는 하위 클래스의 추가로 해결해야한다는 의미입니다.

예를들어 도형의 넓이를 계산하는 코드를 구현한다 했을 때,

public class Rectangle {

    private float mWidth;
    private float mHeight;

    public Rectangle(final float Width, final float height) {
        mWidth = Width;
        mHeight = height;
    }

    public float getWidth() {
        return mWidth;
    }

    public float getHeight() {
        return mHeight;
    }
}

public class Circle {

    private float mRadious;

    public Circle(final float radious) {
        mRadious = radious;
    }

    public float getRadious() {
        return mRadious;
    }
    
}

public class AreaCalculation {
    
   public static float calculate(Object... shapes) {
        float sum = 0;
        for (Object o : shapes) {
            if (o instanceof Rectangle) {
                Rectangle rectangle = (Rectangle) o;
                sum += rectangle.getHeight() * rectangle.getWidth();
            } else if (o instanceof Circle) {
                Circle circle = (Circle) o;
                sum += circle.getRadious() * circle.getRadious() * Math.PI;
            }
        }

        return sum;
    }    
}

위와 같은 코드는 Open / Closed 원칙에 어긋납니다.

Rectangle과 Circle 이외에 다른 클래스를 추가하게 되었을 때 AreaCalculation::calculate
함수에 구현을 변경해주어야 하기 때문입니다.

따라서 interface로 추상화 하여 넓이 계산 부분을 추상화합니다.

public interface Shape {
    public float getArea();
}
public class Rectangle implements Shape {

    private float mWidth;
    private float mHeight;

    public Rectangle(final float Width, final float height) {
        mWidth = Width;
        mHeight = height;
    }

    public float getWidth() {
        return mWidth;
    }

    public float getHeight() {
        return mHeight;
    }

    public float getArea() {
        return mWidth * mHeight;
    }
}

public class Circle implements Shape {

    private float mRadious;

    public Circle(final float radious) {
        mRadious = radious;
    }

    public float getRadious() {
        return mRadious;
    }

    public float getArea() {
        return (float) (mRadious * mRadious * Math.PI);
    }
}

public class AreaCalculation {

    public static float calculate(Shape... shapes) {
        float sum = 0;
        for (Shape shape : shapes) {
            sum += shape.getArea();
        }

        return sum;
    }

}

Liskov substitution principle


서브타입은 언제나 자신이 기반타입으로 교체할 수 있어야한다.

부모 클래스 인스턴스 위치에 동작하던 부분에
하위 클래스 인스턴스로 교체하여도
잘 동작하여야 한다는 의미입니다.

예를 들어 도형의 넓이를 구하는 코드에서,
신규 클래스인 Square가 Rectangle을
상속 받는 것은 자연스럽다고 할 수 있습니다.

그리고 부모 클래스가 Recatangle 대신에
Square를 참조하는 형태로 변경해도,
큰 의미적인 변화 없이 프로그램은 제대로 동작할 것입니다.

하지만 만약에 MusicPlayer가 Recatangle을 처리 받았다고 했을 때,
getArea를 호출 할 경우 소리를 재생한다고 한다면,
이는 위 원칙을 위배하게 된것입니다.

Interface segregation principle


interface는 목적별로 분화해서 사용해라.

interface를 구성할 때, 여러 기능을 한번에 담지말라는 의미입니다.

이는 Single responsibility principle를
interface에 적용한다고 할 수 있습니다.

약간 의미적으로 다른 것은 Single responsibility principle은
클래스를 구체적으로 구현할 때에 대한 내용이고,

Interface segregation principle은
추상성이 되는 base interface를 작성할 때에 대한
내용이라고 할 수 있습니다.

예를 들어,

public interface API {
    public Data load();
    
    public void save(Data data);
    
    public void upload(File file);
}

이런 API는 다음과 같이 Context에 따라 더 분화 할 수 있습니다.

public interface DataAPI {
    public Data load();
    
    public void save(Data data);
}
public interface FileAPI {
    public void upload(File file);
}

이 원칙을 적용하지 않고 구현하게 되면, interface를 상속할 때,
하위 클래스에서 의미없이 override해야 되는 부분이 많아집니다.

그리고 부모 타입 여러곳에서 사용되는 곳이 많아지므로,
수정사항이 발생했을 때, 더 많은 하위클래스의 구현을 손보아야 합니다.

Dependency inversion principle


상위 추상성에 의존해야하며, 하위 클래스의 구현에 의존해서는 안된다.

어떤 프로그램 수정이 발생했을 때,
상위 클래스에서 수정이 일어나서는 안된다는 의미입니다.

보통 프레임웍을 이용하는 입장에서 이런 부분을 잘 이해할 수 있습니다.

App 에 대한 기능을 수정하는 상황이 되었을 때,
User가 정의한 클래스를 수정하지,
Framework에서 제공하는 component 클래스를
수정하지 않는 것으로 이해야할 수 있습니다.

따라서 이 추상성을 잘 지키려면,

디자인 패턴적으로는 template 메서드 같은 형태로 구현하라는 의미가 될 수 있습니다.

즉, 세부 변경이 일어나는 구현사항은 추상화해서 callback으로 정의해서
User가 재정의 해서 사용할 수 있게 하고,

나머지 범용적이고 일반적인 기능은
callback을 이용하는 클래스에서 처리하는 것입니다.

Sort:  

Congratulations @sweetcoffein2! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :

You got your First payout
You published more than 10 posts. Your next target is to reach 20 posts.

You can view your badges on your Steem Board and compare to others on the Steem Ranking
If you no longer want to receive notifications, reply to this comment with the word STOP

To support your work, I also upvoted your post!

Vote for @Steemitboard as a witness to get one more award and increased upvotes!

Coin Marketplace

STEEM 0.27
TRX 0.13
JST 0.032
BTC 61562.85
ETH 2891.34
USDT 1.00
SBD 3.43