IOC와 DI에 대해서 알아보기 전에 순수 자바코드를 먼저보자.
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository = new MemorymemberRepository();
@Override
public void join(Member member){
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId){
return memberRepository.findByid(memberId);
}
}
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 discountPolicy = discountPolicy.discount(member, itemPrice);
return new Order(memberid, itemName, itemPrice, discountPrice);
}
}
위의 코드에서 할인정책이나 데이터 베이스가 변경된다면 OCP와 DIP를 위반하게 된다.
OCP위반 : 클라이언트(요청하는 입장인 : MemberServiceImpl, OrderServiceImpl)의 코드를 수정해야 한다
DIP 위반 : 추상화(인터페이스)에 의존해야 하는데 구체화(구현 객체)에도 의존하고 있다.
로버트 마틴의 객체지향 설계 원칙 (SOLID)
🔶 좋은 객체 지향 설계의 5가지 원칙 1.SOLID란 SRP(The Single Responsibility) : 단일 책임 원칙 OCP(The Open Closed Principle) : 개방 폐쇄 원칙 LSP(The Liskov Substitution Principle) : 리스코프 치환 원칙 ISP(The Interface
hyunbenny.tistory.com
- 현재 MemberRepository라는 인터페이스가 아니라 MemoryMemberRepository라는 구현체,
- DiscountPolicy라는 인터페이스가 아니라 RateDiscountPolicy라는 구현체에 의존하고 있다.
이를 해결하기 위해서는 어떻게 해야할까?
기능을 확장, 변경하는 경우에도 클라이언트의 코드는 수정없이 변경이 이루어져야 한다.
추상화(인터페이스)에만 의존해야 한다.
➡️ 위와 같이 클라이언트에 직접 주입하는 방법이 아니라 구현체를 대신 생성해서 주입해주는 역할을 할 것이 필요할 것 같다.
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
...
}
위의 코드를 아래와 같이 추상화한 인터페이스만 바라보고도 기능이 제대로 동작하도록 만들어보자.
public class OrderServiceImpl implements OrderService{
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
...
}
그렇다면 구현체를 대신 생성해서 주입해주는 역할을 할 AppConfig를 만들어보자.
public class AppConfig {
// 생성자주입 방식 : 생성자를 통해서 구현객체를 넣어줌
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new RateDiscountPolicy());
}
}
- AppConfig가 구현객체를 생성하고 그 인스턴스의 참조값을 생성자를 통해서 주입해준다.
그러면 아까 문제가 되었던 Service 코드는 아래와 같이 변경할 수 있겠다.
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
}
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;
}
...
}
- 이제 MemberRepository, DiscountPolicy 인터페이스에만 의존하여 DIP가 지켜진다.
- 또한 MemberServiceImpl, OrderServiceImpl의 입장에서는 어떤 구현객체가 들어올 지 전혀 모르고(알 필요도 없고) 자신의 역할에만 충실할 수 있다.
- ➡️ 구현체에 대한 정보는 AppConfig가 결정해서 주입해 줌으로써 역할과 구현이 명확하게 분리 되었다.
- 나중에 변경사항이 있을 때도 AppConfig에서 Rate → Fix로, Memory → Jdbc로 바꿔주기만 하면 Service의 구현체에서는 변경을 하지 않아도 되기 때문에 OCP도 위반하지 않고 변경이 가능하다.
그러면 우리가 수정한 코드를 어떻게 사용할까??
- AppConfig 객체를 생성하여 AppConfig객체의 memberService() 혹은 orderService()를 호출하여 사용하면 된다.
public class MemberApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("member = " + member.getName());
System.out.println("findMember = " + findMember.getName());
}
}
public class OrderApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
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);
System.out.println("calculatePrice = " + order.calculatePrice());
}
}
AppConfig리팩토링
현재 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 RateDiscountPolicy();
}
}
- 이제 역할과 구현 클래스가 한 눈에 들어온다
이제 위의 순수 자바 코드들에 스프링을 적용해보자
@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 RateDiscountPolicy();
return new FixDiscountPolicy();
}
}
public class MemberApp {
public static void main(String[] args) {
/*--- 변경된 부분 ---*/
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = ac.getBean("memberService", MemberService.class);
/*----------------*/
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("member = " + member.getName());
System.out.println("findMember = " + findMember.getName());
}
}
public class OrderApp {
public static void main(String[] args) {
/*--- 변경된 부분 ---*/
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = ac.getBean("memberService", MemberService.class);
OrderService orderService = ac.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);
System.out.println("calculatePrice = " + order.calculatePrice());
}
- 이제는 appConfig에서 직접 찾는 것이 아니라 ApplicationContext(스프링 컨테이너)를 통해서 찾아와야 한다.
- (ac.getBean("memberService", MemberService.class) )
그러면 이제 이 글의 주제였던 IOC와 DI에 대해서 알아보자.
1. IOC, DI, 컨테이너란
프레임워크 : 프레임워크가 내가 작성한 코드를 제어하고 대신 실행함
라이브러리 : 내가 작성한 코드가 제어의 흐름을 담당함
1) IoC (Inversion of Control : 제어의 역전)
- 프로그램의 제어 흐름을 OrderServiceImpl이 직접하는 것이 아니라 외부(AppConfig)에서 관리하는데 이를 제어의 역전이라고 한다.
- AppConfig가 프로그램의 흐름을 제어한다. 심지어 OrderServiceImpl을 생성하는 것조차 AppConfig가 함 → 'AppConfig가 IoC를 일으킨다.'고 한다.
2) DI(Dependency Injection : 의존관계 주입)
정적 클래스 의존관계 : import만 보고 애플리케이션을 실행하지 않고도 의존관계를 알 수 있음
동적 인스턴스 의존관계 : 애플리케이션의 실행 시점에 실제 생성된 인스턴스의 참조가 연결된 의존관계
- 위의 코드를 OrderServiceImpl입장에서 보면 의존관계를 외부에서 '주입'하는 것 같다고 해서 '의존관계 주입', '의존성 주입(Dependency Injection)'이라고 한다.
3) IoC컨테이너, DI컨테이너
AppConfig처럼 객체를 생성, 관리하면서 의존관계를 주입해주는 것을 IoC컨테이너 혹은 DI컨테이너라고 함
주로 DI컨테이너라고 많이 부름 (혹은 어셈블러, 오브젝트 팩토리)
- 우리가 스프링을 적용했을 때 본 ApplicationContext가 바로 DI 컨테이너라고 할 수 있다.
- 이 ApplicationContext는 스프링에서 스프링 컨테이너라고 불리는데 아래와 같이 간단하게만 알아보고 바로 다음에 알아보기로 한다.
- 스프링 컨테이너는 Bean의 생성, 의존관계주입과 같은 작업을 담당한다.
- 스프링 컨테이너는 @Configuration붙은 클래스를 설정(구성)정보로 사용한다.
- @Bean 어노테이션이 붙은 메서드들 호출하여 반환된 객체를 스프링 컨테이너에 스프링 빈으로 등록한다.
- 빈 이름을 지정하지 않는 경우, 메서드 이름을 스프링 빈 이름으로 등록한다.
- AnnotationConfigApplicationContext()에 설정정보가 있는 클래스 이름을 적어줘야 한다.
스프링 핵심 원리 - 기본편 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런
www.inflearn.com
'Spring > Spring Framework' 카테고리의 다른 글
[Spring Framework] @ComponentScan (0) | 2023.04.10 |
---|---|
[Spring Framework] 스프링 컨테이너와 싱글톤 컨테이너 (0) | 2023.04.10 |
[Spring Framework] 스프링 컨테이너와 스프링 빈 (0) | 2023.04.06 |
[Spring Framework] 스프링 프레임워크란 (0) | 2023.04.05 |
로버트 마틴의 객체지향 설계 원칙 (SOLID) (0) | 2022.12.30 |