Module View

|

모듈 뷰

시스템의 기본적인 구현 단위 또는 모듈과 함께 이들의 관계를 열거, 서술하는 뷰.

보통 모듈 구조(Module structure)는 시스템의 어떤 부분의 변경이 다른 부분에 얼마나 큰 영향을 미치는지를 결정한다. 변경용이성(Modifiability), 이식성(Portability), 재사용성(Resuability)과 관련된 시스템의 기능을 결정한다.


모듈 뷰의 요소, 관계, 속성

요소

  • 모듈: 모듈은 밀집성을 갖는 책임의 집합을 제공하는 소프트웨어의 구현 단위

관계

  • 일부분(is part of)
  • 의존(depends on)
  • 일종(is a)

속성

모듈의 속성은 구현과 분석에 이용되기 떄문에 모듈 뷰 문서에 기록되어야 한다.

  • 이름(Name)
  • 책임(Responsibility): 모듈의 이름이 책임을 알려줄 수는 있지만, 책임 속성을 통해 훨씬 더 명확하게 보여줄 수 있음
  • 인터페이스 가시성(Visibility of interface): 부모 모듈이 서브 모듈을 가지는 경우, 서브 모듈의 인터페이스는 외부에 노출되지 않는다. 하지만, 선택적으로 서브 모듈의 일부 인터페이스를 외부에 노출시키도록 표현할 수도 있다. 레이어와 서브 시스템이 주로 이런 방식을 사용한다.
  • 구현 정보(Implementation information): 모듈은 구현단위다. 엄격히 말하면 아키텍처적인 정보는 아니지만, 아키텍처 문서에 기록하는 것이 유용하다. 구현 정보에는 소스 코드 단위 매핑, 테스트 정보, 관리 정보, 구현 제약사항 등이 포함될 수 있다.


모듈 뷰 사용

모듈 뷰는 구현, 분석, 의사 소통 등의 용도로 많이 활용된다.

하지만, 모듈 뷰는 소프트웨어의 정적인 부분만을 보여준다. 따라서 모듈 뷰만 이용해서 시스템의 런타임(Runtime) 행위를 알아내는 것은 어렵다. 따라서 모듈 뷰는 일반적으로 성능, 신뢰성 등 런타임 품질 분석에는 사용되지 않는다.


모듈 뷰 표기법

모듈 뷰는 UML 또는 비형식적 표기법으로 둘 다 표현이 가능하다. UML을 이용할 경우 다음과 같이 표현한다.

image


모듈 뷰에서 관계는 다음과 같이 표현한다.

image


다른 뷰와의 관계

모듈 뷰는 일반적으로 C&C 뷰(컴포넌트 커넥터 뷰)에 매핑된다. 모듈은 주로 런타임시 실행되는 컴포넌트와 매핑이 되며, 일대일 또는 일대다 등의 관계로 매핑이 된다.

다만 모듈 뷰는 소프트웨어의 정적인 분할을 표현하는 것이기 때문에, 객체의 여러 인스턴스(ex. 데이터 리파지토리 또는 네트워크 등)는 모듈 뷰에 나타나지 말아야 한다.

System Context Diagram

|

Context Diagram

System Context Diagram의 목적은 시스템의 범위를 표현하는 것이다. 레벨이 존재하긴 하지만 대부분의 Context Diagram은 최상위 수준(TLCD, Top level context diagram)으로 표현한다.

Context는 시스템이 상호작용하는 환경을 의미하며 다음과 같은 것들이 될 수 있다.

  • 사람
  • 다른 시스템
  • 센서, 제어 장치 등의 H/W

Context Diagram은 개발해야 하는 범위를 알 수 있고, 시스템의 범위를 벗어나있는 프레임워크, 라이브러리, 외부 서비스, 다른 시스템, 다른 접점의 S/W 등을 명확하게 보여준다.


Context Diagram 내용

순수한 Context Diagram은 시스템에 대한 아키텍처 세부사항을 보여주지 않는다. (하지만 같은 Context내의 시스템의 일부 내부 구조를 보여주는 경우가 종종 있다.) 또한 인터랙션 정보나 데이터 흐름(데이터가 전송되고, 자극이 발생하고, 메시지가 전송되는 등)을 보여주지도 않는다.


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)
    }
}