일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- apache storm
- Clean Code
- 제주
- Spring Boot
- intellij
- 스프링 배치
- Java
- hadoop
- Spring XD
- 도메인주도설계
- Angular2
- 인텔리J
- Spring
- elasticsearch
- hdfs
- SBT
- Storm
- Spring Batch
- Linux
- elastic search
- nginx
- Gradle
- 엘라스틱서치
- spark
- Hbase
- DDD
- hibernate
- docker
- design pattern
- scala
- Today
- Total
욱'S 노트
도메인 모델링/Domain Modeling (Kotlin 함수형 프로그래밍 #7) 본문
도메인 모델링/Domain Modeling (Kotlin 함수형 프로그래밍 #7)
devsun 2025. 1. 20. 09:34함수형 도메인 모델링의 목적은 비즈니스 도메인을 정확하게 묘사하는 것이다. 컴파일러를 최대한 활용하여 타입-세이프하고, 버그를 방지하고 유닛테스트를 경감 시키는 것이 목적이다. 앞에서 살펴봤듯이 함수형 프로그래밍은 타입 주도 개발(Type Driven Development)을 따르기도 한다. 타입은 함수와 데이터가 준수해야 하는 엄격한 계약이다. 타입 시스템을 이를 어길 수 없다. 그러므로 다양한 케이스에 대한 유닛테스트도 경감시킬 수 있는 것이다.
코틀린은 함수형 도메인 모델링에 적합하다. 코틀린은 data class, sealed class, enum class, value class를 제공한다. 그리고 애로우를 이용한다면, Either와 같은 흥미로운 데이터 타입을 제공 받을 수 있다.
예제
기본 타입 기반의 Event에 대한 구현을 살펴보자. title, organizer, description은 같은 타입을 공유하고 있다.
data class Event(
val id: Long,
val title: String,
val organizer: String,
val description: String,
val date: LocalDate
)
tiltle, organizer랑 description을 혼동했지만 컴파일러는 Event 객체를 생성하고 만다. 더 많은 케이스에서 문제가 있는데, 예를들어 분해를 할 때이다.
Event(
0L,
"Simon Vergauwen",
"In this blogpost we dive into functional DDD...",
"Functional Domain Modeling",
LocalDate.now()
)
Value 클래스를 이용해서 타입을 명시하자.
@JvmInline value class EventId(val value: Long)
@JvmInline value class Organizer(val value: String)
@JvmInline value class Title(val value: String)
@JvmInline value class Description(val value: String)
data class Event(
val id: EventId,
val title: Title,
val organizer: Organizer,
val description: Description,
val date: LocalDate
)
앞의 예제로 돌아가서 아래와 같이 생성을 하려고 한다면 컴파일러는 오류를 발생시킬 것이다.
Event(
EventId(0L),
Organizer("Simon Vergauwen"),
Description("In this blogpost we dive into functional DDD..."),
Title("Functional Domain Modeling"),
LocalDate.now()
)
함수형 프로그래밍에서 위와 같은 타입의 합성을 곱타입 혹은 레코드라고 부른다. 이벤트는 EventId, Title, Organizer, Description, LocalDate로 이루어졌다고 말한다. 아니면 Event는 Long, String, String, String, LocalDate로 이루어졌다고 말해야 할 것이다.
이벤트는 연령 제한이 있다고 가정해보자, MPAA 영화 등급을 따라서 5가지 케이스가 있다면 enum class가 적합할 것이다.
enum class AgeRestriction(val description: String) {
General("All ages admitted. Nothing that would offend parents for viewing by children."),
PG("Some material may not be suitable for children. Parents urged to give \"parental guidance\""),
PG13("Some material may be inappropriate for children under 13. Parents are urged to be cautious."),
Restricted("Under 17 requires accompanying parent or adult guardian. Contains some adult material."),
NC17("No One 17 and Under Admitted. Clearly adult.")
}
함수형 프로그래밍에서 위와 같은 데이터 구성을 합타입이라고 한다. 우리는 연령 제한은 General, PG, PG13, Restricted, NC17이 있다고 설명할 수 있다. 이는 String이라고 말하는 것보다 훨씬 낫다.
다음은 온라인 이벤트와 오프라인 이벤트가 있다고 가정해보자. 온라인 이벤트에는 URL이 오프라인 이벤트에는 주소가 필요할 것이다.
@JvmInline value class Url(val value: String)
@JvmInline value class City(val value: String)
@JvmInline value class Street(val value: String)
data class Address(val city: City, val street: Street)
data class Event(
val id: EventId,
val title: Title,
val organizer: Organizer,
val description: Description,
val date: LocalDate,
val ageRestriction: AgeRestriction,
val isOnline: Boolean,
val url: Url?,
val address: Address?
)
아래는 일반적인 인코딩인데, 문제가 있다.
fun printLocation(event: Event): Unit =
if(event.isOnline) {
event.url?.value?.let(::println)
} else {
event.address?.let(::println)
}
생성을 할때에도 아래와 같이 쉽게 계약이 깨질 수 있다.
Event(
Id(0L),
Title("Functional Domain Modeling"),
Organizer("47 Degrees"),
Description("Building software with functional DDD..."),
LocalDate.now(),
AgeRestriction.General,
true,
null,
null
)
다음은 컴파일러가 행복해지는 정의이다.
sealed class Event {
abstract val id: EventId
abstract val title: Title
abstract val organizer: Organizer
abstract val description: Description
abstract val ageRestriction: AgeRestriction
abstract val date: LocalDate
data class Online(
override val id: EventId,
override val title: Title,
override val organizer: Organizer,
override val description: Description,
override val ageRestriction: AgeRestriction,
override val date: LocalDate,
val url: Url
) : Event()
data class AtAddress(
override val id: EventId,
override val title: Title,
override val organizer: Organizer,
override val description: Description,
override val ageRestriction: AgeRestriction,
override val date: LocalDate,
val address: Address
) : Event()
}
위의 타입은 합타입이다. sealed class는 enum class보다 더 강력한 기능을 제공한다.
object, data class 또는 다른 sealed class의 확장을 제공한다. 인코딩은 훨씬 보기 좋아졌다.
fun printLocation(event: Event): Unit =
when(event) {
is Online -> println(event.url.value)
is AtAddress -> println("${event.address.city}: ${event.address.street}")
}
다음은 애로우 데이터 타입이 우리의 코드를 얼마나 더 명확하게 하는지 살펴보자.
interface EventService {
suspend fun fetchUpcomingEvent(id: EventId): Event
}
EventService에서 누락한 것은 어떠한 종류의 에러 시나리오가 발생하는지이다. 이벤트가 없을 경우, 이벤트가 더이상 유효하지 않을 경우를 모델링하면 다음과 같다.
sealed class Error {
data class EventNotFound(val id: EventId): Error()
data class EventPassed(val event: Event): Error()
}
Either는 Error 혹은 Event를 반환 할 수 있는 제너릭한 합타입니다.
interface EventService {
suspend fun fetchUpcomingEvent(id: EventId): Either<Error, Event>
}
정리
- 기본 타입을 제거하고, value class를 사용하여 runtime에 오버헤드를 줄였다.
- enum class나 sealed class를 적용하여 모델 분리에 활용했다.
- 애로우 Either를 사용하여, 두가지 다른 도메인의 관계에 해서 합성하여 표현했다.
- 타입은 함수와 데이터가 준수해야 하는 엄격한 계약이다.
- 타입은 타입-세이프하고, 버그를 방지하고 유닛테스트를 경감 시키는 것이 목적이다.
참고자료: https://arrow-kt.io/
'Methdology > Functional Programming' 카테고리의 다른 글
외부효과와 입출력/Effects and IO (Kotlin 함수형프로그래밍 #6) (0) | 2025.01.17 |
---|---|
불변 컬렉션/Immutable Collections (Kotlin 함수형 프로그래밍 #5) (4) | 2025.01.16 |
Option을 사용해야하는 이유/Nested Nullability (Kotlin 함수형 프로그래밍 #4) (1) | 2025.01.15 |
타입 에러/Typed Errors (Kotlin 함수형 프로그래밍 #3) (0) | 2025.01.14 |
함수형 데이터구조/대수적 타입/Algebraic Data Type (Kotlin 함수형프로그래밍 #3) (0) | 2025.01.13 |