[JAVA] Optional 사용법 - 어떻게 사용해야할까 (2/2)
Optional의 존재를 알고 이리저리 사용하면서 여러 의문이 들었다. null의 가능성이 있는 곳엔 모두 써줘야 하는 건가?
결국 꺼낼때는 존재 여부를 확인하고 꺼내야 하는데 이럴 거면 Optional을 왜 쓰는가.. 등의 의문이 들어
Optional의 올바른 사용방법에 대해 알아보았습니다.
잘못된 Optional 사용
Optional은 null을 반환하면 에러를 유발할 가능성이 높은 상황에서 '결과 없음'을 명확하게 드러내기 위해 메서드 반환타입으로
사용되도록 매우 제한적으로 설계되었다고 한다. 즉 의도와 맞지않게 사용한다면 부작용이 발생할 수 있다.
- NullPointerException을 피하려다 NoSuchElementException이 발생
- 코드의 가독성이 떨어지는 문제
- 비용 증가로 인한 성능 저하
등의 문제가 발생할 수 있다.
NoSuchElementException
Optional로 받은 데이터가 null인지 여부를 판단하지 않고 꺼내려고 하면 NoSuchElementException이 발생한다.
Optional<User> user = ....
// user null 이라면 NoSuchElementException
user.get() ....
코드의 가독성이 떨어지는 문제
if 문을 사용한 null 체크보다 되려 가독성이 떨어지는 상황이 발생한다. Optional을 사용함으로 인한 추가적인
확인 로직이 발생하기 때문에 Optional을 남용했다간 되려 가독성이 떨어지는 문제가 발생한다.
비용 증가로 인한 성능 저하
Optional은 객체를 감싸는 컨테이너이기에 객체만 사용했을 때 보다 더 많은 자원을 소비한다.
게다가 직렬화를 지원하지 않기 때문에 이로 인한 추가 작업이 발생할 수 있다.
Optional을 효과적으로 사용하는 방법은?
1. 단순히 값을 얻는 목적이라면 Optional 사용 X
Optional의 자원 소비를 생각하면 단순 값은 null 비교를 쓰는 게 유리하다.
public static String userId(User user) {
// Optional
return Optional.ofNullable(user).map(us -> us.getId()).orElse("null");
if(user.getId() != null) {
return user.getId();
}else {
return "null"
}
}
2. Optional.get() 호출 전에 값이 있음을 확실히 하기
NoSuchElementException을 피하기 위해 .get()으로 꺼내기 전에 데이터의 존재 여부를 반드시 확인하고 사용해야 합니다.
// 나쁜예
Optional<User> user = userRepository.get....
String userName = user.get().getName();
// 개선
Optional<User> user = userRepository.get....
if(user.ifPresent()) {
userName = = user.get().getName();
}
조건문을 통해 존재 여부를 판단하고 사용하면 Optional을 왜 사용할까 라는 생각도 했었지만 Optional을 리턴한다는 자체로 null값의 가능성을 알렸기 때문에 효과가 충분하다고 생각된다.
3. 값이 없을 경우 .orElse(), .orElseGet(), orElseThrow() 적극 활용
값비싼 Optional을 사용하는 만큼 값이 없는 상황을 null을 반환하지 않고 처리해야 합니다.
- 빈 기본값 리턴
- 예외 발생
// 만약 User 객체의 생성 비용이 비싸면 사용하지 말아야한다.
// 공통으로 사용할 객체를 미리 생성해서 사용하는 것이 좋은방법이다.
User user = userRepository.getUser(5).orElse(new User());
// 미리 생성된 객체가 없고 매번 새로운 객체를 반환해야한다면
User user = userRepository.getUser(5).orElseGet(() -> new User());
// 예외 던지기
User user = userRepository.getUser(5).orElseThrow(() -> new NoSuchUserException("회원이 존재하지 않습니다."));
4. Optional는 리턴 타입으로만 사용하기
Optional은 리턴 타입을 설계되었고 직렬화를 지원하지 않기 때문에 메서드의 파라미터, 필드에 선언하는 것은 옳지 않다.
5. 컬렉션에 Optional 사용하지 말기
컬렉션은 이미 빈 값에 대한 처리 메서드를 제공하지 때문에 비싼 비용을 들여 Optional을 사용하는 것은 좋은 처리가 아니다.
// 나쁜예
public Optional<List<User>> getUsers() {
List<User> users = ...;
return Optional.ofNullable(users);
}
// 개선
public List<User> getUsers() {
List<User> users = ...;
return users != null ? users : Collections.EMPTY_LIST;
}
6. 원시 타입에는 OptionalInt, OptionalLong, OptionalDouble 사용
반드시 제네릭 타입에 맞춰야 하는 경우가 아니라면 박싱과 언박싱을 피하기 위해 Optionalxxx 사용을 고려하자
OptionalInt optInt = OptionalInt.of(20);
OptionalLong optLong = OptionalLong.of(30L);
OptionalDouble optDouble = OptionalDouble.of(...);
정리
리턴 데이터가 null일 경우 문제가 발생할 것으로 판단되면 적극적으로 활용하나 비용이 높고 주의점이 많기 때문에 신중하게 사용해야 될 것으로 생각된다. 데이터 액세스 결과를 리턴할 때 사용하면 좋을 것으로 보이는데 특히 Stream과 같이 사용할 요소가 많기 때문에 Stream의 활용법도 숙지해놓자.