-
DAO와 REPOSITORY 논쟁[옛날 글들] 설계 이야기 2024. 5. 28. 17:09728x90
얼마 전 지인으로부터 DAO(Data Access Object)와 REPOSITORY의 차이점에 관해 설명해 달라는 요청을 받았다. 대부분의 소프트웨어 개발 이슈가 그렇듯이 이런 류의 질문에 대한 대답은 미묘하지만 격렬한 논쟁을 불러 일으키기 쉽다. 논쟁의 중심에는 항상 극단적인 순수주의와 허무주의 간의 충돌이 존재하며, 파국의 소용돌이 속에서 개념적 편향을 막기 위한 최선의 방법은 논쟁의 대상이 출현하게된 배경과 현재의 개념이 정립되기까지의 과정을 살펴 보는 것이다.
1990년대 말에 등장한 EJB(Enterprise Java Beans)는 Java의 ‘Write Once, Run Anywhere’ 모토를 엔터프라이즈 어플리케이션 환경으로 확장하기 위한 Sun의 야심작이다. EJB 의 중심 전략은 분산, 트랜잭션, 퍼시스턴스, 보안 등의 인프라스트럭쳐 서비스는 WAS(Web Application Server)에서 제공하고 어플리케이션 개발자는 비즈니스 로직에만 집중하도록 하자는 것이다. 개발자들은 Sun에서 제시하는 장미빛 미래에 열광적으로 환호했고, WAS라는 프론티어를 향한 벤더들의 골드 러시는 EJB가 21세기의 은총알일 것이라는 신기루를 만들어 냈다.
초기 J2EE에서 권장한 기본 아키텍처는 도메인 레이어를 EJB Session Bean으로, 퍼시스턴스 레이어를 EJB Entity Bean으로 구성하는 것이다. Entity Bean은 O/R Mapper(Object–Relational Mapper)의 원시적인 형태로 최근 Ruby On Rails의 인기로 인해 주목받고 있는 ACTIVE RECORD 패턴을 적용한 퍼시스턴스 기술이다. Entity Bean은 CMP(Container-Managed Persistence)의 매핑 제약, BMP(Bean-Managed Persistence)의 비유용성, 오버헤드, 빈약한 EJB QL 지원, 침투적인 인프라스트럭쳐 코드, 복잡도 등의 단점으로 인해 개발자들로부터 외면을 받고 말았다.
Entity Bean이 무대의 뒷편으로 쓸쓸히 퇴장할 운명에 처한 반면 선언적으로 트랜잭션을 정의할 수 있는 CMT(Container-Managed Transaction) 기반의 Session Bean은 다양한 어플리케이션에서 폭 넓게 사용되었다. 그러나 Entity Bean을 무시한 채 Session Bean만을 사용하기 시작하면서 부터 한가지 문제점이 발생했다. 비즈니스 로직만을 담아야할 Session Bean 내부로 퍼시스턴스 로직이 침투하기 시작했던 것이다. Session Bean과 Entity Bean에 의해 자연스럽게 구분되던 도메인 레이어와 퍼시스턴스 레이어 간의 경계가 모호해지면서 Session Bean 내에 비즈니스 로직과 퍼시스턴스 로직이 섞여 버리는 경우가 많았다. 이를 해결하기 위해 퍼시스턴스 저장소로 접근하는 모든 로직을 캡슐화하는 별도의 객체를 추가하여 도메인 레이어와 퍼시스턴스 레이어를 명확하게 분리하는 방법이 제시되었다. 이처럼 Entity Bean의 퍼시스턴스 지원 기능을 대체하는 동시에 비즈니스 로직과 퍼시스턴스 로직의 명화한 분리를 위해 퍼시스턴스 로직을 캡슐화하고 도메인 레이어에 객체-지향적인 인터페이스를 제공하는 객체를 DAO(Data Access Object) 라고 한다.
REPOSITORY는 Domain-Driven Design 의 기본 빌딩 블록 중 하나로 도메인 레이어에 객체 지향적인 컬렉션 관리 인터페이스를 제공하기 위해 사용되는 PURE FABRICATION이다. 변경에 대한 불변식을 유지하기 위해 하나의 단위로 취급되면서 변경의 빈도가 비슷하고, 동시 접근에 대한 잠금의 단위가 되는 객체의 집합인 AGGREGATE 별로 하나의 REPOSITORY를 사용한다. 예를 들어 Order와 OrderLineItem이 하나의 불변식을 공유하는 단위이고 OrderLineItem의 생명 주기가 Order에 종속된다면 OrderRepository를 사용해서 모든 Order와 OrderLineItem 컬렉션을 관리한다.
REPOSITORY를 설명하면서 의도적으로 컬렉션이라는 용어를 반복해서 사용했다는 점에 주목하기 바란다. REPOSITORY는 특정 객체 집합의 가상적인 메모리 컬렉션을 관리한다. 시스템에 1억건의 주문 정보가 존재한다면 OrderRepository는 1억건의 Order 인스턴스와 그와 관려된 OrderLineItem 인스턴스가 메모리 내에 로드되어 있다고 가정한다. 클라이언트는 OrderRepository를 사용해서 Order 컬렉션을 처리한다. 전체 컬렉션에 Order 인스턴스를 추가하기도 하고, 컬렉션에서 특정 Order 인스턴스를 제거하기도 하고, 일정 기간 동안의 Order 개수를 카운트하기도 한다. 여기서 중요한 것은 REPOSITORY 자체는 퍼시스턴스 메커니즘에 대한 어떤 가정도 하지 않는다는 점이다. REPOSITORY를 사용할 때는 이미 모든 Order 객체와 OrderLineItem이 메모리에 로드되어 있다고 가정하고 메모리에 로드된 특정 객체, 객체 집합, 전체 객체에 접근하기 위해 REPOSITORY를 사용한다.
REPOSITORY는 문제 도메인 분석 과정에서 도메인 레이어에 속하는 객체들에게 객체-지향적인 컬렉션 인터페이스를 제공하기 위한 용도로 도메인 모델에 추가된다. 따라서 REPOSITORY는 도메인 모델의 일부이며 UBIQUITOUS LANGUAGE의 한 요소이다. 비록 REPOSITORY 자체는 도메인 모델에 속하지만 REPOSITORY 내부에서는 AGGREGATE의 생명 주기를 관리하기 위해 하부의 퍼시스턴스 메커니즘을 사용할 필요가 있다. 그러나 REPOSITORY에 퍼시스턴스 로직이 포함될 경우 REPOSITORY를 직접 사용하는 도메인 객체 역시 퍼시스턴스 메커니즘에 의존하게 되는 부작용이 발생한다. 이를 해결하기 위한 최선의 방법은 REPOSITORY를 인터페이스와 구현부로 분리한 후 인터페이스는 도메인 레이어에 속하도록 하고, 구현부는 퍼시스턴스 레이어에 속하도록 하는 것이다. 이처럼 DIP(Dependency Inversion Principle)에 기반하여 인터페이스와 구현부 사이의 계층을 분리하는 패턴을 SEPARATED INTERFACE이라고 한다.
DAO와 REPOSITORY 모두 퍼시스턴스 로직에 대한 객체-지향적인 인터페이스를 제공하고 도메인 로직과 퍼시스턴스 로직을 분리하여 관심사의 분리(separation of concerns) 원리를 만족시키는데 목적이 있다. 그러나 비록 의도와 인터페이스의 메소드 시그니처에 유사성이 존재한다고 해서 DAO와 REPOSITORY를 동일한 패턴으로 취급하는 것은 성급한 일반화의 오류를 범하는 것이다.
- DAO는 퍼시스턴스 로직인 Entity Bean을 대체하기 위해 만들어진 개념이다. DAO가 비록 객체-지향적인 인터페이스를 제공하려는 의도를 가지고 있다고 하더라도 실제 개발 시에는 하부의 퍼시스턴스 메커니즘이 데이터베이스라는 사실을 숨기려고 하지 않는다. DAO의 인터페이스는 데이터베이스의 CRUD 쿼리와 1:1 매칭되는 세밀한 단위의 오퍼레이션을 제공한다. 반면 REPOSITORY는 메모리에 로드된 객체 컬렉션에 대한 집합 처리를 위한 인터페이스를 제공한다. DAO가 제공하는 오퍼레이션이 REPOSITORY 가 제공하는 오퍼레이션보다 더 세밀하며, 결과적으로 REPOSITORY에서 제공하는 하나의 오퍼레이션이 DAO의 여러 오퍼레이션에 매핑되는 것이 일반적이다. 따라서 하나의 REPOSITORY 내부에서 다수의 DAO를 호출하는 방식으로 REPOSITORY를 구현할 수 있다.
- Core J2EE Patterns나 기타 J2EE 서적에서 언급하고 있는 것처럼 DAO는 데이터베이스 뿐만 아니라 B2B, LDAP, 메인 프레임, 레거시 시스템과 같은 다양한 종류의 외부 시스템과의 상호작용을 캡슐화하기 위해 사용될 수 있다. 이 경우 DAO는 외부 시스템에 대한 GATEWAY 역할을 수행한다. 그에 비해 REPOSITORY는 객체 컬렉션 처리에 관한 책임만을 가지고 있다. Domain-Driven Design에서 외부 시스템과의 상호작용은 별도의 SERVICE가 담당한다.
- DAO는 퍼시스턴스 레이어에 속한다. 반면 REPOSITORY의 인터페이스는 도메인 레이어에 속한다.
- 대부분의 프로젝트에서 TABLE DATA GATEWAY 패턴을 따라 테이블 별로 하나의 DAO를 만든다. 이 전통은 Entiry Bean을 DAO로 대체하면서 테이블 당 하나의 Entity Bean을 만들어야 했던 EJB의 제약을 그대로 답습한 것이 아닌가 추측된다. 이유야 어떻든 현재 DAO를 개발하는 일반적인 패턴은 DB에 존재하는 테이블 당 하나의 DAO를 사용하는 것이며, 이것은 DAO가 인터페이스 차원에서는 데이터베이스에 대한 의존도를 캡슐화할 수 있다고 하더라도 테이블 변경에 대해서는 투명하지 않다는 것을 의미한다. REPOSITOR의 인터페이스는 AGGREGATE ROOT(또는 Entry Point)와 관련된 오퍼레이션만을 포함하며 따라서 테이블 변경에 대해서 투명하다.
- DAO는 TRANSACTION SCRIPT 패턴과 함께 사용된다. 반면 REPOSITORY는 DOMAIN MDOEL 패턴과 함께 사용된다.
- 마지막으로 가장 중요하면서도 분명한 차이점은 두 빌딩 블록을 식별하는 과정에서 나타난다. REPOSITORY는 도메인 모델링 단계에서 하나의 개념 단위로 묶인 AGGREGATE를 도출하는 과정에서 자동으로 식별된다. 이것은 REPOSITORY 식별 과정이 도메인 규칙에 영향을 받는다는 것을 의미한다. 앞에서 설명한 바와 같이 DAO는 TABLE DATA GATEWAY 패턴에 따라 데이터베이스 테이블 별로 생성되는 것이 일반적이며 퍼시스턴스 레이어에 대한 FACADE 역할을 수행한다. 따라서 DAO 식별 과정은 도메인 규칙 보다는 데이터베이스 테이블의 단위에 영향을 받는 경향이 강하다.
지금까지 DAO와 REPOSITORY의 개념적 차이를 살펴보았다. 최근의 기술 흐름으로 살펴 보았을 때 앞서 설명한 차이점이 그렇게 중요한 것은 아니라고 생각된다. 가장 큰 이유는 Spring과 Hibernate와 같은 경량 프레임워크가 EJB를 누르고 산업계의 표준으로 등장함에 따라 EJB 세계에서 통용되던 다양한 개념과 용어들이 POJO(Plain Old Java Object) 기술의 성장에 따라 확장을 계속하고 있기 때문이다.
이런 맥락에서 볼 때 DAO와 REPOSITORY 간의 논쟁 역시 Entity Bean을 대체하기 위한 용도로 등장했던 DAO가 EJB의 제약을 벗어나 REPOSITORY와 유사해 지는 과정에서 불거진 것이라고 볼 수 있다. Rod Johnson 역시 그의 저서 Expert One-On-One J2EE Development without EJB에서 O/R Mapper를 사용할 경우의 DAO 패턴을 REPOSITORY와 유사한 개념으로 설명하고 있다. 차이점이라면 AGGREGATE의 개념을 first-class domain object와 dependent object로 표현하고 있다는 정도 뿐이다. Rod Johnson의 예에서 알 수 있듯이 기술적인 측면에서만 본다면 DAO와 REPOSITORY 간의 차이점을 논하는 것이 무의미할 정도로 두 패턴이 많은 유사성을 띄고 있음을 알 수 있다.
그럼에도 불구하고 DAO와 REPOSITORY를 동일하게 취급하는 것은 올바른 접근 방법이 아니다. 비록 두 패턴이 기술적으로 점점 유사성을 띄어 가고 있다고 하더라도 DAO는 퍼시스턴스 레이어에 속하는 FACADE이며, REPOSITORY는 순수한 도메인 모델 객체이다. DAO는 퍼시스턴스 로직의 요구사항에 따라 설계되는 반면 REPOSITORY는 문제 도메인의 요구사항에 따라 설계된다.
개인적으로 TRANSACTION SCRIPT 패턴에 따라 도메인 레이어가 구성되고 퍼시스턴스 레이어에 대한 FAÇADE의 역할을 하는 객체가 추가될 때는 거리낌 없이 DAO라고 부른다. 도메인 레이어가 DOMAIN MDOEL 패턴으로 구성되고 도메인 레이어 내에 객체 컬렉션에 대한 인터페이스가 필요한 경우에는 REPOSITORY라고 부른다. 결과적으로 두 객체의 인터페이스의 차이가 보잘 것 없다고 하더라도 DAO가 등장하게된 시대적 배경과 현재까지 변화되어온 과정 동안 개발 커뮤니티에 끼친 영향력을 깨끗이 지워 버리지 않는 한 DAO와 REPOSITORY를 혼용해서 사용하는 것은 더 큰 논쟁의 불씨를 남기는 것이라고 생각한다.
728x90'[옛날 글들] 설계 이야기' 카테고리의 다른 글
의존성 끊기와 단위 테스트 – 2부 [끝] (0) 2024.05.30 의존성 끊기와 단위 테스트 - 1부 (0) 2024.05.30 명령-쿼리 분리(Command-Query Separation, CQS) 원리 (0) 2024.05.29 단일 접근 원칙(Uniform Access Principle)을 통한 캡슐화 2부[끝] (0) 2024.05.27 단일 접근 원칙(Uniform Access Principle)을 통한 캡슐화 1부 (0) 2024.05.24