Updated:

15 minute read

개요

Finalizer는 객체가 가비지 컬렉션되기 직전에 호출되는 함수로, runtime.SetFinalizer를 통해 설정합니다.

주요 특징:

  • 실행 비보장: GC 시점에만 실행되며, 프로그램 종료 시 실행 안 될 수 있음
  • 디버깅 용도: 리소스 누수 감지, 메모리 관리 검증
  • 소멸자 아님: C++/Java의 소멸자와 다름, 정리 보장 불가
  • 단일 finalizer: 객체당 하나만 설정 가능
  • GC 의존: 가비지 컬렉션 사이클에 의존
  • 성능 영향: Finalizer가 있는 객체는 GC 처리가 느림
  • 대안 선호: defer, Close 패턴이 더 안전

기본 개념

1. SetFinalizer 기본

package main

import (
    "fmt"
    "runtime"
    "time"
)

type Resource struct {
    ID int
}

func main() {
    // Finalizer 설정
    r := &Resource{ID: 1}
    runtime.SetFinalizer(r, func(res *Resource) {
        fmt.Printf("Resource %d is being collected\n", res.ID)
    })
    
    // r을 nil로 만들어 GC 대상으로
    r = nil
    
    // GC 강제 실행
    runtime.GC()
    time.Sleep(100 * time.Millisecond)
    
    // Output: Resource 1 is being collected
}

2. Finalizer 해제

func main() {
    r := &Resource{ID: 2}
    
    // Finalizer 설정
    runtime.SetFinalizer(r, func(res *Resource) {
        fmt.Println("Finalizer called")
    })
    
    // Finalizer 제거 (nil 전달)
    runtime.SetFinalizer(r, nil)
    
    r = nil
    runtime.GC()
    time.Sleep(100 * time.Millisecond)
    
    // Finalizer가 호출되지 않음
}

3. 실행 시점

func main() {
    fmt.Println("Start")
    
    for i := 0; i < 5; i++ {
        r := &Resource{ID: i}
        runtime.SetFinalizer(r, func(res *Resource) {
            fmt.Printf("Finalizer %d\n", res.ID)
        })
    }
    
    fmt.Println("Loop done")
    
    // GC가 실행되지 않으면 finalizer도 실행 안 됨
    runtime.GC()
    time.Sleep(100 * time.Millisecond)
    
    fmt.Println("End")
    
    // Output 순서 보장 안 됨:
    // Start
    // Loop done
    // Finalizer 3
    // Finalizer 1
    // Finalizer 4
    // ...
    // End
}

실행 특성

1. GC 사이클 의존

func demonstrateGC() {
    // GC 없이는 finalizer 실행 안 됨
    for i := 0; i < 100; i++ {
        r := &Resource{ID: i}
        runtime.SetFinalizer(r, func(res *Resource) {
            fmt.Printf("GC: %d\n", res.ID)
        })
    }
    
    fmt.Println("Created 100 resources")
    // GC가 자동으로 실행될 때까지 finalizer 대기
    
    // 강제 GC
    runtime.GC()
    time.Sleep(100 * time.Millisecond)
}

2. 프로그램 종료 시

func main() {
    r := &Resource{ID: 999}
    runtime.SetFinalizer(r, func(res *Resource) {
        fmt.Println("This may not be printed")
    })
    
    r = nil
    
    // 프로그램 종료 시 finalizer 실행 보장 없음
    // GC가 실행되지 않을 수 있음
}

3. 순환 참조

type Node struct {
    ID   int
    Next *Node
}

func main() {
    // 순환 참조
    n1 := &Node{ID: 1}
    n2 := &Node{ID: 2}
    n1.Next = n2
    n2.Next = n1
    
    runtime.SetFinalizer(n1, func(n *Node) {
        fmt.Printf("Node %d collected\n", n.ID)
    })
    runtime.SetFinalizer(n2, func(n *Node) {
        fmt.Printf("Node %d collected\n", n.ID)
    })
    
    // 순환 참조 끊기
    n1.Next = nil
    n2.Next = nil
    n1, n2 = nil, nil
    
    runtime.GC()
    time.Sleep(100 * time.Millisecond)
}

4. 실행 고루틴

func main() {
    r := &Resource{ID: 100}
    runtime.SetFinalizer(r, func(res *Resource) {
        // Finalizer는 별도 고루틴에서 실행
        fmt.Printf("Goroutine: %d\n", res.ID)
        
        // 동기화 필요 시 주의
        time.Sleep(10 * time.Millisecond)
    })
    
    r = nil
    runtime.GC()
    time.Sleep(200 * time.Millisecond)
}

메모리 관리

1. 메모리 프로파일링

import (
    "fmt"
    "runtime"
)

type LargeObject struct {
    Data [1024 * 1024]byte // 1MB
    ID   int
}

func trackMemory() {
    var m runtime.MemStats
    
    // 메모리 할당 전
    runtime.ReadMemStats(&m)
    before := m.Alloc
    
    // 객체 생성 및 finalizer 설정
    for i := 0; i < 10; i++ {
        obj := &LargeObject{ID: i}
        runtime.SetFinalizer(obj, func(o *LargeObject) {
            fmt.Printf("Collected: %d\n", o.ID)
        })
    }
    
    // 메모리 할당 후
    runtime.ReadMemStats(&m)
    after := m.Alloc
    
    fmt.Printf("Allocated: %d MB\n", (after-before)/(1024*1024))
    
    // GC 실행
    runtime.GC()
    time.Sleep(100 * time.Millisecond)
    
    // 메모리 해제 후
    runtime.ReadMemStats(&m)
    fmt.Printf("After GC: %d MB\n", m.Alloc/(1024*1024))
}

2. Finalizer 큐

func demonstrateQueue() {
    // Finalizer가 많으면 GC 성능 저하
    count := 10000
    
    start := time.Now()
    
    for i := 0; i < count; i++ {
        r := &Resource{ID: i}
        runtime.SetFinalizer(r, func(res *Resource) {
            // 빈 finalizer도 오버헤드 있음
        })
    }
    
    fmt.Printf("Set %d finalizers: %v\n", count, time.Since(start))
    
    start = time.Now()
    runtime.GC()
    time.Sleep(500 * time.Millisecond)
    
    fmt.Printf("GC with finalizers: %v\n", time.Since(start))
}

3. 재부활 (Resurrection)

var global *Resource

func main() {
    r := &Resource{ID: 1}
    
    runtime.SetFinalizer(r, func(res *Resource) {
        fmt.Println("Finalizer called")
        
        // 객체를 전역 변수에 저장 (재부활)
        global = res
        
        // 재부활된 객체는 다시 GC 대상이 됨
        // 하지만 finalizer는 다시 호출 안 됨
    })
    
    r = nil
    runtime.GC()
    time.Sleep(100 * time.Millisecond)
    
    if global != nil {
        fmt.Printf("Resurrected: %d\n", global.ID)
    }
}

일반적인 사용 사례

1. 파일 디스크립터 추적

import (
    "fmt"
    "os"
)

type FileWrapper struct {
    file *os.File
    name string
}

func NewFileWrapper(name string) (*FileWrapper, error) {
    f, err := os.Open(name)
    if err != nil {
        return nil, err
    }
    
    fw := &FileWrapper{file: f, name: name}
    
    // Finalizer로 미닫힘 감지
    runtime.SetFinalizer(fw, func(wrapper *FileWrapper) {
        if wrapper.file != nil {
            fmt.Printf("WARNING: File %s not closed properly\n", wrapper.name)
            wrapper.file.Close()
        }
    })
    
    return fw, nil
}

func (fw *FileWrapper) Close() error {
    if fw.file == nil {
        return nil
    }
    
    err := fw.file.Close()
    fw.file = nil
    
    // 정상 닫힘, finalizer 제거
    runtime.SetFinalizer(fw, nil)
    
    return err
}

func main() {
    // 올바른 사용
    fw1, _ := NewFileWrapper("test1.txt")
    fw1.Close()
    
    // 잘못된 사용 (Close 호출 안 함)
    fw2, _ := NewFileWrapper("test2.txt")
    _ = fw2  // 사용 후 Close 없이 버림
    
    runtime.GC()
    time.Sleep(100 * time.Millisecond)
    // WARNING: File test2.txt not closed properly
}

2. C 라이브러리 리소스

/*
#include <stdlib.h>

typedef struct {
    int* data;
    int size;
} CArray;

CArray* create_array(int size) {
    CArray* arr = (CArray*)malloc(sizeof(CArray));
    arr->data = (int*)malloc(size * sizeof(int));
    arr->size = size;
    return arr;
}

void free_array(CArray* arr) {
    free(arr->data);
    free(arr);
}
*/
import "C"
import "unsafe"

type CArrayWrapper struct {
    carray *C.CArray
}

func NewCArray(size int) *CArrayWrapper {
    wrapper := &CArrayWrapper{
        carray: C.create_array(C.int(size)),
    }
    
    // C 메모리 누수 방지
    runtime.SetFinalizer(wrapper, func(w *CArrayWrapper) {
        if w.carray != nil {
            fmt.Println("WARNING: C array not freed")
            C.free_array(w.carray)
        }
    })
    
    return wrapper
}

func (w *CArrayWrapper) Free() {
    if w.carray != nil {
        C.free_array(w.carray)
        w.carray = nil
        runtime.SetFinalizer(w, nil)
    }
}

3. 데이터베이스 연결 풀

import "database/sql"

type DBConnection struct {
    conn *sql.DB
    id   string
}

type ConnectionPool struct {
    connections map[string]*DBConnection
    mu          sync.Mutex
}

func (p *ConnectionPool) Get(id string) *DBConnection {
    p.mu.Lock()
    defer p.mu.Unlock()
    
    conn := &DBConnection{id: id}
    // 실제로는 sql.Open() 호출
    
    p.connections[id] = conn
    
    // 반환 안 된 연결 추적
    runtime.SetFinalizer(conn, func(c *DBConnection) {
        fmt.Printf("WARNING: Connection %s not returned to pool\n", c.id)
    })
    
    return conn
}

func (p *ConnectionPool) Return(conn *DBConnection) {
    p.mu.Lock()
    defer p.mu.Unlock()
    
    delete(p.connections, conn.id)
    runtime.SetFinalizer(conn, nil)
}

4. 임시 파일 정리

import (
    "io/ioutil"
    "os"
    "path/filepath"
)

type TempFile struct {
    path string
    file *os.File
}

func NewTempFile() (*TempFile, error) {
    f, err := ioutil.TempFile("", "example-*.txt")
    if err != nil {
        return nil, err
    }
    
    tf := &TempFile{
        path: f.Name(),
        file: f,
    }
    
    // 미정리 임시 파일 감지
    runtime.SetFinalizer(tf, func(t *TempFile) {
        if t.path != "" {
            fmt.Printf("WARNING: Temp file %s not cleaned up\n", t.path)
            os.Remove(t.path)
        }
    })
    
    return tf, nil
}

func (tf *TempFile) Close() error {
    if tf.file == nil {
        return nil
    }
    
    err := tf.file.Close()
    if err != nil {
        return err
    }
    
    // 파일 삭제
    os.Remove(tf.path)
    tf.path = ""
    tf.file = nil
    
    runtime.SetFinalizer(tf, nil)
    return nil
}

실전 예제

1. 리소스 누수 탐지기

type ResourceTracker struct {
    resources map[string]int
    mu        sync.Mutex
}

var tracker = &ResourceTracker{
    resources: make(map[string]int),
}

func (rt *ResourceTracker) Track(name string, obj interface{}) {
    rt.mu.Lock()
    rt.resources[name]++
    rt.mu.Unlock()
    
    runtime.SetFinalizer(obj, func(o interface{}) {
        rt.mu.Lock()
        rt.resources[name]--
        rt.mu.Unlock()
    })
}

func (rt *ResourceTracker) Report() {
    rt.mu.Lock()
    defer rt.mu.Unlock()
    
    fmt.Println("Resource Leak Report:")
    for name, count := range rt.resources {
        if count > 0 {
            fmt.Printf("  %s: %d leaked\n", name, count)
        }
    }
}

// 사용 예
type DatabaseConnection struct {
    ID int
}

func NewDBConnection(id int) *DatabaseConnection {
    conn := &DatabaseConnection{ID: id}
    tracker.Track("db_connection", conn)
    return conn
}

func main() {
    // 일부 연결은 정상 해제
    for i := 0; i < 5; i++ {
        conn := NewDBConnection(i)
        _ = conn
    }
    
    // GC 실행
    runtime.GC()
    time.Sleep(100 * time.Millisecond)
    
    tracker.Report()
    // Resource Leak Report:
    //   db_connection: 5 leaked
}

2. 메모리 풀 관리

type Buffer struct {
    data []byte
    pool *BufferPool
}

type BufferPool struct {
    buffers chan *Buffer
    created int
    mu      sync.Mutex
}

func NewBufferPool(size, capacity int) *BufferPool {
    return &BufferPool{
        buffers: make(chan *Buffer, capacity),
    }
}

func (p *BufferPool) Get(size int) *Buffer {
    select {
    case buf := <-p.buffers:
        return buf
    default:
        p.mu.Lock()
        p.created++
        p.mu.Unlock()
        
        buf := &Buffer{
            data: make([]byte, size),
            pool: p,
        }
        
        // 반환 안 된 버퍼 추적
        runtime.SetFinalizer(buf, func(b *Buffer) {
            fmt.Printf("WARNING: Buffer not returned to pool\n")
        })
        
        return buf
    }
}

func (p *BufferPool) Put(buf *Buffer) {
    runtime.SetFinalizer(buf, nil)
    
    select {
    case p.buffers <- buf:
    default:
        // 풀이 꽉 참, 버퍼 폐기
    }
}

func main() {
    pool := NewBufferPool(1024, 10)
    
    // 올바른 사용
    buf1 := pool.Get(1024)
    pool.Put(buf1)
    
    // 잘못된 사용
    buf2 := pool.Get(1024)
    _ = buf2  // 반환 안 함
    
    runtime.GC()
    time.Sleep(100 * time.Millisecond)
}

3. 네트워크 연결 모니터링

import "net"

type MonitoredConn struct {
    net.Conn
    id        string
    createdAt time.Time
}

func WrapConn(conn net.Conn, id string) *MonitoredConn {
    mc := &MonitoredConn{
        Conn:      conn,
        id:        id,
        createdAt: time.Now(),
    }
    
    runtime.SetFinalizer(mc, func(c *MonitoredConn) {
        duration := time.Since(c.createdAt)
        fmt.Printf("WARNING: Connection %s not closed (lived %v)\n",
            c.id, duration)
        c.Conn.Close()
    })
    
    return mc
}

func (mc *MonitoredConn) Close() error {
    runtime.SetFinalizer(mc, nil)
    return mc.Conn.Close()
}

func main() {
    // 연결 생성
    conn, _ := net.Dial("tcp", "example.com:80")
    mc := WrapConn(conn, "conn-1")
    
    // Close 호출 안 함
    _ = mc
    
    runtime.GC()
    time.Sleep(100 * time.Millisecond)
}

4. 캐시 엔트리 만료

type CacheEntry struct {
    Key       string
    Value     interface{}
    ExpiresAt time.Time
}

type Cache struct {
    entries map[string]*CacheEntry
    mu      sync.RWMutex
}

func NewCache() *Cache {
    return &Cache{
        entries: make(map[string]*CacheEntry),
    }
}

func (c *Cache) Set(key string, value interface{}, ttl time.Duration) {
    entry := &CacheEntry{
        Key:       key,
        Value:     value,
        ExpiresAt: time.Now().Add(ttl),
    }
    
    c.mu.Lock()
    c.entries[key] = entry
    c.mu.Unlock()
    
    // 만료 시 자동 삭제 (보조 메커니즘)
    runtime.SetFinalizer(entry, func(e *CacheEntry) {
        c.mu.Lock()
        delete(c.entries, e.Key)
        c.mu.Unlock()
        fmt.Printf("Cache entry %s finalized\n", e.Key)
    })
}

func (c *Cache) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    entry, ok := c.entries[key]
    if !ok {
        return nil, false
    }
    
    if time.Now().After(entry.ExpiresAt) {
        return nil, false
    }
    
    return entry.Value, true
}

5. 작업 추적 시스템

type Task struct {
    ID        string
    StartTime time.Time
    finished  bool
}

type TaskTracker struct {
    tasks map[string]*Task
    mu    sync.Mutex
}

var taskTracker = &TaskTracker{
    tasks: make(map[string]*Task),
}

func StartTask(id string) *Task {
    task := &Task{
        ID:        id,
        StartTime: time.Now(),
    }
    
    taskTracker.mu.Lock()
    taskTracker.tasks[id] = task
    taskTracker.mu.Unlock()
    
    // 미완료 작업 경고
    runtime.SetFinalizer(task, func(t *Task) {
        if !t.finished {
            duration := time.Since(t.StartTime)
            fmt.Printf("WARNING: Task %s not finished (running %v)\n",
                t.ID, duration)
        }
    })
    
    return task
}

func (t *Task) Finish() {
    t.finished = true
    
    taskTracker.mu.Lock()
    delete(taskTracker.tasks, t.ID)
    taskTracker.mu.Unlock()
    
    runtime.SetFinalizer(t, nil)
}

func main() {
    // 완료된 작업
    task1 := StartTask("task-1")
    time.Sleep(10 * time.Millisecond)
    task1.Finish()
    
    // 미완료 작업
    task2 := StartTask("task-2")
    _ = task2
    
    runtime.GC()
    time.Sleep(100 * time.Millisecond)
}

6. 글로벌 리소스 카운터

type ResourceCounter struct {
    name  string
    count *int64
}

var counters sync.Map // map[string]*int64

func TrackResource(name string) *ResourceCounter {
    // 카운터 가져오기 또는 생성
    val, _ := counters.LoadOrStore(name, new(int64))
    counter := val.(*int64)
    
    // 카운트 증가
    atomic.AddInt64(counter, 1)
    
    rc := &ResourceCounter{
        name:  name,
        count: counter,
    }
    
    // 카운트 감소
    runtime.SetFinalizer(rc, func(r *ResourceCounter) {
        atomic.AddInt64(r.count, -1)
    })
    
    return rc
}

func GetResourceCount(name string) int64 {
    val, ok := counters.Load(name)
    if !ok {
        return 0
    }
    return atomic.LoadInt64(val.(*int64))
}

func main() {
    // 리소스 생성
    for i := 0; i < 100; i++ {
        _ = TrackResource("api_client")
    }
    
    fmt.Printf("Active: %d\n", GetResourceCount("api_client"))
    // Active: 100
    
    runtime.GC()
    time.Sleep(100 * time.Millisecond)
    
    fmt.Printf("After GC: %d\n", GetResourceCount("api_client"))
    // After GC: 0
}

7. 디버그 로깅

type DebugObject struct {
    Name      string
    CreatedAt time.Time
    file      string
    line      int
}

func NewDebugObject(name string) *DebugObject {
    _, file, line, _ := runtime.Caller(1)
    
    obj := &DebugObject{
        Name:      name,
        CreatedAt: time.Now(),
        file:      filepath.Base(file),
        line:      line,
    }
    
    if os.Getenv("DEBUG") == "1" {
        runtime.SetFinalizer(obj, func(o *DebugObject) {
            lifetime := time.Since(o.CreatedAt)
            fmt.Printf("[DEBUG] %s finalized (lifetime: %v, created at %s:%d)\n",
                o.Name, lifetime, o.file, o.line)
        })
    }
    
    return obj
}

func main() {
    os.Setenv("DEBUG", "1")
    
    obj1 := NewDebugObject("cache-entry")
    time.Sleep(50 * time.Millisecond)
    obj1 = nil
    
    obj2 := NewDebugObject("temp-buffer")
    time.Sleep(100 * time.Millisecond)
    obj2 = nil
    
    runtime.GC()
    time.Sleep(100 * time.Millisecond)
    
    // [DEBUG] cache-entry finalized (lifetime: 50ms, created at main.go:123)
    // [DEBUG] temp-buffer finalized (lifetime: 100ms, created at main.go:126)
}

8. 테스트 헬퍼

type TestResource struct {
    t       *testing.T
    name    string
    cleaned bool
}

func NewTestResource(t *testing.T, name string) *TestResource {
    tr := &TestResource{
        t:    t,
        name: name,
    }
    
    runtime.SetFinalizer(tr, func(r *TestResource) {
        if !r.cleaned {
            r.t.Errorf("Test resource %s not cleaned up", r.name)
        }
    })
    
    return tr
}

func (tr *TestResource) Cleanup() {
    tr.cleaned = true
    runtime.SetFinalizer(tr, nil)
}

// 테스트에서 사용
func TestExample(t *testing.T) {
    // 정상 케이스
    res1 := NewTestResource(t, "resource-1")
    defer res1.Cleanup()
    
    // 잘못된 케이스 (Cleanup 호출 안 함)
    res2 := NewTestResource(t, "resource-2")
    _ = res2
    
    // 테스트 종료 시 finalizer 검사
}

일반적인 실수

1. 리소스 정리를 finalizer에 의존

// ❌ 나쁜 예 (finalizer가 호출 안 될 수 있음)
type File struct {
    f *os.File
}

func NewFile(name string) *File {
    f, _ := os.Open(name)
    file := &File{f: f}
    
    runtime.SetFinalizer(file, func(file *File) {
        file.f.Close()  // 실행 보장 안 됨
    })
    
    return file
}

// ✅ 좋은 예 (명시적 Close + finalizer는 보조)
type File struct {
    f *os.File
}

func NewFile(name string) *File {
    f, _ := os.Open(name)
    file := &File{f: f}
    
    runtime.SetFinalizer(file, func(file *File) {
        if file.f != nil {
            fmt.Println("WARNING: File not closed")
            file.f.Close()
        }
    })
    
    return file
}

func (f *File) Close() error {
    if f.f == nil {
        return nil
    }
    
    err := f.f.Close()
    f.f = nil
    runtime.SetFinalizer(f, nil)
    return err
}

2. 긴 작업 실행

// ❌ 나쁜 예 (finalizer에서 시간 걸리는 작업)
type Logger struct {
    logs []string
}

func NewLogger() *Logger {
    logger := &Logger{}
    
    runtime.SetFinalizer(logger, func(l *Logger) {
        // 파일 쓰기는 시간이 걸림
        f, _ := os.Create("logs.txt")
        for _, log := range l.logs {
            f.WriteString(log + "\n")
        }
        f.Close()
    })
    
    return logger
}

// ✅ 좋은 예 (명시적 Flush)
type Logger struct {
    logs []string
}

func (l *Logger) Flush() error {
    f, err := os.Create("logs.txt")
    if err != nil {
        return err
    }
    defer f.Close()
    
    for _, log := range l.logs {
        f.WriteString(log + "\n")
    }
    
    runtime.SetFinalizer(l, nil)
    return nil
}

3. 복잡한 로직

// ❌ 나쁜 예 (finalizer에서 복잡한 처리)
type Connection struct {
    conn net.Conn
    pool *ConnectionPool
}

func NewConnection(pool *ConnectionPool) *Connection {
    conn := &Connection{pool: pool}
    
    runtime.SetFinalizer(conn, func(c *Connection) {
        // 락, 네트워크 호출 등 복잡한 로직
        c.pool.mu.Lock()
        c.pool.returnConnection(c)
        c.pool.mu.Unlock()
    })
    
    return conn
}

// ✅ 좋은 예 (단순 경고만)
func NewConnection(pool *ConnectionPool) *Connection {
    conn := &Connection{pool: pool}
    
    runtime.SetFinalizer(conn, func(c *Connection) {
        fmt.Println("WARNING: Connection not returned")
    })
    
    return conn
}

func (c *Connection) Return() {
    c.pool.returnConnection(c)
    runtime.SetFinalizer(c, nil)
}

4. Finalizer에서 패닉

// ❌ 나쁜 예 (패닉 가능)
type Data struct {
    ptr *int
}

func NewData() *Data {
    val := 42
    data := &Data{ptr: &val}
    
    runtime.SetFinalizer(data, func(d *Data) {
        // ptr이 nil일 수 있음
        fmt.Println(*d.ptr)  // 패닉 가능
    })
    
    return data
}

// ✅ 좋은 예 (nil 체크)
func NewData() *Data {
    val := 42
    data := &Data{ptr: &val}
    
    runtime.SetFinalizer(data, func(d *Data) {
        if d.ptr != nil {
            fmt.Println(*d.ptr)
        }
    })
    
    return data
}

5. 객체 재부활 의존

var resurrected *Resource

// ❌ 나쁜 예 (재부활 객체는 finalizer 다시 안 호출됨)
func main() {
    r := &Resource{ID: 1}
    
    runtime.SetFinalizer(r, func(res *Resource) {
        resurrected = res  // 재부활
        
        // 이 finalizer는 다시 호출되지 않음
    })
    
    r = nil
    runtime.GC()
    time.Sleep(100 * time.Millisecond)
    
    // resurrected를 다시 nil로 만들어도 finalizer 실행 안 됨
    resurrected = nil
    runtime.GC()
    time.Sleep(100 * time.Millisecond)
}

// ✅ 좋은 예 (재부활 피하기)
func main() {
    r := &Resource{ID: 1}
    
    runtime.SetFinalizer(r, func(res *Resource) {
        // 단순 로깅만
        fmt.Printf("Resource %d collected\n", res.ID)
    })
}

6. 순서 의존

// ❌ 나쁜 예 (finalizer 순서 의존)
type Parent struct {
    children []*Child
}

type Child struct {
    data string
}

func main() {
    parent := &Parent{}
    
    runtime.SetFinalizer(parent, func(p *Parent) {
        for _, child := range p.children {
            // child가 이미 수집되었을 수 있음
            fmt.Println(child.data)  // 위험
        }
    })
    
    for i := 0; i < 5; i++ {
        child := &Child{data: fmt.Sprintf("child-%d", i)}
        parent.children = append(parent.children, child)
    }
}

// ✅ 좋은 예 (독립적인 finalizer)
func main() {
    parent := &Parent{}
    
    runtime.SetFinalizer(parent, func(p *Parent) {
        fmt.Printf("Parent with %d children collected\n", len(p.children))
    })
}

7. GC 강제 실행

// ❌ 나쁜 예 (프로덕션에서 GC 강제 실행)
func cleanup() {
    // 리소스 정리를 위해 GC 호출
    runtime.GC()  // 성능 문제
    time.Sleep(100 * time.Millisecond)
}

// ✅ 좋은 예 (명시적 정리)
type ResourceManager struct {
    resources []*Resource
}

func (rm *ResourceManager) Cleanup() {
    for _, r := range rm.resources {
        r.Close()  // 명시적 정리
    }
    rm.resources = nil
}

베스트 프랙티스

1. 경고 용도로만 사용

// ✅ Finalizer는 경고/디버깅 용도
type Connection struct {
    closed bool
}

func NewConnection() *Connection {
    conn := &Connection{}
    
    runtime.SetFinalizer(conn, func(c *Connection) {
        if !c.closed {
            log.Println("WARNING: Connection not closed properly")
        }
    })
    
    return conn
}

func (c *Connection) Close() {
    c.closed = true
    runtime.SetFinalizer(c, nil)
}

2. defer와 함께 사용

// ✅ defer로 확실히 정리
func ProcessFile(name string) error {
    fw, err := NewFileWrapper(name)
    if err != nil {
        return err
    }
    defer fw.Close()  // 확실한 정리
    
    // Finalizer는 보조 (Close 누락 감지)
    
    // 파일 처리
    return nil
}

3. 조건부 활성화

// ✅ 디버그 모드에서만 finalizer 활성화
var debugMode = os.Getenv("DEBUG") == "1"

func NewResource(name string) *Resource {
    r := &Resource{name: name}
    
    if debugMode {
        runtime.SetFinalizer(r, func(res *Resource) {
            fmt.Printf("DEBUG: %s not cleaned\n", res.name)
        })
    }
    
    return r
}

4. 통계 수집

// ✅ Finalizer로 통계 수집
var stats struct {
    created   int64
    finalized int64
}

type Object struct {
    id int
}

func NewObject(id int) *Object {
    atomic.AddInt64(&stats.created, 1)
    
    obj := &Object{id: id}
    
    runtime.SetFinalizer(obj, func(o *Object) {
        atomic.AddInt64(&stats.finalized, 1)
    })
    
    return obj
}

func GetStats() (created, finalized int64) {
    return atomic.LoadInt64(&stats.created),
        atomic.LoadInt64(&stats.finalized)
}

5. 짧고 단순하게

// ✅ Finalizer는 짧고 단순하게
type Resource struct {
    id string
}

func NewResource(id string) *Resource {
    r := &Resource{id: id}
    
    runtime.SetFinalizer(r, func(res *Resource) {
        // 단순 로깅만
        log.Printf("Resource %s finalized", res.id)
    })
    
    return r
}

6. 테스트에서 활용

// ✅ 테스트에서 리소스 누수 검증
func TestResourceLeak(t *testing.T) {
    var leaked bool
    
    func() {
        r := &Resource{}
        runtime.SetFinalizer(r, func(*Resource) {
            leaked = true
        })
        // Close 호출 안 함
    }()
    
    runtime.GC()
    time.Sleep(100 * time.Millisecond)
    
    if !leaked {
        t.Error("Resource should have been collected")
    }
}

7. 문서화

// ✅ Finalizer 동작 문서화
// NewDBConn creates a new database connection.
// The connection MUST be closed by calling Close().
// A finalizer is set to detect unclosed connections in debug builds.
func NewDBConn(dsn string) (*DBConn, error) {
    conn := &DBConn{}
    
    if debugMode {
        runtime.SetFinalizer(conn, func(c *DBConn) {
            log.Println("WARNING: DB connection not closed")
        })
    }
    
    return conn, nil
}

8. 정리 함수 제공

// ✅ 명시적 정리 함수 항상 제공
type ResourceManager struct {
    resources []*Resource
}

func (rm *ResourceManager) Add(r *Resource) {
    rm.resources = append(rm.resources, r)
    
    runtime.SetFinalizer(r, func(res *Resource) {
        fmt.Println("WARNING: Resource not removed from manager")
    })
}

func (rm *ResourceManager) Remove(r *Resource) {
    // 명시적 제거
    for i, res := range rm.resources {
        if res == r {
            rm.resources = append(rm.resources[:i], rm.resources[i+1:]...)
            break
        }
    }
    
    runtime.SetFinalizer(r, nil)
}

func (rm *ResourceManager) Close() {
    // 모든 리소스 정리
    for _, r := range rm.resources {
        runtime.SetFinalizer(r, nil)
    }
    rm.resources = nil
}

정리

  • 기본: SetFinalizer로 GC 전 호출 함수 설정
  • 실행: GC 사이클 의존, 프로그램 종료 시 보장 없음
  • 용도: 디버깅, 경고, 통계, 리소스 누수 감지
  • 금지: 리소스 정리 의존, 긴 작업, 복잡한 로직
  • 특성: 순서 보장 없음, 재부활 시 재호출 없음, 별도 고루틴
  • 실전: 파일 추적, C 리소스, 연결 풀, 임시 파일, 작업 추적
  • 실수: 정리 의존, 긴 작업, 복잡한 로직, 패닉, 재부활, 순서 의존, GC 강제
  • 베스트: 경고 용도, defer 함께, 조건부, 짧고 단순, 테스트, 문서화, 정리 함수