-
[Fixture Monkey] CustomArbitraryIntrospector를 등록하는 여러 방법📚 개발백과 2025. 10. 19. 22:21728x90
[Fixture Monkey] 불변 객체를 위한 사용자 정의 인트로스펙터 예제 코드 - 기초편
불변 객체(`google.common.collect.Range`)를 위한 사용자 정의 인트로스펙터 예제 코드입니다.사용 버전: 1.1.11 ⬇️ 코드만 먼저 보기더보기더보기더보기/** * Range 타입에 대한 값을 반환하는 사용자 정
the0.tistory.com
이전 포스팅 글(사용자 정의 인트로스펙터 예제 코드 - 기초편)과 이어집니다.
공식문서를 참고하여 Fixture Monkey의 CustomArbitrayIntrospector를 만들고
customIntrospector를 objectIntrospector에 등록했다.
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에만 적용하는 방법이 있을까?
당연히 존재한다.
이 아티클에서는 인트로스펙터를 설정하는 방법을 다룬다.
참고 자료:
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'📚 개발백과' 카테고리의 다른 글
[Fixture Monkey] 불변 객체를 위한 사용자 정의 인트로스펙터 예제 코드 - 기초편 (0) 2025.10.06 [Spring Data JPA] 서비스 레이어의 어느 메서드에 @Transactional을 안붙이면? (2) 2025.06.27 [Spring Data JPA] 커넥션은 언제 릴리즈 될까 (w/ @Transactional) (0) 2025.06.26 [FastAPI, PostgreSQL] postgresql://와 postgresql+asyncpg:// 의 차이 (1) 2024.10.18 오프라인 상태에서 cURL 사용하기 (0) 2024.10.01