본문으로 건너뛰기

대표 표준 인터페이스

Go 표준 라이브러리는 강력하고 범용적인 인터페이스를 제공합니다. 이 인터페이스들을 이해하고 구현하면 표준 라이브러리 전체와 자연스럽게 연동되는 코드를 작성할 수 있습니다.

io.Reader와 io.Writer

Go에서 가장 중요한 두 인터페이스입니다. I/O를 추상화하여 파일, 네트워크, 메모리 버퍼 등 어떤 소스/목적지도 동일하게 처리할 수 있습니다.

package main

import (
"bytes"
"fmt"
"io"
"strings"
)

// io.Reader: 데이터를 읽는 인터페이스
// type Reader interface {
// Read(p []byte) (n int, err error)
// }

// io.Writer: 데이터를 쓰는 인터페이스
// type Writer interface {
// Write(p []byte) (n int, err error)
// }

// 커스텀 Reader 구현
type CountingReader struct {
r io.Reader
count int
}

func (cr *CountingReader) Read(p []byte) (int, error) {
n, err := cr.r.Read(p)
cr.count += n
return n, err
}

func (cr *CountingReader) BytesRead() int {
return cr.count
}

// 커스텀 Writer 구현
type PrefixWriter struct {
w io.Writer
prefix string
}

func (pw *PrefixWriter) Write(p []byte) (int, error) {
// 줄 앞에 prefix 추가
lines := strings.Split(string(p), "\n")
for i, line := range lines {
if i < len(lines)-1 {
fmt.Fprintf(pw.w, "%s%s\n", pw.prefix, line)
}
}
return len(p), nil
}

func main() {
// io.Copy: Reader → Writer 복사
src := strings.NewReader("Hello, Go 인터페이스!\n두 번째 줄\n세 번째 줄")
cr := &CountingReader{r: src}

var buf bytes.Buffer
pw := &PrefixWriter{w: &buf, prefix: ">>> "}

io.Copy(pw, cr)
fmt.Print(buf.String())
fmt.Printf("읽은 바이트: %d\n", cr.BytesRead())

// 표준 라이브러리 Reader들
readers := []io.Reader{
strings.NewReader("from string"),
bytes.NewReader([]byte("from bytes")),
}
multi := io.MultiReader(readers...)
data, _ := io.ReadAll(multi)
fmt.Println("MultiReader:", string(data))
}

fmt.Stringer

fmt.Stringer 인터페이스를 구현하면 fmt.Println 등에서 커스텀 문자열 표현을 사용합니다.

package main

import (
"fmt"
"strings"
)

// fmt.Stringer 인터페이스
// type Stringer interface {
// String() string
// }

type Direction int

const (
North Direction = iota
South
East
West
)

func (d Direction) String() string {
switch d {
case North:
return "북쪽"
case South:
return "남쪽"
case East:
return "동쪽"
case West:
return "서쪽"
default:
return fmt.Sprintf("Direction(%d)", int(d))
}
}

type Point struct {
X, Y float64
}

func (p Point) String() string {
return fmt.Sprintf("(%.1f, %.1f)", p.X, p.Y)
}

type Person struct {
Name string
Age int
Tags []string
}

func (p Person) String() string {
tags := ""
if len(p.Tags) > 0 {
tags = fmt.Sprintf(" [%s]", strings.Join(p.Tags, ", "))
}
return fmt.Sprintf("%s(%d세)%s", p.Name, p.Age, tags)
}

// GoString: %#v 포맷에서 사용 (fmt.GoStringer 인터페이스)
func (p Person) GoString() string {
return fmt.Sprintf("Person{Name:%q, Age:%d, Tags:%v}", p.Name, p.Age, p.Tags)
}

func main() {
dir := North
fmt.Println(dir) // 북쪽
fmt.Printf("방향: %v\n", dir) // 방향: 북쪽
fmt.Printf("값: %d\n", dir) // 값: 0

pt := Point{3.14, 2.72}
fmt.Println(pt) // (3.1, 2.7)
fmt.Printf("%v\n", pt) // (3.1, 2.7)

p := Person{
Name: "홍길동",
Age: 30,
Tags: []string{"Go", "개발자"},
}
fmt.Println(p) // 홍길동(30세) [Go, 개발자]
fmt.Printf("%#v\n", p) // Person{Name:"홍길동", Age:30, Tags:[Go 개발자]}

// 슬라이스에서도 자동 적용
directions := []Direction{North, East, South, West}
fmt.Println(directions) // [북쪽 동쪽 남쪽 서쪽]
}

error 인터페이스

Go에서 에러는 단순히 Error() string 메서드를 가진 인터페이스입니다.

package main

import (
"errors"
"fmt"
)

// error 인터페이스
// type error interface {
// Error() string
// }

// 계층적 에러 타입
type AppError struct {
Code int
Message string
Err error // 원인 에러 (래핑)
}

func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

func (e *AppError) Unwrap() error {
return e.Err
}

// 도메인별 에러
type DBError struct {
Query string
Err error
}

func (e *DBError) Error() string {
return fmt.Sprintf("DB 오류 (query=%s): %v", e.Query, e.Err)
}

func (e *DBError) Unwrap() error { return e.Err }

var (
ErrNotFound = errors.New("리소스를 찾을 수 없음")
ErrUnauthorized = errors.New("권한 없음")
)

func queryDB(id int) error {
if id == 0 {
return &DBError{
Query: "SELECT * FROM users WHERE id=0",
Err: errors.New("유효하지 않은 ID"),
}
}
if id > 1000 {
return &AppError{
Code: 404,
Message: "사용자 없음",
Err: fmt.Errorf("DB 조회 실패: %w", ErrNotFound),
}
}
return nil
}

func main() {
testIDs := []int{0, 42, 9999}

for _, id := range testIDs {
err := queryDB(id)
if err == nil {
fmt.Printf("ID=%d: 성공\n", id)
continue
}

fmt.Printf("ID=%d: %v\n", id, err)

// 에러 체인 탐색
var dbErr *DBError
if errors.As(err, &dbErr) {
fmt.Printf(" → DB 쿼리: %s\n", dbErr.Query)
}

var appErr *AppError
if errors.As(err, &appErr) {
fmt.Printf(" → 앱 에러 코드: %d\n", appErr.Code)
}

if errors.Is(err, ErrNotFound) {
fmt.Println(" → 404 처리 필요")
}
}
}

sort.Interface

정렬을 커스터마이징할 때 사용하는 인터페이스입니다.

package main

import (
"fmt"
"sort"
)

// sort.Interface
// type Interface interface {
// Len() int
// Less(i, j int) bool
// Swap(i, j int)
// }

type Student struct {
Name string
Grade int
Score float64
}

// 성적 내림차순 정렬
type ByScore []Student

func (s ByScore) Len() int { return len(s) }
func (s ByScore) Less(i, j int) bool { return s[i].Score > s[j].Score } // 내림차순
func (s ByScore) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

// 이름 오름차순 정렬
type ByName []Student

func (s ByName) Len() int { return len(s) }
func (s ByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
func (s ByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

func printStudents(students []Student) {
for _, s := range students {
fmt.Printf(" %s (학년:%d, 점수:%.1f)\n", s.Name, s.Grade, s.Score)
}
}

func main() {
students := []Student{
{"Charlie", 3, 87.5},
{"Alice", 1, 95.0},
{"Bob", 2, 87.5},
{"Diana", 1, 91.0},
{"Eve", 3, 78.0},
}

// 성적 내림차순
sort.Sort(ByScore(students))
fmt.Println("성적 내림차순:")
printStudents(students)

// 이름 오름차순
sort.Sort(ByName(students))
fmt.Println("이름 오름차순:")
printStudents(students)

// sort.Slice — 인터페이스 없이 클로저로 정렬 (Go 1.8+)
sort.Slice(students, func(i, j int) bool {
if students[i].Grade != students[j].Grade {
return students[i].Grade < students[j].Grade // 학년 오름차순
}
return students[i].Score > students[j].Score // 성적 내림차순
})
fmt.Println("학년 오름차순, 성적 내림차순:")
printStudents(students)
}

http.Handler

웹 서버 개발의 핵심 인터페이스입니다.

package main

import (
"fmt"
"net/http"
"strings"
)

// http.Handler 인터페이스
// type Handler interface {
// ServeHTTP(ResponseWriter, *Request)
// }

// 커스텀 핸들러
type GreetHandler struct {
greeting string
}

func (h GreetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
fmt.Fprintf(w, "%s, %s!\n", h.greeting, name)
}

// 미들웨어 패턴 (핸들러를 감싸는 핸들러)
type LoggingHandler struct {
handler http.Handler
}

func (lh LoggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Printf("[LOG] %s %s\n", r.Method, r.URL.Path)
lh.handler.ServeHTTP(w, r)
}

type AuthHandler struct {
handler http.Handler
token string
}

func (ah AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer "+ah.token) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
ah.handler.ServeHTTP(w, r)
}

// http.HandlerFunc — 함수를 Handler로 변환
// type HandlerFunc func(ResponseWriter, *Request)
// func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }

func helloFunc(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "함수로 구현한 핸들러")
}

func main() {
// 구조체 기반 핸들러
greet := GreetHandler{greeting: "안녕하세요"}
logged := LoggingHandler{handler: greet}

// 함수 기반 핸들러
// http.HandleFunc("/hello", helloFunc)

// 실제 서버 대신 시뮬레이션
fmt.Println("http.Handler 인터페이스 구현 완료")
fmt.Printf("GreetHandler: %T\n", greet)
fmt.Printf("LoggingHandler: %T\n", logged)
fmt.Printf("HandlerFunc: %T\n", http.HandlerFunc(helloFunc))
// 실제 서버: http.ListenAndServe(":8080", logged)
}

io.Closer와 defer 패턴

package main

import (
"fmt"
"io"
"strings"
)

// io.Closer
// type Closer interface {
// Close() error
// }

// io.ReadCloser = io.Reader + io.Closer
// io.WriteCloser = io.Writer + io.Closer
// io.ReadWriteCloser = 세 가지 모두

type Resource struct {
name string
closed bool
}

func (r *Resource) Read(p []byte) (int, error) {
if r.closed {
return 0, fmt.Errorf("%s: 이미 닫힌 리소스", r.name)
}
data := []byte("data from " + r.name)
copy(p, data)
return len(data), io.EOF
}

func (r *Resource) Close() error {
if r.closed {
return fmt.Errorf("%s: 이미 닫혔음", r.name)
}
r.closed = true
fmt.Printf("[%s] 리소스 닫힘\n", r.name)
return nil
}

func openResource(name string) io.ReadCloser {
return &Resource{name: name}
}

func processResource(name string) error {
rc := openResource(name)
defer rc.Close() // 항상 닫힘 보장

var sb strings.Builder
buf := make([]byte, 64)
for {
n, err := rc.Read(buf)
if n > 0 {
sb.Write(buf[:n])
}
if err == io.EOF {
break
}
if err != nil {
return err
}
}
fmt.Printf("[%s] 읽은 내용: %s\n", name, sb.String())
return nil
}

func main() {
processResource("DB연결")
processResource("파일핸들")
}

핵심 정리

  • io.Reader/io.Writer: Go I/O의 핵심 — 모든 I/O 소스/목적지 추상화
  • fmt.Stringer: String() string 구현으로 커스텀 출력 형식 지정
  • error: Error() string 만 있는 단순 인터페이스, Unwrap()으로 체이닝
  • sort.Interface: 커스텀 정렬 기준 구현 (또는 sort.Slice 사용)
  • http.Handler: 웹 서버 미들웨어 체인의 기반
  • io.Closer: defer rc.Close() 패턴으로 리소스 해제 보장