Junit Test에서 JPA @OneToMany이 작동하지 않는 현상

dev-well-being·2023년 5월 22일
1
post-thumbnail
post-custom-banner

Junit에서 테스트코드를 작성 중 계속 테스트실패가 일어났다. 아무리봐도 코드에는 이상이 없었고 해당 로직을 Junit이 아닌 API로 호출시에는 정상적으로 작동하였다.

디버깅을 통해 확인해보니 @OneToMany를 통해 자식값들을 가져오는 List 객체가 null로 넘어오는 것이다.

API를 호출할 때는 정상적으로 값이 넘오는데 Junit에서는 왜 null로 넘어오는 것일까?

해당 현상에 대해 파악하던 중 아래 글을 발견하게 되었고 원인과 조치방법에 대해서 알 수 있었다.

Junit Test - @OneToMany relationship returning the collection as null when @Transactional (propagation = Propagation.REQUIRED) is set

원인과 조치방법에 대해서 다시 정리해본다.

우리의 프로젝트의 Layer는 크게 Controller -> Service -> Repository로 구성되어 있다. Junit은 Service Layer를 호출하여 테스트코드를 작성하였다.

@OneToMany가 작동하지 않는 현상은 Service Layer에 @Transactional을 작성한 메소드를 호출할 때 발생하였다.

위의 공유한 URL에서는 @Transactional(propagation = Propagation.REQUIRED) 일 때 발생하며(옵션을 설정하지 않을 때는 Propagation.REQUIRED default이다.) 옵션을 @Transactional (propagation = Propagation.SUPPORTS) 로 했을 때는 적상적으로 작동한다는 것이다.

본인의 테스트코드도 위와 같이 변경해보았더니 정상적으로 작동하였다.

안타깝게도 해당 글에서도 왜 이러한 현상이 발생하는지 모르겠다고 한다. 이러한 현상이 발생하는 원인을 알고자 하는 문의글이었다.

아래 테스트 코드처럼 Layer를 구성했을 때는 @OneToMany @Transactional에 Propagation.SUPPORTS을 추가해야 한다.

샘플코드는 위 글에서 참조하였다.

TEST

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = DemoUsingModelMapperAndOrikaApplication.class)
@Transactional(propagation = Propagation.SUPPORTS)
@ActiveProfiles("test")
class DemoUsingModelMapperAndOrikaApplicationTests {

    private SonController sonController;


    private FamilyController familyController;


    @Autowired
    public DemoUsingModelMapperAndOrikaApplicationTests(SonController sonController, FamilyController familyController) {
        this.sonController = sonController;
        this.familyController = familyController;
    }


    @Test
    public void testAddSonsToMother() {

    	//check to make sure mother now also contains son
        MotherDTO retrievedMother = familyController.getMotherBySon(newSon.getSonId());
    	Assertions.assertTrue( retrievedMother.getSonsId() != null && retrievedMother.getSonsId().size() > 0);
        Assertions.assertEquals(retrievedMother.getSonsId().get(0), retrievedSon.getSonId());
    }

}

Mother Entity

@Table(name = "mother")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString
public
class Mother{
	
	//...
	
	@OneToMany(
            mappedBy = "mother",
            cascade = CascadeType.ALL,
            fetch = FetchType.LAZY,
            orphanRemoval = true
    )
    private List<Son> sons;
}

Son Entity

@Entity
@Table(name = "son")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString
public
class Son{

	//...
	
	@ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "mother_id")
    private Mother mother;
}

MotherRepository

@Repository
public interface MotherRepository extends JpaRepository<Mother, Long> {

    @Transactional(readOnly = true)
    @Query("select distinct m from Mother m inner join m.sons s where s.sonId = :sonId")
    Mother findMotherBySonId(@Param("sonId") long sonId);
}

FamilyService

@Service("familyServiceImpl")
public class FamilyServiceImpl implements FamilyService {

    private MotherRepository motherRepository;

    @Autowired
    public FamilyServiceImpl(MotherRepository motherRepository) {
        this.motherRepository = motherRepository;
    }

    @Override
    @Transactional(readOnly = true)
    public Mother getMotherBySonId(long sonId){
        return motherRepository.findMotherBySonId(sonId);
    }

}

FamilyFacade

public class FamilyFacade {

    @Qualifier("familyServiceImpl")
    @Autowired
    private FamilyService familyService;

    @Autowired
    private ModelMapper modelMapper;

    @Transactional(readOnly = true)
    public MotherDTO getMotherBySonId(long sonId){
        return convertToMotherDTOModelMapper(familyService.getMotherBySonId(sonId));
    }

}

FamilyController

@RestController
@RequestMapping("/family")
public class FamilyController {

    private FamilyFacade familyFacade;

    /**
     * Here we are using the principle of IoC and DI to instantiate the {@link FamilyFacade}
     * @param familyFacade - {@link FamilyFacade}
     */
    @Autowired
    public void setFamilyFacade(FamilyFacade familyFacade) {
        this.familyFacade = familyFacade;
    }


    @GetMapping("/mother/son/{sonId}")
    public MotherDTO getMotherBySon(@PathVariable("sonId") long sonId) {
        return familyFacade.getMotherBySonId(sonId);
    }

}

LOG - Propagation.SUPPORTS

2020-04-05 17:57:08.102  INFO 8072 --- [           main] o.s.t.c.transaction.TransactionContext   : Began transaction (1) for test context [DefaultTestContext@e15b7e8 testClass = DemoUsingModelMapperAndOrikaApplicationTests, testInstance = com.example.family.demo.DemoUsingModelMapperAndOrikaApplicationTests@43f7f48d, testMethod = testAddSonsToMother@DemoUsingModelMapperAndOrikaApplicationTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@1b2abca6 testClass = DemoUsingModelMapperAndOrikaApplicationTests, locations = '{}', classes = '{class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication, class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication}', contextInitializerClasses = '[]', activeProfiles = '{test}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@4c9f8c13, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@be64738, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@2667f029, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@4ac3c60d], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@6fe9c048]; rollback [true]
2020-04-05 17:57:08.181 TRACE 8072 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.family.demo.family.facade.FamilyFacade.saveMother]
2020-04-05 17:57:08.193 TRACE 8072 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.family.demo.family.service.FamilyServiceImpl.saveMother]
2020-04-05 17:57:08.204 TRACE 8072 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2020-04-05 16:21:35.110 TRACE 29840 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.family.demo.family.facade.FamilyFacade.getMotherBySonId]
2020-04-05 16:21:35.111 TRACE 29840 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.family.demo.family.service.FamilyServiceImpl.getMotherBySonId]
2020-04-05 16:21:35.112 TRACE 29840 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findMotherBySonId]
Hibernate: select distinct mother0_.mother_id as mother_i1_0_, mother0_.mother_age as mother_a2_0_, mother0_.mother_name as mother_n3_0_ from mother mother0_ inner join son sons1_ on mother0_.mother_id=sons1_.mother_id where sons1_.son_id=?
2020-04-05 16:21:35.136 TRACE 29840 --- [           main] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findMotherBySonId]
2020-04-05 16:21:35.143 TRACE 29840 --- [           main] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.example.family.demo.family.service.FamilyServiceImpl.getMotherBySonId]
Hibernate: select sons0_.mother_id as mother_i7_1_0_, sons0_.son_id as son_id1_1_0_, sons0_.son_id as son_id1_1_1_, sons0_.mother_id as mother_i7_1_1_, sons0_.son_age as son_age2_1_1_, sons0_.son_birth_date as son_birt3_1_1_, sons0_.son_name as son_name4_1_1_, sons0_.son_notes as son_note5_1_1_, sons0_.son_type as son_type6_1_1_ from son sons0_ where sons0_.mother_id=?
2020-04-05 16:21:35.168 TRACE 29840 --- [           main] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.example.family.demo.family.facade.FamilyFacade.getMotherBySonId]
2020-04-05 16:21:35.185  INFO 29840 --- [           main] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test: [DefaultTestContext@342c38f8 testClass = DemoUsingModelMapperAndOrikaApplicationTests, testInstance = com.example.family.demo.DemoUsingModelMapperAndOrikaApplicationTests@3966c679, testMethod = testAddSonsToMother@DemoUsingModelMapperAndOrikaApplicationTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@c88a337 testClass = DemoUsingModelMapperAndOrikaApplicationTests, locations = '{}', classes = '{class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication, class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication}', contextInitializerClasses = '[]', activeProfiles = '{test}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@48e4374, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@4a22f9e2, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@9225652, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@4f209819], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]

LOG - Propagation.REQUIRED

2020-04-05 17:57:08.102  INFO 8072 --- [           main] o.s.t.c.transaction.TransactionContext   : Began transaction (1) for test context [DefaultTestContext@e15b7e8 testClass = DemoUsingModelMapperAndOrikaApplicationTests, testInstance = com.example.family.demo.DemoUsingModelMapperAndOrikaApplicationTests@43f7f48d, testMethod = testAddSonsToMother@DemoUsingModelMapperAndOrikaApplicationTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@1b2abca6 testClass = DemoUsingModelMapperAndOrikaApplicationTests, locations = '{}', classes = '{class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication, class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication}', contextInitializerClasses = '[]', activeProfiles = '{test}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@4c9f8c13, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@be64738, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@2667f029, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@4ac3c60d], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@6fe9c048]; rollback [true]
2020-04-05 17:57:08.181 TRACE 8072 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.family.demo.family.facade.FamilyFacade.saveMother]
2020-04-05 17:57:08.193 TRACE 8072 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.family.demo.family.service.FamilyServiceImpl.saveMother]
2020-04-05 17:57:08.204 TRACE 8072 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2020-04-05 16:24:16.772 TRACE 29946 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.family.demo.family.facade.FamilyFacade.getMotherBySonId]
2020-04-05 16:24:16.773 TRACE 29946 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.family.demo.family.service.FamilyServiceImpl.getMotherBySonId]
2020-04-05 16:24:16.774 TRACE 29946 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findMotherBySonId]
Hibernate: select distinct mother0_.mother_id as mother_i1_0_, mother0_.mother_age as mother_a2_0_, mother0_.mother_name as mother_n3_0_ from mother mother0_ inner join son sons1_ on mother0_.mother_id=sons1_.mother_id where sons1_.son_id=?
2020-04-05 16:24:16.781 TRACE 29946 --- [           main] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findMotherBySonId]
2020-04-05 16:24:16.781 TRACE 29946 --- [           main] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.example.family.demo.family.service.FamilyServiceImpl.getMotherBySonId]
2020-04-05 16:24:16.781 TRACE 29946 --- [           main] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.example.family.demo.family.facade.FamilyFacade.getMotherBySonId]
2020-04-05 16:24:16.802  INFO 29946 --- [           main] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test: [DefaultTestContext@342c38f8 testClass = DemoUsingModelMapperAndOrikaApplicationTests, testInstance = com.example.family.demo.DemoUsingModelMapperAndOrikaApplicationTests@447543ee, testMethod = testAddSonsToMother@DemoUsingModelMapperAndOrikaApplicationTests, testException = org.opentest4j.AssertionFailedError: expected: <true> but was: <false>, mergedContextConfiguration = [WebMergedContextConfiguration@c88a337 testClass = DemoUsingModelMapperAndOrikaApplicationTests, locations = '{}', classes = '{class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication, class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication}', contextInitializerClasses = '[]', activeProfiles = '{test}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@48e4374, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@4a22f9e2, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@9225652, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@4f209819], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]
profile
안녕하세요!! 좋은 개발 문화를 위해 노력하는 dev-well-being 입니다.
post-custom-banner

0개의 댓글