본문 바로가기

Spring/Spring Basic

스프링 핵심 원리 이해 - 객체 지향 원리 적용

이 글은 김영한 님의 스프링 핵심 원리 - 기본 편 강의를 참고한 글입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

 

스프링 핵심 원리 - 기본편 강의 - 인프런

www.inflearn.com

 

스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런

김영한 | 스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보

www.inflearn.com

 

역할과 구현

객체 지향 프로그램을 설계할 때 역할과 구현으로 구분하는 단계는 굉장히 중요합니다.


예를 들어 자동차라는 역할이 있고 자동차 역할을 실제로 수행하는 소나타, 아반떼 등을 생각해 봅시다.
이때 자동차 역할을 추상화 인터페이스로 소나타, 아반떼를 구현 클래스가 됩니다. 여기서 자동차를 운전하는 운전자는 소나타, 아반떼와 같은 차 종류에 상관없이 운전할 수 있어야 합니다.

 

우리가 자동차 면허를 딸 때 소나타 전용 면허, 아반떼 전용 면허가 없는 것처럼 클라이언트는 구현에 의존하지 않고 역할에 의존해야 합니다. 애플리케이션을 설계할 때도 마찬가지로 특정 구현 클래스가 아닌 인터페이스에 의존해야 합니다.

 

구현 클래스에 의존한다면 구현 클래스가 A에서 B로 변경되어야 할 때 OCP 원칙을 지키지 못하고 여러 곳에서 코드의 수정이 필요합니다. 
 애플리케이션을 설계할 때 역할(인터페이스)과 구현(구현 클래스)을 분리하여 생각하다 보면 자연스럽게 OCP와 DIP 원칙을 지키는 코드를 짤 수 있습니다.


물론 DI 개념까지 적용되어야 하지만 시작은 역할과 구현을 분리하는 아이디어입니다.

 

추상화에 의존

역할과 구현으로 구분했다면 우리는 역할에 의존하도록 설계해야 합니다. 확장에 열려 있고 변경에는 닫혀 있어야 한다는 개방 - 폐쇄 원칙을 따르기 위해서는 구체적인 구현이 아닌 역할에 의존해야 합니다.

 

기능이 확장되었을 때 의존하고 있는 인터페이스를 따르는 새로운 클래스를 만들어 대응하고 변경 사항이 생겼더라도 역할인 인터페이스에 의존하면 변경에 자유롭습니다.

 

스프링의 등장 배경

스프링이 등장한 배경도 SOLID 원칙을 지키는 즉 좋은 객체 지향 프로그래밍을 위함에 있습니다. 순수하게 자바로 OCP, DIP 원칙들을 지키면서 개발하다 보면, 결국 스프링 프레임워크를 만들게 됩니다. 정확히는 DI 컨테이너를 만들게 됩니다.

 

스프링 DI 컨테이너로 의존성을 주입하고 의존관계를 관리할 수 있는 이유 역시 객체 지향 프로그래밍을 따르기 위함입니다. 

 

구현할 비즈니스 요구사항

스프링을 다루기 전에 간단한 비즈니스 요구사항을 설계하고 코드로 구현해 보며 객체 지향의 핵심인 "다형성"에 대해 살펴봅시다.

앞으로 설계할 비즈니스 요구사항은 아래와 같습니다.


1. 회원

- 회원을 가입하고 조회할 수 있습니다.
- 회원은 일반과 VIP 두 가지 등급이 있습니다.
- 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있습니다. (미확정)


2. 주문과 할인 정책

- 회원은 상품을 주문할 수 있습니다.
- 회원 등급에 따라 할인 정책을 적용할 수 있습니다.
- 할인 정책은 모든 VIP는 1000원을 할인해 주는 고정 금액 할인을 적용합니다.
- 할인 정책은 변경 가능성이 높습니다. 회사의 기본 할인 정책이 정해지지 않았습니다. 오픈 직전까지 고민을 미루고 싶고 최악의 경우 할인을 적용하지 않을 수도 있습니다. (미확정)
 
회원 데이터를 저장하는 DB가 아직 정해지지 않았고 할인 정책도 변동 가능성이 큰 상황입니다.


하지만 '데이터를 저장한다', '할인을 한다'와 같은 역할은 존재하기 때문에 인터페이스를 만들고 구현체를 언제든지 갈아 끼울 수 있도록 설계하면 됩니다.

 

회원 도메인과 주문 도메인 설계 

회원 도메인과 주문 도메인을 설계한 그림을 살펴봅시다.

 

회원 도메인을 설계할 때 회원 저장소(MemberRepository) 역할을 하는 인터페이스를 만들어두고 DB가 정해지기 전까지 메모리에 회원 정보를 저장하는 메모리 회원 저장소 구현체를 구현해 사용하려 합니다. 이후 DB가 결정되면 메모리 회원 저장소가 아닌 DB 회원 저장소로 교체할 계획입니다.
 
이때 회원 서비스는 구현체인 메모리 회원 저장소에 의존하지 않고 추상화된 회원 저장소에 의존하도록 하여 회원 저장소의 구현체가 변경되더라도 회원 서비스에 변경이 발생하지 않도록 설계했습니다. 회원 서비스 구현체인 MemberServiceImpl 클래스는 구체 타입인 MemoryMemberReposiory 타입에 의존하지 않고 회원 저장소 인터페이스인 MemberRepository 타입에 의존하고 있습니다.

 

추상화에 의존하기 때문에 MemberRespository 인터페이스를 따르는 다른 구현체에도 대응할 수 있게 됩니다.
 
또한 회원 서비스에서 회원 저장소 객체를 외부에서 주입받도록 구현하여 추상화에만 의존하고 객체를 생성하는 책임을 외부 클래스(AppConfig.class)를 만들어 분리했습니다. 객체 생성의 책임을 AppConfig.class 옮겨서 애플리케이션을 구성 영역과 사용 영역으로 관심사를 분리했습니다.
 
AppConfig
클래스와 의존성 주입에 대해서는 주문 도메인 설계를 통해 살펴봅시다.

 

주문 도메인을 보면 할인에 대해서는 할인 정책 역할(DiscountPolicy)이 모든 책임을 가지고 있습니다.
따라서 주문 서비스에서 할인에 관한 내용은 할인 정책 역할에 물어보게 됩니다.


할인 정책에 변경 사항이 생긴다면 할인 정책 역할만 수정하면 됩니다. 단일 책임 원칙을 잘 따른 설계입니다.
 
만약 처음 기획에서 정액 할인 정책으로 VIP 고객에게 항상 1000원을 할인하기로 했다가 VIP 고객에게 구매 금액에 10%를 할인하는 정률 할인 정책으로 변했다고 가정해 봅시다. 우리는 정액 할인 정책에 의존하지 않고 할인 정책 역할(인터페이스)에 의존하도록 구현했기 때문에 변경에 큰 수정 없이 대응할 수 있습니다.

 

할인 정채 역할 인터페이스를 채택한 정률 할인 정책 클래스를 만들고 구현 객체를 주입하는 AppConfig에서 정액 할인 정책 객체를 주입하던 코드를 정률 할인 정책 객체를 주입하도록 수정하기만 하면 됩니다.

 

이때 주문 서비스는 할인 정책 역할 인터페이스에 의존하기 때문에 주문 서비스에 변경이 발생하지 않습니다.

 

AppConfig을 통한 관심사 분리

AppConfig 클래스에서 객체를 생성하고 의존성을 주입하도록 관심사를 분리해서 새로운 정률 할인 정책 구현체를 만들고 AppConfig 클래스에서 의존성 주입하는 코드만 수정하면 할인 정책 변경에 대응할 수 있었습니다.
 
여기서 AppConfig 클래스의 결정적인 역할이 무엇일까요?


바로 의존성을 주입하여 객체 생성에 대한 관심사를 일반 클래스로부터 분리시키는 역할입니다.
AppConfig 클래스에서 구체적인 구현체를 선택하고 애플리케이션이 어떻게 동작해야 할지 전체 구성을 책임집니다.
 
AppConfig의 등장으로 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성하는 영역으로 분리됩니다.

 

AppConfig 통해 모든 구현체는 구성 영역에서 사용 영역으로 주입되고 사용 영역의 클래스끼리는 구체 타입이 아닌 추상화된 인터페이스에 의존하도록 설계할 있습니다.

 

할인 정책이 변경되더라도 구성 영역인 AppConfig만 영향을 받고, 사용 영역은 전혀 영향을 받지 않습니다.

 

AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 성격의 클래스를 스프링에서는 IoC 컨테이너 또는 DI 컨테이너라 합니다. AppConfig가 등장한 이후에 구현 객체(사용 영역)는 자신의 로직을 실행하는 역할만 담당합니다. 프로그램의 제어 흐름은 AppConfig에서 가져갑니다. 클래스들은 인터페이스에 의존할 뿐 어떤 구현체가 들어올지는 AppConfig가 결정합니다.

 

이렇게 프로그램의 제어 흐름을 직접 내부에서 제어하지 않고 외부에서 관리하는 것을 제어의 역전(IoC)이라 합니다.

 

제어의 역전(IoC)과 의존성 주입(DI)

제어의 역전(IoC)은 역할과 구현을 분리하여 변경에 유연한 코드를 구현하게 만듭니다.


AppConfig가 객체를 생성하고 주입하는 동작을 의존성 주입(DI)이라 합니다. 제어의 역전과 의존성 주입의 관계가 헷갈릴 수 있습니다.
 제어의 역전 원칙을 구현하기 위한 여러 가지 방법 중 하나가 의존성 주입입니다.


프로그램의 제어 흐름의 외부에서 관리하기 위해 구현체를 선택하고 구현체를 프로그램에 주입하기 위해서는 의존성 주입 방식을 사용하게 됩니다. 의존성 주입은 의존관계 주입이라고 말하기도 합니다. 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있습니다.

 

이때 정적인 클래스 의존관계는 인터페이스끼리의 의존 관계를 뜻하고 동적인 객체 인스턴스 의존관계는 Config 성격의 클래스에서 프로그램 실행 이후 결정되는 구현체끼리의 관계를 뜻합니다.
 

정적인 클래스 의존관계
애플리케이션 실행 시점에 실제 생성된 동적인 객체 인스턴스 의존 관계

구현 코드

지금까지 설계한 회원 도메인과 주문 도메인을 구현한 코드입니다.

https://github.com/hongjunehuke/SpringBasic

 

GitHub - hongjunehuke/SpringBasic

github.com

 

'Spring > Spring Basic' 카테고리의 다른 글

싱글톤 컨테이너  (1) 2024.10.15
스프링 빈 조회  (1) 2024.10.15
스프링 컨테이너와 스프링 빈  (0) 2024.10.15
자동 컴포넌트 스캔 vs 수동 컴포넌트 스캔  (1) 2024.09.28