자바/자바 코딩의 기술

자바 코딩의 기술 - 8장 ( 데이터 흐름 )

끄적끄적 2022. 4. 10. 10:49

객체 지향 프로그래밍은 동작부를 캡슐화해 코드를 이해하기 쉽게 만든다. 함수형 프로그래밍은 동작부를 최소화해 코드를 이해하기 쉽게 만든다. - 마이클 페더스

 

8-1. 익명 클래스 대신 람다 사용하기

Map<Double, Double> values = new HashMap<>();

Double square(Double x) {
    Function<Double, Double> squareFunction =
            new Function<Double, Double>() {
                @Override
                public Double apply(Double value) {
                    return value * value;
                }
            };
    return values.computeIfAbsent(x, squareFunction);
}

위와 같이 익명클래스(squareFuction)을 만드는 것보다 람다 표현식이 낫다

Map<Double, Double> values = new HashMap<>();

Double square(Double value) {
    Function<Double, Double> squareFunction = factor -> factor * factor;
    return values.computeIfAbsent(value, squareFunction);
}

아래와 같이 멀티라인으로도 가능하다

Function<Double, Double> squareFunction = factor -> {
    return factor * factor;
};

8-2 .명령행 방식 대신 함수형

List<Supply> supplies = new ArrayList<>();

long countDifferentKinds() {
    List<String> names = new ArrayList<>();
    for (Supply supply : supplies) {
        if (supply.isUncontaminated()) {
            String name = supply.getName();
            if (!names.contains(name)) {
                names.add(name);
            }
        }
    }
    return names.size();
}

위 코드는 한줄씩 내려가면서 내용을 파악해야 하는데, 람다를 사용하면 훨씬 읽기 쉽다.

List<Supply> supplies = new ArrayList<>();

long countDifferentKinds() {
    return supplies.stream()
                   .filter(supply -> supply.isUncontaminated())
                   .map(supply -> supply.getName())
                   .distinct()
                   .count();
}

https://docs.oracle.com/javase/9/docs/api/java/util/stream/package-summary.html#StreamOps

8-3. 람다 대신 메서드 참조

List<Supply> supplies = new ArrayList<>();

long countDifferentKinds() {
    return supplies.stream()
                   .filter(supply -> !supply.isContaminated())
                   .map(supply -> supply.getName())
                   .distinct()
                   .count();
}

람다 표현식이 간단하면 관계없지만 로직이 복잡하면 오류 가능성이 있다. 스트림 내에서 메서드 참조를 사용할 수 있다.

List<Supply> supplies = new ArrayList<>();

long countDifferentKinds() {
    return supplies.stream()
                   .filter(Supply::isUncontaminated)
                   .map(Supply::getName)
                   .distinct()
                   .count();
}

8-4. Side effect 피하기

List<Supply> supplies = new ArrayList<>();

long countDifferentKinds() {
    List<String> names = new ArrayList<>();

    Consumer<String> addToNames = name -> names.add(name);

    supplies.stream()
            .filter(Supply::isUncontaminated)
            .map(Supply::getName)
            .distinct()
            .forEach(addToNames);
    return names.size();

스트림 내에서 forEach 사용은 병렬처리 등에서 문제가 발생할 수 있다. collect() 나 reduce()를 쓰도록 노력하라.

List<Supply> supplies = new ArrayList<>();

long countDifferentKinds() {
    List<String> names = supplies.stream()
                                 .filter(Supply::isUncontaminated)
                                 .map(Supply::getName)
                                 .distinct()
                                 .collect(Collectors.toList());
    return names.size();
}

8-5. 복잡한 스트림 종료시 컬렉트 사용하기

List<Supply> supplies = new ArrayList<>();

Map<String, Long> countDifferentKinds() {
    Map<String, Long> nameToCount = new HashMap<>();

    Consumer<String> addToNames = name -> {
        if (!nameToCount.containsKey(name)) {
            nameToCount.put(name, 0L);
        }
        nameToCount.put(name, nameToCount.get(name) + 1);
    };

    supplies.stream()
            .filter(Supply::isUncontaminated)
            .map(Supply::getName)
            .forEach(addToNames);
    return nameToCount;
}

조건을 만족하는 리스트 내용에 대해 이름과 이름별 개수를 추출하고자 하는 코드이다. 아래와 같이 하면 훨씬 가독성이 높아진다. groupingBy 외에도 partitioningBy(), mapBy(), joining(), mapping(), summingInt(), averagingLong(), reducing(), filtering(), flatMapping() 등 다양한 기능을 제공하고 있다.

List<Supply> supplies = new ArrayList<>();

Map<String, Long> countDifferentKinds() {
    return supplies.stream()
                   .filter(Supply::isUncontaminated)
                   .collect(Collectors.groupingBy(Supply::getName,
                           Collectors.counting())
                   );
}

8-6. 스트림 내 예외 피하기

static List<LogBook> getAll() throws IOException {
    return Files.walk(Paths.get("/var/log"))
                .filter(Files::isRegularFile)
                .filter(LogBook::isLogbook)
                .map(path -> {
                    try {
                        return new LogBook(path);
                    } catch (IOException e) {
                        throw new UncheckedIOException(e);
                    }
                })
                .collect(Collectors.toList());
}

스트림내에서는 IOException과 같이 checked exception을 쓸 수 없어서, UncheckedIOException으로 바꿔서 전달하고 있는데, 안전해 보이지 않는다.  map()대신 flatMap()을 이용해서 다른 타입의 Stream으로 매핑하여 Stream.of 가 수행되도록 하였다.

static List<LogBook> getAll() throws IOException {
    try (Stream<Path> stream = Files.walk(Paths.get("/var/log"))) {
        return stream.filter(Files::isRegularFile)
                     .filter(LogBook::isLogbook)
                     .flatMap(path -> {
                         try {
                             return Stream.of(new LogBook(path));
                         } catch (IOException e) {
                             return Stream.empty();
                         }
                     })
                     .collect(Collectors.toList());
    }
}

8-7. 널 대신 Optional

class Communicator {

    Connection connectionToEarth;

    void establishConnection() {
        // used to set connectionToEarth, but may be unreliable
    }

    Connection getConnectionToEarth() {
        return connectionToEarth;
    }
}

class Usage {
    static void main() {
        Communicator communicator = new Communicator();
        communicator.getConnectionToEarth()
                    .send("Houston, we got a problem!");
    }
}

getConnectionToEarch()가 null을 반환하면 NullPointException이 발생한다.

class Communicator {

    Connection connectionToEarth;

    void establishConnection() {
        // used to set connectionToEarth, but may be unreliable
    }

    Optional<Connection> getConnectionToEarth() {
        return Optional.ofNullable(connectionToEarth);
    }
}

위와 같이 Optional로 반환하면 호출할 때 connection이 없을 수도 있다는 내용을 인지하게 해준다. 아래와 같이 orElse를 호출하면 여전히 null이 반환되므로 ifPresend()를 통해 conneciton이 있을 경우에만 출력을 할 수 있다.

static void main() {
    Communicator communicator = new Communicator();

    Connection connection = communicator.getConnectionToEarth()
                                        .orElse(null);
    connection.send("Houston, we got a problem!");
}
static void main2() {
    Communicator communicationSystem = new Communicator();

    communicationSystem.getConnectionToEarth()
                        .ifPresent(connection ->
                            connection.send("Houston, we got a problem!")
                        );
}

8-8. 선택 필드나 매개변수 피하기

class Communicator {

    Optional<Connection> connectionToEarth;
    
    void setConnectionToEarth(Optional<Connection> connectionToEarth) {
        this.connectionToEarth = connectionToEarth;
    }
    Optional<Connection> getConnectionToEarth() {
        return connectionToEarth;
    }
}

위 코드는 Communicator 클래스내에 변수에도 Optional을 사용해서 존재/부재, 그리고 Optional이 null인 경우까지 3가지 경우가 발생한다. 지역 변수나 매개변수는 Optional을 쓰지않는게 낫다.

class Communicator {

    Connection connectionToEarth;

    void setConnectionToEarth(Connection connectionToEarth) {
        this.connectionToEarth = Objects.requireNonNull(connectionToEarth);
    }
    Optional<Connection> getConnectionToEarth() {
        return Optional.ofNullable(connectionToEarth);
    }

    void reset() {
        connectionToEarth = null;
    }
}

8-9. 옵셔널을 스트림으로 사용하기

void backupToEarth() {
    Optional<Connection> connectionOptional =
            communicator.getConnectionToEarth();
    if (!connectionOptional.isPresent()) {
        throw new IllegalStateException();
    }

    Connection connection = connectionOptional.get();
    if (!connection.isFree()) {
        throw new IllegalStateException();
    }

    connection.send(storage.getBackup());
}

위 코드는 아래와 같이 향상가능하다. filter()를 사용해서 연결이 이어져있고, 사용할 수 있는지 확인하고 그렇지 않으면 IllegalStateException을 발생시킨다.

void backupToEarth() {
    Connection connection = communicator.getConnectionToEarth()
            .filter(Connection::isFree)
            .orElseThrow(IllegalStateException::new);
    connection.send(storage.getBackup());
}

 

 

반응형