시작하며
안녕하세요 저는 카카오엔터프라이즈 신입 개발자 harry 입니다. 3개월간의 인턴 과정을 마치고 전환되어, AI서비스플랫폼실 서버 개발팀 비즈플랫폼 파트 IAM 셀에서 근무하고 있습니다.
제가 현재 소속되어 있는 AI서비스플랫폼실 서버 개발팀은 테스트 코드의 중요성을 강조하는 팀입니다. 사이드 이펙트를 배포 전에 발견하고 견고한 코드를 작성하기 위해서 테스트 코드를 잘 작성하는 것은 개발자의 기본이라고 생각합니다. 그러나 저와 같은 신입 개발자 입장에서 Spring Framework는 여전히 어렵고, JUnit이라는 테스트 프레임워크는 낯설기만 한데다가 Kotlin이라는 언어는 생소하기까지 합니다. 그래서 테스트 코드를 잘 작성하는 일이 쉽지 않았으며, 지금도 여전히 다양한 케이스를 커버하는 테스트 코드를 잘 작성하려고 노력하고 있습니다. 오늘은 제가 테스트 코드를 작성하며 어떤 문제가 발생했고 그 문제를 어떻게 해결했는지에 대해서 이야기해보려고 합니다.
문제 상황
테스트 코드에서 생성자를 이용하여 Bean을 주입받으려고 했으나 오류가 발생한 것이 문제의 발단이었습니다.
아래는 설명을 위한 예제 테스트 코드입니다.
Kotlin에서는 Java와는 달리 생성자를 아주 쉽게 정의할 수 있는데요, Intellij 또한 생성자 주입을 권장하고 있기 때문에 메인 코드에서 위와 같은 생성자로 Bean을 주입받을 수 있었습니다. 그러나 제가 작성한 테스트 코드에서 에러가 발생하였고 저는 원인을 몰라서 의아했습니다.
심지어 기존 Intellij에서 Bean을 주입받는 경우 보이는 콩 모양 아이콘 또한 보이지 않았습니다. 이때 저는 느낌적인 느낌으로 Bean 주입이 안 되는 것이 원인이 아닐까 싶기는 했었지만 그래도 여전히 원인을 몰라 고민을 하고 있었습니다. 그때 같은 팀 개발자인 PJ가 지나가시다가 제가 고민하고 있는걸 보시더니 다음과 같이 코드를 수정해주셨습니다.
놀랍게도 @Autowired constructor를 명시해주자 테스트 코드가 정상적으로 동작했으며 콩 모양도 잘 보이기 시작했습니다.
환호도 잠시 저는 깊은 고민에 빠지기 시작했습니다.
왜 @Autowired constructor를 붙여야지만 Bean 주입이 되는 것인가..?
그렇습니다.
여러분도 잘 아시는 개발자의 딜레마가 시작된 것이지요.
개발자라면 늘 겪는 문제인, 해결하고도 왜 해결이 됐는지 모르는 상황이 발생하고야 말았습니다. 퇴근 후 집에 돌아와 문제를 파헤치기 시작했고 나름의 답을 찾아서 지금부터 여러분들에게 공유를 드리고자 합니다. 그럼 지금부터 논리적으로 이 문제가 왜 발생했으며 왜 해결됐는지를 하나씩 살펴보도록 하겠습니다.
에러 로그를 읽어보자
PJ가 저의 문제를 함께 봐주시면서 이렇게 말씀해주셨습니다.
“이렇게 에러가 나면 에러 로그를 잘 봐야해요 Harry, 여기에 답이 있어요.”
실제 코드에서는 당시 Bean 주입 외에도 다른 여러가지 에러들이 많았기 때문에 에러 로그가 훨씬 많이 찍혔었고 저는 그 수 많은 에러를 일일이 확인할 엄두조차 나지 않았습니다. 그러나 PJ의 조언을 듣고 에러 로그를 침착하게 다시 읽기 시작했습니다. 아래의 에러 로그가 바로 이번 글의 문제를 해결할 핵심에 해당하는 에러 로그입니다.
에러 로그의 내용은 Jupiter가 해당 파라미터에 등록된 ParameterResolver가 없어서 발생한 에러라는 것을 의미하고 있었습니다. 이 문제를 해결하기 위해서는 Jupiter라는게 정확히 무엇인지부터 알아야 했습니다. 현재 저희가 사용하고 있는 JUnit은 5버전인데, 이 JUnit 5가 어떤 구조로 이루어져 있는지를 당시의 저는 몰랐었습니다.그래서 문제를 해결하기에 앞서 JUnit 5의 구조에 대해서 공부해야할 필요성 또한 느꼈습니다.JUnit 5는 어떻게 구성되어 있는지, Jupiter의 역할은 무엇인지 알아야 이 문제의 해결에 실마리를 잡을 수 있을 것 같았습니다.
JUnit 5 Architecture
JUnit 5의 Architecture는 크게 세 부분으로 나누어져 있습니다.
각각 하나씩 간단히 알아보겠습니다. 먼저, JUnit Platform은 테스트를 실행할 수 있는 Test Engine을 포함하며 여러 Tool(콘솔, eclipse, Intellij…)에 일관성 있는 API를 제공하는 역할을 담당합니다. Jupiter와 Vintage 모두 이 JUnit Platform의 Test Engine을 구현한 구현체입니다. 이 Jupiter와 Vintage의 차이점은 Jupiter의 경우 우리가 사용하는 JUnit 5의 구현체이며, Vintage의 경우 하위 JUnit 버전(JUnit 4, JUnit 3)를 지원하는 구현체입니다. 그래서 우리는 일반적으로 JUnit 5로 테스트 코드를 작성한다고 하면 Jupiter를 사용한다고 이해하면 되겠습니다. 여담으로 JUnit 팀이 이러한 JUnit Platform의 Test Engine을 구현하게 하는 이유를 다음과 같이 말하고 있습니다.
Kakao i 번역기를 돌리면 다음과 같이 번역이 됩니다.
JUnit Platform과 구현체를 분리한 이유는 다른 테스트 프레임워크들이 Test Engine API를 구현하게 함으로써 JUnit의 생태계를 넓히고 나아가 도구들(e.g. IDE)과의 일관성 또한 가지게 하기 위함이었습니다.
에러의 원인
그러면 Jupiter는 왜 ParameterResolver를 못찾는 것일까요? Spring에서의 생성자 주입의 경우 메인 코드에서 이를 담당하는 것은 Spring IoC Container 입니다. Spring Framework의 Spring IoC Container는 Bean들을 관리하고 있다가 필요한 시점에 Bean을 주입해주는 역할을 담당합니다. 메인 코드에서 @Autowired construct를 굳이 명시해주지 않아도 Bean 주입이 가능했던 이유는 Spring IoC Container가 적절한 시점에 생성자 주입을 해왔기 때문입니다. 그러나 테스트 코드의 경우는 조금 다릅니다. 테스트 프레임워크에서 테스트 클래스의 관리 주체는 Spring IoC Container가 아닌 Jupiter가 담당합니다. @Autowired construct를 명시적으로 테스트 클래스 생성자에 알려주어야지만 Jupiter가 Spring IoC Container에게 Bean 주입을 요청할 수 있게 됩니다. 테스트 프레임워크에서의 주체는 Jupiter이므로 아무리 생성자 주입이라고 하더라도 @Autowired construct 애노테이션이 명시되어 있지 않은 테스트 클래스에 Bean 주입을 받을 수 없게 됩니다. 결과적으로 Jupiter는 자기 딴에는 열심히 생성자 매개 변수를 처리할 ParameterResolver를 찾아보려 노력하지만 없었고, 결과적으로 예외를 뱉게 되었습니다.
마치며
프레임워크의 사전적 의미는 뼈대라는 뜻이 있지만, 소프트웨어에서의 프레임워크의 의미는 코드를 컨트롤하는 주체가 사용자가 아닌 프레임워크에 있다는 의미가 있습니다. 제가 이 문제가 발생한 원인 또한 테스트 프레임워크에서의 코드를 컨트롤하는 주체가 누구인지를 몰랐기 때문에 발생한 문제였습니다. 이번 기회로 많은 것들을 배울 수 있었는데 혼자 알기보다는 공유해서 더 많은 분들께 도움이 되었으면 하는 바람에서 이 글을 작성해보았습니다. 부족한 글이지만 도움이 되셨기를 바라며 글을 마칩니다. 끝으로 문제 해결에 도움을 주신 PJ에게 진심으로 다시 한번 감사드립니다.
새로운 길에 도전하는 최고의 Krew들과 함께 해요!
댓글