[Go] Method Expression과 Partial Application
Go 언어에서는 메서드를 익명 함수로 다룰 수 있는 독특한 기능을 제공합니다. 이 기능을 통해 특정 메서드를 변수에 할당하거나, 수신자(receiver)를 고정한 형태로 재사용할 수 있습니다. 이 글에서는 Go의 메서드 표현(method expression), 메서드 값(method value), 그리고 이와 관련된 함수 커링(Currying) 개념과 그 활용 방법을 정리합니다.
메서드 표현과 메서드 값
Go에서는 메서드를 함수처럼 사용할 수 있습니다. 이때 메서드를 참조하는 방법에 따라 두 가지 방식이 존재합니다.
1. 메서드 표현 (Method Expression)
메서드를 타입.메서드
형식으로 참조하면, 이는 일반 함수처럼 사용할 수 있으며 수신자 인자를 첫 번째 인자로 명시적으로 받습니다.
var f func(Foo, string) string = Foo.bar
- 이 경우
bar
는Foo
타입의 메서드이며, - 함수는
(Foo, string) string
형식으로 동작합니다.
2. 메서드 값 (Method Value)
메서드를 값.메서드
형식으로 참조하면, 해당 값이 수신자로 고정된 익명 함수로 변환됩니다.
foo := Foo{a: 3, b: "hello"}
var f func(string) string = foo.bar
foo
는 수신자로 고정되므로,f
는(string) string
형식으로 호출 가능합니다.
예제 코드
package main
import "fmt"
type Foo struct {
a int
b string
}
func (f Foo) bar(str string) string {
return fmt.Sprintf("%d, %s, %s", f.a, f.b, str)
}
func main() {
foo := Foo{a: 3, b: "hello"}
var f1 func(Foo, string) string = Foo.bar // Method Expression
var f2 func(string) string = foo.bar // Method Value
fmt.Println(f1(foo, "world")) // 출력: 3, hello, world
fmt.Println(f2("world")) // 출력: 3, hello, world
}
부분 적용과 커링에 대한 오해
위에서 본 foo.bar
의 형태는 커링(Currying)이라기보다는 **부분 적용(Partial Application)**에 가깝습니다. 커링은 다중 인자 함수를 단일 인자 함수들의 체인으로 바꾸는 것을 말하며, Go에서는 함수가 하나의 인자를 반환하는 방식으로 구현되지는 않습니다.
즉, Go는 함수 커링을 직접적으로 지원하지 않지만, 메서드 값을 통해 수신자를 고정하여 유사한 효과를 얻을 수 있습니다.
포인터 리시버의 경우
메서드가 포인터 리시버일 경우, (*Type).method
로 명시해야 메서드 표현으로 사용할 수 있습니다.
func (f *Foo) baz(str string) string {
return fmt.Sprintf("%d - %s - %s", f.a, f.b, str)
}
var f3 func(*Foo, string) string = (*Foo).baz
이런 방식은 포인터와 값의 메서드 셋(method set) 개념에 기반합니다. *Foo
는 Foo
의 메서드뿐 아니라 포인터 수신자 메서드도 포함합니다.
실전 적용: Gin 프레임워크에서의 활용
이 특성을 활용하면 웹 프레임워크에서 컨트롤러 구조를 깔끔하게 구성할 수 있습니다. 예를 들어 Gin에서는 다음과 같은 방식으로 메서드 값을 직접 핸들러로 등록할 수 있습니다.
package main
import (
"database/sql"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
_ "github.com/mattn/go-sqlite3"
)
type UserController struct {
db *sql.DB
}
func (c *UserController) SetUserInfo(ctx *gin.Context) {
fmt.Fprintln(ctx.Writer, "User info set")
}
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
defer db.Close()
controller := &UserController{db: db}
r := gin.Default()
r.GET("/", func(ctx *gin.Context) {
fmt.Fprintln(ctx.Writer, "Index page")
})
r.GET("/user", controller.SetUserInfo)
r.Run()
}
장점
- 메서드를 그대로 핸들러에 등록할 수 있음
- 의존성(DB 등)을 구조체에 묶을 수 있어 테스트 및 확장 용이
- 익명 함수 대신 명확한 책임 분리 가능
정리
- Go의 메서드는 함수로 표현 가능하며, 타입/값에 따라
method expression
또는method value
로 나뉜다. method value
는 특정 수신자를 고정한 형태로, 일종의 부분 적용을 지원한다.- 포인터 리시버를 사용할 때는
(*Type).Method
와 같이 명시적으로 표현해야 한다. - 이 기능은 프레임워크에서의 핸들러 패턴 설계, 의존성 주입, 테스트 용이성 향상 등 실용적인 장점을 제공한다.
Go 언어의 이러한 설계는 함수형 프로그래밍의 요소를 일부 흡수한 것으로, 효율적이고 유연한 코드를 작성할 수 있게 돕는다.