요금제

아키텍처 개요

관련 소스 파일

이 페이지의 내용은 다음 소스 파일을 기반으로 생성되었습니다:

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에서 전체 모듈 구성을 확인할 수 있다.

모듈 간 의존성 구조

正在加载图表渲染器...

의존성 흐름 해석:

  1. API 우선 원칙: object-farm-api가 최하위 계층으로, 모든 모듈이 직간접적으로 의존하는 기반을 제공한다. 이 모듈은 Java 8과 Java 17을 동시 지원하는 Multi-Release JAR 구조를 갖는다 (object-farm-api/build.gradle.kts:1-48).

  2. 엔진 분리: fixture-monkey-engine은 jqwik 엔진을 독립적으로 래핑하여, 메인 모듈이 런타임에만 엔진을 로드하도록 설계되었다 (fixture-monkey/build.gradle.kts:1-20runtimeOnly(projects.fixtureMonkeyEngine)).

  3. 계층적 확장: 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 클래스는 라이브러리의 메인 진입점으로, 다음과 같은 핵심 구성 요소를 관리한다:

java
1public 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

핵심 필드 분석:

필드타입역할
fixtureMonkeyOptionsFixtureMonkeyOptions전역 설정 (프로퍼티 생성기, 옵션 등)
manipulatorOptimizerManipulatorOptimizer조작자 최적화 (중복 제거 등)
monkeyContextMonkeyContext런타임 컨텍스트 및 공유 상태
monkeyManipulatorFactoryMonkeyManipulatorFactory조작자 객체 생성 팩토리
monkeyExpressionFactoryMonkeyExpressionFactory프로퍼티 경로 표현식 파싱
nodeTreeAdapter@Nullable NodeTreeAdapter트리 구조 어댑터 (선택적)
inferredPropertiesCacheConcurrentHashMap타입별 추론된 프로퍼티 캐시

빌더 패턴을 통한 인스턴스 생성

FixtureMonkeyBuilder는 유연한 설정을 위해 빌더 패턴을 제공한다:

java
1public 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): 기본 객체 프로퍼티 생성기 설정

컴포넌트 간 상호작용

正在加载图表渲染器...

시퀀스 다이어그램 해석:

  1. 설정 단계: 클라이언트는 빌더를 통해 프로퍼티 생성기, 옵션 등을 설정한다.
  2. 빌드 단계: build() 호출 시 FixtureMonkeyOptions가 먼저 생성되고, 이를 기반으로 MonkeyContext가 구축된다.
  3. 사용 단계: giveMeBuilder() 호출 시 새로운 ArbitraryBuilder 인스턴스가 생성된다.

객체 생성 빌더 아키텍처

ArbitraryBuilder 인터페이스 계층

DefaultArbitraryBuilder는 객체 생성을 위한 핵심 빌더 구현체로, 여러 인터페이스를 구현한다:

java
1public final class DefaultArbitraryBuilder&lt;T&gt; implements ArbitraryBuilder&lt;T&gt;, 
2    ExperimentalArbitraryBuilder&lt;T&gt;, ObjectBuilder&lt;T&gt;, 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는 두 가지 컨텍스트를 관리한다:

  1. activeContext: 현재 활성화된 조작자(manipulator)를 보관하며, 항상 객체 생성에 적용된다.
  2. standbyContexts: 조건부 조작자를 보관하며, 매칭 조건이 충족될 때만 지연 평가된다.

이 분리는 중첩된 thenApply 호출로 인한 스택 오버플로우를 방지하는 핵심 설계이다.

ObjectBuilder 마커 인터페이스

ObjectBuilder&lt;T&gt;는 내부용 마커 인터페이스로, 1.1.0부터 @API(status = Status.INTERNAL)로 표시된다:

java
1@API(since = "1.1.0", status = Status.INTERNAL)
2public interface ObjectBuilder&lt;T&gt; {
3}

fixture-monkey-api/src/main/java/com/navercorp/fixturemonkey/api/ObjectBuilder.java:19-32

이 인터페이스는 타입 안전성을 위한 마커 역할만 수행하며, 직접 사용하지 않도록 경고한다.

조작자 적용 메커니즘

set() 메서드는 프로퍼티 조작의 핵심 진입점이다:

java
1@Override
2public ArbitraryBuilder&lt;T&gt; 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}

조작자 적용 흐름:

  1. PropertySelectorNodeResolver로 변환하여 트리 내 타겟 노드를 식별한다.
  2. InnerSpec인 경우 내부 특수 처리 경로로 진입한다.
  3. 일반 값인 경우 ArbitraryManipulator를 생성하여 activeContext에 추가한다.
  4. 빌더 자신을 반환하여 메서드 체이닝을 지원한다.

트리 기반 객체 모델

ObjectTree 구조

ObjectTree는 생성할 객체의 구조를 트리로 표현하는 핵심 데이터 구조다:

java
1public 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() 메서드는 트리 내 특정 노드를 찾아 조작을 적용한다:

java
1public 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}

조작 적용 과정:

  1. NodeResolver가 루트 노드에서 시작하여 타겟 노드 목록을 찾는다.
  2. 각 타겟 노드에 대해 NodeManipulator가 조작을 수행한다.
  3. 조작 이력이 노드 컨텍스트에 기록된다.

ObjectNode 계층 구조

ObjectNodeTraverseNodeTraverseNodeMetadata 인터페이스를 구현하며, 트리의 개별 노드를 표현한다:

java
1public 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() 메서드는 노드를 확장하여 자식 노드를 생성한다:

java
1@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는 트리 내 노드를 탐색하기 위한 전략 인터페이스다:

java
1public 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

구현체 예시:

데이터 흐름 및 호출 체인

전체 객체 생성 흐름

正在加载图表渲染器...

흐름 단계별 설명:

  1. 빌더 생성: giveMeBuilder() 호출 시 DefaultArbitraryBuilder 인스턴스가 생성된다. 이때 루트 프로퍼티와 컨텍스트가 초기화된다.

  2. 조작자 등록: set() 메서드 호출 시 프로퍼티 경로가 NodeResolver로 변환되고, 값과 함께 ArbitraryManipulator가 생성되어 activeContext에 추가된다.

  3. 객체 생성 트리거: sample() 호출 시 실제 객체 생성이 시작된다.

  4. 트리 구축: ObjectTree가 생성되며, 타입 정보를 기반으로 전체 객체 그래프가 트리 구조로 표현된다.

  5. 노드 탐색 및 조작: 등록된 조작자들의 NodeResolver가 트리를 탐색하여 타겟 노드를 찾고, NodeManipulator가 해당 노드에 조작을 적용한다.

  6. 최종 생성: 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. 컨텍스트 이중화

결정: activeContextstandbyContexts의 이중 컨텍스트 구조 채택

이유:

  • 중첩 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-apifixture-monkey-api를 Multi-Release JAR로 구성

이유:

  • Java 8 기반 프로젝트 지원 유지
  • Java 17+ 기능 활용 가능
  • 점진적인 마이그레이션 경로 제공

증거: object-farm-api/build.gradle.kts:1-48multiRelease { targetVersions(8, *multiReleaseVersions) } 설정

기술 스택 선정

기술용도선정 이유대안
jqwik속성 기반 테스트 엔진Java/Kotlin 생태계에서 가장 성숙한 PBT 프레임워크QuickCheck, junit-quickcheck
Kotlin ReflectKotlin 리플렉션 지원Kotlin 고유 기능(데이터 클래스, 확장 프로퍼티 등) 처리Java 리플렉션만 사용
JacksonJSON 직렬화/역직렬화Spring 생태계 표준, 다양한 모듈 생태계Gson, Moshi
JSR-303/Jakarta ValidationBean Validation표준 스펙, Spring 통합 용이자체 검증 로직
Mockito목 객체 생성Java 생태계 사실상 표준EasyMock, JMock
KotestKotlin 테스트 프레임워크Kotlin DSL 기반 직관적 테스트 작성Spek, kotlin.test
JUnit JupiterJUnit 5 통합레거시 JUnit 4 마이그레이션 경로 제공JUnit 4만 지원
SpotBugs정적 분석FindBugs 후속, Gradle 플러그인 지원PMD, Checkstyle
Checker Framework타입 시스템 강화Null 안전성 등 컴파일 타임 검증@Nullable 어노테이션만 사용
Multi-Release JARJava 버전 호환단일 아티팩트로 다중 Java 버전 지원별도 모듈 분리

모듈 의존성 그래프

正在加载图表渲染器...

의존성 그래프 해석:

  • 수직 계층: 하단에서 상단으로 의존성이 흐른다. object-farm-api가 최하위, 통합 모듈들이 최상위에 위치한다.
  • 수평 확장: 메인 모듈(fixture-monkey)을 중심으로 다양한 통합 모듈이 독립적으로 확장된다.
  • 외부 격리: 외부 라이브러리 의존성은 각 통합 모듈 내부로 캡슐화되어, 클라이언트가 불필요한 의존성을 강제받지 않는다.

주요 설정 및 초기화

FixtureMonkeyOptions 구성

FixtureMonkeyOptions는 다음과 같은 핵심 설정을 포함한다:

  • 프로퍼티 생성기 체인: 타입별 프로퍼티 생성 전략
  • 객체 프로퍼티 생성기: 복합 객체의 프로퍼티 결정 방식
  • 프로퍼티 이름 리졸버: 프로퍼티 이름 추론 전략
  • 생성자 프로세서: 객체 인스턴스화 방식
  • 난수 생성기: 재현 가능한 랜덤 시드 관리

빌더 초기화 예시

java
1FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
2    .pushExactTypePropertyGenerator(MyType.class, new MyTypePropertyGenerator())
3    .defaultObjectPropertyGenerator(new DefaultObjectPropertyGenerator())
4    .manipulatorOptimizer(new DefaultManipulatorOptimizer())
5    .build();

이 예시는 특정 타입에 대한 커스텀 프로퍼티 생성기를 등록하고, 기본 객체 프로퍼티 생성기와 조작자 최적화 전략을 설정하는 방법을 보여준다.

캐시 전략

FixtureMonkeyConcurrentHashMap 기반의 프로퍼티 추론 캐시를 사용하여 반복적인 리플렉션 비용을 절감한다:

java
1private final Map<Class<?>, Set<Property>> inferredPropertiesCache = new ConcurrentHashMap<>();

이 캐시는 스레드 안전하며, 동일 타입에 대한 프로퍼티 정보를 재사용하여 성능을 최적화한다.