마틴 파울러의 엔터프라이즈 애플리케이션 아키텍처 패턴을 읽고 정리한다.
1부 이야기에서는 다양한 개념과 발전 과정을 말한다.
작성 시점은 20년 전으로 당시와 지금의 용어가 약간 다를 수 있고, 더 이상 사용 안 하는 것도 있을 수도 있다.
자세한 내용이 궁금하면 읽어보는 걸 권한다.
시작하면서
데이터 원본 계층의 역할은 애플리케이션이 작업을 수행하는 데 필요한 인프라의 다양한 부분과 통신하는 것이며, 이 계층에서 가장 중요한 일은 데이터베이스와 상호작용이다. 여기서 데이터베이스란 일반적으로 관계형 데이터베이스를 뜻한다.
관계형 데이터베이스가 성공한 이유는 표준화된 SQL이 있었기 때문이다.
아키텍처 패턴
첫번째 패턴 집합은 도메인 논리가 데이터베이스와 상호작용하는 방법을 좌우하는 아키텍처 패턴으로 구성돼 있다. 이런 패턴의 선택은 전체 설계에 미치는 파급효과가 크고 리팩터링이 어려워 충분한 주의를 기울여야 한다.
SQL은 널리 사용되고 있지만 문제가 없는 것은 아니다. SQL을 이해하지 못해 어려움을 겪는 개발자도 많다. SQL을 프로그래밍 언어에 삽입하는 다양한 기법이 있지만 불편하다. 데이터베이스 관리자(DBA)도 올바른 데이터베이스를 조율하고 인덱스를 구성하는 방법을 이해하려면 SQL을 배워야한다.
이러저러한 이유로 SQL 접근을 도메인 논리와는 별도로 분리하고 개별 클래스에 배치하는 게 좋다. 이런 클래스는 테이블에 대한 게이트웨이가 된다.
게이트웨이를 사용하는 데 두 가지 방법이 있다. 가장 확실한 방법은 쿼리가 반환하는 각 행마다 인스턴스를 하나씩 만드는 것이다. 이를 행 데이터 게이트웨이라 부르며 객체지향적 사고방식과 잘 어울린다.
레코드 집합은 데이터베이스의 테이블식 특성을 흉내 낸 테이블과 행의 범용 자료구조로서 다양한 환경에서 폭넓게 지원한다. 테이블 데이터 게이트웨이는 이런 레코드 집합을 반환하는 메서드를 제공한다.
도메인 모델은 행 데이터 게이트웨이나 테이블 데이터 게이트웨이를 함께 쓸 수 있지만 두 방법은 부족함이 많다.
간단한 애플리케이션에서 도메인 모델은 데이터베이스 테이블 당 도메인 클래스 하나를 사용해 데이터베이스 구조에 아주 근접하게 대응되는 간단한 구조다. 도메인 모델에는 비즈니스 논리를 포함하는 경우가 많다. 따라서 활성 레코드를 사용하는 것이 적절하다.
도메인 논리가 복잡해지면 도메인 모델도 점차 무거워지고, 도메인 논리를 작은 클래스로 분리하기 시작하면 도메인 클래스와 테이블의 일대일 매칭이 깨지기 시작한다.
도메인 모델이 복잡해질수록 도메인 객체와 데이터베이스 테이블 매핑 간의 간접 계층을 통해 분리하는 것이 나은 방법이다.
데이터 매퍼는 두 계층의 완전한 격리라는 장점을 제공한다.
참고사항 : 여기서 말하는 테이블은 테이블뿐만 아니라 뷰/쿼리/저장 프로시저를 모두 뜻한다.
동작 문제
사람들이 O/R 매핑에 대해 말할 때 주로 구조적 측면, 즉 테이블이 객체와 연관되는 방법에 집중한다. 그러나 가장 어려운 측면은 O/R 매핑의 아키텍처와 동작 측면이다.
동작 문제는 객체가 데이터베이스에 저장 및 로드되는 방법에 대한 것이다. (객체방식으로 설계하면 데이터베이스 테이블 구조와 객체 간의 괴리감을 생각해보자.)
하나의 일련의 작업으로 다수의 객체를 메모리로 로드하고 수정할 때는 객체를 데이터베이스에 올바르게 기록하기 위해 관련 객체를 모두 추적해야 한다. 특히 다른 행을 참조하는 경우 문제가 더욱 복잡해진다.
객체를 읽고 수정하는 동안 사용하는 데이터베이스의 상태를 일관되게 유지해야 한다. 이것은 동시성(concurrency)이라는 해결하기 매우 까다로운 문제다.
작업 단위는 이러한 문제를 해결하기 위한 필요한 패턴이다. 작업 단위는 데이터베이스에서 읽은 객체와 수정된 객체를 추적하고 데이터베이스를 업데이트한다. 개발자는 저장 메서드를 직접 호출하지 않고 작업 단위에게 커밋을 요청하면 된다.
작업 단위는 데이터베이스 매핑의 컨트롤러로 작동하는 객체라고 생각하면 이해하기 쉽다.
객체를 로드할 때는 같은 객체를 두번 로드하지 않게 주의해야 한다. 같은 객체를 두번 로드하고 업데이트하면 예상치 못한 결과가 발생할 수 있다. 이를 해결하기 위한 방법으로 식별자 맵을 이용할 수 있다. 데이터를 읽을 때마다 식별자 맵을 이용 해 확인하면 된다. 이미 읽은 경우 식별자 맵이 해당 데이터 참조를 반환한다.
도메인 모델을 사용할 때는 데이터베이스에서 객체를 로드할 때 연관된 객체가 함께 로드되도록 구성하는 것이 일반적이다. 하지만 특정 로직에서는 일부분만 사용할 경우 불필요한 객체까지 조회하는 것을 방지하기 위해 지연 로드 방법을 사용한다.
데이터 읽기
데이터를 읽는 메서드를 SQL select문을 메서드 구조의 인터페이스로 래핑하는 검색기(finder) 메서드라고 생각한다.
검색기 메서드의 위치는 인터페이스 패턴에 따라 달라진다.
데이터베이스와 상호작용하는 클래스가 테이블 기반인 경우 테이블마다 인스턴스가 하나씩 있으므로 검색기 메서드를 삽입 및 업데이트와 결합하여 사용할 수 있다.
행 기반인 경우 행마다 상호작용 클래스가 있어서 결합된 방법은 쓸 수 없다.
행 기반 클래스를 사용하는 경우, 테스트할 때 데이터베이스를 서비스 스텁(목)으로 대체할 수 없다. 가장 좋은 방법은 검색기 객체를 별도로 만드는 것이다.
데이터를 읽을 때는 성능 문제를 인식해야 하며, 몇 가지 규칙을 알아 두자.
- 여러 행을 한 번에 읽는다. 특히 같은 테이블에서 여러 행을 읽기 위해 반복적으로 조회하는 일이 없게 해야 한다.
- 데이터베이스 접근을 줄이는 다른 방법은 조인을 사용해 여러 테이블을 한번에 조회한다. 단, 조인을 많이 사용하면 오히려 성능이 저하된다.
- 다양한 최적화를 지원하는 데이터의 클러스터링, 세심한 인덱스 사용, 메모리 캐시 활용 등을 참고하자.
구조적 매핑 패턴
객체-관계형 매핑을 이야기하면 객체와 데이터베이스 테이블을 매핑하는 구조적 매핑 패턴을 의미하는 경우가 많다.
관계 매핑
객체와 관계형 데이터베이스가 연결을 처리하는 방법에는 차이가 있으며, 두 가지 문제가 발생한다.
- 참조 방법의 차이 : 객체는 참조를 저장하는 방법으로 연결을 처리한다. 관계형 데이터베이스는 다른 테이블에 대한 키를 생성해 연결을 처리한다.
- 객체와 테이블 사이에서 자료 구조가 반전되는 현상 : 객체는 컬렉션으로 여러 참조를 처리할 수 있지만, 관계형은 정규화 때문에 모든 연관 링크가 단일 값을 가져야 한다.
참조 방법의 차이를 해결하려면 각 객체의 관계형 식별자를 객체의 식별자 필드로 유지하고 매핑을 처리하면 된다. 테이블에서 외래 키가 나올 때마다 외래 키 매핑으로 객체 간 참조를 적절하게 구성한다.
컬렉션을 처리할 때는 복잡한 외래 키 매핑이 필요하다. 객체에 컬렉션이 있으면 원본 객체의 ID와 연결된 모든 행을 찾기 위해 다른 쿼리를 수행하여, 검색되는 객체를 컬렉션에 추가한다.
양쪽 끝에 컬렉션이 있는 다대다 관계는 다른 방법이 필요하다. 연관 테이블 매핑을 사용해 때다 관계를 처리하는 새로운 테이블을 만들어야 한다.
그리고 주의 사항이 존재한다.
- 컬렉션을 사용할 때 순서 의존을 조심해야 한다. 객체지향 언어에는 순서가 있는 컬렉션을 사용하지만 관계형 데이터베이스에 저장할 때 순서를 유지하기 힘들다. 조회할 때 정렬 순서를 지정하는 것도 방법이지만 성능 저하가 될 수 있다.
- 참조 무결성 때문에 업데이트가 복잡해질 수 있다. 최신 시스템이면 참조 무결성 검사를 트랜잭션의 끝으로 연기하는 기능이 있어 이를 사용하면 괜찮지만, 그렇지 않으면 모든 쓰기 작업에 대해 검사해야 한다. 이 경우 올바른 순서대로 업데이트를 수행하게 주의해야 한다.
- 외래 키로 변환되는 객체 간 참조에는 식별자 필드가 사용되지만 모든 객체 참조를 이 방법으로 할 필요는 없다. 날짜 범위나 금액 같은 작은 값 객체의 경우 소유하는 객체의 포함 값으로 넣으면 된다.
이러한 작업을 대규모로 수행하려면 객체 클러스터 전체를 테이블의 한 열에 직렬화 LOB로 저장하면 된다. LOB은 큰 객체를 의미하며 이진 데이터, 텍스트 데이터를 포함할 수 있다.
상속
관계형 데이터베이스에서 문제가 되는 또 다른 유형은 상속으로 연결된 클래스 계층이다. SQL는 상속을 처리하는 표준적인 방법이 없기 때문에 매핑을 이용해야 한다.
상속 구조를 처리하는 세 가지 방법이 있다.
- 단일 테이블 상속 : 모든 정보를 한 곳에 저장해서 수정하기 쉽고 조인이 필요 없는 게 장점이지만, 공간 낭비가 많고 병목현상이 발생할 수 있다.
- 구현 테이블 상속 : 조인 없이 한 객체를 한 테이블에서 가져올 수 있다. 하지만 상위 클래스가 변경되면 영향을 받아 변경에 취약하며, 상위 클래스 테이블이 없어 키 관리가 불편하고 참조 무결성 유지가 힘들다.
- 클래스 테이블 상속 : 클래스와 테이블 간의 관계를 가장 단순하게 저장하지만 여러 번 조인이 필요해 성능이 낮다.
위의 선택지는 각자의 상황과 선호를 고려해 선택해야 한다. 가장 좋은 방법은 DBA 에게 문의하는 것이다.
매핑
관계형 데이터베이스로 매핑할 때는 기본적으로 세 가지 상황 중 하나다.
- 스키마를 직접 선택할 수 있다.
- 기존의 스키마로 매핑해야 하며, 스키마를 변경할 수 없다.
- 기존의 스키마로 매핑해야 하지만 합의를 통해 스키마를 변경할 수 있다.
도메인 모델을 사용할 때는 데이터베이스 설계와 비슷한 설계를 조심해야 한다. 즉, 데이터베이스를 염두에 두지 말고 도메인 논리를 간소화하는 데 집중하면서 도메인 모델을 구축해야 한다. 만약에 도메인 모델과 데이터베이스 설계가 같은 구조라면 활성 레코드를 대신 사용하는 것도 고려해 본다.
모델을 먼저 구축해야 한다 했지만, 이를 위해서는 개발 주기가 6주 이내의 단기여야 한다. 이런 주기를 가지고 데이터베이스를 구축하면 데이터베이스 상호작용이 수행되는 방법에 즉각적이고 지속적인 피드백을 얻을 수 있다.
스키마가 있는 경우 약간 다르다. 논리가 간단하면 행 데이터 게이트웨이나 테이블 데이터 게이트웨이 클래스를 만들고 이 위에 도메인 논리를 배치한다.
논리가 복잡할 때는 도메인 모델이 필요한데, 데이터베이스 설계에 일치하지 않을 가능성이 높으며 데이터 매퍼를 포함하면 된다.
이중 매핑
가끔 둘 이상의 데이터 원본에서 같은 종류의 데이터를 가져오는 상황이 있다.
첫째는 여러 데이터베이스에 동일한 데이터가 포함되지만 복붙 때문에 스키마에 약간의 차이가 있는 경우
둘째는 다른 메커니즘으로 저장하는 경우인데, 데이터를 때로는 메시지에 저장할 수 있다.
가장 간단한 방법은 데이터 원본마다 하나씩, 매핑 계층을 여러 개로 만드는 것이다. 문제점은 코드 중복이 심해질 수 있는데 이 경우 2단계 매핑 스키마를 고려할 수 있다.
- 1단계는 인메모리 스키마에서 논리적 데이터 저장소 스키마로 변환
- 2단계는 논리적 데이터 저장소 스키마에서 실제 물리적 데이터 저장소 스키마로 매핑
메타데이터 사용
메타데이터 매핑은 데이터베이스의 열이 객체의 필드에 매핑되는 구체적인 방법을 메타데이터 파일에 기록하는 것이다.
<field name = customer targetClass = "Customer", dbColumn = "custID", targetTable = "customers" lowerBound = "1" upperBound = "1" setter = "loadCustomer" />
이 정보로 읽기와 쓰기 코드를 정의하고, 자동으로 임시 조인을 생성하는 등 정교한 기능까지 실행한다.
이러한 유용성 때문에 상용 O/R 매핑 툴이 메타데이터를 사용한다.
메타데이터 매핑은 인메모리 객체를 얻는 쿼리를 작성하는 데 필요한 기반 정보를 제공한다. 쿼리 객체를 이용하면 개발자가 SQL이나 관계형 스키마의 세부 사항을 몰라도 쿼리를 작성할 수 있다. 즉 쿼리 객체가 메타데이터 매핑을 이용해 객체를 적절한 SQL로 변환한다.
이를 더 발전시키면 뷰에서 데이터베이스를 거의 볼 수 없게 하는 리포지토리를 만들 수 있다. (JPA 생각해보자)
'서적 > 엔터프라이즈 애플리케이션 아키텍처 패턴' 카테고리의 다른 글
1부 이야기 - 동시성 (0) | 2021.07.28 |
---|---|
1부 이야기 - 웹 프레젠테이션 (0) | 2021.07.26 |
1부 이야기 - 도메인 논리 구성 (0) | 2021.07.26 |
1부 이야기 - 계층화 (0) | 2021.07.26 |
엔터프라이즈 애플리케이션 아키텍처 패턴을 시작하며... (0) | 2021.07.14 |