실무에서 써먹는 불변성

소프트웨어에서 말하는 불변성Immutability이라는 것이 있다. 즉 변하지 않는다는 것이다. 다른 말로는 읽기 전용이라고도 해석할 수 있다. 무엇이 변하지 않는다는 것일까? 객체 지향 프로그래밍으로 좁혀서 생각해 보면 한번 만들어진 객체가 변하지 않다는 것을 의미한다.

불변 클래스란 간단히 말해 그 인스턴스의 내부 값을 수정할 수 없는 클래스다.(중략)불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전하다. - 이펙티브 자바 3판, 105 쪽

또한 도메인 주도 설계Domain-Driven Design에서는 값 객체Value Object를 말하면서 이를 불변적으로 다루라고 한다.

모델에 포함된 어떤 요소에 속성에만 관심이 있다면 그것을 VALUE OBJECT로 분류하라.(중략) VALUE OBJECT는 불변적(immutable)으로 다뤄라. - 도메인 주도 설계, 101 쪽

객체를 불변적으로 다루는 것의 장점이 무엇일까?

불변 객체는 단순하다. 불변 객체는 생성된 시점의 상태를 파괴할 때까지 그대로 간직한다. 모든 생성자가 클래스 불변식을 보장한다면 그 클래스를 사용하는 프로그래머가 다른 노력을 들이지 않더라도 영원히 불변으로 남는다. 반면 가변 객체는 임의의 복잡한 상태에 놓일 수 있다. 변경자 메서드가 일으키는 상태 전이를 정밀하게 문서로 남겨 놓지 않은 가변 클래스는 믿고 사용하기 어려울 수 도 있다. - 이펙티브 자바 3판, 107 - 108 쪽

예를 들어 영수증 금액이 바뀐다면 생각만 해도 끔찍할 것이다. 또 다른 예로 전자 상거래에서 주문 처리를 할 때 주문 당시의 정보(상품, 쿠폰, 결제, 회원, 배송지 등)을 그대로 보관한다. 이 정보는 불변하다. 필자가 쓴 DDD 값 객체와 마이크로서비스 글에서 상세한 구현 내용을 확인할 수 있다.

전자 상거래 사이트에서 상품을 받을 주소(배송지)를 입력하고 주문한다고 생각해 보자. 시스템에서 배송지를 임의로 수정한다면 상품은 정상적으로 배송되지 못할 것이다. 따라서 시스템은 배송지를 바뀌지 않게 다뤄야 한다. (중략) 주문 업무의 특징 중 하나는 주문 당시 데이터(상품, 쿠폰, 결제, 회원, 배송지 등 - 이하 스냅샷Snapshot)를 기준으로 주문 처리를 한다는 것이다. 이러한 스냅샷 데이터를 모두 엔터티로 설계한다면 도메인 모델은 매우 복잡해질 것이다. 스냅샷이라는 말 자체가 의미하는 것처럼 바뀌지 않는 값이다. 이것은 값 객체로 설계할 수 있음을 의미한다. -  DDD 값 객체와 마이크로서비스

기부 확인서는 가변인가? 불변인가?

필자는 사회적 기업과 함께 일하고 있다. 여기서는 기부를 받고 있는데 기부자가 회사나 다른 기관에서 기부 내용을 제출하기 위해 ‘기부 확인서'를 발급하는 절차가 있다. 기부 확인서를 날짜를 바꿔서 계속해서 제출할 수 있다면 부정의 소지 있고 이런 일이 반복되면 기부확인서에 대한 불신까지 생길 소지가 있다. 따라서 수정이 되지 않고 설사 실수로 기입이 잘못된 경우 발급한 기부확인서를 무효화하고 새로운 기부확인서를 발급하는 절차가 필요하다.

기부 확인서는 발급 시점의 기부의 상태를 보여주는 것이다. 그렇다면 기부 확인서는 변하는가? 변하지 않는가? 필자의 생각에는 영수증과 같다. 한번 발급한 확인서는 변하지 않는다. 따라서 불변적으로 다루어야 한다.

기부 영수증을 발급할 때 기부 정보는 그대로 보관되어야 한다. 왜냐하면 기부는 가변적이기 때문이다. 기부 영수증 발급 당시의 기부 정보를 저장해 두지 않는다면 추적의 어려움을 겪을 수밖에 없고 재 발급 역시 어려워진다.

기부 정보를 그대로 보관한다는 것은 무슨 의미인가?

사진에서는 스냅샷Snapshot 이라는 말이 있다. 빠르게 순간적인 장면을 촬영하거나 자연스러운 표정을 촬영하는 의미로 쓰인다. 컴퓨터 분야에서는 이 개념을 받아들여 마치 사진 찍듯이 특정 시점에 스토리지의 파일 시스템을 포착해 보관하는 것을 뜻한다. 가상화 도구(패러럴 데스크톱이나 버추얼 박스)에서 스냅샷을 실행하면 언제든지 그 시점으로 돌아갈 수 있다.

기부 정보를 그대로 보관한다는 것은 스냅샷 데이터로 만든다는 것이다. 구체적으로 어떻게 구현할 수 있을까?

객체 지향 프로그래밍에서는 기부 정보는 객체로 만들 수 있으며 기부 객체를 JSON으로 직렬화하여 이를 데이터베이스에 보관할 수 있다.

코드로 만들어보면

개념도는 아래와 같다. 하나의 기부는 하나 이상의 기부 확인서를 발급할 수 있다.

개념도

개념도

이를 데이터베이스로는 두 가지 방식으로 만들 수 있다.

1. 기부 테이블(donations)에 기부 확인서 컬럼(donation_confirmations)을 만들고 기부 확인서 발급시 기부를 JSON으로 직렬화하여 저장한다.

ERD

ERD

2. 별도 기부 확인서 테이블(donation_confirmations)을 만들고 기부를 JSON으로 직렬화하여 저장(confirmation) 한다.

ERD

ERD

1번은 데이터 조회 시 테이블 JOIN이 발생하지 않기 때문에 성능상 2번 보다 낫다고 할 수 있지만 데이터 분석 측면에서는 2번이 더 낫다. 상황에 따라 선택하는 것이 좋다.

1번 방식을 Golang과 GORM으로 구현해 보자.

DonationService는 기부 번호로 기부를 조회하여 기부 확인서를 발급 후에 저장한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type DonationService struct {
}
func (DonationService) PublishConfirmation(ctx context.Context, donationId uint) error {
	// 기부 조회
	donationEntity, err := donationRepository{}.FindById(ctx, donationId)
	if err != nil {
		return err
	}
	// 기부 확인서 발급
	if err := donationEntity.PublishConfirmation(); err != nil {
		return err
	}
	// 저장
	return donationRepository{}.Save(ctx, donationEntity)
}

DonationEntity에서는 현재 기부 상태를 JSON으로 직렬화하여 기부 확인서 필드에 저장한다. 기부 확인서를 여러번 발급될 수 있기 때문에 배열로 만들었다.

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
31
type DonationEntity struct {
	gorm.Model
	// ...
	DonationConfirmations string `json:"-"`
}
func (DonationEntity) TableName() string {
	return "donations"
}
func (d *DonationEntity) PublishConfirmation() error {
	// 1. 현재 기부 상태를 직렬화
	var donationSerialized map[string]interface{}
	b, err := json.Marshal(d)
	if err != nil {
		return err
	}
	json.Unmarshal(b, &donationSerialized)
	// 2. 기존에 발행 되었던 기부 확인서 목록을 역직렬화
	var confirmations []interface{}
	if err := json.Unmarshal([]byte(d.DonationConfirmations), &confirmations); err != nil {
		return err
	}
	// 3. 기존 확인서 목록에 기부 확인서 추가
	confirmations = append(confirmations, donationSerialized)
	// 4. 기부 확인서 목록을 JSON 직력화 후 저장
	b, err = json.Marshal(confirmations)
	if err != nil {
		return err
	}
	d.DonationConfirmations = string(b)
	return nil
}

저장된 기부확인서는 발급 당시 기부정보를 모두 JSON으로 가지고 있기 때문에 UI에서 PDF 같은 양식으로 만들 수 있다.


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.