목록2017/11 (3)

Realm 과 Android Architecture Components (AAC) 를 이용한 간단한 메모장 만들기

AAC 에 대해 평소 호기심이 있어 해당 라이브러리를 이용하여 토이 프로젝트를 하나 만들어 봤다. 2016년 부터 이슈가 되기 시작한 MVP, MVVM 패턴이 있는데 구글에서는 MVVM 을 쉽게 구현할 수 있는 AAC 를 제공해줘서 이와 관련된 모듈들의 간단한 사용 예제를 정리해보았다. * MVP 패턴이나 MVVM 패턴에 대한 고찰은 담겨있지 않다. # 1. 프로젝트 관련 환경 및 종속성 ## A. 프로젝트 환경 * Android Studio 3.0 ## B. project level build.gradle ```gradle buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.0.1' classpath 'io.realm:realm-gradle-plugin:4.1.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() } } task clean(type: Delete) { delete rootProject.buildDir } ``` ## C. app level build.gradle ```gradle ... apply plugin: 'realm-android' ... dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:27.0.1' implementation 'com.android.support.constraint:constraint-layout:1.0.2' testImplementation 'junit:junit:4.12' implementation 'com.android.support:recyclerview-v7:27.0.1' implementation 'com.android.support:cardview-v7:27.0.1' implementation 'android.arch.lifecycle:extensions:1.0.0' annotationProcessor 'android.arch.lifecycle:compiler:1.0.0' androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' } ... ``` # 2. LiveRealmData - Realm 을 위해 LiveData 를 상속받은 클래스 ```java /** * https://github.com/ericmaxwell2003/android-persistence/blob/master/app/src/main/java/com/example/android/persistence/codelab/realmdb/utils/LiveRealmData.java */ public class LiveRealmData<T extends RealmModel> extends LiveData<RealmResults<T>> { private RealmResults<T> results; private final RealmChangeListener<RealmResults<T>> listener = new RealmChangeListener<RealmResults<T>>() { @Override public void onChange(RealmResults<T> results) { setValue(results); } }; public LiveRealmData(RealmResults<T> realmResults) { results = realmResults; setValue(realmResults); } @Override protected void onActive() { System.out.println("LiveRealmData.onActive(): " + results); results.addChangeListener(listener); } @Override protected void onInactive() { System.out.println("LiveRealmData.onInactive()"); results.removeChangeListener(listener); } } ``` 이 프로젝트는 [Room Persistence Library](https://developer.android.com/topic/libraries/architecture/room.html) 를 이용하지 않고 Realm 을 이용하기 때문에 특정 RealmResults 에 데이터 변동시 쓰라고 Realm 에서 제공해주는 RealmChangeListener 를 사용하여 구성된 코드를 발견할 수 있다. 이 클래스가 상속하고 있는 LiveData 는 Activity 나 Fragment 따위의 수명주기를 인식할 수 있는 데이터 홀더 클래스이다. Realm 에서는 질의 결과를 계속해서 관찰하기 위해 리스너를 연결하는데, Lifecycle 에 따라 이 리스너를 제거해줘야 한다. 다행스럽게도 LiveData 에서는 이러한 시점을 onActive 와 onInActive 를 통해 알려준다. onActive 메소드는 LiveData 인스턴스를 옵저빙하는 리스너의 수가 0에서 1이 되었을 때 호출되며, onInActive 메소드는 LifeCycle 이 데이터를 출력하기에 유효하지 않은 상태 (액티비티가 백스택에 들어갔거나 화면에 안보일 때 등등) 에 호출된다. # 3. BaseDao ```java public class BaseDao<T extends RealmModel> { protected Realm mRealm; public BaseDao(Realm mRealm) { this.mRealm = mRealm; } // https://github.com/ericmaxwell2003/android-persistence/blob/master/app/src/main/java/com/example/android/persistence/codelab/realmdb/utils/Realm%2BDao.kt protected LiveRealmData<T> asLiveData(RealmResults<T> data) { return new LiveRealmData<>(data); } } ``` 이 클래스는 모든 DAO 클래스가 상속받는 클래스인데 Realm 의 질의 결과를 LiveRealmData 로 감싸서 뱉어주는 asLiveData 메소드를 포함하고 있다. # 4. LifecycleListener - LifecycleObserver 맛보기 클래스 ```java public class LifecycleListener implements LifecycleObserver { private Context mContext; public LifecycleListener(Context mContext) { this.mContext = mContext; } @OnLifecycleEvent(Event.ON_CREATE) void onCreate() { Toast.makeText(mContext, "LifecycleListener.onCreate()", Toast.LENGTH_SHORT).show(); } @OnLifecycleEvent(Event.ON_RESUME) void onResume() { Toast.makeText(mContext, "LifecycleListener.onResume()", Toast.LENGTH_SHORT).show(); } @OnLifecycleEvent(Event.ON_STOP) void onPause() { Toast.makeText(mContext, "LifecycleListener.onPause()", Toast.LENGTH_SHORT).show(); } @OnLifecycleEvent(Event.ON_START) void onStart() { Toast.makeText(mContext, "LifecycleListener.onStart()", Toast.LENGTH_SHORT).show(); } @OnLifecycleEvent(Event.ON_STOP) void onStop() { Toast.makeText(mContext, "LifecycleListener.onStop()", Toast.LENGTH_SHORT).show(); } @OnLifecycleEvent(Event.ON_DESTROY) void onDestroy() { Toast.makeText(mContext, "LifecycleListener.onDestory()", Toast.LENGTH_SHORT).show(); } } ``` 이 클래스는 LifecycleObserver 를 사용하는 맛보기 클래스인데, 이 클래스가 관찰하고 있는 컴포넌트(LifecycleOwner)의 라이프사이클 변화에 따라 특정 메소드를 자동으로 호출해준다. 구글의 개발자 페이지의 [라이프사이클 핸들링 관련 문서](https://developer.android.com/topic/libraries/architecture/lifecycle.html) 에서는 LocationListener 를 제어하는 모습을 볼 수 있으나, 위의 코드에선 단순히 Toast 메세지만 띄워준다. 이 클래스는 MainActivity 의 Lifecycle 을 관찰하는데, MainActivity 는 AppCompatActivity 를 상속받고 있다. 서포트 라이브러리 26.1.0 부터는 프래그먼트와 액티비티가 이미 LifecycleOwner 인터페이스를 구현하고 있는데, 예시로 든 AppCompatActivity 는 다음과 같은 상속구조를 가진다. ![상속도](/images/gallery/1511418846256_hierarchy.png) 위로 쭉 올라가다보면 SupportActivity 가 있는데 해당 클래스의 선언부는 다음과 같다. ```java public class SupportActivity extends Activity implements LifecycleOwner ``` 따라서 일반적인 경우에는 Activity 나 Fragment 따위가 LifecycleOwner 를 추가적으로 구현할 필요는 없다. 일반적이지 않은, 커스텀이 필요한 경우는 위의 문서를 참고하기 바란다. # 5. MainActivity ```java public class MainActivity extends AppCompatActivity { // 메모 RecyclerAdapter private MemoAdapter mMemoAdapter; // 뷰 모델 private MainViewModel mMainViewModel; // 라이프사이클 변화에 따른 토스트 메시지를 팝업해주는 리스너 private LifecycleListener mLifecycleListener; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 인자로 LifecycleOwner 를 넣어 생성한다. mLifecycleListener = new LifecycleListener(this); // 옵저버로 위의 리스너 인스턴스를 지정한다. getLifecycle().addObserver(mLifecycleListener); mMemoAdapter = new MemoAdapter(mOnMemoItemClickListener, mOnMemoItemUpdateClickListener, mOnMemoItemDeleteClickListener); RecyclerView recyclerView = findViewById(R.id.rv); recyclerView.setLayoutManager(new LinearLayoutManager(getApplicationContext())); recyclerView.setAdapter(mMemoAdapter); findViewById(R.id.confirmBtn).setOnClickListener(mOnMemoAddClickListener); // 뷰 모델 생성 mMainViewModel = ViewModelProviders.of(this).get(MainViewModel.class); // 뷰 모델에서 LiveData 획득 후 관측 시작 mMainViewModel.getMemos().observe(this, new Observer<RealmResults<Memo>>() { @Override public void onChanged(@Nullable RealmResults<Memo> memos) { System.out.println("RealmResults<Memo>.onChanged: " + memos); mMemoAdapter.onUpdate(memos); } }); } private View.OnClickListener mOnMemoAddClickListener = new OnClickListener() { @Override public void onClick(View view) { MemoActionDialog.show(null, MainActivity.this, new Callback() { @Override public void onItemAdd(Memo memo) { mMainViewModel.addMemo(memo); } @Override public void onItemUpdate(Memo oldOne, Memo newOne) { } }); } }; private View.OnClickListener mOnMemoItemClickListener = new OnClickListener() { @Override public void onClick(View view) { } }; private View.OnClickListener mOnMemoItemUpdateClickListener = new OnClickListener() { @Override public void onClick(View view) { Memo oldOne = (Memo) view.getTag(); MemoActionDialog.show(oldOne, MainActivity.this, new Callback() { @Override public void onItemAdd(Memo memo) { } @Override public void onItemUpdate(Memo oldOne, Memo newOne) { mMainViewModel.onItemUpdateClick(oldOne, newOne); } }); } }; private View.OnClickListener mOnMemoItemDeleteClickListener = new OnClickListener() { @Override public void onClick(View view) { Memo clickedItem = (Memo) view.getTag(); mMainViewModel.onItemDeleteClick(clickedItem); } }; } ``` 위의 코드 중 mMainViewModel.getMemos().observe(...); 이 코드 덕분에 메모를 추가하거나 수정하거나 삭제하여 데이터가 변동되면 바로 알 수 있다. # 6. MainViewModel ```java public class MainViewModel extends ViewModel { private Realm mRealm; private MemoDao mMemoDao; private LiveData<RealmResults<Memo>> mMemos; public MainViewModel() { System.out.println("MainViewModel.constructor()"); mRealm = Realm.getDefaultInstance(); mMemoDao = new MemoDao(mRealm); subscribeMemos(); } private void subscribeMemos() { System.out.println("MainViewModel.subscribeMemos()"); mMemos = mMemoDao.findAllMemos(); } public LiveData<RealmResults<Memo>> getMemos() { System.out.println("MainViewModel.getMemos(): " + mMemos.getValue()); return mMemos; } public void addMemo(Memo memo) { mMemoDao.add(memo); } public void onItemDeleteClick(Memo memo) { mMemoDao.delete(memo); } public void onItemUpdateClick(Memo oldOne, Memo newOne) { mMemoDao.update(oldOne, newOne); } @Override protected void onCleared() { System.out.println("MainViewModel.onCleared()"); mRealm.close(); super.onCleared(); } } ``` 위의 뷰 모델에서는 최초 생성시 Realm 에 저장되어 있는 모든 메모 목록을 LiveData 로 불러와서 옵저빙을 할 수 있게 제공해주며, 데이터의 추가, 수정, 삭제 기능들을 Dao 클래스를 통해 제공한다. # 7. 프리뷰 ![preview](/images/gallery/1511420747863_realm_with_aac_preview.png) # 샘플 프로젝트 * [깃허브](https://github.com/prChoe/RealmMemoWithAAC) # 관련 링크 * [AAC 관련 개발자 문서](https://developer.android.com/topic/libraries/architecture/index.html) * [구글 안드로이드 개발자 블로그](https://developers-kr.googleblog.com/2017/06/android-and-architecture.html) * [Realm과 함께하는 안드로이드 아키텍쳐 컴포넌트](https://academy.realm.io/kr/posts/android-architecture-components-and-realm/)

Android 2017.11.23 7:27
Node.js 의 프로세스 관리자인 PM2 모듈 사용하기

Java 웹 애플리케이션을 배포할 때는 Tomcat 따위의 WAS를 이용하여 배포한다. 정적 파일을 요청자에게 제공하거나, 로직상의 문제가 생겨 예외가 발생하면 애플리케이션을 재시작해주기도 한다. Node.js 의 경우 자체적인 웹서버 기능이 존재하여 별도의 WAS 나 웹서버를 이용하여 배포할 필요는 없다. ```text node app.js ``` 일반적으로 위의 명령어를 이용하여 애플리케이션을 시작하게 된다. 물론 express.js 를 이용하여 제작했을 경우 bin 폴더의 www 파일을 실행하면 애플리케이션이 시작되는 차이는 있다. 하지만 위의 명령어를 이용하여 애플리케이션을 시작하게 되면 에러가 발생하여 서버가 뻗어버렸을 때 서버를 자동으로 재시작해준다던가 하는 편리함은 없다. node.js 애플리케이션은 위와 같은 불편함을 극복하기 위해 별도의 프로세스 관리자 모듈을 통해 애플리케이션을 배포한다. ## 1. PM2 설치하기 node.js 관련 모듈은 npm 이라는 패키지 매니저를 이용하여 설치한다. 안드로이드나 Java 애플리케이션을 개발할 때 Maven 이나 Gradle 따위를 이용하여 외부 모듈을 추가하는 것처럼 node.js 에서도 npm 을 이용하여 관련 작업을 진행할 수 있다. ```text // pm2 모듈의 최신 버전을 전역설치한다. // 전역설치를 하게 된다면 최초 1회만 설치하면 이후에도 사용할 수 있다. // 이후에 bash 에서 pm2 xxx 따위로 관련 기능을 진행할 수 있다. npm install pm2@latest -g ``` ## 2. PM2 기본 명령어 ### 1. 프로세스 실행 ```text // xxx 파일을 pm2 를 이용하여 실행한다. // 주로 xxx 파일은 express.js 미사용시 app.js 를, 사용시에는 bin/www 파일을 실행한다 pm2 start xxx.js ``` ![pm2 start](/images/gallery/1510718826400_pm2_start.png) ```text // xxx 파일을 쌍따옴표 안에 지정한 이름을 지정하여 실행한다. // pm2 를 이용하여 애플리케이션을 여러개 실행하게 될건데 별도의 이름을 지정하지 않을 경우 실행한 파일의 이름이 프로세스 이름으로 지정되어 문제가 생길 수 있다. pm2 start --name "앱 이름" xxx.js ``` ![pm2 start with name](/images/gallery/1510718817207_pm2_start_naming.png) ### 2. 실행중인 프로세스 목록 확인 ```text // pm2 를 이용하여 실행한 프로세스의 목록을 확인할 수 있다. pm2 list ``` 아래의 이미지는 bash 상에서 pm2 list 를 실행한 결과이다. ![pm2 list](/images/gallery/1510718764400_pm2_list.png) ### 3. 실행중인 프로세스 정지 ```text // id(숫자) 또는 name 에 해당되는 실행중인 프로세스를 정지한다. pm2 stop id|name ``` ![pm2 stop by id](/images/gallery/1510718837754_pm2_stop_id.png) ### 4. 실행중인 프로세스 재시작 ```text // id(숫자) 또는 name 에 해당되는 프로세스를 재시작한다. pm2 restart id|name ``` ![pm2 restart by name](/images/gallery/1510718794440_pm2_restart_name.png) ### 5. 실행중인 프로세스 제거 ```text // id(숫자) 또는 name 에 해당되는 프로세스를 제거한다. // PM2 의 프로세스 관리 목록에서 제거되며, 실제 프로젝트 파일은 삭제되지 않는다. pm2 delete id|name ``` ![pm2 delete by id](/images/gallery/1510718735552_pm2_delete_id.png) ### 6. 특정 프로세스에 대한 상세정보 출력 ```text // id(숫자) 또는 name 에 해당되는 프로세스의 상세 정보를 출력한다. pm2 show id|name ``` ![pm2 show](/images/gallery/1510718804558_pm2_show_id.png) ### 7. 특정 프로세스의 실시간 로그 확인 ```text // id(숫자) 또는 name 에 해당되는 프로세스의 실시간 로그를 출력한다. pm2 logs id|name ``` ![pm2 logs](/images/gallery/1510718776617_pm2_logs_id.png) ### 8. PM2 셧다운 ```text // PM2 를 셧다운한다. // PM2 에서 관리중인 모든 프로세스가 delete 된다. pm2 kill ``` ![pm2 kill](/images/gallery/1510718747017_pm2_kill.png) ## 3. PM2 의 ecosystem.config.js 를 활용한 개발/배포 모드 실행 ### 1. ecosystem.config.js ```js module.exports = { apps: [ { // pm2로 실행한 프로세스 목록에서 이 애플리케이션의 이름으로 지정될 문자열 name: "PM2Exam", // pm2로 실행될 파일 경로 script: "./bin/www", // 개발환경시 적용될 설정 지정 env: { "PORT": 3000, "NODE_ENV": "development" }, // 배포환경시 적용될 설정 지정 env_production: { "PORT": 8080, "NODE_ENV": "production" } } ] }; ``` ### 2. ecosystem.config.js 를 활용한 실행 ```text // 개발모드로 실행 pm2 start ecosystem.config.js // 배포모드로 실행 pm2 start ecosystem.config.js --env production ``` ### 3. npm script 를 활용한 실행 ```json { "name": "pm2exam", "version": "0.0.0", "private": true, "scripts": { "start": "pm2-dev start ecosystem.config.js", "deploy" : "npm install && pm2 start ecosystem.config.js --env production" }, "dependencies": { "body-parser": "~1.16.0", "cookie-parser": "~1.4.3", "debug": "~2.6.0", "ejs": "~2.5.5", "express": "~4.14.1", "morgan": "~1.7.0", "serve-favicon": "~2.3.2" } } ``` 위의 내용에서 scripts 부분을 보면 ```json ... "scripts": { "start": "pm2-dev start ecosystem.config.js", "deploy" : "npm install && pm2 start ecosystem.config.js --env production" } ... ``` start 와 deploy 가 정의되어 있다. start 에는 pm2 가 아닌 pm2-dev 로 명령어가 시작되는데, pm2-dev 로 애플리케이션을 실행할 경우 프로젝트의 파일이 변경될 때마다 서버가 재시작된다. 단, 웹 페이지 개발시 페이지가 수정되어도 브라우저가 자동으로 새로고침 되지는 않는다. deploy 에서는 우선 package.json 에 존재하는 모든 모듈을 설치하고 pm2 를 이용하여 ecosystem.config.js 를 실행하는데 뒤에 --env production 이 붙어있다. --env production 이 붙어있어서 ecosystem.config.js 내부에 존재하는 env_production 에 해당되는 설정대로 서버가 시작된다. ### 4. 샘플 프로젝트 링크 및 실행 https://github.com/prChoe/PM2Exam 프로젝트 clone 이나 download 후 프로젝트 루트 단에서 터미널로 npm install 을 타이핑하여 연관 모듈들을 다운받고 npm 스크립트를 실행하여 샘플 프로젝트를 테스트할 수 있다. ``` // start 스크립트 실행 - 개발모드 npm run start // deploy 스크립트 실행 - 배포모드 npm run deploy ```

Node.js 2017.11.15 4:32