1. 요구사항과 설계
회원
- 회원을 가입하고 조회할 수 있다.
- 회원은 일반과 VIP 두 가지 등급이 있다.
- 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
주문과 할인 정책
- 회원은 상품을 주문할 수 있다.
- 회원 등급에 따라 할인 정책을 적용할 수 있다.
- 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
- 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루 고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)
2. 회원 도메인 설계
3. 주문과 할인 도메인 설계
4. 관심사의 분리
DIP와 OCP를 위해 애플리케이션의 전체 동작 방식을 구성하는 AppConfig 도입
Before
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
문제점
1. 클라이언트(OrderServiceImpl)가 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다.
> DIP 위반
2. 기능을 확장해서 변경하면(FixDiscountPolicy > RateDiscountPolicy) 클라이언트 코드에 영향을 준다.
> OCP 위반
After
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
1. AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
2. AppConfig는 생성한 객체 인스턴스의 참조를 생성자를 통해서 주입해준다.
3. 클라이언트(OrderServiceImpl)가 추상(인터페이스)에만 의존하며 실행에만 집중하면 된다.
> DIP 만족
4. 할인 정책을 변경해도, 클라이언트 코드를 포함해서 사용 영역의 어떤 코드도 변경할 필요가 없다.
> OCP 만족
5. IOC, DI, 그리고 컨테이너
제어의 역전 IOC(Inversion of Control)
- AppConfig가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당한다.
- 프로그램에 대한 제어 흐름에 대한 권한은 모두 AppConfig가 가지고 있다.
- 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다.
의존관계 주입 DI(Dependency Injection)
- 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입 이라 한다.
- 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
- 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경 할 수 있다.
DI컨테이너
- AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 DI컨테이너라 한다.
- IOC컨테이너, 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다.
6. 스프링으로 전환하기
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
public class OrderApp {
public static void main(String[] args) {
// Before
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
// OrderService orderService = appConfig.orderService();
// After
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, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
System.out.println("order = " + order);
}
}
- ApplicationContext를 스프링 컨테이너라 한다.
- 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정(구성) 정보로 사용한다.
- @Bean이 라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
- 스프링 빈은 @Bean이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. (memberService, orderService)
- 스프링 빈은 applicationContext.getBean()메서드를 사용해서 찾을 수 있다.
'Spring' 카테고리의 다른 글
[스프링 핵심 원리] 06. 의존관계 자동 주입 (0) | 2025.01.21 |
---|---|
[스프링 핵심 원리] 05. 컴포넌트 스캔 (0) | 2025.01.20 |
[스프링 핵심 원리] 04. 싱글톤 컨테이너 (0) | 2025.01.20 |
[스프링 핵심 원리] 03. 스프링 컨테이너와 스프링 빈 (0) | 2025.01.19 |
[스프링 핵심 원리] 01. 객체 지향 설계와 스프링 (0) | 2025.01.15 |