복잡한 코드는 통상 도메인 모델이다.
물론 문제 도메인과 직접적으로 관련이 없는 복잡한 알고리즘이 존재할 수 있다.
협력자가 있는 경우가 거의 없는 코드이다.
복잡도나 도메인 유의성도 거의 없다.

Overcomplicated code


enum UserType {
case customer
case employee
}
final class User {
private(set) var userId: Int = 0
private(set) var email: String = ""
private(set) var type: UserType = .customer
func changeEmail(
userId: Int,
newEmail: String
) {
let (_, currentEmail, currentType) = Database.getUserById(userId)
self.userId = userId
self.email = currentEmail
self.type = currentType
if email == newEmail {
return
}
let (companyDomainName, numberOfEmployees) = Database.getCompany()
let emailDomain = newEmail.split(separator: "@").last.map(String.init) ?? ""
let isEmailCorporate = emailDomain == companyDomainName
let newType: UserType = isEmailCorporate ? .employee : .customer
if type != newType {
let delta = newType == .employee ? 1 : -1
let newNumber = numberOfEmployees + delta
Database.saveCompany(newNumber)
}
email = newEmail
type = newType
Database.saveUser(self)
MessageBus.sendEmailChangedMessage(
userId: userId,
newEmail: newEmail
)
}
}
struct Company {
private(set) var domainName: String
private(set) var numberOfEmployees: Int
mutating func changeNumberOfEmployees(_ delta: Int) {
precondition(numberOfEmployees + delta >= 0)
numberOfEmployees += delta
}
func isEmailCorporate(_ email: String) -> Bool {
guard let emailDomain = email.split(separator: "@").last else {
return false
}
return String(emailDomain) == domainName
}
}
enum CompanyFactory {
static func create(
_ data: (String, Int)
) -> Company {
Company(domainName: data.0, numberOfEmployees: data.1)
}
}
final class UserController {
private let database = Database()
private let messageBus = MessageBus()
func changeEmail(userId: Int, newEmail: String) {
let userData = database.getUserById(userId)
var user = UserFactory.create(userData)
let companyData = database.getCompany()
var company = CompanyFactory.create(companyData)
user.changeEmail(newEmail, company: &company)
database.saveCompany(company)
database.saveUser(user)
messageBus.sendEmailChangedMessage(
userId: userId,
newEmail: newEmail
)
}
}
enum UserType {
case customer
case employee
}
struct User {
private(set) var userId: Int
private(set) var email: String
private(set) var type: UserType
mutating func changeEmail(
_ newEmail: String,
company: inout Company
) {
if email == newEmail {
return
}
let newType: UserType = company.isEmailCorporate(newEmail) ? .employee : .customer
if type != newType {
let delta = newType == .employee ? 1 : -1
company.changeNumberOfEmployees(delta)
}
email = newEmail
type = newType
}
}
enum UserFactory {
static func create(
_ data: (Int, String, UserType)
) -> User {
User(userId: data.0, email: data.1, type: data.2)
}
}
| 코드복잡도와 도메인 유의성 | 협력자가 거의 없음 | 협력자가 많음 |
|---|---|---|
| 높음 | User.ChangeEmail(); Company.ChangeNumberOfEmployees(); Company.IsEmailCorporate(); CompanyFactory.create() | — |
| 낮음 | User와 Company의 생성자 | UserController.ChangeEmail() |
@Test("Changing email from non-corporate to corporate")
func changingEmailFromNonCorporateToCorporate() {
let company = Company(domainName: "mycorp.com", numberOfEmployees: 1)
let sut = User(userId: 1, email: "user@gmail.com", type: .customer)
sut.changeEmail("new@mycorp.com", company: company)
#expect(company.numberOfEmployees == 2)
#expect(sut.email == "new@mycorp.com")
#expect(sut.type == .employee)
}
func Changing_email_from_corporate_to_non_corporate()
func Changing_email_without_changing_user_type()
func Changing_email_to_the_same_one()
@Test(
"Differentiates a corporate email from non-corporate",
arguments: [
("mycorp.com", "email@mycorp.com", true),
("mycorp.com", "email@gmail.com", false),
]
)
func differentiatesCorporateEmail(
domain: String,
email: String,
expectedResult: Bool
) {
let sut = Company(domain: domain, numberOfEmployees: 0)
let isCorporate = sut.isEmailCorporate(email)
#expect(isCorporate == expectedResult)
}

하지만 현실적으로는 어렵다. 때로는 비즈니스 로직을 수행하는 중간 단계에서 외부 의존성으로부터 추가 데이터를 조회해야 한다거나 하는 상황이 있다고 가정한다.

도메인 모델 테스트 유의성(Domain model testability)
도메인 클래스의 협력자 수와 유형에 따른 함수
컨트롤러의 단순성(Controller simplicity)
의사 결정 지점이 있느냐 없느냐에 따라 달라질 수 있다.
성능(Performance)
프로세스 외부 의존성에 대한 호출 횟수로 정의할 수 있다.
옵션 1: 모든 외부 읽기/쓰기를 가장자리로 밀어내기:
-- 장점: 컨트롤러 단순성과 도메인 모델 테스트 용이성을 유지.
-- 단점: 성능을 희생. 필요 없는 경우에도 외부 의존성을 호출.
옵션 2: 프로세스 외부 의존성을 도메인 모델에 주입하기:
-- 장점: 성능과 컨트롤러 단순성을 유지.
-- 단점: 도메인 모델 테스트 용이성을 심각하게 훼손. 도메인 모델이 Overcomplicated code 가 되어 테스트하기 어려움.
옵션 3: 결정 과정을 더 세분화된 단계로 분할하기:
-- 장점: 성능과 도메인 모델 테스트 용이성을 유지.
-- 단점: 컨트롤러 복잡도 증가.

struct User {
private(set) var userId: Int
private(set) var email: String
private(set) var type: UserType
var isEmailConfirmed: Bool
func canChangeEmail() -> String? {
if isEmailConfirmed {
return "Can't change a confirmed email"
}
return nil
}
}
애플리케이션에서 정확히 무슨 일이 일어나는지 외부 시스템에 알려야 하기 때문에 이러한 단계들을 아는 것이 중요할지도 모른다.
도메인 모델에서 중요한 변경 사항을 추적하고 비즈니스 연산이 완료된 후 해당 변경 사항을 프로세스 외부 의존성 호출로 변환한다. 도메인 이벤트로 이러한 추적을 구현할 수 있다.