관리 메뉴

HAMA 블로그

DDD(domain driven design) 관점에서 객체지향 vs 함수형 코드 비교 본문

Scala

DDD(domain driven design) 관점에서 객체지향 vs 함수형 코드 비교

[하마] 이승현 (wowlsh93@gmail.com) 2017. 11. 27. 11:32


실제 도메인 관점에서의 예를 통해 객체지향과 함수형 코드를 비교해보면, 어떤 분들에게는 좀 더 도움이 될 수 있을 거 같아서. 함수형& 반응형 도메인 모델링이라는 책에 있는 내용중 일부를 짧게 소개해 보려고 합니다.
-장,단점을 논하는 글이 아니며, 더 자세한 내용이 필요하다면 책을 참고하세요. 
-DDD 하면 전 아직도 장거리 직통전화 와 김혜림씨가 먼저 생각나네요 :-) 


개발자는 코드로 말하기 때문에 바로 코드 보겠습니다. 스칼라 코드이긴 하나 매우 간단하기 때문에 자바,C++,Python 개발자라면 무난히 이해 하실 겁니다만 약간의 설명은 추가 하겠습니다.

1. 객체지향 


은행등에서 계좌의 돈을 입금하고 출금하는 상황(도메인) 에 대한 코드입니다.
Balance 케이스 클래스는 기본적으로 불변입니다. 즉 내부의 account 는 불변임.
Account(계좌) 라는 클래스 내부에는 balance 라는 멤버변수(var로 선언되어있어서 이것은 변할 수 있다는 의미임. 개인적으로 스칼라는 불변에 대한 것을 덜 강조/강요하기 때문에, 이런 경우 변수명 뒤에 balance_var로 코딩컨벤션을 하는게 좋아보임) 와 출금(debit), 입금(credit) 메소드가 있습니다.

여기서 중요하게 볼 장면은 Account 에 돈을 입금하거나, 출금하는 행위(behavior)가 있을 때마다, 
Account 라는 객체의 내부 상태(state) 는 바뀐다는 점인데요.  (이것이 단점이다라고 말하는 것은 아니며, 일반적인 객체지향 모델링의 특징이라는 겁니다) 풀어서 말하면 내부의 balance 라는 멤버변수의 상태가 변하기 때문에, 객체 자체의 내부상태가 바뀐다고 말합니다.

2. 함수형 -1 

이제 함수형 예제를 보기전에 잠깐 이야기 하나 하자면요.
스타크래프트를 예로 들어보면, 
상대편의 드라군이 던진 공에 나의 마린이 맞으면, 마린의 상태는 변경되게 됩니다.
Marine이라는 객체가 있다고 가정하면 내부에 life 라는 멤버변수가 있으며, 타격을 받으면 life 는 줄어 들겁니다. 즉 내부의 상태가 바뀌게 되는데요.

하지만 함수형 모델링에서는 건강한 마린 -> 피가 20%정도 깍인 마린으로 "대체" 됩니다.
다시 반복해서 예를 들면  슈퍼맨이 조드장군한테 쳐 맞아서 상태가 안좋아지게되면, 그 슈퍼맨이 상태가 안좋아진게 아니라, 상태가 안좋아진 슈퍼맨을 새로 만들어서 대체해 버립니다. 이런 '짓' 에 거부감을 가져서 함수형을 싫어하는 사람들도 많다고 하네요.  대충 감이 잡히시죠?  (뭐가 항상 더 좋고 나쁘다라고 짤라 이야기 할 수 없습니다. 다만 동시성이 강조되는 현재에는 이런 방식으로 문제를 해결하려는 솔루션이 많아지고 있습니다.)

이제 내부를 변경하는 대신 새로운 녀석(계좌)을 만드는 예제를 보겠습니다. 

여전히 Account 클래스 내부에 debit 와 credit 메소드가 있습니다.
변한것은 변할 수 있는 멤버변수였던 balance 가 없어졌으며 대신 val 즉 불변의 멤버변수로 만들어 졌습니다. 
이에 따라서 debit 과 credit 가 반환하는 리턴 값에 큰 변화가 생겼습니다.

즉 입,출금 후에는 새로운 계좌(Account) 객체를 만들어서 반환하고 있네요. 네 상태가 바뀐 새로운 마린(계좌) 을 만들어서 반환하는 겁니다. 이전 마린(계좌)는 버리구요.

3. 함수형 -2

다음으로 볼 것은 위의 예제를 함수형 도메인 모델링 방식으로 리팩토링 할 것인데요.
함수형 도메인 모델링은 보통 "행위" 와 "상태" 를 분리 한다고 합니다. 

//------------- 상태와 행위를 디커플링 한다 --------------------// import java.util.{ Date, Calendar }
import scala.util. { Try, Success, Failure }

def today = Calendar.getInstance.getTime
type Amount = BigDecimal
case class Balance ( amount: Amount = 0)
//도메인 모델 : 상태를 책임 (멤버변수는 모두 immutable 하다)
case class Account(no : String, name: String, dateOfOpening: Date, balance: Balance = Balance())

//도메인 서비스: 행위를 책임 (내부 행위에서 부수작용을 일으키지 않는다) trait AccountService {
def debit(a: Account, amount: Amount): Try[Account] = {
if (a.balance.amount < amount)
Failure(new Exception("Insufficient balance in account"))
else
Success(a.copy(balance = Balance(a.balance.amount + amount)))
}

def credit(a: Account, amount: Amount): Try[Account] =
Success(a.copy(balance = Balance(a.balance.amount + amount)))
}
object AccountService extends AccountService
import AccountService._
val a = Account("a1", "John", today)
val b = credit(a, 10)

도메인 모델(상태)과 도메인 서비스(행위)로 나뉘어 졌습니다.
상태를 책임지는 immutable 한 Account 라는 클래스와  행위를 책임지는 AccountService. 

val b = credit(a, 1000) 을 보시면 , a 라는 계좌 객체에 10억을 입금했더니, a 라는 계좌객체가 부자가 된 것이 아니라, 부자가 된 b 객체를 리턴해주고 있습니다.

* 어쩌면 상태라는 것은 Discrete 된 것이라고 생각하고, 그것들을 관리하는데 있어서는 이게 더 옳을 수도 있고, 상태가 continuous 나 seamless 라고 생각하면 저렇게 하는 것은 말도 안되겠지요 ^^
* trait 는 자바8의 인터페이스라고 생각하시면 됩니다. 즉 has-a 이며 내부 구현이 가능하고, 그 자체로 실질적 객체생성은 못함.
* Try 는 스칼라에서 예외처리를 대신하는데 사용되는 놈으로, Try 내에는 성공객체 or 실패객체 둘 중 하나가 들어갑니다.  따라서 그것을 리턴 받는 쪽은 Try 가 성공인지, 실패인지를 확인해서 처리를 하게됩니다. 확인하는 과정 또한 행사코드 범벅이 되는 문제도 있는데 해결 방안도 공존합니다.

4. 함수형 -3  

마지막으로 설명할 것은 함수형 도메인을 가지고 요리(행위추가)하는 방법에 대한 것인데요. 
자바, 파이썬, 자바스크립트 등에서 사용되는 함수형 스타일에 대한 글을 한번도 읽어보지 않았다면 이해하기 힘드실 거에요 (적어도 map, foldLeft, reduce 가 뭔지 아시는분 대상)

val generateAuditLog: (Account, Amount) => Try[Strng] = //..
val write: String => Unit

debit (source, amount)
.flatMap( b => generateAuditLog(b, amount))
.foreach(write)

debit 은 출금된 이 후의 상태를 가진 새로운 Account 를 돌려준다고 말했잖아요? 슈퍼맨의 예로 기억을 다시 살려보세요. debit (source, amount) 를 호출해서 새로운 Account 를 리턴 받은후에 flatMap 을 적용하게 됩니다.  

아시다시피 Map 이나 flatMap 은 주로 List [A] 와 같이 어떤 타입을 감싸고 있는 타입에 대해 행동을 하는 놈인데요. 위에서는 Try[Account] , 즉 Account이라는 타입을 감싸고 있는 Try에 대해서 행위를 합니다. 

flatMap은 만약 debit 가 Try[Failure] 를 리턴한다면 시퀀스는 진행하지 않고 멈추며,  그렇지 않고 정상적으로  Try[Account] 리턴한다면 그것의 타입을 한단계 벗겨서 , 내부의 Account 를 b => generatedAuditLog 함수리터럴에 적용시킵니다. 물론 generatedAuditLog 는 순수함수입니다.

마지막으로 그 결과 (Try[String]) 을 대상으로 .foreach 로 돌리는데, 보통 foreach 는 부수효과를 가져옵니다.
val write: String => Unit 를 사용하는것만 봐서도 감이 오는데요. 스칼라(함수형 파라다임)에서는 Unit 을 리턴하는 함수라면 즉 리턴하는게 없는 함수는 대게 부수효과가 있는 함수입니다. 여기서는 write 를 통해서 데이터베이스나 파일에 Sting 을 로그 출력합니다. (부수효과 발생!!) 

여기까지 대략적으로 간단히 살펴보았구요. 나중에 기회가 되면 다른 예제들 및 반응형에 대한 도메인 모델링에 대한 글도 작성하겠습니다.감사합니다.


Comments