SOLID 원칙

SOLID 원칙이란 객체지향 설계에서 지켜줘야 할 5개의 원칙( SRP, OCP, LSP, DIP, ISP )을 말합니다.

하지만... 개념을 알아도 실현하기는 어려운 원칙들입니다.


그럼에도 설계원칙을 알아야 하는 이유는 시스템에 예상하지 못한 변경사항이 발생하더라도, 유연하게 대처하고 이후에 확장성이 있는 시스템 구조를 설계하기 위해서입니다.

좋은 설계란 시스템에 새로운 요구사항이나 변경사항이 있을 때, 영향을 받는 범위가 적은 구조를 말합니다.


앞으로 알아볼 여러 디자인 패턴들은 아래의 SOLID 원칙에 입각해서 만들어진 것이므로, SOLID 원칙이 무엇인지 알아보도록 하겠습니다.





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

객체는 단 하나의 책임만 가져야 한다는 원칙을 말합니다.


객체지향적으로 설계할 때는 응집도를 높게, 결합도는 낮게 설계하는 것이 좋습니다.

  • 응집도
    • 한 프로그램의 요소가 얼마나 뭉쳐있는지, 즉 구성 요소들 사이의 응집력을 말합니다.
  • 결합도
    • 프로그램 구성 요소들 사이가 얼마나 의존적인지를 말합니다.

SRP에 따른 설계를 하면 응집도는 높게, 결합도는 낮게 설계할 수 있게 됩니다.



흔히 함수는 하나의 기능만 수행하도록 구현되어야 하는 것을 알고 있습니다.

calculator() 함수가 덧셈, 뺼셈, 곱셈, 나눗셈을 모두 한다면 이는 좋은 설계가 아닙니다.

덧셈, 뺼셈, 곱셈, 나눗셈이 각각의 함수로 정의되어 있어야 유지보수가 쉬울 것입니다.


마찬가지로 Calculator 객체가 있을 때, Calculator 객체는 덧셈, 뺄셈, 곱셈, 나눗셈만 할 수 있어야 합니다.

즉, 사칙연산에 대한 책임만 갖고 있어야 합니다.

이후에 계산기에 알람 기능을 추가한다고 해서, alarm() 함수를 Calculator의 기능으로 추가하는 것은 SRP에 위배됩니다.


한 객체에 책임이 많아질수록 클래스 내부에서 서로 다른 역할을 수행하는 코드끼리 강하게 결합될 가능성이 높아집니다.

즉, 객체마다 책임을 제대로 나누지 않는다면 시스템은 매우 복잡해집니다.

왜냐하면 그 객체가 하는 일( 함수 )에 변경사항이 생기면 이 기능을 사용하는 부분의 코드를 모두 다시 테스트를 해야 하기 때문입니다.


따라서 여러 객체들이 하나의 책임만 갖도록 잘 분배한다면, 시스템에 변화가 생기더라도 그 영향을 최소화 할 수 있기 때문에 SRP 원칙을 따르는 것이 좋습니다.





2. OCP ( Open-Closed Principle ), 개방-폐쇄 원칙

기존의 코드를 변경하지 않으면서( closed ), 기능을 추가할 수 있도록( open ) 설계가 되어야 한다는 원칙을 말합니다.

즉, 확장에 대해서는 개방적이고 수정에 대해서는 폐쇄적이어야 한다는 의미를 갖습니다.


이를 만족하는 설계가 되려면, 캡슐화를 통해 여러 객체에서 사용하는 같은 기능을 인터페이스에 정의하는 방법이 있습니다.



Animal 인터페이스를 구현한 각 클래스들은 울음소리 crying() 함수를 재정의합니다.
울음소리를 호출하는 클라이언트는 다음과 같이 인터페이스에서 정의한 crying() 함수만 호출하면 됩니다.
public class Client {
public static void main(String args[]){
Animal cat = new Cat();
Animal dog = new Dog();

cat.crying();
dog.crying();
}
}
이렇게 캡슐화를 하면, 동물이 추가되었을 때 cyring() 함수를 호출하는 부분은 건드릴 필요가 없으면서 쉽게 확장할 수 있게됩니다.




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

자식 클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야 한다는 설계 원칙입니다.

즉, 자식 클래스는 언제나 부모 클래스의 역할을 대체할 수 있어야 한다는 것을 말하며, 부모 클래스와 자식 클래스의 행위가 일관됨을 의미합니다.


자식 클래스가 부모 클래스를 대체하기 위해서는 부모의 기능에 대해 오버라이드 되지 않도록 하면 됩니다.

즉, 자식 클래스는 부모 클래스의 책임을 무시하거나 재정의하지 않고 확장만 수행하도록 해야 LSP를 만족하게 됩니다.


처음 OOP를 공부할 때 오버라이드가 OOP의 특징이라 배웠고 이를 잘 활용해야 할 것만 같았는데,

LSP에 따르면 객체지향적으로 설계를 하기 위해서는 오버라이드는 가급적 피하는 것이 좋다고 합니다.





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

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

즉, 하나의 거대한 인터페이스 보다는 여러 개의 구체적인 인터페이스가 낫다는 것을 의미합니다.

SRP는 객체의 단일 책임을 뜻한다면, ISP는 인터페이스의 단일 책임을 의미한다고 보면 됩니다.


예를 들어, 핸드폰( Phone )에는 전화( call ), 문자( sms ), 알람( alarm ), 계산기( calculator ) 등의 기능이 있습니다.

옛날 3G폰과 현재 스마트폰은 Phone의 기능들을 사용하므로, call, sms, alarm, calculator 기능이 정의된 Phone 인터페이스를 정의하려고 합니다.


그러나 ISP를 만족하려면 Phone 인터페이스에 call(), sms(), alarm(), calculator() 함수를 모두 정의하는 것보다,

Call, Sms, Alarm, Calculator 인터페이스를 각각 정의하여, 3G폰과 스마트폰 클래스에서 4개의 인터페이스를 구현하도록 설계되어야 합니다.


이렇게 설계를 하면, 각 인터페이스의 메서드들이 서로 영향을 미치지 않게 됩니다.

즉, 자신이 사용하지 않는 메서드에 대해서 영향력이 줄어들게 됩니다.





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

객체들이 서로 정보를 주고 받을 때 의존 관계가 형성되는데, 이 때 객체들은 나름대로의 원칙을 갖고 정보를 주고 받아야 한다는 설계 원칙입니다.

여기서 나름대로의 원칙이란, 추상성이 낮은 클래스보다 추상성이 높은 클래스와 의존 관계를 맺어야 한다는 것을 의미합니다.

일반적으로 인터페이스를 활용하면 이 원칙을 준수할 수 있게 됩니다. ( 캡슐화 )



Client 객체는 Cat, Dog, Bird의 crying() 메서드에 직접 접근하지 않고, Animal 인터페이스의 crying() 메서드를 호출함으로써 DIP를 만족할 수 있습니다.





이상으로 객체 지향 프로그래밍의 5가지 원칙 SOLID에 대해 알아보았습니다.

다음 글에서부터 SOLID 원칙에 신경 쓴 여러 디자인 패턴들에 대해 알아보겠습니다.