개발

[번역] Room auto-migrations

2021. 6. 2. 10:08

Room 2.4.0-alpha01 버전부터 지원하는 오토 마이그레이션 기능을 이용해 Room DB 마이그레이션이 좀 더 간편해졌다. 지금까지는 DB 스키마가 변경될 때마다 Migration 클래스를 구현하고 변경 사항을 Room에 정확히 명시해주어야 했다. 대부분의 경우에 이는 복잡한 SQL 쿼리의 작성과 실행에 연관되어 있었다.

 

오토 마이그레이션 기능을 이용하면, 어떤 버전에서 어떤 버전으로 마이그레이션을 원하는지 명시해주기만 하면 Room이 자동으로 컬럼 추가/삭제, 테이블 추가와 같은 간단한 마이그레이션을 진행한다. 좀 더 애매하고 복잡한 경우는 사용자가 구체적인 사항을 명시해주어 마이그레이션을 진행한다. (ex. 컬럼/테이블 이름 변경)

간단한 변경사항 마이그레이션

테이블에 새로운 컬럼을 추가하는 등의 간단한 변경사항으로 인해 버전이 변경되는 경우, 아래와 같이 @Database 어노테이션의 버전을 증가시키고 auto-migrations에 마이그레이션 될 버전 정보를 입력해준다.

@Database(
    version = 2,
    entities = [ Doggos.class ],
    autoMigrations = [
        AutoMigration (from = 1, to = 2)
    ]
)
abstract class DoggosDatabase : RoomDatabase { }

 

DB 버전이 업데이트 될 때마다 AutoMigration을 추가해주면 간단하게 마이그레이션이 가능하다.

@Database(
    version = 3,
    entities = [ Doggos.class ],
    autoMigrations = [
       AutoMigration (from = 1, to = 2),
       AutoMigration (from = 2, to = 3)
    ]
)
abstract class DoggosDatabase : RoomDatabase { }

 

복잡한 변경사항 마이그레이션

테이블 또는 컬럼의 이름이 변경되는 경우 Room은 이름이 변경된건지 삭제된건지 판단하지 못한다. 이러한 경우가 발생했을 때 Room은 컴파일 에러를 발생시키며 사용자에게 AutoMigrationSpec의 구현을 요구한다. AutoMigrationSpec 클래스는 아래 어노테이션 중 하나 이상을 사용하여 구현할 수 있다.

  • @DeleteTable(tableName)
  • @RenameTable(fromTableName, toTableName)
  • @DeleteColumn(tableName, columnName)
  • @RenameColumn(tableName, fromColumnName, toColumnName)

예를 들어, Doggos 테이블을 GoodDoggos로 변경하는 경우 아래와 같이 명시할 수 있다.

@Database(
    version = 2,
    entities = [ GoodDoggos.class ],
    autoMigrations = [
        AutoMigration (
            from = 1, 
            to = 2,
            spec = DoggosDatabase.DoggosAutoMigration::class
        )
    ]
)
abstract class DoggosDatabase : RoomDatabase {   
    @RenameTable(fromTableName = "Doggos", toTableName = "GoodDoggos")
    class DoggosAutoMigration: AutoMigrationSpec {   }
}

 

Migrations vs. Auto-migrations

수동적으로 DB를 마이그레이션 하는 경우, DB 스키마의 변경이 있을 때마다 Room에서 제공하는 Migration 클래스를 이용했었다. 한 테이블을 2개의 다른 테이블로 나눈다고 했을 때, 아래와 같이 테이블을 어떻게 나눌지, 어떤 데이터가 옮겨질지를 정의한 뒤 addMigrations() 함수를 통해 databaseBuilder()에 추가해야 한다.

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        ...
    }
}

Room.databaseBuilder(applicationContext, DoggosDatabase::class.java, "doggos-database")
    .addMigrations(MIGRATION_1_2,)
    .build()

 

Migrations와 Auto-migrations의 조합

Room은 Migrations와 Auto-migrations의 혼용을 허용한다. 예를 들어, 버전 1에서 2는 Migration을 이용하고 버전 2에서 3은 Auto-migration을 이용할 수 있다. 만약 동일한 버전에 대해 Migration과 Auto-migration이 정의되어 있으면 Migration이 수행된다.

 

내부적으로 Auto-migration은 Migration 클래스를 구성하고 있으므로, 동일한 마이그레이션 로직이 적용된다. DB에 최초 접근 시, Room은 현재 DB 버전과 @Database에 명시된 버전이 동일한지 확인한다. 버전이 다른 경우 해당 버전으로의 마이그레이션 경로를 찾아 연속적으로 마이그레이션이 수행된다.

테스트

Migrations 또는 Auto-migrations를 테스트하기 위해서는 MigrationTestHelper 테스트 룰을 사용하여 helper.runMigrationsAndValidate() 함수를 호출한다. (자세히 보기)

결론

Auto-migration 기능은 DB 스키마 변경에 쉽게 대응할 수 있도록 도와준다. @Database 내부에 autoMigration 파라미터를 추가하여 대부분의 기본적인 변경사항을 Room 자체적으로 핸들링할 수 있다. 테이블, 컬럼의 삭제나 리네임의 경우 AutoMigrationSpec을 구현해야 하며 나머지 경우는 기존의 Migrations을 사용해야 한다.

 

 

원문보기: Room auto-migrations

안드로이드 시스템 아키텍처

Android Open Source Project(AOSP)의 시스템 아키텍처는 다음과 같은 요소들의 계층 구조로 이루어져 있다.

   

어플리케이션 프레임워크

API를 통해 제공되는 안드로이드의 전체 feature-set이다. 어플리케이션 개발자가 주로 사용하는 API를 말한다.

ActivityManager, WindowManager, ContentProvider 등의 시스템 앱 및 서비스들이 여기에 포함된다.

 

사용자 어플리케이션 수준에서는 단순 API 호출로 보이지만, HAL 인터페이스에 직접 매핑되는 API들이 다수 존재한다.

바인더 IPC

바인더 IPC(Inter Process Communication)는 어플리케이션 프레임워크와 시스템 서비스를 연결해주는 인터페이스로, IPC를 통해 어플리케이션 레벨에서 시스템 서비스가 제공하는 기능을 사용할 수 있게 해준다.

하드웨어 추상화 계층(HAL: Hardware Abstraction Layer)

HAL은 하드웨어들을 연동하기 위한 표준 인터페이스이다. 하드웨어 공급업체들에게 이 인터페이스의 구현을 강제함으로써 모델별로 달라질 수 있는 제조업체의 하위 수준 소프트웨어를 안드로이드 프레임워크로부터 분리할 수 있다.

 

HAL은 여러 개의 라이브러리 모듈로 구성되어 있다. 각 모듈은 하드웨어 타입(ex. camera, bluetooth)에 맞는 인터페이스를 구현하고 있으며, 프레임워크 API가 호출되면 안드로이드 시스템은 해당 모듈을 로드하여 기능을 사용할 수 있게 해준다.

 

리눅스 커널

안드로이드 런타임(ART)은 리눅스 커널 기반으로 동작한다. 안드로이드에서 사용하는 커널에는 모바일 환경을 위한 LMK(Low Memory Killer), Wake lock 등의 몇몇 추가 기능이 포함되어 있다.

 

리눅스 커널을 사용하는 이점으로는 우선 안정성이 보장되고, 잘 알려진 커널이기 때문에 하드웨어 공급업체들이 하드웨어 드라이버를 개발하기 용이하다는 점을 들 수 있다.

참고

  1. Android Architecture
  2. Platform Architecture

안드로이드의 화면은 아래와 같은 단위로 구성된다. 화면을 구성하는 최소 단위는 View이며 최대 단위는 Window이다.

Window > Surface > Canvas > View

Window

Window는 화면 구성의 가장 상위 요소로 무언가를 그릴 수 있는 화면상의 사각 영역을 말한다.

하나의 화면 안에는 여러 개의 Window가 존재할 수 있고, 각각의 Window는 용도에 따라 고유한 타입을 가진다. 이들은 WindowManager에 의해 관리된다.

어플리케이션은 WindowManager와 상호작용하여 Window를 만들 수 있다. WindowManager는 각각의 Window에 Surface를 만들어 어플리케이션에 전달하고, 어플리케이션은 이를 통해 화면을 렌더링한다.

Window는 터치 이벤트, 키 이벤트 등 사용자 이벤트를 받아서 처리할 수 있다.

Surface

Surface는 화면에 합성되는 픽셀을 보유한 객체이다.

화면에 보여지는 모든 윈도우(스테이터스 바, 다이얼로그, 액티비티 등)는 자신만의 Surface를 가지고 있으며, Surface Flinger가 각 Surface의 픽셀들을 Z-order에 따라 합성하여 실제 화면에 렌더링하는 역할을 맡고 있다.

Surface는 렌더링시에 더블 버퍼링 방식을 이용하기 위해 일반적으로 두개의 버퍼를 가지고 있다.

Canvas

Canvas는 모든 드로잉 메서드를 포함하고 있는 클래스이다. 각종 도형, 선 등을 그리기 위한 모든 로직이 Canvas 내에 포함되어 있다. Canvas는 Bitmap 또는 OpenGL 컨테이너 위에 그려진다.

View

View는 Window 내에 존재하는 인터랙티브한 UI 요소를 말한다. (Button, TextView 등)

화면이 그려지는 방식

Surface가 잠겨있는 상태

Surface의 Canvas를 가져와 그리기에 사용한다. 계층 구조에 따라 View로 Canvas를 전달(onDraw)하여 각 View에 해당하는 UI를 그려나간다.

Canvas 잠금 해제 및 Post

모든 View가 버퍼에 그려지면 Surface가 잠금 해제되면서 현재 버퍼와 포그라운드 버퍼가 스왑되고 Surface Flinger에 의해 화면에 합성된다.

View가 그려지는 방식

View는 포커스를 얻으면 레이아웃을 그리도록 요청한다. 이 때, 레이아웃의 루트 노드를 제공해야 한다. 그리기는 루트 노드로부터 자식 노드 방향으로 트리를 따라 전위 순회 방식으로 그려진다. 레이아웃을 그리는 과정은 아래와 같다.

measure() -> onMeasure() -> layout() -> onLayout() -> draw() -> onDraw()

measure, onMeasure

View의 크기를 측정하기 위해 호출된다. 측정 과정에서 부모와 자식 View 간의 크기 정보를 정의하기 위해 아래 두 가지의 클래스를 사용한다.

  1. ViewGroup.LayoutParams
  2. 자식 View가 어떻게 측정될지를 요청하는데 사용된다.
    ViewGroup의 하위 클래스에 따라 다른 속성의 LayoutParams가 존재할 수 있다. (ex. LinearLayout.LayoutParams)
  3. ViewGroup.MeasureSpec
    • UNSPECIFIED: 자식 View가 원하는 대로 크기를 결정
    • EXACTLY: 부모 View가 자식 View의 크기를 결정
    • AT_MOST: 지정된 크기까지 자식 View가 원하는 대로 크기를 결정
  4. 부모 View가 자식 View에게 요구사항을 전달하는데 사용된다.

layout, onLayout

View의 크기와 위치를 할당하기 위해 호출된다.

draw, onDraw

실제로 View를 그리는 단계이다. 전달받은 Canvas 객체와 Paint 객체를 이용하여 필요한 모양과 색을 그린다.

참고

  1. Android - Window, View, Surface, Canvas, Bitmap
  2. [안드로이드] Window, Surface, Canvas, View
  3. [Android] View 함수

'개발 > Android' 카테고리의 다른 글

[번역] Room auto-migrations  (0) 2021.06.02
안드로이드 시스템 아키텍처  (0) 2021.04.24
안드로이드 아키텍처: MVVM  (0) 2021.04.05
안드로이드 아키텍처: MVP  (0) 2021.03.03
안드로이드 아키텍처: MVC  (0) 2021.02.24

MVVM 패턴이란?

MVVM 패턴은 앱의 구성 요소를 Model, View, ViewModel로 나눈 아키텍처를 말한다. View를 비즈니스 로직으로부터 분리시키고, ViewModel에서 디스플레이 로직을 제외한 대부분의 것들을 처리하는 방식으로 관심사를 분리하여 각 계층 간의 의존성을 최소화하는 방법이다.

MVVM, MVP, MVC architecture

MVC, MVP 패턴과 비교했을 때, MVVM은 각 계층간에 양방향 의존이 없는 것을 확인할 수 있다. ViewModel은 View의 존재를 모르고 View와 무관하게 동작하기 때문에 View가 다른 View로 대체되어도 사이드 이펙트가 발생하지 않는다. 마찬가지로 View 또한 ViewModel의 존재를 모르기 때문에 어떤 ViewModel에서든 사용될 수 있다.

안드로이드에서의 MVVM 패턴

아래는 이해를 돕기 위해 EditText에 검색어를 입력하고 Button을 누르면 해당 검색어로 검색한 결과를 출력해주는 안드로이드 앱이 있다고 가정하고 Model, View, ViewModel에서 하는 역할을 간단한 코드로 작성한 결과이다.

 

예시 코드에서는 View와 ViewModel의 중개자 역할로 RxJava의 Observable을 이용했다. ViewModel은 Model로부터 가져온 데이터를 Observable::onNext를 통해 발행하고, View는 해당 Observable을 구독해서 데이터를 처리하게 된다.

View

// 사용자 입력 ViewModel로 전달
button.setOnClickListener({
    showProgress()
    viewModel.findAddress(editText.text.toString())
})
// Observable 구독을 통한 결과 처리
viewModel.resultListObservable.subscribe({
    hideProgress()
    updateList(it)
})

ViewModel

fun findAddress(address: String) {
    model.fetchAddress(address)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(
            onSuccess = { entityList ->
                resultListObservable.onNext(entityList)
            },
            onError = { e ->
                resultListErrorObservable.onNext(e as HttpException)
            }
        )
        .addTo(disposable)
}

Model

fun fetchAddress(address: String): Single<List<Entity>> {
    return getRetrofit().fetchAddressFromServer(address)
}

MVVM 패턴의 장단점

MVVM 패턴은 MVP 패턴과 비교해 많은 이점을 갖는다. 각 계층의 관심사 분리가 확실해질 뿐만 아니라 유지보수성, 확장성, 테스트 용이성도 뛰어나다. 또한, View-ViewModel, ViewModel-View 사이의 상호 의존이 사라지면서 프로젝트 규모가 커질 경우, MVP보다 작성해야 할 코드의 양이 감소한다. RxJava/Android, Eventbus, LiveData 등을 이용한 Event-driven 방식의 처리에 익숙하다면 앱을 설계, 개발할 때 가장 유리한 구조가 아닐까 싶다.

MVP 패턴이란?

MVP 패턴은 앱의 구성 요소를 Model, View, Presenter로 나눈 아키텍처를 말한다.

이전 포스팅에서 MVC는 Controller가 Model의 데이터를 View로 직접 전달하지 못하고, View가 직접 Model에 데이터를 요청해야 한다는 단점이 있었는데, 이를 개선한 것이 MVP이다. MVP 패턴의 Presenter는 View와 Model 사이에서 Controller보다 좀 더 나은 중재자 역할을 수행한다.

 

MVP architecture

 

위 그림과 같이 Presenter는 View에서 요청한 입력에 대한 결과를 Model로 부터 가져와 다시 View에 전달해 줌으로써 View와 Model의 의존 관계를 제거하고, View는 오로지 Presenter가 전달해 준 데이터를 어떻게 표시할지에 대해서만 다룰 수 있게 된다.

안드로이드에서의 MVP 패턴

아래는 이해를 돕기 위해 EditText에 검색어를 입력하고 Button을 누르면 해당 검색어로 검색한 결과를 출력해주는 안드로이드 앱이 있다고 가정하고 Model, View, Controller에서 하는 역할을 간단한 코드로 작성한 결과이다.

View

// 사용자 입력 Presenter로 전달
button.setOnClickListener({
    presenter.findAddress(editText.text.toString())
    controller.findAddress(editText.text.toString())
})

Presenter

fun findAddress(address: String) {
    model.fetchAddress(address)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(
            onStart = {
                view.showProgress()
            },
            onSuccess = { entityList ->
                view.hideProgress()
                view.updateList(entityList)
            },
            onError = { e ->
                view.hideProgress()
                view.showErrorMessage(e.message)
            }
        )
        .addTo(disposable)
}

Model

fun fetchAddress(address: String): Single<List<Entity>> {
    return getRetrofit().fetchAddressFromServer(address)
}

MVC 패턴과 비교하여 동일한 동작을 MVP로 구현했을 때, 아래와 같은 이점이 있음을 확인할 수 있다.

  • View가 Presenter에만 의존적이다.
    MVC에서는 View가 Controller와 Model을 모두 알고있어야 했다.
  • Model의 역할이 줄어들었다.
    Model은 API를 호출하고 데이터를 반환하는 역할만 한다.
    UI 로직, 스레드 관련 로직들은 모두 Presenter에서 다룬다.

MVP 패턴의 장단점

장점: 테스트, 모듈화를 쉽게 할 수 있다.

단점: 많은 양의 보일러플레이트 코드가 생긴다. (View의 생명주기에 따른 네트워크 요청 취소 처리 등..)

MVC 패턴이란?

전통적인 아키텍처 패턴으로 UI를 가지는 앱을 개발하기 위해 앱의 구성 요소를 Model-View-Controller(MVC)로 나누는 방법이다. 하지만, MVC 패턴이 가지는 명확한 한계점 때문에 MVP나 MVVM 패턴에 비해 자주 사용되지는 않는다.

 

MVC에서 View는 데이터를 UI에 보여주는 로직만을 알고 있다. 사용자 입력에 반응하여 View는 Controller에게 입력을 전달하고 Controller는 입력에 알맞은 데이터를 Model에게 요청한다. 데이터가 준비되면 Controller는 View에게 이 사실을 알리고 View는 Model로부터 필요한 데이터를 가져와 UI에 표시해 준다.

 

이러한 흐름은 아래와 같이 표시할 수 있다.

MVC architecture

MVC는 View가 Model의 데이터가 사용 가능 여부를 판단하는 방식에 따라 두 종류로 나뉜다.

  • Passive MVC: Controller가 View에게 Model의 데이터가 갱신되었음을 알림
  • Active MVC: View가 직접 Model의 Observable 데이터를 구독함으로써 데이터 갱신을 처리함

안드로이드에서의 MVC 패턴

아래는 이해를 돕기 위해 EditText에 검색어를 입력하고 Button을 누르면 해당 검색어로 검색한 결과를 출력해 주는 안드로이드 앱이 있다고 가정하고 Model, View, Controller에서 하는 역할을 간단한 코드로 작성한 결과이다.

 

구현을 시작하게 되면 'Activity? Fragment? 어떤 안드로이드 컴포넌트를 View로 사용해야 하지?'라는 고민을 하게 될 수도 있다. View는 UI를 표시하고 유저와 상호작용을 하는 역할을 하므로 Activity, Fragment 모두를 View로써 사용할 수 있다.

View

// 사용자 입력 Controller로 전달
button.setOnClickListener({
    controller.findAddress(editText.text.toString())
})

Controller

fun findAddress(address: String) {
    view.showProgress()
    model.findAddress(address)
}

fun doWhenResultReady() {
    view.hideProgress()
    view.showResult()
}

fun doWhenThereIsErrorFetchingTheResult() {
    view.hideProgress()
    view.showError()
}

Model

fun findAddress(address: String) {
    fetchAddress(address)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(
            onSuccess = { entityList ->
                list = entityList
                controller.doWhenResultReady()
            },
            onError = {
                controller.doWhenThereIsErrorFetchingTheResult()
            }
        )
        .addTo(disposable)
}

MVC 패턴의 장단점

MVC 패턴을 도입하게 되면 테스트 용이성, 유지 보수성, 확장성이 증가하고 SOLID 원칙 중 단일 책임 원칙을 만족시킬 수 있다는 장점이 있지만 아래와 같은 단점 또한 존재한다.

  1. View가 Controller, Model 모두에 의존적이다.
    UI에 필요한 데이터를 표시하기 위해 View는 Controller에 데이터를 요청해야 하고, 준비 완료된 데이터를 Model로부터 받아와야 한다. 이는 View가 Controller, Model 모두를 알고 있어야 함을 의미한다.
  2. Model의 역할이 너무 광범위하다.
    위의 예시 코드를 보면 Model은 단순 데이터를 만드는 것뿐만 아니라 네트워크 연결, 컨트롤러로의 알림 역할까지 하고 있다. 앱이 비활성화되거나 화면 전환 등이 일어날 경우, Model에서 데이터 요청 작업에 대한 취소 처리까지 해야 함을 의미한다.
  3. View가 UI 로직으로부터 자유롭지 못하다.
    MVC에서는 View가 요청한 결과 데이터를 '어떻게' 보여줄지를 결정한다. View는 UI에 데이터를 보여주고 사용자 입력을 받는 역할만 해야 하는데 '어떻게'에 대한 로직이 포함되므로 좋은 구조라고 할 수 없다. 그렇다고 이 로직을 이미 너무 많은 역할을 하고 있는 Model에 넘길 수도 없다.

결론

MVC 패턴을 사용하면 아키텍처가 없는 것보다 테스트 용이성, 유지보수성, 확장성이 증가하지만 Controller가 View에 표시할 데이터를 직접 전달해 주지 않기 때문에, Controller가 View에 데이터 준비 완료를 알려주면 View가 직접 Model에서 데이터를 읽어와야 하는 구조적 한계점이 있다. 또한, View와 Model이 너무 많은 역할을 맡게 된다는 문제점이 있기 때문에 MVP, MVVM 패턴에 비해 자주 사용되지는 않는다.

맥 OS를 모하비(v10.14.x)에서 빅 서(v11.0.1)로 업데이트 후

VSCode(v1.51.1)에서 터미널 실행 시 0.5~1초 정도의 딜레이가 생겼다.

 

약간의 딜레이지만 터미널을 자주 사용하던 입장에서 매우 큰 답답함으로 느껴져서

구글링 해본 결과 터미널에서 아래의 커맨드를 입력해주면 대부분 해결되는 듯 하다.

 

codesign --remove-signature /Applications/Visual\ Studio\ Code.app/Contents/Frameworks/Code\ Helper\ \(Renderer\).app

 

출처: GitHub VSCode Issues

 

얼마 전 개발 도중 어이 없는 실수를 저지른 적이 있었습니다.


바로 스레드를 start() 메소드가 아닌 run() 메소드로 실행한 것이죠.


그래서 오늘은 자바/안드로이드에서 스레드가 갖는 메소드인 


run()과 start()의 차이점에 대해 포스팅 해보겠습니다.



프로세스의 메모리 구조 - 스레드의 특징


 

두 함수의 차이점을 설명하기 전에 프로세스의 메모리 구조에 대해 설명하겠습니다.


위의 그림에서 왼쪽은 단일 스레드 프로세스, 오른쪽은 다중 스레드 프로세스를 나타냅니다.


하나의 프로세스는 stack, heap, static, code로 구성된 메모리 영역을 갖게 되는데


특이한 점은 다중 스레드 이용 시, 각 스레드는 heap, static, code 영역은 공유하지만


call stack 영역은 독립적으로 사용하게 됩니다.


Call stack에 대한 추가 정보(Wikipedia)

 


run()과 start()가 다르게 동작하는 이유


자바에서 스레드는 Thread 클래스를 상속받거나 Runnable 인터페이스를 구현하여 만들 수 있습니다.


가장 간단한 Runnable 인터페이스를 구현한 경우를 보면 아래와 같은 구조입니다.



위와 같은 스레드가 있다고 할 때, 


프로세스의 메인 스레드에서 run() 메소드를 호출하는 것은 


단순하게 오버라이딩 된 메소드를 호출해서 사용하는 것과 같습니다.


물론 메인 스레드의 call stack을 이용하기 때문에


 run() 메소드가 끝날 때까지 다른 작업을 하지 못합니다.


반면 start() 메소드를 호출할 경우, JVM은 스레드를 위한 call stack을 새로 만들기 때문에


run() 메소드 호출과는 다르게 진정한 스레드로서 독립적인 동작이 가능합니다.





  요약


JDK에 포함된 jarsigner를 이용해 jar파일 또는 안드로이드 apk파일에 서명하는 경우 발생하는 타임 스탬프관련 경고 메시지를 없애는 방법



  증상 및 원인


jarsigner를 이용해 리패키징한 안드로이드 apk파일에 서명하던 도중 아래와 같은 경고 메시지를 발견했다. 서명은 정상적으로 되었기 때문에 무시해도 상관없으나 왠지 찜찜하여 원인과 해결 방법을 알아보았다.


Warning:

No -tsa or -tsacert is provided and this jar is not timestamped. Without a timestamp, users may not be able to validate this jar after the signer certificate's expiration date <yyyy-mm-dd> or after future revocation date.


< 발생한 경고 메시지 >




서명 인증서가 만료되면 배포 된 파일을 다시 서명해야 하기 때문에 이런한 문제를 해결하기 위해 재서명이 필요없는 URL 기반 인증 방식인 타임 스탬프를 이용한 서명을 권장하는 것으로 확인된다. 관련 Oracle 문서에는 J2SE 5.0부터 추가 된 기능이라고 나와있으나 기존에 사용하던 JDK 버전(1.7021)에서는 경고 없이 서명이 되었던걸로 보아 이후 버전에서 발생하는 것으로 보인다.



  해결 방법


서명 시, -tsa 옵션과 함께 인증 기관 URL을 입력해주면 경고 메시지 없이 서명이 가능하다.


-tsa http://timestamp.digicert.com


< Timestamp를 이용한 서명 결과 >



  관련 포스팅




 

 

  요약

 

다수의 스레드를 사용한 C/C++ 프로그램에서 clock() 함수를 이용했을 때 발생하는 문제점과 clock_gettime() 함수를 이용하여 수행 시간을 정확하게 측정할 수 있는 방법을 설명한다.

 

 

  clock() 함수를 이용한 시간 측정 방법과 그 문제점

 

일반적으로 C/C++ 프로그래밍 시, <time.h>에 포함된 clock() 함수를 이용하여 아래와 같은 방식으로 수행 시간을 측정한다.

 

clock_t begin, end;

 

begin = clock(); 

 

/ *

 

수행 시간 측정을 원하는 작업

 

* /

 

end = clock();

 

cout << ((end - begin) / CLOCKS_PER_SEC) << endl;

 

이 방법은 싱글스레드 프로그램에서는 정상적으로 동작하나 멀티스레드 프로그램에서 사용할 경우, 시간 측정이 올바르게 되지 않는 것을 확인할 수 있다. 이는 clock() 함수가 단순히 프로세스에 의해 사용 된 클럭 수를 리턴해주기 때문에 하나의 스레드에서 위의 코드를 이용해 시간을 측정하는 동안 다른 스레드의 동작에 의해 클럭 값이 증가하게 되기 때문이다.


 


 

  clock_gettime() 함수를 이용한 올바른 시간 측정 방법

 

정확한 수행 시간을 측정하기 위해서는 <time.h>에 포함된 clock_gettime() 함수를 이용해야 한다. clock_gettime() 함수는 시간을 측정할 시계의 타입을 정의하는 clockid_ttimespec이라는 구조체를 인자로 갖는다.

  • clockid_t : 시계의 타입을 나타내는 인자로써, POSIX에서는 기본적으로 2가지 타입을 제공한다.

  • CLOCK_REALTIME

    시스템 전역에서 사용되는 실제 시간 (Epoch 기준)

    CLOCK_MONOTONIC

    특정 시점 이후로 흐른 시간 (ex. 부팅 후 시간)

  • timespec : 초와 나노초를 멤버로 갖는 구조체. <time.h>에 정의되어 있음.
struct timespec {
        time_t   tv_sec;        /* seconds */
        long     tv_nsec;       /* nanoseconds */
};

 

clock_gettime() 함수는 clockid_t에 입력한 시계를 이용하여 시간을 측정하므로 프로세스에서 사용한 클럭 수를 세는 clock() 함수보다 정확한 수행 시간 측정이 가능하다. 사용 방법은 다음과 같다. clockid_t의 인자로 CLOCK_MONOTONIC 대신 CLOCK_REALTIME을 사용해도 되지만 CLOCK_REALTIME 사용 시, 시간 측정 중에 시스템 시간이 변경될 경우 측정값이 변한다는 문제점이 있다.

 

struct timespec begin, end ;

 

clock_gettime(CLOCK_MONOTONIC, &begin);

 

/ *

 

수행 시간 측정을 원하는 작업

 

* /

 

clock_gettime(CLOCK_MONOTONIC, &end);

 

cout << (end.tv_sec - begin.tv_sec) + (end.tv_nsec - begin.tv_nsec) / 1000000000.0 << endl;

 

 

+ Recent posts