Updated:

15 minute read

개요

Go 1.23부터 추가된 unique 패키지는 값 정규화(interning, hash-consing)를 통해 메모리를 최적화합니다.

주요 특징:

  • 값 정규화: 동일한 값은 하나의 인스턴스만 유지
  • 메모리 절약: 중복 값 제거로 메모리 사용량 감소
  • 비교 성능: 포인터 비교로 O(1) 성능
  • 제네릭 기반: comparable 타입 모두 지원
  • 동시성 안전: goroutine-safe 보장
  • 전역 고유성: 프로세스 전체에서 고유한 핸들
  • GC 통합: 자동 메모리 관리

기본 개념

1. Handle의 이해

import "unique"

func main() {
    // Handle은 값의 전역 고유 ID
    h1 := unique.Make("hello")
    h2 := unique.Make("hello")
    h3 := unique.Make("world")
    
    // 같은 값은 같은 Handle
    fmt.Println(h1 == h2)  // true
    fmt.Println(h1 == h3)  // false
    
    // 원본 값 얻기
    fmt.Println(h1.Value())  // hello
}

2. 메모리 정규화

func main() {
    // 같은 문자열을 여러 번 사용
    s1 := "very long string that repeats many times"
    s2 := "very long string that repeats many times"
    s3 := "very long string that repeats many times"
    
    // 각각 별도 메모리 공간
    // 총 3배 메모리 사용
    
    // unique 사용
    h1 := unique.Make(s1)
    h2 := unique.Make(s2)
    h3 := unique.Make(s3)
    
    // h1, h2, h3는 같은 내부 값 참조
    // 1배 메모리만 사용
}

3. 비교 성능

import "time"

func main() {
    longStr := strings.Repeat("abc", 1000)
    
    // 일반 문자열 비교
    start := time.Now()
    for i := 0; i < 1000000; i++ {
        _ = longStr == longStr
    }
    fmt.Println("String compare:", time.Since(start))
    
    // Handle 비교 (포인터 비교)
    h := unique.Make(longStr)
    start = time.Now()
    for i := 0; i < 1000000; i++ {
        _ = h == h
    }
    fmt.Println("Handle compare:", time.Since(start))
    // Handle이 훨씬 빠름
}

4. 타입 안전성

func main() {
    type UserID string
    type SessionID string
    
    // 타입별로 별도 Handle
    uid := unique.Make(UserID("user123"))
    sid := unique.Make(SessionID("user123"))
    
    // 컴파일 에러: 타입이 다름
    // _ = uid == sid
    
    // Value()도 원래 타입 유지
    var u UserID = uid.Value()  // OK
    var s SessionID = sid.Value()  // OK
}

문자열 Interning

1. 기본 문자열 풀

type StringPool struct {
    pool map[unique.Handle[string]]struct{}
    mu   sync.RWMutex
}

func NewStringPool() *StringPool {
    return &StringPool{
        pool: make(map[unique.Handle[string]]struct{}),
    }
}

func (p *StringPool) Intern(s string) unique.Handle[string] {
    h := unique.Make(s)
    
    p.mu.Lock()
    p.pool[h] = struct{}{}
    p.mu.Unlock()
    
    return h
}

func (p *StringPool) Size() int {
    p.mu.RLock()
    defer p.mu.RUnlock()
    return len(p.pool)
}

func main() {
    pool := NewStringPool()
    
    // 같은 문자열 여러 번 추가
    h1 := pool.Intern("hello")
    h2 := pool.Intern("hello")
    h3 := pool.Intern("world")
    
    fmt.Println(h1 == h2)  // true
    fmt.Println(pool.Size())  // 2 (hello, world)
}

2. 로그 레벨 Interning

type LogLevel string

const (
    DEBUG LogLevel = "DEBUG"
    INFO  LogLevel = "INFO"
    WARN  LogLevel = "WARN"
    ERROR LogLevel = "ERROR"
)

type Logger struct {
    level unique.Handle[LogLevel]
}

func NewLogger(level LogLevel) *Logger {
    return &Logger{
        level: unique.Make(level),
    }
}

func (l *Logger) SetLevel(level LogLevel) {
    l.level = unique.Make(level)
}

func (l *Logger) IsEnabled(level LogLevel) bool {
    // 포인터 비교로 빠른 체크
    return unique.Make(level) == l.level
}

func main() {
    logger := NewLogger(INFO)
    
    // 빠른 레벨 체크
    if logger.IsEnabled(DEBUG) {
        // ...
    }
}

3. HTTP 헤더 정규화

type HeaderKey string

var (
    ContentType   = unique.Make(HeaderKey("Content-Type"))
    Authorization = unique.Make(HeaderKey("Authorization"))
    UserAgent     = unique.Make(HeaderKey("User-Agent"))
)

type Headers struct {
    values map[unique.Handle[HeaderKey]]string
}

func NewHeaders() *Headers {
    return &Headers{
        values: make(map[unique.Handle[HeaderKey]]string),
    }
}

func (h *Headers) Set(key HeaderKey, value string) {
    h.values[unique.Make(key)] = value
}

func (h *Headers) Get(key HeaderKey) string {
    return h.values[unique.Make(key)]
}

func main() {
    headers := NewHeaders()
    
    // 빠른 키 비교
    headers.Set("Content-Type", "application/json")
    
    val := headers.Get("Content-Type")
    fmt.Println(val)  // application/json
}

구조체 정규화

1. 설정 값 중복 제거

type Config struct {
    Host string
    Port int
}

type Service struct {
    name   string
    config unique.Handle[Config]
}

func NewService(name string, cfg Config) *Service {
    return &Service{
        name:   name,
        config: unique.Make(cfg),
    }
}

func main() {
    // 같은 설정을 여러 서비스가 공유
    cfg := Config{Host: "localhost", Port: 8080}
    
    s1 := NewService("service1", cfg)
    s2 := NewService("service2", cfg)
    s3 := NewService("service3", cfg)
    
    // s1, s2, s3는 같은 Config 인스턴스 참조
    fmt.Println(s1.config == s2.config)  // true
}

2. 좌표 정규화

type Point struct {
    X, Y int
}

type Shape struct {
    vertices []unique.Handle[Point]
}

func NewShape(points []Point) *Shape {
    vertices := make([]unique.Handle[Point], len(points))
    for i, p := range points {
        vertices[i] = unique.Make(p)
    }
    return &Shape{vertices: vertices}
}

func main() {
    // 여러 도형이 같은 좌표 공유
    p1 := Point{0, 0}
    p2 := Point{1, 0}
    p3 := Point{0, 1}
    
    triangle := NewShape([]Point{p1, p2, p3})
    square := NewShape([]Point{p1, p2, Point{1, 1}, p3})
    
    // p1을 공유하므로 메모리 절약
}

3. AST 노드 공유

type NodeType string

const (
    NumberNode NodeType = "NUMBER"
    StringNode NodeType = "STRING"
    BoolNode   NodeType = "BOOL"
)

type ASTNode struct {
    Type  NodeType
    Value string
}

type AST struct {
    nodes []unique.Handle[ASTNode]
}

func (a *AST) AddNode(node ASTNode) {
    a.nodes = append(a.nodes, unique.Make(node))
}

func main() {
    ast := &AST{}
    
    // 같은 노드 여러 번 사용
    trueNode := ASTNode{Type: BoolNode, Value: "true"}
    
    ast.AddNode(trueNode)
    ast.AddNode(trueNode)
    ast.AddNode(trueNode)
    
    // 실제로는 하나의 인스턴스만 저장됨
}

실전 예제

1. 캐시 키 정규화

import (
    "crypto/sha256"
    "encoding/hex"
    "sync"
)

type CacheKey string

type Cache struct {
    data map[unique.Handle[CacheKey]]interface{}
    mu   sync.RWMutex
}

func NewCache() *Cache {
    return &Cache{
        data: make(map[unique.Handle[CacheKey]]interface{}),
    }
}

func (c *Cache) MakeKey(parts ...string) CacheKey {
    h := sha256.New()
    for _, p := range parts {
        h.Write([]byte(p))
    }
    return CacheKey(hex.EncodeToString(h.Sum(nil)))
}

func (c *Cache) Set(key CacheKey, value interface{}) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    // 키 정규화로 메모리 절약
    c.data[unique.Make(key)] = value
}

func (c *Cache) Get(key CacheKey) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    v, ok := c.data[unique.Make(key)]
    return v, ok
}

func main() {
    cache := NewCache()
    
    // 같은 키가 여러 번 사용되어도 메모리 효율적
    key := cache.MakeKey("user", "123", "profile")
    
    cache.Set(key, map[string]string{"name": "Alice"})
    
    if val, ok := cache.Get(key); ok {
        fmt.Println(val)
    }
}

2. 에러 메시지 중복 제거

type ErrorCode string

const (
    ErrNotFound      ErrorCode = "NOT_FOUND"
    ErrUnauthorized  ErrorCode = "UNAUTHORIZED"
    ErrInternalError ErrorCode = "INTERNAL_ERROR"
)

type AppError struct {
    Code    unique.Handle[ErrorCode]
    Message string
    Details map[string]interface{}
}

func NewError(code ErrorCode, message string) *AppError {
    return &AppError{
        Code:    unique.Make(code),
        Message: message,
        Details: make(map[string]interface{}),
    }
}

func (e *AppError) Is(code ErrorCode) bool {
    // 포인터 비교로 빠른 체크
    return e.Code == unique.Make(code)
}

func (e *AppError) Error() string {
    return fmt.Sprintf("%s: %s", e.Code.Value(), e.Message)
}

func main() {
    err := NewError(ErrNotFound, "User not found")
    
    if err.Is(ErrNotFound) {
        fmt.Println("Handle not found")
    }
    
    // 같은 에러 코드를 여러 에러가 공유
    err2 := NewError(ErrNotFound, "Product not found")
    
    // 메모리 절약 (같은 ErrorCode Handle)
    fmt.Println(err.Code == err2.Code)  // true
}

3. 국제화(i18n) 키 관리

type LocaleKey string

type I18n struct {
    locale       string
    translations map[string]map[unique.Handle[LocaleKey]]string
    mu           sync.RWMutex
}

func NewI18n(locale string) *I18n {
    return &I18n{
        locale:       locale,
        translations: make(map[string]map[unique.Handle[LocaleKey]]string),
    }
}

func (i *I18n) AddTranslation(locale string, key LocaleKey, text string) {
    i.mu.Lock()
    defer i.mu.Unlock()
    
    if i.translations[locale] == nil {
        i.translations[locale] = make(map[unique.Handle[LocaleKey]]string)
    }
    
    i.translations[locale][unique.Make(key)] = text
}

func (i *I18n) Translate(key LocaleKey) string {
    i.mu.RLock()
    defer i.mu.RUnlock()
    
    if text, ok := i.translations[i.locale][unique.Make(key)]; ok {
        return text
    }
    
    return string(key)
}

func main() {
    i18n := NewI18n("ko")
    
    // 번역 추가
    i18n.AddTranslation("ko", "greeting.hello", "안녕하세요")
    i18n.AddTranslation("en", "greeting.hello", "Hello")
    i18n.AddTranslation("ko", "greeting.goodbye", "안녕히 가세요")
    i18n.AddTranslation("en", "greeting.goodbye", "Goodbye")
    
    // 빠른 조회 (키 정규화)
    fmt.Println(i18n.Translate("greeting.hello"))
    // 안녕하세요
}

4. 메트릭 레이블 최적화

type Label struct {
    Name  string
    Value string
}

type Metric struct {
    name   string
    labels []unique.Handle[Label]
    value  float64
}

type MetricRegistry struct {
    metrics map[string][]*Metric
    mu      sync.RWMutex
}

func NewMetricRegistry() *MetricRegistry {
    return &MetricRegistry{
        metrics: make(map[string][]*Metric),
    }
}

func (r *MetricRegistry) Record(name string, labels []Label, value float64) {
    r.mu.Lock()
    defer r.mu.Unlock()
    
    // 레이블 정규화
    uniqueLabels := make([]unique.Handle[Label], len(labels))
    for i, l := range labels {
        uniqueLabels[i] = unique.Make(l)
    }
    
    metric := &Metric{
        name:   name,
        labels: uniqueLabels,
        value:  value,
    }
    
    r.metrics[name] = append(r.metrics[name], metric)
}

func (r *MetricRegistry) Query(name string, labels []Label) []float64 {
    r.mu.RLock()
    defer r.mu.RUnlock()
    
    // 레이블 정규화
    uniqueLabels := make([]unique.Handle[Label], len(labels))
    for i, l := range labels {
        uniqueLabels[i] = unique.Make(l)
    }
    
    var values []float64
    for _, m := range r.metrics[name] {
        if labelsEqual(m.labels, uniqueLabels) {
            values = append(values, m.value)
        }
    }
    
    return values
}

func labelsEqual(a, b []unique.Handle[Label]) bool {
    if len(a) != len(b) {
        return false
    }
    for i := range a {
        if a[i] != b[i] {
            return false
        }
    }
    return true
}

func main() {
    registry := NewMetricRegistry()
    
    // 같은 레이블이 반복적으로 사용됨
    labels := []Label{
        {Name: "service", Value: "api"},
        {Name: "env", Value: "prod"},
    }
    
    registry.Record("requests", labels, 100)
    registry.Record("requests", labels, 150)
    registry.Record("requests", labels, 200)
    
    // 메모리 효율적 (레이블 정규화)
    values := registry.Query("requests", labels)
    fmt.Println(values)  // [100 150 200]
}

5. 설정 관리 시스템

type Environment string

const (
    Development Environment = "development"
    Staging     Environment = "staging"
    Production  Environment = "production"
)

type DatabaseConfig struct {
    Host     string
    Port     int
    Database string
}

type AppConfig struct {
    Env      unique.Handle[Environment]
    Database unique.Handle[DatabaseConfig]
}

type ConfigManager struct {
    configs map[unique.Handle[Environment]]AppConfig
    mu      sync.RWMutex
}

func NewConfigManager() *ConfigManager {
    return &ConfigManager{
        configs: make(map[unique.Handle[Environment]]AppConfig),
    }
}

func (m *ConfigManager) Set(env Environment, dbConfig DatabaseConfig) {
    m.mu.Lock()
    defer m.mu.Unlock()
    
    m.configs[unique.Make(env)] = AppConfig{
        Env:      unique.Make(env),
        Database: unique.Make(dbConfig),
    }
}

func (m *ConfigManager) Get(env Environment) (AppConfig, bool) {
    m.mu.RLock()
    defer m.mu.RUnlock()
    
    cfg, ok := m.configs[unique.Make(env)]
    return cfg, ok
}

func main() {
    manager := NewConfigManager()
    
    // 설정 등록
    manager.Set(Development, DatabaseConfig{
        Host:     "localhost",
        Port:     5432,
        Database: "dev_db",
    })
    
    manager.Set(Production, DatabaseConfig{
        Host:     "prod.example.com",
        Port:     5432,
        Database: "prod_db",
    })
    
    // 빠른 조회
    if cfg, ok := manager.Get(Development); ok {
        db := cfg.Database.Value()
        fmt.Printf("%s:%d/%s\n", db.Host, db.Port, db.Database)
        // localhost:5432/dev_db
    }
}

6. 타입 안전한 상수 집합

type HTTPMethod string

const (
    GET    HTTPMethod = "GET"
    POST   HTTPMethod = "POST"
    PUT    HTTPMethod = "PUT"
    DELETE HTTPMethod = "DELETE"
)

var (
    methodGET    = unique.Make(GET)
    methodPOST   = unique.Make(POST)
    methodPUT    = unique.Make(PUT)
    methodDELETE = unique.Make(DELETE)
)

type Route struct {
    Method  unique.Handle[HTTPMethod]
    Path    string
    Handler func()
}

type Router struct {
    routes []Route
}

func (r *Router) Add(method HTTPMethod, path string, handler func()) {
    r.routes = append(r.routes, Route{
        Method:  unique.Make(method),
        Path:    path,
        Handler: handler,
    })
}

func (r *Router) Match(method HTTPMethod, path string) (func(), bool) {
    m := unique.Make(method)
    
    for _, route := range r.routes {
        // 포인터 비교로 빠른 체크
        if route.Method == m && route.Path == path {
            return route.Handler, true
        }
    }
    
    return nil, false
}

func main() {
    router := &Router{}
    
    router.Add(GET, "/users", func() {
        fmt.Println("GET /users")
    })
    
    router.Add(POST, "/users", func() {
        fmt.Println("POST /users")
    })
    
    // 빠른 라우팅
    if handler, ok := router.Match(GET, "/users"); ok {
        handler()  // GET /users
    }
}

7. 이벤트 타입 시스템

type EventType string

const (
    UserCreated EventType = "user.created"
    UserUpdated EventType = "user.updated"
    UserDeleted EventType = "user.deleted"
)

type Event struct {
    Type      unique.Handle[EventType]
    Timestamp time.Time
    Data      interface{}
}

type EventBus struct {
    handlers map[unique.Handle[EventType]][]func(Event)
    mu       sync.RWMutex
}

func NewEventBus() *EventBus {
    return &EventBus{
        handlers: make(map[unique.Handle[EventType]][]func(Event)),
    }
}

func (b *EventBus) Subscribe(eventType EventType, handler func(Event)) {
    b.mu.Lock()
    defer b.mu.Unlock()
    
    h := unique.Make(eventType)
    b.handlers[h] = append(b.handlers[h], handler)
}

func (b *EventBus) Publish(eventType EventType, data interface{}) {
    b.mu.RLock()
    handlers := b.handlers[unique.Make(eventType)]
    b.mu.RUnlock()
    
    event := Event{
        Type:      unique.Make(eventType),
        Timestamp: time.Now(),
        Data:      data,
    }
    
    for _, handler := range handlers {
        go handler(event)
    }
}

func main() {
    bus := NewEventBus()
    
    // 이벤트 구독
    bus.Subscribe(UserCreated, func(e Event) {
        fmt.Printf("User created: %v\n", e.Data)
    })
    
    bus.Subscribe(UserCreated, func(e Event) {
        fmt.Println("Send welcome email")
    })
    
    // 이벤트 발행
    bus.Publish(UserCreated, map[string]string{
        "id":   "123",
        "name": "Alice",
    })
    
    time.Sleep(100 * time.Millisecond)
}

8. 문자열 집합 최적화

type StringSet struct {
    items map[unique.Handle[string]]struct{}
    mu    sync.RWMutex
}

func NewStringSet() *StringSet {
    return &StringSet{
        items: make(map[unique.Handle[string]]struct{}),
    }
}

func (s *StringSet) Add(item string) {
    s.mu.Lock()
    defer s.mu.Unlock()
    
    s.items[unique.Make(item)] = struct{}{}
}

func (s *StringSet) Contains(item string) bool {
    s.mu.RLock()
    defer s.mu.RUnlock()
    
    _, ok := s.items[unique.Make(item)]
    return ok
}

func (s *StringSet) Remove(item string) {
    s.mu.Lock()
    defer s.mu.Unlock()
    
    delete(s.items, unique.Make(item))
}

func (s *StringSet) Size() int {
    s.mu.RLock()
    defer s.mu.RUnlock()
    
    return len(s.items)
}

func (s *StringSet) ToSlice() []string {
    s.mu.RLock()
    defer s.mu.RUnlock()
    
    result := make([]string, 0, len(s.items))
    for h := range s.items {
        result = append(result, h.Value())
    }
    
    return result
}

func main() {
    set := NewStringSet()
    
    // 같은 문자열 여러 번 추가
    set.Add("apple")
    set.Add("banana")
    set.Add("apple")  // 중복
    set.Add("cherry")
    
    fmt.Println(set.Size())  // 3
    fmt.Println(set.Contains("apple"))  // true
    
    set.Remove("apple")
    fmt.Println(set.Size())  // 2
    
    fmt.Println(set.ToSlice())  // [banana cherry]
}

일반적인 실수

1. Handle 저장 누락

// ❌ 나쁜 예 (Handle 활용 안 함)
type Config struct {
    env string  // 그냥 문자열 저장
}

func main() {
    c1 := Config{env: "production"}
    c2 := Config{env: "production"}
    
    // 비교할 수 없음
    // _ = c1 == c2  // 컴파일 에러
}

// ✅ 좋은 예 (Handle 사용)
type Config struct {
    env unique.Handle[string]
}

func NewConfig(env string) Config {
    return Config{
        env: unique.Make(env),
    }
}

func main() {
    c1 := NewConfig("production")
    c2 := NewConfig("production")
    
    // 빠른 비교
    fmt.Println(c1.env == c2.env)  // true
}

2. Value() 남용

// ❌ 나쁜 예 (매번 Value() 호출)
func main() {
    h := unique.Make("hello")
    
    for i := 0; i < 1000; i++ {
        _ = h.Value() == "hello"  // Value() 호출 비용
    }
}

// ✅ 좋은 예 (Handle 비교)
func main() {
    h1 := unique.Make("hello")
    h2 := unique.Make("hello")
    
    for i := 0; i < 1000; i++ {
        _ = h1 == h2  // 포인터 비교 (빠름)
    }
}

3. comparable 제약 무시

// ❌ 나쁜 예 (comparable 아닌 타입)
type Data struct {
    items []int  // 슬라이스는 comparable 아님
}

func main() {
    // 컴파일 에러
    // h := unique.Make(Data{items: []int{1, 2, 3}})
}

// ✅ 좋은 예 (comparable 타입 사용)
type Data struct {
    id   int
    name string
}

func main() {
    h := unique.Make(Data{id: 1, name: "test"})
    fmt.Println(h.Value())  // {1 test}
}

4. 포인터 타입 혼동

// ❌ 나쁜 예 (포인터는 주소 비교)
type Person struct {
    Name string
}

func main() {
    p1 := &Person{Name: "Alice"}
    p2 := &Person{Name: "Alice"}
    
    h1 := unique.Make(p1)
    h2 := unique.Make(p2)
    
    // 다른 Handle (주소가 다름)
    fmt.Println(h1 == h2)  // false
}

// ✅ 좋은 예 (값 타입 사용)
type Person struct {
    Name string
}

func main() {
    p1 := Person{Name: "Alice"}
    p2 := Person{Name: "Alice"}
    
    h1 := unique.Make(p1)
    h2 := unique.Make(p2)
    
    // 같은 Handle (값이 같음)
    fmt.Println(h1 == h2)  // true
}

5. nil Handle 처리

// ❌ 나쁜 예 (nil 체크 누락)
func GetConfig(env string) unique.Handle[string] {
    if env == "" {
        // zero value Handle 반환
        return unique.Handle[string]{}
    }
    return unique.Make(env)
}

func main() {
    h := GetConfig("")
    // h.Value() 호출 시 패닉 가능
}

// ✅ 좋은 예 (명시적 처리)
func GetConfig(env string) (unique.Handle[string], bool) {
    if env == "" {
        return unique.Handle[string]{}, false
    }
    return unique.Make(env), true
}

func main() {
    if h, ok := GetConfig("prod"); ok {
        fmt.Println(h.Value())
    }
}

6. 맵 키로 값 사용

// ❌ 나쁜 예 (값을 맵 키로 사용)
func main() {
    m := make(map[string]int)
    
    // 같은 문자열이 여러 번 키로 사용됨
    m["configuration"] = 1
    m["configuration"] = 2
    
    // 메모리 중복
}

// ✅ 좋은 예 (Handle을 맵 키로 사용)
func main() {
    m := make(map[unique.Handle[string]]int)
    
    h := unique.Make("configuration")
    m[h] = 1
    m[h] = 2  // 같은 Handle 재사용
    
    fmt.Println(m[h])  // 2
}

7. 동시성 안전 오해

// ❌ 나쁜 예 (자체 데이터 구조 동기화 누락)
type Cache struct {
    data map[unique.Handle[string]]interface{}
    // mutex 없음
}

func (c *Cache) Set(key string, val interface{}) {
    // race condition!
    c.data[unique.Make(key)] = val
}

// ✅ 좋은 예 (적절한 동기화)
type Cache struct {
    data map[unique.Handle[string]]interface{}
    mu   sync.RWMutex
}

func (c *Cache) Set(key string, val interface{}) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    c.data[unique.Make(key)] = val
}

func (c *Cache) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    v, ok := c.data[unique.Make(key)]
    return v, ok
}

베스트 프랙티스

1. 상수는 전역 Handle로

// ✅ 초기화 시점에 Handle 생성
var (
    EnvDev  = unique.Make("development")
    EnvProd = unique.Make("production")
)

type App struct {
    env unique.Handle[string]
}

func NewApp(isDev bool) *App {
    if isDev {
        return &App{env: EnvDev}
    }
    return &App{env: EnvProd}
}

2. 타입별로 별도 Handle

// ✅ 타입 안전성 보장
type UserID string
type SessionID string

type User struct {
    id unique.Handle[UserID]
}

type Session struct {
    id unique.Handle[SessionID]
}

// 컴파일 타임에 타입 체크
func GetUser(id unique.Handle[UserID]) User {
    // ...
    return User{id: id}
}

3. 비교 연산 최적화

// ✅ Handle 비교로 성능 향상
type Status string

const (
    Active   Status = "active"
    Inactive Status = "inactive"
)

var (
    statusActive   = unique.Make(Active)
    statusInactive = unique.Make(Inactive)
)

func IsActive(status unique.Handle[Status]) bool {
    return status == statusActive
}

4. 메모리 프로파일링

import (
    "runtime"
    "runtime/pprof"
)

// ✅ 메모리 사용량 측정
func BenchmarkStringInterning(b *testing.B) {
    var m runtime.MemStats
    
    // Before
    runtime.ReadMemStats(&m)
    before := m.Alloc
    
    handles := make([]unique.Handle[string], b.N)
    for i := 0; i < b.N; i++ {
        handles[i] = unique.Make("repeated string")
    }
    
    // After
    runtime.ReadMemStats(&m)
    after := m.Alloc
    
    b.Logf("Memory used: %d bytes", after-before)
}

5. 생성자 패턴

// ✅ Handle 생성을 캡슐화
type Config struct {
    env  unique.Handle[string]
    mode unique.Handle[string]
}

func NewConfig(env, mode string) Config {
    return Config{
        env:  unique.Make(env),
        mode: unique.Make(mode),
    }
}

func (c Config) Env() string {
    return c.env.Value()
}

func (c Config) IsSame(other Config) bool {
    return c.env == other.env && c.mode == other.mode
}

6. 인터페이스 활용

// ✅ 제네릭 인터페이스
type Identifiable[T comparable] interface {
    ID() unique.Handle[T]
}

type User struct {
    id unique.Handle[string]
}

func (u User) ID() unique.Handle[string] {
    return u.id
}

func FindByID[T comparable](
    items []Identifiable[T],
    id unique.Handle[T],
) Identifiable[T] {
    for _, item := range items {
        if item.ID() == id {
            return item
        }
    }
    return nil
}

7. 문서화

// ✅ 명확한 문서화
// CacheKey represents a unique identifier for cache entries.
// All cache keys are interned to reduce memory usage when the same
// key is used multiple times.
type CacheKey string

// MakeCacheKey creates a unique handle for the given cache key.
// The returned handle can be compared with == for fast equality checks.
func MakeCacheKey(namespace, id string) unique.Handle[CacheKey] {
    key := CacheKey(namespace + ":" + id)
    return unique.Make(key)
}

8. 테스트 작성

// ✅ Handle 동작 테스트
func TestHandleEquality(t *testing.T) {
    tests := []struct {
        name string
        v1   string
        v2   string
        want bool
    }{
        {"same value", "hello", "hello", true},
        {"different value", "hello", "world", false},
        {"empty strings", "", "", true},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            h1 := unique.Make(tt.v1)
            h2 := unique.Make(tt.v2)
            
            got := h1 == h2
            if got != tt.want {
                t.Errorf("got %v, want %v", got, tt.want)
            }
        })
    }
}

정리

  • 개념: 값 정규화로 메모리 최적화, 동일 값은 하나의 인스턴스만 유지
  • 성능: 포인터 비교로 O(1) 성능, 문자열 비교보다 훨씬 빠름
  • 타입: 제네릭 기반, comparable 타입 지원, 타입 안전성 보장
  • 사용: 문자열 풀, 설정 값, AST 노드, 에러 코드, 로그 레벨 등
  • 실전: 캐시 키, 에러 메시지, i18n, 메트릭 레이블, 설정 관리, 상수 집합, 이벤트 시스템, 문자열 집합
  • 실수: Handle 저장 누락, Value() 남용, comparable 무시, 포인터 혼동, nil 처리, 맵 키 값 사용, 동기화 누락
  • 베스트: 전역 Handle, 타입별 Handle, 비교 최적화, 프로파일링, 생성자 패턴, 인터페이스, 문서화, 테스트