이번에는 Room을 사용하여 DB 연결을 해보겠습니다.

*ChatGPT의 도움을 받아 개발하였습니다.*

Room 연결

Room 정의

더보기

Android 공식 ORM(Object-Relational Mapping), SQLite 기반 DB
    - 구성요소: Entity(데이터 클래스), DAO(CRUD 쿼리 정의), Database(DB 인스턴스)
    - 특징
        - DB는 각 디바이스에 로컬로 생성 

        - 즉, Room이 디바이스 내 앱 전용 저장소에 자동으로 SQLite 기반 DB 파일(.db)을 생성하고 저장
        - Room은 외부 앱에서 접근 불가능한 보안된 공간에 저장됨 -> 안전성 높음
        - 단, 앱을 삭제하면 내부 저장소에 있던 DB도 같이 삭제됨
        - 오프라인 저장 중심 앱에서 대부분 사용
    - 설치 방법
        - build.gradle 확인
            1) dependencies 블록 안에 추가
                dependencies {
                    --------------JAVA--------------
                    implementation "androidx.room:room-runtime:2.6.1"
                    annotationProcessor "androidx.room:room-compiler:2.6.1" // Java는 annotationProcessor 사용
                    implementation "androidx.lifecycle:lifecycle-viewmodel:2.7.0" // ViewModel
                    implementation "androidx.lifecycle:lifecycle-livedata:2.7.0" // LiveData
                    --------------Kotlin--------------
                    // Room (Java용)
                    implementation("androidx.room:room-runtime:2.6.1")
                    annotationProcessor("androidx.room:room-compiler:2.6.1")
                    implementation("androidx.lifecycle:lifecycle-viewmodel:2.7.0") // ViewModel
                    implementation("androidx.lifecycle:lifecycle-livedata:2.7.0") // LiveData
                }
            2) android 버전 확인 (java 8 미만일 경우에만 추가)
                android {
                    ...
                    compileOptions {
                        sourceCompatibility JavaVersion.VERSION_1_8
                        targetCompatibility JavaVersion.VERSION_1_8
                    }
                }

build.gardle.kts 코드 수정 및 버전 확인

...
android {
    ...
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
}

dependencies {
    ...
    // Room (Java용)
    implementation("androidx.room:room-runtime:2.6.1")
    annotationProcessor("androidx.room:room-compiler:2.6.1")
    // ViewModel + LiveData
    implementation("androidx.lifecycle:lifecycle-viewmodel:2.7.0")
    implementation("androidx.lifecycle:lifecycle-livedata:2.7.0")
}

 

Todo.java(Entity) 코드 수정

...
@Entity(tableName = "todo_table")
public class Todo {

    @PrimaryKey(autoGenerate = true) // 기본키, 자동 증가
    private int id;
    private String content;
    private boolean isDone;

    // 생성자
    public Todo(String content, boolean isDone) {
        this.content = content;
        this.isDone = isDone;
    }

    // getter, setter
    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    
    public String getContent() { return content; }
    public void setContent(String content) { this.content = content; }

    public boolean isDone() { return isDone; }
    public void setDone(boolean done) { isDone = done; }
}

 

TodoDAO.java 코드

package com.example.todolist.data;

import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import androidx.room.Delete;

import java.util.List;

@Dao
public interface TodoDAO {
    @Insert
    void insert(Todo todo);
    @Update
    void update(Todo todo);
    @Delete
    void delete(Todo todo);
    @Query("SELECT * FROM todo_table ORDER BY id DESC")
    LiveData<List<Todo>> getAllTodos(); // 실시간으로 변경되는 데이터 감지
}

 

TodoDatabase.java 코드

package com.example.todolist.data;

import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;

@Database(entities = {Todo.class}, version = 1) // Entity 설정
public abstract class TodoDatabase extends RoomDatabase {
    private static TodoDatabase instance;
    public abstract TodoDAO todoDAO();
    public static synchronized TodoDatabase getInstance(Context context) {
        if (instance == null) {
            instance = Room.databaseBuilder(context.getApplicationContext(),
                            TodoDatabase.class, "todo_database")
                            .fallbackToDestructiveMigration() // 버전 바뀌었을 때 DB 날리고 다시 생성 (초기 개발 단계에서 사용)
                            .build();
        }
        return instance;
    }
}

 

TodoRepository.java 코드

package com.example.todolist.repository;

import android.app.Application;
import android.util.Log;

import androidx.lifecycle.LiveData;

import com.example.todolist.data.Todo;
import com.example.todolist.data.TodoDAO;
import com.example.todolist.data.TodoDatabase;

import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

// ViewModel과 DB 사이의 중간 다리 역할
public class TodoRepository {

    private TodoDAO todoDao;
    private LiveData<List<Todo>> allTodos;

    // 백그라운드에서 실행할 Executor
    private final ExecutorService executorService = Executors.newSingleThreadExecutor();

    public TodoRepository(Application application) {
        // DB 인스턴스 생성
        TodoDatabase db = TodoDatabase.getInstance(application);
        todoDao = db.todoDAO();
        allTodos = todoDao.getAllTodos();
    }

    public LiveData<List<Todo>> getAllTodos() { return allTodos; }
    // 추가
    public void insert(Todo todo) { executorService.execute(() -> todoDao.insert(todo)); }
    // 수정
    public void update(Todo todo) { executorService.execute(() -> todoDao.update(todo)); }
    // 삭제
    public void delete(Todo todo) { executorService.execute(() -> todoDao.delete(todo)); }
}

 


실제 데이터 확인

현재 기기에 저장된 DB 확인 방법

1. Device File Explorer (Android Studio) 사용하여 db 파일 추출 후 *전용 프로그램을 통해 확인

2. 앱에서 .db 파일을 Export 하도록 직접 코드 구현 후 *전용 프로그램을 통해 확인

3. ADB 명령어로 추출 후 *전용 프로그램을 통해 확인

*전용 프로그램: DB Browser for SQLite, VSCode SQLite 플러그인, Android Studio Database Inspector (API 26+) 등

 

3개 전부 다 해봤는데 오류가 나지 않았던 2번+DB Browser for SQLite로 확인했습니다.

  • 1번 실패 이유: 아래 사진과 같이 Error가 뜸

  • 3번 실패 이유: 권한 문제로 .db 파일 복사 불가능

2번 방법 (.db 파일을 외부에 저장할 수 있도록 직접 코드 구현)

1) TodoDatabase.java에 WAL 비활성화 코드(.setJournalMode()) 추가

  • 비활성화 이유: Room은 기본적으로 WAL 모드를 사용하는데,  WAL 모드를 쓰면 .db 파일뿐만 아니라 -wal, -shm 파일까지 다 복사하거나 Room의 커밋 타이밍을 명시적으로 제어해야 함 -> 테스트나 디버깅이 복잡해짐
...
public abstract class TodoDatabase extends RoomDatabase {
    ...
            instance = Room.databaseBuilder(context.getApplicationContext(),
                            TodoDatabase.class, "todo_database")
                            // WAL 비활성화 (빠른 확인용, 개발 시에만 적용, 실제 배포할 때에는 WAL 활성화하기)
                            .setJournalMode(RoomDatabase.JournalMode.TRUNCATE) // 추가
    ...
}

 

2) activity_main.xml에 DB 내보내기 버튼 추가 (+ 레이아웃 조정)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="40dp" // margin 수정
        >
    	...
        <Button // 추가
            android:id="@+id/buttonExport"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="DB 내보내기"/>
    </LinearLayout>
    ...
</LinearLayout>

activity_main.xml 화면

3) MainActivity.java에 exportDatabase 함수 추가

...
public class MainActivity extends AppCompatActivity {
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        // DB 내보내기 버튼 클릭 시 실행
        Button btnExport = findViewById(R.id.buttonExport); // 추가
        btnExport.setOnClickListener(v -> exportDatabase(MainActivity.this)); // 추가
    }

    // 추가
    // DB 데이터 내보내는 메소드
    public void exportDatabase(Context context) {
        File dbFile = context.getDatabasePath("todo_database");  // ← 실제 DB 이름
        File exportDir = context.getExternalFilesDir(null); // 앱 외부 저장소

        if (exportDir != null && !exportDir.exists()) {
            exportDir.mkdirs();
        }

        File outFile = new File(exportDir, "todo_database_copy.db");

        try (FileChannel src = new FileInputStream(dbFile).getChannel();
             FileChannel dst = new FileOutputStream(outFile).getChannel()) {
            dst.transferFrom(src, 0, src.size());
            Log.d("EXPORT", "DB exported to: " + outFile.getAbsolutePath());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

4) .db 파일을 찾아 전용 프로그램을 통해 확인

- 파일 경로: 각자 테스트한 기기 or 에뮬레이터 폴더 -> Android/data/com.example.todolist/files/todo_database_copy.db

실제 데이터 확인

테스트가 끝나고 배포 직전에 전부 삭제하기 ★

 

나중에는 테스트 자동화로 바꿔볼 계획입니다.

다음 글에서는 삭제, 수정 기능을 추가하겠습니다.

(삭제 기능은 이미 개발한 상태라 실제 데이터 확인 예시 사진에 버튼이 있어요ㅎ.ㅎ

글이 너무 길어져서 수정 기능까지 넣고 같이 써볼게요)

주요 기능 개발을 시작했습니다.

*ChatGPT의 도움을 받아 개발하였습니다.*

간단한 UI 구성

activity_main.xml 코드

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="10dp">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_margin="5dp">
        <EditText
            android:id="@+id/editTextTodo"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="할 일을 입력하세요"
            android:textAlignment="center"
            android:layout_weight="1"
            android:layout_marginRight="10dp"/>
        <Button
            android:id="@+id/buttonAdd"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#20B2AA"
            android:text="추가"
            android:layout_weight="2"/>
    </LinearLayout>
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerViewTodos"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </androidx.recyclerview.widget.RecyclerView>
</LinearLayout>

activity_main.xml 화면

 

item_todo.xml 코드

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="12dp">
    <CheckBox
        android:id="@+id/checkBoxDone"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <TextView
        android:id="@+id/textViewContent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="할 일 텍스트 나올 곳"
        android:textSize="16sp"
        android:layout_marginStart="8dp" />
</LinearLayout>

item_todo.xml 화면

 

Todo.java (Entity) 생성

Todo.java 코드

package com.example.todolist.data;

public class Todo {
    private String content; // 할 일 텍스트
    private boolean done; // 할 일 끝냈는지 여부

    public Todo(String content, boolean done) {
        this.content = content;
        this.done = done;
    }

    public String getContent() { return content; }
    public boolean isDone() { return done; }
    public void setDone(boolean done) { this.done = done; }
}

 

Adapter, MainActivity 연결 (일단 Room 사용X)

TodoAdapter.java 코드

package com.example.todolist.ui;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import com.example.todolist.R;
import com.example.todolist.data.Todo;

import java.util.List;
public class TodoAdapter extends RecyclerView.Adapter<TodoAdapter.TodoViewHolder> {
    private List<Todo> todoList; // 할 일 목록 데이터 리스트

    // 생성자: 데이터 받기
    public TodoAdapter(List<Todo> todoList) {
        this.todoList = todoList;
    }

    // ViewHolder 정의 (아이템 하나당 뷰 참조를 저장)
    public static class TodoViewHolder extends RecyclerView.ViewHolder {
        CheckBox checkBox;
        TextView textView;

        public TodoViewHolder(@NonNull View view) {
            super(view);
            checkBox = view.findViewById(R.id.checkBoxDone); // 체크박스 데이터 가져오기
            textView = view.findViewById(R.id.editTextTodo); // 할 일 텍스트 데이터 가져오기
        }

        //bind() 함수는 해당 줄의 Todo 데이터를 화면에 세팅
        public void bind(Todo todo) {
            textView.setText(todo.getContent());
            checkBox.setChecked(todo.isDone());
        }
    }

    // 아이템 레이아웃을 처음 생성할 때 호출
    @NonNull
    @Override
    public TodoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_todo, parent, false);
        return new TodoViewHolder(view);
    }

    // RecyclerView가 특정 위치의 데이터를 화면에 표시할 때 호출
    @Override
    public void onBindViewHolder(@NonNull TodoViewHolder holder, int position) {
        Todo todo = todoList.get(position);
        holder.bind(todo);
    }

    // 아이템이 몇 개 있는지 알려줌
    @Override
    public int getItemCount() {
        return todoList != null ? todoList.size() : 0;
    }

    // 리스트 업데이트용 메소드
    // ViewModel이나 Repository에서 새 데이터를 받아올 때 호출
    public void setTodoList(List<Todo> list) {
        this.todoList = list;
        notifyDataSetChanged(); // 데이터 바뀐 걸 알려줌
    }
}

 

MainActivity.java 코드

package com.example.todolist.ui;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.example.todolist.R;
import com.example.todolist.data.Todo;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    private EditText editTextTodo;
    private Button buttonAdd;
    private RecyclerView recyclerView;
    private TodoAdapter adapter;

    // 메모리 내 임시 리스트 (Room 없이)
    private List<Todo> todoList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main); // activity_main 연결

        // UI 컴포넌트 연결
        editTextTodo = findViewById(R.id.editTextTodo);
        buttonAdd = findViewById(R.id.buttonAdd);
        recyclerView = findViewById(R.id.recyclerViewTodos);

        // RecyclerView 설정
        adapter = new TodoAdapter(todoList);
        recyclerView.setAdapter(adapter);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));

        // 추가 버튼 클릭 이벤트
        buttonAdd.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String content = editTextTodo.getText().toString().trim();
                if (!content.isEmpty()) {
                    // 새 할 일 객체 추가
                    Todo todo = new Todo(content, false);
                    todoList.add(todo);
                    // RecyclerView 갱신
                    adapter.notifyItemInserted(todoList.size() - 1);
                    // 입력창 비우기
                    editTextTodo.setText("");
                }
            }
        });
    }
}

 

NullPointerException 오류 해결

이렇게 코드를 작성하고 테스트 해보면 오류가 납니다.

오류: 추가 버튼을 누르면 NullPointerException이 뜨면서 앱이 종료됨

그 이유는 Adapter 코드에서 찾아볼 수 있는데요, Adapter는 우리가 입력한 텍스트와 체크박스 데이터를 item_todo.xml에 적용 후 Recyclerview에 연결시키는 것입니다.

즉, MainActivity.java에서 저장한 Todo 데이터를 가져와 item_todo.xml에 입력 후 Recyclerview에 보여주는 것이죠.

        public TodoViewHolder(@NonNull View view) {
            super(view);
            checkBox = view.findViewById(R.id.checkBoxDone); // 체크박스 데이터 가져오기
            textView = view.findViewById(R.id.editTextTodo); // 할 일 텍스트 데이터 가져오기
        }

 

위의 코드를 아래와 같이 바꿔야 합니다.

        public TodoViewHolder(@NonNull View view) {
            super(view);
            checkBox = view.findViewById(R.id.checkBoxDone); // item_todo.xml의 CheckBox 객체
            textView = view.findViewById(R.id.textViewContent); // item_todo.xml의 TextView 객체
        }

 

결과

이렇게 바꾸면 화면에 잘 나타나게 됩니다.

실제 실행 화면

 

다음에는 Room을 이용하여 DB 연결하고, UI 조정을 해보겠습니다.

개요

"할 일 목록 추가/삭제/완료 + 데이터가 RoomDB에 영구 저장되는 구조"

사용할 기술

  • MVVM 아키텍처 패턴
  • RoomDB 사용
  • LiveData + ViewModel
  • RecyclerView

폴더 구조

com.example.todolist
│
├── data/                   ← 데이터 모델 + DB 관련
│   ├── Todo.java           ← @Entity 클래스
│   ├── TodoDao.java        ← DAO 인터페이스
│   └── TodoDatabase.java    ← RoomDatabase 구현
│
├── repository/             ← 데이터 중개자 (Repository)
│   └── TodoRepository.java
│
├── ui/                     ← UI 관련
│   ├── MainActivity.java
│   ├── MainViewModel.java
│   └── TodoAdapter.java
│
└── layout/                 ← 레이아웃 XML 파일
    ├── activity_main.xml
    └── item_todo.xml

각 파일 역할

파일명 역할
Todo.java 할 일 Entity, Room 테이블 역할
TodoDao.java DB 쿼리 인터페이스 (Insert, Delete, Select)
TodoDatabase.java Room DB 클래스 (싱글톤 패턴)
TodoRepository.java ViewModel에 데이터 제공, DB 작업 중계
MainViewModel.java UI 상태 및 데이터 LiveData로 관리
MainActivity.java UI 초기화 및 사용자 이벤트 처리
TodoAdapter.java RecyclerView Adapter, ViewHolder 포함
activity_main.xml 메인 화면 레이아웃 (RecyclerView 포함)
item_todo.xml 리스트 한 줄 아이템 레이아웃

동작 흐름

더보기

사용자 UI 이벤트 (MainActivity)
   ↓
ViewModel 호출 (MainViewModel)
   ↓
Repository로 데이터 요청 (TodoRepository)
   ↓
DAO 통해 DB 작업 (TodoDao)
   ↓
Room DB 반영 (TodoDatabase)
   ↓
LiveData를 통해 ViewModel에 데이터 반영
   ↓
MainActivity가 LiveData 옵저빙 → RecyclerView 갱신

+ Recent posts