코틀린을 주로 사용하다 보니 Optional을 사용할 일이 적었고, 전 직장에서는 DynamoDB 쿼리를 직접 작성하면서 Kotlin 스타일의 Null 처리를 사용하다 보니 Optional 오브젝트를 다룰 일이 많지 않았는데
Jpa 몇몇 메서드들이 객체를 Optional에 감싸서 반환하는 경우가 있다 보니 한번 정리 해줄 필요를 느꼈습니다.
Optional로 wrapping된 객체의 값이 null인 경우에 orElse(), orElseGet() 두 메서드의 동작의 차이를 정확히 이해하지 않고 사용하면 치명적인 이슈를 발생 시킬 수 있습니다.
var name: String? = null
val orElseResult = Optional.ofNullable(name).orElse("이름")
println(orElseResult) // 이름
val orElseGetResult = Optional.ofNullable(name).orElseGet { "이름" }
println(orElseResult) // 이름
위 코드의 결과는 모두 "이름" 이라는 값이 출력됩니다.
"이름" 이라는 결과를 반환한다는 점에서는 큰 차이가 없어 보입니다.
반면에 아래 코드의 실행 결과는 조금 다른 결과를 보여줍니다.
fun main() {
val item1 = getItemOrElse("아이템1")
println(item1.name)
println("---")
val item2 = getItemOrElseGet("아이템2")
println(item2.name)
}
fun getItemOrElse(name: String): Item {
val item = itemRepository.findByName(name) // return Item(name="아이템1")
return Optional.ofNullable(item).orElse(createDefaultItem())
}
fun getItemOrElseGet(name: String): Item {
val item = itemRepository.findByName(name) // return Item(name="아이템2")
return Optional.ofNullable(item).orElseGet { createDefaultItem() }
}
fun createDefaultItem(): Item {
println("createDefaultItem() method called")
return itemRepository.save(Item(name="기본 아이템"))
}
실행 결과
createDefaultItem() method called
기본 아이템
---
아이템2
getItemOrElse(name) 호출 시 item이 null이 아닌데도 불구하고 createDefaultItem()을 호출하여 아이템 객체를 새로 생성하고 반환하여 기존에 검색한 결과를 덮어쓰게 된 것을 확인할 수 있습니다.
getItemOrElseGet(name) 호출 시 의도한 대로 createDefaultItem() 함수를 호출하지 않고 검색한 item을 그대로 반환하는 것을 확인할 수 있습니다.
왜 이런 결과가 나왔는지 두 메서드의 동작차이를 확실히 알기 위해 상세 구현을 살펴보았습니다.

orElse()메소드는 인자로 null인 경우 반환해야할 객체(값)를 입력받고, wrapping한 객체가 null인 경우 반환한다.
인자로 값을 받고 있기 때문에 createDefaultItem()이 호출된 결과를 인자로 받습니다.

orElseGet()메소드는 인자로 Supplier를 입력받고, wrapping한 객체가 null인 경우 supplier.get()을 호출하여 평가한 결과를 값으로 반환한다.
orElseGet() 메서드의 특징은 인자로 값을 받는것이 아니라 Supplier를 받는다는 것인데, wrapping되어 있는 객체가 null인지 판단하기 전까지는 supplier의 평가를 진행하지 않습니다.
익명함수를 인자로 받고 실제 null인지 판단하기 전까지 지연평가가 일어난다는 점에서 NoSuchElementException()과 같은 에러를 발생시킬때 다음과 같이 사용 했던적이 있던것 같습니다.
val item: Item? = itemRepository.findByName(name) // return null
Optional.ofNullable(item).orElseGet { throw NoSuchElementException() }
위 예제 코드에서는 위와 같이 사용해도 큰 문제는 없지만, orElseThrow()라는 상황에 더 적절한 메소드가 존재하고, 특정 값을 취득 하는 행위가 아니기에 코드 가독성 측면에서 orElseThrow를 사용하는것이 바람직하다고 생각합니다.
Optional.ofNullable(item).orElseThrow { NoSuchElementException("Item Not Found") }
orElse(), orElseGet(), orElseThrow() 세 메소드 모두 null인 경우 처리를 간결하게 제공할 수 있는 메서드 이지만 상황에 따라 정확히 사용을 해야합니다.
어떤 사례에 어떤 메서드를 사용해야하는지 정리해보면 다음으로 정리 할 수 있을것 같습니다.