💡지금은 스프링 없는 순수한 자바로만 개발을 진행한다.
➡️ 회원
➡️ 주문과 할인 정책
회원을 메모리가 아닌 실제 DB에서 조회하고, 정률 할인 정책(주문 금액에 따라 % 할인)을 지원해도 주문 서비스를 변경하지 않아도 된다.
즉, 협력 관계를 그대로 재사용 할 수 있다.
코드는 깃허브에 기록한 것 참고하기! https://github.com/MinjiSeo16/inflearnSpring/tree/main/core
이번에는 주문한 금액의 %를 할인해주는 새로운 정률 할인 정책을 추가하자
할인 정책을 변경하려면 OrderServiceTmpl 코드를 고쳐야 한다. (주석은 이전 코드)
public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
인터페이스에만 의존하도록 코드를 변경한다.
public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
}
그런데 구현체가 없는데 코드는 실행할 수 있을까?
⚠️ 실제 실행해보면 NPE(null pointer exception)이 발생한다.
이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현체를 대신 생성하고 주입해주어야 한다.
애플리케이션을 하나의 공연이라 생각해보자. 각각의 인터페이스를 배우 역할이라 생각하자. 그런데 로미오 역할(인터페이스)을 하는 디카프리오(구현체,배우)가 줄리엣 역할(인터페이스)을 하는 여자 주인공(구현체,배우)를 직접 초빙한다면 디카프리오는 공연도 하고 초빙도 하는 다양한 책임을 가지게 된다.
배우는 본인의 역할인 배역에만 집중해야함 -> AppConfig 등장
애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스를 만들자!
🖥️ AppConfig
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy());
}
AppConfig는 memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl을 생성하면서 생성자로 전달한다. 아래는 MemberServiceImpl와 OrderServiceImpl에 생성자 코드를 추가한 것이다.
🖥️ MemberServiceImpl
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
MemberServiceImpl는 MemoryMemberRepository를 의존하지 않음MemberServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정됨MemberServiceImpl은 의존관계 고민은 외부에 맡기고 오직 실행에만 집중하면 됨🖥️ OrderServiceImpl
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;
}
...
MemoryMemberRepository, FixDiscountPolicy 객체의 의존관계가 주입됨 ⚠️ 위의 AppConfig를 보면 중복이 있고, 역할에 따른 구현이 잘 안보인다.
또한 두 개의 서로 다른 MemoryMemberRepository 인스턴스가 만들어져서
memberService와 orderService가 같은 저장소를 공유하지 않게 되는 문제가 발생할 수 있다.
🖥️ AppConfig
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();
}
}
new MemoryMemberRepository() 부분이 중복 제거되어 다른 구현체로 변경할 때 한 부분만 변경하면 됨다시 정액 할인 정책을 %할인 정책으로 변경해보자! 어떤 부분이 변경될까?

🖥️ AppConfig
public class AppConfig {
...
public DiscountPolicy discountPolicy() {
//return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
➡️ SRP 단일 책임 원칙 - 한 클래스는 하나의 책임만
➡️ DIP 의존관계 역전 원칙 - 추상화에 의존, 구체화에 의존 X
➡️ OCP - 확장에는 열려있으나 변경에는 닫혀있음
FixDiscountPolicy -> RateDiscountPolicy로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 됨📌 제어의 역전 IoC(Inversion of Control)
프로그램의 제어 흐름을 직접 제어 하는 것이 아니라 외부에서 관리하는 것
AppConfig가 프로그램에 대한 제어 흐름의 권환을 모두 가짐OrderServiceImpl는 인터페이스를 호출하지만 어떤 구현 객체들이 실행될지 모르고, OrderServiceImpl가 아닌 OrderService의 다른 구현 객체를 실행할 수도 있음📌 의존관계 주입 DI(Dependency Injection)
애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것
📌 IoC 컨테이너, DI 컨테이너
AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것
💡이제부터 스프링을 사용해보자
🖥️ AppConfig
@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();
return new RateDiscountPolicy();
}
}
🖥️ MemberApp
public class MemberApp {
public static void main(String[] args){
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("find member = " + findMember.getName());
}
}
ApplicationContext를 스프링 컨테이너라고 함@Configuration이 붙은 AppConfig를 설정 정보로 사용하고, @Bean이라 적힌 메서드를 모두 호출해 반환된 객체를 스프링 컨테이너에 등록하며 이것을 스프링 빈이라고 함applicationContext.getBean() 메서드를 사용하여 스프링 빈을 찾음코드가 더 복잡해진 거 같은데, 스프링 컨테이너를 사용하면 어떤 장점이 있을까? 다음 글에서 다루어 볼 것이다.