Spring

[Spring] ReflectUtils로 클래스 멤버에 접근하기 + 테스트 코드 리팩토링에 적용

jhkimmm 2022. 6. 1. 19:05

ReflectUtils란?

ReflectUtils는 Spring에서 제공하는 유틸리티 클래스이며, Java Bean 형식의 클래스에 대해서 자바의 Reflection API를 쉽게 사용할 수 있게 도와줍니다. 

ReflectUtils를 사용하면 자바의 Reflection API를 직접 사용하지 않고도 Java Bean 형식 클래스 멤버의 이름, 값에 손쉽게 접근할 수 있습니다. 

 

getConstructor(Class type, Class[] parameterTypes) 메서드를 통해 Constructor 객체를 얻어와서 생성자에 접근하거나,

getBeanProperties(), getBeanGetters(), getBeanSetters() 중 하나를 사용해서 해당 객체의 모든 프로퍼티에 대한 PropertyDescriptor를 얻어올 수 있습니다.

ReflectUtils.java 코드의 일부

위 코드에서 확인 할 수 있듯이 getBeanProperties(), getBeanGetters(), getBeanSetters()는 적용할 객체에 대해 읽기/쓰기를 허용할 것인지에 따라 구분됩니다.

직접 사용해보기

Controller 통합테스트를 진행하며 이미지 전송을 위한 multipart/form-data API의 테스트를 수행해야했는데,

요청을 Mocking하기 위한 MockMultipartHttpServletRequestBuilder.multipart()는 아래 코드처럼

빌더에다가 키 값마다 일일히 .param("key", "value")를 덧붙여 줘야 했습니다.

@Test
void test() throws Exception{
    //Given
    //노트생성요청 DTO 생성
    NoteCreateRequest noteCreateRequest = createNormalNoteCreateRequest();
    //전송할 이미지에 대한 MockFile생성
    List<MockMultipartFile> images = createMockMultipartFiles(2);
    
    //When & Then
    mvc.perform(
        multipart("/api/note")
                .file(images.get(0)).file(images.get(1))
                .param("notebookId", noteCreateRequest.getNotebookId().toString())
                .param("whiskeyId", noteCreateRequest.getWhiskeyId().toString())
                .param("whiskeyName", noteCreateRequest.getWhiskeyName())
                .param("distiller", noteCreateRequest.getDistiller())
                .param("price", noteCreateRequest.getPrice().toString())
                .param("rating", noteCreateRequest.getRating().toString())
                .param("age", noteCreateRequest.getAge().toString())
                .param("nose", noteCreateRequest.getNose())
                .param("taste", noteCreateRequest.getTaste())
                .param("finish", noteCreateRequest.getFinish())
                .param("description", noteCreateRequest.getDescription())
                .param("whiskeyColor", noteCreateRequest.getWhiskeyColor().toString())
                .param("smokey", noteCreateRequest.getSmokey().toString())
                .param("peaty", noteCreateRequest.getPeaty().toString())
                .param("herbal", noteCreateRequest.getHerbal().toString())
                .param("briny", noteCreateRequest.getBriny().toString())
                .param("vanilla", noteCreateRequest.getVanilla().toString())
                .param("fruity", noteCreateRequest.getFruity().toString())
                .param("floral", noteCreateRequest.getFloral().toString())
                .param("woody", noteCreateRequest.getWoody().toString())
                .param("rich", noteCreateRequest.getRich().toString())
                .param("spicy", noteCreateRequest.getSpicy().toString())
                .param("sweet", noteCreateRequest.getSweet().toString())
                .param("salty", noteCreateRequest.getSalty().toString())
                .header(JwtProperties.KEY_NAME, token)
        ).andExpect(status().isOk());
}

문제는 요청 DTO의 프로퍼티가 많았고, 프로젝트 초기라서 해당 DTO가 자주 변경될 가능성이 높았기 때문에 위와 같은 코드는 테스트 코드를 유지보수하기 매우 힘들었습니다.

 

그래서 ReflectUtils를 적용해 위 코드를 아래와 같이 리팩토링하였습니다.

@Test
void refactoredTest() throws Exception{
    //Given
    //노트생성요청 DTO 생성
    NoteCreateRequest noteCreateRequest = createNormalNoteCreateRequest();
    //전송할 이미지에 대한 MockFile생성
    List<MockMultipartFile> images = createMockMultipartFiles(2);
   
    //When & Then
    MockMultipartHttpServletRequestBuilder multipartRequest = multipart("/api/note");
    mvc.perform(
            createMultiPartRequest(multipartRequest, noteCreateRequest, images)
    ).andExpect(status().isOk());
}

private MockHttpServletRequestBuilder createMultiPartRequest(
            MockMultipartHttpServletRequestBuilder multipartRequest,
            NoteCreateRequest noteCreateRequest,
            List<MockMultipartFile> images
    ){
        //Request에 MockFile 먼저 추가
        for(MockMultipartFile image : images){
            multipartRequest.file(image);
        }
        
        //Request에 DTO의 모든 속성,값 정보 추가
        final PropertyDescriptor[] getterDescriptors = ReflectUtils.getBeanGetters(NoteCreateRequest.class);
        for(PropertyDescriptor pd : getterDescriptors){
            try{
                //파라미터로 전달한 인스턴스에 대해 getter함수를 호출한 값 얻어옴
                String value = String.valueOf(pd.getReadMethod().invoke(noteCreateRequest));
                if(!value.equals("null")){
                    multipartRequest.param(pd.getName(), value);
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        
        return multipartRequest;
    }
  • 재사용을 위해 Private 메서드를 분리함
  • DTO정보를 읽기만 하면 되므로 ReflectUtils의 getBeanGetter() 사용함
  • PropertyDescriptor를 순회하면서 DTO의 모든 프로퍼티에 대한 키와 값을 multipart 요청의 파라미터로 추가함