본문 바로가기

Go

Go에서 의존성 주입을 받는 방법

비즈니스 레이어를 테스트하려면 외부 라이브러리에 의존하지 않는 순수한 비즈니스 로직만이 존재하는 것이 유리하다.

 

그래서 꼭 필요한 외부 라이브러리의 경우 유연성을 위해 의존성을 외부에서 주입 받는 것으로 해결한다.

 

스프링의 경우 IOC 컨테이너를 통한 DI가 있지만, Go는 따로 지원해주는 것이 없기 때문에 외부에서 직접 주입해줘야한다.

 

아래의 코드는 olivere라는 오픈소스와 강한 결합을 맺고있기 때문에 테스트를 짜기가 쉽지않다.

 

 

package handler
 
import (
    "context"
    "encoding/json"
    "github.com/2tsumo-hitori/sample-api/config/esclient"
    "github.com/2tsumo-hitori/sample-api/model"
    "github.com/2tsumo-hitori/sample-api/util"
    "github.com/olivere/elastic/v7"
    "log"
)
 
func SearchByKeyword[T model.Response](searchKeyword string, resp *[]T) {
    suggestKeyword := searchKeyword
 
    ch := make(chan bool)
    go BuildSuggestQuery(&suggestKeyword, ch)
 
    q := util.Queue{}
 
    _, s := util.InspectSpell(searchKeyword)
 
    q.Enqueue(elastic.NewMatchQuery(movieNmText, s))
    q.Enqueue(elastic.NewMatchQuery(movieNmEngToKor, s))
    q.Enqueue(elastic.NewMatchQuery(movieNmKorToEng, s))
 
    sendRequestToElastic(q, resp)
 
    if len(*resp) != 0 {
        return
    }
 
    select {
    case <-ch:
        q := util.Queue{}
        q.Enqueue(elastic.NewMatchQuery(movieNmText, suggestKeyword))
        sendRequestToElastic(q, resp)
        close(ch)
    }
}

이렇게 되면 테스트를 할 때 es와 통신하기 위한 Config를 따로 설정해줘야하고, 실제 테스트가 필요하지 않은 olivere 라이브러리도 같이 테스트하게된다.

 

의존성 주입의 본질은 인터페이스이다.

 

인터페이스 구현을 통한 다형성 파라미터의 주입으로 의존성 주입을 해결한다.

 

type SearchService interface {
    BuildSuggestQuery(suggestKeyword *string, ch chan bool)
    BuildMatchQuery(searchKeyword string, q *util.Queue, fields ...string)
    SendRequestToElastic(q util.Queue, resp *[]model.SearchResponse)
    QueryBuildByKeyword(searchKeyword string) elastic.Query
}
 
type DefaultElasticsearchService struct {
    client *elastic.Client
}
 
func NewDefaultElasticsearchService() SearchService {
    return &DefaultElasticsearchService{
        client: esclient.Client(),
    }
}
 
.
.
.
 
 
func (es *DefaultElasticsearchService ) BuildSuggestQuery(suggestKeyword string, ch chan bool) {
    // BuildSuggestQuery의 구현
}
 
func (es *DefaultElasticsearchService ) BuildMatchQuery(searchKeyword string, q *util.Queue, fields ...string) {
    // BuildMatchQuery의 구현
}
 
func (es *DefaultElasticsearchService ) SendRequestToElastic(q util.Queue, resp *[]model.SearchResponse) {
    // SendRequestToElastic의 구현
}
 
func (es *DefaultElasticsearchService) QueryBuildByKeyword(searchKeyword string) elastic.Query {
    // QueryBuildByKeyword의 구현
}

 

func (controller *Controller) MovieSearch(c *gin.Context) {
    var requestBody model.MovieRequest
    var movies []model.SearchResponse
 
    if err := c.ShouldBindJSON(&requestBody); err != nil {
        panic(err)
    }
    // ********* 상위 클래스에서 의존성(구현 객체)을 주입 **********
    handler.SearchByKeyword(requestBody.MovieNm, &movies, elasticsearch.NewDefaultElasticsearchService())
 
    c.JSON(http.StatusOK, response.NewResponse(movies))
}
 
.
.
.
 
// ********* 하위 클래스에서는 인터페이스를 의존 ***********
func SearchByKeyword(searchKeyword string, resp *[]model.SearchResponse, es elasticsearch.SearchService) {
    suggestKeyword := searchKeyword
 
    ch := make(chan bool)
    go es.BuildSuggestQuery(&suggestKeyword, ch)
 
    q := util.Queue{}
 
    _, s := util.InspectSpell(searchKeyword)
 
    es.BuildMatchQuery(s, &q, movieNmText, movieNmEngToKor, movieNmKorToEng)
    es.SendRequestToElastic(q, resp)
 
    if len(*resp) != 0 {
        return
    }
 
    select {
    case <-ch:
        q := util.Queue{}
        es.BuildMatchQuery(suggestKeyword, &q, movieNmText)
        es.SendRequestToElastic(q, resp)
        close(ch)
    }
}

 

위의 코드와 달라진 점은 더이상 외부 라이브러리에 의존하지 않는 점이다.

 

SearchByKeyowrd 라는 함수에서는 인터페이스만 의존할 뿐, 실제로 어떤 구현체를 사용하는지는 관심 밖이다.

 

외부 라이브러리에 의존하지 않기 때문에 유지보수성이 상승했으며, 좀 더 쉽게 테스트를 짤 수 있게 되었다.

 

 

 

위의 개선된 코드에서는 구체적인 라이브러리에 대한 의존을 다형성 파라미터로 받는 것으로 해결했는데, 다른 방식으로 의존성 주입을 해결할 수 있다.

 

구조체 안에 인터페이스 필드를 담고, 해당 구조체의 메서드로 만드는 전략이다. Spring으로 따지자면 생성자 주입과 비슷한 개념인 것 같다.

 

type DefaultService struct {
    Es elasticsearch.SearchService
}
 
func (ds DefaultService) SearchByKeyword(searchKeyword string, resp *[]model.SearchResponse) {
    suggestKeyword := searchKeyword
    q := util.Queue{}
    ch := make(chan bool)
 
    go ds.Es.BuildSuggestQuery(&suggestKeyword, ch)
 
    _, s := util.InspectSpell(searchKeyword)
 
    ds.Es.BuildMatchQuery(s, &q, movieNmText, movieNmEngToKor, movieNmKorToEng)
    ds.Es.SendRequestToElastic(&q, resp)
 
    if len(*resp) != 0 {
        return
    }
 
    select {
    case <-ch:
        ds.Es.BuildMatchQuery(suggestKeyword, &q, movieNmText)
        ds.Es.SendRequestToElastic(&q, resp)
        close(ch)
    }
}

 

SearchByKeyword는 더 이상 es 관련 파라미터를 받지않고 외부에서 초기화된 구조체의 필드값을 참조해서 로직을 수행한다.

 

var esService handler.DefaultService
 
func init() {
    esService = handler.DefaultService{Es: elasticsearch.NewDefaultElasticsearchService()}
}
 
// MovieSearch 함수는 영화 검색을 제공합니다.
// @Summary 영화 검색
// @Description 검색 키워드에 해당되는 영화 목록을 제공합니다.
// @ID MovieSearch
// @Accept  json
// @Produce  json
// @Param request body model.MovieRequest true "영화 검색 요청 정보"
// @Success 200 {array} model.AutoCompleteResponse "검색된 영화 목록"
// @Router /es/search [post]
func (controller *Controller) MovieSearch(c *gin.Context) {
    var requestBody model.MovieRequest
    var movies []model.SearchResponse
 
    if err := c.ShouldBindJSON(&requestBody); err != nil {
        panic(err)
    }
 
    esService.SearchByKeyword(requestBody.MovieNm, &movies)
 
    c.JSON(http.StatusOK, response.NewResponse(movies))
}

상위 클래스에서 해당 구조체를 초기화 하는 방법에는 여러가지가 있겠지만, init 시점에 초기화 해주는 것으로 했다.

 

Spring의 생성자 주입과 비슷하다고 말했는데, 실질적으로는 런타임 시점에서 init 메서드 초기화 이후 esService 변수에 대입된 값을 바꿔 줄 수 없기 때문에 필드 주입과도 비슷한 것 같다.

'Go' 카테고리의 다른 글

고루틴을 사용해서 검색 API의 성능을 끌어올리기  (1) 2023.11.01