Go generic, 드디어 도착!

김성현·2022년 3월 18일
0
post-thumbnail

Go 언어에는 제네릭이 존재하지 않았다.

왜 없는가에 대해서는 인터넷에 검색만 해도 수십페이지 이상이 나올 정도로 논쟁적인 주제였고, 그것을 싫어하는 사람만큼 필요하다는 사람도 많았다.

사실 내 주변에는 필요하다 이야기 하는 사람이 더 많았던 것 같다.

그런데 작년즈음부터 잡작스레 Go 2에서 제네릭을 도입하겠다는 이야기를 하더니,
작년 말에는 갑작스레 1.18버전부터 제네릭을 도입하겠다고 하였다.

물론 이번 1.18에서 도입된 제네릭은 아직 완전한 것 같지는 않다.
내 생각에 가장 필요하다고 생각되지만 현재 없는 기능은 메소드에서 새로운 제네릭을 선언하는 것이다.

이 go 언어에 대한 제안서에 따르면 이는 의도된 사항이라고 한다.
하지만 개인적으로는 이 기능은 필요하다 생각되고, 언젠가는 추가될 것이라 생각하기에 이렇게 썼다.

Go 언어에서 이번에 도입한 제네릭은 언어적으로 큰 변화를 가져올 가능성이 있는 만큼 바로 살펴볼 필요가 있다고 생각된다.

Go에 제네릭이 필요하다 생각하는 이유

정말로 Generic은 낮선, 새로운 기능일까?

일단 공식적으로 Go 1.18 이전에는 제네릭이 존재하지 않았다.

그런데... 정말로?

옛날 옛적, 혹시 JAVA에 제네릭이 없던 시절에 혹시 어떻게 List를 구현했는지 기억나는가?

그당시 자바의 List 클래스는 대략 이런 모습이였다.

class List{
	void add(int index, Object element)
	boolean contains(Object o)
	Object get(int index)
 	int	size()
}

너무 옛날 문서라 오라클에서는 저장된 파일을 다운로드 받아야 해서 이 링크로 대체한다.

우리 모두 알다시피 List에서 어떤 타입이든 List에 넣을 수 있게 하기 위해 모든 클래스들의 부모 클래스인 Object를 이용하는 모습을 볼 수 있다.

만약 Go 였다면 Object 대신 interface{}를 사용했을 것이다.

이를 생각하면서, Go의 append함수에 대해 생각해 보자.

append 함수는 Go 소스코드에 따르면 이렇게 정의되어 있다.

// https://go.googlesource.com/go/+/refs/tags/go1.17.8/src/builtin/builtin.go#103
// Type is here for the purposes of documentation only. It is a stand-in
// for any Go type, but represents the same type for any given function
// invocation.
type Type int

// https://go.googlesource.com/go/+/refs/tags/go1.17.8/src/builtin/builtin.go#134
func append(slice []Type, elems ...Type) []Type

여기서 Type의 주석을 살펴보면 이런 말이 나온다.

represents the same type for any given function invocation

매개변수에 주어진 값의 타입에 따라서, 리턴 타입이 바뀌는 것... 이걸 우리는 무엇이라 불러야 할까?

만약 Go 언어가 제네릭을 정말로, 전혀 지원하지 않았다면 append 함수는 이런 형태였어야 했다.

func append(slice []interface{}, elems ...interface{}) []interface{}

그리고 만약 이런 상태였다면 우리는 append를 할 때마다 이렇게 타입을 강제로 추론시켜야 했을 것이다.

var a = make([]byte, 10)
a = append(a, 1, 2, 3, 4, 5).([]byte)

위의 코드를 보기만 해도 토가 쏠리는데, 만약 진짜로 위와 같이 Go에 제네릭과 같은 요소가 전혀 없었다면 상당히 불편했을 것이다.

이뿐만이 아니다. 추가로 한번 make의 첫번째 매개변수를 자세히 봐 보자.

배열'타입' 아닌가?

매개변수로 타입을 받는 것... 그것이 제네릭이 아니면 뭘까?

만약 make도 진짜 제네릭을 전혀 지원하지 않았다면, C 언어마냥 이렇게 코드를 작성해야 했을 것이다.

var a = make(10 * 1).([]byte)

즉 내가 말하고 싶은 것은,

사실상 Go에는 특정 빌트인 함수에만 적용되는 제네릭과 유사한 문법이 이미 존재했고,

이번에 추가된 제네릭은 단지 이를 '일반화시키는 것'일 뿐이다 라는 것이다.

이런 측면에서 볼 때 본격적인 제네릭의 도입은 나로서는 문법적인 일관성에도 큰 도움이 될 것이라고 여겨져서 매우 기쁘다.

물론 아직 make, append 등은 generic 형태로 전환되지 않았다.
내 생각에 Go 2가 진짜로 나오기 전까지는 빌트인 함수들은 아마 절대로 Go 팀에서 손대지 않을 것이다.

제네릭이 필요한 이유, boilerplate code

Go언어에서 단언코 제일 짜증나는 부분은 sort이다.

Go 언어에서 sort를 구현하려면 다음과 같이 해야 한다.

type StudentMean struct {
	name string
	mean float32
}
type ArrayStudentMean []StudentMean

func (a ArrayStudentMean) Len() int           { return len(a) }
func (a ArrayStudentMean) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a ArrayStudentMean) Less(i, j int) bool { return a[i].mean < a[j].mean }

위 코드에서 만약 StudentMean의 배열을 정렬하고 싶다면 StudentMean[]ArrayStudentMean타입으로 변환하고 sort.Sort함수로 정렬을 수행해야 한다.

별 문제가 아닌거 같아 보일 수도 있다.

사실 별 문제가 아닐수도 있다.

만약, 당신에게 정렬을 해야하는 수십개의 구조체가 존재하지 않는다면.

위의 예시에서 StudentMean타입 하나를 정렬 가능하게 만드는데 사용된 boilerplate는 총 4줄이다.

그러면 단순 산수로, 정렬해야하는 구조체가 총 100개 있다면, boilerplate는 몇 줄이 필요할까?

답은 400줄, 생각만 해도 끔찍한 일이다.

위의 코드를 보면 사실 진짜 중요한 부분은 .Less메서드밖에 없음에도, 나머지 Len, Swap은 정말로 단순반복일 뿐이다.

이는 제네릭이 있다면 간단히 없앨 수 있는 상용구이다.

아직 sort 패키지는 제네릭을 이용하지 않아 여전히 불편하지만, 아마 조만간에 업그레이드 될 것이라 예상한다.


이번에 Go 에서 재미난 기능을 넣어줘서 당분간 즐겁게 공부할 수 있을 것 같다.

개인적으로는 아쉬운 점이 눈에 보였지만 조만간 Go 언어도 강력한 제네릭 지원을 할 것이라 꿈꾸며, 제네릭이 도입되며 생긴 Go 언어의 특징적인 측면들 위주로 한번 포스트를 작성해 보도록 하겠다.

참조

go proposal : type-parameters

profile
수준 높은 기술 포스트를 위해서 노력중...

0개의 댓글