Home Dao와 Repository의 차이점은 무엇일까?
Post
Cancel

Dao와 Repository의 차이점은 무엇일까?

글을 작성하게 된 계기


Dao와 Repository의 차이점에 대해 학습하며 알게 된 내용을 정리하기 위해 글을 작성하게 되었습니다.





1. 각 패턴 살펴보기


DAO(Data Access Object) 패턴과 Repository 패턴은 공통점이 많지만, 명확한 차이가 존재합니다. 이를 알기 위해서는 각각의 개념을 명확하게 알아야 하는데, 이에 대해 살펴보겠습니다.

image



1-1. Dao Pattern

DAO 패턴은 다양한 데이터베이스나 영속성 메커니즘에 대해 공통된 인터페이스를 만드는 설계 방식 입니다. 이를 통해 애플리케이션 계층의 요청을 영속 계층으로 연결하고, 데이터베이스의 복잡한 내부 동작을 드러내지 않고 데이터를 다룰 수 있습니다. 이는 애플리케이션에서 필요한 데이터 접근 방식과 이를 구현하는 방법을 분리합니다.

In software, a data access object (DAO) is a pattern that provides an abstract interface to some type of database or other persistence mechanism. By mapping application calls to the persistence layer, the DAO provides data operations without exposing database details. This isolation supports the single responsibility principle. It separates the data access the application needs, in terms of domain-specific objects and data types (the DAO’s public interface), from how these needs can be satisfied with a specific DBMS (the implementation of the DAO).





즉 도메인 객체 혹은 필요한 데이터를 매핑해서 데이터베이스에 접근하는 것을 추상화하는 것입니다. 이처럼 접근 방식과 구현 방식을 분리하면 구현체가 바뀌더라도 애플리케이션의 로직에는 영향을 미치지 않으며, 개발을 더욱 유연하게 진행할 수 있습니다.

1
2
3
interface UserDao {
    fun save(user: user)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Repository
class UserDaoImpl {  // Impl을 관습적으로 사용하는 것은 좋지 않지만 예시를 위해 사용했습니다.
    private val log = LoggerFactory.getLogger(this::class.java)
    private val connectionUtils = DatabaseConnectionUtil

    fun save(user: User) {
        val sql = "INSERT INTO user(user_id, nickname) values(?, ?)"

        var connection: Connection? = null
        var preparedStatement: PreparedStatement? = null
        try {
            connection = connectionUtils.getConnection()
            preparedStatement = connection.prepareStatement(sql)
            preparedStatement.setLong(1, user.userId)
            preparedStatement.setString(2, user.nickname)
            preparedStatement.execute()
        } catch (exception: SQLException) {
            log.error("Database Error")
            exception.printStackTrace()
        } finally {
            close(connection, preparedStatement, preparedStatement?.resultSet)
        }
    }
}

또한 이를 통해 단일 책임 원칙을 지키며 개발할 수 있게 해줍니다.





1-2. Repository Pattern

Repository 패턴은 도메인 계층과 데이터 매핑 계층 사이를 중재하는 컬렉션 입니다. 이는 도메인 모델에 대한 컬렉션을 제공하고 도메인 로직이 데이터 저장소에 직접 접근하는 것을 방지합니다.

A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection. Client objects construct query specifications declaratively and submit them to Repository for satisfaction. Objects can be added to and removed from the Repository, as they can from a simple collection of objects, and the mapping code encapsulated by the Repository will carry out the appropriate operations behind the scenes. Conceptually, a Repository encapsulates the set of objects persisted in a data store and the operations performed over them, providing a more object-oriented view of the persistence layer. Repository also supports the objective of achieving a clean separation and one-way dependency between the domain and data mapping layers.







예를 들어 아래와 같이 JpaRepository만 선언하면 그 구현체인 SimpleJpaRepository의 세부 구현에 직접 접근하지 않습니다. 우리가 접근하는 단위는 객체 이며, 이 객체들의 컬렉션이 JpaRepository가 됩니다.

1
public interface OrderJpaRepository extends JpaRepository<Order, Long> {}

Repository는 특정 도메인 모델이나 비즈니스 로직에 집중하며, 이 때문에 특정 엔티티에 대한 작업을 수행하는데 제한적일 수 있습니다. Repository는 도메인 엔티티를 직접 관리하며, 해당 엔티티의 컬렉션을 제공하고 그 생명주기를 관리합니다.





추가로 Repository의 계층에 대해서는 의견이 분분하지만, 개인적으로 Repository의 인터페이스는 도메인 계층, 구현체는 영속 계층에 속한다고 생각합니다. 이는 해당 글의 주제와는 관계가 적으므로 기회가 된다면 별도로 포스팅하겠습니다. 간략히 설명하면, 위에서 Repository는 도메인 모델에 대한 컬렉션을 제공하여, 도메인 로직이 데이터 저장소에 직접 접근하는 것을 방지합니다. 라고 했습니다. 즉 Repository는 영구저장소가 아닌 그저 하나의 컬렉션/객체 저장소이며, 해당 구현체를 통해 영속화를 위한 작업(데이터베이스 접근)을 합니다. 따라서 Repository까지는 도메인 계층, 구현체는 영속 계층으로 볼 수 있습니다.

image

이를 통해 도메인 계층과 인프라 계층의 DIP가 이루어지는데, 즉 인프라 계층에서는 도메인을 알아도 되며 이를 통해 엔티티를 영속화하거나, 영속된 엔티티를 찾아올 수 있는 것입니다.







2. 차이점


DAO와 Repository는 데이터 접근에 대한 추상화를 제공하는 것은 동일하지만 둘은 어떤 것을 대상으로 접근하는지 에 대한 차이가 있습니다. DAO는 조금 더 유연하지만, Repository는 조금 더 구체적이고 명확한 대상에 접근합니다. 이는 JpaRepository를 보면 명확한데요, JpaRepository는 항상 특정 엔티티를 대상으로 어떤 행위를 수행합니다.

1
2
// Order 엔티티에 대한 Repository임을 명시
public interface OrderJpaRepository extends JpaRepository<Order, Long> {}





findById( ), delete( )와 같은 메서드도 특정 엔티티나 해당 엔티티의 컬렉션을 대상으로 행위를 수행합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@NoRepositoryBean
public interface JpaRepository<T, ID> extends ListCrudRepository<T, ID>, ListPagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {

        ......
    
	T getOne(ID id);

        ......
    
	@Override
	<S extends T> List<S> findAll(Example<S> example);

	@Override
	<S extends T> List<S> findAll(Example<S> example, Sort sort);
}





물론 fetchJoin을 통해 조인을 사용할 수도 있는데, 이 경우 특정 엔티티를 통해 연관된 엔티티를 찾아오기에 특정 엔티티를 대상으로 사용한다는 Repository의 핵심 개념은 변함이 없습니다.

1
2
3
4
5
6
public interface OrderJpaRepository extends JpaRepository<Order, Long> {

    @Query(
    "SELECT o FROM orders o JOIN FETCH o.orderLines WHERE o.uniqueId = :uniqueId AND o.baseInformation.deleted = :deleted")
    Optional<Order> findOrderByUniqueId(@Param("uniqueId") UUID uniqueId, @Param("deleted") Boolean deleted);
}





반면 DAO는 특정 객체(엔티티)를 대상으로 사용하기도 하며, 엔티티가 아닌 DTO와 같이 정의된 데이터를 대상으로 사용하기도 합니다. 즉 Repository보다 조금 더 유연하고 광범위한 대상을 타겟으로 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Repository
class UserDaoImpl {                          
    private val log = LoggerFactory.getLogger(this::class.java)
    private val connectionUtils = DatabaseConnectionUtil

    fun save(user: User) {
        val sql = "INSERT INTO user(user_id, nickname) values(?, ?)"

        var connection: Connection? = null
        var preparedStatement: PreparedStatement? = null
        try {
            connection = connectionUtils.getConnection()
            preparedStatement = connection.prepareStatement(sql)
            preparedStatement.setLong(1, user.userId)
            preparedStatement.setString(2, user.nickname)
            preparedStatement.execute()
        } catch (exception: SQLException) {
            log.error("Database Error")
            exception.printStackTrace()
        } finally {
            close(connection, preparedStatement, preparedStatement?.resultSet)
        }
    }

    fun findUserNameById(Long userId): UserNameDto {
        val query = "SELECT name FROM user WHERE user_id = ?"

        var connection: Connection? = null
        var preparedStatement: PreparedStatement? = null
        var resultSet: ResultSet?
        try {
            connection = connectionUtils.getConnection()
            preparedStatement = connection.prepareStatement(query)
            preparedStatement.setLong(1, userId)

            resultSet = preparedStatement.executeQuery()
            if (resultSet.next()) {
                return User(
                    resultSet.getLong(USER_ID),
                    resultSet.getString(NICKNAME)
                )
            }
            throw NoSuchElementException()
        } catch (exception: SQLException) {
            log.error("Database Error")
            exception.printStackTrace()
        } finally {
            close(connection, preparedStatement, preparedStatement?.resultSet)
        }
        throw IllegalStateException()
}

Dao를 특정 테이블과 매핑시키고 연관된 데이터만 다룬다는 글이 많은데, 조인이 있는 경우 다른 테이블의 데이터도 해당 Dao에서 다루기 때문에 반은 맞고 반은 틀린 말이라 생각됩니다. 예를 들어 User Dao에서 UserImage를 조인하는 경우 UserImage 데이터까지 함께 핸들링합니다. 따라서 특정 테이블에 한정하기 보다 데이터 접근을 추상화 시킨 것이 Dao라고 보는 것이 더 맞는 말입니다. 다만 이 부분은 사람마다 생각이 다를 수 있기 때문에 각자의 판단에 맡기겠습니다.







3. 정리


Dao는 보다 유연한 특징을 가지고 있어 특정 엔티티에 국한되지 않지만, Repository는 특정 도메인 또는 엔티티에 더욱 초점을 맞추고 있어 보다 제한적일 수 있습니다. Dao는 영속성의 추상화이며, Repository는 객체 컬렉션의 추상화입니다. Dao 보다는 Repository가 조금 더 추상화된 개념이므로 Repository는 Dao를 사용해 구현할 수 있으나, 그 반대는 구현할 수 없습니다.


This post is licensed under CC BY 4.0 by the author.

Duck Typing

@SpringBootConfiguration