: 이 포스트는 "안드로이드 개발 레벨업 교과서" 책을 요약한 내용이다. 구체적인 내용이 궁금하다면 이 책을 구매해서 읽어보면 좋겠다. 해당 포스트에서 활용할 예제 소스는 https://github.com/wikibook/advanced-android-book/tree/master/tech05/DataBindingSample 이다. 이 예제를 통해 MVVM 설계를 하는 데 중요한 데이터 바인딩에 대해서 알아볼 것이다. 아래 사진처럼 구현할 것이다. "좋아요" 버튼을 누르면 위에 있는 숫자가 늘어난다.




1. 데이터 바인딩을 이용하려면 build.gradle에 데이터 바인딩을 활성화 해야 한다. 

android {
	....
	dataBinding {
		enable=true
	}
	....
}


2. 레이아웃 파일에 바인딩할 클래스를 기술한다.

: 데이터 바인딩을 하려면 XML 파일의 루트를 layout 태그로 해야 한다. <data> 태그를 통해 바인딩할 클래스를 기술한다. 아래는 ~ sample.User 클래스를 바인딩하되 이름을 user로 하겠다는 뜻이다.

<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" tools:context="com.github.advanced_android.databindingsample.MainActivity"> <data> <!-- User 클래스에 바인딩한다 --> <variable name="user" type="com.github.advanced_android.databindingsample.User"/> </data> <LinearLayout ...... </LinearLayout> </layout>

아래와 같이 뷰와 데이터를 연결할 수 있다. android:onClick 부분은 User 클래스의 onClickLike 메서드와 연결된다.

<TextView
    android:text="@{user.name}"

    ...


<TextView
    android:text="@{String.valueOf(user.likes)}"
    
    ....

<ImageButton
    android:onClick="@{user::onClickLike}"
   
    ....

다음은 위 프로젝트 예제의 전체 xml 소스이다.

<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.github.advanced_android.databindingsample.MainActivity">

    <data>
        <!-- User 클래스에 바인딩한다 -->
        <variable
            name="user"
            type="com.github.advanced_android.databindingsample.User"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        >

        <!-- 시간을 표시한다 -->
        <TextView
            android:id="@+id/text_time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="right"/>

        <!-- 프로필 -->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="프로필"
            android:textAppearance="@style/TextAppearance.AppCompat.Large"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="이름:"
            android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="@{user.name}"
            android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="나이:"
            android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="@{String.valueOf(user.age)}"
            android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="LIKE:"
            android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="@{String.valueOf(user.likes)}"
            android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>


        <ImageButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{user::onClickLike}"
            android:src="@drawable/ic_thumb_up_black_36dp"
            />

    </LinearLayout>
</layout>


3. 바인딩하는 클래스를 구현한다.  

: XXX.set(YYY)를 사용하면 실제 UI도 바뀌게 된다. TextView.setText() 등을 호출할 필요가 없다. XXX.get()을 하면 해당 UI 값을 얻을 수 있다. 또한 onClickLike를 xml에서 지정해 리스너 또한 별도로 구현할 필요 없다. 

public class User {
    public ObservableField<String> name = new ObservableField<>();
    public ObservableInt  age = new ObservableInt();
    public ObservableInt likes = new ObservableInt();

    public User(String nameString, int ageInt) {
        name.set(nameString);
        age.set(ageInt);
        likes.set(0);
    }

    public void onClickLike(View view){
        likes.set(likes.get() + 1);
    }
}


4. 클래스 인스턴스와 레이아웃을 연결한다.  

: 아래 코드에서 binding.textTime.setText()의 의미은 binding과 연결된 xml 레이아웃에서 id 가 text_Time인 뷰에 setText(data)를 하라는 것이다.

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Binding 오브젝트를 얻는다
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        // Binding 오브젝트에 User를 설정한다
        binding.setUser(new User("kim", 25));

        String date = (String) DateFormat.format("yyyy/MM/dd kk:mm:ss", Calendar.getInstance());
        binding.textTime.setText(date);
        //뷰에 id가 지정돼 있으면, Binding 오브젝트로부터 뷰에 대한 참조를 얻을 수 있다
    }


※ DB 락 문제

: SQLite의 가장 큰 문제는 DB락이다. DB 작업은 메인 스레드보다는 백그라운드 스레드에서 사용되는 게 권장되 스레드간에 DB 작업 시 락을 잡는 시점이 겹치면서 문제가 발생한다.


- 락 상태

: DB에 쓸 때는 exclusive(배타) lock을 잡고 읽을 때는 shared(공유) lock를 잡는다. 배타 락은 다른 락을 허용하지 않고 공유 락은 다른 공유락을 허용한다. 락 상태는 아래와 같이 있다.

1. UNLOCKED : 읽기와 쓰기가 안 되는 기본 상태이다.


2. SHARED : 읽기만 된다. 다른 스레드에서 동시에 공유 락을 가져도 된다. 단, 다른 스레드에서 쓰기는 못 한다. 쓰기를 위해서는 모든 공유 락이 해제될 때까지 기다려야 한다.


3. RESERVED : 쓰기를 예약한다. 예약 락은 하나만 있을 수 있고 여러 공유 락과 같이 있을 수 있다. 또한 새로운 공유 락도 허용된다.


4. PENDING : 쓰기를 예약하되 최대한 빨리 쓰려고 한다. 따라서 새로운 공유 락이 허용 안 된다.


5. EXCLUSIVE : 파일에 쓴다. 다른 락과 같이 있을 수 없으며 하나만 허용된다. 배타 락을 가질 시 다른 작업이 불가능해 배타 락에서의 작업을 최소화 해야 한다.


: DB 락이 발생하는 원인은 CREATE, UPDATE, DELETE에서 쓰기를 하면 배타 락을 잡기 때문이다. 특히, CUD에서 쓰기를 한꺼번에 하는 트랜잭션을 할 때 배타 락을 오래 잡는다.


- 트랜잭션 동작 방식

1. deferred : 락을 뒤로 미룬다. 트랜잭션을 시작할 때 락을 잡지 않다가 읽기 작업이 있으면 공유 락을 잡고 쓰기 작업이 있으면 예약 락을 잡는다. 즉, 락이 뒤로 미뤄지기 때문에 다른 스레드에서 DB 작업이 가능하다


2. immediate : 트랜잭션을 시작할 때 예약 락이 잡힌다. 따라서 다른 스레드에서 읽기는 할 수 있으나 예약 락은 1개만 가능하기 때문에 다른 immediate 트랜잭션을 시작할 수 없다.


3. exclusive : 트랜잭션을 시작할 때 배타 락이 잡힌다. 따라서 트랜잭션 처음부터 끝까지 다른 스레드에서 DB 작업을 할 수 없다.


: 안드로이드에서는 immediate와 exclusive 동작 방식만 지원하고 exclusive 동작 방식보다는 immediate 동작 방식을 권장한다. 기본적으로 안드로이드는 exclusive 동작 방식을 한다. 따라서 db.beginTransaction() 보다는 immediate 동작 방식인 db.beginTransactionNonExclusive()를 사용하면 좋다. 이렇게 하면 DB 락 문제를 조금은 회피 가능하다. 


- DB 락 예

1. 여러 스레드에서 1개의 SQLiteDatabase 인스턴스만 가지고 쓰기를 한다. => DB 락 X

2. 여러 스레드에서 각각의 SQLiteDatabase 인스턴스를 가지고 쓰기를 한다. => DB 락 O

3. 1개의 스레드에서 1개의 SQLiteDatabase 인스턴스를 가지고 계속 쓰기를 하고 여러 스레드에서 각각 SQLiteDatabase 인스턴스를 가지고 읽기만 한다. => DB락 O

4. 여러 스레드에서 각각 SQLiteDatabase 인스턴스를 가지고 읽기만 한다. => DB 락 X

5. 1개의 SQLiteDatabase 인스턴스를 가지고 쓰기 트랜잭션과 읽기를 동시에 한다. => DB 락 X

* 많은 데이터를 처리할 때 트랜잭션으로 감싸면 더 속도가 빨라진다.


: 위 예를 볼 때 SQLiteDatabase 인스턴스를 1개만 가지고 여러 스레드에서 작업하면 DB락이 발생하지 않는다. 따라서 DB 락 문제를 해결하려면 SQLiteDatabase 인스턴스를 1개만 유지하면 된다. 하지만 SQLiteDatabase 인스턴스를 1개만 사용하면 속도가 많이 느리다. 따라서 만약 여러 스레드에서 읽기만 한다면 읽기 전용 DB와 쓰기 전용 DB를 만드는 게 좋다. 읽기의 경우 SQLiteDatabase 인스턴스를 각각 생성해도 DB 락이 발생하지 않기 때문이다.



※ SQLiteOpenHelper 클래스

: SQLiteOpenHelper 클래스는 DB 생성과 DB 버전 관리를 알아서 해주므로 SQLiteDatabse 말고 반드시 SQLiteOpenHelper 클래스를 사용하자.

public class DatabaseHelper extends SQLiteOpenHelper{
	private static final String DATABASE_NAME = "example.db";
	private static final int DATABASE_VERSION = 2;
	
	DatabaseHelper(Context context){
		super(context, DATABASE_NAME, null, DATABASE_VERSION);
	}
	
	@Override
	public void onCreate(SQLiteDatabase db){
		db.execSQL(......);
	}
	
	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
		for(int i=oldVersion+1; i<=newVersion; i++){
			processUpgrade(db, i);
		}
	}
}

DB마다 별도의 DB 헬퍼가 필요하다. 생성자의 DATABASE_NAME이 DB 파일명이다. 생성자의 세 번째 인자에 null을 넣으면 쿼리 결과로 SQLiteCursor 가 반환된다. 대부분 SQLiteCursor을 사용하므로 null 값을 보통 넣는다. 참고로 DB는 SQLiteOpenHelper 생성자에서 생성되지 않는다. getReadableDatabase()나 getWritableDatase 호출할 때 DB 생성/열기가 된다. 즉, onCreate/onOpen/onUpdate가 호출된다. 만약 SQLiteOpenHelper가 이미 생성되었다면 다시 생성하지 않는다. DB 업데이트는 위 소스와 같이 한다. 기존 버전에서부터 각 버전마다의 변경 내용을 적용해야 한다. 추가로 onCreate와 onUpdate는 순차적으로 실행되지 않는다. 둘 중 하나만 호출된다. 따라서 onUpdate에 최근 수정 사항을 코딩했다고 onCreate에 안 하면 안 된다. 처음 앱을 설치했을 때는 onCreate만 호출되기 때문이다. onCreate도 최근 수정 사항을 반영하여 코딩해야 한다.

onCreate와 onUpdate 메서드는 트랜잭션으로 이미 감싸져 있다. 따라서 별도로 트랜잭션을 고려할 필요가 없다.


메모리 DB라는 게 있다. SQLiteOpenHelper 생성자에서 DAATABASE_NAME 대신 null 값을 넣으면 된다. 메모리 DB는 기존의 파일 DB 보다 속도가 훨씬 빠른 대신 휘발성이다. 캐시 용도로 사용하는 게 좋다. 휘발성이기 때문에 버전에 신경쓸 필요는 없다. 


앞 부분에서 DB 락 문제가 생기지 않으려면 하나의 인스턴스만 생성해야 한다고 했다. 따라서 DB 헬퍼는 싱글턴 패턴을 만들어서 사용해야 한다.

private static DatabaseHelper instance;

public static synchronized DatabaseHelper getInstance(Context context){
	if(instance == null){
		instance = new DatabaseHelper(context.getApplicationContext());
	}
	return instance;
}

private DatabaseHelper(Context context){
	....
}

참고로 생성자 인자로 getApplicationContext()를 전달했다. getApplicationContext()는 앱이 살아 있는 동안 제거되지 않기 때문이다. 

close() 메서드는 거의 호출할 필요가 없다. close 메서드를 호출하면 SQLiteDatabase 인스턴스는 null이 된다. 만약 다른 스레드에서 접근할 시 close 때문에 문제가 될 수 있다. close()는 거의 호출 하지 않는 게 좋다. 

DB 기능을 변경할 때는 onConfigure()와 onOpen()을 사용하면 된다. onConfigure()는 SQLiteDatabase 생성/열기 이후, onCreate()와 onUpdate() 전에 호출되며 WAL이나 외래키 지원 기능을 활성화할 수 있다. onOpen()은 onCreate()와 onUpdate() 이후에 호출 되며 DB 연결 설정을 변경할 때 사용한다. 


* 로컬 프로세스에만 데이터가 쓰이면 콘텐트 프로바이더를 사용 안 하는 걸 권장한다. DB에 직접 접근하는 코드에서도 콘텐트 프로바이더를 사용 안 하는 게 좋다. 콘텐트 프로바이더는 외부에 접근하거나 프로세스가 분리될 때 사용하는 게 좋다.

+ Recent posts