
안드로이드 Room
회사 업무를 진행하면서 부득이하게 안드로이드 개발을 하게 되었습니다.
내부 데이터를 관리하기 위하여 SQLite를 사용하기로 하였고 오래전 학부생때나 만져본 기억을 더듬어 사용하고 있었는데 SQLite를 편하게 쓸 수 있는 ORM이 있다는걸 알게 되었고 이를 적용해보고 정말 편하다는 생각이 들었습니다.
안드로이드 개발에서 데이터베이스 관리는 중요한 요소입니다.
특히, 로컬 데이터베이스를 효율적으로 다루기 위한 ORM 도구로 Room이 주목받는 이유가 무엇이었는지 조금이라도 공유하고자 글을 써봅니다.
예제는 Java 코드로 제공 합니다.
1. ORM이란?
ORM은 'Object-Relational Mapping(객체-관계 매핑)'의 약자입니다.
객체 지향 프로그래밍 언어와 관계형 데이터베이스 간의 데이터를 매핑해주는 기술입니다.
이를 통해 개발자는 SQL 쿼리를 직접 작성하지 않고도 객체를 통해 데이터베이스를 조작할 수 있습니다.
ORM의 주요 장점은 코드의 가독성을 높이고, 생산성을 향상시킨다는 점입니다.
대표적인 ORM 프레임워크로는 Hibernate, JPA 등이 있으며, 안드로이드에서는 Room이 이에 해당합니다.
2. Room이란?
Room은 안드로이드에서 제공하는 ORM 라이브러리입니다.
이는 SQLite를 기반으로 동작하며, 복잡한 쿼리 작성 없이도 데이터베이스 작업을 수행할 수 있게 해줍니다.
Room은 Google이 제공하는 Jetpack 라이브러리 중 하나로, 안드로이드 개발의 표준으로 자리 잡았습니다.
다른 프레임워크와 비교해보면, 스프링 프레임워크의 JPA와 유사한 점이 많습니다.
JPA는 Java 객체와 데이터베이스 간 매핑을 처리하며, Room도 안드로이드에서 동일한 역할을 합니다.
Room은 쿼리 검증을 컴파일 타임에 수행하여 런타임 오류를 줄이는 장점이 있습니다.
3. 안드로이드 AndroidX Jetpack과의 연관성
Room은 AndroidX Jetpack 라이브러리의 일부입니다.
Jetpack은 안드로이드 개발을 간소화하고, 일관성 있는 코드를 작성할 수 있도록 도와주는 도구 모음입니다.
Room 외에도 LiveData, ViewModel 같은 컴포넌트와 통합되어 반응형 데이터 흐름을 지원합니다.
예를 들어, Room에서 조회한 데이터를 LiveData로 감싸면 UI가 데이터 변경에 실시간으로 반응할 수 있습니다.
이는 Jetpack의 핵심 철학인 '권장 아키텍처'를 따르는 설계입니다.
처음엔 아무것도 모르고 웹의 백엔드 서버처럼 데이터베이스를 조회하여 바로 사용하다 메인 쓰레드 침범 오류가 발생하여 당황한게 기억이 나네요.
4. 작동 방식 및 구조
Room은 SQLite를 기반으로 동작하며, 개발자가 직접 SQL 쿼리를 작성하거나 SQLiteOpenHelper를 사용할 필요 없이 객체 지향적으로 데이터베이스를 다룰 수 있게 설계되었습니다.
Room의 작동은 크게 세 가지 구성 요소(Entity, DAO, Database)를 중심으로 이루어집니다.
- Entity : 데이터베이스의 테이블을 나타냅니다. 각 Entity 클래스는 테이블의 구조를 정의하며, 필드는 테이블의 열(column)에 매핑됩니다. 각 Entity 클래스는 SQLite의 테이블로 변환됩니다.
- DAO(Data Access Object) : 데이터베이스 작업을 위한 메서드를 정의합니다. @Insert, @Query, @Update, @Delete 등의 어노테이션으로 쿼리를 정의하면, Room은 이를 SQLite 쿼리로 변환합니다.
- Database : Room 데이터베이스의 추상 계층입니다. Entity와 DAO를 연결하며, 앱에서 사용할 데이터베이스 인스턴스를 생성합니다. 데이터베이스 전체를 관리하며 내부적으로 SQLite 데이터베이스 파일을 생성하고 접근합니다.
Room은 컴파일 타임에 DAO의 쿼리를 검증하고, SQLite와의 상호작용을 래핑하여 런타임 오류를 최소화합니다.
예를 들어, 잘못된 쿼리를 작성하면 빌드 시 오류를 발생시켜 문제를 조기에 발견할 수 있습니다.
이를 통해 개발자는 안전하고 효율적인 데이터베이스 작업을 수행할 수 있습니다.
Room은 Jetpack의 라이프사이클 인식 컴포넌트와 긴밀히 연계됩니다. 이를 통해 데이터베이스 작업이 앱의 생명주기와 조화를 이루도록 설계되었습니다.
아래 설정 항목을 통해 좀 더 쉽게 이해할 수 있을 것 같습니다.
5. Database, Entity, Service, DAO, Repository, DTO 의 역할
제 경우 Room(SQLite)를 이용할 때 파일 형식은 아래와 같이 구성하였습니다.
각 구성 요소의 역할은 다음과 같습니다.
Entity
데이터베이스 테이블을 정의합니다.
@Entity 어노테이션을 사용하여 클래스를 테이블로 지정하며, @PrimaryKey로 기본 키를 설정합니다.
주로 사용하는 어노테이션은 아래와 같습니다.
어노테이션 | 설명 | 예시 |
@Entity | - SQLite 테이블 정의 - 기본적으로 클래스 이름이 테이블 이름이 되지만 명시적으로 변경 가능 |
@Entity(tableName = "users") public class User { .... } 외래키 및 Index 처리시 @Entity로 처리 하단 코드 참고 |
@PrimaryKey | - 기본 키 설정 - autoGenerate = true 시 자동증가ID로 됨 |
@PrimaryKey(autoGenerate = true) private int id; |
@ColumnInfo | - 컬럼 이름 지정 | @ColumnInfo(name = "user_name") private String name; |
@Ignore | - Room에서 저장되지 않을 필드 지정 | @Ignore public String test; |
@Embedded | - 객체 안에 객체를 포함할 때 사용 | @Embedded public Information info; |
@NotNull | - Not Null 처리 | @NotNull public String name; |
# 외래키 예제
@Entity(tableName = "orders",
foreignKeys = @ForeignKey(
entity = User.class,
parentColumns = "id",
childColumns = "user_id",
onDelete = ForeignKey.CASCADE))
public class Order {
@PrimaryKey(autoGenerate = true)
private int orderId;
private int userId;
public int getOrderId() { return orderId; }
public void setOrderId(int orderId) { this.orderId = orderId; }
public int getUserId() { return userId; }
public void setUserId(int userId) { this.userId = userId; }
}
# 인덱스 예제 (B-tree 처리로 확인)
@Entity(
tableName = "SYSTEM",
indices = {
@Index(value = {"systemKey"}, unique = true),
@Index(value = {"systemKey", "systemValue"}, name = "composite_index")
}
)
public class SystemEntity {
...
}
Database
데이터베이스의 전체 구조를 관리합니다.
@Database 어노테이션으로 정의되며, 포함할 Entity와 버전 정보를 명시합니다.
주로 사용하는 어노테이션은 아래와 같습니다.
어노테이션 | 설명 | 예시 |
@Database | - Room 데이터베이스 정의 - entities에 포함할 테이블을 지정 - version 지정시 올릴 경우 마이그레이션 필요 (아직 안해봄) |
@Database(entities = {User.class}, version = 1) public abstract class AppDatabase extends RoomDatabase { ... } |
DAO
데이터베이스 작업의 인터페이스를 제공합니다.
@Insert, @Query 같은 어노테이션으로 메서드를 정의하며, SQL 쿼리를 실행합니다.
주로 사용하는 어노테이션은 아래와 같습니다.
어노테이션 | 설명 | 예시 |
@Dao | - 데이터 접근 객체 선언 | @Dao public interface UserDao { .... } |
@Query | - 사용자 정의 SQL 쿼리를 작성 | @Query("SELECT * FROM users") List<User> getAllUsers(); |
@Insert | - 데이터 삽입 - int 반환 시 아이디값 반환 |
@Insert long insertUser(User user); @insert long[] insertUsers(List<UserEntity> user); // 키 충돌시 덮어씀 @Insert(onConflict = OnConflictStrategy.REPLACE)
// 키 충돌시 무시 (SKIP 처리) @Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Update
|
- 데이터 수정 - int 반환 시 업데이트 수 확인 |
@Update int updateUser(User user); @Update int[] updateUser(List<UserEntity> user); |
@Delete
|
- 데이터 삭제 - int 반환 시 삭제 수 확인 |
@Delete int deleteUser(User user); @Delete int[] deleteUsers(List<UserEntity> user); |
@Transaction | - 여러 데이터베이스 작업을 하나의 트랜잭션으로 묶음 - 작업이 모두 성공하거나 실패하도록 보장하며, 데이터 일관성을 유지 |
하단 코드 |
@RawQuery | - 동적으로 생성된 SQL 쿼리를 실행 - SupportSQLiteQuery 객체를 사용 |
@RawQuery List<User> getUsersByRQ(SupportSQLiteQuery query); |
SupportSQLiteQuery 객체 사용 예시
String rawSql = "SELECT * FROM users WHERE age > ? AND name = ?"; SupportSQLiteQuery query = new SimpleSQLiteQuery(rawSql, new Object[]{20, "홍길동"});
# Transcation 예시
Dao
public interface UserDao {
@Transaction
void insertAndUpdate(User user, String newName) {
insertUser(user);
updateUserName(user.getId(), newName);
}
@Insert
void insertUser(User user);
@Query("UPDATE users SET name = :newName WHERE id = :userId")
void updateUserName(int userId, String newName);
}
출처 : Grok3
DTO
Data Transfer Object(데이터 전송 객체)
데이터베이스, 네트워크, 또는 UI와 같은 서로 다른 계층 간의 데이터를 주고받을 때 사용됩니다.
DTO의 주요 목적은 데이터를 단순화하고, 필요한 정보만 전달하여 계층 간 결합도를 낮추는 것입니다.
쉽게말해 SQLite DAO를 통해 받을 데이터 중 필요한 데이터만 Class 화 하여 객체를 만들어 전달하기 위해 사용하는게 DTO라고 이해하시면 편합니다.
어노테이션 | 설명 | 예시 |
@Relation | - 1:N 관계를 표현할 때 사용 | 하단 코드 |
# @Relation 예제
@Entity
public class User {
@PrimaryKey
public int userId;
public String name;
}
@Entity
public class Post {
@PrimaryKey
public int postId;
public int userId; // User 테이블과 관계를 맺는 외래 키
public String content;
}
-------------------------------------------
public class UserWithPosts {
@Embedded
public User user;
@Relation(
parentColumn = "userId",
entityColumn = "userId"
)
public List<Post> posts;
}
출처 : chatGPT
Repository
데이터 소스와 비즈니스 로직을 분리합니다.
Room의 DAO를 호출하여 데이터를 관리, 중간 계층 역할을 합니다.
사실 굳이 안해도 되는 계층이긴 하지만, 단일 책임 원칙에 따라 데이터 소스 관리와 비지니스 로직을 더욱 분리시켜 계층에서의 많은 책임을 줄여줄 수 있도록 하기 위해 사용됩니다.
Google 안드로이드 아키텍쳐 가이드에 Repository를 데이터 게층으로 권장한다고 하여 사용하였습니다.
빠른 프로토타입 개발이나 복잡한 비즈니스 로직이 필요없는 경우라면 SKIP해도 될 것 같습니다.
레포지토리에서는 따로 어노테이션을 사용하지 않습니다. DAO를 감싸서 서비스계층에 전달하는 역할만 하니까요.
Service
네트워크 요청과 같은 외부 데이터 소스를 다룰 때 주로 사용됩니다.
Room은 로컬 데이터베이스에 초점을 맞추므로, Service는 보조적으로 활용됩니다.
6. 간단한 설정 및 사용 방법
본 사용 예시는 안드로이드 DI 관련 라이브러리를 사용하지 않은 예제입니다.
디펜던시 추가 ( gradle.kts 기준 )
dependencies {
....
// room (sqlite)
implementation("androidx.room:room-runtime:2.6.1")
annotationProcessor("androidx.room:room-compiler:2.6.1")
}
엔티티 생성
@Entity(tableName = "CLIENT")
public class ClientEntity {
@PrimaryKey(autoGenerate = true)
private Integer clientId;
@NotNull
private String clientName;
@NotNull
private String ip;
// 생성자, getter setter 정의는 생략
}
DAO 생성
@Dao
public interface ClientDao {
// 예시라 DTO 안만들고 그냥 Entity로 처리했지만 필요에 따라 DTO를 구현하여 처리하시면 됩니다.
@Insert
long insert(ClientEntity clientEntity);
@Update
void update(ClientEntity clientEntity);
@Delete
void delete(ClientEntity clientEntity);
@Query("SELECT * FROM CLIENT WHERE clientId = :clientId")
LiveData<ClientEntity> getClientById(int clientId);
@Query("SELECT * FROM CLIENT")
LiveData<ClientEntity[]> getAllLive();
@Query("SELECT * FROM CLIENT")
ClientEntity[] getAll();
@Query("DELETE FROM CLIENT")
int deleteAll();
}
레포지토리 생성
public class ClientRepository {
private ClientDao clientDao;
public ClientRepository(AppDatabase database) {
this.clientDao = database.clientDao();
}
public LiveData<ClientEntity[]> getAllLive() {
return clientDao.getAllLive();
}
public ClientEntity[] getAll() {
return clientDao.getAll();
}
public long insertClient(ClientEntity client){
return clientDao.insert(client);
}
public int deleteAll(){
return clientDao.deleteAll();
}
public LiveData<ClientEntity> getClientById(int id) {
return clientDao.getClientById(id);
}
서비스 생성
public class ClientService {
private final ClientRepository clientRepository;
public ClientService(Context context) {
AppDatabase db = DatabaseManager.getInstance(context);
this.clientRepository = new ClientRepository(db);
}
public LiveData<ClientEntity[]> getAllLive() {
return clientRepository.getAllLive();
}
public ClientEntity[] getAllSystems() {
return clientRepository.getAll();
}
public long insertClient(ClientEntity client){
return clientRepository.insertClient(client);
}
// 나머진 생략..
}
타입 변환기 생성
위 변환기는 결과에 대한 부분이 List인걸 Entity로 다시 넣어주는 경우에 사용합니다.
본 예시에는 Entity에 List가 없으므로 사실 무관하지만 추후에 필요할 듯 싶어 작성합니다.
Entity 클래스 상단 어노테이션으로 @TypeConverters를 기입하여 특정 타입컨버터스를 지정할 수 있습니다.
예) @TypeConverters(CustomConverter.class)
// Room은 List<String> 같은 복잡한 타입을 지원하지 않습니다. String으로 바꿔줘야 합니다.
// @TypeConverters를 사용하면 사용자 정의 변환기를 적용할 수 있습니다.
import androidx.room.TypeConverter;
import java.util.Arrays;
import java.util.List;
public class Converters {
@TypeConverter
public static String fromList(List<String> value) {
return String.join(",", value); // 리스트 → 문자열 변환
}
@TypeConverter
public static List<String> toList(String value) {
return Arrays.asList(value.split(",")); // 문자열 → 리스트 변환
}
}
꼭 Entity가 아니여도 사용 할 수 있으며 사용 범위는 다음과 같습니다.
- Entity 클래스에 적용 : 해당 Entity 클래스 내의 필드에만 타입 변환기가 적용됩니다. 다른 Entity나 DAO에는 영향을 주지 않습니다.
- Database 클래스에 적용 : 데이터베이스 전체에 타입 변환기가 적용됩니다. 모든 Entity와 DAO에서 해당 변환기를 사용할 수 있습니다.
- DAO 클래스에 적용 : 해당 DAO의 쿼리와 메서드에만 타입 변환기가 적용됩니다.
- Entity 특정 필드에 적용 : Entity 내의 특정 필드에 변환기를 적용할 수 있습니다(필드 수준에서 사용).
데이터베이스 객체 생성
// entities 안에 본인이 구현한 EntityClass를 쉼표기준 등록
@Database(entities = {ClientEntity.class, ContentsEntity.class}, version = 1)
@TypeConverters(Converters.class) // 변환기 적용
public abstract class AppDatabase extends RoomDatabase {
// 본인이 구현한 Dao 등록
public abstract ClientDao clientDao();
public static final String DATABASE_NAME = "{본인 디비명}";
}
사용
// DI 라이브러리 쓰지 않아 명시적으로 객체 생성
ClientService clientService = new ClientService(this.context);
// 객체 생성
ClientEntity ce = new ClientEntity("클라이언트ID","클라이언트이름","127.0.0.1");
long ret = clientService.insert(ce);
마무리
사실 저도 사용한지 하루 되었습니다.
물론 더 깊이 배우고 다뤄봐야 할 부분은 많겠지만, 하루 업무 시간 동안에도 충분히 익히고 사용할 수 있을 정도로 매우 편리하게 개발되어 있는 라이브러리임을 확인하였습니다.
Room은 복잡한 SQLite 코드를 직접 작성하지 않아도 되며, 직관적인 어노테이션과 객체 지향적인 설계로 데이터베이스 작업을 간소화해줍니다. 특히, 컴파일 타임에 쿼리를 검증해주는 기능은 실수를 줄이고 안정적인 앱 개발을 가능하게 합니다.
처음 접하는 개발자라도 기본적인 설정과 사용법을 빠르게 익힐 수 있다는 점이 큰 장점입니다.
또한, Jetpack과의 통합성과 확장성을 고려하면, Room은 안드로이드 개발에서 필수적인 도구로 자리 잡을 가능성이 높습니다.
앞으로 더 많은 기능을 탐구하고, 실제 프로젝트에 적용해보면서 Room의 진가를 더욱 체감할 수 있을 것이라 기대합니다.
여러분도 Room을 사용해보시면, 저처럼 단 하루만에 그 편리함에 놀라실 겁니다.
안드로이드에서 데이터베이스 관리가 고민이셨던 분이라면, 지금 바로 Room을 시작해보시는 것을 추천드립니다.

안드로이드 Room
회사 업무를 진행하면서 부득이하게 안드로이드 개발을 하게 되었습니다.
내부 데이터를 관리하기 위하여 SQLite를 사용하기로 하였고 오래전 학부생때나 만져본 기억을 더듬어 사용하고 있었는데 SQLite를 편하게 쓸 수 있는 ORM이 있다는걸 알게 되었고 이를 적용해보고 정말 편하다는 생각이 들었습니다.
안드로이드 개발에서 데이터베이스 관리는 중요한 요소입니다.
특히, 로컬 데이터베이스를 효율적으로 다루기 위한 ORM 도구로 Room이 주목받는 이유가 무엇이었는지 조금이라도 공유하고자 글을 써봅니다.
예제는 Java 코드로 제공 합니다.
1. ORM이란?
ORM은 'Object-Relational Mapping(객체-관계 매핑)'의 약자입니다.
객체 지향 프로그래밍 언어와 관계형 데이터베이스 간의 데이터를 매핑해주는 기술입니다.
이를 통해 개발자는 SQL 쿼리를 직접 작성하지 않고도 객체를 통해 데이터베이스를 조작할 수 있습니다.
ORM의 주요 장점은 코드의 가독성을 높이고, 생산성을 향상시킨다는 점입니다.
대표적인 ORM 프레임워크로는 Hibernate, JPA 등이 있으며, 안드로이드에서는 Room이 이에 해당합니다.
2. Room이란?
Room은 안드로이드에서 제공하는 ORM 라이브러리입니다.
이는 SQLite를 기반으로 동작하며, 복잡한 쿼리 작성 없이도 데이터베이스 작업을 수행할 수 있게 해줍니다.
Room은 Google이 제공하는 Jetpack 라이브러리 중 하나로, 안드로이드 개발의 표준으로 자리 잡았습니다.
다른 프레임워크와 비교해보면, 스프링 프레임워크의 JPA와 유사한 점이 많습니다.
JPA는 Java 객체와 데이터베이스 간 매핑을 처리하며, Room도 안드로이드에서 동일한 역할을 합니다.
Room은 쿼리 검증을 컴파일 타임에 수행하여 런타임 오류를 줄이는 장점이 있습니다.
3. 안드로이드 AndroidX Jetpack과의 연관성
Room은 AndroidX Jetpack 라이브러리의 일부입니다.
Jetpack은 안드로이드 개발을 간소화하고, 일관성 있는 코드를 작성할 수 있도록 도와주는 도구 모음입니다.
Room 외에도 LiveData, ViewModel 같은 컴포넌트와 통합되어 반응형 데이터 흐름을 지원합니다.
예를 들어, Room에서 조회한 데이터를 LiveData로 감싸면 UI가 데이터 변경에 실시간으로 반응할 수 있습니다.
이는 Jetpack의 핵심 철학인 '권장 아키텍처'를 따르는 설계입니다.
처음엔 아무것도 모르고 웹의 백엔드 서버처럼 데이터베이스를 조회하여 바로 사용하다 메인 쓰레드 침범 오류가 발생하여 당황한게 기억이 나네요.
4. 작동 방식 및 구조
Room은 SQLite를 기반으로 동작하며, 개발자가 직접 SQL 쿼리를 작성하거나 SQLiteOpenHelper를 사용할 필요 없이 객체 지향적으로 데이터베이스를 다룰 수 있게 설계되었습니다.
Room의 작동은 크게 세 가지 구성 요소(Entity, DAO, Database)를 중심으로 이루어집니다.
- Entity : 데이터베이스의 테이블을 나타냅니다. 각 Entity 클래스는 테이블의 구조를 정의하며, 필드는 테이블의 열(column)에 매핑됩니다. 각 Entity 클래스는 SQLite의 테이블로 변환됩니다.
- DAO(Data Access Object) : 데이터베이스 작업을 위한 메서드를 정의합니다. @Insert, @Query, @Update, @Delete 등의 어노테이션으로 쿼리를 정의하면, Room은 이를 SQLite 쿼리로 변환합니다.
- Database : Room 데이터베이스의 추상 계층입니다. Entity와 DAO를 연결하며, 앱에서 사용할 데이터베이스 인스턴스를 생성합니다. 데이터베이스 전체를 관리하며 내부적으로 SQLite 데이터베이스 파일을 생성하고 접근합니다.
Room은 컴파일 타임에 DAO의 쿼리를 검증하고, SQLite와의 상호작용을 래핑하여 런타임 오류를 최소화합니다.
예를 들어, 잘못된 쿼리를 작성하면 빌드 시 오류를 발생시켜 문제를 조기에 발견할 수 있습니다.
이를 통해 개발자는 안전하고 효율적인 데이터베이스 작업을 수행할 수 있습니다.
Room은 Jetpack의 라이프사이클 인식 컴포넌트와 긴밀히 연계됩니다. 이를 통해 데이터베이스 작업이 앱의 생명주기와 조화를 이루도록 설계되었습니다.
아래 설정 항목을 통해 좀 더 쉽게 이해할 수 있을 것 같습니다.
5. Database, Entity, Service, DAO, Repository, DTO 의 역할
제 경우 Room(SQLite)를 이용할 때 파일 형식은 아래와 같이 구성하였습니다.
각 구성 요소의 역할은 다음과 같습니다.
Entity
데이터베이스 테이블을 정의합니다.
@Entity 어노테이션을 사용하여 클래스를 테이블로 지정하며, @PrimaryKey로 기본 키를 설정합니다.
주로 사용하는 어노테이션은 아래와 같습니다.
어노테이션 | 설명 | 예시 |
@Entity | - SQLite 테이블 정의 - 기본적으로 클래스 이름이 테이블 이름이 되지만 명시적으로 변경 가능 |
@Entity(tableName = "users") public class User { .... } 외래키 및 Index 처리시 @Entity로 처리 하단 코드 참고 |
@PrimaryKey | - 기본 키 설정 - autoGenerate = true 시 자동증가ID로 됨 |
@PrimaryKey(autoGenerate = true) private int id; |
@ColumnInfo | - 컬럼 이름 지정 | @ColumnInfo(name = "user_name") private String name; |
@Ignore | - Room에서 저장되지 않을 필드 지정 | @Ignore public String test; |
@Embedded | - 객체 안에 객체를 포함할 때 사용 | @Embedded public Information info; |
@NotNull | - Not Null 처리 | @NotNull public String name; |
# 외래키 예제
@Entity(tableName = "orders",
foreignKeys = @ForeignKey(
entity = User.class,
parentColumns = "id",
childColumns = "user_id",
onDelete = ForeignKey.CASCADE))
public class Order {
@PrimaryKey(autoGenerate = true)
private int orderId;
private int userId;
public int getOrderId() { return orderId; }
public void setOrderId(int orderId) { this.orderId = orderId; }
public int getUserId() { return userId; }
public void setUserId(int userId) { this.userId = userId; }
}
# 인덱스 예제 (B-tree 처리로 확인)
@Entity(
tableName = "SYSTEM",
indices = {
@Index(value = {"systemKey"}, unique = true),
@Index(value = {"systemKey", "systemValue"}, name = "composite_index")
}
)
public class SystemEntity {
...
}
Database
데이터베이스의 전체 구조를 관리합니다.
@Database 어노테이션으로 정의되며, 포함할 Entity와 버전 정보를 명시합니다.
주로 사용하는 어노테이션은 아래와 같습니다.
어노테이션 | 설명 | 예시 |
@Database | - Room 데이터베이스 정의 - entities에 포함할 테이블을 지정 - version 지정시 올릴 경우 마이그레이션 필요 (아직 안해봄) |
@Database(entities = {User.class}, version = 1) public abstract class AppDatabase extends RoomDatabase { ... } |
DAO
데이터베이스 작업의 인터페이스를 제공합니다.
@Insert, @Query 같은 어노테이션으로 메서드를 정의하며, SQL 쿼리를 실행합니다.
주로 사용하는 어노테이션은 아래와 같습니다.
어노테이션 | 설명 | 예시 |
@Dao | - 데이터 접근 객체 선언 | @Dao public interface UserDao { .... } |
@Query | - 사용자 정의 SQL 쿼리를 작성 | @Query("SELECT * FROM users") List<User> getAllUsers(); |
@Insert | - 데이터 삽입 - int 반환 시 아이디값 반환 |
@Insert long insertUser(User user); @insert long[] insertUsers(List<UserEntity> user); // 키 충돌시 덮어씀 @Insert(onConflict = OnConflictStrategy.REPLACE)
// 키 충돌시 무시 (SKIP 처리) @Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Update
|
- 데이터 수정 - int 반환 시 업데이트 수 확인 |
@Update int updateUser(User user); @Update int[] updateUser(List<UserEntity> user); |
@Delete
|
- 데이터 삭제 - int 반환 시 삭제 수 확인 |
@Delete int deleteUser(User user); @Delete int[] deleteUsers(List<UserEntity> user); |
@Transaction | - 여러 데이터베이스 작업을 하나의 트랜잭션으로 묶음 - 작업이 모두 성공하거나 실패하도록 보장하며, 데이터 일관성을 유지 |
하단 코드 |
@RawQuery | - 동적으로 생성된 SQL 쿼리를 실행 - SupportSQLiteQuery 객체를 사용 |
@RawQuery List<User> getUsersByRQ(SupportSQLiteQuery query); |
SupportSQLiteQuery 객체 사용 예시
String rawSql = "SELECT * FROM users WHERE age > ? AND name = ?"; SupportSQLiteQuery query = new SimpleSQLiteQuery(rawSql, new Object[]{20, "홍길동"});
# Transcation 예시
Dao
public interface UserDao {
@Transaction
void insertAndUpdate(User user, String newName) {
insertUser(user);
updateUserName(user.getId(), newName);
}
@Insert
void insertUser(User user);
@Query("UPDATE users SET name = :newName WHERE id = :userId")
void updateUserName(int userId, String newName);
}
출처 : Grok3
DTO
Data Transfer Object(데이터 전송 객체)
데이터베이스, 네트워크, 또는 UI와 같은 서로 다른 계층 간의 데이터를 주고받을 때 사용됩니다.
DTO의 주요 목적은 데이터를 단순화하고, 필요한 정보만 전달하여 계층 간 결합도를 낮추는 것입니다.
쉽게말해 SQLite DAO를 통해 받을 데이터 중 필요한 데이터만 Class 화 하여 객체를 만들어 전달하기 위해 사용하는게 DTO라고 이해하시면 편합니다.
어노테이션 | 설명 | 예시 |
@Relation | - 1:N 관계를 표현할 때 사용 | 하단 코드 |
# @Relation 예제
@Entity
public class User {
@PrimaryKey
public int userId;
public String name;
}
@Entity
public class Post {
@PrimaryKey
public int postId;
public int userId; // User 테이블과 관계를 맺는 외래 키
public String content;
}
-------------------------------------------
public class UserWithPosts {
@Embedded
public User user;
@Relation(
parentColumn = "userId",
entityColumn = "userId"
)
public List<Post> posts;
}
출처 : chatGPT
Repository
데이터 소스와 비즈니스 로직을 분리합니다.
Room의 DAO를 호출하여 데이터를 관리, 중간 계층 역할을 합니다.
사실 굳이 안해도 되는 계층이긴 하지만, 단일 책임 원칙에 따라 데이터 소스 관리와 비지니스 로직을 더욱 분리시켜 계층에서의 많은 책임을 줄여줄 수 있도록 하기 위해 사용됩니다.
Google 안드로이드 아키텍쳐 가이드에 Repository를 데이터 게층으로 권장한다고 하여 사용하였습니다.
빠른 프로토타입 개발이나 복잡한 비즈니스 로직이 필요없는 경우라면 SKIP해도 될 것 같습니다.
레포지토리에서는 따로 어노테이션을 사용하지 않습니다. DAO를 감싸서 서비스계층에 전달하는 역할만 하니까요.
Service
네트워크 요청과 같은 외부 데이터 소스를 다룰 때 주로 사용됩니다.
Room은 로컬 데이터베이스에 초점을 맞추므로, Service는 보조적으로 활용됩니다.
6. 간단한 설정 및 사용 방법
본 사용 예시는 안드로이드 DI 관련 라이브러리를 사용하지 않은 예제입니다.
디펜던시 추가 ( gradle.kts 기준 )
dependencies {
....
// room (sqlite)
implementation("androidx.room:room-runtime:2.6.1")
annotationProcessor("androidx.room:room-compiler:2.6.1")
}
엔티티 생성
@Entity(tableName = "CLIENT")
public class ClientEntity {
@PrimaryKey(autoGenerate = true)
private Integer clientId;
@NotNull
private String clientName;
@NotNull
private String ip;
// 생성자, getter setter 정의는 생략
}
DAO 생성
@Dao
public interface ClientDao {
// 예시라 DTO 안만들고 그냥 Entity로 처리했지만 필요에 따라 DTO를 구현하여 처리하시면 됩니다.
@Insert
long insert(ClientEntity clientEntity);
@Update
void update(ClientEntity clientEntity);
@Delete
void delete(ClientEntity clientEntity);
@Query("SELECT * FROM CLIENT WHERE clientId = :clientId")
LiveData<ClientEntity> getClientById(int clientId);
@Query("SELECT * FROM CLIENT")
LiveData<ClientEntity[]> getAllLive();
@Query("SELECT * FROM CLIENT")
ClientEntity[] getAll();
@Query("DELETE FROM CLIENT")
int deleteAll();
}
레포지토리 생성
public class ClientRepository {
private ClientDao clientDao;
public ClientRepository(AppDatabase database) {
this.clientDao = database.clientDao();
}
public LiveData<ClientEntity[]> getAllLive() {
return clientDao.getAllLive();
}
public ClientEntity[] getAll() {
return clientDao.getAll();
}
public long insertClient(ClientEntity client){
return clientDao.insert(client);
}
public int deleteAll(){
return clientDao.deleteAll();
}
public LiveData<ClientEntity> getClientById(int id) {
return clientDao.getClientById(id);
}
서비스 생성
public class ClientService {
private final ClientRepository clientRepository;
public ClientService(Context context) {
AppDatabase db = DatabaseManager.getInstance(context);
this.clientRepository = new ClientRepository(db);
}
public LiveData<ClientEntity[]> getAllLive() {
return clientRepository.getAllLive();
}
public ClientEntity[] getAllSystems() {
return clientRepository.getAll();
}
public long insertClient(ClientEntity client){
return clientRepository.insertClient(client);
}
// 나머진 생략..
}
타입 변환기 생성
위 변환기는 결과에 대한 부분이 List인걸 Entity로 다시 넣어주는 경우에 사용합니다.
본 예시에는 Entity에 List가 없으므로 사실 무관하지만 추후에 필요할 듯 싶어 작성합니다.
Entity 클래스 상단 어노테이션으로 @TypeConverters를 기입하여 특정 타입컨버터스를 지정할 수 있습니다.
예) @TypeConverters(CustomConverter.class)
// Room은 List<String> 같은 복잡한 타입을 지원하지 않습니다. String으로 바꿔줘야 합니다.
// @TypeConverters를 사용하면 사용자 정의 변환기를 적용할 수 있습니다.
import androidx.room.TypeConverter;
import java.util.Arrays;
import java.util.List;
public class Converters {
@TypeConverter
public static String fromList(List<String> value) {
return String.join(",", value); // 리스트 → 문자열 변환
}
@TypeConverter
public static List<String> toList(String value) {
return Arrays.asList(value.split(",")); // 문자열 → 리스트 변환
}
}
꼭 Entity가 아니여도 사용 할 수 있으며 사용 범위는 다음과 같습니다.
- Entity 클래스에 적용 : 해당 Entity 클래스 내의 필드에만 타입 변환기가 적용됩니다. 다른 Entity나 DAO에는 영향을 주지 않습니다.
- Database 클래스에 적용 : 데이터베이스 전체에 타입 변환기가 적용됩니다. 모든 Entity와 DAO에서 해당 변환기를 사용할 수 있습니다.
- DAO 클래스에 적용 : 해당 DAO의 쿼리와 메서드에만 타입 변환기가 적용됩니다.
- Entity 특정 필드에 적용 : Entity 내의 특정 필드에 변환기를 적용할 수 있습니다(필드 수준에서 사용).
데이터베이스 객체 생성
// entities 안에 본인이 구현한 EntityClass를 쉼표기준 등록
@Database(entities = {ClientEntity.class, ContentsEntity.class}, version = 1)
@TypeConverters(Converters.class) // 변환기 적용
public abstract class AppDatabase extends RoomDatabase {
// 본인이 구현한 Dao 등록
public abstract ClientDao clientDao();
public static final String DATABASE_NAME = "{본인 디비명}";
}
사용
// DI 라이브러리 쓰지 않아 명시적으로 객체 생성
ClientService clientService = new ClientService(this.context);
// 객체 생성
ClientEntity ce = new ClientEntity("클라이언트ID","클라이언트이름","127.0.0.1");
long ret = clientService.insert(ce);
마무리
사실 저도 사용한지 하루 되었습니다.
물론 더 깊이 배우고 다뤄봐야 할 부분은 많겠지만, 하루 업무 시간 동안에도 충분히 익히고 사용할 수 있을 정도로 매우 편리하게 개발되어 있는 라이브러리임을 확인하였습니다.
Room은 복잡한 SQLite 코드를 직접 작성하지 않아도 되며, 직관적인 어노테이션과 객체 지향적인 설계로 데이터베이스 작업을 간소화해줍니다. 특히, 컴파일 타임에 쿼리를 검증해주는 기능은 실수를 줄이고 안정적인 앱 개발을 가능하게 합니다.
처음 접하는 개발자라도 기본적인 설정과 사용법을 빠르게 익힐 수 있다는 점이 큰 장점입니다.
또한, Jetpack과의 통합성과 확장성을 고려하면, Room은 안드로이드 개발에서 필수적인 도구로 자리 잡을 가능성이 높습니다.
앞으로 더 많은 기능을 탐구하고, 실제 프로젝트에 적용해보면서 Room의 진가를 더욱 체감할 수 있을 것이라 기대합니다.
여러분도 Room을 사용해보시면, 저처럼 단 하루만에 그 편리함에 놀라실 겁니다.
안드로이드에서 데이터베이스 관리가 고민이셨던 분이라면, 지금 바로 Room을 시작해보시는 것을 추천드립니다.