본문 바로가기

객체지향프로그래밍

객체지향 프로그래밍의 설계원칙(SOLID)

지난 포스트에서 알아본 객체지향 프로그래밍의 특징을 통해서 설계원칙을 차근차근 알아보려고 합니다. 

이것도 자세히 공부하다 보니 막상 처음 생각처럼 쉽게 체득이 되지는 않았습니다.

그러나 제게 맞는 방법으로 이해하고 정확한 정보를 전달하기 위해 세밀하게 구성을 했으니 도움이 되었으면 좋겠습니다. 

그럼 시작해보겠습니다


객체지향 프로그래밍의 설계원칙(SOLID)란 무엇인가 ? 

객체지향 프로그래밍의 주요 목표는 코드의 유지보수성재사용성 등을 높이고 코드의 관리를 더 효율적으로 할 수 있도록 하는 것입니다.

이러한 목표를 위해서 프로그래밍의 설계 단계에서 지켜야 할 원칙을 SOLID라고 말하는데,

1. 단일 책임 원칙 (Single Responsibility Principle)
2. 개방/폐쇄 원칙  (Open/Closed Principle)
3. 리스코프 치환 원칙 (Liskov Substitution Principle)
4. 인터페이스 분리 원칙 (Interface Segregation Principlw)
5. 의존 역전 원칙 (Dependency Inversion Principle)

이렇게 5가지가 있습니다.

각각의 요소들을 하나씩 살펴보면서 객체지향 프로그래밍의 특징과 어떻게 맞물리는지 알아보겠습니다.




1. 단일 책임 원칙 (Single Responsibility Principle)

클래스는 하나의 책임만을 가져야 하며, 그 책임을 완전히 책임져야 한다는 뜻입니다.

그런데 이렇게 말하면 잘 모를 것 같습니다. 

클래스가 하나의 책임만을 가져야 하는 것이 중요한 핵심인데, 이는 클래스가 변경되어야 하는 이유를 최소화 하기 위함입니다.

조금 더 쉽게 말하자면, 클래스에서 오류가 발생해서 수정을 해야한다고 할 때, 클래스가 하나의 책임만을(기능만을) 가진다면 코드를 수정하는 범위가 좁아지기 때문에 에러가 재차 발생할 확률이 낮아지고 수정하면서 다른 클래스에게 끼치는 영향이 줄어든다고 할 수 있겠습니다.

단일 책임 원칙은 특히 캡슐화와 가장 직접적으로 연관이 있는데, 캡슐화를 통해서 클래스의 변경에 대한 영향을 최소화 하면서 높은 응집도와 낮은 결합도를 유지하는데 도움이 됩니다.

 




2. 개방/폐쇄 원칙 (Open/Closed Principle)

소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 개방적이지만 변경에 대해서는 폐쇄적이어야 한다.

어떻게 보면 정말 모순같은 말일 수 있습니다. 모듈의 확장이 일어나는 순간 어느정도는 수정사항이 생겨야 할텐데 어떻게 확장을 하면서 변경을 하지 않아야 한다는 걸까요? 

설계원칙인 만큼 설계단계에 지켜야 하는 원칙인데, 이는 뼈대가 되는 모듈과 확장을 위한 모듈을 분리하고 상속과 인터페이스를 통해 확장할 계획을 가지고 있다면 확장할 때 수정사항이 생기지 않도록 유도할 수 있을 것입니다.

이는 상속, 다형성과 밀접한 관련이 있는데, 상속을 통해 확장을 하고 필요에 따라 오버라이딩을 통해 적합한 코드로 만들어내면서 유연성과 확장성에 도움이 됩니다. 

 




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

어떤 클래스나 모듈이 상속 관계에 있을 때 서브타입은 언제든지 슈퍼타입으로 대체될 수 있어야 한다.

상속과 다른 점이 있을까요? 처음 공부할 때는 상속에서 공부했던 내용을 그대로 떠올렸습니다. 

서브타입은 슈퍼타입의 필드 및 매서드를 모두 가져오고, 이를 필요에 따라 오버라이딩을 통해 적합한 코드로 변환하여 쓰는 것이 상속의 특징입니다.

그렇다면 당연히 서브타입은 슈퍼타입의 요소들을 모두 가지고 있기 때문에 언제든지 대체할 수 있는게 아닌가? 라고 생각을 했었습니다. 

크게 다르지는 않습니다. 그러나 리스코프 치환 원칙은 상속 관계에서 더 나아가 더 넓은 의미에서의 대체를 강조하고 있습니다.

특징적인 부분은 서브타입이 슈퍼타입을 대체할 때 불변적인 특성을 유지해야 하는 조건을 가지고 있습니다.

불변적인 특성이란 클라이언트 코드(클래스나 모듈 등이 제공하는 매서드나 기능을 사용하는 코드)가 슈퍼타입을 사용하려고 할 때 문제 없이 서브타입으로 대체될 수 있어야 된다는 것입니다.

굉장히 모호할 수 있는 부분인데 가벼운 예시를 들자면 슈퍼타입을 도형 서브타입을 원 삼각형 사각형 등이라고 할 때, 클라이언트 코드가 도형을 사용하려고 할 때 원 삼각형 사각형 등으로 문제 없이 대체를 할 수 있어야 한다는 뜻입니다.

이는 상속과 다형성이 적절하게 섞여서 들어갈 때 좋은 효과를 기대할 수 있습니다. 상속된 메서드를 오버라이딩할 때, 새롭게 구현된 코드가 슈퍼타입이 기대하는 행동과 일관되어야 하기 때문입니다. 

 




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

클라이언트(인터페이스를 사용하는 코드)는 자신이 사용하지 않는 메서드에 의존해서는 안되며, 클래스는 자신이 제공하지 않는 메서드에 의존해서는 안 된다.

인터페이스는 코드의 재사용성을 증대시키기 위해 여러 클래스에서 공통적으로 사용되는 메서드의 집합을 정의합니다. 각 클래스에서는 인터페이스를 구현하여 해당 메서드들을 실제로 구현하고, 구현된 클래스의 인스턴스를 통해 인터페이스의 메서드를 사용할 수 있습니다.

그럼 이런 생각을 할 수 있을 것 같습니다. 

그럼 그냥 다 하나에 모아놓고 필요할 때마다 쓰면 되는거 아니야???

절대 아닙니다. 

객체지향 프로그래밍의 기본 목적은 코드의 재사용성과 유지보수의 원활함입니다. 

클래스와 인터페이스 등의 객체들이 필요한 곳에서 해야할 역할만을 정확하게 수행하는 것을 목표로 가지고 있습니다.

따라서 인터페이스와 클라리언트 또한 필요한 목적과 기능만을 가지고 사용하면서 코드의 유연함과 유지보수에 중점을 두어야 합니다. 

큰 인터페이스를 작게 분리함으로써 각 클라이언트가 필요한 메서드만을 사용할 수 있도록 하는 것은 인터페이스 분리 원칙을 따르는 것입니다. 

이를 통해 클라이언트는 자신이 필요로 하는 메서드에만 의존하게 되어 의존성을 줄이고불필요한 자원 소모를 최소화할 수 있습니다. 

따라서 인터페이스를 필요에 따라 나누어 클라이언트의 불필요한 의존성을 낮추고 유연성을 높이는 것이 중요합니다.

 




5. 의존 역전 원칙 (Dependency Inversion Principle)

고수준 모듈이 저수준 모듈에 의존하지 말고 양쪽 모듈 모두 추상화에 의존해야 한다.

우선 모듈이 무엇인지 알아야겠습니다.

모듈이란 프로그래밍 쪽에서 자주 쓰이는 단어입니다. 쉽게 말하면 특정 기능이나 작업을 수행하는 코드의 묶음 단위라고 할 수 있습니다.

관련된 클래스, 함수와 변수 등을 모두 포함하고 이런 요소들이 그 모듈 내에서도 상호작용하며 기능이나 작업을 수행합니다.

작은 단위의 단일 책임 원칙(SRP)을 지킨 코드블럭이라고 생각할 수도 있겠습니다. 

모듈의 뜻을 알았으니 의존 역전 원칙이 가지는 뜻도 이해를 할 수 있겠습니다.

고수준 모듈(시스템의 핵심 기능)이 저수준 모듈(시스템의 세부 기능 구현)에 의존하지 말고 양쪽 모듈 모두 추상화에 의존해야 한다.

즉 고수준 모듈과 저수준 모듈 모두 추상화된 인터페이스나 추상 클래스에 의존하여 유연성을 높이고 모듈간의 의존성을 최소화하여 코드의 유지 보수성을 높일 수 있습니다.




객체지향 프로그래밍의 설계원칙을 공부하며 프로그램을 설계한다는 것은 목표를 달성하기 위해 계획을 하는 것과 비슷하다고 생각이 들었습니다. 

계획을 한다는 것은 최종 목표를 정하고 그에 맞게 큰 덩어리의 계획을 세우며 점점 작은 목표들을 채워나가듯이,

 

프로그램의 설계도 프로그램의 목표를 명확히 구상하고 그에 맞는 고수준 모듈먼저 놓고 저수준 모듈과 각각의 모듈에게 적합한 책임을 잘 지워준다. 라는 생각이 들어

 

프로그래밍은 단순한 기능구현만이 중요한게 아니라 설계를 시작하는 단계부터 많은 노력을 쏟아야 효율적이고 품질이 좋은 프로그램을 개발할 수 있다는 것을 느꼈습니다.