[Java] 모던자바 - 함수형 프로그래밍, 람다, 스트림, Optional

Hyo Kyun Lee·2025년 2월 19일
0

Java

목록 보기
88/91

1. 개요

JDK1.8(8) 버전 이후 바뀐 java 언어의 발전과 적용 방안에 대해 공부한 내용을 기록한다.

2. 함수형 프로그래밍

Java의 객체 지향 프로그래밍에서 진화하여, 코드의 재사용성/유지보수/신뢰성을 더욱 높일 수 있는 방안으로 나타난 프로그래밍 방법이다.

순수함수처럼, 오로지 입력값으로 결과값을 산출하는 일방향적인 기능을 만들어내는 방법을 함수형 프로그래밍이라 한다.

내부적으로 다른(예측할 수 없는) 데이터 변경 등의 작업없이 프로그래밍이 이루어진다.

  • 함수를 일급(인터페이스)화하여 하나의 값, 반환값으로 사용할 수 있다.
  • 검증이 필요한 부분만 검증할 수 있다.
  • 성능 최적화가 쉽다(특정 input에 대한 output을 재사용할 수 있으며 다른 데이터를 변경하거나 사용하지 않는다(캐싱처리))
  • 인수를 제외한 다른 값을 변경하지 않는다(동시성 처리).
import java.util.ArrayList;
import java.util.List;

public class LambdaAndStream {
    public static void main(String[] args) {
        ArrayList<Car> carsWantToPark = new ArrayList<>();
        ArrayList<Car> parkingLot = new ArrayList<>();

        Car car1 = new Car("Benz", "Class E", true, 0);
        Car car2 = new Car("BMW", "Series 7", false, 100);
        Car car3 = new Car("BMW", "X9", false, 0);
        Car car4 = new Car("Audi", "A7", true, 0);
        Car car5 = new Car("Hyundai", "Ionic 6", false, 10000);

        carsWantToPark.add(car1);
        carsWantToPark.add(car2);
        carsWantToPark.add(car3);
        carsWantToPark.add(car4);
        carsWantToPark.add(car5);

        parkingLot.addAll(parkingCarWithTicket(carsWantToPark));
        parkingLot.addAll(parkingCarWithMoney(carsWantToPark));


        for (Car car : parkingLot) {
            System.out.println("Parked Car : " + car.getCompany() + "-" + car.getModel());
        }


    }

    public static List<Car> parkingCarWithTicket(List<Car> carsWantToPark) {
        ArrayList<Car> cars = new ArrayList<>();
        
        for (Car car : carsWantToPark) {
            if (car.hasParkingTicket()) {
                cars.add(car);
            }
        }

        return cars;
    }

    public static List<Car> parkingCarWithMoney(List<Car> carsWantToPark) {
        ArrayList<Car> cars = new ArrayList<>();
        
        for (Car car : carsWantToPark) {
            if (!car.hasParkingTicket() && car.getParkingMoney() > 1000) {
                cars.add(car);
            }
        }
        
        return cars;
    }
}

class Car {
    private final String company; // 자동차 회사
    private final String model; // 자동차 모델

    private final boolean hasParkingTicket;
    private final int parkingMoney;

    public Car(String company, String model, boolean hasParkingTicket, int parkingMoney) {
        this.company = company;
        this.model = model;
        this.hasParkingTicket = hasParkingTicket;
        this.parkingMoney = parkingMoney;
    }

    public String getCompany() {
        return company;
    }

    public String getModel() {
        return model;
    }

    public boolean hasParkingTicket() {
        return hasParkingTicket;
    }

    public int getParkingMoney() {
        return parkingMoney;
    }
}

위 로직에서 parkingLot에 add하는 부분은 Ticket이 있는지(parkingCarWithTicket)와 주차요금이 있는지(parkingCarWithMoney) 검증하는 로직을 먼저 진행한다.

  • parkingLot.addAll(parkingCarWithTicket(carsWantToPark));
  • parkingLot.addAll(parkingCarWithMoney(carsWantToPark));

하지만 위 두 검증로직은 인수, 검증과정, 반환값이 매우 비슷하고 중복되는 부분이 많기에 이를 하나로 통합할 수 없을까라는 생각을 할 수 있다.

이때 함수형 인터페이스(하나의 추상 메서드)를 생성하여 위 검증 로직을 좀 더 간결하게 작성할 수 있다(아이디어는 검증 로직을 하나의 함수로 전달받아 처리한다).

public static List<Car> vaildateCar(ArrayList<Car> carList, Predicate<Car> validateFunc){
	//내부 주요 검증 로직을 함수로 전달받는다.
    ArrayList<Car> resultList = new ArrayList<>();
    
    for(Car car : carList){
    	//함수를 전달받아 검증 로직을 간결하게 구성
        //if(validateFunc.test(car))
        if(validateFunc.validate(car)){
        	resultList.add(car);
        }
    }
    
    return resultList;
}

interface Predicate<T>{
	//Type : Car
    //해당 함수를 인자로 전달하기 위한 인터페이스, 함수형 인터페이스는 구현 메서드가 하나이므로 명확한 목적으로 인터페이스를 활용할 수 있게 된다.
	boolean validate(T t);
}

관심사분리에 따라, 해당 검증로직을 Car 객체 안에서 구성하도록 설정한다.

public Car(String company, String model, boolean hasParkingTicket, int parkingMoney) {
        this.company = company;
        this.model = model;
        this.hasParkingTicket = hasParkingTicket;
        this.parkingMoney = parkingMoney;
    }

    public String getCompany() {
        return company;
    }

    public String getModel() {
        return model;
    }

    public boolean hasParkingTicket() {
        return hasParkingTicket;
    }

    public int getParkingMoney() {
        return parkingMoney;
    }
    
    public static boolean hasTicket(Car car){
    	return car.hasParkingTicket();
    }
    
    public static boolean noTicketButMoney(Car car){
    	return !car.hasParkingTicket() && car.getParkingMoney() > 1000;
    }

위 함수형 프로그래밍을 적용한 이후에는 메인 로직에서 검증로직을 아래와 같이 간결하게 바꿀 수 있다.

public static void main(String[] args) {
        ArrayList<Car> carsWantToPark = new ArrayList<>();
        ArrayList<Car> parkingLot = new ArrayList<>();

        Car car1 = new Car("Benz", "Class E", true, 0);
        Car car2 = new Car("BMW", "Series 7", false, 100);
        Car car3 = new Car("BMW", "X9", false, 0);
        Car car4 = new Car("Audi", "A7", true, 0);
        Car car5 = new Car("Hyundai", "Ionic 6", false, 10000);

        carsWantToPark.add(car1);
        carsWantToPark.add(car2);
        carsWantToPark.add(car3);
        carsWantToPark.add(car4);
        carsWantToPark.add(car5);

		//Predicate : 해당 객체의 메소드를 추출
        parkingLot.addAll(carsWantToPark, Car::hasTicket());
        parkingLot.addAll(carsWantToPark, Car::noTicketButMoney());
        ....

3. 람다(익명함수)

이름이 없는 함수이기에, 오로지 목적 달성을 위해 사용하는 임시적인 객체이다. 그 자체로 인수 및 하나의 값으로 활용할 수 있기에 일급 객체로 취급할 수 있다.

위 검증 로직에서 익명함수를 사용하여, 별도의 Car 메서드를 만들지 않고 즉각적으로 검증 로직을 구현할 수 있다.

별도의 로직없이 return만 있는 함수의 경우, return 문도 생략하고 바로 검증로직을 작성할 수 있다.

parkingLot.addAll(carsWantToPark, (Car car) -> return car.hasParkingTicket() && car.getParkingMoney > 1000);

4. 스트림

데이터를 기억하지 않고(원본 데이터를 저장하지 않고, 각 단계마다 부분적인 데이터를 전달받고 새로운 흐름(리스트)를 추출한다는 개념), 단계단계마다 내부적인 연산을 통해 필요한 부분을 추출하고 병렬적인 처리를 지원하는 등 컬렉션에 비해 더 효율적인 처리를 할 수 있도록 기능을 제공해주는 인터페이스이다.

자료구조를 추상화하였기에 자료구조에 상관없이 적용할 수 있고, 또한 스트림은 데이터의 한 시점에서 처리하는데 집중하기에, 최종적인 결과를 저장하는 것은 연산 종료 시점에 진행한다(소비(consume)).

5. Optional

NullPointerException을 방지할 수 있는 방안으로, isEmpty()라는 하나의 객체를 전달하여 Null을 전달하여 발생하는 오류를 방지할 수 있는 인터페이스이자 하나의 형태이다.

쉽게 말하면 Null을 참조하여 발생하는 예외를 미연에 방지하고자 Null이 아닌 하나의 객체를 전달하기 위해 만든 도구이다.

if(userId != null){
	logic with userId
}

이를 아래와 같이 발전할 수 있고

SomeObjectNullableReturn isSuccess = findUserIdByUserName(userId);

if(isSuccess)
	logic with userId

...

public SomeObjectNullableReturn findUserIdByUserName(String userName){
	if(data != null) return new SomeObjectNullableReturn(data, true);
    esle return new SomeObjectNullableReturn(null, false);
}

이를 일반화한 개념이 바로 Optional이다.

//carName이 null일 경우 NoCar를 반환받는다.
Optinal<String> carName = carName.orElse("NoCar");

0개의 댓글

관련 채용 정보