Spring

[스프링 핵심 원리] 02. 객체 지향 설계 적용 예제

binning 2025. 1. 16. 22:02

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()메서드를 사용해서 찾을 수 있다.