Optional.orElse() vs Optional.orElseGet()

Glen·2023년 7월 29일
0

배운것

목록 보기
19/37

서론

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

orElse() 메서드를 사용하면 Optional이 값을 포함하지 않을 때 기본 값을 제공한다.

하지만 orElse() 메서드의 파라미터로 받는 값은 Optional의 값의 유무에 상관 없이 파라미터로 전달된다.

orElseGet

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() 메서드를 사용하는 것이 좋다고 생각될 수 있다.

하지만 orElseGet()은 인자로 함수형 인터페이스를 받기 때문에 가독성이 좋지는 않다.

// orElse()
String value = empty.orElse("default value"); 

// orElseGet()
String value = empty.orElseGet(() -> "default value"); 

게다가 문자열 같은 상수는 이미 생성이 된 객체이므로 굳이 게으르게 처리할 필요가 없다.

그렇다면 언제 orElseGet()을 사용해야 할까?

다음과 같은 클래스와 비즈니스 로직이 있다.

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에서 값이 없을 때 특정 값을 반환할 때 사용한다.

하지만 서로의 동작이 다르므로 단순히 특정 메서드만 고집하여 사용하기 보다는 마주치는 상황을 고려하여 적절한 메서드를 선택하고 사용해야 한다.

profile
꾸준히 성장하고 싶은 사람

0개의 댓글