본문 바로가기
Dev/Go

Go와 의존성 주입

by Sovereign 2024. 7. 5.

의존관계 역전 원칙 (Dependency Inversion Principle, DIP)

문제를 작은 단위로 분할해서 해결책을 찾아내는 것은 소프트웨어 엔지니어링의 기본적인 접근법 중 하나다. 여기서 중요한 것은 분할한 문제들 간에 연결 고리를 약하게 하는 것이다. 각 문제의 의존 관계를 제거하고 분할된 작은 문제들을 분담해서 병렬로 문제를 해결할 수 있다.

 

상위 개념의 문제를 하위 개념의 문제와 독립해서 해결하기 위한 방법으로, SOLID 원칙 중 하나인 의존관계 역전 원칙(Dependency Inversion Principle, DIP)이 있다. 클린 소프트웨어에서는 다음과 같이 정의한다.

상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다. 추상화는 상세 구현에 의존해서는 안 된다. 상세 구현이 추상화에 의존해야 한다.

 

 

 

확장성과 유지 관리 효율이 높은 소프트웨어를 실현해 주는 핵심은 구조화와 적절한 경계 정의다. 대상은 타입 또는 패키지로 구조화하고 각각의 경계를 약한 결합으로 만들어서 유연한 설계를 실현할 수 있다. 의존 관계 역전의 원칙을 활용함으로써 타입 또는 패키지 간 약한 결합을 만들 수 있다.

 

Go 언어는 구체형 타입으로 선언해서 구현하는 인터페이스를 명시적으로 기술하지 않는 암묵적 인터페이스 구현만 지원한다. 따라서 상세 구현을 사용하는 측이 인터페이스를 정의한다. 하지만 Go 언어에서도 특정 인터페이스를 골라서 사용되는 것을 고려한 구현이 존재한다. 예를 들어  `database/sql/driver` 패키지의 인터페이스와 구현이 이에 해당한다.

 

 

database/sql/driver 패키지의 DIP

Go의 표준 패키지 중 하나인 "database/sql/driver" 패키지는 DIP(Dependency Inversion Principle)를 이용한 전형적인 설계를 취하고 있다.

 

보통 Go에서 MySQL 등의 RDBMS를 처리할 때는 "database/sql" 패키지를 통해 처리한다. "database/sql" 패키지는 데이터베이스에 접근하고 쿼리를 실행하는 방법을 제공한다. 그러나 이 패키지 자체는 특정 데이터베이스(DBMS, 예: MySQL, PostgreSQL)에 대한 구체적인 구현을 포함하고 있지 않다.

대신, "database/sql" 패키지는 상용 DB 및 오픈소스 DB의 개발 사항에 대응하는 구체적인 인터페이스를 정의하고 있다.

 

그렇다면 어떻게 MySQL이나 PostgreSQL을 처리하는 걸까?

 

각 데이터베이스에 접근하려면 해당 데이터베이스와 소통할 수 있는 드라이버가 필요하다. 드라이버 패키지는 특정 데이터베이스(DBMS)에 맞춰져 있으며, "database/sql/driver" 인터페이스를 구현한다. 예를 들어, "github.com/go-sql-driver/mysql" 패키지는 MySQL 데이터베이스에 접근하기 위한 드라이버 패키다.

 

드라이버 패키지는" database/sql/driver" 인터페이스를 구현하여, 데이터베이스와의 구체적인 통신 방법을 정의한다. 따라서, 각 RDBMS에 대응하는 드라이버 패키지를 임포트하면 해당 드라이버가 "database/sql/driver" 인터페이스를 구현하여 database/sql 패키지에 자신을 등록한다. 이렇게 함으로써 "database/sql" 패키지는 다양한 데이터베이스에 대한 일관된 인터페이스를 제공할 수 있다.

 

 

DIP에 준하는 구현

Go에서 DIP에 준해서 구현하는 경우 의존성 주입(Dependency Injection, DI)을 사용한다. 몇 가지 주입 방법을 살펴보자.

 

의존성 주입

의존성 주입은 DIP(Dependency Inversion Principle)를 실신하기 위한 일반적인 수단이다. 자바나 C# 등에는 클래스의 필드 정의의 애너테이션(annotation)을 붙여서 객체(하위 모듈의 상세)를 주입해주는 구조가 프레임워크에 자체에 존재한다.

 

Go에서는 인터페이스에서 추상을 정의하고 초기화 시에 구체적인 상세 구현의 상세 객체를 설정하는 것이 대부분이다. 대표적인 의존성 주입 구현으로 다음과 같은 패턴이 있다.

  • 객체 초기화 시에 의존성 주입하는 방법
  • setter를 준비해서 의존성 주입하는 방법
  • 메서드(함수) 호출 시에 의존성 주입하는 방법

 

다른 언어에서는 생성자 주입(constructor injection, CI)라는 기법을 주로 사용한다. 상위 계층의 객체를 초기화할 때에 의존성을 주입하는 방법이다.

 

 

setter 메서드를 사용해서 초기화와 실제 처리 사이에 의존성을 주입하는 방법이다.

 

 

메서드(함수)의 인수로 의존성을 주입하는 방법이다.

상위 계층의 객체 주기와 상세 구현의 객체 생성 시점이 다를 때는 이 기법을 사용한다.

 

 

  • 내장형을 이용한 DIP

Go는 구조체 안에 다른 구조체나 인터페이스를 내장할 수 있다. 인터페이스를 내장함으로 추상에 의존한 타입을 정의할 수 있다. 인터페이스의 메서드가 호출될 때까지 구현에 의존을 주입하므로 상세 구현이 호출된다. 테스트 코드의 구조체에서 구현을 목(mock)으로 변경해야 하는 경우 자주 사용한다.

 

 

  • 인터페이스를 사용하지 않는 DIP

DIP에서는 클래스의 부모 자식 관계나 인터페이스의 상속 관계를 자주 사용한다. 하지만 구조체를 정의하지 않고 함수형만 사용해서도 이를 실현할 수 있다.

함수형을 이용한 DI에서 Application 구조체 func(int) error 타입의 Apply 필드를 정의한다. Apply 필드는 구현에 의존하지 않는 추상이다. Apply 필드에 함수의 구현을 주입함으로 DIP를 실현하고 있다.

 

 

DIP를 사용하므로 패키지 간, 구조체 간 결합도를 줄일 수 있다. 단, 너무 과도한 추상화는 소스 코드의 가독성을 떨어뜨리거나 반복적 작업을 유도할 수 있다.

 

 

과도한 추상화(인터페이스 활용) 주의

데이터베이스에 의존하고 싶지 않은 경우 등에는 DI의 용법 및 사용 빈도 등을 적절히 지켜가며 사용해야 한다. 특히 인터페이스를 사용한 과도한 추상화에 주의해야 한다. Go는 암묵적 인터페이스 구현을 채택한다. 자바나 PHP에서 하는 것처럼 implements를 사용해 구체형에 인터페이스명을 기재할 수 없다. 따라서 다음과 같은 정보를 얻으려면 IDE 등의 도움이 필요하다.

  • 구조체가 어떤 인터페이스를 상속하는가
  • 인터페이스를 상속하고 있는 구조체가 얼마나 의존하는가

출처

Go 언어로 배우는 웹 애플리케이션 개발 - 예스 24 (yes24.com)

 

Go 언어로 배우는 웹 애플리케이션 개발 - 예스24

베테랑 고퍼가 알려주는 Go 언어 핸즈온 가이드Go 언어로 REST API 웹 애플리케이션을 개발할 때 반드시 알아야 할 지식을 알려준다. 전반부에는 웹 애플리케이션을 개발하기 전에 알아야 할 Go 언

www.yes24.com