카테고리 없음

15 - 9. Java - Functor, Monad, Optional, Stream

lukeit 2024. 11. 14. 09:08

Functor

Mapping 함수 (값 → 값) 적용 방식을 가지는 “데이터 구조”이다

  • Functor의 예시로는 List 가 있다
  • “값의 데이터 구조에서 → 새 값의 데이터 구조가 나오는것”을 Functor라고 한다
  • 예시
    • const new_array = array.map((each) => [each.name](http://each.name))
      • 여기서 ((each) => each.name) 이 Functor 부분이다

Monad

Mapping 함수 (값 → 값 + 상태) 적용 방식을 가지는 “데이터 구조”이다

  • Monad의 예시로는 Optional 이 있다
  • “값의 데이터 구조에서 → 오류 상태를 포함한 값의 데이터 구조가 나오는것”
  • Promise와 비슷하지만 다른놈이다!
  • Monad의 장점
    • NPE (Null Pointer Exception) 방지
      • 실패 상태를 가진 “값” 이기에 어떤 함수에서도 문제를 발생시키지 않는다
    • 지연 연산 (Lazy Evaluation)
      • 중간에 Exception이 발생해도, 실패상태를 가지고 이어져 나중에 처리한다

Optional

상태(값이 없는 / 값이 있는)를 가진 객체

내부에서 Null 처리를 하기 위함

  • Optional의 등장 이유: Null을 외부가 아닌 내부에서 처리하자는 마인드이다

    • 외부에서 Null을 처리하게 된다면
      • NPE 발생에 대한 처리도 외부에 위치하게 되어 유지보수성과 가독성이 떨어진다
  • 상태를 갖는 객체로서, 값이 없을 수도, 있을 수 도 있다

  • 변수 선언: Optional<Member> : Generic을 통해 어떤 타입의 값을 가질 수 있는지 명시가 가능하다

  • 객체 생성: Optional.ofNullable() : 값이 없을 수도 있는 = 값이 없는 / 값을 가진 Optional이다

    • Optional.empty() : 값이 없는 Optional이다

      Optional<Member> optionalMember = Optional.of(member);          // = 성공(값이 있다) 상태 + Member 객체
      Optional<Member> optionalMember = Optional.empty();             // = 실패(값이 없다) 상태 + Null 객체
      Optional<Member> optionalMember = Optional.ofNullable(member);  // = 값이 있을수도, 없을수도 상태 + 객체
  • 내부에서 Null을 처리했을때의 장점:

    • 외부에서 Null을 직접 다루지 않아도 된다
      • 특정 값이 Null인지 여부 판단을 할 수 있다
      • 특정 값이 Null인 경우 별개 처리가 가능하다
    • 명시적으로 Null인지 여부를 코드로서 표기 할 수 있다
      • ex.) Optional.ofNullable()
  • 아직까지 이해가 되지 않는다면 아래 예시를 보고 이해해보자

    • ex.) ID 기반 Member 데이터 조회 메서드 만들기

      • public User findById(Integer id)
        • id가 Member에 존재하지 않을 시 어떻게 처리할것인가?
          • Objects.isNull(user) 일 경우,
            • throw new RuntimeException("유저가 없습니다")
          • Optional<User> optional = Optional.ofNullable(users.get(id))
            • optional.orElseThrow(() → new RuntimeException(”유저가 없습니다)
              • exception을 발생시킨다
            • optional.orElseGet(() -> new User(0, ""))
              • 기본값을 가져오는 함수
            • optional.orElse(new User(0, “”))
              • 기본값을 가져온다
    • ex.) Optional을 정상적인 방법으로 사용하는 사례:

      • Retrieves the text from getText() and calculates the length of it, if it is not null , if it is null, return 0 .

        int length = Optional.ofNullable(getText()).map((string) -> string.length()).orElse(0);
        
        // String::length로 치환이 가능하다
        int length = Optional.ofNullable(getText()).map(String::length).orElse(0);
    • ex.) .map() 을 활용해 Optional 안에 있는 값을 바꿀 수 있다

        public String getCityOfMemberFromOrder(Order order) {
                return Optional.ofNullable(order)
                **.map((order) -> order.getMember())
                .map((member) -> member.getAddress())
                .map((address) -> address.getCity())**
                        .orElse("Seoul");
        }
      
        // -> 이런식으로 바꿀 수 있다는거 알지?
                return Optional.ofNullable(order)
                        **.map(Order::getMember)
                        .map(Member::getAddress)
                        .map(Address::getCity)**
                        .orElse("Seoul");
      
        // 만약 Optional을 사용하지 않았다면,
        public String getCityOfMemberFromOrder(Order order) {
                if (Objects.nonNull(order)) {
                    Member member = order.member;
                    if (Objects.nonNull(member)) {
                        Address address = member.address;
                        if (Objects.nonNull(address)) {
                            return address.city;
                        } else {
                            return "Seoul";
                        }
                    }
                }
        }
        // 이런식으로 꽤나 길어진다!
      • .map() chaining을 사용해 Optional 객체를 3번 변환했다
        • 3번이나 변환을 마친결과에 Optional이 비어있다면, Failover 값으로 "Seoul" 로 처리하도록 한다
  • ex.) .filter() 를 통해 Optional 안의 값을 검사/분류가 가능하다

      public Optional<Member> getMemberIfOrderWithin(Order order, int min) {
          return Optional.ofNullable(order)
                  .filter(o -> o.getDate().getTime() > System.currentTimeMillis() - min * 1000)
                  .map(Order::getMember);
      }
  • ex.) Null 상황 처리

      Map<Integer, String> cities = new HashMap<>();
      cities.put(1, "Seoul");
      cities.put(2, "Busan");
      cities.put(3, "Daejeon");
    
      Optional<String> maybeCity = Optional.ofNullable(cities.get(4)); // Optional
      int length = maybeCity.map(String::length).orElse(0);            // null-safe
      System.out.println("length: " + length);
    
      // Optional을 사용하지 않았을때, Null이 외부에서 처리된다
      String city = cities.get(4);                                     // returns null
      int length = city == null ? 0 : city.length();                   // null check
      System.out.println("length: " + length);
  • ex.) 예외 처리

      List<String> cities = Arrays.asList("Seoul", "Busan", "Daejeon");
    
      public static <T> Optional<T> getAsOptional(List<T> list, int index) {
              try {
                      return Optional.of(list.get(index));
              } catch (ArrayIndexOutOfBoundsException e) {
                      return Optional.empty();
              }
      }
    
      Optional<String> maybeCity = getAsOptional(cities, 3);           // Optional
      int length = maybeCity.map(String::length).orElse(0);            // null-safe
      System.out.println("length: " + length);
    • Exception 처리와 메인 로직을 분리하였으며, Failover처리까지 같이해줘서 좋은 코드라고 볼 수 있다
  • ex.) .ifPresent() : Optional 안에 있는 값이 존재할 경우 함수를 실행한다

    • ifPresent(Cosnumser <? super T> consumer) : Consumer 함수형 인자를 받을 수 있다

      • .isPresent() : Optional 안에 있는 값이 존재하는지 여부만 판단한다

        Optional<String> maybeCity = getAsOptional(cities, 3); // Optional
        maybeCity.ifPresent(city -> {
              System.out.println("length: " + city.length());
        });

Stream

Collection 내 요소에 대한 내부 접근을 위함

  • 등장 이유

    • Collection 각각 요소에 대한 연산을 위해 for, while 로 외부에서 처리하기보단 내부에서 처리하기 위함
      • Collection 밖에서 Element 요소 처리: Loop를 통해 외부에서 처리 (External Iteration)
      • Collection 안에서 Element 요소 처리: Stream을 통해 내부에서 처리 (Internal Iteration)
  • 비교해보기

      // 외부 반복:
      for (String string : list) {
          if(string.contains("a")) {
              return true;
          }
      }
    
      // 내부 반복:
      boolean isExist = list.stream().anyMatch(element -> element.contains("a"));
  • 변수 선언: Stream: Generic 을 통해 어떤 타입의 요소를 가질 수 있는지 명시해줘야한다

  • 객체 생성: 3가지 방법이 있다

    1. Array로부터 Stream을 생성하는 방법

       String[] arr = new String[]{"a","b","c"};
       Stream<String> stream = Arrays.stream(arr);
    2. List로부터 Stream을 생성하는 방법

       List<String> lists = Arrays.asList("a", "b", "c");
       Stream<String> stream = lists.stream();
    3. Stream을 직접 생성하는 방법

       Strean<String> stream = Stream.of("a", "b", "c");
  • 장점:

    • 선언형이다
      • 더 간결하고 가독성이 좋다
    • 조립할 수 있다
      • 유연성이 좋다
    • 병렬화
      • 성능이 좋다
  • 예시

      List<Member> members = Arrays.asList(
              new Member(1L, "Aaron"),
              new Member(2L, "Baron"),
              new Member(3L, "Caron")
      );
    
      // Stream을 사용해 ID만 뽑아와보는 예시
      List<Long> ids = members.stream()
              .map((each) -> each.id)
              .collect(Collectors.toList());
    • 이런식으로 원하는 결과만 뽑아올 수 있다
반응형