[Java] 모던자바

김세림·2024년 5월 1일

Java

목록 보기
24/24
post-thumbnail

모던자바


자바도 프로그래밍 언어로 계속해서 변화해오고 있고 그에 따른 변화된 자바에 대한 이야기를 해보려고한다.

Java8 이후~

병렬처리

빅데이터를 처리할 필요성이 늘어났고, 멀티코어 컴퓨터와 같은 병렬 프로세싱이 가능한 장비들이 보급되며, 새로운 요구사항이 생겨났다.
그 중 하나가 병렬처리라고 한다.
더 자세한 병렬처리는 다음 링크 참고바란다!
재미로 읽어보는 ‘병렬처리’

함수형 프로그래밍

함수형 프로그래밍은 객체 지향 프로그래밍처럼 프로그래밍의 패러다임(구현방법)의 한 종류이다.
여기서 객체지향프로그래밍과 함수형프로그래밍의 핵심 아이디어에 대해서 알아보자!

객체지향 프로그래밍의 핵심 아이디어?

  1. 프로그램을 객체들의 협력과 상호작용으로 바라보고 구현한다.

  2. 그 객체들을 정의하기 위해 추상화와 같은 개념을 사용한다.

  3. 코드의 재사용성이 높아지고/ 유지보수, 확장이 쉬워지며/ 코드를 신뢰성있게 사용하기 쉬워진다.

함수형 프로그래밍

  1. 수학의 함수처럼 특정한 데이터에 의존하지않고, 관련없는 데이터를 변경하지도 않으며, 결과값이 오직 입력값에만 영향을 받는 함수를 순수함수라고 한다.

  2. 프로그램을 이 순수한 함수의 모음으로 바라보고 구현한다.

  3. 검증이 쉽고, 성능 최적화가 쉬우며, 동시성 문제를 해결하기 쉽다는 효용성을 가진다.
    (특정 input에 대한 output을 재사용할 수 있어 성능 최적화👍-캐싱)

결론 : Java8에서 새롭게 추가된 개념

자바 함수의 변화

  1. 함수형 프로그래밍 아이디어 1: 함수를 일급값으로
  • 지금까지 우리는 기본값과, 객체로 조작할 수 있는 값들을 대해왔는데 이에 대한 특징은 다음과 같다.
    • 함수에 인자로 넘길 수 잇다.
    • 함수의 결과로 반환할 수 있다.
    • 값을 수정할수 있다.
    • 값을 변수에 대입할 수 있다.
  • 여기서 이러한 연산을 모두 지원하는 값들을 일급시민 혹은 일급객체 라고 한다.
  • 그런데 메서드는 저 위의 특징을 가지고있지 않고, 특정 연산을 지원하지않는 값이기 때문에 이급 시민으로 볼 수 있게 된다.
  • 이 함수를 값으로 취급할 수 있다면 더 많은 것을 할 수 있지 않을까? 라는 것에서 나왔다.
  1. 함수형 프로그래밍 아이디어 2: 람다:익명함수
  • 람다는 익명함수를 지칭하는 말로써, 이름이 없는 함수를 뜻한다.(일급 객체)
  • 함수를 값으로 사용할 수도 있으며, 파라미터에 전달도 가능하다.
  • 변수에 대입하기와 같은 연산들이 가능하다.
  1. 스트림
  • 스트림은 데이터의 흐름을 뜻하며, 데이터 처리연산을 지원하도록 소스에서 추출된 연속된 요소이다.
  • 컬렉션이 데이터를 저장하거나 접근하는데 초첨을 맞춘 인터페이스라면,
    스트림은 데이터를 처리하는데 초점을 맞춘 인터페이스이다.
  • 멀티쓰레드 관련 코드를 구현하지않아도 알아서 병렬로 추가해주는 기능이다.
  • 코드를 더 간결하고, 유연하고 성능좋게 구현이 가능하다.

람다와 스트림 문법

함수형 인터페이스

위에서 함수를 값으로 전달한다고 했었는데 어떤식으로 보낼 수 있을까?

  • 함수형 인터페이스!

그러면 함수형 인터페이스가 무엇일까

  • 추상메소드를 딱 하나만 가지고 있는 인터페이스를 말한다.
  • 또한 @FunctionalInterface 어노테이션으로 검증이 가능하다.
//함수형 인터페이스
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

스트림 API는 너무나도 많고 방대해서 다 설명은 못하지만 3가지, map(), forEach(), filter()에 대해서는 자주 사용하기 때문에 소개해보겠다.

  1. forEach()
    각각에 원소에 넘겨받은 함수를 실행해준다.
    하지만 넘겨받은 반한값을 가지고 무엇을 하지않으며, 있어도 무시가 가능하다.
List<String> carNames = Arrays.asList("Series 6", "A9", "Ionic 6");

carNames.stream()
    .forEach(System.out::println);

// 결과 
// Series 6
// A9
// Ionic 6
  1. map()
    모든 요소를 가공해서 반환하며, forEach와는 반대로 넘겨받은 토대로 값을 변환시키는데 주로 사용한다.
carNames.stream()
	.map(name -> name.toUpperCase()).toList();

// 결과
// ["SERIES 6", "A9", "IONIC 6"]
  1. filter()
    위의 예제에서 사용했으며. 조건에 맞는 것만 반환하는데 사용한다.

더 많은 API는 다음 링크를 참고하면 된다!
Java 8 Stream API

Null

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객체 이다.
우리가 위에서 데이터를 감싸며 했던 로직, 그 외에도 처리를 편하게해주는 로직들이 들어있는 객체이다.

Optional<T>

Optional이란?

Java8에서는 Optional<T>클래스를 사용하여 NullPointException을 방지할 수 있도록 도와준다.
또한 null이 올 수 있는 값을 감싸는 Wrapper클래스이다.
Optional이 비어있더라도, 참조해도 NullPointException이 발생하지 않는다.

사용법

  • 값이 null인 Optional 생성하기
Optional<Car> emptyOptional = Optional.empty();
  • 값이 있는 Optional 생성하기
Optional<Car> hasDataOptional = Optional.of(new Car());
  • 값이 있을 수도 없을 수도 있는 Optional 생성하기
Optional<Car> hasDataOptional = Optional.ofNullable(getCarFromDB());
  • Optional 객체 사용하기(값 받아오기)
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!)

0개의 댓글