Java 8에서 소개된 Optional 클래스를 사용하면 null 값을 안전하고 효과적으로 다룰 수 있다.
Optional은 여러 메소드를 제공하는데, Optional의 값이 없을 때, 파라미터로 넘어온 값을 반환해주는 orElse()
메소드가 대표적이다.
그리고 orElseGet()
이라는 메소드도 있는데, 둘의 차이는 무엇이고 어떤 상황에서 사용해야 할지 알아보자.
@Test
void orElse_empty() {
// given
Optional<String> empty = Optional.empty();
// when
String value = empty.orElse(logic());
// then
assertThat(value).isEqualTo("default value");
}
@Test
void orElseGet_empty() {
// given
Optional<String> empty = Optional.empty();
// when
String value = empty.orElseGet(() -> logic());
// then
assertThat(value).isEqualTo("default value");
}
String logic() {
System.out.println("Hello World");
return "default value";
}
다음과 같은 두 개의 테스트 코드가 있다.
두 테스트 코드 모두 Optional 값이 비어있으면, 기본 값이 반환되는지 검사한다.
두 테스트의 차이는 orElse()
, orElseGet()
메소드의 사용 유무이다.
테스트를 실행했을 때 콘솔에 출력되는 결과는 다음과 같다.
// orElse_empty
Hello World
// orElseGet_empty
Hello World
둘 다 똑같은 동작을 한다.
그리고 다음과 같은 테스트가 있다.
@Test
void orElse_present() {
// given
Optional<String> present = Optional.of("value");
// when
String value = present.orElse(logic());
// then
assertThat(value).isEqualTo("value");
}
@Test
void orElseGet_present() {
// given
Optional<String> present = Optional.of("value");
// when
String value = present.orElseGet(() -> logic());
// then
assertThat(value).isEqualTo("value");
}
물론 테스트는 통과한다.
하지만 콘솔에 출력되는 결과는 서로 다르다.
// orElse_present
Hello World
// orElseGet_present
orElse_present
테스트는 콘솔에 Hello World가 출력된 반면, orElseGet_present
테스트는 콘솔에 아무것도 출력되지 않았다.
orElse()
, orElseGet()
메소드가 제공하는 의도는 서로 같다.
하지만 왜 이러한 차이가 발생한 것일까?
orElse()
메서드를 사용하면 Optional이 값을 포함하지 않을 때 기본 값을 제공한다.
하지만 orElse()
메서드의 파라미터로 받는 값은 Optional의 값의 유무에 상관 없이 파라미터로 전달된다.
orElseGet()
메서드는 orElse()
메소드와 동일하다.
모던 자바 인 액션에는 다음과 같이 설명되어 있다.
orElse 메서드에 대응하는 게으른 버전의 메서드다.
orElseGet()
메서드는 파라미터의 타입이 Optional의 타입이 아닌, Supplier
를 타입으로 받는다.
Optional의 orElseGet()
메서드는 다음과 같이 구현되어 있다.
public T orElseGet(Supplier<? extends T> supplier) {
return this.value != null ? this.value : supplier.get();
}
value가 null이 아니면, value를 반환하고, value가 null이면 매개변수로 넘어온 Supplier의 get()
메서드를 호출한다.
따라서 Optional의 값이 없을 때 게으르게 기본 값을 반환할 수 있는 것이다.
정말 값이 없을 때 값을 반환하는 orElseGet()
메서드를 사용하는 것이 좋다고 생각될 수 있다.
하지만 orElseGet()
은 인자로 함수형 인터페이스를 받기 때문에 가독성이 좋지는 않다.
// orElse()
String value = empty.orElse("default value");
// orElseGet()
String value = empty.orElseGet(() -> "default value");
게다가 문자열 같은 상수는 이미 생성이 된 객체이므로 굳이 게으르게 처리할 필요가 없다.
다음과 같은 클래스와 비즈니스 로직이 있다.
Stage(공연), Ticket(티켓)
공연과 티켓은 1:1 관계이다.
공연에 대해 티켓을 생성할 수 있다.
공연에 티켓이 존재하지 않으면 티켓을 새로 생성한다.
공연에 이미 티켓이 있다면 기존의 티켓의 개수에 생성할 티켓의 개수를 더한다.
비즈니스 로직을 구현한 코드는 다음과 같다.
public TicketCreateResponse createTicket(TicketCreateRequest request) {
Stage stage = stageRepository.findById(request.stageId())
.orElseThrow(NoSuchElementException::new);
Ticket ticket = ticketRepository.findByStage(stage)
.orElse(ticketRepository.save(new Ticket(stage)));
ticket.addAmount(request.amount());
return new TicketCreateResponse(ticket.getId(), ticket.getAmount());
}
해당 로직을 한 번 호출하면 문제가 발생하지 않는다.
@Test
void 축제에_티켓이_없으면_새로_생성된다() {
Stage stage = stageRepository.save(new Stage());
TicketCreateRequest request = new TicketCreateRequest(stage.getId(), 100);
TicketCreateResponse response = ticketService.createTicket(request);
assertThat(response.amount()).isEqualTo(100);
}
하지만 두 번 이상 호출하게 된다면 문제가 발생한다.
@Test
void 축제에_티켓이_있으면_기존의_티켓에_수량이_더해진다() {
Stage stage = stageRepository.save(new Stage());
Ticket ticket = new Ticket(stage);
ticket.addAmount(100);
ticketRepository.save(ticket);
TicketCreateRequest request = new TicketCreateRequest(stage.getId(), 200);
TicketCreateResponse response = ticketService.createTicket(request);
assertSoftly(softly -> {
softly.assertThat(response.ticketId()).isEqualTo(ticket.getId());
softly.assertThat(response.amount()).isEqualTo(300);
softly.assertThat(ticketRepository.findAll()).hasSize(1);
});
}
// softly.assertThat(ticketRepository.findAll()).hasSize(1);에서 테스트 실패!
테스트가 실패하는데, 결과를 보면 티켓의 개수가 하나가 아닌, 두 개가 반환된다.
분명 작성한 코드는 흐름상 제대로 작성되었다.
하지만 티켓이 두 개가 반환되는 이유는 바로 orElse()
메서드를 사용함에 있다.
orElse()
를 사용하면 파라미터에 있는 값이 즉시 실행되므로, 공연에 대한 티켓이 존재하더라도 새로운 티켓을 생성하고 데이터베이스에 저장하게 된다.
따라서 이 경우에는 다음과 같이 orElseGet()
을 사용해야 한다.
Ticket ticket = ticketRepository.findByStage(stage)
.orElseGet(() -> ticketRepository.save(new Ticket(stage)));
Optional.orElse()와 Optional.orElseGet() 메서드는 Optional에서 값이 없을 때 특정 값을 반환할 때 사용한다.
하지만 서로의 동작이 다르므로 단순히 특정 메서드만 고집하여 사용하기 보다는 마주치는 상황을 고려하여 적절한 메서드를 선택하고 사용해야 한다.