현재 개발 중인 서비스는 모바일 화면을 기준으로 개발하다 보니 cursor-based 무한스크롤 방식으로 조회하는 기능이 많다.
cursor-based로 구현할 때 항상 마지막 cursor를 구한 후 클라이언트와 주고받아야 하는데, 이를 구하는 메서드는 많은 곳에서 사용된다.
예를 들어 다음 메서드를 보자
위의 메서드는 List로 형식으로 된 content를 받아서 마지막 값에서 userShortsId를 추출해 낸다. 하지만 서비스에는 수많은 조회 기능이 존재하고, content에 FeedShortsOutDto가 아닌 다른 Dto가 들어오는 경우도 생기게 되었다.
물론 모든 경우마다 메서드를 만들어서 사용해도 되지만, 다른 Dto에도 공통적으로 userShortsId를 cursor로 사용하고 있었기에 중복되는 코드가 많이 생길 것이 분명했다. 따라서 나는 기존 코드를 재사용할 방법을 찾아보았다
Generic & Reflection으로 해결 !
동적으로 타입이 바뀌어야 하는 경우 어떤 것이 먼저 떠오르는가? 나는 며칠 전 Java 스터디에서 공부한 Reflection이 떠올랐고, 어떤 타입이든 들어올 수 있게 Generic을 사용하였다. 다음 코드를 보자
- 제네릭에 타입 매개변수를 사용하여, 어떠한 타입이든 받을 수 있음
- content에서 Class 클래스를 가져온 후, getUserShortsId 메서드를 추출하여 사용
이제 타입 매개변수와 리플렉션을 사용했으므로, userShortsId를 필드로 가지고 이를 cursor로 사용하는 모든 Dto를 사용할 수 있게 되었다!! 만..
Error ?
동적으로 클래스의 정보를 가져오는 리플렉션의 동작 방식 때문에 컴파일 에러를 잡지 못하였고, 런타임 에러가 발생했다
??? 이게 무슨 일이지..
분명 Lombok의 @Getter가 붙은 Dto를 사용했는데, 어째서 'NoSuchMethodException'이 발생한 것일까?
나는 Reflection을 공부했던 기억을 떠올리며 신나게 문제에 접근을 해보았다
추측
- 리플렉션은 컴파일 시에 metaspace에 저장된 클래스의 메타정보를 읽어서 동적으로 정보를 가져온다
- 그렇다면, 'NoSuchMethodException'이 발생한 이유는 'getUserShortsId()'라는 메서드가 클래스의 메타정보에 없다는 뜻으로 해석할 수 있다
- 'getUserShortsId()'는 롬복의 @Getter로 생성되는 메서드이므로, 이 메서드가 클래스의 메타정보에 없다! ( -> 1차 오류)
- 그렇다, 리플렉션을 공부할 때, 스프링 프레임워크 대부분의 어노테이션이 리플렉션 기반으로 동작한다고 공부했다. 따라서 @Getter로 생성되는 'getUserShortsId()' 메서드는 동적으로 생성되므로 컴파일시에 생성되어 바이트코드로 저장되는 것이 아니므로 metaspace에 저장되지 않는 것이고, 따라서 리플렉션을 사용하여 가져올 수 없는 것이다! ( -> 2차 오류)
라는 추측에 결국 get 메서드를 Dto마다 직접 만들어 줘야 한다는 결론에 도달했다
그러나... 정답은 예상외로 단순한 문제였다
정답
이전 코드의 문제점을 보자면, content는 List<T>로 되어있는데 content.getClass()로, T가 아닌 List의 Class 클래스를 가져온 것이 문제였다. List에서 Class 클래스를 가져와서 'get~' 메서드를 찾으니 당연히 없다는 예외가 발생한 것이다.
따라서 content에서 곧바로 가져오지 않고, 아무 원소(위의 코드에서는 첫 번째 원소)를 가져온 다음 'get~' 메서드를 가져와서 해결할 수 있었다.
사실 조금만 찾아보면 Lombok의 @Getter는 리플렉션 기반으로 동작하는 것이 아니라, 컴파일 시점에 바이트코드를 변환하여 원하는 부분을 주입해 주는 방식으로 동작한다는 것을 알 수 있다. 따라서 @Getter를 사용하면 metaspace에 'get~'메서드가 저장이 되어있다는 뜻이기에 리플렉션에 전혀 영향을 주지 않는다.
리플렉션에 너무 꽂혀서 흥분했더니 시야가 좁아졌던 것 같다... 추측하지 말고 정확히 알아보고 사용해야겠다는 걸 깨닫는 이슈였다..
그리고 리플렉션 정말 편하긴 하지만,, 컴파일 시점에 예외를 확인할 수 없다는 게 너무 불편한 것 같다. 현재 테스트 커버리지가 0인 우리 서비스에서 당장 사용하기는 조금 위험하지 않나 라는 생각이 든다
Reference
- Lombok의 동작 원리: https://applefarm.tistory.com/136