Domain Model

|

DDD START! 도메인 주도 설계 구현과 핵심 개념 익히기

이 포스팅은 아래 책을 보고 공부한 내용을 요약한 글입니다.

image


도메인

도메인(Domain)은 소프트웨어로 해결하고자 하는 문제 영역입니다. 하나의 도메인은 여러 개의 하위 도메인으로 나눌 수 있습니다.

예를 들면, 온라인 서점 도메인은 아래 그림과 같이 여러 하위 도메인으로 나눠집니다.

image

특정 도메인용 소프트웨어라고 해서 모든 기능을 구현하는 것은 아닙니다. 아래 그림과 같이 외부 시스템과 연동해서 사용하기도 합니다.

image


도메인 모델

도메인 모델은 특정 도메인을 개념적으로 표현한 것입니다. 아래는 주문 모델을 객체 모델로 표현한 그림입니다.

image

도메인 모델이 도메인의 모든 내용을 담고 있지는 않지만, 위 도메인 모델을 보면 주문(Order)은 주문 번호와 총 금액을 갖고 있고, 배송 정보를 변경할 수 있음을 알 수 있습니다. 또한 취소(Cancel) 할 수 있는 것도 알 수 있습니다.

이렇게 도메인 모델을 이용해서 여러 관계자들이 도메인을 이해하고 지식을 공유하는데 도움을 줄 수 있습니다.

도메인 모델은 객체 외에도 아래 그림처럼 상태 다이어그램을 이용해서 표현할 수도 있습니다.

image

도메인 모델은 꼭 UML 표기법만 사용할 필요는 없습니다. 관계가 중요하다면 그래프로, 계산 규칙이 중요하다면 수학 공식으로 도메인 모델을 만들 수도 있습니다. 표현 방식이 중요하지는 않습니다.

도메인 모델은 개념 모델입니다. 개념 모델을 이용해서 바로 코드를 작성할 수 있는 것은 아니기 때문에 구현 모델은 따로 필요합니다. 개념 모델과 구현 모델은 서로 다르지만 최대한 서로 따르게 할 수는 있습니다.

도메인에 따라 용어가 바뀔 수 있기 때문에 여러 하위 도메인을 하나의 다이어그램에 모델링하는 것은 좋지 않습니다. 예를 들어, ‘상품’이라는 용어는 카탈로그에서의 상품과 배송에서의 상품이 서로 다릅니다. 즉, 이 경우 카탈로그 도메인 모델과 배송 도메인 모델을 따로 만들어야 한다는 뜻입니다.


개념 모델과 구현 모델

개념 모델은 순수하게 문제를 분석한 결과물입니다. 개념 모델은 데이터베이스, 트랜잭션 처리, 성능 등을 고려하지 않기 떄문에 실제 코드에 개념 모델을 그대로 사용할 수는 없습니다.

개념 모델을 처음부터 완벽한 모델로 만드는 것은 아주 어렵습니다. 소프트웨어를 개발하면서 개발자와 관계자들이 해당 도메인을 더 잘 이해할 수 있습니다. 프로젝트 초기에 완벽하 도메인 모델을 만들더라도 결국 모델을 수정하거나 보완하는 경우가 발생하게 됩니다.

따라서, 처음부터 완벽한 개념 모델을 만들기보다 전반적 개념을 알 수 있는 수준으로 개념 모델을 작성하는 것이 좋습니다.


도메인 모델 도출 방법

도메인을 모델링할 때 기본은 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾는 것입니다. 이 과정은 요구사항에서 출발합니다.

주문 도메인과 관련된 요구사항은 다음과 같습니다.

  • 최소 한 종류 이상의 상품을 주문해야 한다.
  • 한 상품을 한 개 이상 주문할 수 있다.
  • 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.
  • 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.
  • 주문할 떄 배송지 정보를 반드시 지정해야 한다.
  • 배송지 정보는 받는 사람 이름, 전화번호, 주소로 구성된다.
  • 출고를 하면 배송지 정보를 변경할 수 없다.
  • 출고 전에 주문을 취소할 수 있다.
  • 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다.

이 요구사항에서 알 수 있는 것은 주문(Order)은 아래 기능을 제공한다는 것입니다.

  • 출고 상태로 변경
  • 배송지 정보 변경
  • 주문 취소
  • 결제 완료로 변경

코드로 표현하면 다음과 같습니다.

public class Order {
    public void changeShipped() {...}
    public void changeShippingInfo(ShippingInfo newShipping) {...}
    public void cancel() {...}
    public void completePayment() {...}
}

그 외 위 요구사항 중 아래 요구 사항을 반영하면

  • 한 상품을 한 개 이상 주문할 수 있다.
  • 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.

OrderLine 객체를 구성할 수 있고, Order와의 관계를 표현할 수 있습니다.

또한, 아래 요구 사항을 보면

  • 출고를 하면 배송지 정보를 변경할 수 없다.
  • 출고 전에 주문을 취소할 수 있다.
  • 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다.

주문의 상태(OrderState)가 필요함을 알 수가 있습니다.

public enum OrderState {
    PAYMENT_WATING,
    PREPARING,
    SHIPPED,
    DELIVERING,
    DELIVERY_COMPLETED,
    CANCELED,
}

이와 같은 방식으로 요구사항으로부터 도메인 모델을 점진적으로 만들어 나갑니다.


Entity와 Value

도출한 모델은 크게 EntityValue로 구분할 수 있습니다.

image

Entity

Entity의 가장 큰 특징은 식별자를 갖는다는 것입니다. 식별자는 객체마다의 고유값입니다. 예를 들면 주문 도메인에서 Order는 주문 번호를 가지며, 주문 번호는 Order의 식별자가 됩니다.

배송지가 변경되어도 주문 번호가 바뀌지 않는 것처럼 식별자는 Entity의 생성, 변경, 삭제까지 계속 유지됩니다.


Value

아래 코드에서 receiverNamereceiverPhoneNumber는 서로 다른 데이터를 갖지만, 두 필드는 개념적으로 받는 사람을 의미합니다. 즉, 두 필드가 하나의 개념을 표현하고 있습니다.

public class ShippingInfo {
    private String receiverName;
    private String receiverPhoneNumber;
    // ...
}

Value는 개념적으로 완전한 하나를 표현할 때 사용합니다. 위의 코드에서 받는 사람을 위한 Value 타입인 Receiver를 다음과 같이 작성할 수 있습니다.

public class Receiver {
    private String name;
    private String phoneNumber;
    // ...
}

Value 타입이 꼭 두 개 이상의 데이터를 가질 필요는 없습니다. 의미를 명확하게 표현하기 위해 사용하는 경우도 있습니다. 예를 들면 다음과 같습니다.

public class Money {
    private int value;
    // ...
}

SW 품질속성 예제

|

품질속성 6가지 항목

image

각각의 항목에 대한 구체적인 예시들은 다음과 같습니다.


가용성 시나리오 예시

항목
Source 생명신호 모니터링
Stimulus 서버가 반응하지 않음
Artifact 프로세스
Environment 정상적인 운영
Response 운영자에게 보고하고 운영을 계속한다.
Measure 중단 시간 없음


상호운영성 예시

항목
Source 운송 정보 시스템
Stimulus 현재 자극 위치 전송
Artifact 교통 통제 시스템
Environment 런타임 이전에 알려진 시스템
Response 트래픽 모니터 시스템은 현재 위치와 다른 정보를 결합한다.
구글 맵스 위에 올려놓고 브로드캐스트한다.
Measure 99.9% 정확하게 시간이 포함된 우리 정보


변경 용이성 시나리오

항목
Source 개발자
Stimulus UI를 변경하기 원한다.
Artifact 코드
Environment 설계 시
Response 변경을 하고 단위 테스트를 수행한다.
Measure 3시간 이내


성능 시나리오

항목
Source 사용자
Stimulus 트랜잭션 시작
Artifact 시스템
Environment 정상 운영
Response 트랜잭션이 처리됨
Measure 평균 2초의 지연 시간


보안 시나리오

항목
Source 원격 위치에 있는 불만을 가진 직원
Stimulus 임금률 변경 시도
Artifact 시스템 안에 있는 데이터
Environment 정상 운영
Response 시스템은 감사 트레일을 유지한다.
Measure 하루 안에 정확한 데이터로 복구되고, 공격의 근원을 식별한다.


테스트 용이성 시나리오

항목
Source 단위 테스터
Stimulus 코드 단위 완료
Artifact 코드 단위
Environment 개발
Response 수집된 결과
Measure 3시간 안에 85% 경로 커버리지


사용 편의성 시나리오

항목
Source 사용자
Stimulus 새로운 App을 다운로드한다.
Artifact 시스템
Environment 런타임
Response 사용자가 생산적으로 App을 사용한다.
Measure 측정 2분 동안 시험한다.

간단한 Wallpaper 만들어보기

|

Wallpaper

안드로이드에서 간단한 Wallpaper를 만들어보는 예제입니다.


AndroidManifest.xml

먼저 다음 Permission을 추가해줍니다.

    <uses-permission android:name="android.permission.SET_WALLPAPER" />
    <uses-feature
            android:name="android.software.live_wallpaper"
            android:required="true"/>

그리고 Wallpaper는 android.service를 사용하기 때문에 service도 등록합니다.

    <service
        android:name=".service.snowdeer.SimpleWallpaperService"
        android:enabled="true"
        android:label="@string/app_name"
        android:permission="android.permission.BIND_WALLPAPER">
        <intent-filter>
            <action android:name="android.service.wallpaper.WallpaperService" />
        </intent-filter>

        <meta-data
            android:name="android.service.wallpaper"
            android:resource="@xml/wallpaper" />
    </service>


BaseWallpaperService.kt

import android.content.Context
import android.service.wallpaper.WallpaperService
import android.view.MotionEvent
import android.view.SurfaceHolder
import android.view.WindowManager

interface InteractiveEngine {
    fun setScreenSize(width: Int, height: Int)
    fun onSurfaceCreated(holder: SurfaceHolder)
    fun onDestory()
}

open abstract class BaseWallpaperService : WallpaperService() {

    abstract fun handleScreenSize(width: Int, height: Int)
    abstract fun handleSurfaceCreated(holder: SurfaceHolder)
    open fun handleTouchEvent(event: MotionEvent) {}

    override fun onCreate() {
        super.onCreate()

        val window = getSystemService(Context.WINDOW_SERVICE) as WindowManager
        val display = window.defaultDisplay
        val width = display.width
        val height = display.height

        handleScreenSize(width, height)
    }

    override fun onCreateEngine(): Engine {
        return WallpaperEngine()
    }

    inner class WallpaperEngine : WallpaperService.Engine() {
        override fun onSurfaceCreated(holder: SurfaceHolder) {
            handleSurfaceCreated(holder)
        }

        override fun onTouchEvent(event: MotionEvent?) {
            event?.let {
                handleTouchEvent(event)
            }
        }
    }
}


SimpleWallpaper.kt

import android.view.MotionEvent
import android.view.SurfaceHolder
import com.snowdeer.wallpaper.service.BaseWallpaperService

class SimpleWallpaper : BaseWallpaperService() {
    val engine: InteractiveEngine = RandomColorEngine()
    
    companion object {
        const val TAG = "NormalWallpaperService"
    }

    override fun handleScreenSize(width: Int, height: Int) {
        engine.setScreenSize(width, height)
    }

    override fun handleSurfaceCreated(holder: SurfaceHolder) {
        engine.onSurfaceCreated(holder)
    }

    override fun handleTouchEvent(event: MotionEvent) {
        engine.handleTouchEvent(event)
    }

    override fun onDestroy() {
        super.onDestroy()
        engine.onDestory()
    }
}


RandomColorEngine.kt

import android.graphics.Color
import android.graphics.Paint
import android.os.Handler
import android.os.Looper
import android.view.SurfaceHolder
import com.snowdeer.service.InteractiveEngine
import kotlin.random.Random

class RandomColorEngine : InteractiveEngine {
    private val handler = Handler(Looper.getMainLooper())
    private var isRunning = false

    override fun setScreenSize(width: Int, height: Int) {

    }

    override fun onSurfaceCreated(holder: SurfaceHolder) {
        isRunning = true

        drawCanvas(holder)
    }

    override fun onDestory() {
        isRunning = false
    }

    private fun drawCanvas(holder: SurfaceHolder) {
        if (!isRunning) {
            return
        }

        try {
            val canvas = holder.lockCanvas()

            canvas?.let {
                val paint = Paint().apply {
                    val randomColor = Random.nextInt(16_777_216)
                        .toString(16)
                        .padStart(6, '0')
                    color = Color.parseColor("#$randomColor")
                    style = Paint.Style.FILL
                }
                canvas.drawPaint(paint)
                holder.unlockCanvasAndPost(canvas)

                handler.postDelayed({ drawCanvas(holder) }, 1000)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }

    }
}


WallpaperApplier.kt

import android.app.WallpaperManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent

data class WallpaperListItem(val name: String, val resId: Int, val cls: Class<*>)

class WallpaperApplier(val ctx: Context) {
    fun apply(wallpaper: WallpaperListItem) {
        setWallpaperService(wallpaper.cls)
    }

    private fun setWallpaperService(wallpaperService: Class<*>) {
        val intent = Intent(WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER)
        intent.putExtra(
            WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT,
            ComponentName(ctx, wallpaperService)
        )
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        ctx.startActivity(intent)
    }
}

Git pre-commit 사용법

|

pre-commit

이름에서 볼 수 있듯이 Git commit을 수행하기 전에 자동으로 특정 작업을 수행하도록 해주는 기능입니다. 보통 formatter 또는 linter 등을 실행해서 코드의 잠재적 문제 발견 또는 일관성있는 포맷을 유지하게 해줍니다.


설치 방법

pre-commit는 파이썬 패키지를 이용해서 설치할 수 있습니다.

$ pip3 install pre-commit

Collecting pre-commit
  Downloading pre_commit-2.12.1-py2.py3-none-any.whl (189 kB)
     |████████████████████████████████| 189 kB 813 kB/s
Collecting virtualenv>=20.0.8
  Downloading virtualenv-20.4.4-py2.py3-none-any.whl (7.2 MB)
     |████████████████████████████████| 7.2 MB 869 kB/s
Requirement already satisfied: toml in /usr/local/lib/python3.8/site-packages (from pre-commit) (0.10.2)
Collecting cfgv>=2.0.0
  Downloading cfgv-3.2.0-py2.py3-none-any.whl (7.3 kB)
Collecting nodeenv>=0.11.1
  Downloading nodeenv-1.6.0-py2.py3-none-any.whl (21 kB)
Requirement already satisfied: pyyaml>=5.1 in /usr/local/lib/python3.8/site-packages (from pre-commit) (5.4.1)
Collecting identify>=1.0.0
  Downloading identify-2.2.4-py2.py3-none-any.whl (98 kB)
     |████████████████████████████████| 98 kB 17.3 MB/s
Collecting filelock<4,>=3.0.0
  Downloading filelock-3.0.12-py3-none-any.whl (7.6 kB)
Collecting appdirs<2,>=1.4.3
  Downloading appdirs-1.4.4-py2.py3-none-any.whl (9.6 kB)
Requirement already satisfied: distlib<1,>=0.3.1 in /usr/local/lib/python3.8/site-packages (from virtualenv>=20.0.8->pre-commit) (0.3.1)
Requirement already satisfied: six<2,>=1.9.0 in /usr/local/lib/python3.8/site-packages (from virtualenv>=20.0.8->pre-commit) (1.15.0)
Installing collected packages: filelock, appdirs, virtualenv, nodeenv, identify, cfgv, pre-commit
Successfully installed appdirs-1.4.4 cfgv-3.2.0 filelock-3.0.12 identify-2.2.4 nodeenv-1.6.0 pre-commit-2.12.1 virtualenv-20.4.4

MacOS에서는 brew를 이용해서도 설치 가능하지만, 그냥 pip3를 이용하는게 더 편한 듯 싶습니다.


버전 확인

$ pre-commit -V

pre-commit 2.12.1


설정

pre-commit.pre-commit-config.yaml 설정 파일을 필요로 합니다. 아래 명령어를 이용해서 sample-config라는 템플릿으로 설정 파일을 만들 수 있습니다. git add 명령어로 프로젝트에 추가할 예정이니, 아래 명령어는 pre-commit를 적용하려는 프로젝트의 디렉토리에서 실행하세요.

$ pre-commit sample-config > .pre-commit-config.yaml


.pre-commit-config.yaml

해당 설정 파일은 다음과 같은 내용이 작성되어 있습니다.

# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v3.2.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
    -   id: check-yaml
    -   id: check-added-large-files

총 4개의 hook이 설정되어 있는 것을 볼 수 있습니다.


실행

이제 pre-commit run 명령을 실행해봅니다. 처음에는 해당 repo로부터 다운로드하는 시간이 있어서 약간의 시간이 걸립니다.

$ pre-commit run

[INFO] Initializing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Installing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
Trim Trailing Whitespace.............................(no files to check)Skipped
Fix End of Files.....................................(no files to check)Skipped
Check Yaml...........................................(no files to check)Skipped
Check for added large files..........................(no files to check)Skipped

그 이후 pre-commit run -a 명령어를 이용해서 모든 파일들을 한 번 검사해봅니다.

$ pre-commit run -a

...

다음 명령어를 이용해서 .pre-commit-config.yaml 파일을 git 에 추가해줍니다.

$ git add .pre-commit-config.yaml

$ git commit -m "pre-commit 적용"


git hook에 등록

마지막으로 git commit 할 때 자동으로 pre-commit가 실행되도록 합니다. 아래 명령어를 이용해서 git hook에 등록할 수 있습니다.

$ pre-commit install

Visual Studio Code Plugin `Project Dashboard`

|

Project Dashboard

vscode에서 다양한 프로젝트들을 다음 그림처럼 대시보드 형태로 관리할 수있게 해주는 플러그인입니다.

image

프로젝트 전환도 쉽고, 새 창으로 다른 프로젝트를 열기도 쉬워서 상당히 유용한 플러그인입니다.


설치 방법

설치는 vscode 플러그인 설치 메뉴에서 Project Dashboard를 검색해서 설치하면 됩니다.


단축키

대시보드를 띄우는 단축키는 리눅스에서는

Ctrl + F1 키를 누르면 됩니다.

맥에서는

command + F1 입니다.