Java8


Lambda

자바 8은 많은 변화를 맞이했다.
특히 함수형 프로그래밍 지원을 위한 람다의 도입이 두드러진다.


람다가 도입된 이유

핫해진 용어 가운데 빅데이터가 있다.
프로그래머들에게 빅데이터를 프로그램적으로 다룰 수 있는 방법이 필요해졌다.
그래서 멀티 코어를 활용한 분산 처리 즉, 병렬화 기술이 필요해졌다.

하나의 CPU안에 다수의 코어를 삽입하는 멀티 코어 프로세서들이 등장하면서
프로그래머들에게 병렬화 프로그래밍에 대한 필요성이 생기기 시작했다.

자바 8에서는 병렬화를 위해 컬렉션(배열, List, Map, Set)을 강화했고,
이러한 컬렉션을 더 효율적으로 사용하기 위해 스트림(Stream)을 강화했다.
또 스트림을 효율적으로 사용하기 위해 함수형 프로그래밍이,
다시 함수형 프로그래밍을 위해 람다가, 람다를 위해 인터페이스의 변화가 수반됐다.

람다를 지원하기 위한 인터페이스를 함수형 인터페이스라고 한다.

(위 설명에서 ‘~를 위해 ~가 생겼다.’ 가 무조건 그렇다는 것은 아니다.)


Lambda 람다란 무엇인가?

코드 블럭을 변수처럼 사용할 수 있다는 것이다.
별도의 클래스 정의 없이 코드 블록인 메서드를 사용하고자 할 때 많이 사용되던 익명 객체를 사용하는 방법이다.
자바 8에서는 익명 객체조차 없이 바로 코드 블록만 사용하면 된다.

class myTest implements Runnable {        
    public void run() {
        System.out.println("Hello Lambda!");
    }       
}                

기존 방식으로 Runnable 인터페이스 구현체를 사용하는 코드이다.
main 메서드에서 MyTest 클래스 객체를 생성하여 run() 메서드를 호출한다.


기존 방식의 코드 블록을 사용해보자. - 익명 객체 생성

public static void main(String[] args) {
    Runnable r = new Runnable() {
        public void run() {
            System.out.println("Hello Lambda!");
        }   
    };
    
    r.run();
}

별도의 클래스 정의 없이 코드 블록인 메서드를 사용하고자 할 때 많이 사용되던 익명 객체를 사용하는 방법이다.
자바 8에서는 더 나은 방법을 사용할 수 있다.
즉, 익명 객체조차 없이 바로 코드 블록만 사용하면 된다.


새로운 방식의 코드 블록 사용 - lambda

Runnable r = () -> {   
    System.out.println("Hello Lambda!");    
}  

Runnable 타입으로 참조 변수 r을 만들고 있으니 new Runnable()은 컴파일러가 알아낼 수 있다.
굳이 코드로 작성할 필요가 없다.

마지막으로 화살표 기호 ->
이는 람다의 구조가 다음과 같기 때문에 추가되었다.

(인자목록) -> { 로직 }


함수형 인터페이스

추상 메서드를 하나만 갖는 인터페이스를 자바 8부터 함수형 인터페이스라고 한다.
함수형 인터페이만 람다식으로 변경할 수 있다.

main.java

MyFunctionalInterface mfi = (int a) -> { return a * a; };

MyFunctionalInterface.java

@FunctionalInterface  
interface MyFunctionalInterface { 
    public abstract int runSomething(int count);
}

@FunctionalInterface 어노테이션을 붙이는 것은 옵션이다.
이 어노테이션이 붙은 경우 컴파일러는
인터페이스가 함수형 인터페이스의 조건에 맞는지 검사한다.
즉, 단 하나의 추상 메서드만 갖고 있는지 확인한다.

가장 간단하게 구현하는 방법은 다음과 같다.

MyFunctionalInterface mfi = a -> a * a; 


메서드 호출 인자로 람다 사용

람다식을 변수에 저장하는 것이 가능하다면 당연히 메서드의 인자로도 사용할 수 있다.

람다식을 단 한번만 사용한다면 굳이 변수에 할당할 필요도 없이
인자로 넘겨주면 된다.

doIt(a -> a*a);


메서드 반환값으로 람다 사용

public static MyFunctionalInterface todo() {
    reutrn num -> num * num;
}


컬렉션 스트림에서 람다 사용

람다는 다양한 용도가 있지만 그 중에서도 컬렉션 스트림을 위한 기능에 크게 초점이 맞춰져 있다.
컬렉션 스트림과 람다를 통해 더 적은 코드로 더 안정적인 코드를 만들어 보자.

예제 : 컬렉션 스트림을 활용해서, age 배열에서 20세 미만인 사람들을 거르는 상황을 구현해보자.

Arrays.stream(ages)
      .filter(age -> age < 20)
      .forEach(age -> System.out.fomat("Age %d ! Can't enter\n", age));

예제에서 기존 for, if를 사용하는 것보다 좋아진 점은 How가 아닌 What을 지정했다는 것이다.

함수형 프로그래밍의 장점인 선언적 프로그래밍을 활용하는 것이다.
‘어떻게 하라’를 명령하는게 아니라 ‘무엇을 원한다’라고 선언하는 것과 같다.

또한 스트림은 메서드 체인 패턴을 이용해 최종 연산이 아닌 모든 중간 연산은 다시 스트림으로 반환해
코드를 간략하게 작성할 수 있게 지원한다.


메서드 레퍼런스와 생성자 레퍼런스

위 스트림, 람다 예제에서 아래 코드로도 구현할 수 있다.

Arrays.stream(ages)
      .sorted()
      .forEach(System.out:println);

이 코드를 람다식으로 표현하면 다음과 같다.

Arrays.stream(ages)
      .sorted()
      .forEach(age -> System.out.println(age));

위 람다식은 인자를 아무런 가공없이 그대로 출력한다.
이런 코드를 사용할 때 메서드 레퍼런스라고 하는 간략한 형식을 사용할 수 있다.
메서드 레퍼런스에는 다음과 같은 세 가지 유형이 있다.

마지막으로 메서드 레퍼런스와 유사한 생성자 레퍼런스가 있다.
클래스::new

예제를 통해 사용법을 알아보자.

B016 b = B016::new; // ERROR 
Supplier<B016> factory = B016::new; // OK   

첫 번째 코드에서 ERROR가 발생하는 이유는
생성자 레퍼런스로 생성한 것은 B016 클래스의 객체가 아니라
함수형 인터페이스 구현 객체이기 때문이다.

기본 생성자(인자가 없는 생성자)이기에
이를 만족하는 Supplier 함수형 인터페이스를 사용해 생성자 자체에 대한 참조가 만들어진다.


인터페이스의 디폴트 메서드와 정적 메서드

자바 8 이전에는 인터페이스가 가질 수 있는 멤버는 다음과 같다.

그런데 자바 8에서는 인터페이스에 큰 변화가 있었다.
자바 8에서 인터페이스가 가질 수 있는 멤버는 다음과 같다.

이제 인터페이스도 몸체를 가진 인스턴스 메서드를 가질 수 있게 됐다.
이를 디폴트 메서드라고 하고 default 키워드를 메서드 정의에 사용한다.

또한, (구체) 정적 메서드를 가질 수 있게 되었고 static 키워드를 메서드 정의에 사용하면 된다.


자바 8에서 언어적으로 가장 큰 변화를 꼽으라면 바로 인터페이스의 스펙 변화를 꼽을 수 있다.

이로 인해 람다가 가능해졌고, 연쇄적으로 더 강화된 컬렉션 API를 사용할 수 있게 됐을뿐만 아니라
함수형 프로그래밍이 가능해졌기 때문이다.

그럼 왜 인터페이스에 디폴트 메서드와 정적 메서드를 추가한걸까?
컬렉션 API를 강화하면서 컬렉션의 공통 조상인 Collection의 슈퍼 인터페이스인 스 Iterable 인터페이스에는 많은 변화가 필요했다.

한 예로 내부 반복을 가능하게 하는 forEach의 도입이 있다.
그런데 인터페이스에 변화를 주게 되면,
즉 새로운 추상 인스턴스 메서드를 추가하게 되면
기존에 해당 인터페이스를 구현한 모든 사용자 정의 클래스는 이를 추가적으로 구현해야만 한다.

이전 JDK를 기반으로 작성된 프로그램도 자바 8 JVM에서 구동될 수 있게
디폴트 메서드라고 하는 새로운 개념을 인터페이스 스펙에 추가한 것이다.


Stream

간결하게 컬렉션의 데이터를 처리하는 기능이다.

// Before      
List<Shape> list = new ArrayList<Shape>();           
for (Shape s : shapes) {
	if (s.getColor() == RED) {
		list.add(s);
	}
}
 
// After
shapes.stream().filter(s -> s.getColor() == Red).collect(toList());           


Stream API : 병렬 연산을 지원하는 스트림이라는 새로운 API


Parallel Stream

shapes.parallelStream().forEach(s -> doSomething());  


Optional

간단하게 아래와 같이 설명할 수 있다.

변수의 값이 null인지 아닌지 확인하는 것을 더 간결하게 구현하고자
자바 8에서는 Optional 인터페이스가 추가되었다.

JPA Repository의 findOne, findById 등 메소드의 리턴타입이
엔터티 클래스이어도 되고, Optional<엔터티클래스> 이어도 된다.

Student findOne(int id);
Optional<Student> findById(int id);

리턴 값이 Student 인 경우에, 해당 학생이 존재하지 않을 경우에, null 이 리턴된다.

리턴 값이 Optional인 경우에, 해당 학생이 존재하지 않을 경우에는, 내부 값이 비어있는 Optional 객체가 리턴된다. 내부 값이 비어 있는 경우에, Optional 객체의 ifPresent() 메소드는 false를 리턴한다. 내부 값이 들어있는 경우에, Optional 객체의 get() 메소드는 그 값을 리턴한다.

Optional - 오라클 공식 문서 보러가기


@RequestMapping("findOne/{id}")  
public Music findOne(@PathVariable("id") String id) {  
    Optional<Music> result=musicService.findById(id);       
    return result.orElse(new Music());      
}

만약 findById 메소드의 리턴 타입이 다음과 같았다면,
Music findById(Stirng id);

액션 메소드의 마지막 return 문은 다음과 같아야 한다.
return result == null ? new Misuc() : result;

위와 같이, null 인지 아닌지 검사하는 코드를 간결하게 구현하기 위해서,
Optional 을 사용한다.

JPA Repository

Student findById(int id);
Optional<Student> findById(int id); // 최신 버전

Student findOne(int id);
Optional<Student> findOne(int id); // 최신 버전

서비스나 컨트롤러에서

Student student = studentRepository.findOne(id); // 과거 코드

Optional<Student> result = studentRepository.findOne(id); // 최신 코드
Student student = result.orElse(new Student()); // 최신 코드 

model.addAttribute("student", studentRepository.findOne(id)); // 과거
model.addAttribute("student", studentRepository.findOne(id).orElse(new Student())); // 최신


// StudentService.java
public Student findById(int id) {
    return studnetRepository.findById(id).orElse(new Student());
}

// StudentController.java 
model.addAttribute("studnet", studentService.findById(id));