[스프링 입문을 위한 자바 객체 지향의 원리와 이해] 부록B 내용정리
2022.04.14 - [IT 책 독서록] - [스프링 입문을 위한 자바 객체지향의 원리와 이해] 7장 내용정리
[스프링 입문을 위한 자바 객체지향의 원리와 이해] 7장 내용정리
2022.04.08 - [IT 책 독서록] - [스프링 입문을 위한 자바 객체 지향의 원리와 이해] 6장 내용정리 [스프링 입문을 위한 자바 객체 지향의 원리와 이해] 6장 내용정리 6장은 스프링 프레임워크에서 사용
blog.robinjoon.space
이번 장은 자바 8에서 추가된 람다와 함수형 프로그래밍에 대해 간략히 소개한다. 정말 간략히만 소개해서 사실 본격적인 람다식과 스트림, 메서드 레퍼런스에 대해 공부하기 위해서는 별도의 책이 필요하다. 그저 이런게 있으니 한번 찾아봐라~ 정도로 소개한다. 이 포스팅에선 이런 점을 고려하여 몇가지 개념들을 추가로 찾아서 같이 정리해 두었다.
자바 8 에서 변경된 것들
람다식의 컨셉과 기본 문법
람다식이 등장하기 이전에는 자바에선 어떤 로직을 구현한 코드블록은 반드시 메서드 내에 위치해야 했고, 그 메서드는 클래스 내부에 위치해얌나 했다. 따라서 코드 블록을 가지고 싶으면 메서드를 불러야했고, 이를 위해선 익명 객체를 만들어야 만 했다. 하지만 람다식의 도입으로 익명객체나 메서드 없이 코드 블록을 사용할 수 있게 되었다. 아래 Runnable 인터페이스를 사용하는 코드를 보면서 이해하자.
public class B001 {
public static void main(String[] args) {
MyTest mt = new MyTest();
Runnable r = mt;
r.run();
}
}
class MyTest implements Runnable {
public void run() {
System.out.println("Hello Rambda!!!");
}
}
전형적인 Runnable 인터페이스를 사용하기 위해 이의 구현 클래스를 작성한 코드다. 이 클래스가 한번만 사용된다면 익명객체를 사용할 수도 있다.
public class B002 {
public static void main(String[] args) {
Runnable r = new Runnable() {
public void run() {
System.out.println("Hello Rambda 2!!!");
}
};
r.run();
}
}
잘 생각해보면, r 이라는 변수는 Runnable 타입인게 명확하므로 new Runnable() 은 컴파일러가 추론할 수 있다. 또한, Runnable 인터페이스는 오직 하나의 추상 메서드만 가지므로 public void run() 도 굳이 명시할 이유가 없다. 따라서 람다식에서는 다음과 같이 이 모든 것을 생략한다.
public class B003 {
public static void main(String[] args) {
Runnable r = () -> {
System.out.println("Hello Rambda 3!!!");
};
r.run();
}
}
람다식에서는 if문이나 for문같은 제어문에서와 마찬가지로 내부에 코드가 한줄이면 { } 를 생략할 수 있다. 이 때, 세미콜론도 함께 생략한다.
public class B003 {
public static void main(String[] args) {
Runnable r = () -> System.out.println("Hello Rambda 3!!!");
r.run();
}
}
람다식을 사용할 수 있는 경우는 인터페이스가 오직 하나의 추상메서드만을 가져야 한다. 이런 인터페이스를 함수형 인터페이스라고 한다. @FunctionalInterface 어노테이션을 사용하면 컴파일러가 함수형인터페이스인지 여부를 검사하게 할 수 있다. 다음 코드를 보자.
public class B005 {
public static void main(String[] args) {
MyFunctionalInterface mfi = (int a) -> { return a * a; };
int b = mfi.runSomething(5);
System.out.println(b);
}
}
@FunctionalInterface
interface MyFunctionalInterface {
public abstract int runSomething(int count);
}
세번째 줄에 람다식을 사용하였다. MyFunctionalInterface에서 runSomething 메서드는 매개변수로 int 타입의 변수를 받는다고 명시되어있다. 따라서 굳이 람다식에서 int 라고 매개변수의 타입을 명시할 이유가 없다. 따라서 다음 처럼 작성할 수 있다.
(a) -> { return a * a; };
매개변수가 하나인 경우 ( ) 도 생략할 수 있고, 앞에서 말한대로 중괄호를 생략하고, 리턴문도 생략하면(코드가 한 줄일 때 리턴문도 같이 생략해야 한다) 최종적으로 다음 코드가 된다.
public class B005 {
public static void main(String[] args) {
MyFunctionalInterface mfi = a -> a * a;
int b = mfi.runSomething(5);
System.out.println(b);
}
}
@FunctionalInterface
interface MyFunctionalInterface {
public abstract int runSomething(int count);
}
람다식의 활용 - 메서드의 입력과 출력으로
람다식은 함수형인터페이스 타입의 익명객체를 대체한다. 따라서 당연히 람다식을 변수에 담을 수 있고, 메서드의 호출 인자로 사용하거나, 메서드의 반환값으로 사용할 수 있다.
람다식을 메서드의 호출 인자로 넘긴다는 것은, "로직을 주입한다"는 것이다. 자바에선 몇가지 함수형 인터페이스들을 미리 정의해 두었다. 이 인터페이스들은 메서드가 "어떤 타입을 입력으로 받아 어떤 타입을 반환할 것인지"를 정해둔 것이다. 즉, 구체적으로 어떻게 바꾸는지는 이 인터페이스를 구현하는 즉, 람다식으로 표현하는 개발자가 할 일이고, 결국 이것은 논리의 주입을 의미하게 된다. 아래는 예시 코드다.
public class B007 {
public static void main(String[] args) {
MyFunctionalInterface mfi = a -> a * a;
doIt(mfi);
}
public static void doIt(MyFunctionalInterface mfi) {
int b = mfi.runSomething(5);
System.out.println(b);
}
}
public class B008 {
public static void main(String[] args) {
doIt(a -> a * a);
}
public static void doIt(MyFunctionalInterface mfi) {
int b = mfi.runSomething(5);
System.out.println(b);
}
}
public class B009 {
public static void main(String[] args) {
MyFunctionalInterface mfi = todo();
int result = mfi.runSomething(3);
System.out.println(result);
}
public static MyFunctionalInterface todo() {
return num -> num * num;
}
}
람다식의 활용 - Stream (스트림) 활용
스트림은 그 어원에서 처럼 데이터의 흐름을 나타낸다. 스트림을 사용하는 기본적인 방법은 다음과 같다.
- 스트림생성하기 : 기존 컬렉션 프레임워크를 스트림으로 변환하거나 새로운 스트림을 생성한다.
- 가공하기(중간작업) : 제공되는 필터나 매핑 메서드를 이용해 스트림 속 데이터들을 가공한다.
- 결과물 만들기 : 최종적으로 필요한 데이터로 바꾼다.
아래는 나이가 저장된 배열로부터, 20세 미만인 경우 경고문을 출력하는 스트림과 람다를 활용한 코드다.
import java.util.Arrays;
public class B013 {
public static void main(String[] args) {
Integer[] ages = { 20, 25, 18, 27, 30, 21, 17, 19, 34, 28 };
Arrays.stream(ages)
.filter(age -> age < 20)
.forEach(age -> System.out.format("Age %d!!! Can't enter\n", age));
}
}
Arrays.stream() 메서드를 통해 배열을 스트림으로 변환하였다. 그 후 filter 메서드로 람다식 즉 로직을 주입하여 스트림 속에 20 미만인 데이터들만 포함하는 스트림을 만든다. 최종적으로 forEach 메서드를 통해 스트림 속 각각의 데이터를 화면에 출력하는 람다식 즉 로직을 주입해 결과물을 만든다.
메서드 레퍼런스
람다식을 더 간결하게 사용하기 위한 것으로 메서드 레퍼런스 라는게 있다. 하지만 내 기준으로 이건 정보가 너무 생략되어있어 가독성이 더 떨어지는 것 같다. 나중에 필요하면 정리하는 것으로 하고 넘어간다.
인터페이스의 default 메서드와 static 메서드
자바 8에서 컬렉션 프레임워크를 강화하기 위해 여러 메서드들이 인터페이스에 추가되어야 했다. 예를들면 forEach() 메서드처럼. 그러나 기존의 인터페이스 정의대로면 컬렉션 프레임워크의 계층구조의 아주 높은 부분인 Iterable 인터페이스부터 변경경되어야 할 것이고, 이를 "구현한 수많은 것들이 모두 변경되어야만" 했다. 이렇게 되면 수많은 자바 8 이전의 코드들이 자바 8에선 동작할 수 없게 된다. 이런 문제를 해결하기 위해 오라클은 아예 인터페이스의 정의를 고쳐, 디폴트 메서드 라는 것을 추가했다. 이 메서드는 구현이 있는 메서드로, 하위에서 오버라이딩을 선택적으로 할 수 있다. 또한, 람다식은 추상 메서드를 오버라이딩 한다. 따라서 앞에 서술한 문제가 발생되지 않는다. 비슷한 이유로 정적 메서드도 추가되었다. 각각을 사용하는 방법은 다음과 같다.
public class B017 {
public static void main(String[] args) {
System.out.format("정적 상수: %d\n", MyFunctionalInterface2.constant);
MyFunctionalInterface2.concreteStaticMethod();
MyFunctionalInterface2 mfi2 = () -> System.out.println("추상 인스턴스 메서드");
mfi2.abstractInstanceMethod();
mfi2.concreteInstanceMethod();
}
}
@FunctionalInterface
interface MyFunctionalInterface2 {
// 정적 상수
public static final int constant = 1;
// 추상 인스턴스 메서드
public abstract void abstractInstanceMethod();
// JAVA 8 디폴트 메서드 - 구체 인스턴스 메서드
public default void concreteInstanceMethod() {
System.out.println("디폴트 메서드 - 구체 인스턴스 메서드");
}
// JAVA 8 정적 메서드 - 구체 정적 메서드
public static void concreteStaticMethod() {
System.out.println("정적 메서드 - 구체 정적 메서드");
}
}
정리
자바 8은 함수형 프로그래밍 개념을 도입하면서 람다식을 통해 로직을 변수에 저장하거나, 메서드의 인자로 넘기거나, 메서드의 반환값으로 로직을 넘기는 행위가 가능해졌다. 이를 위해 인터페이스의 정의가 변경되었고, 스트림이라는 새로운 개념을 도입하였다. 이 외에도 다양한 변경이 있으니 다른 전문 책을 읽어보자.