Go로 센스있는 패키지를 만들기 위해 꼭 알야아하는 숨겨진 interface들

Go는 interface 구현을 duck-typing 으로 사용한다. 즉, Go에서는 어떤 클래스가 해당 인터페이스에 해당하는지를 체크할 때, ‘이 클래스는 해당 인터페이스를 구현해요’ 라고 선언하는 것이 아닌, 해당 인터페이스에서 요청하는 모든 함수를 구현했을 때를 기준으로 한다는 것이다.

이러한 특징으로 인해, 특정한 함수를 구현해놓으면, go언어의 기본 패키지에서 해당 함수를 사용해주는 일이 일어난다. go의 패키지 중에는 어떠한 인터페이스를 구현했는지를 보고, 구현되어 있으면 이를 먼저 사용하는 형식으로 구현을 하기 때문이다.’ 그렇기 때문에, 몇몇 인터페이스를 잘 알고있으면, go의 주요 패키지에서 더 자연스럽게 결합되도록 프로그래밍 할 수 있을 것이다. 외부에 패키지를 공개할 예정이라면, 더욱 신경써야 할 포인트이다. 이 글에서는 이러한 함수들을 몇 개 설명해보고자 한다.

첫번째로 소개할 인터페이스는 fmt.Stringer 인터페이스이다. 이 인터페이스는 아래처럼 정의된다.

type Stringer interface {
   String() string
}

이 인터페이스를 구현하면, go에서 fmt.Println처럼 어떤 struct를 출력해야 하는 순간에 해당 함수의 결과값으로 사용한다. 디버그 용으로 해당 struct를 출력할 일이 있다면, 이 함수를 적절하게 구현한다면 센스있는 패키지로 거듭날 수 있을 것이다.

그 다음 소개할 인터페이스는 json.Marshaler, json.Unmarshaler 이다. 두 인터페이스는 아래처럼 정의된다.

type Marshaler interface {
	MarshalJSON() ([]byte, error)
}

type Unmarshaler interface {
	UnmarshalJSON([]byte) error
}

encoding/json 패키지는 웹에서 데이터 입출력을 위해 사용되는 JSON 값을 struct로 쉽게 변환하기 위해 사용하는 패키지이다. 이 때 struct의 필드가 JSON의 어떤 필드에 해당하는지를 표시하기 위해 struct tag를 사용하는 방법은 널리 알려져있다. 다만 JSON 모델의 필드와 struct의 필드가 1:1 대응이 되지 않는 상황이나, 단순 타입변환 이상의 전환이 필요하다면, 이 함수들을 주목해야 한다.

우리의 struct가 json.Marshaler 인터페이스를 구현한다면, json 패키지는 struct를 JSON으로 바꿀 때 MarshalJSON() ([]byte, error) 함수를 우선적으로 사용한다. 마찬가지로 JSON을 struct로 바꿀 때에도 json.Unmarshaler 가 구현된 struct라면 UnmarshalJSON([]byte) error 를 우선적으로 사용한다. 내 struct에 맞는 특수한 json 입출력 설정이 필요하다면, 이 함수들에게서 도움을 받아보길 추천한다.

마지막으로 설명할 친구는 조금 더 특별하다. 바로 Unwrap() error 함수이다. 이 함수의 역할은 go1.13 에서 소개된 새로운 error 처리를 돕는다. go 공식블로그글 보기

추후 다른 포스트에서 설명하겠지만, go에서 다른 에러를 Wrap 할 수 있는 기능이 추가되었다. 따라서 Wrap된 에러를 Unwrap 할 수 있도록 Unwrap() error 함수가 제공된다면, errors 패키지의 Is(err, target error) bool 이나 As(err error, target any) bool 함수에서 활용되게 된다. Is(err error) boolAs(err interface{}) boolUnwrap() error와 마찬가지로 struct에서 구현하게 되면, 우선적으로 참조하게 된다.

이 함수가 특별한 이유는 인터페이스화 되어있지 않다는 것이다. errors 패키지 내에서 어떻게 활용되는지 확인 해보면,

func Unwrap(err error) error {
	u, ok := err.(interface {
		Unwrap() error
	})
	if !ok {
		return nil
	}
	return u.Unwrap()
}

처럼 임시로 인터페이스를 만들어서, 비교하는 형태를 활용하고 있다. 이유는 여기 에서 밝히기로는, 네이밍 이슈 때문이라고 한다.

나는 아직 패키지를 만들어서 외부에 공개한 적은 없지만, 다른 패키지들을 보면서 좋은 패키지들은 저 위의 인터페이스들을 적절히 구현해두었다는 것을 알게 되었다. 특히 저런 함수들은 사용되는 시점이 다른 패키지안에 숨어있기 때문에, 잘 모르는 보면 쉽게 지나치기 쉬운 포인트이다. 이러한 디테일이, 센스있는 패키지를 만드는 것이 아닐까.