내부저장소에 메모를 저장하는 앱으로 보는 DAO, DTO, Entity

Sora Do·2025년 3월 1일

Room을 이용한 안드로이드 메모앱을 만들다가 이들을 활용하게 되었다. 그래서 이 개념을 정리하고, 이용해보고자 포스팅을 한다.

Summary

  • DAO: 실제 db에 접근하는 객체 (챔프)
  • DTO: 데이터 교환을 위한 객체 (팥빵)
  • Entity: 실제 DB와 매칭될 클래스 (도라야끼)

Intro

괄호 안의 예시는 듣도보도 못했을 것이다. 내가 떠올린 비유 방법이니.
이 요상한 비유로 우선 친근하게 접근해보자.
이해를 돕기 위해, 일본 애니메이션 '도라에몽'의 예시를 가지고 왔다.

도라에몽이 원래 좋아하는 빵은 일본에서 '도라야끼'라고 불린다.
하지만 우리나라에서 의역하면서, '팥빵'이라고 알려져 왔다.

  • DAO (챔프): 예전에는 챔프라는 애니메이션 전문 채널에서 도라에몽을 방영했다. 챔프사에서, 이 도라야끼를 팥빵이라는 친근한 단어로 번역했을 것이다. 이렇게 번역을 했듯이, DAO는 애플리케이션을 대신해 실제 데이터베이스에 접근하고 작업을 수행한다.

  • DTO (팥빵): 도라야끼가 한국 시청자들에게 팥빵으로 번역되어 전달되듯, DTO는 데이터베이스의 원래 형태(Entity)를 애플리케이션의 다른 계층(UI 등)에서 사용하기 적합한 형태로 변환한 객체다.

  • Entity (도라야끼): 일본 원작에서의 도라야끼처럼, Entity는 데이터베이스의 원래 형태와 직접 매핑되는 객체다. 데이터베이스 테이블의 구조를 그대로 반영한다.

이제 워밍업 끝. 실제 메모앱의 코드로 각각의 역할을 알아보자!

1. DAO (Data Access Object)

실제로 db에 접근하는 역할을 하는 객체

1) 데이터베이스 액세스 추상화

DAO는 데이터베이스 작업을 추상화하는 인터페이스로, 실제 SQL 쿼리나 데이터베이스 로직을 숨긴다.
@Dao 어노테이션을 통해 Room에게 이것이 데이터 액세스 객체임을 알린다.

2) CRUD 작업 제공

  • Create: insertNote(note: NoteEntity) - 새로운 노트 삽입
  • Read: getAllNotes(), getNoteById(id: String) - 노트 조회
  • Update: updateNote(note: NoteEntity) - 노트 업데이트
  • Delete: deleteNote(note: NoteEntity), deleteNoteById(id: String) - 노트 삭제

3) 쿼리 메서드 정의

@Query 어노테이션을 사용하여 SQL 쿼리를 정의한다.

  • @Query("SELECT * FROM notes")
  • @Query("DELETE FROM notes WHERE id = :id")

4) 비동기 처리 지원

suspend 키워드를 사용하여 코루틴과 함께 비동기 데이터베이스 작업을 지원한다.
대부분의 메서드가 suspend 함수로 선언되어 메인 스레드를 차단하지 않고 작업할 수 있다.

5) 리액티브 프로그래밍 지원

Flow를 반환 타입으로 사용하여 데이터 변경을 관찰할 수 있다.
getAllNotes()Flow<List<NoteEntity>>를 반환하여 노트 목록의 변경을 실시간으로 감지한다.

6) 비즈니스 로직과 데이터 액세스 분리

DAO는 순수하게 데이터 액세스만 담당하며, 비즈니스 로직은 Repository나 ViewModel에서 처리한다.
이는 관심사 분리(Separation of Concerns) 원칙을 따르는 것이다.

7) 충돌 전략 관리

@Insert(onConflict = OnConflictStrategy.REPLACE)와 같이 데이터 충돌 시 처리 방법을 정의한다.

@Dao
interface NoteDao {
    @Query("SELECT * FROM notes")
    fun getAllNotes(): Flow<List<NoteEntity>>

    @Query("SELECT * FROM notes WHERE id = :id")
    suspend fun getNoteById(id: String): NoteEntity?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertNote(note: NoteEntity)

    @Update
    suspend fun updateNote(note: NoteEntity)

    @Delete
    suspend fun deleteNote(note: NoteEntity)

    @Query("DELETE FROM notes WHERE id = :id")
    suspend fun deleteNoteById(id: String)
}

2. DTO (Data Transfer Object)

DTO: 데이터 교환을 위한 객체

Note.kt 파일에서 볼 수 있는 Note 클래스를 통해 DTO(Data Transfer Object)의 역할을 파악해보자.

1) 데이터 전송 구조 제공

Note 클래스는 UI 레이어와 데이터 레이어 사이에서 데이터를 전송하는 객체로 기능했다. 복잡한 데이터베이스 엔티티를 UI에 적합한 형태로 변환하는 역할을 담당했다.

2) 계층 간 데이터 변환

NoteEntitytoNote() 메서드와 Note 클래스의 fromNote() 정적 메서드를 통해 DTO와 Entity 간의 변환이 이루어졌다. 이는 각 계층이 자신의 관심사에만 집중할 수 있게 했다.

3) UI 친화적 데이터 구조

Note 클래스는 positionOffset 타입으로 저장해 UI 계층에서 직접 사용하기 쉽게 했다. 반면 Entity는 이를 positionXpositionY로 분해했다.

부연 설명을 하자면, 데이터베이스에서는 positionXpositionY라는 두 개의 Float 값으로 저장하지만, UI 레이어에서는 이를 하나의 Offset 객체로 사용한다. DTO는 이 두 형식 간의 번역기 역할을 해서 각 레이어가 자신에게 가장 적합한 형태로 데이터를 다룰 수 있게 한다.

4) 비즈니스 로직 캡슐화

Note 클래스는 식별자(id)와 함께 이모지, 제목, 내용, 위치 정보를 캡슐화했다. 이 정보들은 비즈니스 로직에 필요한 형태로 구성됐다.

5) 유연한 데이터 표현

Note 클래스는 기본값을 제공해 객체 생성을 유연하게 만들었다. 이는 새로운 노트 생성 시 유용하게 활용됐다.

6) 불변성 보장

Kotlin의 data class를 사용해 객체의 불변성을 보장했다. 이는 다중 스레드 환경에서 안전하게 데이터를 다룰 수 있게 했다.

7) 표현 계층과의 연결

Note 객체는 UI 컴포넌트(NoteDisplay, NoteDetailDialog 등)에서 직접 사용됐다. 이는 DTO가 표현 계층과 밀접하게 연결되어 있음을 보여줬다.

data class Note(
    val id: String = UUID.randomUUID().toString(),
    val emoji: String = "",
    val title: String = "",
    val content: String = "",
    val position: Offset = Offset.Zero
)

3. Entity

실제 DB와 매칭될 클래스
DB 구조와 객체 지향 코드 사이의 다리 역할을 하는 녀석이다.

1) 데이터베이스 테이블 매핑

@Entity(tableName = "notes") 어노테이션을 통해 이 클래스가 데이터베이스의 "notes" 테이블과 매핑됨을 선언했다.
각 필드는 테이블의 컬럼과 직접적으로 연결된다.

2) 기본 키 정의

@PrimaryKey 어노테이션을 통해 id 필드를 테이블의 기본 키로 지정했다.
UUID를 사용해 고유 식별자를 생성하는 방식을 채택했다.

3) 데이터 구조 정의

Entity는 emoji, title, content, positionX, positionY 등 저장해야 할 데이터의 구조를 명확히 정의했다.
Kotlin의 data class를 사용해 불변성과 유용한 메서드들(equals, hashCode, toString 등)을 자동으로 제공받았다.

4) 도메인 모델과의 변환 기능

fromNote() 메서드는 도메인 모델(Note)을 Entity로 변환했다.
toNote() 메서드는 Entity를 도메인 모델로 변환했다.
이런 변환 메서드들은 데이터 계층과 도메인 계층 사이의 경계를 명확히 했다.

5) 데이터 타입 매핑

복잡한 타입(예: Offset)을 기본 타입(예: Float)으로 분해해 데이터베이스에 저장할 수 있게 했다.
이는 ORM의 기본 원칙인 객체-관계 불일치 문제를 해결한 사례다.

6) 영속성 계층의 기본 단위

Entity는 Room 데이터베이스의 영속성 계층에서 기본 단위로 작동했다.
데이터베이스 작업(CRUD)의 대상이 되는 객체다.

@Entity(tableName = "notes")
data class NoteEntity(
    @PrimaryKey val id: String = UUID.randomUUID().toString(),
    val emoji: String,
    val title: String,
    val content: String,
    val positionX: Float,
    val positionY: Float
) {
    // Note 모델을 Entity로 변환하는 함수
    companion object {
        fun fromNote(note: Note): NoteEntity {
            return NoteEntity(
                id = note.id,
                emoji = note.emoji,
                title = note.title,
                content = note.content,
                positionX = note.position.x,
                positionY = note.position.y
            )
        }
    }

    // Entity를 Note 모델로 변환하는 함수
    fun toNote(): Note {
        return Note(
            id = id,
            emoji = emoji,
            title = title,
            content = content,
            position = androidx.compose.ui.geometry.Offset(positionX, positionY)
        )
    }
}
profile
호기심 많은 전자과 출신 플러터 개발자

0개의 댓글