DDD(Domain Driven Design) - Repository 리파지토리

반 버논의 도메인 주도 설계 구현 이라는 책을 완독하고 나서 여러가지 정리할 점들이 있는데, 그 중 하나가 레포지토리 이다. 오늘은 리파지토리에 대해서 다루어 보려고 한다.

지금 재직중인 회사나 전회사에서 일할때도 Database 중심적 설계를 바탕으로 시스템을 구축했다. 그리고 내가 가보지 못한 대부분의 회사들도 아마도 DDD 를 실무에서 적용하고 있지 않을 것이라고 감히 추측해 본다. 물론 DDD 를 실질적으로 도입하고 있는 회사도 많을 것이다.

잡소리는 그만하고, 리파지토리에 대해서 다루어 보겠다.

반 버논의 도메인 주도 설계 구현에서 리파지토리에 아래와 같이 2 가지를 다루고 있다.

- 컬렉션 지향 리파지토리 ( collection-oriented-repository )
- 영속성 지향 리파지토리 ( persistence-oriented-repository )

먼저, 컬렉션 지향 리파지토리이다.

책에서는, 컬렉션 지향 리파지토리는 Set 컬렉션을 흉내 내야 한다고 한다.
이 말이 무슨 말이냐면, Set에 있는 같은 객체(equals and hashcode)의 인스턴스는 두 번 추가되로록 허용해서는 안 된다.
또한 리파지토리로부터 객체를 가져오게 하고 수정할 때 이를 리파지토리에 "재저장" 할 필요가 없다.

이를 단순하게 이해하기 위해서, 책에서 나온 예제를 살펴보겠다. 표준 java.util.HashSet 을 확장하고 고유 식별자로 특정 객체를 찾도록 해주는 새로운 타입상의 메서드를 생성한다고 한다. 확장 클래스로 CalendarRepository 라는 이름을 주어 인식할 수 있도록 해주었다. 하지만 이는 단순한 인메모리 HashSet 이다.



리파지토리에서 find 를 통해서 찾은 객체를 수정한 뒤, 명시적으로 다시 add 해주지 않는다.
수정내역은 해당 객체에서 바로 일어나기 때문이다. 이와 관련된 예제로 직접 작성해보았다.
아래에서 확인할 수 있듯이, me 라는 User 타입의 객체의 name 속성은 "me" 였다.
Set 에 add 가 된 후, stream 과 id 를 통해서 find 하고  name 속성을 "동길" 로 변경했다.
그리고 다시 add 하지 않았다. 
분명히 add 를 하지 않았지만, updatedMe 라는 변수로 다시 리파지토리에서 find 를 한 객체와
기존에 이미 find 했던 객체 findMe 객체의 name 속성은 모두 "동길" 로 되어있다.
따라서 Assertion 들은 모두 통과하게 된다.



 컬렉션 지향 리파지토리는 영속성 메커니즘이 퍼블릭 인터페이스로 전혀 표현되지 않으면서 실제로 컬렉션을 흉내 내고 있음을 알 수 있다. 결국 HashSet이 보여준 특성을 나타내지만 영속성 데이터 저장소와 함께 동작하는 컬렉션 지향 리파지토리를 설계하고 구현하는 것이 목표라고 말하고 있다.

따라서, 이를 위해 이면의 영속성 메커니즘에 몇 가지 특성 기능이 필요하다. 영속성 메커니즘은 어떤 방식으로든 그것이 관리하는 각 영속성 객체에 일어난 변화를 암시적(implicit) 으로 추적하는 기능을 지원해야 한다.

1. 암시적 읽기 시 복사 (Copy-on-Read) - 저장소로부터 읽어와 재구성 시 암시적으로 영속성 객체의 복사본을 만들고, 커밋 시에 클라이언트의 복사본과 자신의 복사본을 비교한다. 영속성 메커니즘을 통해 생성된 트랜잭션이 커밋될 때, 해당 영속성 메커니즘은 가져온 복사본을 비교해 수정 여부를 확인한다. 변경이 발견된 모든 객체는 데이터 저장소에 해당 내용을 반영시킨다.

2. 암시적 쓰기 시 복사 (Copy-on-Write) - 영속성 메커니즘은 모든 로드된 영속성 객체를 프록시를 통해 관리한다. 각 객체가 데이터 저장소로부터 로드되면, 얇은 프록시가 생성되고 클라이언트로 전달된다. 클라이언트는 프록시의 존재를 눈치채지 못한 상태로 프록시 객체의 행동을 호출하게 되고, 이는 진짜 객체의 행동을 반영하게 된다. 프록시의 메소드가 처음으로 호출되는 시점에 객체의 복사본을 만들어 관리하게 된다. 프록시는 관리되는 객체의 상태에 일어난 변화를 추적해 더티(dirty) 로 표시한다. 영속성 메커니즘을 통해 생성된 트랜잭션이 커밋되면, 더티한 객체를 모두 찾아서 데이터 저장소로 반영시킨다.

단적인 예로, spring-data-jpa 를 사용할 때, @Transactional 어노테이션이 붙은 메서드는 알아서 dirty check 가 일어나는 것이 암시적 쓰기 시 복사이다.

각 접근법의 차이점과 이점은 여러 가지가 있다고 한다. 따라서 비즈니스에 따라 신중한 고려가 필요하다고 한다.

책에서는, 이와 같이 암시적 복사를 통해 변화를 추적하는 하이버네이트와 같은 영속성 메커니즘을 사용할 때 바람직하지 않은 상황도 있다고 한다. 예를 들어 아주 많은 객체를 메모리로 가져와야 하며 매우 고성능의 도메인이 필요하다면, 이런 종류의 메커니즘은 메모리와 실행 모두에 불필요한 오버헤드를 더하게 된다고 한다. (대용량 데이터 처리를 언급하는 것으로 추측된다.) 모든 도구를 사용할 땐 그에 따르는 반작용으로 완전히 숙지해야 한다고 한다.


암시적인 방법이 두 가지를 살펴 보았다면, 명시적인 방법 또한 있다.
3. 명시적으로 쓰기 전 카피 (Copy-before-Write) - 여기서 명시적이라 함은 작업 단위(Unit of Work) 에 변화가 일어난다는 점을 클라이언트에 반드시 알려야 한다는 의미다. 이는 작업 단위에게 수정을 대비해 주어진 객체를 복사할 수 있는 기회를 부여한다.

이를 지원하는 도구로는 오라클의 탑링크(TopLink) 와 이클립스링크(EclipseLink) 가 있다. 탑링크는 작업 단위를 제공한다.


먼저,  암시적 복사 구현에 대해서 잠깐 이야기 해보려고 한다.
첫 번째 단계에서 컬렉션을 흉내 내는 인터페이스를 정의한다.
아마도 아래와 같을 것이다. 자체적으로 예제를 만들어 봤다. 표준 java.util.Collection 제공하는 메소드와 매우 유사한 메서드를 가진다.

책에서는 또한 addAll() 과 removeAll() 에 대해서는 포함 시키지 않는것을 권장하고 있다. 이는 나도 매우 동의한다. 하나의 트랜잭션에서 다수의 애그리게잇 인스턴스를 추가하거나 삭제하는 것이 적절하지 않은 상황도 있을 수 있다. removeAll() 의 경우 일반적인 어플리케이션 유스케이스로는 절대 삭제되면 안 될 때가 있다. 어플리케이션에서 더 이상 사용할 수 없게 된 지 오래된 인스턴스를, 참조나 기록과 같은 이유로 다시 가져와야 할 수도 있다. 예를 들면, 탈퇴 회원의 경우가 이 케이스일 듯 하다.
비즈니스의 관점에선 일부 객체의 삭제가 현명하지 못한 일이 될 수 있다. 무분별하거나 심지어 불법적인 행동이 될 수도 있다. 따라서 단순히 비활성이나 사용 불가로 표시하거나 논리적으로 삭제됨으로 표시하자고 결정할 수도 있다. 혹은 삭제 전체를 불가능하도록 하는 편이 더 쉽다는 결론에 다다를 수도 있다.


이제는 명시적 복사에 대한 얘기를 잠깐 해보자.
오라클의 탑링크는 세션과 작업의 단위(Unit of work) 를 모두 갖고 있다고 한다. 명시적 복사의 반대로 암시적 복사의 예로는 하이버네이트를 들 수 있는데, 하이버네이트의 세션은 작업의 단위이기도 하다는 점에서 탑링크와 다르다. 작업의 단위를 세션과 구분해 사용하는 관점을 살펴보겠다.
책에서의 예제는 참 Calendar 를 좋아한다.


UnitOfWork 는 객체를 수정하려는 의도를 명시적으로 알려줘야 하기 때문에 메모리와 처리 전력을 훨씬 더 효율적으로 사용할 수 있도록 해준다. UnitOfWork의 메소드 commit() 이 호출되면, 모든 수정된 객체는 DB 로 커밋된다.

그렇다면, 컬렉션 지향 리파지토리는 여기까지만 이야기하고,
영속성 지향의 리파지토리를 잠깐 얘기해보겠다. 책에서는 컬렉션 지향의 스타일이 맞지 않을 때 영속성 지향의 리파지토리를 사용해야 한다고 한다.
이는 영속성 메커니즘이 암시적, 명시적 둘 다 객체의 변화를 감지하고 추적하지 못할 때 사용한다.
인메모리 데이터 패브릭이나 NoSQL key-value 저장소를 사용할 때 주로 그렇다.
새로운 애그리게잇 인스턴스를 생성하거나 이미 존재하는 대상을 변경할 때마다 save() 나 이와 유사한 메소드를 사용해 데이터를 저장소에 저장해야 한다.


컬렉션 지향 리파지토리는 HashSet 을 흉내냈다면, 영속성 지향 리파지토리는 HashMap 을 흉내 낸 인메모리 Map 이다.
이는 명시적으로 새로 생성하거나, 변경된 객체를 저장소에 put() 해야 한다. 이전에 주어진 키와 연관된 모든 값을 효과적으로 대체해야 한다.

앞서 언급했던 변화를 추적할 수 있는 작업의 단위를 제공하지 않거나 원자적 쓰기를 통제할 수 있는 트랜잭션 경계를 지원하지 않는다.

영속성 지향의 리파지토리와 컬렉션 지향의 주요한 차이점은 클라이언트가 어떻게 메소드를 사용하는가이다. 컬렉션 지향 스타일은 애그리게잇 인스턴스가 생성될 때만 추가한다. 반대로 영속성 지향 스타일은 애그리게잇 인스턴스가 생성될 때와 수정될 때 모두 저장되어야 한다.

아래에서 확인할 수 있듯이, User 를 새로 생성 했을 때, save() 하고 변경할 때 다시 save() 해줘야 한다.

컬렉션 지향 스타일은 생성 시에만 add() 를 해주고, 명시적 혹은 암시적으로 변화를 추적하기 때문에, 추가로 변경사항에 대해서 저장할 필요가 없다.

이밖에도, 세부적인 디테일이 많지만 굵직한 것들만 다루어 보았다.
추가적으로 놓친 부분이 있다면 알려주세요~

댓글

이 블로그의 인기 게시물

About JVM Warm up

About idempotent

About Kafka Basic

About ZGC

sneak peek jitpack

Spring Boot Actuator readiness, liveness probes on k8s

About Websocket minimize data size and data transfer cost on cloud

About G1 GC

대학생 코딩 과제 대행 java, python, oracle 네 번째