🏗️

Struct vs Class — 상속 없이 사는 법

Ruby의 class < Base 대신 Go의 struct 임베딩

Ruby 개발자에게 class는 공기같은 존재다. 모든 게 클래스 안에 있다. class User < ApplicationRecord — 상속으로 기능을 물려받고, include로 모듈을 섞는다.

Go에는 class도 없고, 상속도 없다. struct가 데이터 구조를 정의하고, 그 struct에 메서드를 붙이는 게 전부다.

Ruby의 class vs Go의 struct

Ruby: class User; attr_accessor :name, :email; end
Go: type User struct { Name string; Email string }

비슷해 보이지만 근본이 다르다. Ruby의 class는 인스턴스 변수, 메서드, 상속, 믹스인을 전부 담는 그릇이다. Go의 struct는 그냥 데이터 필드의 묶음이다. 메서드는 바깥에서 붙인다.

상속이 없다 — 임베딩으로 대체

class Admin < User 이런 건 Go에서 불가능하다. 대신 struct 안에 다른 struct를 넣는다:

type Admin struct {
    User  // 임베딩 — User의 모든 필드와 메서드를 가져옴
    Role string
}

Admin은 User를 "상속"한 게 아니라 "포함"한 거다. admin.Name으로 User의 필드에 접근 가능하고, User의 메서드도 그대로 호출된다.

이건 Ruby의 include UserModule과 비슷하다. 다만 Go의 임베딩은 타입 수준에서 일어나고, 런타임 오버헤드가 없다.

new가 없다

Ruby에서 User.new(name: "kim") — initialize가 호출된다. Go에서는 생성자가 없다. User{Name: "kim"}으로 리터럴로 만들거나, 관례적으로 NewUser("kim") 팩토리 함수를 만든다.

private/public이 대소문자로 결정된다

Ruby의 private, protected, public 키워드 대신, Go는 이름의 첫 글자로 결정한다. 대문자 시작 = 외부 접근 가능(exported). 소문자 시작 = 패키지 내부용. User.Name은 외부에서 접근 가능하고, User.name은 불가능하다.

Ruby에서 Go로

1

Ruby: class < Base 상속 → Go: struct 임베딩 (포함 관계)

2

Ruby: attr_accessor → Go: struct 필드 (대문자=public, 소문자=private)

3

Ruby: User.new(initialize) → Go: 생성자 없음, User{} 리터럴 또는 NewUser() 팩토리

4

Ruby: include Module → Go: struct 임베딩 (비슷하지만 컴파일 타임에 결정)

장점

  • 임베딩으로 상속의 복잡성(다이아몬드 문제 등) 없이 코드 재사용 가능
  • 대소문자만으로 접근 제어 — private/public 키워드가 필요 없다

단점

  • Ruby의 동적 디스패치(method_missing, respond_to?)가 없다 — 메타프로그래밍 불가
  • 생성자가 없어서 초기화 검증을 팩토리 함수에서 수동으로 해야 한다

사용 사례

Rails의 ActiveRecord 모델 상속 구조를 Go로 재설계할 때 Ruby의 module include 패턴을 Go의 임베딩으로 전환할 때

참고 자료