본문 바로가기

프로그래밍/Java

Optional 클래스의 orElse와 orElseGet에 대한 정리

이번 글에서는 Java 8 에서부터 지원하기 시작한 Optional 클래스의 orElse와 orElseGet 메소드에 대해서 정리를 해보려한다. 이 글에서는 Optional 클래스가 무엇인지에 대해서는 언급하지는 않고 다만 orElse와 orElseGet 메소드 이 두 개의 메소드에 대해서만 집중해서 보려고 한다.

 

먼저 이 2개의 메소드가 하는 역할은 Optional 클래스 객체가 가지고 있는 실제 값이 null 일경우 무슨 값으로 대체해서 return 해줘야 하는 지를 정의한다. 역할은 같은 역할이지만 사용되어지는 파라미터는 다른데 이 부분은 다음과 같다.

 

T orElse(T other)
T orElseGet(Supplier<? extends T> other)

 

orElse 메소드의 경우 Optional 클래스 사용시 지정했던 Generic 타입(여기서는 T) 클래스 객체를 받아서 그 객체를 그대로 return 하는 구조라면 orElseGet의 경우는 T 클래스 상속받은 하위 클래스를 return 해주는 supplier 메소드를 받아서 T 객체를 return 하는 메소드이다.  메소드의 정의에서 보는 바와 같이 orElse 메소드와 orElseGet 메소드 모두 값이 null일때 Optional 클래스의 Generic 클래스 객체를 return 해주는 것은 동일하다. 단지 그 값을 미리 지정해서 파라미터로 전달해주느냐 아니면 하위 클래스까지 고려하는 supplier 함수 인터페이스를 사용하느냐 이 차이가 존재할 뿐이다.

 

근데 여기까지만 알면 이 두 메소드가 결과론적으로는 동일하기 때문에 어느것을 써도 상관이 없겠지..하고 생각할수 있겠으나 이 두 메소드가 동작하는 시점을 같이 묶어서 놓고 보게 되면 얘기가 달라진다. 이 부분을 결론만 가지고 얘기를 하자면 다음과 같다

 

  • orElse 메소드는 해당 값이 null이거나 null이 아니어도 실행된다
  • orElseGet 메소드는 해당 값이 null 일때만 실행된다

이 문제와 관련하여 기존의 글들을 검색해보면 이런 내용들이 많이 있어서 나도 일단은 이 내용을 차용해서 썼다. 분명 코드에 중단점을 걸고 디버깅을 해보면 이러한 흐름은 감지된다. 다음의 코드는 내가 사용했던 테스트 코드이다.

 

@Test
public void javaOptionalCheck() {
  String checkName = "myname";
  String result = Optional.ofNullable(checkName).orElse(defaultName());
  System.out.println("first result is " + result);

  result = Optional.ofNullable(checkName).orElseGet(this::defaultName);
  System.out.println("second result is " + result);
}

private String defaultName() {
  System.out.println("return defaultName");
  return "default name";
}

 

디버그를 하기 전에 defaultName 메소드의 System.out.println 에 중단점을 걸고 디버그를 들어가게 되면 orElse 메소드나 orElseGet 메소드 모두 defaultName 메소드를 호출하는데도 불구하고 중단점은 한번만 동작하게 되고 실행한 뒤의 결과를 보면 다음과 같이 나온다

 

return defaultName
first result is myname
second result is myname

 

그러나 checkName 변수에 null을 주고 다시 진행해보면 다음과 같은 결과가 나온다

 

return defaultName
first result is default name
return defaultName
second result is default name

 

이러한 결과만 놓고 보면 마치 값이 null 이든 null 이 아니든 orElse 메소드는 매번 동작하고 orElseGet 메소드는 값이 null 일때만 동작하는 것 같아 보인다. 그러나 이것은 잘못알고 있는 것이다. orElse 든 orElseGet 이든 이 두 메소드 모두 null 이든 null 이 아니든 매번 동작한다. 다음의 코드는 Optional 클래스에 정의되어 있는 orElse 메소드와 orElseGet 메소드의 실제 코드이다.

 

public T orElse(T other) {
  return value != null ? value : other;
}

public T orElseGet(Supplier<? extends T> other) {
  return value != null ? value : other.get();
}

 

위의 코드를 살펴보자. 두 코드 모두 value가 null 이 아니면 value 자체를 return 하고 있다. 또 이 두 메소드의 javadoc을 살펴보면 값이 null 이 아닐경우 값을 return 한다고도 언급되어 있다. 때문에 값이 null 이든 null 이 아니든 orElse나 orElseGet 모두 실행되는 것이다. 값이 null 이 아닐때 값을 return 하고 있으니까..이러한 정의때문에 orElse나 orElseGet 메소드만 사용해도 값이 null 이 아닐때 값을 return 할 수 있게 되는 것이다.

 

그러면 왜 맨 처음 테스트를 진행했을때 값이 null이 아닌데도 불구하고 defaultName 메소드가 실행된 것일까? 그것은 우리가 orElse 메소드에 파라미터로 넘길때 값을 넘긴게 아니라 값을 return 해주는 메소드를 넘겼기 때문이다. 이 부분을 이해할려면 우리는 orElse 입장에서 빙의(?)가 되어볼 필요가 있다. 이제 우리가 orElse 입장이라 생각하고 다음의 내용을 보자. orElse 메소드의 파라미터에 내가 실제 값을 넣었다고 가정하면 아마 실제 내부 구현코드는 다음과 같이 동작할것이다

 

public T orElse("return defaultName") {
  return value != null ? value : "return defaultName";
}

 

orElse 메소드의 파라미터에 내가 실제 값을 파라미터로 받았기 때문에 orElse 메소드의 정의에 맞춰서 위와 같이 동작할 수 있다. 그러나 내가 실제 값이 아닌 값을 return 하는 메소드인 defaultName() 메소드를 파라미터로 주었다고 가정해보자. 그러면 어떻게 동작할까?

 

public T orElse(defaultName()) {
  return value != null ? value : defaultName();
}

 

이렇게 동작할까? 아니다. orElse 입장에서는 defaultName 메소드는 그 자체가 값이 아니라 값을 return 해주는 메소드이다. 그렇기때문에 orElse 메소드에서 defalutName 메소드를 먼저 실행시켜서 defaultName 메소드가 return 한 값을 orElse 메소드 정의에서 파리미터로 정의된 T other로 사용해야 하는 것이다. 먼저 실행시켜야 할 필요가 생기는 것이다. 그래서 동작 순서를 정의하면 다음과 같은 순서로 진행된다고 보면 된다(위에서 써놨던 orElse 메소드 코드와 같이 보면 이해하기 쉬울꺼라 생각한다)

 

  1. 파라미터로 들어온 defaultName 메소드를 실행시켜서 메소드가 return 한 값을 실제 파라미터로 삼는다(defaultName 메소드가 return 한 값 = orElse 메소드의 파라미터로 정의된 T other)
  2. return value != null ? value : other 부분에서 other 부분이 1번에서 구했던 T other가 된다

이러한 순서로 전개되기 때문에 orElse 메소드에서 파라미터로 받은 메소드가 값이 null 이어도 실행이 되는 것이다. 그러면 orElseGet 메소드에서 파라미터로 받은 메소드는 왜 실행이 되지 않는 것일까? 그것은 메소드가 실행이 되는 시점이 value가 null 일때 실행이 되기 때문이다. orElse 같이 파라미터로 값을 받는 것이 아니라 그 값을 return 해주는 메소드 자체를 파라미터로 받기 때문에 파라미터로 받은 시점에 실행이 되는 것이 아니라 값이 null 인지 아닌지 판단하는 시점에 실행이 되는 것이다.

 

지금까지의 설명이 잘 이해가 되지 않으면 위에서 언급했던 orElse 메소드와 orElseGet 메소드의 실제 코드를 봐가면서 설명 내용을 하나하나 맞춰보면 이해하기 쉬울것이다. 정리하자면 orElse 메소드의 파라미터로 메소드를 넣으면 orElse 메소드의 파라미터 정의가 메소드가 아닌 값이기 때문에 해당 값을 구하기 위해 먼저 메소드를 실행시켜 값을 구한뒤에 구체적인 orElse 메소드의 내부 구현을 실행하게 되는 것이라 보면 된다.

 

그러면 이러한 흐름에 있어서 주의할 점이 없을까? 아니다. 이걸 제대로 알고 사용하지 않으면 큰 문제로 발전할 소지가 다분히 있다(어떤것이든 그렇기는 하지만..) orElse 메소드에서 우리가 값을 직접 넣으면 사전에 막을수 있는 문제이지만 값이 아닌 메소드일 경우엔 아무 생각없이 지나칠수도 있기 때문이다.

 

일례를 들어보자. 값이 null 일 경우 대체가 되는 값을 생성하고 그에 따른 기록을 남기는 작업이라고 가정해보자. 이러한 작업을 실행하는 메소드(여기서는 testjob 이라는 이름의 메소드라 가정해보자)를 하나 만들고 이러한 메소드를 orElse에서 사용한다면 다음과 같이 될 것이다.

 

String result = Optional.ofNullable(checkName).orElse(testjob());

 

위에서 썼던 샘플코드를 그대로 써보았다. 그러면 생각해보자. checkName 변수가 null 일 경우엔 문제가 없다. testjob 메소드가 하는 일과 맞춰서 생각해보자. checkName 변수가 null 이면 testjob 메소드가 그에 따른 기록을 남긴뒤 대체되는 값을 return 할테니까 전혀 문제가 없다. 그러나 이 변수가 null이 아니라고 가정해보자. 위에서 설명한대로 testjob 메소드가 먼저 실행된다. 먼저 실행이 되어서 그에 따른 기록을 남기고 대체되는 값이 파라미터로 넘어갈 것이다. 그러나 값이 null 인지 아닌지 비교하는 과정에서 null 이 아니기 때문에 testjob 메소드가 return 한 대체되는 값이 아닌 checkName 변수 값을 그대로 사용할 것이다. 그러면 여기서 문제점이 무엇일까? checkName 변수가 null 이 아니어서 기록을 남길 필요가 없는데도 불구하고 testjob 메소드가 먼저 실행되어 기록을 남기게 된다. 문제가 없는 상황인데도 문제가 있다..라고 기록하게 되어서 앞뒤가 맞지 않는 상황이 오게 되는 것이다. 메소드를 실행하는 시점이 값이 null 인지 아닌지를 비교하는 시점에 실행하는게 아니라 파라미터로 받은 시점에 먼저 실행이 된 뒤 그 다음에 비교를 하기 때문에 이러한 문제가 나타나는 것이다.

 

이해하기 쉽게 하기 위해 단순히 기록을 남긴다..는 식으로의 작업으로 설명했지만 만약 이게 기록을 남기는게 아니라 JPA를 사용하면서 Entity 객체가 null 일 경우 대체되는 이를 return 해주는 작업..이라고 바꿔서 생각해보자. 검증해야 되는 변수(Entity 객체)가 null 일 경우엔 문제가 없다. 대체되는 Entity 객체를 생성한뒤에 이를 사용하는거니까..그러나 null 이 아닐 경우 어떻게 될까? 대체되는 Entity 객체를 먼저 만들기는 하지만 최종적으로는 null 이 아니기 때문에 기존 Entity 객체를 그대로 사용할 것이다. 그러나 JPA의 PersistenceContext 에서는 대체되는 Entity 객체가 이미 생성되어 있는 상태이기 때문에 이 상태에서 작업을 마치게 되면 자동으로 flush가 되면서 Entity 객체와 관련된 테이블에 레코드가 하나 만들어지게 되는 것이다. Entity 객체 비교작업을 10번 하면서 10번 모두 null 이 아니면 대체되는 Entity 객체 10개가 만들어지지만 실제로 이를 사용하지는 않게 되고 PersistenceContext에 있는 이러한 Entity 객체들은 flush 과정을 거쳐서 자동으로 DB 테이블에 10개의 레코드가 만들어지게 된다. 이렇듯 불필요한(실제로는 Dummy 이며 버그인) 레코드가 생성되는 것이다.

 

이러한 상황을 막을려면 자기가 무슨 작업을 거쳐서 대체되는 값을 구하는 것인지를 올바르게 파악할 필요가 있다. null일 경우 대체되는 값을 구하기 위해 다른 자원을 이용(이것도 이용하는 과정에서 또 다른 자원을 생산할 경우 문제가 있는지에 대한 판단도 해야 할 필요가 있다)하는 정도라면 문제는 없다. 그러나 대체되는 값을 구하기 위해 다른 자원을 따로 만들어야 하거나 수정, 또는 삭제를 해야 한다면 그때는 orElse가 아닌 orElseGet을 사용해서 먼저 비교대상 변수가 null 인지 아닌지를 먼저 판단하게 하는 과정을 거친뒤에 관련 메소드를 실행하게끔 해야 할 것이다. 이런거 저런거 다 복잡하면 orElse에는 파라미터로 대체되는 값을 구하는 메소드가 아닌 대체되는 값을 직접 파라미터로 사용하고 orElseGet 에서는 대체되는 값을 구하는 메소드를  파라미터로 사용하면 된다. 

 

물론 orElse에서 직접 파라미터로 사용해도 문제가 아주 없는 것은 아니다. 다음의 코드를 보자

 

MyEntity testValue = testJob();
MyEntity result = Optional.ofNullable(checkEntity).orElse(testValue);

 

위에서 언급했던 Entity 관련 내용을 testJob 메소드가 한다고 가정해보자. MyEntity 라고하는 Entity 클래스가 있다고 가정하고 testJob 메소드가 위에서 언급했던 대체되는 Entity 클래스 객체를 return 한다고 보자. checkEntity 라는 변수가 null 인지 아닌지를 체크해서 null 이면 testValue 변수를 사용하게 되는 것이다. 만약 checkEntity 변수가 null 이 아니라고 보자. 그러면 checkEntity 변수를 result 변수가 그대로 받을 것이다. 그러나 이 과정에서 testJob() 메소드를 통해 testValue 변수에 들어가 있는 Entity 객체가 JPA의 PersistenceContext에 들어가기 때문에 PersistenceContext의 내용들이 flush 되면 testValue 엔티티 객체로 인한 레코드가 DB 테이블에 생성이 되는 것이다. 이렇듯 orElse에 값을 입력해도 그 값을 구하는 과정에서 이러한 의도치 않은 동작이 발생하는 것이다. 다만 이러한 과정을 별도의 메소드를 사용하지 않고 이렇게 밖으로 드러나게 코딩하면 문제점을 찾기가 그나마 쉽다. 그러나 이것을 별도의 메소드로 만들면 orElse의 동작 원리를 제대로 알지 못할 경우 이러한 문제점을 찾기가 쉽지가 않다.

 

orElse 나 orElseGet 이나 모두 잘 만들어진 메소드라 생각한다. 다만 이것을 사용하는데 있어서 단순히 값이 null 이든 null 이 아니든 orElse는 동작한다..라는 식의 생각으로 메소드를 이해해버리면 방금 얘기한 이러한 문제의 동작을 찾기 어렵게 된다. 작업을 캡슐화해서 별도의 메소드로 만들어 사용하는 것이 자칫 이러한 의도치 않은 동작이 나타나는것을 찾기 어렵게 할 수도 있다. 주의해서 사용하길 바라는 마음에서 이 글을 써보았다.