“표현 문제(Expression Problem)“는 프로그래밍 언어 설계 분야의 고전적인 난제입니다. 이 글에서는 Featherweight Go 논문에 제시된 아이디어를 참고하여 제네릭을 사용해 Go에서 이 문제를 해결하는 접근법을 살펴보고, 현재 Go 구현이 지닌 한계를 짚어보겠습니다.
이 과제를 이해하려면 Featherweight Go 논문에서 이를 어떻게 정의하는지 살펴보는 것이 도움이 됩니다.
“목표는 경우(cases)에 따라 데이터 타입을 정의하되, 기존 코드를 재컴파일하지 않고 정적 타입 안전성을 유지하면서 데이터 타입에 새로운 경우를 추가하고 그 데이터 타입에 대한 새로운 함수를 추가할 수 있게 하는 것이다.”
논문은 이를 시각적으로 이해하기 쉽게 훌륭한 비유를 들어 설명합니다.
“언어가 표현 문제를 해결할 수 있는지 여부는 그 언어의 표현력을 보여주는 중요한 지표다. 표에서 경우(cases)를 행(row)으로, 함수를 열(column)로 생각할 수 있다. 함수형 언어에서는 행(데이터 타입 선언의 경우들)은 고정되어 있지만 새로운 열(함수)을 추가하기가 쉽다. 객체지향 언어에서는 열(클래스 선언의 메서드들)은 고정되어 있지만 새로운 행(하위 클래스)을 추가하기가 쉽다. 우리는 행이나 열 어느 쪽이든 쉽게 추가할 수 있기를 원한다.”
요컨대, 진정한 해결책이 되려면 기존 소스 코드를 수정하지 않고도 코드에 새로운 데이터 변형(행)과 새로운 연산/동작(열)을 모두 손쉽게 추가할 수 있어야 합니다.
초기 Featherweight Go 초안에서 제안된 해결책은 최종 Go 1.18 제네릭 릴리스에 포함되지 않은 기능에 의존하고 있습니다.
구체적으로, 논문의 이상적인 해결책은 기본 구조체(struct)에 정의된 것보다 메서드 리시버(receiver)에 더 엄격한 타입 제약 조건(type constraints)을 적용할 수 있다고 가정합니다. 예를 들어 이상적으로는 다음과 같이 작성하고 싶을 것입니다.
// 가상의 코드 (유효한 Go 문법이 아님):
// 메서드 리시버에 직접 제약 조건 적용하기
func (e Plus[T Evaler]) Eval() int
func (e Plus[T Stringer]) String() string
하지만 Go의 실제 제네릭 구현에서는 메서드 리시버의 타입 제약 조건이 구조체의 타입 정의와 완벽하게 일치해야 합니다. 메서드별로 새로운 제약 요건(Evaler나 Stringer 등)을 개별적으로 추가할 수 없습니다.
메서드 수준에서 타입 매개변수를 제약할 수 없기 때문에, 어쩔 수 없이 구조체 수준에서 제약을 걸어야 합니다.
아래는 Go 1.18 이상의 제네릭에서 완벽하게 동작하도록 수정한 코드 예제입니다. 하지만 결합된 Expr 인터페이스를 미리 정의해야만 한다는 점에 주목하세요. 새로운 연산을 추가하고 싶을 때마다 이 인터페이스를 수정해야 하므로, 이는 표현 문제에 대한 진정한 해결책이 아니라 논문에서 응용한 실용적인 우회법에 해당합니다.
package main
import (
"fmt"
)
// 1. 기본 데이터 타입 정의
type Num struct {
value int
}
// 2. 연산(열) 정의
type Evaler interface {
Eval() int
}
type Stringer interface {
String() string
}
// 기본 타입에 대한 연산 구현
func (e Num) Eval() int {
return e.value
}
func (e Num) String() string {
return fmt.Sprintf("%d", e.value)
}
// 3. 타협점: 결합된 인터페이스를 미리 정의해야 합니다.
// 나중에 새로운 연산을 추가하려면 이 인터페이스를 수정해야 하므로,
// 표현 문제의 핵심 규칙을 위반하게 됩니다.
type Expr interface {
Evaler
Stringer
}
// 4. 제네릭 데이터 타입(행) 정의
// Go는 메서드 수준의 제약을 허용하지 않으므로 여기서 T를 Expr로 제약해야 합니다.
type Plus[T Expr] struct {
left T
right T
}
func (e Plus[T]) Eval() int {
return e.left.Eval() + e.right.Eval()
}
func (e Plus[T]) String() string {
return fmt.Sprintf("(%s+%s)", e.left.String(), e.right.String())
}
// 5. 실행
func main() {
var e Expr = Plus[Expr]{left: Num{1}, right: Num{2}}
fmt.Println(e.Eval()) // 출력: 3
fmt.Println(e.String()) // 출력: (1+2)
}
Go의 제네릭은 매우 강력하며 언어에 꼭 필요했던 유연성을 가져다주었지만, Featherweight Go 논문이 원래 구상했던 방식으로 표현 문제를 자체 해결하지는 못합니다. 제약 조건을 메서드 수준이 아닌 구조체 수준에서 정의하도록 요구하기 때문에, 개발자들은 제네릭 타입에 새로운 연산을 추가할 때 여전히 인터페이스를 사전에 계획하거나 기존 코드를 수정해야 합니다.
카우에의 삶의 방식 오프라인 에디션을 받아보세요—🍏 건강, 💰 부, ❤️ 사랑, 🧘 의미를 최적화하기 위한 실전 가이드입니다.
더 나은 삶을 위한 새로운 에세이, 도서 요약, 실전 가이드를 발행할 때 알림을 받아보세요. 스팸은 없으며 언제든 구독을 취소할 수 있습니다.
질문, 의견 또는 제안이 있으신가요? 아래에 생각을 남겨주세요. 댓글은 GitHub Discussions(광고 및 스팸 없음)를 통해 제공됩니다—댓글이나 반응을 남기려면 아래의 GitHub으로 로그인을 클릭하세요(무료 GitHub 계정은 1분 이내에 만들 수 있습니다).
최종 업데이트: 2026. 10. 11..
면책 조항: 본 사이트의 콘텐츠는 정보 제공 및 교육 목적으로만 제공되며 의학, 심리, 재무, 투자, 세무 또는 법률 자문을 구성하지 않습니다. 여기에 제시된 생각, 큐레이션 및 의견은 전적으로 저 개인의 것이며 고용주의 입장을 대변하지 않습니다. 도서, 강연 및 아티클 요약은 독립적인 교육적 해설이며 각 저자의 지적 재산입니다. 편집, 서식 지정 및 요약을 돕기 위해 AI 도구가 사용될 수 있으며, 정확성을 기하기 위해 최선을 다하고 있으나 오류가 포함될 수 있습니다. 개인정보 처리방침: 본 사이트는 선택적 뉴스레터 구독(follow.it), 댓글(giscus / GitHub Discussions) 및 임베디드 미디어(YouTube)를 위해 제3자 서비스를 사용하며, 해당 서비스들은 각자의 개인정보 처리방침에 따라 방문자의 IP 주소와 쿠키를 처리할 수 있습니다.