JAVA

Java에서의 지연 초기화,지연 평가 Optional 활용: orElse vs orElseGet

경딩 2025. 1. 6. 22:47

 

 

 

지연 평가는 언제 사용할까?

Optional 케이스에서  orElseGet() 인 경우에는 Optinal.empty() 일때만 평가를 하지때문에 평가하는데 비용이 크거나,

empty 가 아닐때 평가를 안해도 되는 비용들을 좀 줄여야하는 필요가 있을때  orElseGet을 써서 지연평가로 해당 부분을 해결한다.

 

핵심: orElseGet을 통해 평가 비용이 큰 연산을 지연시키고, 필요할 때만 실행하는 방식이 지연 평가의 본질이다. 이를 통해 성능을 최적화할 수 있다.


 

지연 초기화는 객체나 리소스의 초기화 시점을 지연시키는 것이고

지연 평가는 연산 시점을 지연시킨다.

 

둘다 성능 최적화라는 공통점이 있다.

 

Supplier 의 내부 람다식이 언제 평가(실행)될까?

import java.util.function.Supplier;


public class Main {


    public static void main(String[] args) {
        Supplier<String> supplier = () -> {
            System.out.println("Lambda is being executed");
            return "Hello, World!";
        };

        // 람다식이 실행되기 전에는 아무런 출력도 없음
        System.out.println("Before calling get()");

        // get() 메서드가 호출되면 람다식이 평가되고 실행됨
        String result = supplier.get(); // 여기서 "Lambda is being executed" 출력

        System.out.println("After calling get(): " + result);
    }
}

 

결과:

Before calling get()
Lambda is being executed
After calling get(): Hello, World!

 

 

Supplier는 값을 제공하는 역할을 하는 함수형 인터페이스이다. 입력 없이 출력을 반환한다.

 

Supplier 내부의 람다식은 get() 메서드가 호출될 때만 실행된다. 이는 지연 초기화(lazy initialization) 기법입니다.

 

 

 

즉 필드이 초기화 시점을 그 값이 처음 필요할때까지 늦추는 기법이다. 그래서 값이 전혀 쓰이지 않으면 결코 초기화도 일어나지 않는다.  이 기법은 정적 필드와 인스턴스 필드 모두에 사용할 수 있다. 

 

이펙티브 자바에서는 지연 초기화에 대해 필요할 때 까지는 하지 말라고 언급한다.

지연 초기화의 장단점

  • 장점: 초기화 비용이 큰 객체의 초기화를 실제로 필요한 시점까지 미루어 클래스나 객체 생성 비용을 줄일 수 있다
  • 단점: 지연 초기화로 인해 실제 초기화가 이뤄지게 되면, 해당 필드에 접근하는 비용이 커질 수 있다. 특히, 자주 접근할 경우 성능 저하를 초래할 수 있다.
  • 주의: 지연 초기화가 적합한 상황은, 초기화 비용이 크고, 해당 필드를 사용하는 인스턴스의 비율이 낮을 때이다. 지연 초기화가 성능에 미치는 영향을 평가하기 위해서는 성능 측정이 필요합니다.

 

 

import java.util.Optional;


public class Main {


    public static void main(String[] args) {

        String username = "이름";
        String result1 = Optional.ofNullable(username).orElse("no name");
        System.out.println(result1);

        String result2 = Optional.ofNullable(username).orElseGet(() -> "no name");
        System.out.println(result2);

    }

}

 

// 실행 결과
이름
이름

 

실행결과는 이름 이름이다 왜냐하면 username != null 이기 때문이다.

import java.util.Optional;


public class Main {


    public static void main(String[] args) {

        String username = null;
        String result1 = Optional.ofNullable(username).orElse("no name");
        System.out.println(result1);

        String result2 = Optional.ofNullable(username).orElseGet(() -> "no name");
        System.out.println(result2);

    }


}

 

// 실행결과
no name
no name

 null 값이면 "no name" 이다.

 

orElse 와 orElseGet 의 차이는 무엇일까?

 

아래의 경우의 결과를 생각해보자

import java.util.Optional;
import java.util.function.Supplier;


public class Main {


    public static void ohMyGod() {
        String username = null;
        String result1 = Optional.ofNullable(username).orElse(getDefaultName());
        System.out.println(result1);

        String result2 = Optional.ofNullable(username).orElseGet(() -> getDefaultName());
        System.out.println(result2);
    }

    private static String getDefaultName() {
        System.out.println(" getDefaultName 호출");
        return "no name";
    }

    public static void main(String[] args) {

        String username = "이름";
        String defaultName = getDefaultName(); // 함수 호출
        String result1 = Optional.ofNullable(username).orElse(defaultName);
        System.out.println(result1);

//        Supplier<String> supplier = () -> getDefaultName(); // supplier 를 활용한 함수 호출
//        String result2 = Optional.ofNullable(username).orElseGet(supplier);
//        System.out.println(result2);

    }


}

 

실행 결과

 getDefaultName 호출
이름

 

 

 

supplier 를 활용하여 호출해보자!

    public static void main(String[] args) {

//        String username = "이름";
//        String defaultName = getDefaultName(); // 함수 호출
//        String result1 = Optional.ofNullable(username).orElse(defaultName);
//        System.out.println(result1);

        String username = "이름";
        Supplier<String> supplier = () -> getDefaultName(); // supplier 를 활용한 함수 호출
        String result2 = Optional.ofNullable(username).orElseGet(supplier);
        System.out.println(result2);

    }

 

실행결과

이름

 

 

Supplier 를 활용한 경우는

getDefaultName 호출

 

이라는 로그가 남지않았고 바로 호출한 함수의 경우는 로그가 남았다.

즉 orElse 는 값이 null 여부와 상관없이 실행되고 orElseGet 은 null 일때만 실행되는것을 확인할 수 있다.

 

내부 코드를 살펴보자

public T orElse(T other) {
    return value != null ? value : other;  // other는 즉시 반환됨
}



public T orElseGet(Supplier<? extends T> supplier) {
    return value != null ? value : supplier.get();  // supplier.get()은 지연 평가
}

 

함수는 실행될때 계산되기 때문에 지연 실행이 된다. 하지만 other 은 외부에서 받는 미리 계산이 완료된 상태이다.

 

jpa 에서 orElseGet 과 orElse 를 유의해서 사용해야하는 사례를 알아보자

public User findByUsername(String name) {
    return userRepository.findByName(name)
            .orElse(createUserWithName(name));  // orElse는 createUserWithName(name)을 즉시 실행합니다.
}

private User createUserWithName(String name) {
    User newUser = new User();
    newUser.setName(name);
    return userRepository.save(newUser);
}

 

만약 user 테이블의 name이 unique였다면?

이 코드에서 orElse의 동작

  • findByName(name)이 Optional.empty()를 반환할 경우, orElse(createUserWithName(name))는 createUserWithName(name)을 즉시 실행한다.
  • 즉, createUserWithName(name)가 Optional.empty() 여부와 관계없이 항상 실행된다.

아래 코드와 동일하게 동작한다.

public User findByUsername(String name) {
	User newUser = createUserWithName(name); 
	return userRepository.findByName(name).orElse(newUser);
}

private User createUserWithName(String name) {
	User newUser = new User();
	newUser.setName(name)
	return userRepository.save(user);
}

 

때문에 orElseGet 을 활용하여 null 때만 해당 함수가 호출되도록 해주야한다.

public User findByUsername(String name) {
    return userRepository.findByName(name)
            .orElseGet(() -> createUserWithName(name));  // orElseGet을 사용하면 createUserWithName(name)을 필요할 때만 실행합니다.
}

 

 

참고 자료 :

이펙티브 자바

https://cfdf.tistory.com/34

 

'JAVA' 카테고리의 다른 글

Runnable과 Callable  (0) 2025.01.12
ReentrantLock  (0) 2025.01.07
고급 동기화 - concurrent.Lock  (1) 2025.01.05
생산자 소비자 문제1  (1) 2024.12.31
메모리 가시성  (0) 2024.12.29