본문 바로가기
JAVA

[OOP] 객체지향 설계의 5가지 원칙 (SOLID)

by yonikim 2023. 12. 22.
728x90

 

Design Principles and Design Patterns 

 

모든 개발이 그렇듯 프로젝트 초반에는 완벽하게 설계가 된 것 같고 견고하게 구조를 다듬어 나아가는듯해 보이지만, 개발을 하면 할수록 나의 코드와 구조의 틀이 점점 어긋나기 시작한다. 

결국 어쩔 수 없이 리팩토링을 하게 되는데 그럴 때마다 항상 떠오르는 기본중의 기본 원칙, 바로 이번에 소개하는 개발지향 설계의 5가지 원칙을 항상 떠올리게 된다. 

 


 

 

 

SOLID 설계 원칙은 OOP 의 4가지 특징(추상화, 상속, 다형성, 캡슐화)와 더불어, 객체 지향 프로그래밍의 단골 면접 질문 중 하나이다. 또한 앞으로 배우게 될 여러 디자인 패턴(Design Pattern) 들이 SOLID 설계 원칙에 입각해서 만들어진 것이기 때문에, 표준화 작업에서부터 아키텍처 설계에 이르기까지 다양하게 적용되는 것의 근간이 된다. 

 

본래 좋은 소프트웨어란 변화에 대응을 잘 하는 것을 말한다.

좋은 설계란 시스템에 새로운 요구사항이나 변경사항이 있을 때, 영향을 받는 범위가 적은 구조를 말한다. 그래서 시스템에 예상하지 못한 변경사항이 발생하더라도, 유연하게 대처하고 이후에 확장성이 있는 시스템 구조를 만들 수 있다.

즉, SOLID 객체 지향 원칙을 적용하면 코드를 확장하고 유지 보수 관리하기가 더 쉬워지며, 불필요한 복잡성을 제거해 리팩토링에 소요되는 시간을 줄임으로써 프로젝트 개발의 생산성을 높일 수 있다. 

 

SOLID 는 어떠한 특정 프로그래밍 언어 혹은 프레임워크를 위해 만든 원칙이 아니다. 

따라서 JAVA 뿐만 아니라 TypeScript 등 선호하는 프로그래밍 언어나 프레임워크에 원칙을 자유롭게 적용할 수 있다. 

 

 

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


하나의 클래스(객체)는 단 하나의 책임만 가져야 한다는 원칙이다. 

여기서 '책임' 이라는 의미는 하나의 '기능 담당' 으로 보면 된다. 즉, 하나의 클래스는 하나의 기능을 담당하여 하나의 책임을 수행하는데 집중되도록 클래스를 따로따로 여러개 설계하라는 원칙이다. 

 

만일 하나의 클래스에 기능(책임)이 여러개 있다면 기능 변경(수정)이 일어났을때 수정해야 할 코드가 많아진다. 

예를 들어 A 를 고쳤더니 B 를 수정해야 하고 또 C 를 수정해야하고, C 를 수정했더니 다시 A 로 돌아가서 수정해야 하는, 마치 책임이 순환되는 형태가 되어버린다.

따라서 SRP 원칙을 따름으로써 한 책임의 변경으로부터 다른 책임의 변경으로의 연쇄작용을 극복할 수 있게 된다. 

 

최종적으로 SRP 원칙의 목적은 프로그램의 유지보수성을 높이기 위한 설계 기법이다.

이때 책임의 범위는 딱 정해져있는 것이 아니고, 어떤 프로그램을 개발하느냐에 따라 개발자마다 생각 기준이 달라질 수 있기 때문에, 단일 책임 원칙에 100% 해답은 없다. 

 

 

2. OCP (개방폐쇄의 원칙: Open Close Principle)


OCP 원칙은 클래스는 확장에 열려있어야 하며, 변경(수정)에는 닫혀있어야 한다 를 뜻한다.

 

기능 추가 요청이 오면 클래스 확장을 통해 손쉽게 구현하면서, 확장에 따른 클래스 수정은 최소화 하도록 프로그램을 작성해야 하는 설계 기법이다. 

  • 확장에 열려있다 - 새로운 변경 사항이 발생했을 때 유연하게 코드를 추가함으로써 큰 힘을 들이지 않고 애플리케이션의 기능을 확장할 수 있음
  • 변경에 닫혀있다 - 새로운 변경 사항이 발생했을 때 직접적인 객체 수정을 제한함 

어렵게 생각할 필요없이, OCP 원칙은 추상화 사용을 통한 관계 구축의 권장을 의미하는 것이다. 

즉, 다형성과 확장을 가능케 하는 객체지향의 장점을 극대화하는 기본적인 설계 원칙이다. 

 

 

3. LSP (리스코브 치환의 원칙: The Liskov Substitution Principle)


LSP 원칙은 서브 타입은 언제나 기반(부모) 타입으로 교체할 수 있어야 한다 는 원칙이다.

 

LSP 는 상속의 기본적인 메커니즘을 표현하고 있다. 

LSP 는 사용자의 관점에서 기능에 영향을 미치지 않고 서브 클래스를 부모 클래스로 대체할 수 있어야 한다.

상위 클래스는 하위 클래스에서 기본적으로 구현해야 할 기능만 가지고 있거나 추상화해야 하며, 하위 클래스는 상위 클래스에서 정의해놓은 정말 필요한 기능만을 구현하도록 해야 한다.

 

상속이 있으면 다형성도 같이 가져갈 수 있다. 즉, LSP 원칙은 다형성 원리를 이용하기 위한 원칙의 개념으로 보면 된다.

하지만 다형성의 이점을 얻기 위해서는 하위 클래스와 상위 클래스의 클라이언트 간의 규약을 항상 지켜야 한다. 

LSP 원칙에 따라서 구현을 하게 된다면 OCP 원칙은 자연스럽게 따라오게 된다. 

 

자바에선 대표적으로 Collection 인터페이스를 LSP 원칙의 예로 들 수 있다.
Collection 타입의 객체에서 자료형을 LinkedList 에서 전혀 다른 자료형 HashSet 으로 바꿔도 add() 메소드를 실행하는데 있어 원래 의도대로 작동되기 때문이다. 
한마디로 다형성 이용을 위해 부모 타입으로 메소드를 실행해도 의도대로 실행되도록 구성을 해줘야 하는 원칙이라 이해하면 된다. 

 

public void myData() {
	// Collection 인터페이스 타입으로 변수 선언
    Collection data = new LinkedList();
    data = new HashSet(); // 중간에 전혀 다른 자료형 클래스를 할당해도 호환됨
    
    modify(data); // 메소드 실행
}

public void modify(Collection data){
    list.add(1); // 인터페이스 구현 구조가 잘 잡혀있기 때문에 add 메소드 동작이 각기 자료형에 맞게 보장됨
    // ...
}

 

 

 

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


ISP 원칙은 인터페이스를 각각 사용에 맞게끔 잘게 분리해야 한다 는 설계 원칙이다.

 

SRP 원칙이 클래스의 단일 책임을 강조한다면, ISP 원칙은 인터페이스의 단일 책임을 강조하는 것으로 보면 된다. 

즉, SRP 원칙의 목표는 클래스 분리를 통해 이뤄진다면, ISP 원칙은 인터페이스 분리를 통해 설계된다. 

 

ISP 원칙은 인터페이스를 사용하는 클라이언트를 기준으로 분리함으로써, 클라이언트의 목적과 용도에 적합한 인터페이스만을 제공하는 것이 목표이다. 

ISP 원칙에서 주의해야 할 사항은 기존 클라이언트에 변화를 주지 않으면서 인터페이스만을 분리하여 구현해야 한다는 점이다. 그렇게 인터페이스를 분리함으로서 의존성을 약화시켜 리팩토링 및 구조 변경에 용이하게 만들어준다. 

 

인터페이스는 제약 없이 자유롭게 다중 상속(구현)이 가능하기 때문에, 분리할 수 있으면 분리하여 각 클래스 용도에 맞게 implements 하라는 설계 원칙이라고 이해하면 된다.

 

 

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


DIP 원칙은 어떤 클래스를 참조해서 사용해야 하는 상황이 생긴다면, 그 클래스를 직접 참조하는 것이 아니라 그 대상의 상위 요소(추상 클래스 or 인터페이스)로 참조하라는 원칙이다.  

 

쉽게 말하면 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻이다. 

의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다는, 변화하기 어려운 것 혹은 거의 변화가 없는 것에 의존하라는 것이다. 

의존성 역전의 원칙의 지향점은 각 클래스간의 결합도(coupling) 을 낮추는 것이다.

 

 

 

 

References 

객체지향 개발 5대 원칙 (SOLID)

객체 지향 설계의 5가지 원칙 - S.O.L.I.D

728x90