본 포스팅은 혼자서 얇게 학습해본 내용이므로, 틀린 내용이 많을 수 있습니다. 감안하고 읽어주세요!!
사람과 자동차 객체의 협력 관계에서, 자동차 객체는 다양한 차종으로 구분될 수 있을것이라고 했었습니다. 여러 자동차들은 하나의 동일한 역할(= 책임의 집합) 을 수행하는 "자동차" 로 구분됩니다.
즉 하나의 역할 아래에서 다양한 구현이 될 수 있으며, 서비스 로직을 설계할때 각 객체들은 구현이 아닌 역할을 중점으로 상호작용 해야합니다.
핵심만 정리해보자면 아래와 같았죠.
구현을 중점으로 객체간에 소통하면 유연성이 떨어지니, 역할을 중점으로 설게해야한다. 언제든지 다른 구현 객체로 바뀌더라도 문제가 없도록 설계해야한다.
저희는 이 객체지향의 특징에 대해 꼭 기억하고 있어야합니다! 그래야 SOLID 설계원칙은 무엇인지 이해가 가능하고, 설계가 가능하기 때문이죠.
또 미리 중요한 내용을 미리 말씀드리자면 아래와 같습니다.
자바(JAVA) 에서 역할(=책임)은 인터페이스를 통해 구현해내고, 구현(=역할)은 클래스를 통헤 구현해낸다.
그러면 본격적으로 SOLID 에 대해 알아봅시다.
SOLID 는 객체지향에 대한 5대 설계원칙을 줄여서 부르는것입니다.
이들이 무엇인지 차근차근 이론적으로 먼저 알아보고, 추후 코드로 직접 구현도 해보면서 이해해봅시다.
SRP (Single Responsibility Principle) 단일 책임원칙이란 한 클래스는 하나의 책임만 가져야한다는 것입니다.
여기서 클래스에 대한 "책임"이란 무엇인지 저희가 알고있죠? 제가 계속 강조하며 다루었던 내용입니다. 객체간에 협력을 할때, 각 객체마다 책임(= 역할)을 지니고, 이를 중점으로 협력해야 한다고 했었습니다.
이때 중요한 것은 바로 "변경의 정도" 입니다. 변경이 있을때 해당 서비스에 미치는 파급효과가 적으면 단일책임원칙을 잘 따른것입니다.
OCP(Open/Closed Principle) 이란 확장에는 열려있으나, 변경에는 닫혀있어야한다는 것입니다.
쉽게말해, 자바로 코드를 짤때 코드 변경이 인터페이스에서 있어서는 안된다는 것입니다. 인터페이스의 변경없이 언제든지, 문제없이 구현 클래스를 갈아끼울 수 있어야한다는 것이죠.
위와 같이 자동차 인터페이스가 있고 그에 대한 구현 클래스가 소나타, 스타랙스, 캠핑카 관련 서비스의 구현 클래스가 있다고 해봅시다. 그 중 소나타 구현 클래스가 선택된 상황이라고 해보죠! 자바 코드로 나타내보면 아래와 같습니다.
public interface UserService{
CarService carservice = new SonarTarService();
...
}
그런데 현재 자동차 인터페이스를 구현한 구현 클래스가 소나타일때, 스타랙스로 구현을 바꾼다고 해봅시다. 이때 자바 코드로 구현할때 인터페이스에 영향을 주지 않고 갈아끼우는 것이 가능할까요?
이는 다형성을 잘 활용하면 해결이 될것같지만, 순수 자바코드로는 불가능합니다. OCP 를 위반하는 상황이 발생하는 것이죠.
핵심 요약
구현 클래스 코드를 변경해도 인터페이스 코드에는 영향이 없습니다. 각 인터페이스끼리의 협력관계, 즉 역할에 영향을 미치지 않기 때문이죠.
- OCP 위반하는 상황 : 그러나 문제는 구현 클래스를 다른 클래스로 갈아끼울때 발생합니다. 인터페이스에서 구현 객체를 선택해야해서, 코드를 수정해야하기 때문입니다.
위 상황을 좀 더 자세히 설명드리겠습니다.
순수 자바 코드로 구현했을때는 OCP를 지키는 것에 한계가 있습니다. 자동차 인퍼페이스를 구현할때, 구현 클래스를 직접 선택해줘야 한다는 문제가 있습니다.
만일 아래처럼 UserService 에서 CarService 인터페이스가 있고 그에대한 구현객체로 소나타 관련 구현 클래스(SonarTarService) 객체를 지정했다고 해봅시다.
public interface UserService{
CarService carservice = new SonarTarService();
...
}
그런데 소나타가 아닌 스타랙스 서비스로 갈아끼워야하는 경우, 아래처럼 직접 UserService 에서 코드를 변경해줘야 합니다.
public interface UserService{
CarService carservice = new StarRexService(); // 인터페이스에서 코드 수정이
... // 일어났다! 구현 객체를 변경하면 DIP가 위반된다.
}
변경에는 닫혀있어야 하는데(= 인터페이스에 코드 변경이 일어나서는 안되는데), 인터페이스 코드를 수정해야하므로 OCP 를 위반한 것이죠.
이를 잘 생각해보면, 인터페이스 내부에서 직접 구현 클래스를 선택하는 방법이 아닌, 외부에서 구현 클래스를 선택하게 할 수 있다면 OCP 를 지킬 수 있지 않을까요?
- 스프링에서는 에서는 DIP 를 지키기 위해, 객페를 생성하고 연관관계를 맺어주는 별도의 조립, 설정자(DI, loc 컨테이너)를 제공해준다고 합니다 🤔
위 내용은 중요하니, 꼭 기억하고 넘어갑시다.
LSP(Lisvov Substitution Principle) 란 프로그램 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야한다는 것입니다.
즉 LSP 를 위반한 것이고, 이 인터페이스의 구현 클래스를 엑셀을 밟았을때 앞으로 가도록 수정해서 해결해야겠죠.
ISP(Interface segregation principle) 란 특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다는 것입니다.
잘게 쪼개지 않는경우, 인퍼페이스 A의 주문 기능을 수정할때 상품조회와 같은 다른 기능도 포함되어 있는경우 어쩌면 영향을 미칠수도 있습니다. 상황이 곤란해질 수 있는것이죠.
DIP(Dependency inversion principle) 란 프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다" 라는 것입니다.
쉽게말해, 클라이언트는 구현 클래스에 의존하지 말고, 인터페이스에 의존하게 설계해서 역할 중심의 설계가 되도록 만들라는 것입니다.
계속 말씀드렸듯이, 클라이언트가 역할에 의존해야 유연하게 구현체를 변경가능할겁니다!
그런데 이 DIP 원칙도 순수 자바코드에서는 지킬 수 없습니다. 아까 예제로 살펴봤던 UserService 를 다시 살펴봅시다.
public interface UserService{
CarService carservice = new SonarTarService();
...
}
아시듯이, UserService 클라이언트가 직접 구현 클래스를 선택하는 방식입니다. CarService 라는 추상화(인터페이스) 에도 의존하고 이지만, 동시에 SonarTarService 라는 구체화(구현 클래스) 에도 의존하고 있기 때문에 DIP 를 위반하는 것이죠.
OCP 의 문제 발생상황가 마찬가지로, 스프링부트에서 DIP 를 지킬 수 있도록 외부에서 인터페이스의 관계를 주입해주는 DI(Dependency Injection) 컨테이너 이라는 것을 제공해준다고 하네요! 😲
지금까지 객체지향의 특성을 살린 SOLID 5원칙에 대해 자세히 알아봤습니다. 처음부터 이해하기엔 어려울 수 있는 원칙이니 많이 시간을 투자하여 꼭 이해하셨으면 하는 바람입니다.
이해가 안가시거나 햇갈리는 부분이 있다면 댓글로 알려주세요! 도와드리겠습니다 😉