2024. 4. 20. 15:15ㆍSoftware Architecture/Software design
- 목차
도메인 '모델링' = 비즈니스 프로세스를 모델링
- 소프트웨어를 개발하는 목적은 어떤 비즈니스(작업)을 전산화 하고자 하는데 있습니다.
- 도메인은 전산화 하고자 하는 비즈니스에 대한 내용(영역)을 의미합니다.
- 모델링은 비즈니스 프로세스를 S/W 설계 방법을 통해 비즈니스 처리 과정을 표현하는 것을 의미합니다.
- 그렇기에 모델링은 요구사항 분석 과정의 주요 도구로서 사용됩니다.
개념 정리
모델링 vs. 설계
- 모델링은 비즈니스 프로세스를 전산으로 표현하는 과정까지를 수행하는 것을 의미합니다.
- 모델링의 결과물도 설계 산출물 (광의적 의미로 보면 모델링도 설과 과정 중 일부)로 간주할 수 있습니다.
- 반면 설계는 모델링된 (즉, S/W 요구사항) 결과물을 통해 S/W 적으로 구현하기 위한 내용들을 설계하는 것을 의미합니다.
저수준 설계 vs. 고수준 설계
설계 진행 시 우선 고수준 설계를 수행합니다.
- 고수준 설계
- 설계하고자 하는 컴포넌트에 대한 식별 및 인터페이스를 정의합니다.
- 이후 요구사항을 통해 도출된 주요 유스케이스를 기반으로 인터페이스를 통해 여러 컴포넌트 간의 인터렉션을 설계합니다.
- 이때 데이터 및 이벤트의 흐름 등 구체적인 통신 프로토콜도 함께 정의됩니다.
- 저수준 설계
- 이후 식별된 컴포넌트를 주요 서브 컴포넌트로 분리하여 식별하고 이들 (클래스)의 인터페이스와 내부 구현(알고리즘 포함) 방법에 대해 설계합니다.
- 고수준 설계때와 마찬가지로 식별된 서브 컴포넌트(클래스 수준)들의 인터페이스와 인터렉션을 설계하며, 구현에 사용할 알고리즘까지 설계합니다.
도메인 주도 개발 ?
앞서 설명드린 내용에 의하면 도메인 모델링은 원래 요구사항 분석 단계에서 수행하는 과정 중 하나입니다. 즉, 방법과 사용하는 도구 등에 있어서는 새로울 것이 없습니다. 그런데 왜 도메인 주도 개발이라는 용어를 사용하게 되었을까요? 요구사항 분석 단계에서 산출된 모델 결과물 및 도메인 용어들을 분석단계 뿐만 아니라, 설계(즉 개발) 단계에서도 계속 사용하자는 것이 도메인 주도 개발의 핵심입니다. 도메인 모델링을 통해 산출된 주요 산출물 중 하나는 유비쿼터스 용어집이며, 여기서 정의된 용어와 도메인 프로세스 등을 수정 없이 설계 과정에서도 그대로 사용하는 것을 의미합니다. 즉, 도메인 모델링을 통해 산출되는 산출물들이 설계에서도 영향을 주고 그대로 사용되기 때문에 도메인이 전체 개발을 driven(주도) 한다고 하여 '도메인 주도 개발'이라는 용어가 탄생하게 되었습니다.
참고로 TDD도 유사한데, 요구사항 모델링 과정을 통해 산출된 유스케이스 시나리오를 바탕으로 테스트 케이스를 작성하고 테스트 케이스가 필요로하는 컴포넌트를 정의하고 설계/개발 하는 과정을 진행하는 즉, 테스트가 개발을 주도하는 것이 TDD 입니다.
BDD란것도 있는데, BDD는 행위 주도 개발을 의미합니다. 이는 시스템의 행동을 중심으로 개발을 하자는 개발 방법론 중 하나인데, TDD와 BDD 둘 다 유스케이스 시나리오를 사용하여 개발 할 수 있습니다.
TDD
TDD는 테스트 코드를 먼저 개발해서 개발을 주도해 나가는 개발 방법론을 의미합니다. 테스트 코드를 먼저 작성하면 TDD, 그렇지 않으면 기존 개발 방법이라고 할 수 있겠습니다. 기술적으로 어떤 특별한 것이 있는 것이 아닙니다. 이렇게 요구사항에 대해 테스트 코드를 작성해서 모든 요구사항을 우선 검증할 수 있게 되기 때문에 요구사항을 명확히 하는 도구로서도 큰 의미를 지니게 됩니다. 세부 요소에 대한 개발을 수행하기전에 요구사항을 먼저 명확히 할 수 있기 때문에 TDD는 개발 공수 (즉, 개발하고 잘 못된 요구사항 혹은 요구사항의 잘못된 이해에 의해 발생하는 재 개발 등)를 줄일 수 있다는 큰 장점이 있습니다. 이외에도 몇 가지 장점이 있으며 이는 다음과 같습니다.
- 사양을 명확히 하는 작업을 먼저 수행하게 됨
- 사용하기 쉬운 코드를 작성하게 됨
- 테스트 하기 쉬운 코드는 결국 보다 커플링 되지 않은 응집된 코드이며, 인자의 설정 결과의 확인 방법이 보다 명확하고 쉽다는 것을 의미함
- 또한 테스트 코드를 먼저 작성하기 때문에 테스트 코드가 빠져서 검증이 안 되는 경우가 없어짐
이렇게 failing test를 먼저 작성하는 것에 의해 '작성된' 코드에 대해서는 테스트가 어느정도 진행된 것을 보장하게 됩니다.
BDD
TDD에서 한발 더 나아가 테스트 케이스 자체가 요구사양을 반영하도록 하는 것입니다.
BDD는 유스케이스 시나리오를 기반으로 테스트 케이스를 작성합니다. 이는 시스템이 행동해야 할 행위를 기반으로 충분히 추상화된 수준에서의 시스템 동작을 테스트로 작성합니다. 이에 유스케이스 시나리오가 필요한 것들을 BDD에서도 명시합니다.
- Feature: 테스트의 기능/책임
- Scenario: 기능의 사용 시나리오
- Given: 시나리오 동작에 주어지는 사항들
- When: 시나리오 진행 상황
- Then: 시나리오 완료 시 결과
이를 통해 시나리오를 먼저 개발하고 이를 통해 테스트 코드 -> 실제 코드를 개발하는 단계를 따르게 됩니다. 즉, 시나리오의 행위를 먼저 개발하게 되기에 행위(시나리오) 주도 개발이라고 합니다.
TDD는 테스트 코드 작성 시 무엇을 받고, 언제하고 어떻게 해야 한다는 '기술' 내용이 없습니다. 또한 코드에 직접적으로 연관되어 있기때문에 구현 코드에 수정이 발생하면 테스트 코드도 함께 수정되어야 합니다. 그리고 모든 사양에 대해서 테스트 코드를 작성해야 할 의무도 없습니다. 그저 작성된 코드를 테스트할 뿐이죠. 당연히 테스트 코드가 무엇을 위한 테스트 코드인지에 대해서 관리하지 않습니다.
BDD는 대화와, 구체적인 테스트 코드 등을 통해서 원하는 행위를 도출합니다. 우선 '무엇'을 해야 하는지에 대해 파악합니다. 또한 자연어로 테스트를 기술하기에 특정 구현, 언어에 종속적이지 않은 테스트를 작성할 수 있습니다.
ex.
BDD와 TDD는 상호보완적 관계이므로 둘을 적절히 섞어서 사용해야 합니다.
BDD를 지원하는 툴로서 Cucumbers가 있습니다. 이는 Gherkin 문법으로 기능의 내용을 자연어로 작성합니다.
go 언어에서 BDD로 테스트를 작성하는 예를 살펴보겠습니다.
Ginkgo를 우선 설치합니다.
$ go get github.com/onsi/ginkgo/ginkgo
$ go get github.com/onsi/gomega/...
cd test1
ginkgo bootstrap을 수행하면 test1_suite_test.go 파일이 다음의 내용을 담고 생성됩니다.
package test1_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestTest1(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Test1 Suite")
}
$ go test
혹은
$ ginkgo
로 통해 작성된 테스트를 수행할 수 있습니다.
테스트를 작성해 보겠습니다.
우선 다음의 명령어로 테스트 파일을 생성합니다.
$ ginkgo generate test1
test1_test.go가 생성됩니다.
다음과 같이 테스트 코드를 작성할 수 있습니다.
(Ginkgo 페이지에서 발췌한 코드 입니다)
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
...
)
var _ = Describe("Checking books out of the library", Label("library"), func() {
var library *libraries.Library
var book *books.Book
var valjean *users.User
BeforeEach(func() {
library = libraries.NewClient()
book = &books.Book{
Title: "Les Miserables",
Author: "Victor Hugo",
}
valjean = users.NewUser("Jean Valjean")
})
When("the library has the book in question", func() {
BeforeEach(func(ctx SpecContext) {
Expect(library.Store(ctx, book)).To(Succeed())
})
Context("and the book is available", func() {
It("lends it to the reader", func(ctx SpecContext) {
Expect(valjean.Checkout(ctx, library, "Les Miserables")).To(Succeed())
Expect(valjean.Books()).To(ContainElement(book))
Expect(library.UserWithBook(ctx, book)).To(Equal(valjean))
}, SpecTimeout(time.Second * 5))
})
Context("but the book has already been checked out", func() {
var javert *users.User
BeforeEach(func(ctx SpecContext) {
javert = users.NewUser("Javert")
Expect(javert.Checkout(ctx, library, "Les Miserables")).To(Succeed())
})
It("tells the user", func(ctx SpecContext) {
err := valjean.Checkout(ctx, library, "Les Miserables")
Expect(err).To(MatchError("Les Miserables is currently checked out"))
}, SpecTimeout(time.Second * 5))
It("lets the user place a hold and get notified later", func(ctx SpecContext) {
Expect(valjean.Hold(ctx, library, "Les Miserables")).To(Succeed())
Expect(valjean.Holds(ctx)).To(ContainElement(book))
By("when Javert returns the book")
Expect(javert.Return(ctx, library, book)).To(Succeed())
By("it eventually informs Valjean")
notification := "Les Miserables is ready for pick up"
Eventually(ctx, valjean.Notifications).Should(ContainElement(notification))
Expect(valjean.Checkout(ctx, library, "Les Miserables")).To(Succeed())
Expect(valjean.Books(ctx)).To(ContainElement(book))
Expect(valjean.Holds(ctx)).To(BeEmpty())
}, SpecTimeout(time.Second * 10))
})
})
When("the library does not have the book in question", func() {
It("tells the reader the book is unavailable", func(ctx SpecContext) {
err := valjean.Checkout(ctx, library, "Les Miserables")
Expect(err).To(MatchError("Les Miserables is not in the library catalog"))
}, SpecTimeout(time.Second * 5))
})
})
다음과 같이 요구사항이 작성되어 있다고 가정합니다.
(아래 내용은 https://semaphoreci.com/community/tutorials/getting-started-with-bdd-in-go-using-ginkgo에서 가져온 내용입니다)
Given a shopping cart
initially
it has 0 items
it has 0 units
the total amount is 0.00
when a new item is added
the shopping cart has 1 more unique item than it had earlier
the shopping cart has 1 more unit than it had earlier
the total amount increases by item price
when an existing item is added
the shopping cart has the same number of unique items as earlier
the shopping cart has 1 more unit than it had earlier
the total amount increases by item price
that has 0 unit of item A
removing item A
should not change the number of items
should not change the number of units
should not change the amount
that has 1 unit of item A
removing 1 unit item A
should reduce the number of items by 1
should reduce the number of units by 1
should reduce the amount by the item price
that has 2 units of item A
removing 1 unit of item A
should not reduce the number of items
should reduce the number of units by 1
should reduce the amount by the item price
removing 2 units of item A
should reduce the number of items by 1
should reduce the number of units by 2
should reduce the amount by twice the item price
이를 깅코에서는 다음과 작성할 수 있습니다.
package cart_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestCart(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Shopping Cart Suite")
}
var _ = Describe("Shopping cart", func() {
Context("initially", func() {
It("has 0 items", func() {})
It("has 0 units", func() {})
Specify("the total amount is 0.00", func() {})
})
Context("when a new item is added", func() {
Context("the shopping cart", func() {
It("has 1 more unique item than it had earlier", func() {})
It("has 1 more unit than it had earlier", func() {})
Specify("total amount increases by item price", func() {})
})
})
Context("when an existing item is added", func() {
Context("the shopping cart", func() {
It("has the same number of unique items as earlier", func() {})
It("has 1 more unit than it had earlier", func() {})
Specify("total amount increases by item price", func() {})
})
})
Context("that has 0 unit of item A", func() {
Context("removing item A", func() {
It("should not change the number of items", func() {})
It("should not change the number of units", func() {})
It("should not change the amount", func() {})
})
})
Context("that has 1 unit of item A", func() {
Context("removing 1 unit item A", func() {
It("should reduce the number of items by 1", func() {})
It("should reduce the number of units by 1", func() {})
It("should reduce the amount by item price", func() {})
})
})
Context("that has 2 units of item A", func() {
Context("removing 1 unit of item A", func() {
It("should not reduce the number of items", func() {})
It("should reduce the number of units by 1", func() {})
It("should reduce the amount by the item price", func() {})
})
Context("removing 2 units of item A", func() {
It("should reduce the number of items by 1", func() {})
It("should reduce the number of units by 2", func() {})
It("should reduce the amount by twice the item price", func() {})
})
})
})
이후 구현 사항을 추가하여 테스트 코드를 완성합니다.
package cart_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
. "."
)
func TestCart(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Shopping Cart Suite")
}
var _ = Describe("Shopping cart", func() {
itemA := Item{ID: "itemA", Name: "Item A", Price: 10.20, Qty: 0}
itemB := Item{ID: "itemB", Name: "Item B", Price: 7.66, Qty: 0}
Context("initially", func() {
cart := Cart{}
It("has 0 items", func() {
Expect(cart.TotalUniqueItems()).Should(BeZero())
})
It("has 0 units", func() {
Expect(cart.TotalUnits()).Should(BeZero())
})
Specify("the total amount is 0.00", func() {
Expect(cart.TotalAmount()).Should(BeZero())
})
})
Context("when a new item is added", func() {
cart := Cart{}
originalItemCount := cart.TotalUniqueItems()
originalUnitCount := cart.TotalUnits()
originalAmount := cart.TotalAmount()
cart.AddItem(itemA)
Context("the shopping cart", func() {
It("has 1 more unique item than it had earlier", func() {
Expect(cart.TotalUniqueItems()).Should(Equal(originalItemCount + 1))
})
It("has 1 more unit than it had earlier", func() {
Expect(cart.TotalUnits()).Should(Equal(originalUnitCount + 1))
})
Specify("total amount increases by item price", func() {
Expect(cart.TotalAmount()).Should(Equal(originalAmount + itemA.Price))
})
})
})
Context("when an existing item is added", func() {
cart := Cart{}
cart.AddItem(itemA)
originalItemCount := cart.TotalUniqueItems()
originalUnitCount := cart.TotalUnits()
originalAmount := cart.TotalAmount()
cart.AddItem(itemA)
Context("the shopping cart", func() {
It("has the same number of unique items as earlier", func() {
Expect(cart.TotalUniqueItems()).Should(Equal(originalItemCount))
})
It("has 1 more unit than it had earlier", func() {
Expect(cart.TotalUnits()).Should(Equal(originalUnitCount + 1))
})
Specify("total amount increases by item price", func() {
Expect(cart.TotalAmount()).Should(Equal(originalAmount + itemA.Price))
})
})
})
Context("that has 0 unit of item A", func() {
cart := Cart{}
cart.AddItem(itemB) // just to mimic the existence other items
cart.AddItem(itemB) // just to mimic the existence other items
originalItemCount := cart.TotalUniqueItems()
originalUnitCount := cart.TotalUnits()
originalAmount := cart.TotalAmount()
Context("removing item A", func() {
cart.RemoveItem(itemA.ID, 1)
It("should not change the number of items", func() {
Expect(cart.TotalUniqueItems()).Should(Equal(originalItemCount))
})
It("should not change the number of units", func() {
Expect(cart.TotalUnits()).Should(Equal(originalUnitCount))
})
It("should not change the amount", func() {
Expect(cart.TotalAmount()).Should(Equal(originalAmount))
})
})
})
Context("that has 1 unit of item A", func() {
cart := Cart{}
cart.AddItem(itemB) // just to mimic the existence other items
cart.AddItem(itemB) // just to mimic the existence other items
cart.AddItem(itemA)
originalItemCount := cart.TotalUniqueItems()
originalUnitCount := cart.TotalUnits()
originalAmount := cart.TotalAmount()
Context("removing 1 unit item A", func() {
cart.RemoveItem(itemA.ID, 1)
It("should reduce the number of items by 1", func() {
Expect(cart.TotalUniqueItems()).Should(Equal(originalItemCount - 1))
})
It("should reduce the number of units by 1", func() {
Expect(cart.TotalUnits()).Should(Equal(originalUnitCount - 1))
})
It("should reduce the amount by item price", func() {
Expect(cart.TotalAmount()).Should(Equal(originalAmount - itemA.Price))
})
})
})
Context("that has 2 units of item A", func() {
Context("removing 1 unit of item A", func() {
cart := Cart{}
cart.AddItem(itemB) // just to mimic the existence other items
cart.AddItem(itemB) // just to mimic the existence other items
//Reset the cart with 2 units of item A
cart.AddItem(itemA)
cart.AddItem(itemA)
originalItemCount := cart.TotalUniqueItems()
originalUnitCount := cart.TotalUnits()
originalAmount := cart.TotalAmount()
cart.RemoveItem(itemA.ID, 1)
It("should not reduce the number of items", func() {
Expect(cart.TotalUniqueItems()).Should(Equal(originalItemCount))
})
It("should reduce the number of units by 1", func() {
Expect(cart.TotalUnits()).Should(Equal(originalUnitCount - 1))
})
It("should reduce the amount by the item price", func() {
Expect(cart.TotalAmount()).Should(Equal(originalAmount - itemA.Price))
})
})
Context("removing 2 units of item A", func() {
cart := Cart{}
cart.AddItem(itemB) // just to mimic the existence other items
cart.AddItem(itemB) // just to mimic the existence other items
//Reset the cart with 2 units of item A
cart.AddItem(itemA)
cart.AddItem(itemA)
originalItemCount := cart.TotalUniqueItems()
originalUnitCount := cart.TotalUnits()
originalAmount := cart.TotalAmount()
cart.RemoveItem(itemA.ID, 2)
It("should reduce the number of items by 1", func() {
Expect(cart.TotalUniqueItems()).Should(Equal(originalItemCount - 1))
})
It("should reduce the number of units by 2", func() {
Expect(cart.TotalUnits()).Should(Equal(originalUnitCount - 2))
})
It("should reduce the amount by twice the item price", func() {
Expect(cart.TotalAmount()).Should(Equal(originalAmount - 2*itemA.Price))
})
})
})
})
'Software Architecture > Software design' 카테고리의 다른 글
Test double (테스트 더블) (0) | 2023.11.25 |
---|---|
entity vs. object (0) | 2023.11.01 |
DIP Dependency Inversion Principle 의존성 역전 원칙 (0) | 2023.05.29 |
UML (0) | 2022.05.06 |
Memento pattern (0) | 2022.02.28 |