(인프런) 김영한님의 스프링 핵심 원리-기본편을 공부하고 리뷰한 글입니다.
8. Ioc, DI, 그리고 컨테이너
1. 제어의 역전: Ioc(Inversion of Control)
프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)라 한다.
1) 기존 프로그램
기존 프로그램은 구현 객체가 프로그램 제어 흐름을 스스로 제어했다.
- 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성, 연결, 실행 했다.
2) AppConfig 등장
AppConfig의 등장 후, 구현 객체는 자신의 로직을 실행하는 역할만 담당하고 프로그램의 제어 흐름은 AppConfig가 가져간다.
- OrderServiceImpl은 필요한 인터페이스를 호출하지만 어떤 구현 객체들이 실행될지 모른다.
- OrderServiceImpl도 AppConfig가 생성한다.
- AppConfig가 OrderServiceImpl이 아닌 OrderService 인터페이스의 다른 구현 객체를 생성할 수도 있다.
2. 프레임워크 vs 라이브러리
프레임워크와 라이브러리를 구분할 때 제어의 역전이 중요하다!
1) 프레임워크(내가 제어 흐름 갖지 X)
프레임워크는 내가 작성한 코드를 제어하고 대신 실행한다.
- e.g. JUnit : MemberServiceTest의 @Test join() 와 같은 테스트를 실행하고 제어하는 권한은 JUnit, 테스트 프레임워크가 갖고 있다.
2) 라이브러리(내가 제어 흐름 갖고 O)
내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 라이브러리다.
3. 의존관계 주입: DI(Dependency Injection)
의존 관계는 정적인 클래스 의존 관계 & 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 2개를 분리해서 생각해야 한다.
- OrderServiceImpl은 DiscountPolicy 인터페이스에 의존하지만 실제 어떤 구현 객체가 사용될지는 모른다.
4. 정적인 클래스 의존관계
클래스가 사용하는 import 코드만 보고 의존 관계를 쉽게 판단할 수 있다.
정적인 클래스 의존관계는 애플리케이션을 실행하지 않아도 분석할 수 있다.
(참고) Intellij 에서 Diagrams > Show Diagram > Show Dependency 의존 관계 볼 수 O
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
public class OrderServiceImpl implements OrderService{
...
}
- OrderServiceImpl은 MemberRepository, DiscountPolicy 에 의존함을 알 수 O
- 이 클래스 의존 관계만으로 실제 어떤 객체가 OrderServiceImpol에 주입될 지 알 수 X(FixDiscountPolicy 일지, RateDiscountPolicy 일지 알 수 X)
5. 클래스 다이어그램
6. 동적인 객체 인스턴스 의존 관계
애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계다.
7. 객체 다이어그램
<의존관계 주입>
의존관계 주입은 애플리케이션실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존 관계가 연결된는 것을 말한다.
- 객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결된다.
- 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
- 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다. (중요!)
8. Ioc 컨테이너, DI 컨테이너
AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 DI 컨테이너(Ioc컨테이너)라고 한다. (어셈블러, 오브젝트 팩토리 등으로 불리기도 함)
9. 스프링으로 전환하기
지금까지는 순수 자바 코드만으로 DI를 적용했지만 이제는 스프링을 사용해보자.
1. AppConfig 스프링 기반으로 변경
AppConfig 클래스 앞에 @Configuration 을 추가하고 모든 메서드 앞에 @Bean을 추가한다.
package hello.core;
import hello.core.discount.DiscountPolicy;
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 {
// MemberService 역할 스프링 빈 등록
@Bean
public MemberService memberService() {
// 생성자 주입
return new MemberServiceImpl(memberRepository());
}
// MemberRepository 역할 스프링 빈 등록
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository(); // 메모리 회원 저장소로
}
// OrderService 역할 스프링 빈 등록
@Bean
public OrderService orderService() {
// 생성자 주입
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
// DiscountPolicy 역할 스프링 빈 등록
@Bean
public DiscountPolicy discountPolicy() {
//return new FixDiscountPolicy(); // 고정 할인 정책으로
return new RateDiscountPolicy(); // 정률 할인 정책으로 변경
}
}
- @Configuration: 설정 파일을 만들기 위한 어노테이션
- @Bean: 스프링 빈으로 등록
2. MemberApp에 스프링 컨테이너 적용
1) 기존
주석처리된 2줄처럼 AppConfig 객체를 생성하고 appConfig의 memberService() 메서드를 호출해 memberService 객체를 직접 조회했다.
2) 스프링 컨테이너 적용
AppConfig의 설정 정보를 갖고 스프링 빈을 스프링 컨테이너에 넣어서 관리한다.
스프링 컨테이너를 통해 스프링 빈(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 org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MemberApp {
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
// AppConfig에 있는 설정 정보를 토대로 스프링 컨테이너에 넣어서 관리
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
// 스프링 컨테이너를 통해 memberService를 찾음(이름=메서드명, 타입)
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("findMember = " + findMember.getName());
}
}
- ApplicationContext: 스프링 컨테이너
- applicaiotn.getBean(): 스프링 컨테이너에서 해당 (이름, 타입)인 스프링 빈을 조회
3) MemberApp 실행하여 스프링 컨테이너에 스프링 빈 등록된 결과
3. OrderApp에 스프링 컨테이너 적용
1) 기존
주석처리된 3줄처럼 AppConfig 객체를 생성하고 appConfig의 memberService(), orderService() 메서드를 호출해 memberService, orderService 객체를 직접 조회했다.
2) 스프링 컨테이너 적용
AppConfig의 설정 정보를 갖고 스프링 빈을 스프링 컨테이너에 넣어서 관리한다.
스프링 컨테이너를 통해 스프링 빈(memberService, orderService) 를 조회해서 사용한다.
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();
// AppConfig에 있는 설정 정보를 토대로 스프링 컨테이너에 넣어서 관리
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
// 스프링 컨테이너를 통해 memberService, orderService를 찾음(이름=메서드명, 타입)
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", 20000);
System.out.println("order = " + order); // order.toString() 출력됨
//System.out.println("order.calculatePrice = " + order.calculatePrice()); // 할인된 최종 가격
}
}
- ApplicationContext: 스프링 컨테이너
- applicaiotn.getBean(): 스프링 컨테이너에서 해당 (이름, 타입)인 스프링 빈을 조회
3) OrderApp 실행하여 스프링 컨테이너에 스프링 빈 등록된 결과
4. 스프링 컨테이너
스프링 컨테이너는 스프링 빈을 생성하고 관리하는 컨테이너로 Application.Context를 스프링 컨테이너라 한다.
1) @Configuration 이 붙은 AppConfig를 설정(구성) 정보로 사용한다.
2) @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 스프링 빈으로 등록한다.
- 스프링 빈은 default로 @Bean이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. (memberService, orderService)
// @Bean이 붙은 메서드명 = memberService
@Bean
public MemberService memberService() {
// 생성자 주입
return new MemberServiceImpl(memberRepository());
}
// 스프링 빈의 이름 = memberService
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
3) applicationContext.getBean() 메서드로 스프링 컨테이너를 통해 필요한 스프링 빈(객체)를 조회한다.
<정리>
기존에는 개발자가 AppConfig를 사용해 직접 객체를 생성하고 DI를 했지만,
스프링 컨테이너에 객체를 스프링 빈으로 등록하고 스프링 컨테이너에서 스프링 빈을 찾아 사용하도록 변경하였다.
Q. 코드가 약간 더 복잡해진 것 같은데, 스프링 컨테이너를 사용하면 어떤 이점이 있을까?
싱글톤을 직접 구현하지 않아도 된다. 더 자세한 내용은 여기