그 이상한 한 줄의 Go 코드가 내 주말(그리고 정신)을 구했다

발행: (2025년 11월 30일 오후 03:00 GMT+9)
10 min read
원문: Dev.to

Go에서 인터페이스 준수 검사

Go에서 인터페이스 준수 검사는 타입이 실제로 구현한다고 주장하는 인터페이스를 구현했는지 컴파일 타임에 확인할 수 있게 해 줍니다. var _ http.Handler = (*Handler)(nil)와 같은 간단한 선언을 추가하면, 코드가 실행되기 전에 컴파일러가 구현을 검증하도록 강제할 수 있어, 몇 초 만에 버그를 잡을 수 있습니다.

설정 – 금요일 오후 4:47

그때 나는 마치 10배 개발자처럼 느껴졌습니다. 방금 HTTP 핸들러 시스템을 리팩터링했거든요. 깔끔한 코드. 아름다운 추상화. 셰프의 키스.

배포를 누르고 노트북을 닫았습니다. 주말 모드: 활성화.

배신 – 토요일 새벽 2:13

PING PING PING

내 폰이 크리스마스 트리처럼 빛났습니다. 프로덕션이 다운되었습니다. 사용자들이 500 오류를 받고 있었습니다. 내 핸들러가… 핸들링을 하지 못하고 있었습니다.

알고 보니 ServeHTTP 메서드 시그니처를 ServeHttp(소문자 t) 로 바꿔버렸습니다. Go는 신경 쓰지 않았습니다. 컴파일러는 어깨를 으쓱했습니다. 모든 것이 완벽히 컴파일되었습니다.

하지만 런타임에서는? 내 핸들러는 이제 쓸모없는 메서드만 가진 슬픈 구조체였고, 더 이상 http.Handler를 구현하지 않았습니다. 인터페이스가 깨졌지만 아무도 알려주지 않았습니다.

나는 파자마 차림으로 이를 고쳤고, 여기서는 다시는 말하지 않겠습니다.

반전 – 모든 것을 바꾸는 한 줄

월요일에 시니어 개발자가 내 커밋을 보고 마법 같은 한 줄을 추가했습니다:

var _ http.Handler = (*Handler)(nil)

“이게 무슨 마법이야?” 라고 물었습니다.

“인터페이스 준수 검사야,” 라고 그녀는 커피를 마시며 코드 마법사처럼 말했습니다. “이제 컴파일 타임에 컴파일러가 당신에게 소리칠 거고, PagerDuty가 새벽 2시에 소리치는 일은 없을 거야.”

이 마법이 실제로 동작하는 방식

var _ http.Handler = (*Handler)(nil)

밑줄 _

“값은 신경 쓰지 않아, 타입만 확인해.”
Go가 이걸 검증하고 버려라 라는 뜻입니다.

http.Handler

당신이 구현하고 있다고 생각하는 인터페이스. 당신이 약속하는 것.

(*Handler)(nil)

당신 타입의 제로 값. 포인터라면 nil이고, 구조체라면 {}입니다.

마법: 컴파일러가 당신 타입을 인터페이스 변수에 할당하려고 시도합니다. 타입이 실제로 인터페이스를 구현하지 않았다면 컴파일이 실패합니다. 배포도 안 되고, 프로덕션도 안 되고, 새벽 2시 호출도 없습니다.

실제 이야기: 언제 써야 할까

✅ 반드시 사용해야 할 경우

  • 타입이 export되어 있고 공개 API의 일부로 인터페이스를 구현해야 할 때
  • 여러 타입이 모두 같은 인터페이스를 구현해야 할 때
  • 인터페이스가 깨지면 사용자(또는 주말)가 고통받을 때
  • 깜짝 놀라기보다 잠을 더 소중히 여길 때

🤷 필요 없을 수도 있는 경우

  • 내일 삭제될 짧은 스크립트를 작성 중일 때 (내레이터: 삭제되지 않을 거야)
  • 타입이 private이고 한 곳에서만 사용될 때
  • 런타임 패닉 디버깅을 즐길 때 (도움을 청해라)

제로 값의 종류

타입마다 제로 값이 다릅니다:

// 포인터 타입
var _ http.Handler = (*Handler)(nil)

// 구조체 타입
var _ http.Handler = LogHandler{}

// 인터페이스 타입
var _ io.Reader = (*bytes.Buffer)(nil)

프로 팁: 잘못된 것을 사용하면 컴파일러가 알려줍니다. 바로 그게 핵심입니다.

흔히 하는 실수 (나도 다 해봤음)

“나중에 추가할게” 실수

type Handler struct {
    // TODO: 인터페이스 체크 추가
}

func (h *Handler) ServeHttp(...) { // ← 오타, 컴파일은 됨
    // This is fine.jpg
}

내레이터: 전혀 괜찮지 않았습니다.

“한 가지만 바꿨다” 실수

// 메서드에 새로운 파라미터를 추가함
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, ctx context.Context) {
    // ...
}

체크 없이: 컴파일됨! 런타임 패닉!

체크와 함께: 컴파일되지 않음. 누군가 눈치채기 전에 고칠 수 있습니다.

FAQ: 궁금한 점들

왜 해킹 같은 느낌이 들까요?
왜냐하면 보기에 이상하기 때문입니다. 사용하지 않을 변수를 선언하고, nil 포인터를 할당하고, 이 모든 것이 컴파일러를 화나게 하기 위해 존재합니다. 하지만 공식적인 Go 관용구이니 안심하세요.

컴파일러가 그냥 알면 안 될까요?
Go의 인터페이스는 암시적으로 만족됩니다. implements 같은 키워드가 없죠. 이것이 특징입니다! 실제로 준수가 중요한 순간에 명시적인 검사가 필요합니다.

이걸 빼먹으면 어떻게 되나요?
아무 일도 일어나지 않습니다! 뭔가 문제가 생길 때까지요. 코드가 몇 주, 몇 달 잘 돌아갈 수도 있습니다. 어느 날 리팩터링을 하다가 프로덕션에서 런타임 패닉이 발생합니다. 어떻게 알게 되었는지는 비밀이죠.

런타임 오버헤드가 있나요?
전혀 없습니다. 제로. 체크는 컴파일 타임에만 일어나고, 변수 자체는 최종 바이너리에 존재하지도 않습니다.

결론

그 한 줄—var _ http.Handler = (*Handler)(nil)—은 컴파일 타임 보험 정책입니다. 코드가 컴파일되지 않는다 (성가심)와 코드가 프로덕션에서 패닉을 일으킨다 (커리어 파멸)의 차이를 만들어 줍니다.

  • 우아한가? 논쟁의 여지가 있습니다.
  • 필요하냐? 밤새 푹 잘 자고 싶다면 .
  • 당신을 전문가처럼 보이게 하나? 확실히 그렇습니다.

마무리: 트와일라이트보다 나은 러브 스토리

Go 타입 시스템과의 관계가 이렇게 변했습니다:

: “왜 내가 틀렸는지 바로 알려주지 않아?”

: “컴파일 타임에 틀렸다고 알려줘서 고맙다, 새벽 2시에 알게 해 주는 대신에 말이야.”

이제 모든 핸들러에 이 인터페이스 체크를 넣습니다. 내 안전망이자 가드 레일, 그리고 미래의 나에게 하는 내가 말했지 입니다.

솔직히 말해서, 이게 있으면 더 잘 잡니다.

당신도 “내가 살린 한 줄 코드” 이야기가 있나요? 댓글에 남겨 주세요. PagerDuty와 나쁜 선택이 얽힌다면 보너스 포인트!

P.S. – 아직 인터페이스 준수 검사를 쓰지 않는다면 제가 도와줄 수 없습니다. 하지만 당신의 온콜 로테이션은 도와줄 수 없겠죠.

Back to Blog

관련 글

더 보기 »