예재로 이해하는 SOLID 5원칙

Posted by , January 21, 2023
객체지향OOPSOLID

학습배경

본 포스팅은 저 혼자서 인강 을 듣고 나만의 예재로 새롭게 만들어 본 내용입니다. 틀린 내용이 많을 수 있으니, 감안해주세요!

본 포스팅은 제 지난 포스팅 시리즈 객체지향을 아는척하지말자 : 우리가 오해하고 있었던 객체지향에 대해 에 이어서 관련 학습 키워드를 인터넷에 열심히 찾아보니, SOLID 라는 패턴이 등장했습니다! 이번엔 SOLID 라는 디자인패턴이 무엇인지를 자세한 예제와 함께 만들어봤으니 열심히 읽어봐주세요 😆😆😆


객체지향의 역할과 구현 (지난 포스팅 리마인딩)

지난 포스팅에서 다루었던 객체지향의 내용중, 일부 내용은 간단히만 리마인딩하고 넘거가겠습니다. 활용했던 예제를 보고 다시 복습해보죠. 그만큼 중요한 내용이기 때문입니다!

사람과 자동차 객체의 협력 관계에서, 자동차 객체는 다양한 차종으로 구분될 수 있을것이라고 했었습니다. 여러 자동차들은 하나의 동일한 역할(= 책임의 집합) 을 수행하는 "자동차" 로 구분된다고 했었습니다.

즉 하나의 역할 아래에서 다양한 구현이 될 수 있으며, 서비스 로직을 설계할때 각 객체들은 구현이 아닌 역할을 중점으로 상호작용해야한다고 했었습니다.

핵심만 정리해보자면 아래와 같았죠.

구현을 중점으로 객체간에 소통하면 유연성이 떨어지니, 역할을 중점으로 설게해야한다. 언제든지 다른 구현 객체로 바뀌더라도 문제가 없도록 설계해야한다.

저희는 이 객체지향의 특징에 대해 꼭 기억하고 있어야합니다! 그래야 SOLID 설계원칙은 무엇인지 이해가 가능하고, 설계가 가능하기 때문이죠.

또 미리 중요한 내용을 미리 말씀드리자면 아래와 같습니다.

자바(JAVA) 에서 역할(=책임)은 인터페이스를 통해 구현해내고, 구현(=역할)은 클래스를 통헤 구현해낸다.

  • 생각해보면 맞는 말인것 같습니다. 역할은 마치 추상적인이고, 구현은 추상적인것을 말 그대로 구현해내는 것인데 말입니다. 자바에서도 이를 가능케하는 것이 인터페이스와 클래스입니다. 추상화 되어있는 인퍼테이스로 각 인터페이스끼리 협력관계를 설게해놓고, 클래스를 통해 구체화시켜놓으면 되는 것입니다.

그러면 본격적으로 SOLID 에 대해 알아봅시다.


SOLID

SOLID 는 객체지향에 대한 5대 설계원칙을 줄여서 부르는것입니다.

  • S(Single Responsibility Principle) : 단일 책임원칙
  • O(Open/Closed Principle) : 개방-폐쇄 원칙
  • L(Liskov Substitution Principle) : 리스코프 치환 원칙
  • I(Interfacwe Segregation Principle) : 인터페이스 분리 원칙
  • D(Dependency Inversion Principle) : 의존관계 역전 원칙

이들이 무엇인지 차근차근 이론적으로 먼저 알아보고, 추후 코드로 직접 구현도 해보면서 이해해봅시다.


SRP : 단일 책임원칙

SRP (Single Responsibility Principle) 단일 책임원칙이란 한 클래스는 하나의 책임만 가져야한다는 것입니다.

여기서 클래스에 대한 "책임"이란 무엇인지 저희가 알고있죠? 제가 계속 강조하며 다루었던 내용입니다. 객체간에 협력을 할때, 각 객체마다 책임(= 역할)을 지니고, 이를 중점으로 협력해야 한다고 했었습니다.

이때 중요한 것은 바로 "변경의 정도" 입니다. 변경이 있을때 해당 서비스에 미치는 파급효과가 적으면 단일책임원칙을 잘 따른것입니다.


OCP : 개방-폐쇄 원칙

OCP(Open/Closed Principle) 이란 확장에는 열려있으나, 변경에는 닫혀있어야한다는 것입니다.

쉽게말해, 자바로 코드를 짤때 코드 변경이 인터페이스에서 있어서는 안된다는 것입니다. 인터페이스의 변경없이 언제든지, 문제없이 구현 클래스를 갈아끼울 수 있어야한다는 것이죠.

위와 같이 자동차 인터페이스가 있고 그에 대한 구현 클래스가 소나타, 스타랙스, 캠핑카 관련 서비스의 구현 클래스가 있다고 해봅시다. 그 중 소나타 구현 클래스가 선택된 상황이라고 해보죠! 자바 코드로 나타내보면 아래와 같습니다.

public interface UserService{
  CarService carservice = new SonarTarService();
  ...
}

그런데 현재 자동차 인터페이스를 구현한 구현 클래스가 소나타일때, 스타랙스로 구현을 바꾼다고 해봅시다. 이때 자바 코드로 구현할때 인터페이스에 영향을 주지 않고 갈아끼우는 것이 가능할까요?

이는 다형성을 잘 활용하면 해결이 될것같지만, 순수 자바코드로는 불가능합니다. OCP 를 위반하는 상황이 발생하는 것이죠.

핵심 요약 구현 클래스 코드를 변경해도 인터페이스 코드에는 영향이 없습니다. 각 인터페이스끼리의 협력관계, 즉 역할에 영향을 미치지 않기 때문이죠.

  • OCP 위반하는 상황 : 그러나 문제는 구현 클래스를 다른 클래스로 갈아끼울때 발생합니다. 인터페이스에서 구현 객체를 선택해야해서, 코드를 수정해야하기 때문입니다.

순수 자바코드에서 OCP 가 위반되는 상황

위 상황을 좀 더 자세히 설명드리겠습니다.

순수 자바 코드로 구현했을때는 OCP를 지키는 것에 한계가 있습니다. 자동차 인퍼페이스를 구현할때, 구현 클래스를 직접 선택해줘야 한다는 문제가 있습니다.

만일 아래처럼 UserService 에서 CarService 인터페이스가 있고 그에대한 구현객체로 소나타 관련 구현 클래스(SonarTarService) 객체를 지정했다고 해봅시다.

public interface UserService{
  CarService carservice = new SonarTarService();
  ...
}

그런데 소나타가 아닌 스타랙스 서비스로 갈아끼워야하는 경우, 아래처럼 직접 UserService 에서 코드를 변경해줘야 합니다.

public interface UserService{
  CarService carservice = new StarRexService(); // 인터페이스에서 코드 수정이
       ...                      // 일어났다! 구현 객체를 변경하면 DIP가 위반된다.
}

변경에는 닫혀있어야 하는데(= 인터페이스에 코드 변경이 일어나서는 안되는데), 인터페이스 코드를 수정해야하므로 OCP 를 위반한 것이죠.

OCP 를 어떻게 지키지? : DI 컨테이너

이를 잘 생각해보면, 인터페이스 내부에서 직접 구현 클래스를 선택하는 방법이 아닌, 외부에서 구현 클래스를 선택하게 할 수 있다면 OCP 를 지킬 수 있지 않을까요?

  • 스프링에서는 에서는 DIP 를 지키기 위해, 객페를 생성하고 연관관계를 맺어주는 별도의 조립, 설정자(DI, loc 컨테이너)를 제공해줍니다.

위 내용은 중요하니, 꼭 기억하고 넘어갑시다.


LSP : 리스코프 치환 원칙

LSP(Lisvov Substitution Principle) 란 프로그램 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야한다는 것입니다.

  • 쉽게말해, 자동차 인터페이스의 엑셀 기능은 전진하라는(= 규약)이 있을겁니다. 그런데 반대로 후진하게 구현해버려면, 컴파일 단계에서는 에러도 안터지고 문제없이 빌드에 성공하겠지만 프로그램의 정확성과 정해놓은 규약에 위반된 것입니다.

즉 LSP 를 위반한 것이고, 이 인터페이스의 구현 클래스를 엑셀을 밟았을때 앞으로 가도록 수정해서 해결해야겠죠.


ISP 인터페이스 분리 원칙

ISP(Interface segregation principle) 란 특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다는 것입니다.

  • 쉽게말해, 인터페이스를 자잘하게 쪼개면 좋다는 것입니다.

잘게 쪼개지 않는경우, 인퍼페이스 A의 주문 기능을 수정할때 상품조회와 같은 다른 기능도 포함되어 있는경우 어쩌면 영향을 미칠수도 있습니다. 상황이 곤란해질 수 있는것이죠.


DIP 의존관계 역전 원칙

DIP(Dependency inversion principle) 란 프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다" 라는 것입니다.

  • 쉽게말해, 클라이언트는 구현 클래스에 의존하지 말고, 인터페이스에 의존하게 설계해서 역할 중심의 설계가 되도록 만들라는 것입니다.

  • 계속 말씀드렸듯이, 클라이언트가 역할에 의존해야 유연하게 구현체를 변경가능할겁니다!

순수 자바코드에서 DIP 를 위반하는 상황

그런데 이 DIP 원칙도 순수 자바코드에서는 지킬 수 없습니다. 아까 예제로 살펴봤던 UserService 를 다시 살펴봅시다.

public interface UserService{
  CarService carservice = new SonarTarService();
  ...
}

아시듯이, UserService 클라이언트가 직접 구현 클래스를 선택하는 방식입니다. CarService 라는 추상화(인터페이스) 에도 의존하고 이지만, 동시에 SonarTarService 라는 구체화(구현 클래스) 에도 의존하고 있기 때문에 DIP 를 위반하는 것이죠.


DIP 를 어떻게 지키지? : DI 컨테이너

OCP 의 문제 발생상황가 마찬가지로, 스프링에서는 DIP 를 지킬 수 있도록 외부에서 인터페이스의 관계를 주입해주는 DI(Dependency Injection) 컨테이너 이라는 것을 제공해줍니다.


정리

객체지향의 핵심은 다형성이며, 결국 다형성 만으로는 쉽게 부품을 갈아 끼우듯이 개발할 수가 없습니다. 직접 일일이 수정해줘야하느라 OCP, DIP 를 위반하게 됩니다.


마치며

지금까지 객체지향의 특성을 살린 SOLID 5원칙에 대해 자세히 알아봤습니다. 처음부터 이해하기엔 어려울 수 있는 원칙이니 많이 시간을 투자하여 꼭 이해하셨으면 하는 바람입니다.

이해가 안가시거나 햇갈리는 부분이 있다면 댓글로 알려주세요! 도와드리겠습니다 😉