본문 바로가기

Development/Java

[JAVA] 중첩된 목록을 기준으로 목록을 필터링하는 방법

728x90

List에서 특정 조건에 해당하는 것을 찾는 것은 다양한 방법이 존재합니다.

원시적인 방법으로 반복문으로 처리하게 될 경우 생각보다 코드량도 많고 쓸데 없는 객체를 생성하기도 합니다.

Stream API를 활용하면 보다 가독성 높고 간략한 코드로 구성 가능해집니다.

이러한 부분을 알아보기 위해 다음 글을 가져왔습니다.

 

 


 

 

 

1. 개요

Java에서 중첩된 리스트를 포함하는 리스트를 필터링하는 방법을 살펴보겠습니다. 다른 리스트를 포함하는 객체 리스트와 같은 복잡한 데이터 구조로 작업할 때는 특정 기준에 따라 특정 정보를 추출하는 것이 필수적입니다.

 

2. 문제 이해

User 클래스와 Order 클래스가 있는 간단한 예제로 작업해 보겠습니다 . User 클래스에는 이름과 Orders 목록이 있고 , Order 클래스에는 product와 price가 있습니다. 목표는 중첩된 주문에 적용된 일부 조건에 따라 사용자 목록을 필터링하는 것입니다.


데이터 모델의 구조는 다음과 같습니다.

class User {
    private String name;
    private List<Order> orders;
    
    public User(String name, List<Order> orders) {
        this.name = name;
        this.orders = orders;
    }

    // set get methods
}

class Order {
    private String product;
    private double price;
    
    public Order(String product, double price) {
        this.product = product;
        this.price = price;
    }

    // set get methods
}

 

각 사용자는 여러 개의 주문 객체를 가질 수 있으며, 주문 객체는 가격 등의 주문과 관련된 특정 기준에 따라 사용자를 필터링하는 데 필요한 세부 정보를 제공합니다.

필터링 논리를 보여주기 위해 먼저 샘플 테스트 데이터를 만들겠습니다. 아래 예에서 두 개의 Order 객체를 준비하고 이를 세 개의 User 객체와 연결합니다.

Order order1 = new Order("Laptop", 600.0);
Order order2 = new Order("Phone", 300.0);
Order order3 = new Order("Monitor", 510.0);
Order order4 = new Order("Monitor", 200.0);

User user1 = new User("Alice", Arrays.asList(order1, order4));
User user2 = new User("Bob", Arrays.asList(order3));
User user3 = new User("Mity", Arrays.asList(order2));

List users = Arrays.asList(user1, user2, user3);

 

$500보다 큰 가격으로 주문한 모든 사용자를 찾고 싶다고 가정해 보겠습니다. 이 경우 Alice 와 Bob이 모두 이 기준을 충족하는 주문을 했기 때문에 조건에 맞는 필터링(추출된) 데이터는 두 명으 ㅣ사용자를 반환할 것으로 예상합니다.

 

3. 전통적인 반복 접근 방식

Java 8이 Stream API를 도입하기 전에 목록을 필터링하는 일반적인 방법은 기존의 for 반복을 사용하는 것이었습니다. 같은 예를 들어 중첩된 for 반복을 사용하여 필터링을 구현해 보겠습니다.

double priceThreshold = 500.0;

List<User> filteredUsers = new ArrayList<>();
for (User user : users) {
    for (Order order : user.getOrders()) {
        if (order.getPrice() > priceThreshold) {
            filteredUsers.add(user);
            break;
        }
    }
}

assertEquals(2, filteredUsers.size());

 

이 방법은 각 User를 반복한 다음 해당 Orders 목록을 반복합니다. 주어진 조건과 일치하는 order를 찾으면 필터링된 목록에 user를 추가하고 break로 내부 반복을 종료합니다.

이 방법은 잘 작동하고 이해하기 쉽지만 중첩 반복의 수동 관리가 필요합니다. 또한 스트림이 제공하는 함수형 프로그래밍 스타일이 부족합니다.

 

4. Java Streams를 사용한 필터링

Java 8에서는 Streams를 사용하여 더 깔끔한 코드를 만들 수 있습니다. 이 방법을 사용하여 이전과 같은 문제를 해결해 보겠습니다. 가격이 500달러 이상인 주문을 한 사용자를 필터링하고 싶습니다. Java Streams를 사용하여 각 사용자의 주문 목록을 확인하여 조건을 충족하는 주문이 포함되어 있는지 확인할 수 있습니다.

double priceThreshold = 500.0;

List<User> filteredUsers = users.stream()
  .filter(user -> user.getOrders().stream()
    .anyMatch(order -> order.getPrice() > priceThreshold))
  .collect(Collectors.toList());

assertEquals(2, filteredUsers.size());

 

여기서는 Alice가 "Laptop"을 주문하고 500달러 이상을 지불한 유일한 사용자이므로 filteredUsers 목록에 user1만 포함될 것으로 예상합니다.

6. 사용자 정의 Predicate 사용

또 다른 방법은 필터링 로직을 커스텀(사용자 정의)한 Predicate에 캡슐화하는 것입니다. 이렇게 하면 더 읽기 쉽고 재사용 가능한 코드를 만들 수 있습니다. 사용자의 주문이 조건과 일치하는지 확인하는 Predicate를 정의해 보겠습니다.

Predicate<User> hasExpensiveOrder = user -> user.getOrders().stream()
  .anyMatch(order -> order.getPrice() > priceThreshold);

List<User> filteredUsers = users.stream()
  .filter(hasExpensiveOrder)
  .collect(Collectors.toList());

assertEquals(2, filteredUsers.size());

 

이 방법은 필터링 논리를 격리하여 가독성을 향상시키고, 필요한 경우 다른 필터링 작업에도 해당 Predicate를 재사용할 수 있습니다.

 

7. 구조를 유지하면서 필터링

모든 사용자를 목록에 유지하지만 특정 조건을 충족하는 주문만 포함하려는 시나리오를 고려해 보겠습니다. 유효한 주문이 없는 사용자를 제거하는 대신 해당 주문 목록을 수정하고 조건을 충족하지 않는 주문만 필터링합니다.

이 경우 나머지 사용자 객체 를 보존하면서 사용자의 주문 목록을 수정해야 합니다.

List<User> filteredUsersWithLimitedOrders = users.stream()
  .map(user -> {
    List<Order> filteredOrders = user.getOrders().stream()
      .filter(order -> order.getPrice() > priceThreshold)
      .collect(Collectors.toList());
    user.setOrders(filteredOrders);
    return user;
  })
  .filter(user -> !user.getOrders().isEmpty())
  .collect(Collectors.toList());

assertEquals(2, filteredUsersWithLimitedOrders.size());
assertEquals(1, filteredUsersWithLimitedOrders.get(0).getOrders().size());
assertEquals(1, filteredUsersWithLimitedOrders.get(1).getOrders().size());

 

여기서는 map()을 사용하여 각 User 객체를 수정합니다. 각 사용자에 대해 가격 조건을 충족하는 주문만 포함하도록 주문 목록을 필터링한 다음 필터링된 결과로 주문 목록을 업데이트합니다.

 

8. flatMap() 사용하기

중첩 스트림을 사용하는 대신, flatMap() 메서드를 활용하여 중첩 목록을 평면화하고 항목을 단일 스트림으로 처리할 수 있습니다. 이 접근 방식은 여러 stream() 호출을 피함으로써 중첩 목록에서 필터링을 간소화합니다 .


flatMap()을 사용하여 Order 목록을 기준으로 User 객체를 필터링하는 방법을 살펴보겠습니다.

List<User> filteredUsers = users.stream()
  .flatMap(user -> user.getOrders().stream()
    .filter(order -> order.getPrice() > priceThreshold)
    .map(order -> user)) 
  .distinct()
  .collect(Collectors.toList());

assertEquals(2, filteredUsers.size());

 

이 접근 방식에서 우리는 flatMap() 메서드를 사용하여 각 User를 연관된 Order 객체의 스트림으로 변환합니다. 그렇게 함으로써 모든 주문을 단일 통합 스트림으로 처리할 수 있습니다.

 

9. 엣지 케이스 처리

일부 사용자에게는 주문이 전혀 없는 경우가 있을 수 있습니다. 주문이 없는 사용자를 처리할 때 잠재적인 NullPointerException을 방지하려면 주문이 null이거나 비어 있지 않은지 확인하는 검사를 구현해야 합니다.

User user1 = new User("Alice", Arrays.asList(order1, order2));
User user2 = new User("Bob", Arrays.asList(order3))
User user3 = new User("Charlie", new ArrayList<>());
List users = Arrays.asList(user1, user2, user3);

List<User> filteredUsers = users.stream()
  .filter(user -> user.getOrders() != null && !user.getOrders().isEmpty()) 
  .filter(user -> user.getOrders().stream()
    .anyMatch(order -> order.getPrice() > priceThreshold))
  .collect(Collectors.toList());

assertEquals(2, filteredUsers.size());

 

이 예에서 우리는 추가 필터를 적용하기 전에 먼저 주문 목록이 null이 아니고 비어 있지 않은지 확인합니다. 이렇게 하면 코드가 더 안전해지고 런타임 오류가 방지됩니다.

 

10. 결론

Java에서 중첩된 목록을 기준으로 목록을 필터링하는 방법을 알아보았습니다. 기존 반복, Java Streams, 사용자 정의 Predicate, 에지 케이스를 처리하는 방법을 살펴보았습니다. Streams를 사용하면 더 깔끔하고 효율적인 코드를 작성할 수 있습니다.

 

 

 


 

 

 

 

작성한 내용중에 잘못 서술하였거나 궁금한 점은 댓글로 같이 공유해주세요.

 

감사합니다.

 

 

 

 

 

 

 

 

 

 

 

 

참조 : https://www.baeldung.com/java-list-filter-by-nested-lists

728x90
반응형