개요
Spring Data JPA를 활용해 CRUD 연산을 해보신 분이라면 findById 등의 api가 Optional<T>를 반환함을 아실 겁니다. 이 포스팅에서는 Optional은 무엇인지, 왜 사용하는지, 어떻게 사용하는지 알아보겠습니다.
Optional 이란
Optional은 메서드 반환 값으로 사용합니다. "No Result" 결과 없음이라는 값을 표현해야 될 때, 또는 null 반환 시 NullPointException을 야기할 때 사용합니다.
// 1. Person 객체를 직접적으로 참조
Person p1 = new Person();
Person p2 = null;
System.out.println(p1.toString());
System.out.println(p2.toString()); // NPE 발생!
// 2. Person 객체를 Optional을 통해 간접적으로 참조
Optional<Person> opt1 = Optional.of(new Person());
Optional<Person> opt2 = Optional.empty();
opt1.ifPresent(p -> System.out.println(p.toString()));
opt2.ifPresent(p -> System.out.println(p.toString())); // 값이 없어도 NPE 발생 X
옵셔널은 참조 변수를 Optional이라는 클래스로 한번 더 감싼 것입니다. 참조 변수가 객체를 직접적으로 가리키는 대신 Optional<T>를 가리킴으로써 null을 직접적으로 참조할 가능성을 제거합니다. 즉 NullPointerException 발생을 방지합니다.
Optional 사용법
Item 1. 메서드 반환 값으로 사용할 것
위에서 살펴봤던 Javadoc 공식문서의 API Note에 따라 Optional을 메서드 반환 값으로 사용합시다. 따라서 다음에 대해 옵셔널을 사용하지 말아야 합니다.
- 객체 필드
- 생성자 or 메서드 매개변수
- 생성자 혹은 메서드 매개변수로 Optional을 작성할 경우, 메서드를 사용(호출)하는 클라이언트 쪽에서 호출할 때마다 Optional을 생성해서 인자로 넘겨줘야 합니다.
- 메서드 내부에서 null을 허용하지 않는다면, 매개변수로 null이 들어왔는지 아닌지 어차피 검사해야 합니다. 따라서 인자가 null인지 체크하는 책임을 메서드 내부로 옮기는 것이 적합합니다.
- 컬렉션 원소
- HashMap의 Key로 Optional을 사용한다고 가정합시다. 이 경우 맵에 Key가 없다는 사실을 표현하는 방법이 두가지가 됩니다. Key 자체가 null이거나, Key는 있지만 내부 값이 빈 옵셔널인 경우입니다.
- 이렇게 한가지 사실에 대한 표현 방법이 다양해지면, 복잡성이 높아지고 일관성이 깨질 가능성이 생깁니다.
참고로 메서드 반환 타입으로 Optional을 사용하기로 정했다면, 절대로 null을 반환해서는 안됩니다. 이는 옵셔널 도입 취지를 완전히 무시하는 행위입니다.
Item 2. isPresent 대신 orElse 종류를 사용할 것
Optional<T>은 옵셔널 내부에 값이 존재하는지 확인하는 isPresent() api를 제공합니다. 존재한다면 true, 존재하지 않는다면 false를 리턴하는데, 개발자는 이 api를 활용해 각 경우에 따라 다르게 처리할 수 있습니다.
그러나 isPresent로 값 존재 여부를 확인하고 if else로 분기 처리 하기보다, 다음 api를 사용하는 것이 좋습니다. 이는 중간 절차를 줄여 코드 가독성이 높아집니다.
- orElse: 옵셔널 내부에 값이 존재하면 해당 값을 리턴. 존재하지 않으면 지정한 객체를 리턴.
- orElseGet: 옵셔널 내부에 값이 존재하면 해당 값을 리턴. 존재하지 않으면 Supplier 람다식이 제공하는 객체를 리턴.
- orElseThrow: 옵셔널 내부에 값이 존재하면 해당 값을 리턴. 존재하지 않으면 Supplier 람다식이 제공하는 예외를 던짐.
Optional<Person> opt1 = Optional.of(new Person());
Optional<Person> opt2 = Optional.empty();
// 1. isPresent 사용
if(opt2.isPresent()) {
System.out.println(opt2.get());
} else {
System.out.println("Empty");
}
// 2. orElse, orElseGet, orElseThrow 사용
System.out.println(opt2.orElse(new Person())); // Item3: orElse에서 new 연산을 하지 말 것.
System.out.println(opt2.orElseGet(Person::new));
System.out.println(opt2.orElseThrow(RuntimeException::new));
인프런 질의응답에 따르면 findById 호출 시 값이 있다면 가져와 사용하고, 없다면 orElseThrow를 사용해 적절한 예외를 던져 예외처리 한다고 합니다.
Item 3. orElse 대신 orElseGet을 사용할 것
orElse는 옵셔널 내부에 값이 존재하지 않을 경우 매개변수로 준 객체를 반환합니다. 이때 매개변수에 new 연산자 또는 팩토리 메서드 등을 통한 새로운 객체 생성을 피해야 합니다.
메서드 이름 때문에 마치 값이 없을 때만 객체를 생성할 것 같지만, 사실은 그렇지 않습니다. 옵셔널 내부에 값이 있을지라도 우선적으로 새롭게 객체를 생성한 후 무시합니다. 이는 불필요한 객체 생성 비용을 발생시킵니다.
Optional<Person> opt1 = Optional.empty();
Optional<Person> opt2 = Optional.of(new Person());
System.out.println(opt1.orElse(new Person()));
System.out.println(opt2.orElse(new Person())); // opt2에 값이 있음에도 객체를 또 생성함
System.out.println(opt2.orElseGet(Person::new)); // opt2에 값이 있어서 람다식을 호출하지 않음
실제로 그러한지 디버거를 실행해보았습니다. opt2에 이미 Person 객체가 존재함에도 불구하고 orElse 매개변수로 주어진 new Person 연산을 수행하는 것을 확인할 수 있습니다.
orElse 메서드에서 우선적으로 id=30인 Person 객체를 생성하고, 기존 opt2에 존재하던 id=26인 Person 객체를 리턴하는 모습입니다.
반면 orElseGet은 옵셔널 내부에 값이 있다면 매개변수로 주어진 람다식을 실행하지 않습니다. 값이 존재하지 않을 때만 실행합니다.
따라서 orElse를 사용해야 할 경우, 미리 생성된 객체를 반환하도록 사용해야 합니다. 그렇지 않을 경우 orElseGet을 사용해야 합니다.
Item 4. 컨테이너 타입은 옵셔널로 감싸지 말 것
컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안됩니다.
특정 메서드가 DB에서 검색 결과를 리스트 형태로 반환한다고 가정합시다. 만약 검색 결과가 없을 경우 Optional<List<T>> 형태로 반환하면 안됩니다. 검색 메서드를 호출하는 클라이언트는 옵셔널 처리 코드를 작성해야하기 때문입니다.
Spring Data JPA의 findAll 메서드 또한 List를 그대로 반환합니다.
Item 5. int, long, double은 Optional<T>를 사용하지 말 것
Optional에 담기는 값은 객체타입이어야 합니다. int, long, double과 같은 원시타입은 할당할 수 없습니다. 이럴 경우 Optional<Integer> 타입을 사용해야 하는데, 원시타입을 감싼 래퍼타입을 옵셔널로 또 한번 감싸게 됩니다. 기본 타입을 두 겹 씩이나 감싸는 대신, OptionalInt, OptionalLong, OptionalDouble 옵셔널 클래스를 사용합시다.
Reference
'개발 > Java' 카테고리의 다른 글
[Java] 자바 배열 원소를 이어 붙여서 출력하는 4가지 방법 (0) | 2024.05.31 |
---|---|
[Java] Map을 순회하는 3가지 방법 (0) | 2024.05.28 |
[Java] 자바 String 문자열, char 문자를 int 정수로 변환하기 (0) | 2024.05.21 |
[Java] 자바 객체 배열, 리스트 정렬하기 - Comparable vs. Comparator (0) | 2024.05.19 |
정적 팩토리 메서드 Static Factory Method (0) | 2024.04.11 |