- 병렬 처리
- 빅데이터, 멀티 코어 컴퓨터와 같은 병렬 프로세싱이 가능한 장비의 보급으로 인해 병렬처리에 대한 요구사항 증가
- 함수형 프로그래밍
- 프로그래밍 패러다임의 한 종류
- 프로그램을 순수한 함수의 모음으로 바라보고 구현하는 것
- 검증이 쉬움(필요한 부분만의 검증이 가능하기 때문에)
- 성능 최적화가 쉬움(특정 input에 대한 output을 재사용할 수 있기 때문 → 캐싱)
- 동시성 문제 해결이 쉬움(다른 값의 변경을 야기하지 않음)💡 순수함수란?
수학의 함수처럼 특정한 데이터에 의존하지 않고 관련 없는 데이터를 변경하지도 않으며, 결과값이 오직 입력값에만 영향을 받는 함수// 수학의 함수 f(x, y) = x + 2xy // 순수 함수 // input에 따라 output은 항상 일정 public int someMethod(int x, int y) { return x + 2y; } class notFunc { private int y = 0; private int result; // 메소드 내부의 제어할 수 없는 y로 인해 output이 변경될 수 있음 public int anotherMethod(int x) { return x + this.y * 2; } // 메소드 내부에서 this.result의 값을 변경하고 반환 public int otherMethod(int x, int y) { int result = x + 2 * y; this.result = result; return result; } }
- 함수형 프로그래밍의 아이디어와 문법을 지원
- 함수를 값으로 다루거나 다른 함수에 넘길 수 있음
- 익명 함수 사용 가능
- 스트림 기능의 지원으로 더 간결하고 유연하며 성능 좋은 코드의 작성 가능
- 데이터 처리 연산을 지원하도록 추출된 연속된 요소
- 데이터 처리에 초점을 맞춘 인터페이스
public exampleMethod(int parameter1, ??? parameterFunction) { ... }
- 함수를 전달할 때 타입을 알려주기 위해 함수형 인터페이스를 선언하거나 사용해야함 (인터페이스가 타입 역할을 하기 때문에)
- 추상 메소드를 딱 하나만 가지고 있음
@FunctionalInterface
어노테이션으로 검증 가능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(parkCars(carsWantToPark, Car::hasTicket)); parkingLot.addAll(parkCars(carsWantToPark, Car::noTicketButMoney)); for (Car car : parkingLot) { System.out.println("Parked Car : " + car.getCompany() + "-" + car.getModel()); } } // Predicate<Car> 인터페이스를 타입 삼아 함수를 전달 public static List<Car> parkCars(List<Car> carsWantToPark, Predicate<Car> function) { List<Car> cars = new ArrayList<>(); for (Car car : carsWantToPark) { // 전달된 함수를 통해 주차 가능 여부를 판단 if (function.parkCheck(car)) { 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; } // 클래스 내부에 메소드 구현 public static boolean hasTicket(Car car) { return car.hasParkingTicket; } public static boolean noTicketButMoney(Car car) { return !car.hasParkingTicket && car.getParkingMoney() > 1000; } } // 함수형 인터페이스 정리 interface Predicate<T> { boolean parkCheck(T t); }
- 함수를 값으로 전달할 때, 함수를 따로 구현하지 않아도 람다식으로 전달 가능
- 함수 값으로서 한 번만 사용됨
- 모든 유형의 함수에 적용 가능하며, 간결하게 작성 가능
(파라미터 값, ...) -> { 실행문 } // 리턴 타입, return문 여부에 따라 {}까지도 생략 가능 public int toLambdaMethod(int x, int y) { return x + y; } 👇 (x, y) -> x + y public int toLambdaMethod2() { return 100; } 👇 () -> 100 public void toLambdaMethod3() { System.out.println("Hello World"); } 👇 () -> System.out.println("Hello World")
- 스트림의 특징
- 원본 데이터를 변경하지 않음
- Java 컬렉션으로부터 스트림을 받아서 사용
- 한 번 사용한 스트림은 어디에도 남지 않음 (일회용)
ArrayList<Car> benzParkingLotWithoutStream = new ArrayList<>(); for (Car car : carsWantToPark) { if (car.getCompany().equals("Benz")) { benzParkingLotWithoutStream.add(car); } } 👇 List<Car> benzParkingLot = // carsWantToPark의 스트림을 받아서 carsWantToPark.stream() // 제조사가 Benz인 요소를 .filter((Car car) -> car.getCompany().equals("Benz")) // List로 리턴 .toList();
public class NullIsDanger { public static void main(String[] args) { SomeDBClient myDB = new SomeDBClient(); String userId = myDB.findUserIdByUsername("HelloWorldMan"); System.out.println("HelloWorldMan's user Id is : " + userId); } } class SomeDBClient { public String findUserIdByUsername(String username) { String data = "DB Connection Result"; if (data != null) { return data; } else { return null; } } }
- 논리적으로도 환경적으로도 null이 리턴될 여지가 있음에도 null이 리턴될 수 있음을 명시하지 않음
- 메인 함수에서 null 체크를 하지 않는다면
NullPointerException
발생
public class NullIsDanger { public static void main(String[] args) { SomeDBClient myDB = new SomeDBClient(); String userId = myDB.findUserIdByUsernameOrThrowNull("HelloWorldMan"); // 개선 1: null이 리턴될 경우를 대비 if (userId != null) { System.out.println("HelloWorldMan's user Id is : " + userId); } } } class SomeDBClient { // 개선 1: 메소드에서 null이 리턴될 수 있음을 명시 public String findUserIdByUsernameOrThrowNull(String username) { String data = "DB Connection Result"; if (data != null) { return data; } else { return null; } } }
- 실수로 null 체크를 하지 않게 된다면
NullPointerException
발생
class SomeObjectForNullableReturn { private final String returnValue; private final Boolean isSuccess; SomeObjectForNullableReturn(String returnValue, Boolean isSuccess) { this.returnValue = returnValue; this.isSuccess = isSuccess; } public String getReturnValue() { return returnValue; } public Boolean isSuccess() { return isSuccess; } } public class NullIsDanger { public static void main(String[] args) { SomeDBClient myDB = new SomeDBClient(); // 개선 2 : 객체를 리턴 받기 때문에 메소드가 위험할 수 있다는 것을 더 쉽게 인지 가능 SomeObjectForNullableReturn getData = myDB.findUserIdByUsername("HelloWorldMan"); if (getData.isSuccess()) { System.out.println("HelloWorldMan's user Id is : " + getData.getReturnValue()); } } } class SomeDBClient { // 개선 2 : 결과값을 감싼 객체를 리턴 public SomeObjectForNullableReturn findUserIdByUsername(String username) { String data = "DB Connection Result"; if (data != null) { return new SomeObjectForNullableReturn(data, true); } else { return new SomeObjectForNullableReturn(null, false); } } }
class SomeObjectForNullableReturn<T> { private final T returnValue; private final Boolean isSuccess; SomeObjectForNullableReturn(T returnValue, Boolean isSuccess) { this.returnValue = returnValue; this.isSuccess = isSuccess; } public T getReturnValue() { return returnValue; } public Boolean isSuccess() { return isSuccess; } }
- 개선 Ver.2의 발전시켜서 감싸는 객체를 조금 수정하면, 위험할 수 있는 모든 메소드에 사용 가능
- Optional<T> 클래스를 사용하여
NullPointerException
을 방지- null인 값을 참조해도
NullPointerException
이 발생하지 않도록 값을 감싸는 래퍼 클래스// 값이 null인 Optional 생성 Optional<Car> emptyOptional = Optional.empty(); // 값이 있는 Optional 생성 Optional<Car> hasDataOptional = Optional.of(new Car()); // 값이 있을 수도 없을 수도 있는 Optional 생성 Optional<Car> hasDataOptional = Optional.ofNullable(getCarFromDB()); // 값이 없을 경우 대체할 값, 함수, 예외처리 설정 가능 String carName = getCarNameFromDB().orElse("NoCar"); Car car = getCarNameFromDB().orElseGet(Car::new); Car car = getCarNameFromDB().orElseThrow(() -> new CarNotFoundException("NO CAR!)