csv파일을 이용한 대량 등록 작업을 처리하면서 opencsv를 활용한 경험에 대해서 글을 작성해보려고합니다.

 

고민했던 상황

  • csv 파일을 통해서 업로드를 진행하지만, 추후  excel을 통한 확장 가능성을 열어두고 싶었음
  • 깔끔하고 쉽게 csv파일을 읽어오고 싶었다.
  • opencsv를 통해서 쉽게 csv 파일을 읽을 수 있지만, 중복 코드를 제거하고 싶었음

 

opencsv는 csv 파일을 쉽게 객체로 변환해주는 역할을 한다. 이런식으로 쉽고 가시성있게 코드를 작성할 수 있다.

      final List<Man> list = new CsvToBeanBuilder<>(new FileReader("text.csv"))
                .withType(Man.class)
                .build()
                .parse();

open csv에 대해 자세한 사용 방법에 대해 알고 싶으시면 여기서 확인하시면 됩니다.

 

1단계

다음과 같이 인터페이스를 정의한 뒤, 어뎁터를 이용해서 구현체를 찾아서 읽는 상황

 

 

1. MultipartFile을 읽어주는 인터페이스 정의

interface MultipartFileConverter {

    <T> List<T> toPojo(MultipartFile file, Class<T> pojoType);

    boolean isSupport(String fileExtension);
}

 

 

2. csv 파일을 읽어오는 구현체

@Component
public record CsvConverter() implements MultipartFileConverter {

    private static final String MATCHING_EXTENSION = "csv";

    @Override
    public <T> List<T> toPojo(final MultipartFile file, final Class<T> clazz) {
        try {
            final BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()));

            return new CsvToBeanBuilder<T>(reader)
                .withType(clazz)
                .build()
                .parse();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public boolean isSupport(final String fileExtension) {
        return MATCHING_EXTENSION.equals(fileExtension);
    }
}

 

3. 어뎁터

apache라이브러리가 있어서 FileUtils를 통해서 확장자를 추출했지만, 순수 코드로 확장자를 추출하는 것도 좋습니다.

@Component
public record MultipartFileConvertAdapter (
    List<MultipartFileConverter> multipartFileConverters
) {
    public <T> List<T> toPojo(final MultipartFile file, final Class<T> clazz) {
        final var converter = findConvertByExtension(file);

        return converter.toPojo(file, clazz);
    }

    private MultipartFileConverter findConvertByExtension(final MultipartFile file) {
        final String extension = FilenameUtils.getExtension(file.getOriginalFilename());

        return multipartFileConverters.stream()
            .filter(converter -> converter.isSupport(extension))
            .findAny()
            .orElseThrow(IllegalStateException::new);
    }
}

 

테스트 코드 작성

test
  resource
    ㄴ foo.csv
    
    
# foo.csv
age
10
20

 

class CsvConverterTest {

    MultipartFileConverter converter;

    @BeforeEach
    void setUp() {
        converter = new CsvConverter();
    }

    @Test
    @DisplayName("csv 파일을 읽어서 object로 변환한다.")
    void csv_convert() throws IOException {
        final File file = readCsvFile();
        final MockMultipartFile mockFile = generateMockMultipartFile(file);

        final List<Human> humans = converter.toPojo(mockFile, Human.class);
        assertThat(humans).extracting("age").containsExactly(10, 20);
    }

    private File readCsvFile() {
        return new File(getClass().getClassLoader().getResource("foo.csv").getFile());
    }

    private MockMultipartFile generateMockMultipartFile(final File file) throws IOException {
        return new MockMultipartFile("foo", new FileInputStream(file));
    }
}

테스트가 정상적으로 성공하는 것을 확인할 수 있습니다.


사용 코드

이제 다음과 같이 컨트롤러에서 어뎁터를 호출해서 작업을 진행할 수 있다.

@RestController
public record HumanController(
    MultipartFileConvertAdapter multipartFileConvertAdapter
) {

    @PostMapping("/api/v1/human")
    public void batchInsert(@RequestParam MultipartFile file) {
        final List<Human> humans = multipartFileConvertAdapter.toPojo(file, Human.class);
        // do something
    }
}

이제 만들어놓은 기능을 이용해서 컨트롤러에서 받은 MultipartFile을 객체로 변환하면됩니다.

하지만 위 코드처럼 사용하면 다음과 같은 문제점이 발생합니다.

 

문제점

  • 불필요한 의존성 추가 발생 (해당 어뎁터에 대한 의존관계가 계속 필요합니다)
  • 어뎁터를 호출하는 코드 중복이 여러 컨트롤러에서 발생

해결방법

ArgumentResolver를 이용해서 이런 중복 로직을 처리할 수 있습니다. 풀 네임은 HandlerMethodArgumentResolver이며 이 인터페이스의 정체는 핸들러 어뎁터에서 핸들러를 호출할때  메서드 파라미터에 값을 넣어주는 역할을 한다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface MultipartFileConvert {
}

 

@Component
public record MultipartFileConvertResolver (
    MultipartFileConvertAdapter multipartFileConvertAdapter
) implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(final MethodParameter parameter) {
        return parameter.hasMethodAnnotation(MultipartFileConvert.class);
    }

    @Override
    public Object resolveArgument(final MethodParameter parameter,
                                  final ModelAndViewContainer mavContainer,
                                  final NativeWebRequest webRequest,
                                  final WebDataBinderFactory binderFactory) throws Exception
    {
        final MultipartHttpServletRequest request = (MultipartHttpServletRequest) webRequest.getNativeRequest();
        final MultipartFile file = request.getFile(Objects.requireNonNull(parameter.getParameterName()));
        final Class<?> actualTypeArgument = extractParameterActualType(parameter);

        return multipartFileConvertAdapter.toPojo(file, actualTypeArgument);
    }

    private Class<?> extractParameterActualType(final MethodParameter parameter) {
        return (Class<?>) ((ParameterizedType) parameter.getGenericParameterType()).getActualTypeArguments()[0];
    }
}

 

@Component
public record WebConfig(
    MultipartFileConvertResolver multipartFileConvertResolver
) implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(multipartFileConvertResolver);
        WebMvcConfigurer.super.addArgumentResolvers(resolvers);
    }
}

이제 다음과 같이 편리하게 사용할 수 있습니다.

@RestController
public record HumanController() {

    @PostMapping("/api/v1/human")
    public void batchInsert(@MultipartFileConvert List<Human> humans) {
        // do something
    }
}

이전이랑 비교했을 때 어뎁터에 대한 의존성이 없고, 호출하는 중복 코드를 삭제하여 훨씬 더 깔끔해진 것을 확인할 수 있습니다.

 

최종 다이어그램

 

'IDE, Git, Etc' 카테고리의 다른 글

git-flow에 gitmoji를 얹어보자  (0) 2021.02.11