제어의 역전/의존성 주입 - IoC/DI (1) - DI

스프링의 대표적인 특성 3가지로 IoC/DI, AOP, PSA가 있다. IoC/DI는 Inversion Of Control/Dependency Injection의 줄임말로 제어의 역전/의존성 주입이라는 뜻이고, AOP(Aspect Oriented Programming)는 관점지향 프로그래밍, PSA(Portable Service Abstraction)는 확장이 용이한 서비스 추상화를 의미한다. 직관적으로 봤을 때 꽤나 애매한 말들이라 가볍게 넘어가기 마련인데, 좋은 서비스를 만들기 위해서는 각각이 의미하는 바가 무엇인지 정확히 알아야 하고 또한 이러한 개념들이 스프링 구조에 어떻게 녹아있는지 이해해야하기 때문에 해당 개념에 대해 조금은 깊게 알아볼 필요가 있다고 생각한다.


IOC/DI(1)

해당 개념을 함께 이해하기 위해서는 DI부터 알아야 한다. 왜냐하면 DI, 의존성 주입이라는 개념을 먼저 알고있어야 IoC, 제어의 역전이라는 개념이 와닿기 때문이다. 그러면 DI는 무엇일까?

DI (Dependency Injection)

객체를 직접 생성하는 게 아니라 외부에서 생성한 후 주입 시켜주는 방식이다. 이런 개념이 어쩌다가 나왔는지 왜 필요한지 어떤 의의를 가지고 있는지에 대해 알아볼 필요가 있다.

DI가 없는 코드

간단한 예시를 들어보겠다. 본 상황에서는 여러 종류의 Internet Service Provider가 있고 Client는 Internet Service Provider를 변경할 수 있다고 가정한다.

다음과 같이 여러종류의 Internet Service에 연결할 수 있는 코드가 있다고 가정해보자. 현재 상황에서는 KT, SK, LG 세 가지의 예시만 들것이다.

public class KTInternetService {
    public void connectTo() {
        System.out.println(">>> Connect to Internet Service Provider KT");
    }
}
.
.
public class LGInternetService {
    public void connectTo() {
        System.out.println(">>> Connect to Internet Service Provider LG U+");
    }
}
.
.
public class SKInternetService {
    public void connectTo() {
        System.out.println(">>> Connect to Internet Service Provider SK Broadband");
    }
}

이 중 현재 클라이언트는 KT 인터넷을 사용하고 있으며 연결을 지원하는 코드는 다음과 같다.

public class Client {
    private KTInternetService ktInternetService;

    public void connectToInternet() {
        ktInternetService = new KTInternetService();
        ktInternetService.connectTo();
    }
}

클라이언트가 인터넷에 연결되었는지 테스트를 해보면 아래 주석과 같은 결과가 나온다.

public class ConnectionTest {
    public static void main(String args[]) {
        Client client = new Client();
        client.connectToInternet();
    }
}

// 결과
// >>> Connect to Internet Service Provider KT

위와 같은 상황의 문제점은 바로 무엇일까??
바로 Client가 특정 InternetService에 과하게 의존하고 있다는 것이다.

💡 의존성이란?
의존성이란 코드에서 두 객체간의 연결이나 관계를 의미한다. 쉽게 말해 한 객체가 어떤 기능을 사용하기 위해 다른 객체를 사용하는 것이다. 다른 객체를 통해 기능을 사용하기 때문에 의존한다 라고 표현한다. 위 코드에서는 Client가 특정 Internet Service(KT)에 과하게 의존하는 모습을 보인다.

그러면 과하게 의존해서 발생되는 문제를 두가지 관점에서 살펴보자.


  • OCP(Open-Closed Principle) 위배
    OCP란 확장에 자유롭고 수정에 폐쇄적이어야 한다는 법칙을 의미한다. 위 코드는 확장에 자유롭지 못하다. 예를 들어 SK나 LG의 Internet Service를 사용하려면 클라이언트 코드를 일일이 다음과 같이 변경하여야 할 것이다.
public class Client {
    private SKInternetService skInternetService;
    // 또는 private LGInternetService lgInternetService;

    public void connectToInternet() {
        skInternetService = new SKInternetService();
        // 또는 LGInternetService = new LGInternetService();
        skInternetService.connectTo();
        // 또는 lgInternetService.connectTo();
    }
}
  • High Coherence, Low Coupling
    High Coherence, Low Coupling는 소프트웨어 개발원리로써 높은 응집도와 느슨한 결합도를 의미한다. 응집도가 높다는 것은 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 관련이 있다는 것이다. 만약 여러 관심사가 하나의 클래스에 엮여있다면 변경이 필요한 부분을 찾아내서 하나하나 바꿔주는 것도 큰 일일 것이다. 느슨한 결합은 최소한의 결합만 간접적인 형태로 제공하고, 나머지는 독립적으로 알 필요도 없이 만들어 주는 것이다. 위 코드는 Client에서 Internet Service를 분리하여 각 클래스가 높은 응집도를 가지고 있다는 것을 알 수 있지만, Client가 특정 회사의 Internet Service를 알아야 사용할 수 있기 때문에 타이트한 결합이 이루어지는 것을 알 수 있다.

그러면 문제점을 정리한 후 개선 전략을 세워 리팩토링 해보겠다.

DI가 적용된 코드

  • Low coupling
    느슨한 결합은 최소한의 결합만 간접적인 형태로 제공하고, 나머지는 독립적으로 알 필요도 없이 만들어 주는 것이다.
    -> Client에게 Internet Service 인터페이스를 제공한다.
  • DI (Dependency Injection)
    Client가 결국 특정 회사의 Internet Service를 사용하려면 인터페이스 구현체에게 의존을 해야하는데 이를 내부에서 의존하기보다 외부에서 의존받도록 만들어 준다. 이는 다형성이라는 특성때문에 가능하다.
    -> Internet Service 생성시 특정 회사의 인터페이스 구현체를 생성자로 주입받는다.

정리하자면 문제를 해결하기위해 Client는 인터페이스를 제공 받으며 특정 구현체를 생성자로 주입받도록 만들어 볼 수 있다.

public interface InternetService {
    void connectTo();
}
.
.
public class KTInternetService implements InternetService{
// below connect code
.
.
public class LGInternetService implements InternetService{
// below connect code
.
.
public class SKInternetService implements InternetService{
// below connect code
.
.

Client는 InternetService 추상적인 인터페이스에 의존하며 각 서비스가 어떻게 구성되어있는지 알지 못하고 알 필요도 없다.

public class Client {
    private InternetService internetService;

    public Client(InternetService internetService) {
        this.internetService = internetService;
    }

    public void connectToInternet() {
        internetService.connectTo();
    }
}

마찬가지로 클라이언트가 인터넷에 연결되었는지 테스트를 해보면 아래 주석과 같은 결과가 나온다.

public class ConnectionTest {
    public static void main(String args[]) {
        Client client1 = new Client(new KTInternetService());
        client1.connectToInternet();

        Client client2 = new Client(new LGInternetService());
        client2.connectToInternet();

        Client client3 = new Client(new SKInternetService());
        client3.connectToInternet();
    }
}
// 결과
// >>> Connect to Internet Service Provider KT
// >>> Connect to Internet Service Provider LG U+
// >>> Connect to Internet Service Provider SK Broadband

언급한 문제점들이 해결되었음을 알 수 있다.



정리

스프링 핵심개념 중 IoC/DI 중 DI는 꽤 많은 철학과 객체지향 원칙이 녹아있다는 것을 알 수 있었다. 그것을 바탕으로 이 글의 핵심개념을 정리하자면 다음과 같다.

  • 한 객체의 기능이 사용될때 다른 객체의 도움이 필요하다면 의존한다 라고 부를 수 있다.
  • 의존성을 낮추는 것을 소프트웨어 개발 철학인 low coupling으로 볼 수 있고 이는 인터페이스의 다형성을 통해 구현할 수 있다.
  • high coherence와 low coupling이 잘 되도록 구현한다면 OCP나 SRP와 같은 객체지향 원칙을 잘 지킬 수 있다.

전체코드는 깃허브에서 볼 수 있다.

https://github.com/waonderboy/spring-triangle/







Reference

  • 토비의 스프링
  • geeksforgeeks

좋은 웹페이지 즐겨찾기