2024-01-10T13:27:51.341+09:00 ERROR 19502 --- [ main] o.s.b.web.embedded.tomcat.TomcatStarter : Error starting Tomcat context. Exception: org.springframework.beans.factory.UnsatisfiedDependencyException. Message: Error creating bean with name 'jwtAuthenticationFilter' defined in URL [jar:file:/Users/jun/Desktop/Developer/Motivoo-Team/Motivoo-Server/build/libs/motivooServer-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/sopt/org/motivooServer/domain/auth/config/JwtAuthenticationFilter.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'jwtTokenProvider' defined in URL [jar:file:/Users/jun/Desktop/Developer/Motivoo-Team/Motivoo-Server/build/libs/motivooServer-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/sopt/org/motivooServer/domain/auth/config/JwtTokenProvider.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'tokenRedisRepository' defined in URL [jar:file:/Users/jun/Desktop/Developer/Motivoo-Team/Motivoo-Server/build/libs/motivooServer-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/sopt/org/motivooServer/domain/auth/repository/TokenRedisRepository.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'stringRedisTemplate' defined in class path resource [org/springframework/boot/autoconfigure/data/redis/RedisAutoConfiguration.class]: Unsatisfied dependency expressed through method 'stringRedisTemplate' parameter 0: Error creating bean with name 'redisConfig': Injection of autowired dependencies failed
서버에서 Controller 테스트를 통해 RestDocs로 문서화하는 작업을 자동화하기 위해서 BaseControllerTest라는 상위의 추상 클래스를 두고 하위 컨트롤러 각각에 대응하도록 테스트 파일을 만들어 상속받아 구현하도록 하였다.
@WebMvcTest로 테스트 시 빈 등록이 이루어지지 않는 문제 에서 ExceptionHandler에 의해 컴포넌트 스캔이 제대로 되지 않는 문제가 있었는데, JWT 의존성을 머지한 이후 같은 문제가 발생했다.
결론적으로 하루 간 해당 이슈 해결에 골머리를 앓으며 찾은 문제 원인은 다음과 같이 정리할 수 있다.
Controller-Service-Repository-Domain 계층의 클래스와 같이 내가 직접 만들고 어노테이션을 통해 빈 등록을 하며 의존성을 주입받도록 명시하는 클래스 외에 JWT, Slack, AWS, Redis, Firebase와 같이 외부 라이브러리에 존재하는 클래스를 빈으로 등록하고자 할 때, 테스트 Application 환경에서도 Main과 동일하게 설정파일이 구성되어 있어야 @Value 어노테이션으로 값을 읽어오는 등 정상적인 처리가 이루어질 수 있다.
#Keyword - RedisTemplate 주입, 테스트에서의 @Value 어노테이션, 테스트 실행 환경
APPLICATION FAILED TO START
Description:
Field restTemplate in com.github.fabiomaffioletti.firebase.repository.DefaultFirebaseRealtimeDatabaseRepository required a bean of type 'org.springframework.web.client.RestTemplate' that could not be found.
The injection point has the following annotations:
- @org.springframework.beans.factory.annotation.Autowired(required=true)
Action:
Consider defining a bean of type 'org.springframework.web.client.RestTemplate' in your configuration.
Process finished with exit code 1
위와 같은 에러 로그가 반복되어서 RestTemplate을 빈으로 등록하고 있는 RedisConfig
클래스를 뜯어보았다.
@Configuration
@EnableRedisRepositories
public class RedisConfig {
@Value("${data.redis.host}")
private String redisHost;
@Value("${data.redis.port}")
private int redisPort;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisHost, redisPort);
}
@Bean
public RedisTemplate<String, String> redisTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
먼저, @Value
어노테이션으로 application.yml의 값을 잘 받아오고 있는가에 의문이 들었다.
서버의 로컬/개발/운영 환경 분리를 위해 application-**.yml의 포맷으로 파일을 관리하고 있었다.
각 테스트 클래스마다 상속받는 BaseControllerTest에서 특정 설정 파일을 읽도록 명시했지만, 해당 부분부터 읽는 데 문제가 발생하지 않았는지 의심이 갔다.
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@ExtendWith({RestDocumentationExtension.class})
@WebMvcTest(properties = "spring.config.location=classpath:/application.yml")
public abstract class BaseControllerTest {
...
}
클래스 단의 어노테이션 구성은 위와 같았고, @WebMvcTest
어노테이션의 속성으로 application.yml을 위와 같이 명시하고 테스트용 환경 설정 application.yml 파일을 생성하여 해결할 수 있었다.
아래 참고자료에 의하면 @Value의 동작 시점이 의존관계 주입 시점인 원인도 고려해볼 수 있지만, 위처럼 application-test.yml의 형태가 아닌 기본 application.yml로 파일을 세팅하니 잘 동작하는 것을 확인하였다.
*참고 자료
위 RedisConfig 클래스와 연관되는 곳은?
*TokenRedisRepository*
- 기존 코드
@Repository
@RequiredArgsConstructor
public class TokenRedisRepository {
private final StringRedisTemplate stringRedisTemplate;
private final ValueOperations<String, String> valueOperations;
public void saveRefreshToken(String refreshToken, String account) {
String key = PREFIX_REFRESH + refreshToken;
valueOperations.set(key, account);
redisTemplate.expire(key, refreshTokenValidityInMilliseconds, TimeUnit.SECONDS);
}
...
}
@Repository
@RequiredArgsConstructor
public class TokenRedisRepository {
private final RedisConfig redisConfig;
public void saveRefreshToken(String refreshToken, String account) {
RedisTemplate<String, String> redisTemplate = redisConfig.redisTemplate();
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
String key = PREFIX_REFRESH + refreshToken;
valueOperations.set(key, account);
redisTemplate.expire(key, refreshTokenValidityInMilliseconds, TimeUnit.SECONDS);
}
...
}
위 코드에서 확인할 수 있는 기존 코드의 문제점은, RedisConfig에서 이미 redisTemplate에 대한 빈 등록을 하고 있는데 정작 TokenRedisRepository에서 등록하지 않은 다른 RedisTemplate을 사용하고 있는 것이었다.
따라서, 개선된 코드처럼 RedisConfig 자체를 주입받아 해당 클래스에서 등록한 redisTemplate을 그대로 가져다 쓰도록 변경하였다.
keyword - @SpringBootTest VS @WebMvcTest
@SpringBootTest
가 아닌, @WebMvcTest
를 사용하므로 classpath에 존재하는 모든 컴포넌트를 스캔하는 것이 아니라 테스트하고자 하는 일부 컨트롤러에 관련된 클래스만 빈으로 등록할 수 있었다.
이는 스프링 컨텍스트에서 컨트롤러와 관련된 것들만 빈으로 등록하고 테스트를 한다. (나머지 Service, Repository와 같은 레이어는 의존성을 끊어버림)
따라서 모든 ControllerTest 클래스에서 상속받는 BaseControllerTest에 @MockBean으로 가짜 빈을 등록해주고 빌드 시 컴포넌트 스캔에서 오류가 발생하는 것을 막았다.
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@ExtendWith({RestDocumentationExtension.class})
@WebMvcTest(properties = "spring.config.location=classpath:/application.yml")
public abstract class BaseControllerTest {
@Autowired
protected WebApplicationContext ctx;
@Autowired
protected ObjectMapper objectMapper;
@Autowired
protected MockMvc mockMvc;
@MockBean
private RedisConfig redisConfig;
@MockBean
private TokenRedisRepository tokenRedisRepository;
@MockBean
private JwtTokenProvider jwtTokenProvider;
@MockBean
private CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint;
@MockBean
private SlackService slackService;
@MockBean
private AWSConfig awsConfig;
@MockBean
private S3Service s3Service;
@MockBean
private FirebaseConfig firebaseConfig;
@MockBean
private FirebaseService firebaseService;
@BeforeEach
void setUp(final RestDocumentationContextProvider restDocumentation) {
mockMvc = MockMvcBuilders.webAppContextSetup(ctx)
.apply(documentationConfiguration(restDocumentation))
.addFilters(new CharacterEncodingFilter("UTF-8", true))
.alwaysDo(print())
.build();
}
}
*다른 클래스에서는 아래와 같이 상속받아 사용
@WebMvcTest(UserController.class)
public class UserControllerTest extends BaseControllerTest {
}
*참고 자료
[Spring] @Mock과 @MockBean의 차이점은 무엇일까?
이렇게 어찌저찌 로컬에서 직접 ./gradlew clean build
명령어를 실행해 빌드하고 jar파일을 실행했을 때 문제없이 동작하는 것을 확인했고, 이를 도커 이미지로 빌드하여 컨테이너를 실행시키니 바로 죽어버리는 문제가 또 발생하였다.
블루/그린 무중단 배포 전략을 가져가며 blue, green 각각의 도커 컨테이너를 띄우는 방식으로 구축했는데 두 컨테이너 모두 어플리케이션 실행에서 바로 죽어버려 빌드 과정의 배포 설정 파일에서 에러가 있음을 감지했다.
Docker log를 직접 확인하고, Redis 서버 관련 에러가 나는 것을 발견했다. 문제 원인은 Redis 역시 도커 컨테이너로 띄우는 과정에서 컨테이너 이름을 application.yml의 redis host 설정 부분에 동일하게 적어주지 않아서 발생한 문제였다.
data:
redis:
host: redis # 로컬에서 테스트 할 때는 localhost로 사용
port: 6379
위 부분에서 로컬 환경 테스트 시에는 localhost로, 배포 시에는 redis로 설정해줘야 정상적으로 빌드될 수 있었다.
그동안 ./gradlew clean build -x test
로 배포 스크립트를 작성하며, 테스트 빌드에 대해서 오류를 경험해본 적이 많지 않은데, 이번에 RestDocs 에 Swagger UI를 적용하도록 새로운 시도를 하며 테스트 환경에서 경험할 수 있는 다양한 에러 상황을 잦게 마주한 것 같다.
배포의 경우, 직접적인 테스트 의존성 주입 문제와는 관련 없지만 같은 실수를 반복하지 않도록 기록하고 공유하고자 한다..