Java 8의 특징

동동주·2024년 4월 30일

CS스터디_자바

목록 보기
13/19

주요 변경사항

Java 8의 주요 변경 사항은 아래와 같다.

  1. Lambda expression(람다 표현식)
  2. Functional interface(함수형 인터페이스)
  3. Default method(디폴트 메서드)
  4. Stream(스트림)
  5. Optional(옵셔널)
  6. 새롭게 추가된 날짜 API
  7. CompletableFuture(컴플리터블 퓨처)
  8. JVM의 변화

1. Lambda expression(람다 표현식)

  • 메서드로 전달할 수 있는 Anonymous function(익명 함수)를 단순한 문법으로 표기한 것을 람다 표현식이라고 한다. (Thread 객체를 생성하여 동작시키는 코드이다.)
// 익명 클래스로 Runnable을 구현
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Start to new thread!");
    }
});

thread.start();


// 람다 표현식으로 단순하게 표현
Thread thread = new Thread(() -> System.out.println("Start to new thread!"));
        
thread.start();
  • 위 예제에서는 '()'가 람다 파라미터, '->'가 화살표, 'System.out.println("Start to new thread!")'가 람다 바디에 해당한다.
    -> 어떤 값이 들어가면(람다 파라미터) 결과물이 나온다.(람다 바디)

1) 하나의 파라미터를 갖고 리턴 타입이 없는 람다 표현식

(String str) -> System.out.println("parameter is " + str);
  • 람다의 파라미터의 타입은 생략이 가능하며, 파라미터가 한 개인 경우에는 괄호를 생략할 수 있다.
// 타입을 생략한 코드
(str) -> System.out.println("parameter is " + str);

// 람다 파라미터 괄호를 생략한 코드
str -> System.out.println("parameter is " + str);

2) 하나의 파라미터를 갖고 리턴 타입이 있는 람다 표현식

// 람다 바디를 하나의 라인으로 표현
(i) -> i + 10;

// 람다 바디를 여러 라인으로 표현
(i) -> {
    int result = i + 10;
    return result;
};
  • 람다 바디가 짧아서 한 줄로 끝난다면 중괄호와 return 키워드를 생략할 수 있다. 두 줄 이상이 된다면 명시적으로 중괄호와 return 키워드를 선언해야 한다.

3) 파라미터가 없고 리턴 타입이있는 람다 표현식

()-> Integer.MAX_VALUE;
  • Integer.MAX_VALUE를 반환하는 람다 표현식이다. 파라미터가 없는 경우에는 빈 괄호를 명시적으로 표시해야한다.

2. Functional interface(함수형 인터페이스)

  • 단 하나의 추상 메서드를 갖는 인터페이스를 함수형 인터페이스라고 한다. 앞서 예제로 사용했던 Runnable 인터페이스는 추상 메서드 run() 하나만 있기 때문에 함수형 인터페이스라고 할 수 있다.
  • Java 8부터는 함수형 인터페이스를 컴파일 시점에도 확인할 수 있도록 @FunctionalInterface 애노테이션도 제공한다.

함수형 인터페이스 만들기

@FunctionalInterface
interface Calculation {
  Integer apply(Integer x, Integer y);
}

Calculation addition = (x, y) -> x + y;
Calculation subtraction = (x, y) -> x - y;

3. Default method(디폴트 메서드)

  • default 키워드를 사용하여 간단하게 구현할 수 있다는 장점이 있다.
public interface SampleInterface {
    // abstract method
    String returnHello(String msg);

    // default method
    default void hello(String msg) {
        System.out.println("hello " + msg);
    }
}
  • 인터페이스 내에서도 구현이 가능하며 static 메소드도 추가로 구현이 가능하다.

4. Stream(스트림)

  • 스트림은 Collection(컬렉션)을 멋지고 편리하게 처리하는 방법을 제공하는 API이다. 별다른 노력 없이 병렬 처리도 제공해주며, 마치 데이터베이스 쿼리를 작성하듯 직관적인 코드를 제공해준다.

  • 요구사항을 통해 스트림의 기능을 알아보자.
    <요구사항>
    "책들 중 니체가 작성한 책의 ISBN 정보가 필요합니다. 정렬은 책 이름을 기준으로 해주세요."

1) 스트림을 사용하지 않은 코드

books.sort(Comparator.comparing(Book::getName));

List<String> booksWrittenByNietzsche = new ArrayList<>();
for (Book book : books) {
    if (book.getAuthor().equals("Friedrich Nietzsche")) {
        booksWrittenByNietzsche.add(book.getIsbn());
    }
}
  • 어떤 요구사항을 구현한 코드인지 한눈에 알아보기 힘들다.

2) 스트림을 사용한 코드

List<String> booksWrittenByNietzsche = 
            books.stream()
                .filter(book -> book.getAuthor().equals("Friedrich Nietzsche"))
                .sorted(Comparator.comparing(Book::getName))
                .map(Book::getIsbn)
                .collect(Collectors.toList());
  • for문과 if문이 없어졌기 때문에 코드만 봐도 어떠한 요구사항을 구현했는지 알 수 있다.

3) 스트림의 특징

  • 파이프라이닝을 지원한다.
    • filter(), sorted(), map(), collect()등이 이어진다. 스트림 객체끼리 연속으로 처리하면서 하나의 파이프라인이 되어 최종적인 결괏값을 반환하게 된다.
  • 내부 반복을 지원한다.
    • for문을 사용하지 않고 filter()처럼 내부에서 알아서 처리한다.
  • 딱 한 번만 탐색한다.
    • 연산 이전의 값을 저장하지 않고 연산된 값만 새롭게 반환한다는 뜻이다.
  • 게으르게 작동한다.
    • 게으르게 작동한다는 것은 필요한 시점까지 실행을 미루다 특정 시점이 되면 동작하는 기법을 말한다.
  • 중간 연산과 종료 연산으로 구분한다.
    • Stream 형태의 스트림이 반환된다면 중간 연산이고, 그렇지 않으면 종료 연산이다. " 게으르게 동작한다" 의 예제가 동작하지 않는 이유는 filter()나 sorted()는 모두 중간 연산이기 때문이다. 따라서 foreach(), count(), collect() 등과 같은 종료 연산이 없어 실행되지 않는 것이다.

5. Optional(옵셔널)

  • Optional은 Java가 가지고 있는 null의 문제점을 보완하고자 등장하였다. 기본 콘셉트는 Integer나 Double과 같은 Wrapper class로서 객체를 바로 호출하지 않고 Optional 안에서 호출함으로써 null이 발생할 가능성을 봉쇄시킨다.
  • 옵셔널도 요구사항을 통해 알아보자.
    <요구사항>
    "책 정보를 담는 Book 객체에는 저자의 정보를 담는 Author 객체를 필드 값으로 갖는다. 이를 바탕으로 Book 객체에서 저자의 이름을 반환하는 메서드를 만들어 주세요"

전통적인 null 처리

public class BookService {
    // 파라미터로 Book 객체를 받는다.
    // Book 객체가 가진 Author객체의 getName()으로 저자의 이름을 반환한다.
    public String getAuthorName(Book book) {
        if (book == null) {
            throw new NullPointerException("This book is null");
        }
        
        Author author = book.getAuthor();
        return author.getName();
    }
}
  • 위 코드에서는 getAuthorName()의 결괏값이 null일 가능성이 있고 그 결괏값이 null인지 아닌지 신뢰할 수 없기 때문에 NullPointerException을 방어하는 코드를 추가해야한다. 따라서 소스코드가 어떤 기능을 수행하는지 분석할 때 많은 시간이 필요하다.
  • 이 문제점을 Optional를 통해 해결해보자.

Optional을 활용한 null 처리

public class BookService {
    // Optional을 사용하여 null에서 안전한 코드 작성하기
    public Optional<String> getAuthorName(Book book) {
        return Optional.ofNullable(book)
                .map(bookObject -> bookObject.getAuthor())
                .map(authorObject -> authorObject.getName());
    }
}
  • Optional를 통한 해결방법
    1) null을 방어하는 코드를 추가해야 한다
    -> Optional을 사용하여 null이 발생할 여지를 제거했다. 파라미터의 Book 객체가 null이어도 메서드 내부에서는 NPE가 발생하지 않고 비즈니스 로직 그대로 흘러갈 수 있다.
    2) getAuthorName()을 호출하는 곳에서 결괏값으로 null을 받을 수 있다.
    -> 근본적인 문제는 해결되지 않았다. 다만 리턴 타입을 Optional으로 했기 때문에 3) 메서드를 사용하는 개발자는 '빈 결괏값이 반환될 수 있다'라고 인지할 수 있다. 만약 null이 반환되지 않는 것이 확실한 경우에는 리턴 타입을 String으로 표기하여 호출하는 입장에서 별도의 null 방어 코드를 넣지 않아도 되게 할 수 있다.
    비즈니스 코드와 null 방어 코드가 뒤섞여 분석하는데 오랜 시간이 걸린다.
    -> null의 방어 코드가 완전히 사라져 getAuthorName()이 어떤 로직을 수행하는지 한눈에 파악할 수 있다.

6. 새롭게 추가된 날짜 API

  • 크게 기계용 날짜 API와 사람용 날짜 API로 나눌 수 있다. 먼저 기계용으로 Instant 클래스가 있고, 사람용으로 LocalDate, LocalTime, LocalDateTime이 있다

기계용 날짜 API :: Instant

  • Instant 클래스는 Unix epoch time(유닉스 에포크 시간)을 기준으로 특정 시점까지의 시간을 초로 표현한다. 현재 시간을 유닉스 에포크 시간을 기준으로 초(Second)로 표현한다면 다음과 같다. 참고로 유닉스 에포크 시간은 흔히 '타임스탬프'로 표현되는 시간이다.
// 현재 시간을 유닉스 에포크 시간으로 나타내는 코드 (모든 시간을 초로 환산하여 표시)
System.out.println(Instant.now().getEpochSecond());

// 유닉스 에포크 시간의 기준시를 나타내는 코드(기준시기 때문에 초로 환산해도 0으로 출력된다)
System.out.println(Instant.ofEpochSecond(0).getEpochSecond());
  • 모든 시간은 초를 기준으로 계산되기 때문에 사람에게 익숙한 연, 월, 달 혹은 시와 분의 개념이 없다는 점을 참고하고 사용해야 한다.

사람용 날짜 API :: LocalDate, LocalTime, LocalDateTime

  • 기존의 Date 클래스는 이름과 다르게 날짜, 요일은 물론 시간까지 포괄하는 정보를 갖는다. 이름이랑 맞지 않게 너무 많은 역할을 하게 되는 문제가 있다. 따라서 Java 8에서 날짜, 시간 별로 클래스를 분리시켰다. 이들은 기본적으로 다루는 정보가 다른 것 외에는 모두 동일한 사용법을 가지고 있어서 시간과 날짜를 모두 포함하는 LocalDateTime을 샘플로 코드를 작성했다.

  • 날짜&시간을 String으로 표현하는 코드다.

// 현재 날짜와 시간을 String 값으로 표현
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));

// 2021-01-01T12:00:00 을 String 값으로 표현
System.out.println(LocalDateTime.of(2021, 1, 1, 12, 0).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
  • 실무에는 꼭 날짜를 계산하는 로직이 들어가 있다. 간단히 오늘 날짜에 10일을 더한 날짜를 계산하는 코드를 Date 객체와 비교하여 보자.
// LocalDateTime 객체를 사용한 코드
LocalDateTime plusTenDay = LocalDateTime.now().plusDays(10L);

// Date 객체를 사용한 코드
Date today = new Date();
today.setDate(today.getDate() + 10);

7. CompletableFuture(컴플리터블 퓨처)

CompletableFuture를 사용한 조회

  • Java 8에서 새롭게 등장한 Future 인터페이스의 구현체다. 먼저 예외처리를 지원하는 메서드와 순서의 의존관계를 맞는 스레드 프로그래밍, 콜백을 지원하기도 하며 여러 스레드를 하나로 묶어 처리하기에도 용이하다. 먼저 요구사항을 CompletableFutrure를 사용하여 개발하면 다음과 같다.
CompletableFuture<Integer> lowPriceSearchFuture = CompletableFuture.supplyAsync(() -> {
    int bookPrice = Enterpark.getBookPrice();
    System.out.println("Enterpark : " + bookPrice);
    return bookPrice;
})
.thenCombine(CompletableFuture.supplyAsync(() -> {
    int bookPrice = Illidan.getBookPrice();
    System.out.println("Illidan : " + bookPrice);
    return bookPrice;
}), (enterpartBookPrice, illidanBookPrice) -> printPriceAndBookstore(enterpartBookPrice, illidanBookPrice))
.exceptionally(throwable -> {
    System.out.println(throwable.getMessage());
    return -1;
});

lowPriceSearchFuture.get(3, TimeUnit.SECONDS);
  • thenCombine()을 통해 서로 다른 스레드를 엮어 비동기로 진행하고 두 스레드가 정상적으로 처리되면 printPriceAndBookstore()를 호출하도록 했다. 또한 exceptionally()을 통해 예외상황도 처리했다. 만약 서점들 API 호출에 순서가 필요하다면 thenCombine() 대신 thenCompose()를 사용할 수 있다.
  • 또 모든 스레드의 처리가 정상 종료되면 콜백으로 Database에 log를 기록해야 한다면 아래와 같은 메서드를 추가하여 간단하게 구현할 수 있다

8. JVM의 변화

  • JVM에도 변화가 있었다. 기존의 PermGen 영역이 사라지고 대신 Metaspace 영역이 새롭게 생겨났다. 실무적인 관점에서 보면 Java 8부터는 메모리 옵션 중 -XX:PermSize나 -XX:MaxPermSize가 사라지고 -XX:MetaspaceSize, -XX:MaxMetaspaceSize 옵션이 새롭게 등장한 것이다. 따라서 Java 8을 사용하면서 PermGen의 메모리 옵션을 줘도 아무런 영향을 주지 못한다.

출처:
https://bbubbush.tistory.com/23
https://github.com/devSquad-study/2023-CS-Study/blob/main/java/java_eight_characteristic.md

0개의 댓글