제어의 역전 IoC(Inversion of Control)

 

기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고,

연결하고, 실행했다. 즉 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다는 것이다. 

이것은 개발자 입장에서는 자연스러운 흐름이다.

 

반면에 AppConfig가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당한다.

프로그램의 제어 흐름은 이제 AppConfig가 가져간다. 예를 들어 OrderServiceImpl은 필요한

인터페이스들을 호출하지만 어떤 구현 객체들이 실행될지 모른다.

 

프로그램에 대한 제어 흐름에 대한 권한은 모두 AppConfig가 가지고 있다.

심지어 OrderServiceImpl도 AppConfig가 생성한다. 그리고 AppConfig는 

OrderServicempl이 아닌 OrderService 인터페이스의 다른 구현 객체를 생성하고

실행할 수 도 있다. 그런 사실은 모른채 OrderServiceImpl은 묵묵히 자신의 로직을 실행할 뿐이다.

 

이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전이라한다.

 


 

프레임워크 vs 라이브러리

 

프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크가 맞다.(Junit)

 

반면에 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리다.

 


 

의존관계 주입 DI

 

OrderServiceImpl은 DiscountPolicy 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지는 모른다.

 

의존관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적은 객체 의존 관계 둘을 분리해서 생각해야 한다.

 

 

정적인 클래스 의존관계

클래스가 사용하는 import코드만 보고 의존관계를 쉽게 판단할 수 있다. 정적인 의존관계는 애플리케이션을 

실행하지 않아도 분석할 수 있다.

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/lecture/55349?tab=note&mm=close&speed=0.75

OrderServiceImpl은 MemberRepository, DiscountPolicy에 의존한다는 것을 알 수 있다.

 

그런데 이러한 클래스 의존관계 만으로는 실제 어떤 객체가 OrderServiceImpl에 주입 될지 알 수 없다.

 

 

동적인 객체 인스턴스 의존 관계

애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계다.

 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/lecture/55349?tab=note&mm=null&speed=0.75

애플리케이션 실행시점에 외부에서 실제 구현 객체를 생성하고, 클라이언트에 전달해서

클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다. 

 

객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결된다.

 

의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.

 

의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.

 


 

IoC 컨테이너, DI 컨테이너

 

AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는것을 Ioc컨테이너 또는 DI컨테이너 라고한다.

 

의존관계 주입에 초점을 맞추어 최근에는 주로 DI컨테이너라 한다.

 

또는 어셈블러, 오브젝트 팩토리 등으로 불리기도 한다.

SRP 단일 책임 원칙

   한 클래스는 하나의 책임만 가져야 한다.

 

클라이언트 객체는 직접 구현 객체를 생성하고, 연결하고, 실행하는 다양한 책임을 가지고 있었다.

SRP 단일 책임 원칙을 따르면서 관심사를 분리하였다.

구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당

클라이언트 객체는 실행하는 책임만 담당

 


 

DIP 의존관계 역전 원칙

   프로그래머는 추상화에 의존해야지 구체화에 의존하면 안된다. 의존성 주입은 이 원칙을 따르는 방법 중 하나다.

 

새로운 할인 정책을 개발하고, 적용하려고 하니 클라이언트 코드도 함께 변경해야 했다. 

왜냐하면 기존의 클라이언트 코드(OrderServiceImpl)은 DIP를 지키며 DiscountPolicy 추상화 인터페이스에

의존하는것 같았지만 FixDiscountPolicy 구체화 구현 클래스에도 함께 의존하고 있었다.

클라이언트 코드가 DiscountPolicy 추상화 인터페이스에만 의존하도록 코드를 변경했다.

하지만 클라이언트 코드는 인터페이스만으로는 아무것도 실행할 수 없었다.

AppConfig가 FixDiscountPolicy 객체 인스턴스를 클라이언트 코드 대신 생성하여 클라이언트

코드에 의존관계를 주입하니 DIP원칙을 따르면서 문제를 해결할 수 있었다.

 


 

OCP

   소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.

 

다형성을 사용하고 클라이언트가 DIP를 잘 지키면 OCP의 적용 가능성이 열린다.

애플리케이션을 사용영역과 구성영역으로 나누었다.

AppConfig가 의존관계를 FixDiscountPolicy RateDiscountPolicy로 변경해서

클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 되게 되었다.

결과적으로 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀있게 되었다.

그말은 즉 클라이언트 코드를 변경할 필요가 없게되었다.

처음에 새로운 할인 정책을 개발하였다.

다형성 덕분에 새로운 정률 할인 정책 코드를 추가로

개발하는 것 자체에는 아무 문제가 없었다.

 

하지만 적용하는 부분에서

클라이언트 코드인 주문 서비스 구현체도 함께 변경해야하는 문제가 생겼다.(OCP위반)

주문 서비스 클라이언트가 인터페이스인 DiscountPolicy 뿐만 아니라

구체 클래스인 FixDiscountPolicy도 함께 의존하였다. (DIP위반)

 

그래서 관심사를 분리하였다.

애플리케이션을 하나의 공연으로 생각하고

기존에는 클라이언트가 의존하는 서버 구현 객체를 직접 생성하고, 실행하였다.

이 문제를 해결하기 위해 공연을 기획하는 AppConfig를 만들었다.

AppConfig는 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고 연결하는 책임을한다.

이제부터 클라이언트 객체는 자신의 역할을 실행하는 것만 집중하여 권한이 줄어들게 되었다.

 

AppConfig리팩터링을 통해

구성 정보에서 역할과 구현을 명확하게 분리하고

중복을 제거하여 역할이 잘 들어나게 되었다.

 

새로운 구조와 할인 정책 적용을 하였다.

정액 할인 정책에서 정률 할인 정책으로 변경하였더니

AppConfig의 등장으로 애플리케이션이 사용영역과 구성영역으로 분리하였다.

AppConfig가 있는 구성 영역만 변경하면 사용 영역을 변경할 필요가 없게되었다.

 

 

 

 

 

노드

 

각각의 노드는 데이터 필드와 하나 혹은 그 이상의

링크 필드로 구성

링크 필드는 다음노드를 참조

첫번째 노드의 주소는 따로 저장해야함

 

package section6;

public class Node<T> {
	
	public T data;
	public Node<T> next;
	
	public Node(T data){
		this.data = data;
		next = null;
	}
	
}

노드 클래스

T 타입의 데이터변수,

다음 노드의 주소값을 가지는 변수 

를 가진다.

package section6;

public class MySingleLinkedList<T> {
	
	public Node<T> head;
	public int size = 0;
	
	public MySingleLinkedList() {
		head = null;
		size = 0;
	}
	
	public void addFirst(T item) {
		Node<T> newNode = new Node<T>(item);
		newNode.next = head;
		head = newNode;
		size++;
	}
	
	public void add(int index, T item) { //insert
		
	}
	
	public void remove(int index) { //delete
		
	}
	
	public T get(int index) {  //get
		return null;
	}
	
	public int indexOf(T item) { //search
		return -1;
	}
	
	public static void main(String[] args) {
		
	}

}

연결리스트 클래스 

아직 전부 구현하지는 않았지만 

첫번째 노드를 가리키는 주소변수,

총 사이즈를 체크하는 변수와

 

노드들이 있을때 첫번째 노드에 새로운 노드를 추가하는

함수를 구현해보았다.

하지만 이 함수에는 큰 문제가 있다.

기존의 연결리스트의 크기가 0인경우, head가 null인

경우에도 문제가 없는지 확인해야한다.

리스트

 

기본적인 연산 : 삽입, 삭제, 검색 등

리스트를 구현하는 대표적인 두 가지 방법 : 배열, 연결리스트

 

배열의 단점

 

크기가 고정 - reallocation이 필요

리스트의 중간에 원소를 삽입하거나 삭제할 경우 다수의 데이터를 옮겨야 함

 

연결 리스트

 

다른 데이터의 이동없이 중간에 삽입이나 삭제가 가능하며,

길이의 제한이 없음

하지만 랜덤 액세스가 불가능

랜덤 액세스란 배열의 경우에는 배열의 10번째 데이터를 읽어야한다면

a[10] 하면 되지만 어떤 칸에 읽는데 걸리는 시간이 거의 동일하다.

연결리스트는 10번째 데이터를 읽고 싶다면 

첫번재 데이터부터 순서대로 가야만 한다.

 

어떤 데이터와 나의 다음데이터의 주소의 데이터 쌍을

노드라고 부른다.

첫번째 노드의 주소는 절대 잃어버려서는 안된다.

 

 

처음으로 돌아가 정률 할인 정책으로 변경해보았다.

FixDiscountPolicy -> RateDiscountPolicy

 

AppConfig의 등장으로 애플리케이션이

크게 사용영역과, 객체를 생성하고 구성하는 영역으로 분리되었다.

 

AppConfig 코드만 고치면 가능하다!

 

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
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;

public class AppConfig {

    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }

    private MemoryMemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    public DiscountPolicy discountPolicy(){
        return new RateDiscountPolicy();
    }

}

discountPolicy() 함수에서 

객체생성 부분만 RateDiscountPolicy으로 변경해주면 된다.

이제 사용영역의 어떠한 코드도 변경할 필요가 없고

구성영역은 당연히 변경된다. 구성역할을 담당하는 AppConfig를 공연 기획자로 생각하면

구현객체들을 모두 알아야한다.

AppConfig는 현재 중복과 역할에따른 구현이 잘 보이지 않는다.

 

 

package hello.core;

import hello.core.discount.FixDiscountPolicy;
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;

public class AppConfig {

    public MemberService memberService(){
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService(){
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }

}

현재 AppConfig 코드이다.

 

 

 

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
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;

public class AppConfig {

    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }

    private MemoryMemberRepository  memberRepository() {
        return new MemoryMemberRepository();
    }

    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(),discountPolicy());
    }
    
    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }

}

new MemoryMemberRepository() 이 부분이 중복 제거되었다.

이제 MemoryMemberRepository 를 다른 구현체로 변경할 때 한 부분만 변경하면 된다.

AppConfig 를 보면 역할과 구현 클래스가 한눈에 들어오게 된다.

이로써

애플리케이션 전체 구성이 어떻게 되어있는지

한눈에 알아보기 쉽게 되었다.

애플리케이션을 하나의 공연이라 생각하고

각각의 인터페이스를 배역이라 생각하면 실제 배역에 맞는

배우를 선택하는 것은 누가 하는가?

 

로미오와 줄리엣 공연을 하면 로미오 역할, 줄리엣 역할을 누가 할지는 

배우들이 정하는 것이 아니다. 이전 코드는 마치 로미오 역할을 하는 디카프리오가 줄리엣을 하는

여자 주인공을 직접 초빙하는 것과 같다. 디카프리오는 공연도 해야하고 초빙도 해야하는 

다양한 책임을 가지고 있다.

 

배우는 본인의 역할인 배역을 수행하는 것에만 집중해야하낟.

 

디카프리오는 어떤 여자 주인공이 선택되더라도 똑같이 공연을 할 수 있어야한다.

 

공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 배우를 지정하는 책임을

담당하는 공연 기획자가 나올 시점이다.

 

공연 기획자를 만들고, 배우와 공연 기획자의 책임을 확실히 분리해야한다.

 

package hello.core;

import hello.core.discount.FixDiscountPolicy;
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;

public class AppConfig {

    public MemberService memberService(){
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService(){
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }

}

AppConfig 클래스를 생성하였다.

AppConfig는 애플리케이션의 실제 동작에 필요한 구현객체를 생성한다.

 

MemberServiceImpl, MemoryMemberRepository, OrderServiceImpl, FixDiscountPolicy

 

AppConfig는 생성한 객체 인스턴스의 참조를 생성자를 통해서 주입 해준다.

MemberServiceImpl -> MemoryMemberRepository

OrderServiceImpl -> MemoryMemberRepository , FixDiscountPolicy

 

package hello.core.member;

public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

설계 변경으로 MemberServiceImpl은 MemoryMemberRepository를 의존하지 않는다.

단지 MemberRepository 인터페이스만 의존한다.

MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지는 알 수 없다.

MemberServiceImpl 의 생성자를 통해서 어떤 구현객체가 주입할지는 오직 외부에서 결정된다.

MemberServiceImpl 는 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면된다.

 

이제 객체의 생성과 연결은 AppConfig가 담당한다.

DIP가 완성이 되었다.

객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.

 

appConfig 객체는 memoryMemberRepository 를 생성하고 그 참조값을 memberServiceImpl을 생성하면서

생성자로 전달한다.

 

클라이언트인 memberServiceI 입장에서 보면 의존관계를 마치 외부에서 주입해주느것 과같다고 해서

DI 우리말로 의존관계 주입 또는 의존성 주입이라 한다.

 

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

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);
    }
}

설계 변경으로 OrderServiceImpl 은  FixDiscountPolicy를 의존하지 않는다.

단지 DiscountPolicy  인터페이스에만 의존한다.

OrderServiceImpl 입장에서 생성자를 통해 어떤 구현객체가 들어올지 알 수없다.

OrderServiceImpl 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부에서 결정한다.

OrderServiceImpl 은 이제 실행에만 집중하면 된다.

 

OrderServiceImpl 에는 MemoryMemberRepositoty , FixDiscountPolicy 객체의 의존관계가 주입된다.

 

AppConfig는 공연 기획자다.

AppConfig는 구체 클래스를 선택한다. 애플리케이션이 어떻게 동작해야할 지 전체 구성을 책임진다.

새로운 할인 정책을 적용을 해보았다.

 

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

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);
    }
}

문제점이 발견되었다.

 

할인정책을 변경하려면 클라이언트인 OrderServiceImpl 코드의 수정이 필요하다.

 

역할과 구현을 충실하게 분리하였고,

다형성도 활용하고, 인터페이스와 구현객체를 분리했지만

OCP, DIP와 같은 객체지향 설계원칙을 준수한 것처럼 보이지만

사실은 아니다.

 

먼저 DIP 관점에서

OrderServiceImpl는 DiscountPolicy 인터페이스에 의존하면서

DIP를 지킨 것 같지만 

클래스 의존관계를 분석해보면 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다.

추상 의존 : DiscountPolicy

구체 클래스 : FixDiscountPolicy , RateDiscountPolicy

그렇기에 DIP 위반!

 

OCP 관점에서 

FixDiscountPolicy를 RateDiscountPolicy로 바꾸는 순간

코드의 변경이 일어난다. 

그렇기에 OCP 위반!

 


 

이 문제를 어떻게 해결할 수 있을까??

 

클라이언트 코드인 OrderServiceImpl은 DiscountPolicy의 인터페이스 뿐만 아니라 

구체 클래스도 함꼐 의존한다.

그래서 구체 클래스를 변경할때 클라이언트 코드도 함께 변경해야 한다.

DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경해야 한다.

 

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private 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);
    }
}

인터페이스에만 의존하도록 코드를 변경해 보았다

그런데 구현체가 없는데 어떻게 코드를 실행할까?

실제 실행을 해보면 널포인트익셉션이 발생한다.

 

이 문제를 해결하려면 누군가가 클라이언트인  OrderServiceImpl에 DiscountPolicy의 구현

객체를 대신 생성하고 주입해주어야 한다.

이전에 만들었던 스케줄러 프로그램을 

ArrayList를 사용하도록 수정해보았다. 

 

package chapter4;

import java.util.ArrayList;
import java.util.Scanner;

public class Scheduller {
	
	public ArrayList<Event> events = new ArrayList<>();
	private Scanner kb;
	
	public void processCommand() {
		
		kb = new Scanner(System.in);
		while(true) {
			System.out.print("$ ");
			String command = kb.next();
			if(command.equals("addevent")) {
				String type = kb.next();
				if(type.equalsIgnoreCase("oneday"))
					handleAddOneDayEvent();
				else if(type.equalsIgnoreCase("duration"))
					handleAddDurationEvent();
				else if(type.equalsIgnoreCase("deadline"))
					handleAddDeadlineEvent();
			}
			else if(command.equals("list")) {
				handleList();
				
			}
			else if(command.equals("show")) {
				handleshow();
			}
			else if(command.equals("exit")) {
				break;
			}
				
		}
		kb.close();
	}

	private void handleshow() {
		String dateString = kb.next();
		MyDate theDate = parseDateString(dateString);
		for (int i=0; i<events.size();i++) {
			if(events.get(i).isRelevent(theDate))
				System.out.println(events.get(i).toString());
		}
	}

	private void handleList() {
		for(Event ev : events)
			System.out.println("   "+events.get(i).toString());
		}
		
	}

	private void handleAddDeadlineEvent() {
	
	}

	private void handleAddDurationEvent() {
		// TODO Auto-generated method stub
		
	}

	private void handleAddOneDayEvent() {
		System.out.print("  when: ");
		String dateString = kb.next();
		System.out.print("  title: ");
		String title = kb.next();
		
		
		MyDate date = parseDateString(dateString);
		OnedayEvent ev = new OnedayEvent(title,date);
		System.out.println(ev.toString());
		addEvent(ev);
	}

	private void addEvent(Event ev) {
		events.add(ev);
	}

	private MyDate parseDateString(String dateString) {
		String[] tokens = dateString.split("/");
		
		int year = Integer.parseInt(tokens[0]);
		int month = Integer.parseInt(tokens[1]);
		int day = Integer.parseInt(tokens[2]);
		
		MyDate d = new MyDate(year,month,day);
		return d;
	}

	public static void main(String[] args) {
		
		Scheduller app = new Scheduller();
		app.processCommand();
		
	}

}

+ Recent posts