예제로 이해하는 SOLID 설계원칙, 그리고 스프링 DI 컨테이너의 등장

msung99·2023년 1월 21일
35

Spring

목록 보기
15/19
post-thumbnail

시작에 앞서

본 포스팅은 제 지난 포스팅 시리즈 객체지향을 아는척하지말자 : 우리가 오해하고 있었던 객체지향에 대해 에 이어서 진행되는 내용입니다. 꼭 이전 포스팅을 읽고 오시는 것을 권장드립니다! 현 포스팅에서는 객체지향의 특성을 살린 SOLID 5대 설계원칙에 대해 알아보겠습니다. 또한 5개의 원칙중에서 일부 원칙을 순수 자바코드에서 구현하는 경우 어길 수밖에 없는데, 이를 어떻게 스프링에서 해결해주는 것인지 알아봅시다!

객체지향 시리즈 포스팅 진행현황

현재 진행중인 포스팅 내용은 아래와 같습니다. 그 중 2번째 포스팅을 지금 진행해볼까합니다!

객체지향 관련 시리즈 진행현황

  • 객체지향을 아는척하지 말자 : 오해하고 있었던 객체지향의 정체
  • 예제로 이해하는 SOLID 5원칙, 그리고 스프링 DI 컨테이너의 등장 (현재 포스팅)
  • 직접 만들어보며 이해하는 SOLID 원칙과 DI 설계 : 수동으로 직접 의존관계 주입해보기
  • 싱글톤(SingleTon) : 왜 스프링 컨테이너를 써야할까?
  • 컴포넌트 스캔과 @Autowired 의 메커니즘 : 필요성에 대해
  • 알면 도움될 컴포넌트 스캔의 다양한 대상들과 DI 에 대한 해결방법

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

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

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

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

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

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

저희는 이 객체지향의 특징에 대해 꼭 기억하고 있어야합니다! 그래야 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 를 위반하게 됩니다.

DI, DI 컨테이너

스프링은 다음 기술로 다형성과 OCP, DIP 를 가능하게 지원해줍니다.

  • DI(Dependency Injection) : 의존관계, 의존성 주입
  • DI 컨테이너 제공

이 기술들을 사용하면, 클라이언트의 코드의 변경없이 기능 확장이 가능합니다. 즉, 쉽게 부품을 교체하듯이 개발이 가능해지죠.

DI 컨테이너를 사용하면 인터페이스를 미리 설계해놓고, 구현을 나중에 결정해주면 구현 기술이 계속 바뀌더라도 언제든지 변경가능하다!

기술스택이 늦게 정해지더라도 (ex. DB를 jdbc, RDB, .. 중에서 뭘쓸지 계속 안정해지는 상황) 구현이 가능해집니다. 나중에 DB 및 기술스택이 확정됨에 따라 그때 미리 구현해놓은 구현 클래스중에서 하나 선택해서 끼워주면 되기 때문이죠.


마치며

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

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


추후 포스팅 계획 : 계속 이어지는 내용들

앞서 말씀드렸지만, 저는 객체지향에 대한 내용을 이 포스팅을 마무리로 끝내지 않습니다. 현재 스프링부트를 학습하고 있는 입장으로써, 학습한 내용들을 기반으로 어떻게 객체지향의 특성을 살려서 프레임워크에 적용할 수 있을지에 대해 계속 다루어보고자 합니다.

다음 포스팅에서는 스프링의 DI 컨테이너, @Bean 에 대해 다루어 보겠습니다!

.

profile
꾸준히 성장하는 과정속에서, 제 지식을 많은 사람들과 공유하기 위한 블로그입니다 😉

0개의 댓글