entgo
는 Go
진영에서 사용되는 ORM
중 하나다. 주요 기능 중 하나로 트랜잭션도 제공한다.
다음은 공식 예제 코드다.
func WithTx(ctx context.Context, client *ent.Client, fn func(tx *ent.Tx) error) error {
tx, err := client.Tx(ctx)
if err != nil {
return err
}
defer func() {
if v := recover(); v != nil {
tx.Rollback()
panic(v)
}
}()
if err := fn(tx); err != nil {
if rerr := tx.Rollback(); rerr != nil {
err = fmt.Errorf("%w: rolling back transaction: %v", err, rerr)
}
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("committing transaction: %w", err)
}
return nil
}
WithTx
함수는 파라미터로 fn
을 받는데, 트랜잭션 내부에서 실행할 작업을 정의한 함수다.
defer
를 사용하여 함수 종료 시 panic
이 발생하면 트랜잭션을 롤백하고 상위 호출자에게 전달한다.
뿐만 아니라 fn
을 실행했을 때 에러가 발생하면 트랜잭션을 롤백한다.
작업이 성공하면 트랜잭션을 커밋한다.
함수 안에 함수를 넣어서 함수 전후 동작을 컨트롤하는 것이 파이썬
의 데코레이터
나 Spring
의 AOP
와 비슷한 느낌이다.
위 코드를 사용하는 코드는 다음과 같다. 공식 예제긴 한데, 바로 이해가 잘 안 갈 것이다.
entgo
는 공식 문서가 좀 불친절 한 것 같다.
func Do(ctx context.Context, client *ent.Client) {
// WithTx helper.
if err := WithTx(ctx, client, func(tx *ent.Tx) error {
return Gen(ctx, tx.Client())
}); err != nil {
log.Fatal(err)
}
}
출처 : https://entgo.io/docs/transactions
WithTx
를 사용하려면, 다음처럼 작성할 수 있다.
err := tx.WithTx(
ctx,
client,
func(tx *ent.Tx) error {
session, err := s.sessionRepository.Create(
ctx,
tx.Client(),
input,
)
if err != nil {
return err
}
return nil
})
위 코드에서 나는 다음 두 가지를 하고 싶었다.
1. session
이라는 변수를 WithTx
라는 코드가 끝난 이후에도 사용해야 한다.
2. WithTx
의 리턴 값 형식을 바꾸고 싶지 않다. (다른 곳에서도 이미 사용하고 있는 공통 코드이기 때문)
그래서 내부 함수에서 외부 함수의 변수를 참조하는 closure
를 사용하기로 했다.
다음은 그 코드다.
var createdSession *ent.Session
err := tx.WithTx(
ctx,
client,
func(tx *ent.Tx) error {
session, err := s.sessionRepository.Create(
ctx,
tx.Client(),
input,
)
if err != nil {
return err
}
createdSession = session//익명 함수에서 외부 변수를 사용함.
return nil
})
하지만 closure
는 주의해야 할 요소가 있다.
closure
는 외부 변수에 접근하고 변경할 수 있기 때문에 동시성 문제가 발생할 수 있다.closure
는 스코프를 벗어나는 것이다. 전역 변수
문제는 스코프를 벗어나는 대표적인 문제다. closure
역시 잘못하면 전역 변수
처럼 디버깅이 어려워질 수 있다. 어떤 문제가 발생해도 데이터의 추적이 끔찍해서 원인을 찾아내기 어렵다. closure
에서 사용하는 외부 변수는 closure
가 살아있는 동안 계속 메모리에 존재하므로 메모리 누수
가 발생할 수 있다.다음은 최종 코드다.
createdSession
이라는 closure
의 외부 변수 범위를 메서드 내로 제한하여 사이드 이펙트를 줄였다. 메서드 내에서 선언된 지역 변수이므로 메서드 실행이 끝나면 가비지 컬렉션
의 대상이 될 것이다. createdSession
은 사용자 id마다 다르다. 따라서 각 사용자 요청마다 만들어지는 것도 다르다. 따라서 사용자 별로 고립된 상태가 보장되서 동시성 문제가 발생하지 않는다.func (s *somethingService) createSession(ctx context.Context, client *ent.Client, input ent.CreateSessionInput) (*ent.Session, error) {
// 익명 함수에서 사용하는 외부 변수.
// 생성과 사용 영역을 전역이 아닌, 함수 내부로 좁히기 위해 함수를 따로 만듬.
var createdSession *ent.Session
err := tx.WithTx(
ctx,
client,
func(tx *ent.Tx) error {
session, err := s.sessionRepository.Create(
ctx,
tx.Client(),
input,
)
if err != nil {
return err
}
createdSession = session//익명 함수에서 외부 변수를 사용함.
return nil
})
return createdSession, err
}