-
도메인 주도 설계의 적용 - 3. 의존성 주입과 관점 지향 프로그래밍 4부[옛날 글들] 도메인 주도 설계 2024. 4. 18. 15:23728x90
이전글 : 도메인 주도 설계의 적용 - 3. 의존성 주입과 관점 지향 프로그래밍 3부
이 글은 제가 2008년 6월부터 10월까지 5개월간 마이크로소프트웨어에 연재했던 "도메인 주도 설계의 적용"이라는 원고의 원글입니다. 잡지에 맞추어 편집을 하는 과정에서 지면 상의 제약으로 인해 수정되거나 삭제된 부분이 있어 제 블로그에 원글을 올립니다. 도메인 주도 설계(Domain-Driven Design)에 관심 있는 분들에게 도움이 되었으면 합니다.
사용(Use)으로부터 구성(Configuration)을 분리하라
UML 다이어그램을 통해 현재 설계에 어떤 문제점이 있는지 살펴보자.
그림2 OrderLineItem이 직접 CollectionProductRepository를 생성하기 때문에 OrderLineItem 이 여전히 구체적인 클래스에 의존한다.
OrderLineItem이 직접 CollectionProductRepository를 생성하기 때문에 여전히 OrderLineItem과 CollectionProductRepository 간에 강한 결합 관계가 존재한다. 만약 CollectionProductRepository를 데이터베이스에 접근하도록 수정한다면 여전히 리팩토링을 수행하기 이전의 설계가 안고 있던 OCP 위반, 단위 테스트의 번거로움, 데이터베이스에 대한 종속성과 같은 문제점을 고스란히 안게 될 것이다.
문제의 원인은 객체의 구성(Configuration)과 사용(Use)이 OrderLineItem 한 곳에 공존하고 있다는 것이다. 현재 설계에서는 OrderLineItem이 직접 구체적인 클래스인 CollectionProductRepository와의 관계를 설정한다. 객체의 구성과 사용이 한 곳에 모여 있을 경우 객체 간의 결합도가 높아진다. 해결 방법은 외부의 객체가 OrderLineItem과 CollectionProductRepository 간의 관계를 설정하도록 함으로써 구성을 사용으로부터 분리시키는 것이다.
그림3 구성과 사용을 분리시킴으로써 OrderLineItem과 CollectionProductRepository 간의 직접적인 결합도를 제거했다.
이처럼 협력하는 객체들의 외부에 존재하는 제3의 객체가 협력하는 객체 간의 의존성을 연결하는 것을 의존성 주입(Dependency Injection)이라고한다. 직접 의존성 주입을 수행하는 인프라 스트럭처 코드를 작성할 수도 있으나 이를 수월하게 해주는 다양한 오픈소스 프레임워크가 개발되어 있다. 이 프레임워크들은 의존성을 주입할 객체들의 생명주기를 관리하는 컨테이너 역할을 수행하기 때문에 경량 컨테이너(lightweight container)라고도 불린다. 본 아티클에서는 이들 중 가장 폭넓은 기능을 제공하면서도 가장 많은 개발자 커뮤니티의 지지를 받고 있는 경량 컨테이너인 Spring 프레임워크를 사용하기로 한다.사용되는Spring의 버전은 2.5이다.우선 OrderLineItem에서 CollectionProductRepository를 생성하는 부분을 제거한다. CollectionProductRepository와 OrderLineItem 간의 의존성을 삽입할 수 있도록 setter 메소드를 추가한다. 이처럼 setter 메소드를 이용하여 객체 간의 의존성을 삽입하는 것을 세터 주입(SETTER INJECTION)이라고 한다. setter 메소드로 전달된 인자의 타입 역시 ProductRepository 인터페이스라는 것에 주의하자.
public class OrderLineItem { private ProductRepository productRepository; public OrderLineItem() { } public OrderLineItem(String productName, int quantity) { this.product = productRepository.find(productName); this.quantity = quantity; } public void setProductRepository(ProductRepository productRepository) { this.productRepository = productRepository; } }
Spring과 같은 경량 컨테이너를 사용함으로써 얻을 수 있는 또 하나의 장점은 불필요한 싱글턴(SINGLETON)을 줄일 수 있다는 점이다. Spring은 컨테이너에서 관리할 객체를 등록할 때 객체의 인스턴스를 하나만 유지할 지 필요 시 매번 새로운 인스턴스를 생성할 지를 정의할 수 있다. 따라서 오버라이딩이 불가능하고 결합도가 높은 static 메서드를사용하지 않고서도 객체를 싱글턴으로 유지할 수 있다. 따라서 Spring을 사용하면 싱글턴으로 구현된 Registrar를 인터페이스와 구체적인 클래스로 분리함으로써 낮은 결합도와 높은 유연성을 제공할 수 있다. EXTRACT INTERFACE 리팩토링을 적용하자.
package org.eternity.common; import java.util.Collection; public interface Registrar { void init(); void add(Class<?> entryPointClass, EntryPoint newObject); EntryPoint get(Class<?> entryPointClass, String objectName); Collection<? extends EntryPoint> getAll(Class<?> entryPointClass); EntryPoint delete(Class<?> entryPointClass, String objectName); }
Registrar 인터페이스의 구현 클래스는 더 이상 싱글턴일 필요가 없다. static 멤버 변수와 생성 메서드(CREATION METHOD), static 메소드들을 인스턴스 메소드로 변경하자.
package org.eternity.common; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; public class EntryPointRegistrar implements Registrar { private Map<Class<?>,Map<String,EntryPoint>> entryPoints; public EntryPointRegistrar() { init(); } public void init() { entryPoints = new HashMap<Class<?>, Map<String, EntryPoint>>(); } public void add(Class<?> entryPointClass, EntryPoint newObject) { Map<String,EntryPoint> theEntryPoint = entryPoints.get(entryPointClass); if (theEntryPoint == null) { theEntryPoint = new HashMap<String,EntryPoint>(); entryPoints.put(entryPointClass, theEntryPoint); } theEntryPoint.put(newObject.getIdentity(), newObject); } public EntryPoint get(Class<?> entryPointClass, String objectName) { Map<String,EntryPoint> theEntryPoint = entryPoints.get(entryPointClass); return theEntryPoint.get(objectName); } @SuppressWarnings("unchecked") public Collection<? extends EntryPoint> getAll(Class<?> entryPointClass) { Map<String,EntryPoint> foundEntryPoints = entryPoints.get(entryPointClass); return (Collection<? extends EntryPoint>)Collections.unmodifiableCollection( foundEntryPoints != null ? entryPoints.get(entryPointClass).values() : Collections.EMPTY_SET); } @SuppressWarnings("unused") public EntryPoint delete(Class<?> entryPointClass, String objectName) { Map<String,EntryPoint> theEntryPoint = entryPoints.get(entryPointClass); return theEntryPoint.remove(objectName); } }
이제 CollectionProductRepository 클래스는 Registrar 인터페이스에 의존할 수 있다. SETTER INJECTION을 위해 setter 메소드를 추가하자.
public class CollectionProductRepository implements ProductRepository { private Registrar registrar; public CollectionProductRepository() { } public void setRegistrar(Registrar registrar) { this.registrar = registrar; } }
Spring이 EntryPointRegistrar와 CollectionProductRepository 클래스의 생명 주기를 관리하도록 하기 위해서는 Spring 빈 컨텍스트에 두 클래스의 설정 정보를 정의해야 한다. org/eternity 클래스 패스에 “order-beanContext.xml” 파일을 추가하자. EntryPointRegistrar를 id가 “registrar”인 빈으로 등록한 후 CollectionProductRepository의 registrar 프로퍼티에 의존성이 주입되도록 설정한다.
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"> <bean id="registrar" class="org.eternity.common.EntryPointRegistrar"/> <bean id="productRepository" class="org.eternity.customer.memory.CollectionProductRepository"> <property name="registrar" ref="registrar"/> </bean> </beans>
그림4 Spring의 의존성 주입을 통해 약하게 결합된 클래스 구조도. 구체적인 클래스가 추상적인 인터페이스에 의존한다.
728x90'[옛날 글들] 도메인 주도 설계' 카테고리의 다른 글
도메인 주도 설계의 적용-4.ORM과 투명한 영속성 1부 (0) 2024.04.23 도메인 주도 설계의 적용 - 3. 의존성 주입과 관점 지향 프로그래밍 5부 (1) 2024.04.20 도메인 주도 설계의 적용 - 3. 의존성 주입과 관점 지향 프로그래밍 3부 (0) 2024.04.17 도메인 주도 설계의 적용 - 3. 의존성 주입과 관점 지향 프로그래밍 2부 (0) 2024.04.16 도메인 주도 설계의 적용 - 3. 의존성 주입과 관점 지향 프로그래밍 1부 (0) 2024.04.13