[Go] Duck typing 과 side effect 분리, 그리고 테스트
Duck Typing이란?
덕 타이핑(Duck Typing)은 객체가 특정 타입인지 확인하지 않고, 해당 타입처럼 행동하는지를 확인하는 방식입니다. “If it walks like a duck and quacks like a duck, it’s a duck.“이라는 표현에서 유래했습니다.
Go는 정적 타입 언어지만, 인터페이스를 암묵적으로 만족시키는 방식으로 덕 타이핑을 구현합니다.
예를 들어, Read([]byte)
메서드를 구현한 타입은 명시적 선언 없이도 io.Reader
인터페이스로 사용 가능합니다.
type Reader interface {
Read(p []byte) (n int, err error)
}
type MyReader struct{}
func (MyReader) Read(p []byte) (int, error) {
return 0, nil
}
// MyReader는 Reader를 암시적으로 구현함
Go에서의 활용 이점
- 불필요한 인터페이스 선언 제거: 구현체를 작성할 때 인터페이스를 의식할 필요 없음
- 사용자 중심 설계: 인터페이스는 사용하는 쪽에서 정의
- 구체 타입 기반의 명확한 구조: 코드를 이해하고 리팩토링하기 쉬움
인터페이스 제약을 활용한 제너릭 예시
Go 1.18부터 도입된 제너릭 타입과 덕 타이핑을 결합하면 다음과 같은 패턴으로 추상화가 가능합니다.
type pinger interface {
Ping() error
}
func FromCfg[T pinger](config Config, f func(Config) (T, error)) (T, error) {
instance, err := f(config)
if err == nil {
err = instance.Ping()
}
return instance, err
}
이 방식은 sql.DB
, memcache.Client
등 공통 Ping()
메서드를 갖는 객체의 초기화에 효과적입니다.
func sql(config Config) (*sql.DB, error) {
// db 초기화
}
func Sql(config Config) (*sql.DB, error) {
return FromCfg(config, sql)
}
Side Effect 분리
사이드 이펙트는 외부 상태 변경이나 입출력을 포함하는 작업입니다. Go에서는 인터페이스나 함수 주입을 통해 사이드 이펙트를 로직에서 분리하는 것이 중요합니다.
분리 이유
- 유닛 테스트 가능성 확보
- 비즈니스 로직과 실행 환경 분리
- 동시성 및 에러 핸들링 단순화
분리 전략
- 실행 시점 효과 →
config
,env
인자로 전달 - 처리 시점 효과 →
interface
,function
인젝션
type repo interface {
Get(string) (string, error)
Set(string, any) error
}
func logic(key string, cfg Config, r repo) (string, error) {
val, err := r.Get(key)
if err != nil {
return "", err
}
// 로직 처리 예: val 가공, 조건 체크 등
return val, r.Set(key, val)
}
이렇게 하면 외부 의존성을 테스트 더블(mock/stub)로 대체할 수 있습니다.
테스트 전략
Go는 testing
패키지를 통해 내장된 유닛 테스트 도구를 제공합니다.
하지만 실무에서는 테스트 코드 작성이 어려운 이유가 사이드 이펙트와 로직이 섞여 있기 때문인 경우가 많습니다.
테스트 분리 원칙
- 로직은 유닛 테스트로 검증
- 외부 효과는 통합 테스트 또는 E2E 테스트로 검증
현실적인 예외 처리
단순한 CRUD API의 경우 통합 테스트 중심 설계가 더 효율적일 수도 있습니다. 그러나 비즈니스 로직이 존재한다면 유닛 테스트는 필수입니다.
또한, 다음과 같은 경계 상황을 위한 전략도 유용합니다:
- DB →
in-memory DB
사용 (e.g., SQLite, go-memdb) - 외부 API →
httptest.Server
,mockgen
활용
테스트의 추가 가치
- 집중력 향상: 작성 중간에 확인 지점 확보
- 코드 자신감 증가: 변경 후 리스크 감소
- 유지보수 효율: 사이드 이펙트는 통합, 로직은 유닛 기준으로 분리
type adder interface {
Add(int, int) int
}
type realAdder struct{}
func (realAdder) Add(a, b int) int { return a + b }
func TestAdder(t *testing.T) {
type mockAdder struct{}
func (mockAdder) Add(a, b int) int { return 42 }
result := mockAdder{}.Add(1, 2)
if result != 42 {
t.Fatal("unexpected result")
}
}
결론 및 요약
- Go의 암시적 인터페이스 만족은 덕 타이핑의 장점을 정적 타입에서도 실현 가능하게 합니다.
- 제너릭과 인터페이스 제약을 조합하면 간결하고 재사용 가능한 구조를 만들 수 있습니다.
- 사이드 이펙트를 분리하면 테스트 가능한 구조로 변환되며, 신뢰성 있는 시스템을 구축할 수 있습니다.
- 유닛 테스트와 통합 테스트의 역할을 명확히 구분하는 설계 습관이 필요합니다.
- 코드의 복잡도가 높아질수록, 테스트는 가드레일이자 문서 역할을 겸하게 됩니다.
이러한 구조적 접근은 유지보수성과 신뢰성을 동시에 확보하는 Go 개발의 핵심 전략입니다.