객체지향 설계 원칙

Seongcheol Jeon·2024년 11월 14일
0

CPP

목록 보기
14/47
post-thumbnail

SOLID 원칙이란?

SOLID 원칙은 로버트 C. 마틴이 2000년대 초반에 발표한 객체지향 설계의 다섯 가지 원칙을 마이클 C. 페더스가 부르기 쉽게 머리글자로 소개한 것이다.

SOLID는 소프트웨어의 유지/보수와 확장성에 도움이 되는 다섯 가지 기본 원칙이다.

  • Single Responsibility Principle (SRP) : 단일 책임 원칙
  • Open-Closed Principle (OCP) : 개방/폐쇄 원칙
  • Liskov Subsitution Principle (LSP) : 리스코프 치환 원칙
  • Interface Segregation Principle (ISP) : 인터페이스 분리 원칙
  • Dependency Inversion Principle (DIP) : 의존성 역전 원칙

단일 책임 원칙 (SRP)

단일 책임 원칙(single responsibility principle)클래스는 한 가지 기능만 수행해야 하고, 한 가지 이유로만 변경해야 한다 는 원칙이다.

산탄총 수술 (shotgun surgery)

클래스를 설계할 때 역할을 복잡하지 않게 해야 한다는 의미이다. 주목할 점은 뒤쪽에 있는 변경에 대한 언급으로, 클래스는 한 가지 목적으로만 수정돼야 한다는 뜻이다.
즉, 어떤 클래스가 A라는 기능을 수정할 때도 변경되고, B라는 기능을 수정할 때도 변경되는 현상을 지양해야 한다고 말하는 것이다.

현대 소프트웨어 개발에서는 구조가 복잡해지고 다양한 인력이 개발에 참여하면서 유지/보수성이 꽤 중요해졌다. 한 가지 기능을 수정할 때 클래스를 여러 개 수정해야 한다면 유지/보수성은 떨어지기 마련이다. 이러한 현상을 가리켜 산탄총 수술 (shotgun surgery)이라고 한다. 산탄총처럼 탄흔을 사방에 남긴다는 의미이다.

만약 수정된 내용이 다른 클래스에 영향을 준다면 한 클래스만 수정하더라도 문제가 될 수 있다. 구현부가 변경되는 것은 상관없지만 함수의 시그니처가 변경되거나 함수 자체가 변경된다면 다른 클래스에도 수정이 필요하다.

기능을 수정할 때 여러 클래스가 변경되지 않아야 하는 것은 물론이고, 변경된 클래스가 다른 클래스에 영향을 주지 않아야 한다. 변경 사항이 한 클래스에 국한되는 것과 변경된 클래스가 다른 클래스에 영향을 주지 않는 것, 이 두 가지가 단일 책임 원칙의 핵심이다. 변경 사항이 한 클래스에 갇혀야 한다는 점은 인터페이스 분리 원칙 (ISP)과도 연결되는 개념이다.

클래스 추출

단일 책임 원칙을 적용하는 구체적인 방법을 설계 측면과 리팩터링 측면에서 살펴보자.

리팩터링 (Refactoring) 이란?
프로그램의 실행 결과는 유지한 채 유지/보수가 쉽도록 코드를 정리하는 것을 의미한다.

먼저 설계 측면에서는 상속 관계보다는 컴포지션이나 어그리게이션을 적극 활용하는 방법이 있다. 상속도 객체지향 언어의 중요한 특징이지만, 다중 상속이 너무 많아지거나 상속이 쌓이면 클래스가 커진다. 다중 상속을 받았다는 것은 자식 클래스의 역할이 하나가 아니라 여러 가지라는 의미이다.

프로그램을 제작하다 보면 클래스가 여러 가지 기능을 가질 수밖에 없다. 다중 상속이 필요한 상황에서 컴포지션과 어그리게이션은 단일 책임 원칙을 지켜 수정 범위를 한 클래스에 갇히게 하는 좋은 방법이다.

리팩토링 측면에서는 클래스를 추출해 거대 클래스 (large class)를 작은 단위로 나눈다. 거대 클래스를 작은 단위로 나누더라도 논리적인 관계는 유지돼야 한다. 기존 거대 클래스는 여러 가지 기능을 묶는 역할만 하고, 단일 책임으로 추출한 하위 클래스는 컴퍼지션이나 어그리게이션을 이용해 has-a 관계로 구현한다.

거대 클래스의 상속 관계를 잘 정리하거나 컴포지션, 어그리게이션을 적용하는 작업에는 정답이 없다. 많은 시행착오와 경험을 쌓을 수밖에 없다.

About...🤔 다중 상속

다중 상속을 남발하면, 클래스가 커져서 산탄총 수술 현상이 발생할 수 있다. 하지만 상속 관계가 복잡하지 않거나 상속 세대가 깊지 않을 때는 다중 상속으로 간단하게 구현할 수도 있다. 다중 상속은 필요할 때 한정해서 사용하는 것이 좋다.


개방/폐쇄 원칙 (OCP)

개발/폐쇄 원칙 (open-closed principle)은 이름만 보면 모순처럼 느껴지지만 개방과 폐쇄를 적용해야 할 대상이 다르다. 모든 SOLID 원칙과 마찬가지로 유지/보수성을 향상하기 위한 방법이다.

확장에 열려 있고, 수정에 닫혀 있다.

개방/폐쇄 원칙을 풀어서 설명하면, 확장에는 열려(개방)있고, 수정에는 닫혀(폐쇄) 있어야 한다 라고 말할 수 있다.
동적 바인딩을 이용하면 프로그램은 새로운 기능을 추가할 수 있는 방법(확장에 개방)이 생기며, 다른 코드에 파급 효과가 없어 추가되는 기능 외에는 수정이 필요 없다(수정에 폐쇄).

주변에서 비슷한 예를 살펴보자. 도시에 있는 커피 전문점에서 "따뜻한 아메리카노 주세요"라고 주문하면 커피 제조를 담당하는 직원은 주문을 받아 커피를 제조한다. 그날 추천하는 브랜딩 원두가 에스프레소 머신에서 잘게 갈리고 커피 잔에 따뜻한 물과 함께 추출되어 커피가 완성된다. 그리고 주문자를 부르면 커피를 가지러 간다.

만약 이 커피 전문점에서 연말을 맞이하여 시즌 메뉴를 추가하더라도(확장에 열림) 주문 방법은 바뀌지 않는다(수정에 닫힘). 커피 제조 직원이 시즌 메뉴를 만드는 제조 방법을 새로 배우기만 하면 된다.

추상 클래스 활용

개방/폐쇄 원칙은 추상 클래스(인터페이스)를 통해서 구현할 수 있다. 프로그램에서 주요 기능의 흐름은 추상 클래스를 활용해 작성하고 이를 상속받아 구현하는 클래스에 따라서 세부 동작이 결정되게 한다. 즉, 흐름의 뼈대는 켐플릿으로 만들고 살을 붙이는 작업은 자식 클래스에 위임하는 것으로, 이러한 방식으로 설계하는 패턴을 가리켜 템플릿 메서드 패턴 (template method pattern) 이라고 한다.

추상 클래스에 주요 기능을 모두 가상 함수로 선언하고 이를 상속받는 자식(구현) 클래스에서 가상 함수를 각각 구현한다. 그리고 템플릿 함수에서 자식 클래스의 객체에 오버라이딩된 가상 함수를 호출해 논리의 흐름을 완성한다. 만약 확장이 필요하면 자식 클래스를 추가하면 된다.


리스코프 치환 원칙 (LSP)

리스코프 치환 원칙 (Liskov substitution principle)하위 클래스는 상위 클래스를 대체할 수 있어야 한다 는 의미이다. 이 춴칙은 바바라 리스코프(Barbara Liskov)가 OOPSLA 87 기조 연설로 발표한 "Data abstraction and hierarchy"에서 소개한 개념으로, 다형성의 동작 원리를 설명한다.

자식 클래스가 부모 클래스를 치환한다는 것은 부모 클래스의 역할을 자식 클래스가 수행할 수 있다는 이야기이다.

상속에서 자식 클래스가 부모 클래스를 완전히 대체할 수 있는 관계를 is-a 관계라고 한다. is-a 관계로 정의된 클래스는 리스코프 치환 원칙에 따르는 클래스이며, 이는 다음의 두 가지를 의미한다.

  1. 부모 클래스를 상속받아 구현한 자식 클래스는 부모 클래스로 업캐스팅(upcastring)이 가능하다.
  2. 자식 클래스에서 부모 클래스의 멤버 함수를 상속받아 오버라이딩(overriding)하거나 유지해야 한다.

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

인터페이스 분리 원칙 (interface segregation principle)은 단일 책임 원칙을 인터페이스에 적용한 것으로 생각하면 이해가 쉽다. 단일 책임 원칙은 클래스는 한 가지 기능만 수행해야 하고, 한 가지 이유로만 변경해야 한다는 것이다.

인터페이스를 상속받은 클래스에서는 인터페이스를 구현해야 한다. 그런데 클래스에서 여러 인터페이스를 구현하다 보면 단일 책임 원칙에 위배된다. 단일 책임 원칙을 지키려면 인터페이스가 작고 섬세(fine grained)해야 하며, 클래스는 역할에 특화된 최소한의 인터페이스를 구현해야 한다.

결국 인터페이스 분리 원칙은 인터페이스는 작고 섬세해야 하며, 클래스는 필요한 인터페이스만 구현해야 한다라고 정리할 수 있다.


의존성 역전 원칙 (DIP)

의존성 역전 원칙 (dependency inversion principle)상위 수준 모듈은 하위 수준의 모듈에 의존해서는 안 되며, 상위/하위 수준 모두 추상레이어(인터페이스)에 의존해야 한다로 정리할 수 있다.

의존성 역전 원칙은 개방/폐쇄 원칙을 적용하기 위해 사용된다.

의존성 역전 원칙이 적용되지 않은 구조와 적용된 구조를 비교해 보자. 다음 그림은 몬스터를 상대할 플레이어에 무기와 탈것을 포함하는 구조를 보여 준다. 의존성 역전 원칙을 적용하지 않고 클래스를 활용해 기능적으로 설계한 예이다.
그런데 이렇게 하면 플레이어 클래스가 구상 클래스(concrete class)를 통해 무기와 탈것에 직접 의존하게 된다. 따라서 무기나 탈것을 추가하거나 삭제할 대에 플레이어 클래스를 수정해야 한다.

반면에 다음 그림은 무기와 탈것이 추상 계층, 즉 인터페이스를 거쳐서 플레이어가 사용하도록 설계한 예이다. 이렇게 하면 의존성 역전 원칙이 적용된다.

탈 것과 무기를 관리하는 인터페이스를 추가하고, 플레이어는 인터페이스를 참조하도록 했다. 이렇게 의존성 역전 원칙을 적용하면 무기나 탈것을 추가하더라도 플레이어 클래스는 변경하지 않고 확장할 수 있다. 즉, 개방/폐쇄 원칙도 함께 적용된다.

0개의 댓글