자바도 프로그래밍 언어로 계속해서 변화해오고 있고 그에 따른 변화된 자바에 대한 이야기를 해보려고한다.
빅데이터를 처리할 필요성이 늘어났고, 멀티코어 컴퓨터와 같은 병렬 프로세싱이 가능한 장비들이 보급되며, 새로운 요구사항이 생겨났다.
그 중 하나가 병렬처리라고 한다.
더 자세한 병렬처리는 다음 링크 참고바란다!
재미로 읽어보는 ‘병렬처리’
함수형 프로그래밍은 객체 지향 프로그래밍처럼 프로그래밍의 패러다임(구현방법)의 한 종류이다.
여기서 객체지향프로그래밍과 함수형프로그래밍의 핵심 아이디어에 대해서 알아보자!
프로그램을 객체들의 협력과 상호작용으로 바라보고 구현한다.
그 객체들을 정의하기 위해 추상화와 같은 개념을 사용한다.
코드의 재사용성이 높아지고/ 유지보수, 확장이 쉬워지며/ 코드를 신뢰성있게 사용하기 쉬워진다.
수학의 함수처럼 특정한 데이터에 의존하지않고, 관련없는 데이터를 변경하지도 않으며, 결과값이 오직 입력값에만 영향을 받는 함수를 순수함수라고 한다.
프로그램을 이 순수한 함수의 모음으로 바라보고 구현한다.
검증이 쉽고, 성능 최적화가 쉬우며, 동시성 문제를 해결하기 쉽다는 효용성을 가진다.
(특정 input에 대한 output을 재사용할 수 있어 성능 최적화👍-캐싱)
위에서 함수를 값으로 전달한다고 했었는데 어떤식으로 보낼 수 있을까?
그러면 함수형 인터페이스가 무엇일까
//함수형 인터페이스
interface Predicate<T> {
boolean test(T t);
}
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;
}
위와같은 메소드가 두가지가 있다고 생각해보자, 하나는 티켓 하나는 머니인데 두개의 코드는 거의 유사하며, 아래 조건문만 다른것을 확인할 수 있다.
이 코드를 함수형 인터페이스를 사용하여 리팩토링해서 더 효율적으로 사용할 수 있게된다.
// 변경점 1 : Predicate<Car> 인터페이스를 타입 삼아 함수를 전달
public static List<Car> parkCars(List<Car> carsWantToPark, Predicate<Car> function) {
List<Car> cars = new ArrayList<>();
for (Car car : carsWantToPark) {
// 변경점 2 : 전달된 함수는 다음과 같이 사용
if (function.test(car)) {
cars.add(car);
}
}
return cars;
}
//실제 사용법
parkingLot.addAll(parkCars(carsWantToPark, Car::hasTicket));
parkingLot.addAll(parkCars(carsWantToPark, Car::noTicketButMoney));
위 코드처럼 Predicate로 함수를 매개변수로 지정해서 넣게되고, 티켓과 머니부분은 따로 hasTicket, noTicketButMoney 메소드를 티켓, 돈이 있으면 true, 없으면 false로 return 값을 받게 만들었다.
또한 위에서면 Car::hasTicket과 같은 새로운 표현이 보일텐데
우리는 함수를 값으로 취급하기 땜누에 참조로 불러서 쓸 수 있는 표현방법이다.
여기서 더 나아가 우리가 주말에는 티켓과 돈 모두 있어야 주차가 가능하도록 하려고 하는데 메소드가 아닌 다른 방법으론 할 수 없을까?
// 주말의 주차장 추가
ArrayList<Car> weekendParkingLot = new ArrayList<>();
weekendParkingLot
.addAll(parkCars(carsWantToPark, (Car car) -> car.hasParkingTicket() && car.getParkingMoney() > 1000));
이전에 나왔던 람다식이 여기서 등장한다!
함수를 값으로 전달하는데, 어딘가에 구현하지않고 그냥 간단하게 구현해서 넘길 수 있다.
람다식은 함수값으로 평가되며, 한번만 사용된다.
더 자세한 내용은 아래 링크 참고바란다!
람다 표현식
스트림은 정확하게는 java8부터 제공되는 한번 더 추상화된 자료구조와 자주 사용하는 프로그래밍 API를 제공한 것이다.
자료구조를 한번 더 추상화 했기 때문에 자료구조의 종류와는 상관없이 같은 방식으로 다룰 수 있다.
거기다 병렬처리에 유리한 구조이기 때문에 조건부로 성능도 챙길 수 있다.
(알고리즘 문제에서 실제로 쓰는 코드를 보니 간결하고 매우보기 좋았다..)
List<Car> benzParkingLot =
// carsWantToPark의 스트림값을 받아와서
carsWantToPark.stream()
// 거기 구현되어 있는 filter()메서드를 사용합니다.
// filter메서드는 함수를 파라미터로 전달받습니다.
.filter((Car car) ->
// 필터 메서드는 이름처럼 false를 반환한 스트림의 원소들을 제거
// 여기서 함수는 제조사가 벤츠면 true를 반환
car.getCompany().equals("Benz"))
// 이 결과를 반환을 받아서 다시 리스트로 묶어준다.
.toList();
위 코드처럼 이어이어서 함수를 조합하여 원하는 형태로 만들어 낼 수 있다.
또한 스트림의 객체는 자료구조의 모든 원소가 있으며, 이미 filter와 같이 구현되어 있는 메소드들도 있다.
위 스트림을 원래 우리가 사용하던 코드로 만든다면 아래와 같을 것이다.
ArrayList<Car> benzParkingLotWithoutStream = new ArrayList<>();
for (Car car : carsWantToPark) {
if (car.getCompany().equals("Benz")) {
benzParkingLotWithoutStream.add(car);
}
}
다시 말해 stream사용법은 1. 스트림을 받아와서 2. 가공하여 3. 결과를 만든다고 보면된다.
1. carsWantToPark.stream()
2. .filter((Car car) -> car.getCompany().equals("Benz"))
3. toList();
스트림 API는 너무나도 많고 방대해서 다 설명은 못하지만 3가지, map(), forEach(), filter()에 대해서는 자주 사용하기 때문에 소개해보겠다.
List<String> carNames = Arrays.asList("Series 6", "A9", "Ionic 6");
carNames.stream()
.forEach(System.out::println);
// 결과
// Series 6
// A9
// Ionic 6
carNames.stream()
.map(name -> name.toUpperCase()).toList();
// 결과
// ["SERIES 6", "A9", "IONIC 6"]
더 많은 API는 다음 링크를 참고하면 된다!
Java 8 Stream API
null은 비어있는 깡통, 참조가 없는 경우를 뜻한다.
제일 많이 보는 에러가 NullPointerException..일 지경으로 우리는 거의 모든 상황에 null이 발생할 수 있음을 경계하고 프로그램을 짜야한다.
만약 null이 올 수 있는 경우를 대비해서 조건을 단다고 해도 바쁘거나 익숙하지않아 null체크를 하지않는다면 시스템은 위험에 빠지게 될 것이다.
// 결과값을 감싼 객체를 만듭니다.
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();
// 이제 해당 메서드를 사용하는 유저는, 객체를 리턴받기 때문에 더 자연스럽게 성공여부를 체크하게 됩니다.
SomeObjectForNullableReturn getData = myDB.findUserIdByUsername("HelloWorldMan");
if (getData.isSuccess()) {
System.out.println("HelloWorldMan's user Id is : " + getData.getReturnValue());
}
}
}
class SomeDBClient {
// 결과값을 감싼 객체를 리턴합니다.
public SomeObjectForNullableReturn findUserIdByUsername(String username) {
// ... db에서 찾아오는 로직
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;
}
}
여기서도 더 나아가 발전시킨것이 java.util.Optional객체 이다.
우리가 위에서 데이터를 감싸며 했던 로직, 그 외에도 처리를 편하게해주는 로직들이 들어있는 객체이다.
Java8에서는 Optional<T>클래스를 사용하여 NullPointException을 방지할 수 있도록 도와준다.
또한 null이 올 수 있는 값을 감싸는 Wrapper클래스이다.
Optional이 비어있더라도, 참조해도 NullPointException이 발생하지 않는다.
Optional<Car> emptyOptional = Optional.empty();
Optional<Car> hasDataOptional = Optional.of(new Car());
Optional<Car> hasDataOptional = Optional.ofNullable(getCarFromDB());
Optional<String> carName = getCarNameFromDB();
// orElse() 를 통해 값을 받아옵니다, 파라미터로는 null인 경우 반환할 값을 적습니다.
String realCarName = carName.orElse("NoCar");
// 위는 예시코드고 실제는 보통 아래와 같이 사용하겠죠?
String carName = getCarNameFromDB().orElse("NoCar");
// orElseGet()이라는 메서드를 사용해서 값을 받아올 수 있습니다.
// 파라미터로는 없는 경우 실행될 함수를 전달합니다.
Car car = getCarNameFromDB().orElseGet(Car::new);
// 값이 없으면, 그 아래 로직을 수행하는데 큰 장애가 되는경우 에러를 발생시킬수도 있습니다.
Car car = getCarNameFromDB()
.orElseThrow(() -> new CarNotFoundException("NO CAR!)