-
도메인 주도 설계의 적용-4.ORM과 투명한 영속성 3부[옛날 글들] 도메인 주도 설계 2024. 5. 1. 07:36728x90
이전글 : 도메인 주도 설계의 적용-4.ORM과 투명한 영속성 2부
이 글은 제가 2008년 6월부터 10월까지 5개월간 마이크로소프트웨어에 연재했던 "도메인 주도 설계의 적용"이라는 원고의 원글입니다. 잡지에 맞추어 편집을 하는 과정에서 지면 상의 제약으로 인해 수정되거나 삭제된 부분이 있어 제 블로그에 원글을 올립니다. 도메인 주도 설계(Domain-Driven Design)에 관심 있는 분들에게 도움이 되었으면 합니다.
객체 관계 맵핑과 도메인 모델
3부에서 살펴본 바와 같이 상태와 행위를 함께 가지는 풍부한 객체 모델로 도메인 레이어를 구성하는 것을 도메인 모델(DOMAIN MODEL) 패턴이라고 한다. 도메인 모델 패턴은 상속, 캡슐화, 다형성과 같은 객체 지향의 장점을 십분 활용함으로써 수정이 용이하고 확장성이 높으며 이해하기 쉬운 시스템을 개발할 수 있도록 한다.도메인 모델 패턴이 객체 지향의 모든 특징을 활용하기 때문에 영속성 메커니즘을 주도하는 관계형 데이터베이스와의 임피던스 불일치 문제가 발생한다. 임피던스 불일치를 해결하는 가장 좋은 방법은 도메인 로직을 처리하는 도메인 레이어와 영속성 로직을 처리하는 퍼시스턴스 레이어 간에 불일치를 조정하는 중간 레이어를 도입하는 것이다. 중간 레이어가 불일치 사항들을 조정함으로써 객체와 테이블이 독립적으로 발전할 수 있다. 이처럼 객체와 관계형 데이터베이스 간의 독립성을 보장할 수 있도록 객체와 데이터베이스 테이블 간의 데이터를 이동시키는 객체를 데이터 매퍼(DATA MAPPER)라고 한다.
데이터 매퍼를 구현한 소프트웨어를 ORM(Object-Relational Mapper)이라고 한다. ORM은 객체와 데이터베이스 테이블 간의 매핑 정보를 XML 파일이나 Java 5 Annotation 등의 메타 데이터로 표현할 수 있도록 메타데이터 매핑(METADATA MAPPING)을 지원한다. 도메인 객체 자체는 자신이 데이터베이스에 저장된다는 사실 자체를 알지 못 한다 객체가 데이터베이스에서 조회된 정보를 저장하고 있는지 아니면 메모리에 임시로 생성된 일시적인 상태를 표현하고 있는 지와 무관하게 도메인 로직을 작성할 수 있다. 도메인 레이어는 메모리에 생성된 객체 그래프를 대상으로 로직을 실행한다. 이 과정에서 하부의 영속성 메커니즘이 개입하지만 도메인 레이어의 객체들은 이 영속성 메커니즘에 대해 투명하다. 이처럼 도메인 객체가 하부의 영속성 메커니즘에 독립적인 특징을 투명한 영속성(transparent persistence)이라고 한다.
도메인 객체가 영속성에 대해 투명해지기 위해서는 비침투적인 인프라 스트럭처를 사용해야 한다. EJB3.0 이전의 엔티티 빈과 같은 초기의 미숙한 ORM 기술들은 도메인 객체에 대해 침투적이었다. 따라서 도메인 객체는 하부 인프라 스트럭처의 특정 클래스나 인터페이스와 강한 결합도를 가졌으며, 이로 인해 개발 복잡도 증가, 단위 테스트 및 재사용성 저해, 개발 리듬의 저하와 같은 다양한 문제점이 제기되었다. 최근의 ORM 제품 또는 JPA와 같은 스펙은 도메인 객체에 비침투적인 기법을 사용함으로써 투명한 영속성(transparent persistence)을 지원한다.정교한 ORM은 로드된 객체들의 상태 변경을 자동으로 감지하고 트랜잭션 커밋(commit) 시에 변경된 내용을 데이터베이스에 저장한다. 이를 위해 ORM 내부에서는 작업 단위(UNIT OF WORK)가 현재 트랜잭션 내에서 비즈니스 연산에 의해 수정된 객체들의 집합을 유지한다. 트랜잭션 수행 동안 변경된 객체들은 커밋 시점에 더티(dirty) 상태에 있다고 한다. 작업 단위는 실제로 변경된 객체들에 대해서만 update문을 실행시킨다. 이처럼 데이터베이스 커밋 시점에 더티 상태에 있는 객체들을 자동으로 데이터베이스에 저장하는 ORM의 특징을 자동 더티 체킹(automatic dirty checking)이라고 한다.
ORM은 동일 트랜잭션 내에서 동일한 객체들이 한번만 로드될 수 있도록 하기 위해 식별자 맵(IDENTITY MAP)을 유지한다. 객체를 로드하라는 요청을 받으면 ORM은 내부의 식별자 맵을 조사한다. 만약 식별자 맵에 존재하지 않으면 이를 데이터베이스로부터 로드하고 식별자 맵에 추가한다. 식별자 맵에 존재할 경우 데이터베이스에 요청을 보내지 않고 식별자 맵에 저장되어 있는 객체를 반환한다. 식별자 맵을 사용하면 동일 트랜잭션 내에서 객체들을 캐싱함으로써 성능을 향상시키는 동시에 자동으로 트랜잭션의 반복 가능한 읽기(REPEATABLE READ) 격리 레벨의 특성을 얻을 수 있다. 또한 동일한 트랜잭션 내에서는 항상 동일한 객체가 반환되기 때문에 데이터베이스 식별자가 동일한 객체의 경우 객체 식별자도 항상 동일하게 유지시켜 준다. 즉, 트랜잭션 범위 내에서는 객체 식별자를 비교하는 “==” 연산자와 데이터베이스 식별자를 비교하는 equals() 메소드의 결과가 동일하다.
일반적으로 식별자 맵은 작업 단위 내에 위치한다. 트랜잭션 확약 시에 작업 단위는 식별자 맵에 저장된 모든 객체들의 변경 상태를 확인한 후 모든 외래 키 제약 조건을 위반하지 않으면서도 가장 효율적인 SQL문 조합을 생성한다. 이를 트랜잭션 이면 쓰기(transactional write-behind)라고 한다.
객체들이 최초에 로드될 때 데이터베이스 접근 코드는 객체 그래프의 어떤 부분들 까지 사용될 지 알 수 없다. 따라서 접근되지 않을 수도 있는 객체들을 모두 로딩하는 것은 매우 비효율적이며 성능에 문제를 일으킬 것이다. 따라서 최초에 필요한 객체만을 로드한 후 나머지 객체들은 연관 관계를 통한 항해를 통해 필요 시점에 로딩하는 것이 효율적일 것이다. 이처럼 객체를 필요한 시점에 데이터베이스로부터 로드하는 것을 지연 로딩(LAZY LOADING)이라고 한다.
주문 시스템의 Order 애그리게이트를 살펴 보자. 애그리게이트의 정의에 따라 Order 애그리게이트 내에서 전역적으로 접근할 수 있는 객체는 엔트리 포인트인 Order 뿐이다. 따라서 OrderRepository를 통해 Order만을 로드하고 OrderLineItem은 필요 시 Order로부터의 항해를 통해서만 접근 가능하다. 이것을 ORM 관점에서 살펴보면 OrderRepository는 데이터베이스로부터 Order 객체만을 로드하고 이를 작업 단위 내의 식별자 맵에 추가한다. 후에 Order로부터 OrderLineItem으로 연관을 통해 항해가 일어날 경우 ORM은 지연 로딩 기법을 사용하여 OrderLineItem을 로딩한다. 따라서 애그리게이트와 엔트리 포인트 별 리포지토리할당의 배후에는 구현 기술로서의 지연 로딩이 위치하고 있다.
그림 1 OrderLineItem은 Order로부터의 항해를 통해서만 로드된다. 즉, 지연 로딩된다.
고객이 새로운 주문을 입력했다고 하자. 시스템 내부에서는 새로운 트랜잭션이 시작되고 Order 객체와 OrderLineItem이 생성된 후 Order에 추가된다. Order 객체가 생성은 되었지만 단지 메모리 상의 객체로만 존재하고 데이터베이스와 관계가 맺어지지 않은 상태를 비영속 상태(transient state)라고 한다. 어플리케이션은 트랜잭션 종료 시점에 OrderRepository.save()를 호출하여 생성된 Order를 데이터베이스에 저장한다. 이 때부터 Order는 데이터베이스 테이블의 한 레코드로 저장되는데 이처럼 데이터베이스와 연관 관계를 가지고 있는 상태를 영속 상태(persistence state)라고 한다.
이제 OrderLineItem에 대해 생각해 보자. 앞에서 OrderRepository를 통해 Order만을 저장했을 뿐 OrderLineItem에 대한 영속성 로직은 처리하지 않았다. 이 경우 Order만 저장되고 OrderLineItem은 데이터베이스에 저장되지 않을까? ORM은 영속성 전이(transitive persistence)라는 특징을 지원한다. 영속성 전이는 영속 객체와 연관된 객체들에게 영속성이 전이된다는 것을 의미한다. 따라서 영속 객체인 Order와 연관된 OrderLineItem 역시 영속 객체가 되며 따라서 트랜잭션 확약 시 자동으로 데이터베이스에 저장된다. 삭제의 경우도 마찬가지이다. 영속 객체인 Order가 삭제될 경우 연관 관계로 묶인 OrderLineItem 역시 함께 삭제된다. 이것은 애그리게이트의 불변식을 보장하기 위해 애그리게이트 전체가 하나의 단위로 처리돼야 하고 애그리게이트 내부 객체들이 엔트리 포인트의 생명주기에 종속된다는 개념을 지원한다. 즉, 애그리게이트의 생명주기와 관련된 제약사항의 구현 메커니즘으로 ORM의 영속성 전이를 사용할 수 있다.
도달 가능성에 의한 영속성(persistence by reachability)은 어떤 영속 객체로부터 도달 가능한 모든 객체들이 영속 객체가 된다는 것을 의미한다. 즉, 어떤 객체가 영속 객체라면 연관된 객체는 무조건 영속 객체가 되는 것이다. 일반적인 ORM 솔루션들은 이 특징을 완전하게 지원하지는 않는다. 대신 영속성을 전이 시킬지를 매핑 시에 설정하게 함으로써 더 세밀한 제어가 가능하도록 한다. 따라서 ORM을 사용할 경우 영속 객체와 연관된 객체라고 해서 무조건 함께 저장되거나 삭제되지 않는다는 사실에 주의하자.
ORM과 관련해서 살펴볼 마지막 이슈는 값 객체의 매핑이다. 값 객체는 식별자를 가지지 않으며 엔티티의 생명주기에 종속된다. 값 객체는 속성 값이 같은 경우 동등한 것으로 판단한다. 일반적으로 엔티티가 별도의 테이블로 매핑되는 반면 값 객체는 자신이 속한 엔티티의 단순 컬럼으로 매핑된다. 이를 포함 값(EMBEDDED VALUE) 패턴이라고 한다.
주문 시스템에서 Product 엔티티를 PRODUCTS 테이블에 매핑하는 경우를 살펴 보자. Product는 상품의 가격을 나타내기 위해 값 객체인 Money를 속성으로 포함한다. 이 경우 값 객체인 Money를 별도의 테이블로 매핑하지 않고 PRODUCTS 테이블의 한 컬럼으로 매핑한다.
그림 2 VALUE OBJECT는 ENTITY가 매핑되는 테이블의 컬럼으로 매핑된다.
이제 주문 시스템의 리포지토리를 구현하기 위해 필요한 모든 기본 개념들을 살펴 보았다. 자, 서두르자. 성난 사용자들의 항의글이 게시판을 뒤덮고 있다.
728x90'[옛날 글들] 도메인 주도 설계' 카테고리의 다른 글
도메인 주도 설계의 적용-4.ORM과 투명한 영속성 5부[끝] (0) 2024.05.08 도메인 주도 설계의 적용-4.ORM과 투명한 영속성 4부 (0) 2024.05.02 도메인 주도 설계의 적용-4.ORM과 투명한 영속성 2부 (5) 2024.04.24 도메인 주도 설계의 적용-4.ORM과 투명한 영속성 1부 (0) 2024.04.23 도메인 주도 설계의 적용 - 3. 의존성 주입과 관점 지향 프로그래밍 5부 (1) 2024.04.20