본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout
TechProjectsBooksAbout
© 2026 Kreath. All rights reserved.
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 2장: Go 타입 시스템과 인터페이스
2026년 8월 10일·프로그래밍·

2장: Go 타입 시스템과 인터페이스

Go의 구조체, 메서드, 암묵적 인터페이스 만족, 제네릭(타입 파라미터), 타입 제약 조건, 임베딩을 통한 합성 패턴을 체계적으로 다룹니다.

15분618자8개 섹션
concurrencyperformancedesign-patternstestinginfrastructure
공유
go-backend2 / 11
1234567891011
이전1장: Go 언어의 철학과 백엔드 개발에서의 강점다음3장: 고루틴과 채널 - Go의 동시성 모델

학습 목표

  • 구조체(Struct)와 메서드(Method)의 관계를 이해한다
  • 암묵적 인터페이스 만족의 원리와 장점을 파악한다
  • 제네릭(Generics)의 타입 파라미터와 제약 조건을 활용할 수 있다
  • 임베딩(Embedding)을 통한 합성 패턴을 적용할 수 있다

구조체와 메서드

구조체 정의

구조체(Struct)는 Go에서 데이터를 묶는 기본 단위입니다. 클래스가 없는 Go에서 구조체는 상태(필드)를 담는 유일한 사용자 정의 복합 타입입니다.

struct-definition.go
go
type User struct {
    ID        int64
    Email     string
    Name      string
    CreatedAt time.Time
    IsActive  bool
}
 
// 구조체 초기화 -- 필드명 명시 방식 권장
user := User{
    ID:        1,
    Email:     "kreath@example.com",
    Name:      "Kreath",
    CreatedAt: time.Now(),
    IsActive:  true,
}
Tip

구조체 초기화 시 필드명을 명시하는 방식을 사용하세요. 필드 순서에 의존하는 방식은 구조체에 필드가 추가될 때 컴파일 에러를 유발합니다.

메서드 정의

메서드는 특정 타입에 연결된 함수입니다. **리시버(Receiver)**를 통해 구조체와 메서드를 연결합니다.

method-definition.go
go
// 값 리시버 -- 구조체의 복사본에서 동작
func (u User) FullName() string {
    return u.Name
}
 
// 포인터 리시버 -- 구조체를 직접 수정 가능
func (u *User) Deactivate() {
    u.IsActive = false
}

리시버를 값 타입으로 할지 포인터 타입으로 할지는 중요한 설계 결정입니다.

기준값 리시버포인터 리시버
구조체 수정불가가능
복사 비용크기에 비례8바이트 고정
nil 호출불가가능 (주의 필요)
관례불변 메서드변경 메서드
Warning

하나의 타입에서 값 리시버와 포인터 리시버를 혼용하면 혼란을 야기합니다. 일반적으로 구조체의 모든 메서드에 동일한 리시버 타입을 사용하는 것이 권장됩니다. 구조체를 수정하는 메서드가 하나라도 있다면, 모든 메서드를 포인터 리시버로 통일하세요.


인터페이스 -- 암묵적 만족

인터페이스의 핵심 원리

Go의 인터페이스는 다른 언어와 근본적으로 다릅니다. Java나 C#에서는 implements 키워드를 명시해야 하지만, Go에서는 인터페이스에 정의된 메서드를 모두 구현하면 자동으로 해당 인터페이스를 만족합니다. 이를 덕 타이핑(Duck Typing)이라고 부르기도 합니다.

implicit-interface.go
go
// 인터페이스 정의
type Writer interface {
    Write(data []byte) (int, error)
}
 
// FileWriter는 Writer 인터페이스를 "자동으로" 만족
type FileWriter struct {
    path string
}
 
func (fw *FileWriter) Write(data []byte) (int, error) {
    return os.WriteFile(fw.path, data, 0644), nil
}
 
// ConsoleWriter도 Writer 인터페이스를 만족
type ConsoleWriter struct{}
 
func (cw *ConsoleWriter) Write(data []byte) (int, error) {
    return fmt.Print(string(data))
}

작은 인터페이스의 힘

Go 표준 라이브러리에서 가장 널리 사용되는 인터페이스들은 메서드가 1-2개뿐입니다.

small-interfaces.go
go
type Reader interface {
    Read(p []byte) (n int, err error)
}
 
type Writer interface {
    Write(p []byte) (n int, err error)
}
 
type Stringer interface {
    String() string
}
 
type Error interface {
    Error() string
}

이 작은 인터페이스들이 합성되어 강력한 추상화를 만듭니다.

interface-composition.go
go
// 인터페이스 합성
type ReadWriter interface {
    Reader
    Writer
}
 
type ReadCloser interface {
    Reader
    io.Closer
}
Info

Go 커뮤니티에서는 "인터페이스가 클수록 추상화가 약하다(The bigger the interface, the weaker the abstraction)"라는 격언이 있습니다. 인터페이스에 메서드를 추가할수록 해당 인터페이스를 구현하는 타입이 줄어들기 때문입니다.

의존성 역전과 테스트

암묵적 인터페이스 만족은 의존성 역전(Dependency Inversion)과 테스트에서 큰 이점을 줍니다. 서드파티 라이브러리의 구체 타입에 의존하는 대신, 사용하는 쪽에서 인터페이스를 정의할 수 있습니다.

consumer-interface.go
go
// 사용하는 쪽에서 필요한 인터페이스를 정의
type UserRepository interface {
    FindByID(ctx context.Context, id int64) (*User, error)
    Save(ctx context.Context, user *User) error
}
 
type UserService struct {
    repo UserRepository // 인터페이스에 의존
}
 
// 테스트에서는 모의 객체(Mock)를 주입
type mockUserRepo struct{}
 
func (m *mockUserRepo) FindByID(ctx context.Context, id int64) (*User, error) {
    return &User{ID: id, Name: "테스트 사용자"}, nil
}
 
func (m *mockUserRepo) Save(ctx context.Context, user *User) error {
    return nil
}

제네릭 -- 타입 파라미터

Go 1.18에서 도입된 **제네릭(Generics)**은 타입 안전성을 유지하면서 코드 재사용성을 높여줍니다.

기본 문법

generics-basic.go
go
// 타입 파라미터를 가진 함수
func Map[T any, U any](slice []T, fn func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}
 
// 사용
names := Map(users, func(u User) string {
    return u.Name
})

타입 제약 조건

타입 제약 조건(Type Constraints)은 타입 파라미터가 만족해야 하는 조건을 정의합니다. any는 가장 느슨한 제약이며, 더 구체적인 제약을 통해 타입 파라미터에서 사용할 수 있는 연산을 명시할 수 있습니다.

type-constraints.go
go
// 숫자 타입으로 제한
type Number interface {
    ~int | ~int32 | ~int64 | ~float32 | ~float64
}
 
func Sum[T Number](numbers []T) T {
    var total T
    for _, n := range numbers {
        total += n
    }
    return total
}
 
// comparable 제약 -- == 연산자 사용 가능
func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {
            return true
        }
    }
    return false
}

~int의 틸드(Tilde) 문법은 기본 타입이 int인 모든 정의된 타입을 포함한다는 의미입니다. 예를 들어 type UserID int도 ~int에 포함됩니다.

제네릭 구조체

generic-struct.go
go
// 제네릭 결과 타입
type Result[T any] struct {
    Data  T
    Error error
}
 
func NewResult[T any](data T, err error) Result[T] {
    return Result[T]{Data: data, Error: err}
}
 
// 제네릭 캐시
type Cache[K comparable, V any] struct {
    mu    sync.RWMutex
    items map[K]V
}
 
func (c *Cache[K, V]) Get(key K) (V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    v, ok := c.items[key]
    return v, ok
}
 
func (c *Cache[K, V]) Set(key K, value V) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.items[key] = value
}
Warning

Go의 제네릭은 의도적으로 단순하게 설계되었습니다. 타입 파라미터의 메서드 지원, 공변성/반공변성, 고차 타입 파라미터 등은 지원하지 않습니다. 제네릭을 과도하게 사용하면 오히려 코드 가독성이 떨어질 수 있으니 꼭 필요한 곳에만 사용하세요.


임베딩 -- 합성을 통한 코드 재사용

구조체 임베딩

Go에서 합성(Composition)의 핵심 메커니즘은 구조체 임베딩입니다. 필드명 없이 타입만 선언하면, 해당 타입의 메서드와 필드가 외부 구조체로 "승격(Promoted)"됩니다.

struct-embedding.go
go
type Timestamps struct {
    CreatedAt time.Time
    UpdatedAt time.Time
}
 
func (t *Timestamps) Touch() {
    t.UpdatedAt = time.Now()
}
 
type Article struct {
    Timestamps // 임베딩 -- 상속이 아닌 합성
    ID         int64
    Title      string
    Content    string
}
 
func main() {
    article := Article{
        ID:    1,
        Title: "Go 타입 시스템",
    }
    article.Touch()            // Timestamps의 메서드 직접 호출
    fmt.Println(article.UpdatedAt) // Timestamps의 필드 직접 접근
}

인터페이스 임베딩

인터페이스도 다른 인터페이스를 임베딩하여 합성할 수 있습니다. 이를 통해 작은 인터페이스들을 조합하여 더 큰 인터페이스를 만듭니다.

interface-embedding.go
go
type Repository[T any] interface {
    Finder[T]
    Saver[T]
    Deleter
}
 
type Finder[T any] interface {
    FindByID(ctx context.Context, id int64) (*T, error)
    FindAll(ctx context.Context) ([]*T, error)
}
 
type Saver[T any] interface {
    Save(ctx context.Context, entity *T) error
}
 
type Deleter interface {
    Delete(ctx context.Context, id int64) error
}

실전 패턴: 옵션 패턴

Go에서 자주 사용되는 함수형 옵션 패턴(Functional Options Pattern)은 인터페이스와 함수 타입을 결합한 우아한 설정 방법입니다.

functional-options.go
go
type Server struct {
    host         string
    port         int
    readTimeout  time.Duration
    writeTimeout time.Duration
    maxConns     int
}
 
type Option func(*Server)
 
func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}
 
func WithTimeouts(read, write time.Duration) Option {
    return func(s *Server) {
        s.readTimeout = read
        s.writeTimeout = write
    }
}
 
func WithMaxConns(n int) Option {
    return func(s *Server) {
        s.maxConns = n
    }
}
 
func NewServer(host string, opts ...Option) *Server {
    s := &Server{
        host:         host,
        port:         8080,       // 기본값
        readTimeout:  5 * time.Second,
        writeTimeout: 10 * time.Second,
        maxConns:     100,
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}
 
// 사용 -- 필요한 옵션만 선택적으로 지정
server := NewServer("0.0.0.0",
    WithPort(3000),
    WithMaxConns(500),
)

이 패턴은 생성자의 매개변수가 많아질 때, 기본값을 유지하면서도 유연한 설정을 가능하게 합니다. 표준 라이브러리의 net/http, gRPC, 그리고 대부분의 인기 라이브러리에서 이 패턴을 볼 수 있습니다.


정리

이번 장에서 살펴본 핵심 내용을 정리합니다.

  • Go의 구조체는 데이터를 담는 기본 단위이며, 리시버를 통해 메서드를 연결합니다
  • 암묵적 인터페이스 만족은 Go의 가장 독특한 특성으로, implements 키워드 없이 인터페이스를 구현합니다
  • 작은 인터페이스를 합성하는 것이 Go의 권장 패턴이며, 이는 테스트와 의존성 역전에 유리합니다
  • 제네릭은 Go 1.18에서 도입되었으며, 타입 제약 조건을 통해 안전한 코드 재사용을 지원합니다
  • 임베딩은 상속 없이 코드 재사용을 달성하는 Go의 합성 메커니즘입니다

다음 장 미리보기

3장에서는 고루틴과 채널을 다룹니다. Go의 동시성 모델의 핵심인 고루틴의 내부 동작 원리, 버퍼드/언버퍼드 채널, sync 패키지의 동기화 프리미티브, 그리고 고루틴 생명주기 관리를 살펴봅니다.

이 글이 도움이 되셨나요?

관련 글

프로그래밍

1장: Go 언어의 철학과 백엔드 개발에서의 강점

Go 언어의 핵심 철학인 단순성과 합성을 살펴보고, 백엔드 개발에서 Go가 가진 빠른 컴파일, 단일 바이너리, 고루틴 등의 강점을 Java, Python, Rust와 비교하여 분석합니다.

2026년 8월 8일·17분
프로그래밍

3장: 고루틴과 채널 - Go의 동시성 모델

Go의 동시성 핵심인 고루틴의 내부 구조, 버퍼드/언버퍼드 채널, sync 패키지의 WaitGroup과 Mutex, 고루틴 생명주기 관리와 일반적인 동시성 패턴을 다룹니다.

2026년 8월 13일·15분
프로그래밍

4장: 동시성 패턴 심화 - select, context, errgroup

Go의 고급 동시성 패턴인 select 문, context.Context를 활용한 취소/타임아웃, errgroup, 팬아웃/팬인, 파이프라인, 워커 풀, 레이트 리미팅 패턴을 다룹니다.

2026년 8월 15일·14분
이전 글1장: Go 언어의 철학과 백엔드 개발에서의 강점
다음 글3장: 고루틴과 채널 - Go의 동시성 모델

댓글

목차

약 15분 남음
  • 학습 목표
  • 구조체와 메서드
    • 구조체 정의
    • 메서드 정의
  • 인터페이스 -- 암묵적 만족
    • 인터페이스의 핵심 원리
    • 작은 인터페이스의 힘
    • 의존성 역전과 테스트
  • 제네릭 -- 타입 파라미터
    • 기본 문법
    • 타입 제약 조건
    • 제네릭 구조체
  • 임베딩 -- 합성을 통한 코드 재사용
    • 구조체 임베딩
    • 인터페이스 임베딩
  • 실전 패턴: 옵션 패턴
  • 정리
  • 다음 장 미리보기