1. ent 소개

Ent는 Go 언어를 위해 Facebook에서 개발한 엔터티 프레임워크입니다. 대규모 데이터 모델 애플리케이션을 구축하고 유지하는 과정을 단순화합니다. ent 프레임워크는 주로 다음 원칙을 따릅니다:

  • 데이터베이스 스키마를 그래프 구조로 쉽게 모델링합니다.
  • Go 언어 코드 형식으로 스키마를 정의합니다.
  • 코드 생성을 기반으로 정적 타입을 구현합니다.
  • 데이터베이스 쿼리 및 그래프 탐색 작성이 매우 간단합니다.
  • Go 템플릿을 사용하여 쉽게 확장 및 사용자 정의할 수 있습니다.

2. 환경 설정

ent 프레임워크를 사용하려면 개발 환경에 Go 언어가 설치되어 있는지 확인해야 합니다.

프로젝트 디렉토리가 GOPATH 외부에 있는 경우 또는 GOPATH를 잘 모르는 경우, 다음 명령어를 사용하여 새로운 Go 모듈 프로젝트를 생성할 수 있습니다:

go mod init entdemo

이렇게 하면 새로운 Go 모듈이 초기화되고 entdemo 프로젝트를 위한 새로운 go.mod 파일이 생성됩니다.

3. 첫 번째 스키마 정의

3.1. ent CLI를 사용하여 스키마 생성

먼저 프로젝트의 루트 디렉토리에서 ent CLI 도구를 사용하여 User라는 스키마를 생성하려면 다음 명령어를 실행해야 합니다:

go run -mod=mod entgo.io/ent/cmd/ent new User

위 명령어는 entdemo/ent/schema/ 디렉토리에 User 스키마를 생성합니다:

파일 entdemo/ent/schema/user.go:

package schema

import "entgo.io/ent"

// User holds the schema definition for the User entity.
type User struct {
    ent.Schema
}

// Fields of the User.
func (User) Fields() []ent.Field {
    return nil
}

// Edges of the User.
func (User) Edges() []ent.Edge {
    return nil
}

3.2. 필드 추가

다음으로, User 스키마에 필드 정의를 추가해야 합니다. 아래는 User 엔터티에 두 개의 필드를 추가하는 예시입니다.

수정된 파일 entdemo/ent/schema/user.go:

package schema

import (
    "entgo.io/ent"
    "entgo.io/ent/schema/field"
)

// Fields of the User.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age").
            Positive(),
        field.String("name").
            Default("unknown"),
    }
}

이 코드 조각은 User 모델에 두 개의 필드를 정의합니다: age는 양의 정수이고 name은 "unknown"이라는 기본값을 갖는 문자열입니다.

3.3. 데이터베이스 엔터티 생성

스키마를 정의한 후에는 이를 기반으로 하위 데이터베이스 접근 로직을 생성하기 위해 go generate 명령어를 실행해야 합니다.

프로젝트의 루트 디렉토리에서 다음 명령어를 실행합니다:

go generate ./ent

이 명령어는 이전에 정의한 스키마를 기반으로 해당하는 Go 코드를 생성하여 다음과 같은 파일 구조를 만들어냅니다:

ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── generate.go
├── mutation.go
... (간결함을 위해 일부 파일 생략)
├── schema
│   └── user.go
├── tx.go
├── user
│   ├── user.go
│   └── where.go
├── user.go
├── user_create.go
├── user_delete.go
├── user_query.go
└── user_update.go

4.1. 데이터베이스 연결 초기화

MySQL 데이터베이스에 연결하기 위해 ent 프레임워크에서 제공하는 Open 함수를 사용할 수 있습니다. 먼저 MySQL 드라이버를 가져와서 올바른 연결 문자열을 제공하여 데이터베이스 연결을 초기화합니다.

package main

import (
    "context"
    "log"

    "entdemo/ent"
    
    _ "github.com/go-sql-driver/mysql" // MySQL 드라이버 가져오기
)

func main() {
    // ent.Open을 사용하여 MySQL 데이터베이스와의 연결을 설정합니다.
    // 아래의 "your_username", "your_password", "your_database" 자리표시자를 교체하세요.
    client, err := ent.Open("mysql", "your_username:your_password@tcp(localhost:3306)/your_database?parseTime=True")
    if err != nil {
        log.Fatalf("mysql 연결을 열지 못했습니다: %v", err)
    }
    defer client.Close()

    // 자동 마이그레이션 도구 실행
    ctx := context.Background()
    if err := client.Schema.Create(ctx); err != nil {
        log.Fatalf("스키마 리소스 생성에 실패했습니다: %v", err)
    }
    
    // 여기에 추가적인 비즈니스 로직 작성 가능
}

4.2. 엔터티 생성

사용자 엔터티를 생성하는 것은 새로운 엔터티 객체를 만들고 Save 또는 SaveX 메서드를 사용하여 데이터베이스에 영속화하는 과정을 포함합니다. 다음 코드는 새로운 사용자 엔터티를 만들고 agename 두 개의 필드를 초기화하는 방법을 보여줍니다.

// CreateUser 함수는 새로운 사용자 엔터티를 생성하는 데 사용됩니다.
func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // client.User.Create()를 사용하여 사용자를 만드는 요청을 작성한 다음, SetAge와 SetName 메서드를 연결하여 엔터티 필드의 값을 설정합니다.
    u, err := client.User.
        Create().
        SetAge(30).    // 사용자 나이 설정
        SetName("a8m"). // 사용자 이름 설정
        Save(ctx)     // Save를 호출하여 데이터베이스에 엔터티를 저장합니다
    if err != nil {
        return nil, fmt.Errorf("사용자 생성에 실패했습니다: %w", err)
    }
    log.Println("사용자가 생성되었습니다: ", u)
    return u, nil
}

main 함수에서 CreateUser 함수를 호출하여 새로운 사용자 엔터티를 생성할 수 있습니다.

func main() {
    // ...데이터베이스 연결 설정 코드는 생략

    // 사용자 엔터티 생성
    u, err := CreateUser(ctx, client)
    if err != nil {
        log.Fatalf("사용자 생성에 실패했습니다: %v", err)
    }
    log.Printf("생성된 사용자: %#v\n", u)
}

4.3. 엔터티 조회

엔터티를 조회하기 위해 ent에서 생성된 쿼리 빌더를 사용할 수 있습니다. 다음 코드는 "a8m"이라는 이름을 가진 사용자를 조회하는 방법을 보여줍니다.

// QueryUser 함수는 지정된 이름을 가진 사용자 엔터티를 조회하는 데 사용됩니다.
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // client.User.Query()를 사용하여 사용자에 대한 쿼리를 작성한 후 Where 메서드를 연결하여 사용자 이름으로 조회하는 쿼리 조건을 추가합니다.
    u, err := client.User.
        Query().
        Where(user.NameEQ("a8m")).      // 이름이 "a8m"인 경우의 조회 조건 추가
        Only(ctx)                      // Only 메서드는 하나의 결과만을 기대한다는 것을 나타냅니다
    if err != nil {
        return nil, fmt.Errorf("사용자 조회에 실패했습니다: %w", err)
    }
    log.Println("사용자 조회 결과: ", u)
    return u, nil
}

main 함수에서 QueryUser 함수를 호출하여 사용자 엔터티를 조회할 수 있습니다.

func main() {
    // ...데이터베이스 연결 설정 및 사용자 생성 코드는 생략

    // 사용자 엔터티 조회
    u, err := QueryUser(ctx, client)
    if err != nil {
        log.Fatalf("사용자 조회에 실패했습니다: %v", err)
    }
    log.Printf("조회된 사용자: %#v\n", u)
}

5.1. 엣지와 역 엣지 이해

ent 프레임워크에서 데이터 모델은 그래프 구조로 시각화되며, 엔터티는 그래프의 노드를 나타내고 엔터티 간의 관계는 엣지로 표현됩니다. 엣지는 한 엔터티에서 다른 엔터티로의 연결을 나타내며, 예를 들어 사용자는 여러 대의 자동차를 소유할 수 있습니다.

역 엣지는 엣지의 반대 참조로, 논리적으로 엔터티 간의 역 관계를 나타내지만 데이터베이스에 새로운 관계를 생성하지는 않습니다. 예를 들어 자동차의 역 엣지를 통해 이 자동차를 소유한 사용자를 찾을 수 있습니다.

엣지와 역 엣지의 주요 의미는 연관된 엔터티 간의 탐색을 매우 직관적이고 간단하게 만드는 데 있습니다.

팁: ent에서 엣지는 전통적인 데이터베이스 외래 키에 해당하며, 테이블 간의 관계를 정의하는 데 사용됩니다.

5.2. 스키마에서 엣지 정의

먼저, ent CLI를 사용하여 CarGroup에 대한 초기 스키마를 생성합니다:

go run -mod=mod entgo.io/ent/cmd/ent new Car Group

다음으로, User 스키마에서 Car와의 엣지를 정의하여 사용자와 자동차 간의 관계를 나타냅니다. 사용자 엔터티에서 Car 유형을 가리키는 cars 엣지를 추가할 수 있으며, 이를 통해 사용자가 여러 대의 자동차를 소유할 수 있음을 나타냅니다:

// entdemo/ent/schema/user.go

// 사용자의 엣지.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("cars", Car.Type),
    }
}

엣지를 정의한 후, 해당 코드를 생성하기 위해 go generate ./ent를 다시 실행해야 합니다.

5.3. 엣지 데이터 조작

사용자와 관련된 자동차를 생성하는 것은 간단한 프로세스입니다. 사용자 엔터티가 주어지면 새로운 자동차 엔터티를 생성하고 사용자와 연결할 수 있습니다:

import (
    "context"
    "log"
    "entdemo/ent"
    // 자동차의 스키마 정의를 가져와야 합니다.
    _ "entdemo/ent/schema"
)

func 사용자를 위한 자동차 생성(ctx context.Context, client *ent.Client, userID int) error {
    user, err := client.User.Get(ctx, userID)
    if err != nil {
        log.Fatalf("사용자 가져오기 실패: %v", err)
        return err
    }

    // 새로운 자동차 생성 및 사용자와 연결
    _, err = client.Car.
        Create().
        SetModel("Tesla").
        SetRegisteredAt(time.Now()).
        SetOwner(user).
        Save(ctx)
    if err != nil {
        log.Fatalf("사용자를 위한 자동차 생성 실패: %v", err)
        return err
    }

    log.Println("자동차가 생성되고 사용자와 연결되었습니다.")
    return nil
}

마찬가지로, 사용자의 자동차를 조회하는 것도 간단합니다. 사용자가 소유한 모든 자동차의 목록을 조회하려면 다음과 같이 할 수 있습니다:

func 사용자의_자동차_조회(ctx context.Context, client *ent.Client, userID int) error {
    user, err := client.User.Get(ctx, userID)
    if err != nil {
        log.Fatalf("사용자 가져오기 실패: %v", err)
        return err
    }

    // 사용자가 소유한 모든 자동차 조회
    cars, err := user.QueryCars().All(ctx)
    if err != nil {
        log.Fatalf("자동차 조회 실패: %v", err)
        return err
    }

    for _, car := range cars {
        log.Printf("자동차: %v, 모델: %v", car.ID, car.Model)
    }
    return nil
}

위 단계를 통해 스키마에서 엣지를 정의하는 방법과 엣지와 관련된 데이터를 생성하고 조회하는 방법을 배웠습니다.

6. 그래프 탐색 및 조회

6.1. 그래프 구조 이해

ent에서 그래프 구조는 엔터티와 그들 사이의 엣지로 표현됩니다. 각 엔터티가 그래프의 노드에 해당하고, 엔터티 간의 관계는 엣지에 의해 나타낼 수 있으며, 이는 일대일, 일대다, 다대다 등이 될 수 있습니다. 이러한 그래프 구조는 관계형 데이터베이스에 대한 복잡한 조회 및 작업을 간단하고 직관적으로 만듭니다.

6.2. 그래프 구조 탐색

그래프 탐색 코드 작성은 주로 개체간의 엣지를 통해 데이터를 조회하고 연관시키는 작업을 포함합니다. 아래는 ent에서 그래프 구조를 탐색하는 간단한 예제입니다:

import (
    "context"
    "log"

    "entdemo/ent"
)

// GraphTraversal은 그래프 구조를 탐색하는 예제입니다
func GraphTraversal(ctx context.Context, client *ent.Client) error {
    // "Ariel"이라는 이름을 가진 사용자 조회
    a8m, err := client.User.Query().Where(user.NameEQ("Ariel")).Only(ctx)
    if err != nil {
        log.Fatalf("사용자 조회 실패: %v", err)
        return err
    }

    // Ariel이 소유한 모든 자동차를 탐색
    cars, err := a8m.QueryCars().All(ctx)
    if err != nil {
        log.Fatalf("자동차 조회 실패: %v", err)
        return err
    }
    for _, car := range cars {
        log.Printf("Ariel이 소유한 차 모델: %s", car.Model)
    }

    // Ariel이 속한 모든 그룹을 탐색
    groups, err := a8m.QueryGroups().All(ctx)
    if err != nil {
        log.Fatalf("그룹 조회 실패: %v", err)
        return err
    }
    for _, g := range groups {
        log.Printf("Ariel이 속한 그룹: %s", g.Name)
    }

    return nil
}

위의 코드는 그래프 탐색의 기본적인 예제로, 먼저 사용자를 조회한 후 사용자의 자동차와 그룹을 탐색합니다.

7. 데이터베이스 스키마 시각화

7.1. Atlas 도구 설치

ent에 의해 생성된 데이터베이스 스키마를 시각화하기 위해 Atlas 도구를 사용할 수 있습니다. Atlas의 설치 단계는 매우 간단합니다. 예를 들어 macOS에서는 다음과 같이 설치할 수 있습니다:

brew install ariga/tap/atlas

참고: Atlas는 다양한 데이터베이스의 테이블 구조 버전 관리를 처리할 수 있는 범용 데이터베이스 마이그레이션 도구입니다. Atlas에 대한 자세한 소개는 이후 장에서 제공될 예정입니다.

7.2. ERD 및 SQL 스키마 생성

Atlas를 사용하여 스키마를 조회하고 내보내는 것은 매우 간단합니다. Atlas를 설치한 후에 다음 명령어를 사용하여 Entity-Relationship Diagram (ERD)을 조회할 수 있습니다:

atlas schema inspect -d [database_dsn] --format dot

또는 직접적으로 SQL 스키마를 생성할 수 있습니다:

atlas schema inspect -d [database_dsn] --format sql

여기서 [database_dsn]은 데이터 소스 이름(DSN)을 가리킵니다. 예를 들어 SQLite 데이터베이스의 경우 다음과 같을 수 있습니다:

atlas schema inspect -d "sqlite://file:ent.db?mode=memory&cache=shared" --format dot

이러한 명령어로 생성된 출력물은 각각의 도구를 사용하여 뷰 또는 문서로 추가 변환될 수 있습니다.

8. 스키마 마이그레이션

8.1. 자동 마이그레이션 및 버전 관리 마이그레이션

ent는 두 가지 스키마 마이그레이션 전략을 지원합니다: 자동 마이그레이션과 버전 관리 마이그레이션. 자동 마이그레이션은 런타임에서 스키마 변경을 검토하고 적용하는 과정을 의미하며, 개발 및 테스트에 적합합니다. 버전 관리 마이그레이션은 마이그레이션 스크립트를 생성하고, 제품 배포 전에 신중한 검토와 테스트가 필요합니다.

팁: 자동 마이그레이션에 대한 내용은 4.1절의 내용을 참고하세요.

8.2. 버전 관리 마이그레이션 수행

버전 관리 마이그레이션의 과정은 Atlas를 통해 마이그레이션 파일을 생성하는 것을 포함합니다. 아래는 관련 명령어입니다:

마이그레이션 파일 생성:

atlas migrate diff -d ent/schema/path --dir migrations/dir

그 후 이러한 마이그레이션 파일을 데이터베이스에 적용할 수 있습니다:

atlas migrate apply -d migrations/dir --url database_dsn

이 과정을 통해 각 마이그레이션 전에 데이터베이스 마이그레이션의 이력을 유지하고 각 마이그레이션 전에 철저한 검토를 보장할 수 있습니다.

팁: https://github.com/ent/ent/tree/master/examples/start의 샘플 코드를 참고하세요.