Java

[객체지향] 2. 객체지향 설계 5원칙 (SOLID)

jwKim96 2021. 7. 25. 20:55

0. 압도적 감사...!

SOLID 원칙 이라고도 불리는, 객체지향 설계 5원칙은 선배 개발자분들의 시행착오와 고민들로 만들어진 객체지향 설계 가이드라인 입니다. 후배 개발자들이 자신들과 같은 고민을 하여 시간을 낭비하지 않도록 열심히 고민해주신 선배 개발자 분들에게 감사하며, 객체지향 설계 5원칙에 대해 알아봅시다.


1. S - 단일책임원칙

SRP (Single Responsibility Principle)

모든 클래스하나의 책임만 가져야 하고, 클래스는 그 책임을 완전히 캡슐화 해야한다는 원칙입니다.

자동차 클래스 (다중 책임)

class 자동차 {
    public void 시동을건다(type) {
        if (type=="저가형") {
            this.is엔진동작 = true;
        } else if (type="고급형") {
            this.is엔진동작 = true;
            this.네비게이션켜기();
            this.드라이브모드_변경('기본_드라이빙');
        }
    }
}

위의 다중 책임 예제에서는 시동을건다() 라는 메소드저가형 자동차, 고급형 자동차 등 복수의 책임이 부여되어 있습니다. 만약 전기 자동차, 수소전기 자동차 등 종류가 늘어난다면, 계속해서 else if 를 추가해야하고 이 때문에 하나의 클래스에 여러 책임이 부여됩니다. 만약 시동을 건다() 메소드에 오류가 발생한다면, 모든 종류의 자동차의 시동을건다() 메소드는 동작하지 않게 되는 문제가 발생하겠죠.

자동차 클래스들 (단일 책임)

class 저가형자동차 {
    public void 시동을건다() {
        this.is엔진동작 = true;
    }
}

class 고급형자동차 {
    public void 시동을건다(type) {
        this.is엔진동작 = true;
        this.네비게이션켜기();
        this.드라이브모드_변경('기본_드라이빙');
    }
}

단일 책임으로 분리한 예제를 보면, 책임별로 클래스를 나누어서 각 클래스는 하나의 책임만 가집니다. 그러면 전기 자동차, 수소전기 자동차가 추가되더라도 기존 클래스들을 수정하지 않아도 됩니다. 그리고 저가형 자동차가 문제가 생기더라도 다른 자동차들은 영향을 받지 않죠.

이렇게 각 클래스에 하나의 책임만 부여함으로서, 변경과 확장에 유연한 프로그램 구조를 만들 수 있습니다.

2. O - 개방 폐쇄 원칙

OCP : (Open Closed Principle)

자신의 확장에는 열려있고, 주변의 변화에는 닫혀있어야 한다는 원칙입니다.

최근 전기 자동차가 많이 상용화 되었습니다. 그러면서 자동차의 구조에도 많은 변화가 생겼는데요. 그 중 주된 변화의 한가지로 엔진 vs 모터를 생각해볼 수 있습니다. 여기서 운전자(사용자)가 자동차를 사용하는 방식에 중점을 두고 생각해 봅시다. 자동차를 움직이는 기술의 변화 즉, 주변의 변화는 사용자가 자동차를 사용하는데 영향을 끼쳐서는 안됩니다.

  • 엑셀 = 속도 증가
  • 브레이크 = 속도 감소

위와 같은 자동차의 특성이 주변의 변화에 영향을 받아선 안되죠. 이것이 주변의 변화에 닫혀있는 상태인 것입니다.

이 처럼 운전자는 자동차를 사용할 때, 내가 엑셀을 밟으면 엔진 RPM이 올라가며 바퀴에 연결된 축이 빠르게 돌아가서 속도가 빨라지는지, 아니면 모터가 더 빠른속도로 돌아서 빨라지는지 몰라도 됩니다. 그저 엑셀 = 속도 증가, 브레이크 = 속도 감소 만 알고 자동차를 사용할 수 있어야 하죠.

그리고 위와 같은 특성을 유지한 채로, 엔진으로 움직이는 자동차가 아닌 모터로 움직이는 자동차가 추가되는, 즉 자신의 확장에는 열려있죠.

마찬가지로, 상위 클래스 혹은 인터페이스하위 클래스에 상속시킴으로서 기본적으로 갖춰야 할 속성메소드 형식을 유지시킬 수 있습니다. 그리고 그 내부에서 어떤 작업이 실행되는지 몰라도 사용자가 기대한 기능이 수행되도록 합니다. 이는 객체지향의 캡슐화상속이라는 특성을 잘 이용한 원칙입니다.

3. L - 리스코프 치환 원칙

LSP : (Liskov Substitution Principle)

하위 클래스는 언제나 자신의 상위 클래스교체할 수 있어야 한다.

다시 자동차로 예를들어볼까요? 😄

옛날에는 은 주 이동수단이었죠. 그러다 자동차가 개발되어 우리는 휘발유 자동차도 타고 전기 자동차도 탑니다. 이 처럼 말은 자동차라는 이동수단의 조상님 격이 될 수 있습니다.
이제 그림을 살펴보면, 왼쪽 그림처럼 휘발유 자동차전기 자동차자동차로 치환할 수 있습니다.
하지만 오른쪽 그림처럼 자동차로 치환할 수 없죠.
이 경우, 리스코프 치환 원칙에 위배된다고 할 수 있습니다.

이 처럼, 하위 클래스상위 클래스로 치환된 수 없는 관계(상속 등)를 맺고 있다면, 이는 잘못된 관계일 확률이 매우 크겠죠?

4. I - 인터페이스 분리 원칙

ISP : (Interface Segregation Principle)

인터페이스 분리 원칙이란, 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다는 원칙입니다.

위 그림에서는 자동차를 상속받아 저가형 자동차, 고급형 자동차라는 클래스를 정의하였습니다.
저가형 자동차에서는 드라이브모드_변경() 이라는 메소드는 사용되지 않지만, 상속을 하였기 때문에 어쩔 수 없이 구현이 된 상태입니다. 이를 인터페이스 분리 원칙에 맞게 변경하면 다음과 같습니다.

이렇게 되면, 저가형 자동차, 고급형 자동차 모두 자신이 필요한 인터페이스만 구현하게 되었습니다!

...

뭔가 이상한 점을 발견하셨나요? 🤔

이대로 설계를 진행한다면, 1번의 단일책임원칙에 위배됩니다. 저가형 자동차는 {시동, 속도} 2가지 책임, 고급형 자동차는 {시동, 속도, 드라이브모드} 3가지 책임을 갖게 됩니다. 그래서 인터페이스 분리 원칙에서는 복수의 책임을 갖는것을 허용하게 되어 단일책임원칙과 함께 적용될 수 없습니다.

그래서 프로젝트의 상황에 따라, 단일책임원칙과, 인터페이스 분리 원칙을 선택하여 설계해야 합니다. 이 원칙에 따라 설계한다면, 불필요한 인터페이스가 줄어들어 효율적으로 인터페이스를 사용할 수 있습니다.

5. D - 의존의 역전 원칙

DIP : (Dependency Inversion principle)

의존의 역전 원칙이란, 의존 관계를 맺을 때 자신보다 변하기 쉬운 것에 의존하지 말아야 한다는 원칙입니다.

휘발유 자동차아반테를 상속하여 만든다고 가정해 봅시다.
아반테는 매년 새로운 모델을 출시하고, 그러면 매년 휘발유 자동차상위 클래스아반테를 추가/변경 해주어야 합니다. 이러한 구조가 의존의 역전 원칙을 위배한 예시라 할 수 있습니다.

6. 마무리하며..

객체지향 설계 5원칙의 핵심은 좋은 객체지향 설계를 위해서는 결합도를 낮추고 응집도를 높혀야 한다 인것 같습니다.

  • 결합도
    객체간의 상호 의존 정도를 나타내는 지표
    결합도가 낮다는 것은, 객체간의 상호 의존성이 낮다는 의미이고 객체 의 재사용성이 증가하고, 유지보수가 용이함
  • 응집도
    하나의 객체 내부에 존재하는 속성메소드의 기능적 관련성
    응집도가 높은 객체는 하나의 책임(역할)에 집중되고 독립성이 높아져서 재사용성이 증가하고, 유지보수가 용이함

하지만

예외 앞에 장사 없다(?)

우리가 항상 명심할 것은, 아무리 좋은 방법론이라 하더라도 프로젝트 상황에 따라 예외적인 상황이 있을 수 있다는 것이고, 이러한 방법들은 최고의 방법이 아니라 최선의 방법 이라는 것을 생각해야합니다.

모든것을 의심하고 검증하며 이게 과연 최선인가? 를 항상 고민하는 개발자가 됩시다!

(저도 포함ㅎ)