본문 바로가기
야미스터디/Java

[Java - Stream] 04. mapping

by 의정부핵꿀밤 2022. 12. 9.
728x90

[Reference]

http://www.yes24.com/Product/Goods/17252419

 

자바 8 인 액션 - YES24

자바 8의 새로운 기능은 자바 1.0이 나온 이후 18년을 통틀어 가장 큰 변화다. 기존의 자바 코드 모두 그대로 사용하면서도 새로운 기능과 새로운 문법과 새로운 디자인 패턴으로 더 명확하고, 간

www.yes24.com


Intro

수많은 데이터(Data) 중에 특정 조건에 해당되는 정보(Information)를 선별하는 작업은 데이터 처리 방식에서도 자주 수행되는 연산 중 하나이다.

데이터 소스에서 특정 조건의 데이터 리스트를 추출해야 하는 경우 또는 필드를 가공해야 하는 경우가 필요할 수 있다.

 

예를 들어, 사용자의 요청을 받아서 처리하는 부분에서는 유효성 검사를 통해 검증된 값들만 처리할 수 있도록 필터링하는 처리를 하거나, 비즈니스 로직을 처리하기 위해서 필요한 여러 값들을 하나의 필드로 만드는 방식의 작업 등 다양한 방식의 데이터 변환이 필요할 수 있다.

 

코드를 설계하는 방법에 따라 처리하는 방식이 천차만별로 다양해질 수 있으나, 여기서는 어떠한 요청이 들어오면 스트림으로 데이터를 필터링하고 변환하는 파이프라인에서 데이터가 처리되는 과정에 대해서 알아보도록 한다.

 

 

 

 

매핑(Mapping)

스트림(Stream)은 Function 인터페이스를 인수로 받는 map 메서드를 제공하여, 인수로 제공된 Function 인터페이스의 로직이 각 요소에 적용되어 새로운 요소로 매핑된다.

여기서 "새로운 요소로 매핑된다"의 의미는 새로운 버전의 데이터를 만드는 변환(transforming)하는 작업으로 정의할 수 있다.

 

Stream API의 map 메서드가 데이터를 변환하는 방식과 같은 결과라도 다양한 방법으로 표현하는 방법을 살펴보도록 한다.

 

1) 데이터를 필요한 값으로 변환하여 반환

원천 데이터(List<Chicken>)에서 필요한 값만 추출하여 브랜드 목록을 한 줄로 출력하는 기능이 필요하다는 요구사항이 있다고 가정하고 구현하였다.

public class ChickenMapping {  
   private final List<Chicken> chickens;  
   public ChickenMapping(List<Chicken> chickens) {  
      this.chickens = chickens;  
   }  
   public String convertToChickenBrand() {  
      return chickens.stream()  // datasource
         .map(chicken -> chicken.getBrand().name()) // mapping
         .distinct()
         .collect(Collectors.joining(","));
   }  
}

 

위 코드에서 convertToChickenBrand라는 메서드 내에 구현된 Stream 구조를 분석해보면 다음과 같다.

위 Stream의 처리 순서대로 어떻게 데이터 처리를 하고 있는지 확인해보자.

  1. List<Chicken> 의 각 요소의 Brand의 name을 추출하여 Stream을 생성
  2. 새로 생성된 Stream<String> 내에 중복을 제거하기 위해 distinct() 라는 중간 연산을 연결
  3. 결과를 반환하기 위해 collect 라는 최종 연산에 Collectors 클래스의 정적 메서드 팩토리인 joining를 사용하여 최종 결과인 String을 생성

 

리스트에 등록된 치킨의 브랜드를 쉼표(,)로 구분하여 문자열을 만드는 테스트를 작성하여 결과를 확인해본다.

class ChickenMappingTest {  
   @DisplayName("브랜드를 문자열로 변환하는 테스트")  
   @Test  
   void testCase1() {  
      List<Chicken> originChickenList = List.of(  
         new Chicken(KFC, 10_000, "맛있는 치킨"),  
         new Chicken(KFC, 12_000, "더 맛있는 치킨"),  
         new Chicken(GCOVA, 9_000, "X코바 치킨"),  
         new Chicken(BBQ, 15_000, "BBC 치킨")  
      );  
      ChickenMapping chickenMapping = new ChickenMapping(originChickenList);  
      String convertToChickenBrand = chickenMapping.convertToChickenBrand();  
      assertAll(  
         () -> assertThat(convertToChickenBrand).isNotEqualTo("KFC,GCOVA"),  
         () -> assertThat(convertToChickenBrand).isEqualTo("KFC,GCOVA,BBQ")  
      );  
   }  
 }

매핑 API 들여보기

IDE 도구를 사용하면 결과를 쉽게 출력할 수 있으나, 어떠한 과정에서 처리되는지 고민해봐야 한다.

map을 사용하는 선에서 더 나아가 Stream의 map 메서드의 메서드 시그니처를 자세히 확인해보자.

public class ChickenMapping {  
   private final List<Chicken> chickens;  
   public ChickenMapping(List<Chicken> chickens) {  
      this.chickens = chickens;  
   }  
   public String convertToChickenBrand() {  
      return chickens.stream()  
         .map(?) // 람다로 작성된 코드가 궁금
         .distinct()  
         .collect(Collectors.joining(","));  
   }  
}

 

 

일단 Stream 클래스 내에 map의 시그니처를 보면 parameter로 Function이라는 함수형 인터페이스를 받아서 Return Type으로 Stream 인터페이스를 반환하는 추상메서드로 정의되어 있다.

map 시그니처

대표적인 함수형 인터페이스 중 하나인 Function은 SAM(Single Abstract Method) 인터페이스라고도 불리며, 그 의미는 하나의 추상 메서드를 가지고 있다는 뜻이다.

SAM의 정의를 만족하는 인터페이스만 익명 클래스, 람다 표현식으로 구현이 될 수 있다.

 

 

Function 인터페이스

Function 인터페이스에 접근하여 내용을 확인해보자.

추상 메서드(apply)의 시그니처를 보면 T라는 제네릭 타입을 파라미터로 받고, return type을 R이라는 제네릭 타입으로 반환한다.

 

람다로 작성되어 있던 코드를 익명 클래스 표현하여 어떻게 동작하는지 유추해보자.

여기서 익명 클래스로 작성하는 목적은 내부 메서드의 요청과 응답에 대한 구현부를 확인하기 위함이다.

 

익명 클래스는 재사용이 불가능하고, 객체지향의 특징인 캡슐화를 하지 못한다는 단점이 있으나, 단일 기능에 대한 정의가 가능하다는 점과 구현이 한 번만 사용되어 재사용이 불필요한 경우 클래스 선언 비용을 줄일 수 있다는 장점이 있다.

 

 

익명 클래스로 장황하게 작성된 코드의 가독성을 높이기 위해서는 처음으로 돌아가 다음과 같이 람다 또는 메서드 레퍼런스로 작성할 수 있다.

public class ChickenMapping {  
   private final List<Chicken> chickens;  
   public ChickenMapping(List<Chicken> chickens) {  
      this.chickens = chickens;  
   }  
   public String convertToChickenBrand() {  
      return chickens.stream()  
         .map(new Function<Chicken, String>() {  
            @Override  
            public String apply(Chicken chicken) {  
               return chicken.getBrand().getName();  
            }  
         })  
         .distinct()  
         .collect(Collectors.joining(","));  
   }  
}

 

  1. List<Chicken> 의 각 요소(Chicken)의 Brand(enum)을 추출하여 Stream<Brand> 생성
  2. Brand(enum)의 name(String)을 추출하여 Stream<String> 생성
  3. 새로 생성된 Stream<String> 내에 중복을 제거하기 위해 distinct()라는 중간 연산을 연결
  4. 결과를 반환하기 위해 collect라는 최종 연산에 Collectors 클래스의 정적 메서드 팩토리인 joining을 사용하여 최종 결과인 String을 생성

 

구현하는 방식은 하기 나름이기 때문에 다양하게 구성해보고, 현 상황에 맞는 방식은 무엇인지 구분할 수 있도록 해야 한다.

 


정리

  • map, flatMap 메서드로 스트림의 요소를 추출하거나 변환할 수 있다.
  • 함수형 인터페이스는 SAM 인터페이스라 부르기도 한다.
  • 익명 클래스의 장단점

 

💡 고민해 볼 거리
- 익명 클래스를 람다로 리팩토링해야 하는 이유?
- 익명 클래스와 람다의 중요한 차이점

(위의 내용은 공부 후 블로그 링크 첨부하도록 하겠음!)

https://yummy0102.tistory.com/661

 

[Java] 익명 클래스 vs 람다식

익명 클래스 Inner class, 이름이 없는 클래스를 말한다 클래스 정의와 동시에 객체를 생성할 수 있다 Java의 Interface와 Class 모두 익명 함수로 객체를 만들 수 있다 익명 함수를 사용하는 이유 프로그

yummy0102.tistory.com

옛다!ㅋㅋㅋ

728x90

댓글