일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 |
- RDS
- goland
- Kubernetes
- AI
- GoF
- authorizationpolicy
- GIT
- 티스토리챌린지
- replication lag
- esbuild
- context7
- 오블완
- ssh 에이전트
- 구조체
- Infra
- 디자인패턴
- go-sql-driver
- redirect-gateway
- 2024 톨스토이문학상 수상
- elasticsearch
- golang
- sqs fifo queue
- 캡슐화
- database/sql
- javascript
- go
- AWS
- Intellij
- typescript
- blank import
- Today
- Total
Fall in IT.
Go에서 DB 데이터를 도메인 엔티티로 복원 전략과 Java 비교 본문
DDD(Domain Driven Design)를 적용하여 Go 프로젝트를 개발할 때, 데이터베이스에서 조회한 데이터를 도메인 엔티티로 복원하는 과정은 항상 수반되는 아주 귀찮은 작업이다.
이 글에서는 엔티티의 캡슐화를 유지하면서 효율적으로 복원하는 방법에 대해 정리해보았다.
Go에서 DB → DTO → Domain Entity로 복원하는 세 가지 방법과 각각의 장단점 그리고 마지막으로 Java(Spring boot + JPA)에서의 처리 방법을 비교해보겠다.
먼저, 일반적인 Account Entity 구조를 살펴보자.
// 계좌 엔티티
type Account struct {
id ID
name string
balance money.Money
version int
createdAt time.Time
updatedAt time.Time
events []string // 도메인 이벤트
}
// 도메인 로직 메서드
func (a *Account) Withdraw(amount money.Money) error { ... }
func (a *Account) Deposit(amount money.Money) error { ... }
이런 엔티티를 데이터베이스에서 조회한 DTO로 부터 복원해야할때, 어떤 방법을 이용해서 복원할 수 있을까?
- Entity 복원용 팩토리 메서드
- Entity 객체를 생성해주는 Builder 패턴
- Entity의 필드를 private → public으로 전환
1. 팩토리 메서드
account 패키지 내부에 엔티티 객체를 복원하는 팩토리 메서드(생성자)를 구현하여 사용한다.
장점은 엔티티 캡슐화와 무결성을 유지할 수 있다. 외부에서 임의로 필드를 변경하거나 생성할 수 없기 때문에 안전하고 도메인 규칙이 account 엔티티 내부에 모여있기 때문에 유지보수가 쉽다.
단점은 필드가 많아질수록 팩토리 메서드의 인자가 길어져 가독성이 저하될 수 있다.
예시코드
func RestoreAccount(
id string,
name string,
balance money.Money,
version int,
createdAt time.Time,
updatedAt time.Time,
) (*Account, error) {
if id == "" {
return nil, errors.New("계좌 ID는 필수입니다")
}
if name == "" {
return nil, errors.New("계좌명은 필수입니다")
}
return &Account{
id: ID{value: id},
name: name,
balance: balance,
version: version,
createdAt: createdAt,
updatedAt: updatedAt,
events: []string{},
}, nil
}
// usage
func (r *AccountRepository) toDomainModel(dto *accountDTO) (*account.Account, error) {
balanceMoney, _ := money.New(dto.Balance)
return account.RestoreAccount(
dto.ID,
dto.Name,
balanceMoney,
dto.Version,
dto.CreatedAt,
dto.UpdatedAt,
)
}
2. Builder 패턴
Entity 객체를 생성해주는 Builder 패턴을 구현해서 사용한다.
장점은 가독성이 높고 선택적으로 필드를 설정할 수 있다.
단점은 필드가 많을수록 관리해야하는 코드가 많아진다.
예시코드
type AccountBuilder struct {
id ID
name string
balance money.Money
version int
createdAt time.Time
updatedAt time.Time
events []string
}
func NewAccountBuilder() *AccountBuilder {
return &AccountBuilder{events: []string{}}
}
func (b *AccountBuilder) WithID(id ID) *AccountBuilder {
b.id = id
return b
}
func (b *AccountBuilder) WithName(name string) *AccountBuilder {
b.name = name
return b
}
(생략...)
func (b *Accountbuilder) Build() (*Account, error) {
// 필수 피드 검증
if b.id.value == "" || b.name == "" {
return nil, errors.New("필수 필드가 누락되었습니다")
}
return &Account{
id: b.id,
name: b.name,
balance: b.balance,
version: b.version,
createdAt: b.createdAt,
updatedAt: b.updatedAt,
events: b.events,
}, nil
}
// usage
func (r *AccountRepository) toDomainModel(dto *accountDTO) (*account.Account, error) {
balanceMoney, _ := money.New(dto.Balance)
return account.NewAccountBuilder().
WithID(account.ID{value: dto.ID}}.
WithName(dto.Name).
WithBalance(balanceMoney).
WithVersion(dto.Version).
WithTimestamps(dto.CreatedAt, dto.UpdatedAt).
Build()
}
3. Entity의 필드를 private → public으로 전환
엔티티의 필드를 private에서 public으로 전환하는 방법이다.
장점은 코드가 가장 단순하고 직관적이다. DTO → Entity로 변환이 가장 간단하다.
단점은 도메인 객체의 캡슐화가 보장되지 않는다. 즉, 어디서든 제약없이 필드 변환이 가능하고 생성 또한 가능하다. 도메인 규칙에 위배되기 쉽고 데이터 조작이 가능하다. 따라서 장기적인 유지보수 측면에서 버그 위험성이 높아진다.
예시코드
type Account struct {
ID ID
Name string
Balance money.Money
Version int
CreatedAt time.Time
UpdatedAt time.Time
events []string
}
Go 언어 철학의 관점과 개인적인 생각
Go 언어는 복잡한 추상화보다는 명시적이고 이해하기 쉬운 실용적인 코드를 추구한다. 따라서, 단순한 구조체를 선호하는 경향이 있다.
개인적인 생각으로는 비즈니스 도메인 모델을 표현할 때는 캡슐화 또한 굉장히 중요하기 때문에 유지보수 관점에서 도메인 엔티티의 필드 속성 자체를 private에서 public으로 변환하는 것 보다는 팩토리 메서드를 두거나 Builder 패턴을 적절히 활용하는게 더 현명한 방법이라고 생각한다.
Java(Spring Boot + JPA)와의 차이점
Java에서는 ORM(JPA/Hibernate)이 엔티티 복원을 자동으로 처리한다. 갑자기 Java를 해보고 싶다.
@Entity
@Table(name = "accounts")
public class Account {
@Id
private String id;
private String name;
private BigDecimal balance;
private int version;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@Transient
private List<String> events = new ArrayList<>();
protected Account() {}
public static Account create(String name, BigDecimal initialBalance) {
// Do something..
}
public void withdraw(BigDecimal amount) { ... }
public void deposit(BigDecimal amount) { ... }
}
// JPA가 DB row를 읽어서 필드를 채워 자동으로 복원 처리를 한다.
// 별도의 팩토리 메서드가 필요하지 않다.
왜 Go에서는 ORM이 Java처럼 강력하지 않을까?
여러가지 이유가 있겠지만, 그 중 두 가지 원인이 있다.
- 언어적 제약
- Go 언어의 철학
언어적 제약
Java의 경우 강력한 리플렉션 시스템이 있기 때문에 런타임에 클래스의 메타데이터를 자유롭게 조작할 수 있다. 하지만 Go 언어는 리플렉션이 존재하지만 Java 만큼 자유롭지 않다. (자세한 내용은 이 글에서 다루지 않는다.)
Go 언어의 철학
Java는 프레임워크의 자동화를 추구하는 문화이다. Go의 경우는 추상화를 경계하는 문화이다. 디버깅이 용이하고, 성능 예측이 가능한 명시적인 코드를 지향한다.
정리하면, 이런 언어적 제약이나 철학 차이로 인해서 ORM이 Java만큼 강력하지 않다고 생각한다. 하지만, 최근 Go ORM인 ent 프로젝트(반 자동화 수준)를 보면 미래에는 Java 보다 더 강력한 혹은 비슷한 수준의 ORM 도구가 나올수도 있지 않을까 예상해본다.
그래도 결국 중요한건 Go 언어가 어떤 분야에 많이 사용되느냐에 따라 그에 맞게 발전될 거라고 생각한다.
'프로그래밍언어 > Golang' 카테고리의 다른 글
Go에서 context.WithValue() 안전하게 사용하기 (0) | 2025.09.03 |
---|---|
Go에서 _ "github.com/go-sql-driver/mysql"는 왜 필요한가? (0) | 2025.09.02 |
DB 업데이트, SQS 메시지 발송 트랜잭션으로 묶을 수 있을까? (4) | 2025.07.18 |
Go언어에서 구조체의 필드는 Public? Private? 어떤게 맞을까? (0) | 2025.03.29 |
Go 언어로 PubSub 모델 개발하기 (2) | 2024.11.24 |