Go 1.21 제네릭으로 타입 안전한 Repository 패턴 구현하기
배경
회사 프로젝트에서 여러 엔티티에 대한 CRUD 로직이 반복되고 있었다. User, Product, Order 등 각각의 Repository마다 거의 동일한 메서드를 구현하고 있었고, interface{}를 사용하다 보니 타입 캐스팅 에러가 런타임에 발견되는 문제가 있었다.
기존 코드의 문제
type UserRepository struct {
db *sql.DB
}
func (r *UserRepository) FindByID(id int) (*User, error) {
// 쿼리 실행 로직
}
type ProductRepository struct {
db *sql.DB
}
func (r *ProductRepository) FindByID(id int) (*Product, error) {
// 거의 동일한 로직 반복
}
각 엔티티마다 Repository 구조체와 메서드를 중복 작성해야 했다.
제네릭 적용
Go 1.21에서 제네릭이 안정화되면서 이를 활용한 범용 Repository를 만들었다.
type Entity interface {
GetID() int
TableName() string
}
type Repository[T Entity] struct {
db *sql.DB
}
func NewRepository[T Entity](db *sql.DB) *Repository[T] {
return &Repository[T]{db: db}
}
func (r *Repository[T]) FindByID(id int) (*T, error) {
var entity T
query := fmt.Sprintf("SELECT * FROM %s WHERE id = ?", entity.TableName())
err := r.db.QueryRow(query, id).Scan(&entity)
if err != nil {
return nil, err
}
return &entity, nil
}
사용 예시
type User struct {
ID int
Name string
}
func (u User) GetID() int { return u.ID }
func (u User) TableName() string { return "users" }
userRepo := NewRepository[User](db)
user, err := userRepo.FindByID(1)
// user는 *User 타입으로 추론됨
결과
- 코드 중복이 70% 이상 감소
- 타입 캐스팅 에러가 컴파일 타임에 잡힘
- 새로운 엔티티 추가 시 Entity 인터페이스만 구현하면 됨
제네릭 도입 초기라 팀원들과 학습 곡선이 있었지만, 타입 안전성과 유지보수성 측면에서 충분히 가치가 있었다.