Fixture Monkey로 만든 객체의 필드가 null일 때
null을 만들지 않도록 했는데 NPE가 발생했습니다
Fixture Monkey로 테스트 객체를 생성하다가 NPE가 발생했습니다. AnimalHospital 안에 있는 HospitalInfo의 name을 조회할 때였습니다. 객체 생성 코드에서 필요한 부분만 줄여 남긴 예시입니다.
public class AnimalHospitalTestFixture extends DomainTestFixture {
public static AnimalHospital createAnimalHospital() {
ArbitraryBuilder<HospitalInfo> hospitalInfoArbitraryBuilder = SUT.giveMeBuilder(HospitalInfo.class)
.set(javaGetter(HospitalInfo::getName), Arbitraries.strings().alpha().ofLength(10))
// ...
return SUT.giveMeBuilder(AnimalHospital.class)
.setNull(javaGetter(AnimalHospital::getId))
.set(javaGetter(AnimalHospital::getHospitalInfo), hospitalInfoArbitraryBuilder)
.sample();
}
//...
}공통 설정에서는 생성자 기반의 introspector를 쓰고 있었습니다. introspector는 객체를 어떤 방식으로 만들고 값을 채울지 결정합니다.
public abstract class DomainTestFixture {
protected static final FixtureMonkey SUT = FixtureMonkey.builder()
.objectIntrospector(ConstructorPropertiesArbitraryIntrospector.INSTANCE)
.defaultNotNull(true)
.build();
}이 설정으로 record DTO는 잘 만들어졌습니다. defaultNotNull(true)도 설정했으므로, null이 일부러 만들어졌다기보다는 객체 생성 방식에 문제가 있을 수 있다고 봤습니다.
생성자 접근 제어자를 먼저 바꿨습니다
HospitalInfo의 생성자를 확인했습니다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Embeddable
public class HospitalInfo {
@Column(name = "name")
private String name;
//...
@Builder
private HospitalInfo(
String name,
//...
) {
this.name = name;
//...
}
}JPA가 사용할 기본 생성자는 protected로, Lombok 빌더용 생성자는 private로 두었습니다. 혹시 public 생성자가 없어서 값을 채우지 못하는 걸까요? 파라미터가 있는 생성자를 public으로 바꿔 봤습니다.

그래도 해결되지 않았습니다.
빌더 방식도 시도했습니다
그럼 Lombok 빌더가 있으니 BuilderArbitraryIntrospector.INSTANCE로 바꾸면 되지 않을까요? 하지만 이 방법으로도 같은 문제를 만났습니다.

Lombok @Builder의 동작 원리를 읽고 생성자에 붙인 @Builder와 클래스에 붙인 경우의 차이가 원인이라고 생각했습니다. 당시에는 “클래스에 붙이지 않았으므로 빌더가 없다”고 본 것입니다.
하지만 Lombok 공식 Builder 문서에서 설명하듯 생성자에 붙인 @Builder도 해당 생성자를 호출하는 빌더를 만듭니다. 이 실패는 당시 프로젝트에서 관찰한 결과이고 애노테이션을 붙인 위치만으로는 설명할 수 없습니다.
필드 리플렉션으로 값을 채웠습니다
FieldReflectionArbitraryIntrospector.INSTANCE로 바꾸자 이 사례의 NPE가 해결됐습니다.

그렇다고 Entity 때문에 기존 record DTO에 쓰던 생성자 방식을 모두 버리고 싶지는 않았습니다. 그래서 여러 생성 방식을 순서대로 시도할 수 있는 failover 구성을 쓰기로 했습니다.
이름도 FailoverArbitraryIntrospector인 줄 알았지만 실제 코드에서는 FailoverIntrospector를 썼습니다. 현재 프로젝트에 적용하려면 쓰고 있는 Fixture Monkey 버전이 어떤 이름과 설정을 제공하는지 확인해야 합니다. 아래는 필요한 설정 부분입니다.
public abstract class DomainTestFixture {
protected static final FixtureMonkey SUT = FixtureMonkey.builder()
.objectIntrospector(new FailoverIntrospector(
Arrays.asList(
FieldReflectionArbitraryIntrospector.INSTANCE,
ConstructorPropertiesArbitraryIntrospector.INSTANCE
)
))
.defaultNotNull(true)
.build();
}당시 선택한 순서는 필드 리플렉션, 생성자 방식이었습니다. BeanArbitraryIntrospector는 기본 생성자와 setter를 쓰는 방식이어서 넣지 않았습니다. 테스트 객체를 만들기 위해 엔티티에 setter를 추가하고 싶지 않았기 때문입니다.