의존성 주입에 대한 오해와 실수들

의존성 주입 Dependency Injection

객체 지향 프로그래밍을 하다 보면, 의존성 주입을 자주 사용하게 됩니다. 의존성 주입이 무엇인지 설명하기 전에, 첫 번째 오해를 먼저 소개합니다.

오해 1: 의존성 주입은 배워야 쓸 수 있다?

의존성 주입이라는 모호한 이름과, 의존성 주입을 사용하면 이러이러한 것이 좋다! 하는 설명들 때문에 의존성 주입을 배워야만 쓸 수 있는 특별한 기법으로 오해하는 경우가 있습니다. 하지만 객체 지향 프로그래밍 언어로 클래스를 정의해본 적이 있다면 이미 사용하고 있을 가능성이 큽니다.

의존성 주입의 핵심을 표현하면 이렇습니다.

객체에게 인스턴스 변수를 넘겨주는 것

따지자면, 아래와 같은 코드도 의존성 주입이죠.

// 의존성 주입 O
class Printer {
public:
  explicit Printer(std::string message) : message_(std::move(message)) {}

private:
  std::string message_;
};

// 의존성 주입 X
class Printer {
public:
  Printer() { message_ = "Hello, World!"; }

private:
  std::string message_;
};

Printer 클래스가 message_ 변수에 담긴 값을 프린트하는 역할이라고 해봅시다. 의존성 주입을 사용하지 않는 아래쪽 Printer 클래스는 “Hello, World!”라는 구체적인 값에 의존합니다. 이러면 클래스를 재사용 할 수 없습니다. 다른 문자열도 프린트하려면 새로운 클래스를 정의해야 합니다. 혹은 Printer 클래스를 수정해야 하는데, 이는 OCP 위반을 뜻합니다.(OCP?)

반면 의존성 주입을 사용하는 위쪽 Printer 클래스는 재사용 할 수 있습니다. 생성자를 통해 의존성을 주입해주기 때문에, 주입된 어떤 값이든 프린트 할 수 있습니다.

이렇듯이 클래스 생성자로 어떤 값을 넘겨주는 코드를 작성한 적이 있다면, 의존성 주입을 이미 사용하고 있었던 거죠! 따라서 “더 좋은 코드를 작성하려면 의존성 주입을 사용해라.”는 틀린 표현입니다. 알맞게 고치면 “더 좋은 코드를 작성하려면 의존성 주입을 잘 이해하고, 제대로 사용해라.”가 되겠죠!

오해 2: 의존성 주입은 다형성에 관한 것이다?

객체 지향을 설명하려면 인터페이스를 빼놓을 수는 없습니다. 그래서인지 의존성 주입을 설명할 때에도 인터페이스가 자주 등장합니다. “구체 클래스에 의존하지 마라, 인터페이스에 의존해라.” 라는 유명한 객체 지향 격언도 자주 인용되구요. 듣다 보면 자연스럽게 의존성 주입은 인터페이스와 다형성에 관한 것이구나, 하고 오해하기 쉽습니다.

하지만 근본적으로 의존성 주입은 인터페이스보다 인스턴스에 관한 것입니다. 이를 설명하기 위해 먼저, 의존성 주입을 설명할 때 보통 사용되는 인터페이스 중심 예시를 보겠습니다.

class System {
public:
  explicit System(IPrinter* printer) : printer_(printer) {}

private:
  IPrinter* printer_;
};

int main() {
  System systemA(new LaserPrinter{});
  System systemB(new InkjetPrinter{});
...
}

익숙한 모양의 예시 아닌가요? 구체 클래스 대신 인터페이스에 의존하고, 직접 생성하는 대신 생성된 객체를 주입하고… 훌륭한 의존성 주입의 예시입니다.

그렇지만 의존성 주입에 꼭 인터페이스가 사용되어야 하는 것은 아닙니다. 아래 코드를 보시죠.

class System {
public:
  explicit System(Printer printer) : printer_(printer) {}

private:
  Printer printer_;
};

int main() {
  System systemA(Printer("Hello, World!"));
  System systemB(Printer("Hello, C++!"));
}

인터페이스나 다형성이 사용되지 않았지만, 여전히 훌륭한 의존성 주입의 예시입니다. “어떤 인스턴스를 사용할 것이냐?”에 대한 의존성이 끊어졌죠. 이로써 System 클래스는 Printer 객체가 어떤 메세지를 출력할 것인가 하는 특정 구성(Configuration)에서 독립될 수 있습니다.

“의존성 주입은 인스턴스에 관한 것이다.” 를 되새기면서, 첫 번째 오해에 대한 설명을 다시 읽어보세요. 인터페이스나 다형성과 관계 없이, 의존성 주입이 무엇이고 어떤 문제를 해결하는지 감이 오시나요?

노파심에 덧붙이면, 의존성 주입이 다형성과 아예 관련이 없다는 이야기는 아닙니다! 다형성은 객체 지향의 핵심이고 그 자체이기 때문에 관련이 없을 수 없죠.

의존성 주입, 흔한 실수 3가지

의존성 주입을 사용할 때, 자주 볼 수 있는 3가지 실수가 있습니다. 의존성 주입을 잘 이해했다면 무엇이 문제인지 직접 알아낼 수도 있습니다.

1. 의존성을 위해 의존성 전달하기

class Model {
public:
  Model(int width, int height)
    : board_(std::make_unique<Board>(width, height)) // Bad
  {} 

  explicit Model(std::unique_ptr<IBoard> board) // Better
    : board_(std::move(board))
  {} 

private:
  Printer printer_;
};

위의 나쁜 경우는 불필요할 뿐 아니라, Board 클래스의 생성자가 변경될 경우 Model 클래스도 수정하도록 만듭니다.

2. 상속 계층간 의존성 운반

class Model : public Service { // Bad
public:
  explicit Model(std::unique_ptr<IBoard> board) // Bad
    : Services(std::move(board))
  {}

  void update() {
    Service::do_something_with_board(); // Bad
  }
};

class Model { // Better
public:
  explicit Model(std::unique_ptr<Service> service) { // Better
    : service_(std::move(service))
  {}

  void update() {
    service_->do_something_with_board(); // Better
  }

private:
  std::unique_ptr<Service> service_;
};

상속을 사용할 때 볼 수 있는 경우입니다. “상속보다는 구성을 사용하라.” 라는 객체지향 격언과도 연결되죠. 항상 필요한 의존성만 전달하도록 하는 것이 좋습니다.

3. Service Locator 패턴

class Model {
public:
  explicit Model(ServiceLocator& sl) // Bad
    : service_(sl.resolve<std::unique_ptr<Service>())
  {}

  explicit Model(std::unique_ptr<Service> service) // Better
    : service_(std::move(service))
  {}

private:
  std::unique_ptr<Service> service_;
};

Service Locator 패턴은 안티 패턴으로 여겨집니다. 모든 생성자의 유일한 인자로 전달되기 때문입니다. 그렇게 되면 코드는 Service Locator 프레임워크에 강하게 결합될 수 밖에 없습니다. 게다가, 클래스의 의존성이 직접적으로 드러나지 않기 때문에 가독성도 해칩니다.

Example reference: Introduction- [Boost::ext].DI

마치며

의존성 주입에 대해 알아봤습니다. 객체 지향 관련 주제를 설명하다 보면 항상 아쉬운 점이 있습니다. 간단한 예시들 만으로는 객체 지향의 여러 요소와 장단점을 설명하기 어렵다는 점입니다. 객체 지향이 해결하는 문제들은 주로 수 십, 수 백만 줄의 코드로 이루어진 대규모 프로젝트에서 나타납니다. 그런데 하나의 글에서 다룰 수 있는 예시들은 주로 (요리사-레시피), (자동차-바퀴, 엔진) 같은 기초적인 것 뿐이죠.

제가 생각하기에 객체 지향을 이해하기에 가장 좋은 방법은 잘 설계된 실제 코드를 작성하고, 수정해보면서 객체 지향이 해결해주는 문제들을 직접 겪어보는 것입니다. 학생이나 개인 입장에서 가장 현실적인 방법은 훌륭한 오픈소스 프로젝트에 참여하는 것일 수 있겠네요!

부족한 글 읽어주셔서 감사합니다. 혹시나 잘못된 내용이 있다면 댓글로 알려주세요!

0 0 vote
Article Rating
구독
Notify of
guest
0 Comments
Inline Feedbacks
View all comments