Go

Go 언어로 웹 애플리케이션 만든 경험담

드리프트2 2024. 3. 1. 20:12

시작하기

Go 언어를 사용하여 웹 애플리케이션을 만들어 보았는데요.

 

스크래치에서부터 작성했기 때문에 작성 도중 계속된 시행착오를 겪으며 최종적으로는 웹 애플리케이션 모양새가 구현될 수 있었습니다.

 

그래서 MVC 애플리케이션을 구성할 때 특히 컨트롤러 계층을 중심으로 몇가지 생각을 정리해 볼까 합니다.

 

** 목 차 **


View

작성한 웹 애플리케이션은 API 서버였기 때문에 실제로 여기에 대한 지식은 많지 않습니다.

 

뷰가 있는 앱을 만든다면 개인적으로는 React 등을 사용하여 프론트엔드 애플리케이션으로 구현하고 Go 부분은 API 서버 역할만 하는게 조금은 쉬운데요.

 

그러나 서버 측에서 HTML을 생성해야 하는 경우 표준 template 패키지를 활용하는 것이 좋습니다.

 

다양한 기능은 없지만 간단하고 특히 초보자가 사용하기 편한 템플릿 엔진입니다.

 

뷰는 아니지만, 환경 변수를 설정 파일에 삽입하는 용도로 이 템플릿 패키지를 사용할 수도 있습니다.


Controller

컨트롤러 계층에 대해서는 먼저 프레임워크를 사용할지 여부를 고려하는 것이 좋은데요.

 

많은 Go 마이크로 프레임워크는 실제로 컨트롤러 부분을 주로 구현하고 있습니다.

 

프레임워크를 사용할지 여부는 프로그래머의 선택인데요.

 

저는 프레임워크를 사용하지 않는 편입니다.

 

그 이유는 일반적으로 웹 애플리케이션 컨트롤러에 필요한 기능이 대부분 표준 패키지로 제공되기 때문입니다.

 

가능한한 표준 패키지를 활용하여 간단하게 구현하는 것이 Go의 방식이라고 생각합니다.

 

컨트롤러 계층에 속하는 요소들은 다음과 같습니다:

  1. 리퀘스트 핸들러 (Request Handler)
  2. 라우팅 (Routing)
  3. 필터 (Filter)
  4. 리퀘스트 컨텍스트 (Request Context)

리퀘스트 핸들러(Request Handler)

리퀘스트 핸들러의 역할은 요청을 처리하고 응답을 반환하는 겁니다.

 

모델 계층의 처리를 호출하여 뷰로 전달하는 것도 리퀘스트 핸들러 내에서 수행됩니다.

 

리퀘스트 핸들러는 표준 http 패키지에서 정의된 http.Handler 인터페이스를 따르면 됩니다.

 

프레임워크를 사용하지 않는 이유 중 하나는 일부 널리 사용되는 프레임워크가 http.Handler 인터페이스를 지원하지 않는 핸들러를 사용하고 있어 표준 패키지와 호환되지 않는다는 단점이 있기 때문입니다.

 

추가로, Go에서는 함수형에도 메서드를 추가할 수 있으므로 http.HandlerFunc 타입으로 캐스팅할 수 있는 함수라면 http.Handler 인터페이스를 구현할 수 있습니다.

 

구조체를 정의하기 귀찮을 때는 http.ServeHTTP 메서드 대신 함수를 만들고 해당 함수를 http.HandlerFunc 타입으로 캐스팅하여 사용할 수 있습니다.

func function(w http.ResponseWriter, r *http.Request) {
  // 이 타입의 함수는 `http.HandlerFunc`와 동일한 인수와 반환 값을 가지므로 캐스팅할 수 있습니다.
  // `http.HandlerFunc`는 `http.Handler` 인터페이스를 구현하고 있으므로
  // 함수를 그대로 핸들러로 사용할 수 있습니다.
}

라우팅(Routing)

웹 애플리케이션에서 컨트롤러의 역할은 사용자 입력인 HTTP 리퀘스트와 모델을 중개하고 연결하는 역할입니다.

 

이런 관점에서 라우팅 처리가 컨트롤러의 중요한 역할이라고 생각합니다.

 

Go의 웹 애플리케이션에서 라우팅 처리를 담당하는 기능을 "Mux" 또는 "루터"라고 합니다.

 

라우팅 처리 부분만은 표준이 아닌 것을 사용하고 있는데요.

 

표준 net/http에 포함된 라우터는 그리 좋지 않기 때문입니다.

 

그러나 간단한 앱의 경우 충분히 사용할 수 있습니다.

 

라우터 라이브러리는 http.Handler 인터페이스를 지원하는 것이 좋습니다.

 

저는 chi를 사용하고 있습니다.

 

또 다른 메이저한 라우터 라이브러리로는 Gorilla 라이브러리의 Mux가 많이 사용되고 있다고 생각합니다.


필터(Filter)

핸들러를 래핑하여 리퀘스트와 리스판스를 수정하는 기능을 통틀어 필터라고 합니다.

 

필터의 예로는 리퀘스트 로그 및 인증 처리가 있습니다.

 

필터는 Middleware라고 불리는 패턴으로 구현하는 것이 좋습니다.

 

Middleware는 http.Handler를 받아들이고 http.Handler를 반환하는 함수입니다.

func filter(next http.Handler) http.Handler {
  // http.HandlerFunc는 함수에 `http.Handler` 인터페이스를 가진 타입으로,
  // 이를 캐스팅하여 `http.Handler`로 반환합니다.
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // 전처리
    next.ServeHTTP(w, r) // 래핑할 대상 `http.Handler`
    // 후처리
  })
}

 

기본적으로 Middleware 함수로 핸들러를 래핑하는 것이 필터의 주요한 작성 방법입니다.

 

라이브러리나 프레임워크에 따라 라우터에 필터를 추가하거나 필터 체인(래핑의 연쇄)을 간단하게 작성할 수 있는 기능도 제공됩니다.

 

리퀘스트 컨텍스트(Request Context)

리퀘스트 핸들러나 필터를 계층적으로 사용하면 리퀘스트 전체에서 공유해야 할 변수가 나타납니다.

 

예를 들어 데이터베이스 핸들러, 세션, 특정 ID를 식별하는 리퀘스트 등이 있습니다.

 

이러한 변수를 보유하는 구조체를 리퀘스트 컨텍스트라고 부릅니다.

 

저는 프로세스 전체에서 공유되는 변수도 전역 변수가 아닌 리퀘스트 컨텍스트에 설정하도록 하고 있습니다.

 

전역 변수가 없으면 테스트하기가 매우 다르기 때문입니다.

 

리퀘스트 내에서 리퀘스트 컨텍스트를 유지하는 데는 표준 context 패키지를 사용하면 됩니다.

 

Go 1.7 이상에서는 리퀘스트 변수 자체에서 리퀘스트별 context.Context을 추출할 수 있으므로 이를 사용하여 리퀘스트 컨텍스트를 저장합니다.

 

그러나 context.Context 구조체 자체에 리퀘스트 컨텍스트 역할을 부여하고 개별 변수를 저장하는 것은 권장하지 않습니다.

 

context.Contextinterface{} 타입을 보유하는 컬렉션처럼 사용할 수 있습니다.

 

반대로 보관된 변수는 interface{} 타입이므로 추출할 때 nil 확인과 캐스팅이 필요합니다.

 

이는 복잡하고 관리하기 어려우므로 리퀘스트 컨텍스트에 대해서는 하나의 구조체에 모아 캐스팅을 한 번만 하도록 하는 것이 좋습니다.

 

추출한 구조체의 멤버는 타입이 지정되어 있으므로 캐스팅이 필요하지 않습니다.

 

정적 타이핑은 Go의 장점이므로 타입 정보가 손실되지 않도록 최대한 피해야 합니다.

 

참고로 interface{} 타입의 캐스팅과 nil 확인은 약간 복잡합니다.

v, ok := context.Value(key).(ValueType)
if !ok || v == nil {
  // 빈 경우 처리
}

 

이는 "interface{}의 nil 값" ≠ "어떤 타입의 nil 값을 interface{} 타입으로 캐스팅한 값"이기 때문에 interface{} 타입의 nil 확인으로 후자의 nil 값을 감지할 수 없습니다.

 

컨트롤러 계층이 길어졌지만 이 부분이 일반적으로 설계에서 혼란스러운 부분입니다.

 

모델 등은 특별한 구성 없이도 웹이 아닌 일반적인 Go 프로그램과 같은 방식으로 설계하면 됩니다.


Model

모델에 대해서는 사람마다 구성 방법이 다를 수 있는 부분입니다.

 

Go에서는 Rails의 ActiveRecord와 같은 모델 생성을 중심으로 하는 라이브러리가 없기 때문에 상당히 자유롭게 구성할 수 있다고 생각합니다.

 

어떤 사람들은 ORM 매퍼를 채택할 수도 있겠지만, 저는 표준 database/sql을 사용하여 데이터 액세스 계층을 구축했습니다.

 

그래서 여기서 말씀드릴 수 있는 내용은 그리 많지 않습니다.

 

제가 주의하고 있는 몇 가지 사항은 다음과 같습니다:

  1. 입력 값에 대한 인터페이스 활용: 테스트 가능성을 높이고 느슨한 결합을 위해 인터페이스 형식을 활용합니다.
  2. 강제적인 객체 지향적인 접근 방식 피하기: 강제적으로 객체 지향적으로 작성하지 않고 패키지 로컬 함수를 사용하여 처리합니다. 패키지 외부에서는 일정한 수준의 객체로 다룰 수 있으면 좋습니다.
  3. 로그: 로그를 요청과 연결하고자 하기 때문에 모델 계층에서는 에러를 로그에 기록하지 않고 일단 리퀘스트 핸들러로 돌려놓은 후에 기록합니다.

Go의 웹 앱은 정해진 방법이 없기 때문에 처음에는 많이 헤메실 수 있습니다.

 

지금까지 Go로 웹 앱 만든 저의 경험담이었습니다.

 

그럼.