본문 바로가기

자바

[Java] Lambda, Stream API 강의 정리

2021-03-16글

함수형 프로그래밍의 장점 ?

관심사의 분리

관심사의 분리란 무엇일까? 예제를 들어보자.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); 리스트에 있는 원소마다 콜론을 추가하려 한다.

이때 forEach() 를 사용한다면 ?

    @Test
    public void ForEach를_활용하여_콜론을_추가하는_문자열_작성() {
        StringBuilder stringBuilder = new StringBuilder();

        for (Integer number : numbers) {
            stringBuilder.append(number).append(" : ");
        }

        if (stringBuilder.length() > 0) {
            stringBuilder.delete(stringBuilder.length() - 3, stringBuilder.length());
        }
    }

나는 원소마다 콜론 추가하는 것을 원하는데, forEach문을 작성하니 이를 어떻게(How) 구현할지에 집중하고 있는 것 같다.

그렇다면 함수형 프로그래밍의 일종인 Stream을 사용한다면 ?

@Test
public void Stream을_활용하여_콜론을_추가하는_문자열_작성() {
    final String result = numbers.stream()
        .map(String::valueOf)
        .collect(joining(COLON_DELIMITER));
}

요소마다 콜론을 결합하는 것을 메소드를 사용함으로서 개발자인 내가 무엇을(What)을 수행할 것인지에 집중할 수 있다.

Side Effect가 발생하지 않는다.

  • Side Effect란 ? 함수 내의 실행으로 인해 함수 외부가 영향을 받는 것

함수형 프로그래밍의 특징은 지역 변수만을 변경할 수 있고, 매개변수를 변경하지 않는다.
즉, 함수는 같은 인수값으로 함수를 호출했을 때 항상 같은 값을 반환한다. (이 때 다른 값을 반환하는 Random, Scanner 등은 함수가 아니다).
만약 함수에 참조하는 객체가 있다면 그 객체는 불변이어야 하며, 해당 객체의 모든 참조 필드도 불변 객체를 직접 참조해야 한다.
함수 내에서 생성한 객체의 필드는 갱신할 수 있지만, 새로 생성한 필드의 갱신은 외부에 노출되면 안된다.
또한 다음에 메서드를 다시 호출한 결과에 영향을 미치지 않으며, 어떠한 예외도 일으키지 않아야 한다.
값이 변경되는 것을 허용한 객체를 멀티 스레드 프로그램에서 접근한다면, 값이 일정하지 않을 것이다.

하지만 단순히 구조만으로 순수성이 보장되지는 않고, 입력에 참조값이 오는 경우는 Side-Effect가 생긴다.
이에 대한 내용은 참조 투명성을 살펴보자.

참조 투명성

참조 투명성은 함수가 함수 외부의 영향을 받지 않는 것을 의미한다.
또, 함수의 결과는 입력 파라미터에만 의존하고, 함수 외부 세계(입력 콘솔, 파일, 데이터 베이스 등)에서 데이터를 읽지 않는다.

public static int add(int a, int b) {
  if (b > 0) {
    a++;
  }
  return a;
}

위 예제의 경우 원시 타입인 a의 값을 바꾸었는데, 자바의 원시타입의 매개변수는 call by value (메서드 호출 시 기본 자료형의 값을 인자로 전달하는 방식) 형태로 전달이 되어, 함수를 벗어나도 a에는 영향이 없다.
여기서 인자로 참조 변수를 넣어주면 어떤 일이 발생할 수 있을까?

public static int position(Position a, Position b) {
  a.setX(b.getX())
  return a;
}

이렇게 참조 변수를 넘겨주면 함수 안에서 참조 변수의 값을 바꿀 수도 있기 때문에 side effect가 발생할 수 있다.
함수형 프로그래밍은 이런 Side Effect가 발생하지 않는, 참조 투명성이어야 한다.

일급 객체

함수형 프로그래밍은 함수인 메서드가 일급 객체임을 말하는데, 일급 객체는 다음과 같은 특성을 가진다.

  • 변수나 데이터에 할당할 수 있어야 한다.
  • 객체의 매개변수로 넘길 수 있어야 한다.
  • 객체의 반환 값으로 리턴할 수 있어야 한다.

자바8 이전까지, 메서드는 일급 객체가 아니었지만, 자바8의 익명 함수의 등장으로 메서드도 일급 객체로 다룰 수 있게 되었다.
또 이 익명 함수를 좀 더 단순화 한 것이 바로 람다 표현식(lambda expression)이다.

Boxing과 UnBoxing

원시 타입이 래퍼 클래스로 변환하는 것을 Boxing이라고 하며,
래퍼 클래스를 원시 타입으로 형변환 하는 것을 UnBoxing이라고 한다.
JDK1.5부터는 래퍼 클래스와 기본 자료형 사이의 변환을 자동으로 해주는 Auto Boxing과 Auto UnBoxing 기능을 지원한다.

int i = 10
Integer integer = i; // Auto Boxing
Integer integer1 = new Integer(777); // 명시적 Boxing

int primitive = integer; // Auto UnBoxing

Auto Boxing이 일어나는 예

자바에서는 래퍼 클래스에 대한 연산이 시도될 때, 연산을 하려는 두 객체를 Auto Unboxing을 하여 원시타입으로 변환 후,
연산을 수행하게 된다. 래퍼 클래스와 원시 타입 간 연산도 동일하다.

final Integer integer127 = 127;
System.out.println(
  Stream.of(1, 2, 3, 4, 127)
  .filter(i -> i == integer127)
  .findFirst()
);

System.out.println(
  Stream.of(1, 2, 3, 4, 128)
  .filter(i -> i.equals(integer127))
  .findFirst()
);

두 스트림 연산에서 원시 타입 int 인 값들의 Stream을 만들고 각각 래퍼 클래스 Integer 와 비교를 할 때 Auto Boxing이 일어난다.
이때, findFirst().get() 을 하게 된다면 래퍼 클래스가 반환된다.
이렇게 되면 각각 원시 타입에 Auto Boxing이 일어나니,
원시 타입에 대한 스트림은 기본형 특화 스트림 (IntStream, LongStream, DoubleStream)을 사용하는 것이 성능상 좋다.

➕ 수업 예제에서 이 부분이 왜 127과 128로 나누었을까 크루들이랑 이야기를 했다.
Integer 래퍼 클래스는 127까지 인스턴스를 미리 생성해 두기 때문에 == 연산자로도 비교가 가능하지만 (주소값을 비교하는 것이다),
128부터는 새로운 인스턴스를 반환하기 때문에 동일성 검사인 equals() 를 사용해 반환한다.

참고 자료

  • [부수 효과 (Side Effect), 참조 투명성 (Referential Transparency)](