목차
서론
SOLID의 원칙과 스프링을 연관지어 알아봅시다.
클린 코드로 유명한 로버트 마틴의 좋은 객체 지향 설계의 5가지 원칙 (SOLID)
- SRP: 단일 책임 원칙 (Single Responsibility Principle)
- OCP: 개방-폐쇄 원칙(Open/Closed Principle)
- LSP: 리스코프 치환 원칙(Liskov Substitution Principle)
- ISP: 인터페이스 분리 원칙(Interface Segregation Principle)
- DIP: 의존관계 역전 원칙(Dependency Inversion Principle)
SRP 단일 책임 원칙
Single Responsibility principle
- 한 클래스는 하나의 책임만 가져야 한다는 원칙입니다.
- 하지만, 하나의 책임이라는 것은 모호합니다. 문맥과 상황에 따라 달라질 수 있습니다.
- 중요한 기준은 변경입니다. 변경이 있을 때 파급 효과가 적다면, 단일 책임 원칙을 잘 따른 것입니다.
ex) UI 변경 시, 객체의 생성과 사용을 분리 시
OCP 개방-폐쇄 원칙 ⭐
Open/Closed Principle
- 소프트웨어 요소는 확장에는 열려있으나, 변경에는 닫혀 있어야 한다는 원칙입니다.
- 조금 이상하게 들릴 수도 있습니다. 확장을 하려면 당연히 변경을 해야 할텐데 말입니다.
- 이때 사용되는 것이 다형성입니다.
- 직전 글에서 배운 역할과 구현의 분리를 생각해 봅시다.
public class MemberService {
private MemberRepository memberRepository = new MemoryMemberRepository();
}
public class MemberService {
// private MemberRepository memberRepository = new MemoryMemberRepository();
private MemberRepository memberRepository = new JdbcMemberRepository();
}
MemberRepository 인터페이스를 구현한 MemoryMemberRepository와 JdbcMemberRepository가 있습니다.
위의 코드와 같이 두개의 클래스는 같은 인터페이스를 구현했기 때문에 다형성을 이용하여 교체할 수 있습니다.
하지만, OCP의 원칙을 지켰다기엔 문제가 있습니다.
현재 코드의 문제점
: MemberService가 직접 구현 클래스를 선택했다.
MemberRepository m = new MemoryMemberRepository(); // 기존 코드
MemberRepository m = new JdbcMemberRepository(); // 변경코드
- 이와 같이 작성하면 구현 객체를 변경하려면 클라이언트인 MemberService의 코드도 수정해야 합니다.
- 다형성을 적용했는데, OCP의 원칙을 지킬 수 없습니다.
- 이 문제를 어떻게 해결해야 할까요?
해결책
클라이언트인 MemberService는 MemberRepository의 사용까지만 선언합니다.
즉, 다음과 같이 작성합니다.
MemberRepository m;
그렇다면 객체 m이 MemoryMemberRepository, JdbcMemberRepository 둘 중 어느 구현체를 선택했는지 어떻게 알려줄까요?
여기서 구현체 객체의 생성과 연관 관계를 맺어주는 별도의 조립, 설정자가 필요하게 됩니다.
이러한 역할을 하는 것이 스프링 컨테이너입니다.
LSP 리스코프 치환 원칙
Liskov Substitution Principle
- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서, 하위 타입의 인스턴스로 바꿀 수 있어야 한다는 원칙입니다.
- 단순히 컴파일에 성공하는 것을 넘어서는 이야기입니다.
- 예) 자동차 인터페이스의 엑셀은 앞으로 가는 기능인데, 뒤로 가게 구현하면 LSP를 위반한 것이다. 느리더라도 앞으로 가야 한다.
- 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것, 다형성을 지원하기 위한 원칙, 인터페이스를 구현한 구현체는 믿고 사용하려면 이 원칙이 절대적으로 지켜져야 합니다.
- 즉, 인터페이스를 구현하는 구현체가 인터페이스의 규약을 깨뜨리면 안됩니다.
ISP 인터페이스 분리 원칙
Interface Segregation Principle
- 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 더 낫다는 원칙입니다.
- 예) 한 개의 자동차 인터페이스 → 운전 인터페이스, 정비 인터페이스 두 개로 분리를 한다면,
한 개의 사용자 클라이언트도 → 운전자 클라이언트, 정비사 클라이언트로 분리할 수 있습니다. - 분리를 하면 정비 인터페이스 자체가 변해도, 운전자 클라이언트에 영향을 주지 않습니다.
- 인터페이스가 더욱 명확해지고, 대체 가능성이 높아집니다.
DIP 의존관계 역전 원칙 ⭐
Dependency Inversion Principle
- 프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다" 는 원칙입니다. 의존성 주입은 이 원칙을 따르는 방법 중 하나입니다.
- 쉽게 이야기하면 구현 클래스에 의존하지 말고 인터페이스에 의존하라는 뜻입니다.
(클라이언트 코드가 구현 클래스를 직접 의존하지 말고, 인터페이스를 의존하라.
MemberService 클래스가 MemberRepository 인터페이스를 의존하고, MemoryMemberRepository 등 구현체는 몰라야 한다.) - MemberService 클래스가 구현체인 MemoryMemberRepository 를 의존한다면 의존성 방향은 MemberService → MemoryMemberRepository가 됩니다. (MemoryMemberRepository가 변경되면, MemberService도 수정해야 하기 때문입니다.)
- 하지만, MemberService 클래스가 인터페이스인 MemoryRepository를 의존한다면 의존성의 방향은 반대로 MemberService ← MemoryRepository 가 됩니다. MemoryRepository가 어떤 것으로 변경이 되어도, MemberService에는 영향을 끼치지 않고, MemberService 의 비지니스 로직에 따라 MemberRepository를 Jdbc 혹은 Mybatis 로 교체할 수 있기 때문에 의존관계의 역전 원칙이라고 부릅니다.
- 위의 OCP에서 작성한 다음의 코드는 인터페이스에도 의존하지만, 구현 클래스에도 동시에 의존하는 코드입니다.
MemberRepository m = new MemoryMemberRepository();
- 따라서 DIP를 위반한 코드입니다. 이 역시도 스프링 컨테이너의 개입이 필요합니다.
정리
- 스프링은 좋은 객체 지향 프로그램을 위한 프레임워크입니다.
- 객체 지향의 핵심은 다형성입니다.
- 하지만, 다형성만으로는 쉽게 부품을 갈아 끼우듯이 개발할 수 없습니다.
- 다형성으로는 OCP, DIP 원칙을 지킬 수 없습니다. (객체를 수정하면 클라이언트 코드도 함께 수정된다.)
- 뭔가가 더 필요하다. 👉🏻 스프링
객체 지향 설계와 스프링
- 스프링은 다음 기능을 제공함으로써 다형성 + OCP, DIP를 가능하게 지원합니다.
- DI 컨테이너 제공 : 객체의 라이프 사이클 관리
- DI(Dependency Injection) : 의존관계, 의존성 주입
- 이 기능을 사용함으로써 클라언트의 코드 변경 없이 확장이 가능합니다.
- 즉, 쉽게 부품을 교체하듯이 컴포넌트들을 교체할 수 있습니다.
스프링이 없던 시절
옛날에 어떤 개발자가 좋은 객체 지향 개발을 하기 위해 OCP, DIP 원칙을 지키며 개발을 하려고 하니 너무 할일이 많았습니다. 배보다 배꼽이 더 큰 격이었습니다. 따라서 프레임워크로 만들어 버린 것이 스프링입니다.
순수한 자바로 OCP, DIP 원칙을 지키며 개발을 하면 결국 스프링 프레임워크를 만들게 됩니다. (더 정확히는 DI 컨테이너)
스프링의 DI의 개념은 말로 설명하는 것보다 코드를 짜봐야 이해가 갑니다.
스프링이 DI를 어떻게 동작시키는지 코드로 다음 글에 작성하겠습니다
정리
- 모든 설계에 역할과 구현을 분리하자.
- 공연의 예시처럼, 애플리케이션 설계도 배역의 역할만 만들어두고 구현체인 배우는 언제든지 유연하게 변경할 수 있도록 설계하는 것이 좋은 객체 지향 설계입니다.
- 이상적으로 모든 설계에 인터페이스를 부여하도록 합시다.
실무 고민
하지만, 인터페이스를 도입하면 추상화라는 비용이 발생합니다.
여기서 말하는 비용이란, 개발자가 코드를 한번 더 열어봐야 합니다. 인터페이스만 보이기 때문에 구현 클래스가 무엇인지 한번 더 확인해야 하는 번거로움이 발생합니다.
기능을 확장할 가능성이 없다면, 구체 클래스를 직접 사용하도록 하고, 향후 꼭 필요할 때 리팩토링하여 인터페이스를 도입하는 것도 하나의 방법입니다.
출처
'Backend > Spring | SpringBoot' 카테고리의 다른 글
[Spring] 스프링 예제 - 주문과 할인2 (SRP, OCP, DIP 적용) (0) | 2023.07.28 |
---|---|
[Spring] 스프링 예제 - 주문과 할인1 (OCP와 DIP 미적용) (0) | 2023.07.28 |
[Spring] 스프링은 왜 만들었나요? 스프링과 객체지향 (0) | 2023.07.20 |
[SpringBoot] 웹 스코프 (Request 스코프) (0) | 2023.07.10 |
[SpringBoot] DL (ObjectProvider, JSR-330 Provider) (0) | 2023.07.10 |