지연 평가는 언제 사용할까?
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)을 필요할 때만 실행합니다.
}
참고 자료 :
이펙티브 자바
'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 |