아키텍처 개요
관련 소스 파일
이 페이지의 내용은 다음 소스 파일을 기반으로 생성되었습니다:
- fixture-monkey/src/main/java/com/navercorp/fixturemonkey/FixtureMonkey.java
- fixture-monkey/src/main/java/com/navercorp/fixturemonkey/FixtureMonkeyBuilder.java
- fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/ObjectBuilder.java
- fixture-monkey/src/main/java/com/navercorp/fixturemonkey/builder/DefaultArbitraryBuilder.java
- fixture-monkey/src/main/java/com/navercorp/fixturemonkey/tree/ObjectTree.java
- settings.gradle.kts
- fixture-monkey/src/main/java/com/navercorp/fixturemonkey/tree/ObjectNode.java
- fixture-monkey/src/main/java/com/navercorp/fixturemonkey/tree/NodeResolver.java
- fixture-monkey/src/main/java/com/navercorp/fixturemonkey/tree/ObjectNodeList.java
- fixture-monkey/src/main/java/com/navercorp/fixturemonkey/tree/NodeKeyPredicate.java
- build.gradle.kts
- docs/package.json
- buildSrc/build.gradle.kts
- fixture-monkey/build.gradle.kts
- object-farm-api/build.gradle.kts
- fixture-monkey-api/build.gradle.kts
- fixture-monkey-tests/build.gradle.kts
- fixture-monkey-engine/build.gradle.kts
- fixture-monkey-kotest/build.gradle.kts
- fixture-monkey-kotlin/build.gradle.kts
Fixture Monkey는 테스트 픽스처 자동 생성을 위한 Java/Kotlin 라이브러리로, 복잡한 객체 그래프를 임의의 값으로 채워주는 역할을 수행한다. 이 프로젝트는 멀티 모듈 아키텍처를 채택하여 API 정의와 구현체를 분리하고, 다양한 프레임워크 및 라이브러리와의 통합을 지원한다.
모듈 구조 및 의존성
전체 모듈 구성
Fixture Monkey는 fixture-monkey-parent 루트 프로젝트 하위에 16개 이상의 서브 모듈로 구성된다. 핵심 모듈은 다음과 같이 분류된다:
API 계층 모듈:
object-farm-api: 객체 생성 팜의 기본 API 정의fixture-monkey-api: Fixture Monkey 핵심 API 인터페이스 및 타입 정의
코어 구현 모듈:
fixture-monkey: 메인 진입점 및 기본 구현체fixture-monkey-engine: jqwik 엔진 기반 속성 기반 테스트 지원
통합 모듈:
fixture-monkey-kotlin: Kotlin 확장 함수 및 리플렉션 지원fixture-monkey-kotest: Kotest 프레임워크 통합fixture-monkey-jackson: Jackson 직렬화/역직렬화 지원fixture-monkey-javax-validation: JSR-303 Bean Validation 지원fixture-monkey-jakarta-validation: Jakarta Bean Validation 지원fixture-monkey-mockito: Mockito 목 객체 생성 지원fixture-monkey-junit-jupiter: JUnit 5 통합fixture-monkey-autoparams: AutoParams 프레임워크 통합
테스트 및 스타터 모듈:
fixture-monkey-tests: 통합 테스트 스위트fixture-monkey-starter: Java 스타터 템플릿fixture-monkey-starter-kotlin: Kotlin 스타터 템플릿
settings.gradle.kts:1-31에서 전체 모듈 구성을 확인할 수 있다.
모듈 간 의존성 구조
正在加载图表渲染器...
의존성 흐름 해석:
-
API 우선 원칙:
object-farm-api가 최하위 계층으로, 모든 모듈이 직간접적으로 의존하는 기반을 제공한다. 이 모듈은 Java 8과 Java 17을 동시 지원하는 Multi-Release JAR 구조를 갖는다 (object-farm-api/build.gradle.kts:1-48). -
엔진 분리:
fixture-monkey-engine은 jqwik 엔진을 독립적으로 래핑하여, 메인 모듈이 런타임에만 엔진을 로드하도록 설계되었다 (fixture-monkey/build.gradle.kts:1-20의runtimeOnly(projects.fixtureMonkeyEngine)). -
계층적 확장: Kotlin 모듈은 메인 모듈에 의존하고, Kotest 모듈은 Kotlin 모듈에 의존하는 계층 구조를 형성한다 (fixture-monkey-kotest/build.gradle.kts:1-12).
빌드 구성 특징
모든 서브 프로젝트는 공통 빌드 로직을 공유한다:
- Java 8 기본 호환:
java { toolchain { languageVersion = JavaLanguageVersion.of(8) } }설정으로 Java 8을 기본으로 하되, Multi-Release JAR로 Java 17 기능도 지원한다 (build.gradle.kts:5-77). - 커스텀 Gradle 플러그인:
buildSrc에 정의된 5개의 커스텀 플러그인이 반복적인 빌드 로직을 캡슐화한다 (buildSrc/build.gradle.kts:1-35). - jqwik 테스트 엔진:
useJUnitPlatform { includeEngines("jqwik") }설정으로 속성 기반 테스트를 기본 테스트 엔진으로 사용한다.
핵심 컴포넌트 구조
FixtureMonkey 진입점
FixtureMonkey 클래스는 라이브러리의 메인 진입점으로, 다음과 같은 핵심 구성 요소를 관리한다:
java1public final class FixtureMonkey { 2 private final FixtureMonkeyOptions fixtureMonkeyOptions; 3 private final ManipulatorOptimizer manipulatorOptimizer; 4 private final MonkeyContext monkeyContext; 5 private final MonkeyManipulatorFactory monkeyManipulatorFactory; 6 private final MonkeyExpressionFactory monkeyExpressionFactory; 7 private final @Nullable NodeTreeAdapter nodeTreeAdapter; 8 private final AdapterTracer adapterTracer; 9 private final Map<Class<?>, Set<Property>> inferredPropertiesCache = new ConcurrentHashMap<>(); 10 // ... 11}
fixture-monkey/src/main/java/com/navercorp/fixturemonkey/FixtureMonkey.java:62-133
핵심 필드 분석:
| 필드 | 타입 | 역할 |
|---|---|---|
fixtureMonkeyOptions | FixtureMonkeyOptions | 전역 설정 (프로퍼티 생성기, 옵션 등) |
manipulatorOptimizer | ManipulatorOptimizer | 조작자 최적화 (중복 제거 등) |
monkeyContext | MonkeyContext | 런타임 컨텍스트 및 공유 상태 |
monkeyManipulatorFactory | MonkeyManipulatorFactory | 조작자 객체 생성 팩토리 |
monkeyExpressionFactory | MonkeyExpressionFactory | 프로퍼티 경로 표현식 파싱 |
nodeTreeAdapter | @Nullable NodeTreeAdapter | 트리 구조 어댑터 (선택적) |
inferredPropertiesCache | ConcurrentHashMap | 타입별 추론된 프로퍼티 캐시 |
빌더 패턴을 통한 인스턴스 생성
FixtureMonkeyBuilder는 유연한 설정을 위해 빌더 패턴을 제공한다:
java1public final class FixtureMonkeyBuilder { 2 private static final int DEFAULT_PRIORITY = Integer.MAX_VALUE; 3 private final FixtureMonkeyOptionsBuilder fixtureMonkeyOptionsBuilder = FixtureMonkeyOptions.builder(); 4 private boolean expressionStrictMode = false; 5 private PropertyNameResolver defaultPropertyNameResolver; 6 private final List<MatcherOperator<PropertyNameResolver>> propertyNameResolvers = new ArrayList<>(); 7 private ManipulatorOptimizer manipulatorOptimizer = new NoneManipulatorOptimizer(); 8 private MonkeyExpressionFactory monkeyExpressionFactory = new ArbitraryExpressionFactory(); 9 // ... 10}
fixture-monkey/src/main/java/com/navercorp/fixturemonkey/FixtureMonkeyBuilder.java:75-134
빌더 주요 메서드:
pushPropertyGenerator(MatcherOperator<PropertyGenerator>): 타입 매칭 기반 프로퍼티 생성기 등록pushAssignableTypePropertyGenerator(Class<?>, PropertyGenerator): 할당 가능한 타입에 대한 생성기 등록pushExactTypePropertyGenerator(Class<?>, PropertyGenerator): 정확한 타입 매칭 생성기 등록manipulatorOptimizer(ManipulatorOptimizer): 조작자 최적화 전략 설정defaultObjectPropertyGenerator(ObjectPropertyGenerator): 기본 객체 프로퍼티 생성기 설정
컴포넌트 간 상호작용
正在加载图表渲染器...
시퀀스 다이어그램 해석:
- 설정 단계: 클라이언트는 빌더를 통해 프로퍼티 생성기, 옵션 등을 설정한다.
- 빌드 단계:
build()호출 시FixtureMonkeyOptions가 먼저 생성되고, 이를 기반으로MonkeyContext가 구축된다. - 사용 단계:
giveMeBuilder()호출 시 새로운ArbitraryBuilder인스턴스가 생성된다.
객체 생성 빌더 아키텍처
ArbitraryBuilder 인터페이스 계층
DefaultArbitraryBuilder는 객체 생성을 위한 핵심 빌더 구현체로, 여러 인터페이스를 구현한다:
java1public final class DefaultArbitraryBuilder<T> implements ArbitraryBuilder<T>, 2 ExperimentalArbitraryBuilder<T>, ObjectBuilder<T>, ArbitraryBuilderContextProvider { 3 private final TreeRootProperty rootProperty; 4 private final ArbitraryResolver resolver; 5 private final MonkeyManipulatorFactory monkeyManipulatorFactory; 6 private final MonkeyExpressionFactory monkeyExpressionFactory; 7 private final ArbitraryBuilderContext activeContext; 8 private final List<PriorityMatcherOperator<ArbitraryBuilderContext>> standbyContexts; 9 private final MonkeyContext monkeyContext; 10 private final InstantiatorProcessor instantiatorProcessor; 11 private final Class<?> rootClass; 12 // ... 13}
fixture-monkey/src/main/java/com/navercorp/fixturemonkey/builder/DefaultArbitraryBuilder.java:84-168
컨텍스트 이중 구조:
DefaultArbitraryBuilder는 두 가지 컨텍스트를 관리한다:
- activeContext: 현재 활성화된 조작자(manipulator)를 보관하며, 항상 객체 생성에 적용된다.
- standbyContexts: 조건부 조작자를 보관하며, 매칭 조건이 충족될 때만 지연 평가된다.
이 분리는 중첩된 thenApply 호출로 인한 스택 오버플로우를 방지하는 핵심 설계이다.
ObjectBuilder 마커 인터페이스
ObjectBuilder<T>는 내부용 마커 인터페이스로, 1.1.0부터 @API(status = Status.INTERNAL)로 표시된다:
java1@API(since = "1.1.0", status = Status.INTERNAL) 2public interface ObjectBuilder<T> { 3}
fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/ObjectBuilder.java:19-32
이 인터페이스는 타입 안전성을 위한 마커 역할만 수행하며, 직접 사용하지 않도록 경고한다.
조작자 적용 메커니즘
set() 메서드는 프로퍼티 조작의 핵심 진입점이다:
java1@Override 2public ArbitraryBuilder<T> set(PropertySelector propertySelector, @Nullable Object value, int limit) { 3 NodeResolver nodeResolver = toMonkeyExpression(propertySelector).toNodeResolver(); 4 if (value instanceof InnerSpec) { 5 this.setInner((InnerSpec)value); 6 } else { 7 ArbitraryManipulator arbitraryManipulator = 8 monkeyManipulatorFactory.newArbitraryManipulator(nodeResolver, value, limit); 9 this.activeContext.addManipulator(arbitraryManipulator); 10 } 11 return this; 12}
조작자 적용 흐름:
PropertySelector를NodeResolver로 변환하여 트리 내 타겟 노드를 식별한다.InnerSpec인 경우 내부 특수 처리 경로로 진입한다.- 일반 값인 경우
ArbitraryManipulator를 생성하여activeContext에 추가한다. - 빌더 자신을 반환하여 메서드 체이닝을 지원한다.
트리 기반 객체 모델
ObjectTree 구조
ObjectTree는 생성할 객체의 구조를 트리로 표현하는 핵심 데이터 구조다:
java1public final class ObjectTree { 2 private final ObjectNode rootNode; 3 private final ObjectTreeMetadata metadata; 4 private final GenerateFixtureContext generateFixtureContext; 5 6 public ObjectTree( 7 TreeRootProperty rootProperty, 8 GenerateFixtureContext generateFixtureContext, 9 TraverseContext traverseContext 10 ) { 11 this.rootNode = new ObjectNode( 12 DefaultTraverseNode.generateRootNode(rootProperty, traverseContext), 13 generateFixtureContext 14 ); 15 MetadataCollector metadataCollector = new MetadataCollector(rootNode); 16 this.metadata = metadataCollector.collect(); 17 this.generateFixtureContext = this.rootNode.getObjectNodeContext(); 18 } 19 // ... 20}
fixture-monkey/src/main/java/com/navercorp/fixturemonkey/tree/ObjectTree.java:33-68
트리 구성 요소:
| 구성 요소 | 역할 |
|---|---|
rootNode | 객체 그래프의 루트 노드 |
metadata | 트리 메타데이터 (프로퍼티 정보 등) |
generateFixtureContext | 픽스처 생성 컨텍스트 |
트리 조작 인터페이스
manipulate() 메서드는 트리 내 특정 노드를 찾아 조작을 적용한다:
java1public void manipulate(NodeResolver nodeResolver, NodeManipulator nodeManipulator) { 2 List<ObjectNode> nodes = nodeResolver.resolve(rootNode); 3 for (ObjectNode node : nodes) { 4 nodeManipulator.manipulate(node); 5 node.getObjectNodeContext().addManipulator(nodeManipulator); 6 } 7}
조작 적용 과정:
NodeResolver가 루트 노드에서 시작하여 타겟 노드 목록을 찾는다.- 각 타겟 노드에 대해
NodeManipulator가 조작을 수행한다. - 조작 이력이 노드 컨텍스트에 기록된다.
ObjectNode 계층 구조
ObjectNode는 TraverseNode와 TraverseNodeMetadata 인터페이스를 구현하며, 트리의 개별 노드를 표현한다:
java1public final class ObjectNode implements TraverseNode, TraverseNodeMetadata { 2 private final TraverseNode traverseNode; 3 private final GenerateFixtureContext generateFixtureContext; 4 private @Nullable ObjectNode parent; 5 private List<ObjectNode> children; 6 7 public ObjectNode(TraverseNode traverseNode, GenerateFixtureContext generateFixtureContext) { 8 this.traverseNode = traverseNode; 9 this.generateFixtureContext = generateFixtureContext; 10 this.generateFixtureContext.setTraverseNode(this); 11 } 12 // ... 13}
fixture-monkey/src/main/java/com/navercorp/fixturemonkey/tree/ObjectNode.java:47-99
노드 확장 메커니즘:
expand() 메서드는 노드를 확장하여 자식 노드를 생성한다:
java1@Override 2public boolean expand() { 3 if (!this.traverseNode.expand() && this.children != null) { 4 return false; 5 } 6 this.setChildren( 7 nullSafe(this.traverseNode.getChildren()).asList().stream() 8 .map(it -> new ObjectNode(it, generateFixtureContext.newChildNodeContext())) 9 .collect(Collectors.toList()) 10 ); 11 return true; 12}
확장 시 각 자식 TraverseNode에 대해 새로운 ObjectNode가 생성되며, 부모-자식 관계가 설정된다.
NodeResolver 인터페이스
NodeResolver는 트리 내 노드를 탐색하기 위한 전략 인터페이스다:
java1public interface NodeResolver { 2 /** 3 * Resolves the next nodes. The nextNode can be omitted if it cannot be resolved from traversal. 4 * 5 * @param nextNode it may be the root node or the parent node resolved by the previous {@link NodeResolver} 6 * @return the next nodes 7 */ 8 List<ObjectNode> resolve(ObjectNode nextNode); 9}
fixture-monkey/src/main/java/com/navercorp/fixturemonkey/tree/NodeResolver.java:27-35
구현체 예시:
NodeKeyPredicate: Map의 키 요소 프로퍼티인지 검사하는 프레디케이트 (fixture-monkey/src/main/java/com/navercorp/fixturemonkey/tree/NodeKeyPredicate.java:29-36)ObjectNodeList: 노드 목록을 래핑하는 컬렉션 (fixture-monkey/src/main/java/com/navercorp/fixturemonkey/tree/ObjectNodeList.java:34-45)
데이터 흐름 및 호출 체인
전체 객체 생성 흐름
正在加载图表渲染器...
흐름 단계별 설명:
-
빌더 생성:
giveMeBuilder()호출 시DefaultArbitraryBuilder인스턴스가 생성된다. 이때 루트 프로퍼티와 컨텍스트가 초기화된다. -
조작자 등록:
set()메서드 호출 시 프로퍼티 경로가NodeResolver로 변환되고, 값과 함께ArbitraryManipulator가 생성되어activeContext에 추가된다. -
객체 생성 트리거:
sample()호출 시 실제 객체 생성이 시작된다. -
트리 구축:
ObjectTree가 생성되며, 타입 정보를 기반으로 전체 객체 그래프가 트리 구조로 표현된다. -
노드 탐색 및 조작: 등록된 조작자들의
NodeResolver가 트리를 탐색하여 타겟 노드를 찾고,NodeManipulator가 해당 노드에 조작을 적용한다. -
최종 생성:
generateFixtureContext.generate()가 조작이 적용된 트리를 기반으로 최종 객체를 생성한다.
지연 평가 메커니즘
standbyContexts는 조건부 조작을 위한 지연 평가 메커니즘을 제공한다. 등록 시점이 아닌 객체 생성 시점에 매칭 조건을 평가하여, 필요한 경우에만 조작을 적용한다. 이 설계는 특히 thenApply와 같은 후속 조작에서 중요한 역할을 한다.
핵심 설계 결정
1. 멀티 모듈 분리 전략
결정: API(fixture-monkey-api)와 구현(fixture-monkey)을 별도 모듈로 분리
이유:
- 클라이언트 코드가 구현체가 아닌 API에만 의존하도록 강제
- 내부 구현 변경 시 클라이언트 영향 최소화
- 다양한 통합 모듈이 동일 API 기반으로 확장 가능
증거: fixture-monkey-api/build.gradle.kts:1-57에서 API 모듈이 object-farm-api에만 의존하고, jqwik 관련 의존성은 compileOnly로 선언됨
2. 빌더 패턴과 불변성
결정: FixtureMonkey는 불변 객체로 설계하고, 모든 설정은 FixtureMonkeyBuilder를 통해 수행
이유:
- 스레드 안전성 보장
- 설정 완료 후 상태 변경 방지
- 테스트에서의 예측 가능성 향상
증거: fixture-monkey/src/main/java/com/navercorp/fixturemonkey/FixtureMonkey.java:62-133의 모든 필드가 final로 선언됨
3. 트리 기반 객체 모델
결정: 생성할 객체를 트리 구조로 표현하고, 조작을 노드 단위로 적용
이유:
- 복잡한 중첩 객체 구조를 직관적으로 표현
- 프로퍼티 경로 표현식을 통한 정밀한 타겟팅
- 조작의 순서 독립성 보장
증거: fixture-monkey/src/main/java/com/navercorp/fixturemonkey/tree/ObjectTree.java:33-68에서 manipulate() 메서드가 NodeResolver를 통해 노드를 찾고 NodeManipulator를 적용하는 구조
4. 컨텍스트 이중화
결정: activeContext와 standbyContexts의 이중 컨텍스트 구조 채택
이유:
- 중첩
thenApply호출 시 스택 오버플로우 방지 - 조건부 조작의 지연 평가 지원
- 무조건 적용 조작과 조건부 조작의 명확한 분리
증거: fixture-monkey/src/main/java/com/navercorp/fixturemonkey/builder/DefaultArbitraryBuilder.java:84-168의 주석에서 "Keeping them separate prevents stack overflow" 명시
5. 런타임 엔진 분리
결정: fixture-monkey-engine을 런타임 전용 의존성으로 분리
이유:
- 컴파일 타임에 엔진 구현 세부 사항 노출 방지
- 엔진 교체 가능성 확보
- 메인 모듈의 의존성 최소화
증거: fixture-monkey/build.gradle.kts:1-20에서 runtimeOnly(projects.fixtureMonkeyEngine) 사용
6. Multi-Release JAR 지원
결정: object-farm-api와 fixture-monkey-api를 Multi-Release JAR로 구성
이유:
- Java 8 기반 프로젝트 지원 유지
- Java 17+ 기능 활용 가능
- 점진적인 마이그레이션 경로 제공
증거: object-farm-api/build.gradle.kts:1-48의 multiRelease { targetVersions(8, *multiReleaseVersions) } 설정
기술 스택 선정
| 기술 | 용도 | 선정 이유 | 대안 |
|---|---|---|---|
| jqwik | 속성 기반 테스트 엔진 | Java/Kotlin 생태계에서 가장 성숙한 PBT 프레임워크 | QuickCheck, junit-quickcheck |
| Kotlin Reflect | Kotlin 리플렉션 지원 | Kotlin 고유 기능(데이터 클래스, 확장 프로퍼티 등) 처리 | Java 리플렉션만 사용 |
| Jackson | JSON 직렬화/역직렬화 | Spring 생태계 표준, 다양한 모듈 생태계 | Gson, Moshi |
| JSR-303/Jakarta Validation | Bean Validation | 표준 스펙, Spring 통합 용이 | 자체 검증 로직 |
| Mockito | 목 객체 생성 | Java 생태계 사실상 표준 | EasyMock, JMock |
| Kotest | Kotlin 테스트 프레임워크 | Kotlin DSL 기반 직관적 테스트 작성 | Spek, kotlin.test |
| JUnit Jupiter | JUnit 5 통합 | 레거시 JUnit 4 마이그레이션 경로 제공 | JUnit 4만 지원 |
| SpotBugs | 정적 분석 | FindBugs 후속, Gradle 플러그인 지원 | PMD, Checkstyle |
| Checker Framework | 타입 시스템 강화 | Null 안전성 등 컴파일 타임 검증 | @Nullable 어노테이션만 사용 |
| Multi-Release JAR | Java 버전 호환 | 단일 아티팩트로 다중 Java 버전 지원 | 별도 모듈 분리 |
모듈 의존성 그래프
正在加载图表渲染器...
의존성 그래프 해석:
- 수직 계층: 하단에서 상단으로 의존성이 흐른다.
object-farm-api가 최하위, 통합 모듈들이 최상위에 위치한다. - 수평 확장: 메인 모듈(
fixture-monkey)을 중심으로 다양한 통합 모듈이 독립적으로 확장된다. - 외부 격리: 외부 라이브러리 의존성은 각 통합 모듈 내부로 캡슐화되어, 클라이언트가 불필요한 의존성을 강제받지 않는다.
주요 설정 및 초기화
FixtureMonkeyOptions 구성
FixtureMonkeyOptions는 다음과 같은 핵심 설정을 포함한다:
- 프로퍼티 생성기 체인: 타입별 프로퍼티 생성 전략
- 객체 프로퍼티 생성기: 복합 객체의 프로퍼티 결정 방식
- 프로퍼티 이름 리졸버: 프로퍼티 이름 추론 전략
- 생성자 프로세서: 객체 인스턴스화 방식
- 난수 생성기: 재현 가능한 랜덤 시드 관리
빌더 초기화 예시
java1FixtureMonkey fixtureMonkey = FixtureMonkey.builder() 2 .pushExactTypePropertyGenerator(MyType.class, new MyTypePropertyGenerator()) 3 .defaultObjectPropertyGenerator(new DefaultObjectPropertyGenerator()) 4 .manipulatorOptimizer(new DefaultManipulatorOptimizer()) 5 .build();
이 예시는 특정 타입에 대한 커스텀 프로퍼티 생성기를 등록하고, 기본 객체 프로퍼티 생성기와 조작자 최적화 전략을 설정하는 방법을 보여준다.
캐시 전략
FixtureMonkey는 ConcurrentHashMap 기반의 프로퍼티 추론 캐시를 사용하여 반복적인 리플렉션 비용을 절감한다:
java1private final Map<Class<?>, Set<Property>> inferredPropertiesCache = new ConcurrentHashMap<>();
이 캐시는 스레드 안전하며, 동일 타입에 대한 프로퍼티 정보를 재사용하여 성능을 최적화한다.
