Java8 Stream 특정 Key 중복제거

2022. 5. 30. 01:19Spring/JPA

Java8 Stream 특정 Key 중복 제거

안녕하세요! 이번에는 제가 실무에서 겪었던 특정 키 중복제거를 위한 방법을 소개해보려고 합니다!

문제상황

  • 엑셀 Bulk 등록을 하는 로직을 구현하는 중이었는데요! FE에서 다음과 같은 데이터를 JSON으로 배열로 보내주고 있었죠.

    @Getter  
    @Setter  
    @NoArgsConstructor  
    @AllArgsConstructor  
    public class UploadExcelCarsDTO  
    {  
      @NotBlank(message = "잘못된 modelCode 입니다.")  
      private String modelCode;  
    
      @NotBlank(message = "잘못된 차량번호입니다.")  
      private String number;  
    
      @Min(0)  
      @Max(9999)  
      private Integer produceYear;  
    
      private String memo;  
    
      private LocalDate registrationDate;   
    }
  • 여기서 차량 별로 modelCode라는 데이터가 들어오게 될 텐데 중복이 되는 modelCode도 올 수 있고, 아니면 한 개라도 중복이 되지 않을 수 도 있죠. 저는 이 DTO 객체 List에서 modelCode만 데이터를 뽑아서 중복을 제거 하고 싶었습니다.

distinct 사용법

  • Stream에서는 distinct() 메서드를 활용하여 중복을 제거 할 수 있는데요.
    distinct() 메서드는 특정 객체의 중복을 지울 때 사용 될 수 있습니다.
public static void main(String[] args){
        List<String> names = Arrays.asList("Jang", "Hyan", "UYjh", "IIuy", "KiM");

        names.stream()
            .distinct() // 중복 제거
            .forEach(System.out::println);

        System.out.println();

        names.stream()
            .filter(n -> n.startsWith("Jang")) // 필터링
            .forEach(System.out::println);

        System.out.println();

        names.stream()
            .distinct()
            .filter(n -> n.startsWith("K")) // 중복 제거 후 필터링
            .forEach(System.out::println);
}
  • 그래서 DTO 혹은 클래스에 Equals 메서드가 재정의가 되어야하거나 구현이 되어있어야합니다.

    Returns a stream consisting of the distinct elements (according to Object.equals(Object)) of this stream.
    For ordered streams, the selection of distinct elements is stable (for duplicated elements, the element appearing first in the encounter order is preserved.) For unordered streams, no stability guarantees are made.

modelCode distinct 테스트

  • 위에 정의 되어 있는 UploadExcelCarDTO 클래스에서 equals 메서드를 재정의해서 정말 modelCode 중복이 제거 되는지 확인해봅시다.

    @Test  
    void distinctTest() {  
      List<UploadExcelCarsDTO> tests = new ArrayList<>();  
      tests.add(new UploadExcelCarsDTO("010H314", "123가1234", 2022, "", LocalDate.now()));  
      tests.add(new UploadExcelCarsDTO("010H314", "123나1234", 2022, "", LocalDate.now()));  
      tests.add(new UploadExcelCarsDTO("010L101", "123다1234", 2022, "", LocalDate.now()));  
      tests.add(new UploadExcelCarsDTO("010L101", "123라1234", 2022, "", LocalDate.now()));  
      tests.add(new UploadExcelCarsDTO("010N202", "123마1234", 2022, "", LocalDate.now()));  
      tests.add(new UploadExcelCarsDTO("0201109", "123바1234", 2022, "", LocalDate.now()));  
      tests.add(new UploadExcelCarsDTO("020C1P3", "123사1234", 2022, "", LocalDate.now()));  
    
      List<UploadExcelCarsDTO> collect = tests.stream().distinct().collect(Collectors.toList());  
    
      assertThat(collect.size()).isEqualTo(5);  
    }
  • 중복이 총 2개가 있으니 제가 기대하는 결과값은 5개 입니다. 과연 기대한 결과가 맞을까요?🥲

  • Stream<T> 의 T타입 객체의 인스턴스만을 중복키로 사용하여 제거한다.
    때문에 T타입 객체내의 특정 키를 기준으로 중복제거를 할수는 없기때문에...ㅠㅠ
    기대했던 방법이 잘못되었다는 걸 깨닫고 열심히 삽질을 해봅니다...🤔

특정 Key의 중복제거를 위한 방법

  • 열심히 삽질과 간단한 방법이 없을까하는 고심끝에 StackOverflow에서 한 줄기 빛이 내려왔다...🥹
    public static <T> Predicate<T> distinctByKey( Function<? super T, Object> keyExtractor) {Map<Object, Boolean> map = new ConcurrentHashMap<>();return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;  
    }
    다음과 같은 메서드인데, ==Predicate를 리턴하는 distinctByKey 라는 메서드는 메서드의 지역변수로 HashMap을 생성하고 리턴하는 Predicate 에서는 Map을 참조하고 있다.==
  1. ConcurrentHashMap으로 된 이유가 있을까?
    • parallelStream 의 경우 때문이라고 생각되어짐
  2. stream 일 경우 ?
    • HashMap으로 해도 괜찮음
  3. 메서드를 통해 생성된 Predicate 객체는 생성될 때 마다 개별 Map을 가진다.
  • 단순히 사용할 때는 몰랐는데, 한 블로그에서도 똑같은 내용이이었는데 메서드의 로직에 대한 의구점이 있어서 좋은 고민이어서 참조하였다. 블로그 바로가기

사용예

List<String> distinctModeCode = 
        uploadExcelCarsDTO
            .stream()
            .filter(distinctByKey(UploadExcelCarsDTO::getModelCode))
            .map(UploadExcelCarsDTO::getModelCode)
            .collect(Collectors.toList());
@Test  
@DisplayName("오버라이드 distinctByKey 메서드 테스트")  
void customDistinctMethod() {  
    List<UploadExcelCarsDTO> tests = new ArrayList<>();  
    tests.add(new UploadExcelCarsDTO("010H314", "123가1234", 2022, "", LocalDate.now()));  
    tests.add(new UploadExcelCarsDTO("010H314", "123나1234", 2022, "", LocalDate.now()));  
    tests.add(new UploadExcelCarsDTO("010L101", "123다1234", 2022, "", LocalDate.now()));  
    tests.add(new UploadExcelCarsDTO("010L101", "123라1234", 2022, "", LocalDate.now()));  
    tests.add(new UploadExcelCarsDTO("010N202", "123마1234", 2022, "", LocalDate.now()));  
    tests.add(new UploadExcelCarsDTO("0201109", "123바1234", 2022, "", LocalDate.now()));  
    tests.add(new UploadExcelCarsDTO("020C1P3", "123사1234", 2022, "", LocalDate.now()));  

    List<UploadExcelCarsDTO> uploadExcelCarsDTOS = tests.stream()  
                                                        .filter(distinctByKey(UploadExcelCarsDTO::getModelCode))  
                                                        .collect(Collectors.toList());  

    for (UploadExcelCarsDTO carsDTO : uploadExcelCarsDTOS) {  
        Assertions.assertTrue(tests.contains(carsDTO));  
    }  
    assertThat(uploadExcelCarsDTOS.size()).isEqualTo(5);  
}

'Spring > JPA' 카테고리의 다른 글

Bulk Exel Data Insert 2부  (0) 2022.06.20
Bulk Excel Data Insert 1부  (0) 2022.06.20
JPQL은 만능이 아니다!  (0) 2021.12.23
[Spring JPA] 즉시로딩과 지연로딩  (0) 2021.06.28
Spring JPA [상속관계 맵핑]  (0) 2021.06.17