티스토리 뷰
개요
# 참고
IMPLEMENTING DOMAIN DRIVEN DESIGN (도메인 주도 설계 구현) - 반 버논 지음
# 개요
DDD에 대한 기술서적을 읽고 DDD가 무엇인지 이해하고 어떻게 코드에 적용시킬 수 있는지 고민해 보려고 한다.
# 12장의 로드맵
- 두 가지 종류의 리파지토리가 무엇인지, 어떤 경우에 어떤 유형을 사용해야 하느지
- 하이버네이트, 탑링크, 로히어런스, 몽고DB를 위한 리파지토리를 어떻게 구현하는지 살펴보자
- 왜 리파지토리의 인터페이스상에 추가적인 행동이 필요한지 이해하자.
- 리파지토리를 사용할 때 트랜잭션이 어떻게 작용하는지
- 타입 계층구조를 위한 리파지토리를 설계할 때의 어려움에 익숙해지자
- 리파지토리와 데이터 액세스 객체 사이의 근본적인 차이점을 살펴보자.
12장. 리파지토리
리파지토리는 보통 저장소의 위치를 말하는데, 주로 그 안에 저장된 항복의 안전이나 보존을 위한 장소로 여겨진다. 무언가를 저장소에 저장하고 나중에 회수하기 위해 돌아왔을 때, 저장할 때와 같ㅇ느 상태로 있길 기대하게 된다.
이런 기본적인 원리들은 DDD 리파지토리에도 적용된다. 애그리게잇 인스턴스를 해당하는 저장소에 저장한 후 나중에 리파지토리를 통해 같은 인스턴스를 가져오게 되면 전체 객체가 기대처럼 다시 만들어진다. 리파지토리에서 가져온 기존의 애그리게잇 인스턴스를 변경하면, 그 변화는 영속성을 갖게 된다. 리파지토리로부터 인스턴스를 삭제하면, 그 시점 이후로는 이를 가져올 수 없다.
모든 영속성 애그리게잇 타입은 리파지토리를 갖게 된다. 일반적으로 애그리게잇 타입과 리파지토리 사이에는 일대일의 관계가 성립한다. 그러나 때로 두 개 이상의 애그리게잇 타입이 객체 계층 구조를 공유할 경우에는 그 타입들이 하나의 리파지토리를 공유할 수도 있다. 12장에선 이 두 접근법을 논의한다.
- 하나의 애그리게잇 타입당 하나의 레파지토리 관계
- 두 개 이상의 애그리게잇 타입이 하나의 레파지토리를 공유
정확히는 애그리게잇만이 리파지토리를 갖게된다. 바운디드 컨텍스트에서 애그리게잇을 사요하지 않는다면, 리파지토리 패턴의 유용함은 줄어들 수 있따. 현재 애그리게잇ㅇ의 트랜잭션 경계를 만들지 않고 애드혹 방식으로 엔티티를 가져와서 사용하고 있따면, 리파지토리의 사용을 꺼려 하는 경향을 보일 수도 있따. 하지만 DDD를 부분적으로만 고려하면서 기수적인 측면에서 일부 패턴만을 도입한 이들도 데이터 액세스 객체보다 리파지토리를 선호 할 수 있다.
컬렉션 지향 설계와 영속성 지향 설계란 두 가지 방식의 리파지토리 설계가 있따. 컬렉션 지향의 설계가 잘 맞는 상황이 있고, 영속성 지향 설계를 사용하는 방향이 최선인 상황이 있따.
- 컬렉션 지향 설계
- 영속성 지향 설계
12-1장. 컬렉션 지향 리파지토리
컬렉션 지향 설계는 전통적인 접근법으로 볼 수 있다. 본래 DDD패턴에 나타난 기본적인 개념에 충실하기 때문이다. 이는 컬렉션을 아주 비슷하게 흉내 내며, 표준 인터페이스 중 적어도 일부를 모사하고 있다. 이 설계는 영속성 메커니즘을 전혀 눈치채지 못하도록 리파지토리 인터페이스를 설계해서, 데이터를 저장소에 저장하거나 영속하다는 생각을 전혀 할 수 없다.
이 설계 접근법은 하위 영속성 메커니즘의 기능 중 일부를 반드시 구현해야 한다. 자바에서 객체는 컬렉션에 추가되며 삭제될 때까지 그 컬렉션에 남아있다. 컬레션에게 특정 객체로의 참조를 요청해 가져온 후에 해당 객체 자체의 상태를 수정하는 요청을 그 객체로 보내기만 하면, 컬렉션이 직접 객체에서 일어나느 변화를 인식하기 위한 추가적인 절차를 수행할 필요가 없다. 여저히 컬렉션은 이전과 같은 객체를 갖고 있는 가운데, 수정 전에 포함됐던 객체의 상태는 그 시점에 비해 변경되게 된다.
리파지토리는 Set 컬렉션을 흉내 내야 한다. 특정 영속성 메커니즘을 지탱하는 구현이 무엇이든, 같은 객체의 인스턴스는 두 번 추가되도록 허용해선 안 된다. 또한 리파지토리로부터 객체를 가져오게 하고 수정할 때 이를 리파지토리에 '재저장'할 필요가 없다.
결국 컬렉션 지향 리파지토리는 영속성 메커니즘이 퍼븍릭 인터페이스를 스스로 전혀 표현되지 않으면서 실제로 컬렉션을 흉내 내고 있음을 알 수 있다. 결국 HashSet이 보여준 특성을 나타내지만 영속성 데이터 저장소와 함께 동작하는 컬렉션 지향 리파지토리를 설계하고 구현하는 것이 우리의 목표다. 이를 위해서 영속성 메커니즘에 몇 가지 특정 기능이 필요하다. 영속성 메커니즘은 어떤 방식으로든 그것이 관리하는 각 영속성 객체에 일어난 변화를 암시적으로 추적하는 기능을 지원해야 한다. 이는 다양한 방법으로 수행할 수 있는데, 다음의 두가지도 그중 일부이다.
# 영속성 객체 변화 추적 방법
1. 암시적 읽기 시 복사
영속성 메커니즘은 저장소로부터 읽어와 재구성할 때 암시적으로 각 영속성 객체의 복사본을 만들고 커밋 시에 클라이언트의 복사본과 자신의 복사본을 비교한다. 영속성 메커니즘이 데이터 저장소에서 하나의 객체를 읽어오도록 요청하면 이를 수행하면서 그 즉시 전체 객체의 복사본을 만들게 된다. 영속성 매커니즘을 통해 생성된 트랜잭션이 커밋될 때, 해당 영속성 메커니즘은 가져온 복사본을 비교해 수정 여부를 확인한다. 변경이 발견되 모든 객체는 데이터 저장소에 해당 내용을 반영시킨다. 조금 이해가 안 된다면 JPA의 변경감지를 찾아보자. 변경감지와 동작 원리가 비슷하다.
2. 암시적 쓰기 시 복사
영속성 메커니즘은 모든 로드된 영속성 객체를 프록시를 통해 관리한ㄷ다. 각 객체가 데이터 저장소로부터 로드되면, 얇은 프록시가 생성되고, 클라이언트로 전달된다. 클라이언트는 프록시의 존재를 눈치채지 못한 상태로 프록시 객체의 행동을 호출하게 되고, 이는 진짜 객체의 행동을 반영하게 된다. 프록시의 메소드가 처음으로 호출되는 시점에 객체의 복사본을 만들어 관리하게 된다. 프록시는 관리되는 객체의 상태에 일어난 변화를 추적해 더티로 표시한다. 영속성 메커니즘을 통해 생성된 트랜잭션이 커밋되면, 더티한 객체를 모두 찾아서 데이터 저장소로 반영시킨다.
이 모든 접근법의 전반적인 장점은 영속성 객체의 변경이 암시적으로 추적되기 때문에, 영속성 메커즘에게 변경을 인식하도록 하기 위해 명시적인 클라이언트 정보나 관여가 필요하지 않다는 것이다. 여기서 결론은 하이버네이트와 같은 영속성 매커니즘을 이렇게 사용함으로써 기존의 컬렉션 지향 리파지토리를 사용할 수 있게 된다는 점이다.
(JPA - update x, 변경감지)
하지만 암시적으로 복사해 변화를 추적하는 하이버네이트와 같은 영속성 멬터니즘을 사용할 자유가 주어지더라도 적절하지 않은 상황도 있다. 아주 많은 객체를 메모리로 가져와야 하며 매우 고성능의 도메인이 필요하다면, 이런 종류의 매커니즘은 메모리와 실행 모두에 불필요한 오버헤드를 더하기 때문에 신중하게 결정해야 한다. (JPA - 즉시로딩, 지연로딩)
이와 같은 상황에서 좀 더 최적화돼 동작하는 컬렉션 지향의 리파지토리 지원 객체 관계형 매핑 도구를 도이하려 할 수 있다. 오라클의 탑링크와 이클립스링크가 있다. 하이버네이트와 많이 다르지는 않지만 탑링크는 작업 단위는 읽기 시 암시적 카피를 만들지 않다. 대신 명시적으로 쓰기 전 카피를 만든다. 여기서 명시적이란 클라이언트가 작업 단위에 변화가 일어난다는 점을 반드시 알려야 한다는 의미다. 하지만 탑링크는 오직 필요할 때만 메모리를 소비한다는 특징이 있다.
12-2. 하이버네이트 구현
어떤 방향으로 리파지토리를 생성하든 두 가지 주요 단계를 거치게 된다. 하나의 퍼블릭 인터페이스와 더불어 한 가지 이상을 구현을 정의해야 한다.
특히 컬렉션 지향 설계의 경우, 첫 번째 단계에서 컬렉션을 흉내 내는 인터페이스를 정의하고, 두 번쨰 단계에선 하이버네이트와 같은 지원하는 주요 저장소 메커니즘을 다루는 구현을 제공해야 한다.
개인적으로 컬렉션처럼 save(), add(), remove() 같은 메소드에서 boolean 결과로 돌려주는 방식을 선호하지 않는다. 그 이유는 인스턴스를 추가해주는 유형의 오퍼레이션에선 true라는 반환 값이 성공을 보장하지 않는 경우도 있기 때문이다. true라는 결과를 받았더라도 여전히 데이터 저장소로의 트랜잭션 커밋 대상이 될 수 있따. 그러므로 리파지토리의 경우에는 void가 좀 더 정확한 타입일 수 있다.
하나의 트랜잭션에서 다수의 애그리게잇 인스턴스를 추가하거나 삭제하는 것이 적절하지 않은 상황도 있을 수 있따. 도메인에서 이런 상황이 발생하면 addAll( ), removeAll( ) 같은 메소드는 포함시키지 말자. 이런 메소드는 편의를 위해서만 제공될 뿐이다. 클라이언트는 반복문을 사용해 자신이 갖고 있는 컬렉션을 순회하면서 add( )나 remove( )를 호출할 수 있따. 따라서 addAll( )과 removeAll( ) 메소드의 제거는 상징적인 전책일 뿐이며, 하나의 트랜잭션에서 다수의 객체를 추가하거나 삭제하는 것을 감지하는 수단을 설치하지 않는 한 설계의 측면에서 강제할 수는 없다. 이를 위해선 각 트랜잭션마다 새로운 리파지토리를 인스턴스화해야 할 가능성이 높은데, 이는 잠재적으로 큰 비용을 초래 할 수 있다.
일대일 매핑을 사용하는 애그리게잇 삭제와 관련해 한 가지 추가적인 세부사항이 있는데, 식별자와 액세스 컨텍스트에서 필요하다. 이런 관계에서 변경을 연속해 전팔할 수는 없으므로, 양쪽 연결 모두의 객체를 명시적으로 삭제해야 한다. 안쪽 Person객체가 먼저 삭제돼야 하고, User 애그리게잇 루트가 삭제돼야 한다. 내부 Person 객체를 삭제하지 않으면, 해당하는 데이터베이스 테이블에서 이르 잃어버리게 된다. 일발적으로 이런 이유로 일대일 연결을 피해야 하고, 대신 제한된 단일 다대일 단방향 연결을 사용해야 한다. 그러나 의도적으로 일대일 양방향 구현을 해서 문제가 되는 매핑을 사용해 보고자 한다.
12-4. 영속성 지향 레파지토리
컬렉션 지향의 스타일이 맞지 않을 때는 영속성 지향의 저장 기반 리파지토리를 사용해야 한다. 영속성 메커니즘이 암묵적으로나 명시적으로나 객체의 변화를 감지하고 추적하지 못할 때가 이런 상황이다.
컬렉션 지향 접근법을 지원하는 객체 관계적 매퍼를 사용한다고 할지라도 영속성 지향의 접근법을 선택할 때 고려해야 할 사항이 한가지 더 있다. 만약 컬렉션 지향의 리파지토리를 설계했는데 관계적 데이터베이스를 키-값 저장소로 바꾸기로 결정한다면 어떻게 될까? 애플리케이션 계층에 많은 파문이 생길 것이데, 이는 애그리게잇 업데이트가 생기는 위치마다 save( )를 사용하도록 변경해야 하기 때문이다.
영속성 지향의 리파지토리는 리파지토리 패터이 애플리케이션에 영향을 거의 미치지 않고 영속성 메커니즘을 완벽히 대체할 수 있게 해준다는 장점이 있지만, 반대로 객체 관계형 매퍼로 인해 더 이상 지원하는 작업의 단위가 없을 때에서야 알 수 있는 save( )의 필수적인 사용처를 빠트릴 수 있다는 점이다.
1. 젬파이어나 오라클 코히어런스와 같은 인메모리 데이터 패브릭을 사용하느 상황에서의 저장소는 HashMap을 흉내 낸 인메모리 Map인데, 각 매핑된 컴포넌트는 엔트리로 간주된다.
2. 몽고DB나 리악과 같은 NoSQL 저장소를 사용할 때 객체 영속성은 테이블이나 열이나 행이 아닌 컬렉션과 같아 보이는 착각을 일으킨다.
이들은 키-값 짝을 저장한다. 이는 Map과 유사한 효과적인 저장소이지만 주메모리 저장소의 도구로 메모리 대신 디스크를 사용한다. 영속성 메커니즘의 두 가지 스타일 모두 Map 컬렉션을 어느 정도 흉내 내고 있음에도 불구하고, 불행하게도 새롭고 변경된 객체를 이전에 키와 연관돼 있던 모든 값을 효과적으로 대체하면서 저장소로 put( ) 해야 한다. 변경된 객체가 노리적으로 이미 저장된 객체와 같은 객체더라도 그렇게 해야 하는데, 그 이유는 이들이 전형적으로 변화를 추적할 수 있는 작업의 단위를 제공하지 않거나 원자적 쓰기를 통제할 수 있는 트랜잭션 경계를 지원하지 않기 떄문이다. 이런 종류의 데이터 저장소 중 하나를 사용하는 것은 애그리게잇의 기본적 쓰기와 읽기를 대단히 단순화한다.
젬파이어나 코히어런스의 캐시, 몽고DB나 리악의 키-값 저장소, 그 밖의 몇몇 다른 종류의 NoSQL 영속성을 사용할 때, 여러분은 아마도 애그리게잇을 직렬화된/문서 형태로 변화시킨 후 다시 객체 형태로 되돌릴 빠르고 편리한 도구를 사용하고 싶을 것이다. 그렇다면 이 도전을 뛰어넘는 일은 그렇게 어렵지 않다. 예를 들면 젬파이어나 코히어런스에 의해 영속된 애그리게잇을 위해 최적의 직렬화를 생성하는 일은 객체 관계형 매퍼를 위한 매핑 내용을 생성하는 일보다 쉽다. 그러나 put( )과 get( )을 Map상에서 사용하는 것만큼 쉽지는 않다.
12-5. 트랜잭션의 관리
도메인 모델과 이를 둘러 싸는 도메인 계층은 트랜잭션을 관리하기에 올바른 장소가 아니다. 모델과 연관된 오퍼레이션 자체가 트랜잭션을 관리하기에는 일반적으로 그 단위가 너무 작고, 그들의 수명주기에 트랜잭션이 영향을 미쳐서도 안 된다. 모델 안에서 트랜잭션의 문제를 다루지 않도록 해야 한다면 도대체 어디서 이를 처리해야 할까?
도메인 모델의 영속성 측면에서 트랜잭션을 관리하는 일반적인 아키텍처적 접근은 애플리케이션 계층 내에서 관리하는 것이다. 일반적으로 애플리케이션/시스템에서 처리할 각 주요 유스케이스 그룹당 하나의 파사드를 애플리케이션 계층에 생성한다. 파사드는 큰 단위의 비즈니스 메소드로서 보통 각 유스케이스 흐름마다 하나씩 설계한다. 이런 각 비즈니스 메소드는 유스케이스의 요구에 따라 태스크를 조정한다. 비즈니스 메소드는 트랜잭션을 시작하고 도메인 모델의 클라이언트로서 동작한다. 도메인 모델과의 필요한 모든 상호작용이 완료되면, 파사드의 비즈니스 메소드는 자신이 시작한 트랜잭션을 커밋한다.
트랜잭션의 관리는 선언적일 수도 있고 개발자의 코드에 의한 명시적 방식일 수도 있다. 트랜잭션이 선어적이든 사용자 관리에 따르든, 앞서 설명한 부분은 논리적 측면에서 다음과 같이 작동하게 된다. 트랜잭션 내에서 도메인 모델에 일어난 변화를 등록하기 위해 리파지토리의 구현이 애플리케이션 계층이 시작한 트랜잭션과 같은 세션이나 작업 단위에 접근할 수 있는지 확인하자. 이에 따라 도메인 계층에 발생하는 수정사항이 내부 데이터베이스로 적절하게 커밋되거나 롤백된다.
하이버네이트에서 트랜잭션을 사용하는 방법은 선언적 트랜잭션 방식으로 어노테이션을 이요하여 관리할 수 있따. 이에 대한 설명은 생략하도록 하겠습니다. 선언적 어노테이션에 대해 궁금하거나 트랜잭션을 구현하는 방법이 궁금하다면 구글링을 해보자.
트랜잭션을 과도하게 사용하는 것에 대해 마지막 경고를 남겨야 할 의무감을 느낀다. 애그리게잇은 올바른 일관성 경계를 확보하기 위해 신중하게 설계돼야 한다. 단인 트랜잭션에서 여러 애그리게잇의 수정을 커밋하는 기능을 과도하게 사용하지 않도록 주의하자.
12-6. 타입 계층
객체지향 언어를 사용해서 도메인 모델을 개발할 때, 타입 계층 구조를 만들기 위해 상속을 사용하려는 유혹에 빠질 수 있따. 이를 기본 클래스 안에 기본값 상태와 행동을 두고 서브클래스를 사용해 확장해나갈 기회로 생각할 수 있다.
여기서는 공통의 도메인 특성 슈퍼클래스를 확장하는 상대적으로 적은 수의 애그리게잇 타입을 생성하는 내용을 다룬다. 이는 상호 교체가 가능하고 다형성의 특징을 가진 근접하게 연결된 타입의 계층구조를 형성하도록 설계된다. 이런 계층구조에선 클라이언트가 인스턴스를 상호 교체 가능하도록 사용해야 하기 때문에 단일 리파지토리를 사용해 별도 타입의 인스턴스를 저장하고 가져오도록 하는데, 클라이언트는 언제든지 그들이 다루고 있는 특정 서브클래스에 대해 전혀 알고 있을 필요가 없으며 이는 리스코프 치환 원칙을 반영한다.
상속이 사용된다고 하더라도, 애그리게잇 다형성 행동은 어떤 특별한 경우도 클라이언트에게 노출되지 않도록 신중하게 설계돼야 한다.
12-7. 리파지토리 대 데이터 액세스 객체
리파지토리의 개념은 데이터 액세스 객체(DAO)와 동의어로 간주된다. 둘 다 영속성 메커니즘의 추상적 개념을 제공한다. 그러나 객체 관계형 매핑 도구 역시 영속성 메커니즘의 추상적 개념을 제공하지만 이는 리파지토리도 DAO도 아니다. 따라서 모든 영속성 추상 개념을 DAO라고 부르진 않는다.
일반적으로 리파지토리와 DAO 사이에 차이점이 있다고 생각합니다. 기본적으로 DAO는 데이터베이스 테이블에 따라 표현되며 CRUD 인터페이스를 제공한다. DAO 같은 장치의 사용을 도메인 모델과 함께 사용하는 자치들과 구분한다. 한편, 객체 선호도를 갖는 리파지토리와 데이터 매퍼는 도메인 모델과 함께 사용되는 전형적인 패턴이다.
애그리게잇의 일부로 간주됐을 데이터상에 작은 단위의 CRUD 오퍼레이션을 수행하기 위해 DAO를 비롯한 관련 패턴을 사용할 수 있기 때문에, 이는 도메인과 함께 사용되는 상황을 피해야 하는 패턴이다. 일반적인 상황에선 애그리게잇 스스로가 자신의 비즈니스 로직과 기타 냅부 사항을 직접 관리하는 가운데 그 밖의 요소는 모두 제외하도록 하는 편이 바람직하다.
저장된 프로시저나 데이터 그리드 엔트리 처리기는 요구되는 비기능적 요구사항을 맞추기 위해 필수적인 경우가 있다고 언급했다. 그러나 시스템의 비기능적 요구사항이 이를 주도하지 않는다면 차라리 피하는 편을 권장한다. 비즈니스 로직을 데이터 저장소에 두고 실행하게 되면 많은 경우에 DDD와 대치된다. 저장된 프로시저를 과다하게 사용하면 잠재적으로 DDD에 방해가 되는데, 이는 일반적으로 모델링 팀이 프로그래밍 언어를 잘 이해하기가 어렵고 그들의 시각에 맞춰 안전하게 구현을 마무리하기가 어려기 때문이다. 이런 상황에선 DDD가 이루고자하는 바에 정확히 반대 방향으로 움직이게 된다.
리파지토리를 일반적 관념에서 DAO로 생각할 수도 있다. 주요한 점은 가능한 데이터 액세스 지향보다는 컬렉션 지향으로 설계하려고 노력해야 한다는 점이다. 이는 데이터가 아닌 모델로서 도메인에 집중하는 가운데 보이지 않는 뒤편에서 사용되는 CRUD 오퍼레이션을 통해 영속성을 관리하도록 해준다.
'DDD' 카테고리의 다른 글
14장. 애플리케이션 / IDDD (도메인 주도 설계 구현) (0) | 2022.03.17 |
---|---|
13장. 바운디드 컨텍스트 통합 / IDDD (도메인 주도 설계 구현) (0) | 2022.03.17 |
11장. 팩토리 / IDDD (도메인 주도 설계 구현) (0) | 2022.03.16 |
10장. 애그리게잇 / IDDD (도메인 주도 설계 구현) (0) | 2022.03.16 |
9장. 모듈(패키지) / IDDD (도메인 주도 설계 구현) (0) | 2022.03.16 |