개발하다가 궁금한 게 생겼는데요.

저는 IoC Container를 쓰는 것에 찬성입니다.

물론 IoC Container를 사용하는 것에 반대하시는 견해들도 있습니다.

사전에 타입을 등록하지 않고 생성자 주입을 받을 경우 주입되지 않은 타입에 대해 인스턴스를 생성할 수 없기 때문에 프로세스에 대놓고 런타임 에러가 나는 부분이있기 때문인데요.

객체지향이란 여러 서적들에서 말하듯, 트레이드오프의 개념이 중요하다고 합니다.
이건 결합 vs 응집 사이에서 열심히 머리를 굴리는 것이 객체지향이라는 뜻인데요.

결합은 직접 참조를 말합니다.
이 경우 디버깅이 용이하다는 장점이 있습니다.
응집은 직접 참조하지 않고 인터페이스를 통해 참조합니다.
즉, 참조가 인터페이스에 걸려있고 실제 객체에 걸려있지 않습니다.
따라서 실제 클래스에 변형을 가해도 사용하는 곳에서는 인터페이스를 참조하고 있기 때문에 문제가 없습니다. (이 부분이 사용하시는 분에 따라 호불호가 갈리는 부분입니다)

객체지향의 확장판이라고 생각하는 DDD(도메인 주도 설계)에서는 도메인끼리 응집하라고 합니다.
같은 업무를 하는 프로세스끼리의 도메인 이기도 하겠지만, 이것을 코드에 접목해보면 인터페이스를 이용하여 개발했을 때 결합도를 낮추고 응집도를 높이는 효과가 있습니다.
물론 너무 심하게 추상화를 해버리면 개발 인원이 적을 경우 디버깅에 불편을 겪을 수 있기 때문에 적당한 추상화가 좋다고 생각합니다.

IoC Container의 Inversion of Control 가 나오기 이전부터, 이미 객체지향 개발의 아이덴티티인 SOLID 패턴의 마지막 패턴, DIP가 존재했습니다.
객체지향의 꽃은 의존성과 추상화를 어떻게 다루느냐 인데, 인터페이스는 이 두 가지를 적절하게 해결해 줄 수 있는 방법입니다.
추상화를 통해 객체를 클래스로, 클래스를 인터페이스로 추상화하여 중복소스코드를 줄이고, 강한 타입 참조를 제거하여 유지보수에 대한 유연성을 제공합니다. (물론 제공할 필요가 없는 곳에서는 굳이 안해도 됩니다.)

여기서 IoC Container와 Interface를 함께 사용하면 구현 은닉과 의존성을 둘 다 챙길 수 있습니다.
IoC Container를 사용하면 통상 생성자 주입을 통해 타입을 제공 받게 되는데, 생성자의 parameter가 적다면, IoC Container의 이점을 크게 못 누릴 수 있지만, 엄청나게 많다면, IoC Container의 장점을 제대로 누릴 수 있습니다.

예를 들면,

public class A {}
public class B {}
public class C {}
public class D {}
public class E {}
public class F {}
public class G {}

public class Alphabet
{
    public alphabet(
        A a,
        B b,
        C c,
        ...
    }
}

Alphabet alphabet = new(
    new A(),
    new B(),
    new C(),
    ...
);

위와 같이 생성해야만 합니다.
여기서 A B C D E F G class가 생성자가 각각 3개씩 있게 바뀐다면…우리는 코드를 이곳저곳을 많이 수정해줘야합니다.

IoC Container를 사용한다면

service.AddTransient<A>();
service.AddTransient<B>();
service.AddTransient<C>();
service.AddTransient<D>();
service.AddTransient<E>();
service.AddTransient<F>();
service.AddTransient<G>();
service.AddTransient<Alphabet>();

Alphabet alphabet = ServiceCollection.GetService<Alphabet>();

이렇게 되어버리니 생성자 수정으로 인해 코드를 수정하는 일이 적어집니다.
게다가 클래스를 직접 등록하는 게 아니라 인터페이스를 등록한다면,

public interface IA {}
public interface IB {}
public interface IC {}
public interface ID {}
public interface IE {}
public interface IF {}
public interface IG {}

public class A : IA {}
public class B : IB {}
public class C : IC {}
public class D : ID {}
public class E : IE {}
public class F : IF {}
public class G : IG {}

public interface IAlphabet {}

public class Alphabet : IAlphabet
{
    public Alphabet(
        IA a,
        IB b,
        IC c,
        ...
    )
}


service.AddTransient<IA, A>();
service.AddTransient<IB, B>();
service.AddTransient<IC, C>();
service.AddTransient<ID, D>();
service.AddTransient<IE, E>();
service.AddTransient<IF, F>();
service.AddTransient<IG, G>();
service.AddTransient<IAlphabet, Alphabet>();

IAlphabet alphabet = ServiceCollection.GetService<IAlphabet>();

이렇게 하면 코드가 변경되면서 발생하는 모든 의존성이 다 해결되게 됩니다.
물론 이렇게 했을 때 인터페이스에 참조가 걸리기 때문에, 인터페이스와 DI에 익숙하지 않은 사람이라면 디버깅이 어려울 수 있습니다.
하지만 인터페이스라는 것은 쓰면 쓸 수록 잘 쓸 수 있게 되고, 추상화를 위해 꼭 필요한 도구이며, 코드만 많아지는 쓸모없는 기능이라고 생각이 안 들게되며, 협업이 반드시 필요한 기능으로 느껴질 것이라고 생각합니다.


다 쓰고 댓글을 달고 다시 봤더니, 글의 요지는 Interface와 상관이 없는 내용인데 IoC Container에 대해 설명하다가 의존성 얘기를 하면서 Interface 삼천포로 빠져버리는 설명이었군요…;

아무튼…결론은 저는 찬성입니다…!

16개의 좋아요