[Spring 핵심원리 기본편] 객체 지향 원리 적용 (->스프링 컨테이너)
by mini_min
새로운 할인 정책이 개발되어야할 때 발생하는 문제를 막는다. 이를 위해 관심사를 분리해보자.
새로운 정책 개발
새로운 할인 정책이 개발 되었다. 할인 정책을 변경하려면, 클라이언트인 OrderServiceImpl 을 변경해야한다.
문제점
역할 구현을 충실히 분리했는가? o
다형성도 활용하고 인터페이스와 구현체를 분리했는가? o
OCP, DIP 와 같은 객체 지향 원칙을 준수했는가? x
= 클래스 의존관계를 분석해보면, 추상 뿐만 아니라 구체 클래스에도 의존하고 있다. 인터페이스 뿐만 아니라, 구체 클래스에도 함께 의존하고 있다.
추상에만 의존해야 하는데, 구체에도 의존하고 있다.
즉, DIP 를 위반하고 있다.
FixDiscount 를 RateDiscount 로 바꾸는 순간, 클라이언트 OrderServiceImpl 도 변경해야한다.
(OCP 또한 위반)
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
discountPolicy 라는 인터페이스 뿐만 아니라, 뒤에 구현 클래스가 오고 있다.
문제 해결
추상(인터페이스)에만 의존하도록 변경한다!
아래처럼 DiscountPolicy (인터페이스) 만 불러온다.
오잉? 그런데 구현체가 오지 않아도 실행이 될까?
= 당연히 아래처럼만 변경하면 NullPointerException 이 발생한다.
✨ 클라이언트인 OrderServiceImpl 에 DiscountPolicy 구현 객체를 대신!!! 생성하고 주입해주면 된다....!
private final MemberRepository memberRepository = new MemoryMemberRepository();
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
관심사의 분리
애플리케이션을 하나의 공연이라고 생각했을 때, 배우들의 역할을 정하는 사람은 누구일까? 바로 공연 기획자다. 배우들은 본인들의 '역할'을 수행하는 것에만 집중해야한다. 공연을 구성하고 배우를 섭외하고 지정하는 책임은 공연 기획자가 해야한다. 즉, 공연 기획자를 만들고 배우와 공연 기획자의 책임을 확실히 분리한다.
AppConfig 의 등장 ✨✨✨
애플리케이션의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스인 AppConfig 를 생성한다.
AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다. (필요한 구현 객체를 다 생성해준다.)
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {
//생성자에 MemoryMemberRepository 가 들어간다.
private MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
public DiscountPolicy discountPolicy() {
//return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
}
각 클래스에 생성자를 만든다.
이제, 인터페이스에만 의존하게 되었다!!!
생성자를 통해서 어떤 구현 객체가 주입될지는 오직 외부(AppConfig)에서만 결정된다!
이제 의존 관계에 대한 고민은 X, 실행에만 집중한다!
package hello.core.member;
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
//AppConfig 에서 MemoryMemberRepository 가 들어와서
//생성자에서 받는다!
//생성자 주입 방식이다.
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
객체 생성과 연결은 AppConfig 가 담당한다.
MemberServiceImpl은 MemberRepository 인 추상에만 의존하면 된다. (구체 클래스는 몰라도 된다.)
객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.
정리
AppConfig 를 통해서 관심사를 확실하게 분리했다.
AppConfig 는 공연 기획자라고 생각하자.
ServiceImpl 은 기능을 실행하는 책임만 지면 된다.
좋은 객체 지향 설계의 5가지 원칙의 적용
SRP : 단일 책임 원칙
하나의 클래스는 하나의 책임만 가진다.
- SRP 단일 책임 원칙을 따르면서 관심사를 분리했다.
- 구현 객체를 생성하고 연결하는 책임은 AppConfig 가 담당한다.
- 클라이언트 객체는 실행하는 책임만 담당한다.
DIP : 의존 관계 역전 원칙
- AppConfig 가 DiscountPolicy 추상화 인터페이스에 들어가는 구현 클래스 코드를 클라이언트 대신 생성해서 클라이언트 코드에 의존관계를 주입했다. 이렇게 해서 DIP 원칙을 따르면서 기존 문제를 해결했다.
OCP : 확장에는 열리고 변경에는 닫혀있어야한다.
- 다형성을 사용하고 클라이언트 DIP 를 지켰다.
- 애플리케이션 사용 영역과 구성 영역으로 나뉜다.
IoC, DI, 그리고 컨테이너
제어의 역전
구현 객체가 프로그램의 제어 흐름을 스스로 조종했다. 이는 개발자 입장에서는 자연스러운 흐름이다. 반면 AppConfig 가 등장한 후에 구현 객체는 실행 역할만을 담당한다. 그리고 프로그램의 제어 흐름은 이제 AppConfig 가 가져간다.
= 프로그램에 대한 제어 흐름의 권한을 AppConfig 가 모두 가져간다.
= 외부에서 관리하는 것이 제어의 역전이다.
프레임워크 vs 라이브러리
프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크가 맞다.
반면에 내가 작성한 코드가 직접 제어를 담당하면 그건 라이브러리다...!
의존관계 주입 DI
OrderServiceImpl 은 DiscountPolicy 만 알고, 그 안에 어떤 구현 클래스가 있는지 알지 못한다.
의존 관계는 "정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계" 둘을 분리해서 생각해야한다.
정적인 클래스 의존관계
클래스가 사용하는 import 만 보고 어떤 의존관계인지 파악할 수 있다. 정적인 의존관계는 애플리케이션을 실행하지 않아도 분석 가능하다. 그런데, 이러한 클래스 의존관계 만으로는 실제 어떤 객체가 OrderServiceImpl 에 주입되는지 알 수 없다.
동적인 클래스 의존관계
애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계다.
✨✨✨ 실행시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입이라고 한다.
IoC 컨테이너, DI 컨테이너
AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 해당 컨테이너라고 말한다. 또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다.
스프링으로 전환하기
AppConfig 에는 @Configuration , @Bean 애노테이션 붙이기
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
//생성자에 MemoryMemberRepository 가 들어간다.
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
}
스프링 컨테이너를 사용하려면, ApplicationContext 를 호출한다.
//memberService 객체를 찾을거야. 타입은 MemberService 야! (라는 뜻)
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class OrderApp {
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
// OrderService orderService = appConfig.orderService();
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
Long memberId = 1L;
Member member = new Member(memberId, "홍길동", Grade.VIP);
memberService.join(member); // 회원가입
Order order = orderService.createOrder(memberId, "itemA", 10000);
System.out.println("order = " + order);
System.out.println("할인 금액 = " + order.calculatePrice());
}
}
스프링 컨테이너 ✨
ApplicationContext 를 스프링 컨테이너라고 한다.
기존에는 개발자가 AppConfig 를 사용해서 직접 객체를 생성하고 DI 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다. 스프링 컨테이너는 @Configuration 이 붙은 AppConfig 를 설정 정보로 사용한다. 여기서 @Bean 이 붙은 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 한다.
스프링 빈은 메서드 이름과 동일한다.
스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾을 때, applicationContext.getBean() 메서드로 찾을 수 있다.
= 기존에는 개발자가 자바 코드로 직접 모든 것을 했다면, 스프링 컨테이너에 객체를 스프링 빈으로 등록하고 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경되었다.
'Spring' 카테고리의 다른 글
[Spring 핵심원리 기본편] 웹 애플리케이션과 싱글톤 (0) | 2023.05.21 |
---|---|
[Spring 핵심원리 기본편] 스프링 컨테이너와 스프링 빈 (빈 조회) (0) | 2023.05.21 |
[Spring 핵심원리 기본편] 비즈니스 요구사항과 회원 도메인 설계/개발 (0) | 2023.05.20 |
[Spring 핵심원리 기본편] 객체 지향 설계와 스프링 2 (0) | 2023.05.20 |
[Spring 핵심원리 기본편] 객체 지향 설계와 스프링 (0) | 2023.05.16 |
블로그의 정보
개발자 미니민의 개발로그
mini_min