- 흐름
- java로만 이용해서 스프링의 container가 작동하는 원리를 보여준다.
- 이후 실제 SpringContainer의 작동방식을 보여준다.
1. 문제점
- DIP, OCP 위반
- DIP : 인터페이스에만 의존해야하는데 구현체에도 의존을 했었다.
- OCP : 기능을 확장하게 되면 clientCode에 영향을 준다.
2. 관심사의 분리
- 역할과 실제배역은 분리되어야 한다. 역할 = 인터페이스, 실제배역 = 구현체
- 이를 확실히 구분해주기 위해선 역할에 맞는 구현체를 선정해주는 설정(=기획자)이 필요
- 예시
- 연극에서 로미오 역할을 하는 디카프리오가 줄리엣 역할을 하는 여주인공을 직접 선정하는 것과 같다. - 여주인공 선정하는 기획자 필요
- memberService의 역할을 하는 memberServiceImpl 가 memberRepository의 구현체를 직접 선정하는 중
- 해결 방법
- 구현 객체를 생성하고
- 생성한 객체를 연결하는
별도의 설정 클래스인 AppConfig 가 필요하다.
AppConfig
- 구현 객체 생성 == SpringContainer에 등록
- 생성자 주입을 통해서 연결 == @AutoWired , @bean끼리 연결관계 맺어주기
public class AppConfig{
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
// 참고. MemoryMemberRepository먼저 생성이 되고 MemberServiceImpl()가 생성된다.
}
public Orderservice orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
1. serviceImpl 관심사 분리
// 관심사 분리 안된 코드
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
// 관심사 분리된 코드
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
// 생성자
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// AppConfig (다른 class)
public class AppConfig{
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
}
2. OrderService 관심사 분리(=생성자 주입)
public class OrderServiceImpl{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
// AppConfig
public class AppConfig{
public Orderservice orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy())
}
}
원리
- 관심사의 분리가 목적 - 구현체의 변경(=코드의 확장)이 일어나도 소스코드의 변화가 일어나면 안된다
- service class의 인스턴스 생성을 하기 위해서 params로 memberRepository를 받도록 생성자를 작성
- service class 인스턴스 생성은 AppConfig가 생성 및 repository 종류 설정 가능
- ServiceImple은 추상화에만 의존하기 때문에 AppConfig 내부 구현체들이 변경이 되어도 영향을 받지 않는다.
1. 소스 코드는 어떤 구현체로 바뀌어도 수용할 수 있게 추상적으로 변화되어야 된다. DIP
private final MemberRepository memberRepository; // 구현체는 없애고 인터페이스만 남김 DIP
// 생성자 - 인터페이스로만으로는 객체를 사용할 수 없다.
// 생성자의 조건에 맞게 구현체가 주입 될 수 있도록 생성자 초기화 값에 memberRepository의 객체가 들어 올수 있게 작성
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
2. AppConfig로 부터 repository의 구현체를 선택한 후 ServiceImpl에 들어 가도록 코드 작성 - IoC(제어의 역전)
public class AppConfig{
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
}
3. AppConfig 실행
1. MemberApp
public class MemberApp {
public static void main(String[] args){
Member member = new Member(1L, "memberA", Grade.VIP);
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("find Member = " + findMember.getName());
}
}
2. OrderApp
public class OrderApp{
public static void main(String[] args){
AppConfig appConfig = new AppConfig();
OrderService orderService = appConfig.OrderService();
MemberService memberService = appConfig.memberService();
// 현재 2개의 memoryMemberRepository 객체가 생성이 되지만 내부 store가 static이므로 공동의 메모리 db를 가진다.
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);
}
}
4. Junit5 Test file
import static org.junit.jupiter.api.Assertions.*;
class OrderServiceTest{
// 변경전 부분
// MemberService memberService = new MemberServiceImpl();
// OrderService orderService = new OrderServiceImpl();
// 변경 후 부분
MemberService memberService;
OrderService orderService;
@BeforeEach // Test마다 새로운 memberService(),orderService() 객체 만든다. - 객체 값의 중첩으로 인한 오류 방지
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
OrderService = appConfig.orderService();
}
// 이후 동일
@Test
void createOrder(){
Long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
asserThat(order.getDiscountPrice()).isEqualTo(1000);
}
@Test
@DisplayName("VIP가 아니면 1000원 할인 못받음") // test code 이름 설정
void createOrder_BASIC(){
//given
Member member = new Member(2L, "memberBASIC", Grade.BASIC);
//when
int discount = discountPlicy.discount(member, 10000);
//then
asserThat(discount).isEqualTo(0);
}
}
* JUNIT-5 lifecycle
6. AppConfig 리팩터링
- AppConfig
- 중복 제거 & 역할에 따른 구현을 잘 드러내도록 함
- AppConfig와 같은 설정파일은 한눈에 보여야 하는 것이 중요.
// refectory 전
public class AppConfig{
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public Orderservice orderService(){
return new OrderServiceImpl(
new MemoryMemberRepository(),
new FixDiscountPolicy();
)
}
}
// refectory 후
public class AppConfig{
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
public Orderservice orderService(){
return new OrderServiceImpl(
memberRepository(),
discoutnPolicy()
)
}
// 기획자의 요청에 따라 변경하는 부분
public MemberRepository memberRepository(){
return new MemoryMemberRepository(); // 변경할 수 있는 부분
}
public DiscountPolicy discoutnPolicy(){
return new FixDiscountPolicy(); // 변경할 수 있는 부분
}
}
7. 이론 - 좋은 객체 지향 설계의 3가지 원칙
- SRP 단일 책임 원칙
- 한 클래스는 하나의 책임만 가져야한다. - 로직을 수행하던가, 구현체를 선정하던가 둘중 하나만 수행
- 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당
- DIP 의존관계 역전 원칙
- 추상화에 의존해야지, 구체화에 의존하면 안된다.
- AppConfig를 이용한 의존관계 주입 및 serviceImpl에 interface 만 변수로 남김
- OCP 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
- source code의 변경이 없다.
8. spring 이론 - IoC, DI, 컨테이너
제어의 역전 IoC
서비스 코드들의 필요한 구현 repository 객체들을 자신들이 제어 하지 못하고 그 권한들을 AppConfig에게 주었다.
심지어 무슨 구현 객체를 받는지도 모른다.
의존관계 주입 DI
의존관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야 한다.
* 정적인 클래스 의존 관계: 클래스 도메인 이미지 * 동적인 클래스 의존 관계: 객체 다이어그램 이미지
애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성(AppConfig)하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다. (관심사 분리 원리 내용)
IoC 컨테이너, DI 컨테이너
AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다.
의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다. 또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다.
springContainer도 동일한 역할 수행
9. 스프링으로 전환하기
@Configuration // spring 설정정보
public class AppConfig {
@Bean // spring container에 넣기
public MemberRepository memberRepository() {
System.out.println("call AppConfig.memberRepository");
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
// service
@Bean
public MemberService memberService(){
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService(){
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
}
- @Configuration: spring의 설정정보, spring의 AppConfig (== spring container)가 수행
- Spring Container class 명 : ApplicationContext
- @Bean : Spring Container에 해당 method()를 실행해서 나온 객체들을 저장한다. - (key, value) 형식으로 저장
- key : 저장된 Bean 이름은 해당 method 명이 default 값
- value : method() 실행할 때 나오는 객체
public class MemberApp {
public static void main(String[] args) {
// 기획자 없을때 DIP OCP 무시한 code
// MemberService memberService = new/ MemberServiceImpl(memberRepository);
// 기획자 실행법
// AppConfig appConfig = new AppConfig(); // 1) 생성
// MemberService memberService = appConfig.memberService(); // 2) 필요 주입 method 호출
// spring 기획자 실행법
// springContainer 생성(@Configuration 사용된 설정파일 중 AppConfig.class 관한 것으로 bean 생성함)
ApplicationContext applicationContext
= new AnnotationConfigApplicationContext(AppConfig.class);
// springContainer bean 호출(bean 이름, type)
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());
}
}
이전 발행글 : 스프링 핵심 원리 이해 1 - 예제 만들기(회원)
다음 발행글 : 스프링 핵심 원리 이해 3 - 스프링 컨테이너와 스프링 빈
출처: 인프런 스프링 핵심 원리 - 기본편