상속 관계 Bean의 주입과 Mocking중 발생한 문제

taehee kim·2023년 4월 4일
0

1. 문제 상황

1-1. 단위 테스트에서 Mocking이 적용 되지 않음.

  • CustomOAuth2UserService의 loadUser는 OAuth인증 시 발급 받은 code 와 Access Token정보를 활용하여OAuth2UserRequest에 인자로 받아 ClientRegistration에 저장된 Resource Server로 회원 정보를 요청하고 회원가입 및 로그인을 진행하는 로직을 가지고 있습니다.
  • Spring Security설정시 활용해야하기 때문에 OAuth2UserService 타입을 상속 받습니다.
  • DefaultOAuth2UserService 의 loadUser는 ClientRegistration에 저장된 Resource Server로 회원 정보를 요청하는 로직을 가지고 있는데 이것이 외부 API호출이기 때문에 테스트를 위해서 Mocking을 해야합니다.
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
    @Transactional
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        Map<String, Object> attributes = super.loadUser(userRequest).getAttributes();
        //resource Server로 부터 받아온 정보중 필요한 정보 추출.
        ...
    }
}
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import({CustomOAuth2UserService.class,
    Auditor.class, QuerydslConfig.class, JpaAndEntityPackagePathConfig.class,
    TestBootstrapConfig.class, BootstrapDataLoader.class, BCryptPasswordEncoder.class
})
class CustomOAuth2UserServiceWithDaoTest {

    @MockBean
    private DefaultOAuth2UserService defaultOAuth2UserService;
    @Autowired
    private DefaultOAuth2UserService customOAuth2UserService;

    @Test
    void loadUser() {
        //given
        ...
        given(defaultOAuth2UserService.loadUser(any(OAuth2UserRequest.class)))
            .willReturn(new DefaultOAuth2User(null,
                Map.of("id", 3, "login", "test-login", "email", "test-email", "image",
                    Map.of("link", "test-link")), "login"));
        
    }

}

Mocking이 적용되지 않은 이유

  • super.loadUser는 빈으로 등록된 CustomOAuth2UserService가 자신이 가지고 있는 부모 객체의 내용을 내부적으로 호출하기 때문에 @MockBean에 의해서 등록된 Bean을 호출 하지 않습니다.

첫 번째 해결 시도

1. super.loadUser()가 아니라 DefaultOAuth2UserService를 Bean으로 개별 등록하고 이 Bean으로 loadUser를 호출하고 Mocking을 합니다.

이렇게 하게 될 경우 DefaultOAuth2UserService타입의 빈이 두 개가 되기 때문에 주입 받을 때 필드명을 빈 이름으로 특정하거나 @Qualifier를 활용하여 구분해서 주입받아야 합니다.

@Configuration
public class DefaultOAuth2UserServiceConfig {
    @Bean
    public DefaultOAuth2UserService defaultOAuth2UserService() {
        return new DefaultOAuth2UserService();
    }
}
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
	    private final DefaultOAuth2UserService defaultOAuth2UserService;
    @Transactional
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        Map<String, Object> attributes = defaultOAuth2UserService.loadUser(userRequest).getAttributes();
        //resource Server로 부터 받아온 정보중 필요한 정보 추출.
        ...
    }
}
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import({CustomOAuth2UserService.class,
    Auditor.class, QuerydslConfig.class, JpaAndEntityPackagePathConfig.class,
    TestBootstrapConfig.class, BootstrapDataLoader.class, BCryptPasswordEncoder.class
})
class CustomOAuth2UserServiceWithDaoTest {

    @MockBean
    private DefaultOAuth2UserService defaultOAuth2UserService;
    @Autowired
    private DefaultOAuth2UserService customOAuth2UserService;

    @Test
    void loadUser() {
        //given
        ...
        given(defaultOAuth2UserService.loadUser(any(OAuth2UserRequest.class)))
            .willReturn(new DefaultOAuth2User(null,
                Map.of("id", 3, "login", "test-login", "email", "test-email", "image",
                    Map.of("link", "test-link")), "login"));
        
    }

}

실패

unable to register mock bean org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService expected a single matching bean to replace but found [defaultOAuth2UserService, partner42.moduleapi.service.user.CustomOAuth2UserService]

  • Bean의 이름을 필드명에 지정하여 특정하도록 하려고 했지만 실패했습니다.

2. Bean이름 주입 대신 Qualifier사용해서 해결

  • MockBean을 특정하는 방식이 @Autowired의 주입 방식과 달라서 생기는 문제이다.
    @MockBean
    @Qualifier("defaultOAuth2UserService")
    private DefaultOAuth2UserService defaultOAuth2UserService;
    @Autowired
    @Qualifier("customOAuth2UserService")
    private DefaultOAuth2UserService customOAuth2UserService;
@Configuration
public class DefaultOAuth2UserServiceConfig {}
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
@Qualifier("customOAuth2UserService")
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

실패

  • Lombok의 Constructor를 통해 주입하는 경우 Annotation이 적용되지 않기 때문에 Qualifier가 적용되지 않는다.
@Configuration
@EnableWebSecurity
//Secured, PrePost 어노테이션 활성화
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, proxyTargetClass = true)
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Qualifier("customOAuth2UserService")
    private final DefaultOAuth2UserService customOAuth2UserService;

3.lombok.config생성

  1. src/main/java/lombok.config 파일을 만들어주세요.(resources가 아닙니다. src/main/java입니다!)

  2. lombok.config에 다음 내용을 넣어주세요.

lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier

  1. 프로젝트를 다시 컴파일 한 다음 실행해주세요. IntelliJ를 사용하면, out이라는 폴더가 있는데 이 폴더를 꼭 모두 지우고 다시 실행해주세요. gradle은 gradlew clean을 한번 해주고 실행해주세요.

해당 옵션을 적용한 후에 빌드된 .class 파일을 확인해보면 다음과 같이 @Qulifier가 포함 된 것을 확인할 수 있습니다.

profile
Fail Fast

0개의 댓글