Spring Data JPA에서 엔티티를 찾을 때 기본적으로는 Optional로 반환하는 것을 알 수 있는데, 쿼리 메소드를 직접 만들거나 QueryDsl을 사용하여 메서드를 생성할 때 반환값을 값(DTO, Entity)으로 반환할지 Optional로 반환할지 기준이 서지 않았다. 그러다보니 NPE를 발생시키지 않으면서 Optional을 불필요하기 사용하지 않을 확실한 기준을 세우고 싶었다.
Optional<T> findById(ID id); // Spring Data JPA가 만들어준 기본 메서드
List<MemberSimpleResponseDto> getCurrentMembers(Set<String> currentMemberIdSet); // 내가 만든 Querydsl 메서드
Optional<MemberDetailResponseDto> getMemberDetail(String memberId); // 내가 만든 Querydsl 메서드
또한 반환값이 Optional이어도 이를 get 메서드로 가져와서 사용했었는데, Optional 안의 값이 null이면 에러가 발생하는 것은 기존과 동일했고 심지어 이는 NPE가 아닌 NoSuchElementException 이었다. 결국 해당 에러가 발생했을 때의 처리를 따로 해줘야했는데..
분명 Optional은 null이 될 수 있는 값을 보다 효과적으로 처리하기 위한 장치임은 알고있었기에 뭔가 잘못 사용하고 있다는 생각을 지울 수 없었다..ㅎㅎ 때문에 모던 자바 인 액션의 Optional 섹션을 정리해보고 해당 프로젝트에 적용한 내용을 작성해보았다! 🏋️🏋️🏋️
프로젝트에서 사용한 Repository 메서드를 보면서 적용해보자
Optional<T> findById(ID id); // 1
List<MemberSimpleResponseDto> getCurrentMembers(Set<String> currentMemberIdSet); // 2
Optional<MemberDetailResponseDto> getMemberDetail(String memberId); // 3
findById 에서 Optional을 사용했다는 것은 해당 메서드로 엔티티(T)를 찾았을 때 그 값이 null일 수 있다는 뜻이다. 따라서 이에 대한 특정 예외를 던지거나 기본값을 넣어주는 처리 등을 해주어야 한다
getCurrentMembers 에서 Optional을 사용하지 않았다는 뜻은 해당 메서드로 조회할 때 반드시 반환값이 존재한다는 뜻이다. 따라서 null이 발생했다면 이는 로직상에 문제가 있다는 뜻으로 null이 발생되지 않도록 디버깅해야한다.
getMemberDetail도 findById와 마찬가지이다. 반환값이 null일 수 있음으로 이에 따른 처리를 해주면 된다.
스트림의 연산에서 큰 영감을 받았다. 따라서 스트림의 map 등의 메서드를 지원한다!
public final class Optional<T> {
private static final Optional<?> EMPTY = new Optional<>(null);
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
}
Optional은 map 함수를 지원한다!
마찬가지로 flatMap도 지원한다
map과 flatMap의 차이를 예시에서 더 자세히 살펴보자
첫 코드는 컴파일 되지 않고, 아래 코드는 컴파일에 성공한다. 이유는 무엇일까??
Optional<String> name =
Optional.of(person).map(Person::getCar) //Optional<Optional<Car>>
.map(Car::getInsurance) // 컴파일 X
.map(Insurance::getName);
Optional<String> name =
Optional.of(person).flatMap(Person::getCar) //Optional<Car>
.flatMap(Car::getInsurance) //Optional<Insurance>
.map(Insurance::getName) //Optional<String>
.orElse("UnKnown"); // 컴파일 O
getCar는 원래 Optional 객체를 반환한다
따라서 map(Person::getCar)에서 Optional<<Optional>를 반환하게 되어, getInsurance는 반환값을 받을 수 없다
이와 달리 flatMap은 2차원 Optional을 1차원으로 평준화해준다
flatMap을 빈 Optional에 호출하면 아무 일도 일어나지 않고 그대로 반환되나 값을 감싸고 있다면 그 값에 주어진 함수가 적용된다
대표적으로 사용하는 Optional 처리 메서드이다. 간단히 메서드를 설명하고 이를 활용한 예시를 기술하겠다.
get( T )
orElse( T )
orElseGet( supplier )
orElseThrow( supplier )
ifPresent( consumer )
멤버를 삭제하는 서비스 메서드가 있다고 생각해보자. 사용자는 존재하지 않는 멤버를 삭제하라는 명령을 보낼 수도 있다.
이 때 삭제하려는 멤버가 존재하는지 확인을 하고 삭제하는 것은 쿼리를 2번 발생시킴으로 매우 비효율적이다.
그렇다면 findById
에서 null
을 가져올 수도 있다는 뜻인데, 만약 findById
를 그냥 get
으로 가져오게 되면 NoSuchElementException
에러가 발생하는데, 이는 다른 엔티티들을 찾지 못했을 때도 모두 동일한 에러가 발생함으로 우리는 에러만으로 어떠한 이유로 에러가 발생하였는지 알기 어렵다.
따라서 orElseThrow
사용자 지정 Exception (RestApiException) 을 던지도록 하여 간결하고 명확하게 오작동한 이유를 기술할 수 있다.
@Transactional
public void delete(String memberId) {
Member member = memberRepository.findById(memberId).orElseThrow(() -> new RestApiException(CustomErrorCode.NO_MEMBER));
member.delete();
roomMemberRepository.deleteAll(member.getRoomMembers());
}
멤버의 팔로잉 수, 팔로워 수를 가져오는 쿼리 2개를 응답 dto에 넣어주는 private 메서드이다.
Querydsl에서 fetchOne
한 값을 Optional.ofNullable
로 감싸주어 NPE를 방지해주었다.
만약 팔로워, 팔로잉이 존재할 때만 값을 설정해주기 위해 ifPresent
를 사용하였다.
private void setFollowCount(MemberDetailResponseDto res, String memberId) {
Optional<Long> followerCount = Optional.ofNullable(jpaQueryFactory.select(follow.to.count())
.from(follow)
.where(follow.to.memberId.eq(memberId))
.fetchOne());
Optional<Long> followingCount = Optional.ofNullable(jpaQueryFactory.select(follow.from.count())
.from(follow)
.where(follow.from.memberId.eq(memberId))
.fetchOne());
followerCount.ifPresent(count -> res.setFollowerCount(count.intValue()));
followingCount.ifPresent(count -> res.setFollowingCount(count.intValue()));
}
모던 자바 인 액션에 들어있는 예시였는데 흥미로워서 정리해보았다. person과 car가 둘다 null이 아닐 때만 myMethod라는 메서드가 실행되게 하려면 어떻게 해야할까? 물론 if(person != null && car != null)
으로 처리할 수도 있겠지만 Optional을 사용해보는건 어떤가! Optional의 Stream한 연산을 사용해보면 그렇게 어렵지 않다. 모던 자바 인 액션에서 제시하는 방법은 flatMap & map 활용하는 것이다.
Optional<Person> person;
Optional<Car> car;
person.flatMap(p -> car.map(c -> myMethod(p,c)));
Optional은 매우 Stream하다! stream 처럼 filter를 사용할 수 있음으로 활용해보자.
optional에 값이 없다면 filter는 아예 작동하지 않는다
filter를 거치고 남은 값을 활용할 수 있을 것이다!
만약 이름이 John인 사람이 있다면 그 이름을 출력한다고 하면?
Optional.of(person).filter(person -> "John".equals(person.getName()))
.ifPresent(System.out::println);
null이 발생할 수 있는 값을 if-else-then 대신 ofNullable을 이용하여 깔끔하게 처리하자
Optional<Object> value = Optional.ofNullable(map.get("key"));
어떤 값이 null일 수 있고, null이거나, 값을 int로 변환하는데 실패하거나, 값이 없으면 0을 반환해야 한다면?
아래와 같이 함수형 프로그래밍으로 할 수도 있다!
return Optional.ofNullable(props.getProperty(name))
.flatMap(OptionalUtility::stringToInt) // Optional 값이 int로 바뀔 수 있으면 파싱, 안되면 empty 반환하는 "사용자" 메서드
.filter(i -> i > 0)
.orElse(0);
참고자료
모던 자바 인 액션