2. IOC와 DI

2021. 11. 22. 15:31Spring/Boot

앞서 살펴봤듯이 기본적인 다형성으로는 OCP와 DIP를 충분히 지킬 수 없었다. 이는 DI를 통해 해결할 수 있다.

1. 도메인 설계

휴대폰 매장을 오픈한다고 가정해보자. 클라이언트는 통신사에 해당하는 휴대폰을 개통할 수 있을 것이다.사장님은 SKT, KT, LG U+ 중에 어떤 매장으로 오픈할지 정하지 못하셨다. 일단은 KT로 생각 중이라고 하신다. 그렇다고 손만 빨고 있을 수는 없다. 다형성을 이용해 미리 개발해놓도록 하자.

이를 역할과 구현을 분리하여 클래스 다이어그램으로 표현하면 다음과 같다.

이를 간단하게 코드로 구현해보자.

public interface MemberService {
    void open();
}
public class MemberServiceImpl implements MemberService {

    Telecom telecom = new KT();

    public void open(String number) {
        Phone phone = new Phone();
        phone.setNumber(number);

        telecom.register(phone);
    }
}
public interface Telecom {
    void register(Phone phone);
}
public class KT implements Telecom {

    private Map<String, Phone> store = new HashMap<>();

    public String register(Phone phone) {
        String number = phone.getNumber() + "";
        store.put(number, phone);
        return number;
    }
}
public class Phone {

    private String number;

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }
}
public class Main {
    MemberService memberService = new MemberServiceImpl();

    memberService.open("010-1234-5678");
}

2. 요구사항 변경 발생

갑자기 사장님께서 SKT로 통신사를 바꾸고 싶다고 하신다. 머리가 아프다. 하지만 우리는 역할과 구현을 분리하여 설계하였기 때문에 SKT 객체를 새로 만들어 갈아끼우기만 하면 된다.

public class SKT implements Telecom {

    private Map<String, Phone> store = new HashMap<>();

    public String register(Phone phone) {
        String number = phone.getNumber() + "";
        store.put(number, phone);
        return number;
    }
}
public class MemberServiceImpl implements MemberService {

//    Telecom telecom = new KT(); // 클라이언트 코드 변경!!
    Telecom telecom = new SKT();

    public void open(String number) {
        Phone phone = new Phone();
        phone.setNumber(number);

        telecom.register(phone);
    }
}

이러면 충분한 걸까? 우리는 다형성을 이용해 개발하였지만 OCP를 준수하지 못했다. 클라이언트의 코드가 변경되었기 때문이다. 또한 현재 코드는 구현체에 SKT라는 구현체에 의존하기 때문에 DIP 또한 위반하고 있다. 따라서 이를 인터페이스에 의존하도록 변경해야 한다. OCP를 준수하려면 어떻게 해야 할까? 이를 위해서는 통신사를 외부에서 지정해주도록 해야 한다. 다음과 같이 말이다.

3. IOC와 DI

class AppConfig {
    public MemberService memberService() {
        return new MemberServiceImpl(new SKT());
    }
}
public class MemberServiceImpl implements MemberService {
    Telecom telecom;

    public MemberServiceImpl(MemberService telecom) {
        this.telecom = telecom;
    }

    public void open(String number) {
        Phone phone = new Phone();
        phone.setNumber(number);

        telecom.register(phone);
    }
}
public class Main {
    AppConfig appConfig = new Appconfig();
    MemberService memberService = appConfig.memberService();

    memberService.open("010-1234-5678");
}

위와 같이 외부에서 필요한 객체를 생성하고 주입해주는 방식을 사용한다면 OCP와 DIP를 지키면서 개발할 수 있다. MemberServiceImpl 코드를 살펴보면 생성자를 통해 telecom 객체를 생성하지만 MemberService라는 인터페이스만 존재할 뿐 구현체는 보이지 않는다. 그리고 SKT, KT, LG U+ 등 어떤 통신사를 사용한다 하더라도 AppConfig 클래스에서 생성되는 객체만 변경해주면 다른 그 어떤 코드도 변경하지 않고 교체가 가능하다.

이렇게 변경함으로써 AppConfig에서는 프로그램에서 사용되는 객체를 구체화하여 제공하는 역할을 하고 MemberServiceImpl과 같은 클래스는 생성자로 주입받은 구현체를 실행하는 역할만을 수행한다. 이로써 관심사의 분리를 이루고 OCP, DIP를 지키는 코드를 만들어냈다.

이처럼 프로그램의 제어가 클라이언트의 코드에서 외부 코드로 이동(MemberServiceImplAppConfig)한 것을 제어의 역전(IOC)라고 부른다. 그리고 객체의 의존관계 생성자를 통해서 주입하는 방식을 의존관계 주입(Dependency Injection, DI)이라고 한다. IOC는 DI보다 큰 범위를 나타낸다. 프레임워크를 사용하여 개발할 때 프레임워크의 사용 방법에 따라 개발하는 것도 IOC 중 하나이다.

정리

우리는 객체 지향 프로그래밍에서 다형성만으로는 객체 지향 설계 원칙을 지킬 수 없다는 것을 깨달았고 이 문제를 IOC와 DI라는 방법을 적용하여 해결했다.