[Spring] 핵심 원리 기본 (3) - 객체 지향 원리 적용
새로운 할인 정책 개발
기존의 고정 할인 금액 정책에서 정률 할인 금액 정책으로 변경하자는 요구사항이 들어왔다. 우리는 역할과 구현을 잘 분리해놨기 때문에 DiscountPolicy의 구현체로 RateDiscountPolicy를 추가하고 적용해주면 된다.
새로운 할인 정책 적용 방식의 문제점
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
위 코드는 역할과 구현을 분리하고 다형성을 활용했지만 DIP, OCP를 잘 지키지 못한 코드이다.'
1. 클라이언트 코드가 역할인 DiscountPolicy와 함께 구현체인 RateDiscountPolicy까지 의존하고 있다. → DIP 위반
2. 기능을 확장하기 위해서는 클라이언트 코드를 변경해야 한다. → OCP 위반
의존 관계
기대했던 것은 OrderServiceImpl이 역할에만 의존하는 것이였지만 사실 FixDiscountPolicy까지 의존하고 있다.
정책을 변경하게 되면 기존의 구체 클래스에 대한 의존 관계를 끊고 새로운 구체 클래스와 의존 관계를 맺게 된다.
문제 해결
DIP를 위반하지 않도록 클라이언트 코드가 인터페이스에만 의존하면 된다.
OrderServiceImpl
OrderServiceImpl을 역할에만 의존하도록 변경했다. → DIP 완성
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
AppConfig
따라서 대신 구현체를 생성해서 넣어주는 설정 정보 클래스가 필요하다. 이 클래스에서는 각 클래스의 생성자를 통해 생성한 구현체의 참조값을 전달해준다. 이렇게 생성자를 통해 의존 관계를 주입하는 것을 생성자 주입이라고 한다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
설계 변경 후
- OrderServiceImpl은 어떤 구현 객체가 들어올지 알 수 없다.
- OrderServiceImpl은 의존 관계를 설정할 필요없이 실행에만 집중하면 된다. → SRP 완성
- 생성자를 통해 어떤 구현 객체를 주입할지는 오직 AppConfig에서만 결정된다.
- 기능을 확장하려면 AppConfig 설정 정보만 변경하면 되므로 클라이언트 코드를 변경할 필요가 없다. → OCP 완성
- 관심사 분리 : 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확하게 분리되었다!
AppConfig 리팩터링
public class AppConfig {
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
public MemberService memberService() {
return new MemberService(memberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
}
- 기존의 AppConfig는 OrderService와 MemberService에서 각각 MemoryMemberRepository를 생성해주었다. (중복)
- 리팩토링 된 AppConfig의 경우 만약 할인 정책을 변경하게 된다면 코드를 한군데만 고치면 된다.
- 또한 역할과 구현 클래스가 한눈에 들어와 애플리케이션의 전체 구성이 어떻게 되어있는지 쉽게 파악할 수 있다.
사용 영역과 구성 영역의 분리
AppConfig를 추가해서 이제 애플리케이션이 크게 사용 영역과 구성 영역으로 분리되었다. 정책을 변경해도 구성 영역만 영향을 받고 사용 영역은 전혀 영향을 받지 않는다.
IoC, DI, 그리고 컨테이너
제어의 역전 IoC(Inversion of Control)
- 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고 연결하고 실행했다. 구현 객체가 제어 흐름을 스스로 조종했다.
- 반면 AppConfig가 등장하면서 구현 객체는 자신의 로직을 실행하는 역할만 담당하게 된다. 제어 흐름은 이제 AppConfig가 가져간다.
- 이렇게 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전이라고 한다.
의존관계 주입 DI(Dependency Injection)
- 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 설정되는 것을 의존관계 주입이라고 한다.
- 객체 인스턴스를 생성하고 그 참조값을 전달해서 연결된다.
- 의존 관계 주입을 사용하면 클라이언트 코드를 변경하지 않고 호출하는 인스턴스를 변경할 수 있다.
- 이를 다르게 말하면 의존 관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고 동적인 객체 인스턴스의 의존 관계를 변경할 수 있다는 것이다.
IoC 컨테이너, DI 컨테이너
- AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC 컨테이너, DI 컨테이너라고 한다.
- 최근에는 의존 관계 주입에 초점을 맞추어 DI 컨테이너라고 한다.
- 어셈블러, 오브젝트 팩토리 등으로 불리기도 한다.
스프링 사용하기
// ApplicationContext : 스프링 컨테이너
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
// getBean("methodName", instanceType.class)
// key는 메서드 이름, value는 인스턴스로 컨테이너에 빈으로 등록
applicationContext.getBean("memberService", MemberService.class);
- 기존에는 개발자가 AppConfig를 사용해서 객체를 직접 생성하고 의존 관계를 주입했지만 이제는 스프링 컨테이너를 통해 사용한다.
- 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정정보로 사용한다. (빈으로 등록됨)
- 또한 AppConfig의 @Bean이 붙은 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다.
- 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 한다.
- 스프링 컨테이너에는 스프링 빈의 이름과 객체가 함께 저장되는데 이 때 스프링 빈의 이름은 메서드의 이름과 같다.
- 스프링 빈을 조회하기 위해서는 applicationContext.getBean() 메서드를 사용한다.
- 이제부터는 스프링 컨테이너에 객체를 빈으로 등록해놓고 스프링 컨테이너를 통해 빈을 사용하도록 변경되었다.
강의 링크
스프링 핵심 원리 - 기본편 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...
www.inflearn.com