ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Fixture Monkey] CustomArbitraryIntrospector를 등록하는 여러 방법
    📚 개발백과 2025. 10. 19. 22:21
    728x90
     

    [Fixture Monkey] 불변 객체를 위한 사용자 정의 인트로스펙터 예제 코드 - 기초편

    불변 객체(`google.common.collect.Range`)를 위한 사용자 정의 인트로스펙터 예제 코드입니다.사용 버전: 1.1.11 ⬇️ 코드만 먼저 보기더보기더보기더보기/** * Range 타입에 대한 값을 반환하는 사용자 정

    the0.tistory.com

    이전 포스팅 글(사용자 정의 인트로스펙터 예제 코드 - 기초편)과 이어집니다. 

     

    공식문서를 참고하여 Fixture Monkey의 CustomArbitrayIntrospector를 만들고

    customIntrospector를 objectIntrospector에 등록했다.

    참고: https://naver.github.io/fixture-monkey/v1-1-0-kor/docs/generating-objects/custom-introspector/#다른-인트로스펙터와-함께-사용

     

    FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
        .objectIntrospector(new FailoverIntrospector(
            Arrays.asList(
                rangeInstantArbitraryIntrospector,  // ⭐️ 사용자 정의 인트로스펙터
                ConstructorPropertiesArbitraryIntrospector.INSTANCE,
                BuilderArbitraryIntrospector.INSTANCE,
                FieldReflectionArbitraryIntrospector.INSTANCE,
                BeanArbitraryIntrospector.INSTANCE
            )
        ))
        .build();

     

    이후 `giveMeOne()` 혹은 `giveMeBuilder().sample()` 등 수행 과정을 디버깅해본 결과,

    모든 Request/Response 객체가 생성될 때마다 rangeInstantArbitraryIntrospector에 접근한다.

    public class RangeInstantArbitraryIntrospector implements ArbitraryIntrospector {
    
        @Override
        public ArbitraryIntrospectorResult introspect(ArbitraryGeneratorContext context) {
    
            Property property = context.getResolvedProperty();
            Class<?> type = Types.getActualType(property.getType());
            List<AnnotatedType> typeArguments = Types.getGenericsTypes(property.getAnnotatedType());
            Class<?> genericType = typeArguments.isEmpty() ? null : Types.getActualType(typeArguments.getFirst());
    
            if (!type.equals(Range.class)
                    || typeArguments.size() != 1
                    || !genericType.equals(Instant.class)) {
                return ArbitraryIntrospectorResult.NOT_INTROSPECTED;
            }
    
            // 후략
        }
    }

     

    이 RangeInstantArbitraryIntrospector는 Range.class 이면서 제네릭이 Instant인 타입을 위한 Custom Introspector이다.

     

    하지만 Foo.class 를 비롯한 사용자 정의 객체들이 모두 접근하고, if 조건문에서 return 된다.

    Foo.class는 예시 클래스입니다. ⬇️

    더보기
    더보기
    import java.time.Instant;
    import com.google.common.collect.Range;
    
    public record Foo(
    	...
    	Range<Instant> period,
     	...
    ) {
    }
    
    // 예시: ["2025-10-06 01:00:00.000+00","2025-10-06 09:00:00.000+00"]

     

     

    커스텀 Introspector를 특정 Class에만 적용하는 방법이 있을까?

     


     

    당연히 존재한다.

     

    이 아티클에서는 인트로스펙터를 설정하는 방법을 다룬다.

    참고 자료: 

    - https://naver.github.io/fixture-monkey/v1-0-0-kor/docs/fixture-monkey-options/generation-options/#arbitraryintrospector

    - https://naver.github.io/fixture-monkey/v1-0-0-kor/docs/fixture-monkey-options/generation-options/#arbitraryintrospector

     

    0. Fixture Monkey 인트로스펙터 적용/검증 순서

    JavaArbitraryIntrospector : String.class, char.class, int.class, Long.class 등

    JavaTimeArbitraryIntrospector : Date.class, LocalDateTime.class, Instant.class, ZoneId.class 등

    priorityIntrospector : Boolean.class, boolean.class UUID.class, Enum 타입
    containerIntrospector : Optional.class, Set.class, Queue.class, Stream.class, Map.class 등

    objectIntrospector : JavaBean 패턴을 따르는 객체들을 분석, 생성

    fallbackIntrospector : 아무 인트로스펙터들도 처리하지 못했다는 결과 반환

     

    이 아티클에서 일련의 모든 내장 Introspector를 JavaDefaultArbitrary라고 칭하겠다.

     

    참고 ⬇️

    더보기
    더보기

    Fixture Monkey 소스코드

    /*
    JavaDefaultArbitraryGeneratorBuilder.java
    line 168 ~ 181
    */
    
    public IntrospectedArbitraryGenerator build() {
    	return new IntrospectedArbitraryGenerator(
    		new MatchArbitraryIntrospector(
    			Arrays.asList(
    				new JavaArbitraryIntrospector(this.javaTypeArbitraryGeneratorSet),
    				new JavaTimeArbitraryIntrospector(this.javaTimeArbitraryGeneratorSet),
    				this.priorityIntrospector,
    				this.containerIntrospector,
    				this.objectIntrospector,
    				this.fallbackIntrospector
    			)
    		)
    	);
    }

     

     


    1.  기존 방식 : objectIntrospector

    인트로스펙터를 설정한 기존 방식은 다음과 같이 objectIntrospector에 등록했다.

    FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
        .objectIntrospector(new FailoverIntrospector(
            Arrays.asList(
                rangeInstantArbitraryIntrospector,  // ⭐️ 사용자 정의 인트로스펙터
                ConstructorPropertiesArbitraryIntrospector.INSTANCE,
                BuilderArbitraryIntrospector.INSTANCE,
                FieldReflectionArbitraryIntrospector.INSTANCE,
                BeanArbitraryIntrospector.INSTANCE
            )
        ))
        .build();

     

     

    1.1. objectIntrospector 정의

    ObjectIntrospector는 JavaBean 패턴을 따르는 객체들을 분석하고 생성한다.

     

    프로젝트 내에서 작성한 DTO, DAO 등 클래스 객체들이 ObjectIntrospector의 주 대상이다.

     

    1.2. 문제, 원인과 결론

    문제 : 모든 Request/Response 객체가 생성될 때마다 rangeInstantArbitraryIntrospector에 접근한다.

    원인: JavaBean 객체 하나하나가 FailoverIntrospector(Arrays.asList()) 에 등록되어 있는 순서대로 인트로스펙터에 접근해보기 때문

     

    결론: 내가 원하는 Range.class인 경우에만 접근하고, 나머지 경우는 접근조차 하지 않도록 objectIntrospector가 아닌 ArbitraryIntrospector를 설정한다.

     


     

    2.  개선 방식 : ArbitraryIntrospector 정의

    ArbitraryIntrospector 는 Arbitrary를 생성하는 방법을 결정한다.

     

    Arbitrary 생성 전략을 기반으로 Arbitrary가 만들어지고, 이를 기반으로 ObjectIntropsector를 활용해 객체가 생성된다.

     

    아래 옵션을 사용하여 특정 타입에 대해 사용자 정의 ArbitraryIntrospector를 사용할 수 있다.

     

    2.1. ArbitraryIntrospector 종류

    - pushExactTypeArbitraryIntrospector

    - pushAssignableTypeArbitraryIntrospector

    - pushArbitraryIntrospector

     

    2.1.2. pushExactTypeArbitraryIntrospector

    지정된 클래스와 정확히 일치하는 타입에만 적용한다.

    상속 관계는 무시된다.

    public class Animal {}
    
    // Dog 클래스 (Animal을 상속)
    public class Dog extends Animal {}
    
    // Animal 타입에만 적용, Dog는 적용 안됨.
    FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
        .pushExactTypeArbitraryIntrospector(Animal.class, customIntrospector)
        .build();

     

    2.1.2. pushAssignableTypeArbitraryIntrospector

    지정된 클래스의 하위 타입들에도 적용한다.

    내부적으로 특정 Class가 어떤 클래스/인터페이스를 상속/구현했는지 체크하는 isAssignableFrom()을 사용하므로 상속 관계를 포함한다.

    public class Animal {}
    
    // Dog 클래스 (Animal을 상속)
    public class Dog extends Animal {}
    
    // Animal과 Dog 타입 모두 적용.
    FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
        .pushAssignableTypeArbitraryIntrospector(Animal.class, customIntrospector)
        .build();

     

    2.1.3. pushArbitraryIntrospector

    MatcherOperator<ArbitraryIntrospector>를 직접 구현하고, 해당 MatcherOperator가 적용된다.

    사용자가 정의한 복잡한 매칭 로직을 적용할 수 있다.

     

    pushArbitraryIntrospector의 예제코드는 3.에서 기술한다.

     

     

    2.2. 성능상 이점

    ArbitraryIntrospector 설정은 JavaArbitraryIntrospector(-> JavaTimeArbitraryIntrospector -> ... -> fallbackIntrospector)보다 이전에 처리된다.

    참고 소스코드 ⬇️

    더보기
    더보기
    /*
    FixtureMonkeyOptionsBuilder.java
    line 637 ~ 651
    */
    
    // JavaDefaultArbitrary의 Introspector들
    ArbitraryGenerator defaultArbitraryGenerator = 
    	defaultIfNull(this.defaultArbitraryGenerator, this.javaDefaultArbitraryGeneratorBuilder::build);
    
    // 사용자 정의 ArbitraryIntrospector들
    List<ArbitraryIntrospector> typedArbitraryIntrospectors = arbitraryIntrospectors
    	.getList()
    	.stream()
    	.map(TypedArbitraryIntrospector::new)
    	.collect(Collectors.toList());
    
    ArbitraryGenerator introspectedGenerator =
    	new IntrospectedArbitraryGenerator(new MatchArbitraryIntrospector(typedArbitraryIntrospectors));
    
    // 사용자 정의 ArbitraryIntrospector가 JavaDefaultArbitrary 보다 우선된다.
    defaultArbitraryGenerator = new MatchArbitraryGenerator(
    	Arrays.asList(introspectedGenerator, defaultArbitraryGenerator)
    );

    즉, 커스텀한 ArbitraryIntrospector가 Introspector 체인의 가장 앞에 추가된다.

     

    ArbitraryIntrospector에서 객체 생성에 성공하면

     

    objectIntrospector를 비롯한 여러 기본 Introspectors 를 거치지 않으므로

     

    불필요한 인트로스펙터 호출을 피하고 오버헤드를 줄일 수 있다.

     

     

    2.3. 사용 시 유의사항

     

    ArbitraryIntrospector(pushExactTypeArbitraryIntrospector, pushAssignableTypeArbitraryIntrospector, pushArbitraryIntrospector)는 가장 마지막에 등록한 설정이 가장 먼저 적용되는 선입 후출(First-In, Last-Out) 구조이다.

     

    가장 높은 우선순위를 두어야 하는 설정은 가장 마지막에 작성한다.

     

    FixtureMonkey fixtureMonkey = FixtureMonkey.builder()
        // 가장 낮은 우선순위로 두고 싶은 설정을 먼저 작성한다. (JavaDefaultIntrospector보단 우선됨)
        .pushAssignableTypeArbitraryIntrospector(LowPriority.class, customIntrospector3)
        // 가장 높은 우선순위로 두고 싶은 설정
        .pushAssignableTypeArbitraryIntrospector(HighPriority.class, customIntrospector1)
        .objectIntrospector(...)
        .build();

     

    ※ List 형식으로 여러 개를 한번에 등록할 순 없음


     

     

     

    3. 매개변수화된 클래스(parameterized class)를 등록하는 방법

    이전 아티클에서도 다루었던 Range.class를 예시로 기술한다.

    Range class 문서: https://guava.dev/releases/19.0/api/docs/com/google/common/collect/Range.html

     

    Range는 Range<C extends Comparable> 형식이며

     

    이 글에서는 제네릭 타입으로 Instant를 사용하여 Range<Instant> 를 사용한다.

     

    위와 같은 Parameterized Class는 pushArbitraryIntrospector로만 등록할 수 있다.

     

    예제 코드는 다음과 같다.

        private final FixtureMonkey FIXTURE_MONKEY = FixtureMonkey.builder()
                .pushArbitraryIntrospector(
                        new MatcherOperator<>(
                                new AssignableTypeMatcher(Range.class),
                                rangeInstantArbitraryIntrospector
                        )
                )
                .build();

     

    3.1. 제네릭 개수 체크

     

    매개변수화된 클래스의 제네릭 타입 개수까지 미리 검증하도록 설정할 수 있다.

     

    예제 코드는 다음과 같다.

        private final FixtureMonkey FIXTURE_MONKEY = FixtureMonkey.builder()
                .pushArbitraryIntrospector(
                        new MatcherOperator<>(
                                new AssignableTypeMatcher(Range.class).intersect(new SingleGenericTypeMatcher()),
                                rangeInstantArbitraryIntrospector
                        )
                )
                .build();

     

    Range<C> 는 하나의 제네릭을 가지므로 `.intersect(new SingleGenericTypeMatcher())` 를 설정한다.

     

    다른 예로,

    커스텀한 Pair<T, T> 와 같이 두개의 제네릭을 가진다면 `.intersect(new DoubleGenericTypeMatcher())` 를 설정한다.

     

    현재(25.10.19) 기준 세 개의 제네릭 매칭까지 지원한다. (`TripleGenericTypeMatcher`)

    728x90
Designed by Tistory.