database by narae :p

Optional 클래스 사용하기 본문

개발 노트

Optional 클래스 사용하기

dbbymoon 2019. 5. 16. 01:34

스프링 데이터 JPA를 사용하며 CrudRepository의 findById 메서드 리턴 타입인 Optional 클래스에 처음 접하게 되었습니다. Optional은 Java 8에 추가된 새로운 API로 이전에 하던 '고통스러운 null 처리'를 '잘' 다룰 수 있게 도와주는 클래스라고 합니다. 

 

저는 Optional 클래스를 처음 접하게 되며, "대체 이런걸 왜 쓰는 거야?" 라는 의문을 강하게 품고 있었습니다. 아마 그 이유는 제가 Optional을 Optional답게 사용하지 못했던 것 때문이라고 생각합니다.

 

NullPointerException

Null 처리를 돕는 Optional 클래스에 대한 글이기 때문에 NullPointerException에 대해 먼저 이야기하며 시작하겠습니다.

class Aclass {
	String str;
    
    String getStr() {
    	return str;
    }
}

위와 같이 Aclass가 있고, 다음 코드의 getStr 메서드를 통해 str 변수에 접근하려고 합니다. 

Aclass a = new Aclass();
System.out.println(a.getStr());

아마 이 코드를 실행하게 된다면 NullPointerException이 발생하고 프로그램이 종료될 것입니다. 그 이유는 현재 null인 str 변수를 참조하려고 하기 때문입니다. 이러한 실행 중단은 실제 서비스에 있어 치명적인 상황을 불러 일으킬 것입니다. 따라서 이렇게 NullPointerException이 발생할만한 상황을 예측하여 저희는 if문이나 try catch문 등을 사용하며 null 처리를 해왔습니다.

 

예를 들면 이렇게 한다거나, 

// 조건문을 이용한 Null 처리
class Aclass {
	String str;
    
	String getStr(){
    	if(str!=null) {
        	return str;
        }
        return "str is null";
 	}
}

이런 식으로 할 수도 있습니다.

// try catch 문을 이용한 null 처리
try {
	System.out.println(a.getStr());
} catch (NullPointerException e) {
	System.out.println("str is null");
}

 

이 방법은 NullPointerException이 발생할만한 모든 상황을 예측해야 하며, if문 등을 통한 null 처리는 핵심 로직과 관련된 코드의 가독성을 해칠 수 있다는 점입니다.

 

 

 

 

 

Optional<T> findById(ID id)

CrudRepository의 findById를 살펴보는 것으로 Optional에 대해 간단하게 살펴보겠습니다.

findById는 Entity의 id로 검색을 해서, Optional<T>를 리턴 타입으로 반환하는 메서드입니다. 따라서 다음과 같이 isPresent() 를 이용하여 Optional 내부에 객체가 들어있는지 확인한 후에 get()을 이용해 반환된 객체를 꺼내 쓸 수 있습니다.

Optional<ExEntity> optional = exRepsoitory.findById(id);
if(optional.isPresent()) {
	ExEntity exEntity = optional.get();
} else {
	throw new Exception();
}

 

여기에서 의문이 들었습니다. 기존에 Null 체크를 할 때와 큰 차이를 느끼지 못했기 때문입니다. 오히려 Optional 타입의 변수를 하나 더 두어야 한다는 것에 대해 이상하다고 생각했습니다. 기존이라면 이렇게 하는데 말입니다.

ExEntity exEntity = exRepository.findById(id);
if(exEntity != null) {
	// null이 아닐 때 
} else {
	// null일 때
}

 

 

그래서 저는 실제로 이런 코드를 작성하였습니다.

이런 코드를 작성해본 다음에야 Optional 클래스에 대해 살펴보았습니다. 모르는 것을 사용하게 되면 바로 그 자리에서 어떻게 구성되어 있고 어떻게 사용하는지 미리 보아야 했을텐데 굳이 이런 과정을 거친 후에 찾아봤다니 ..

앞으로는 바로 알아보는 습관을 더욱 가지도록 노력해야겠습니다.

 

 

 

Optional 클래스란?

Optional이란 'null일 수도 있는 객체'를 감싸는 일종의 Wrapper 클래스입니다.

Optional<T> optional

즉, 이러한 optional 변수 내부에는 null이 아닌 T 객체가 있을 수도 있고 null이 있을 수도 있습니다. 따라서, Optional 클래스는 여러 가지 API를 제공하여 null일 수도 있는 객체를 다룰 수 있도록 돕습니다.

 

1 ) Optional 객체 생성

- Optional.empty() : 비어있는 Optional 객체로 생성합니다. 따라서 이 내부에 있는 Member 객체는 null입니다. 그리고 이 객체는 Optional 내부적으로 미리 생성해놓은 Singleton 인스턴스입니다.

Optional<Member> optMember = Optional.empty();

- Optional.of(value) : 인자 value를 담고 있는 Optional 객체로 생성합니다. 이 객체는 미리 만들어 놓은 null이 아닌 객체를 넘기면 되는데, 만약 인자로 넘긴 객체가 null이라면 NullPointerException이 발생할 수 있습니다.

Optional<Member> optMember = Optional.value(member);

- Optional.ofNullable(value) : null일 수도 있고 아닐 수도 있는 객체를 담아 Optional 객체를 생성합니다. 만약 member가 null인지 null이 아닌지 확신이 서지 않는다면 이 방법으로 Optional 객체를 생성하면 됩니다.

Optional<Member> optMember = Optional.ofNullable(member);

 

2 ) Optional 객체 접근

- get() : Optional 내부에 담긴 객체를 반환합니다. 만약, null인 객체라면 NoSuchElementException이 발생합니다. 따라서, isPresent() 로 체크한 후에 이 get 메서드를 사용했습니다.

Member member = optMember.get();

- orElseThrow(Supplier<? extends X> exceptionSupplier) : Optional 내부에 담긴 객체가 null이 아니라면 담겨 있는 객체를 반환하고, null이라면 인자로 넘겨준 함수형 인자를 통해 생성된 예외를 발생시킵니다.

Member member = optMember.orElseThrow(NullPointerException::new);

- orElse(T other) : Optional 내부에 담긴 객체가 null이 아니라면 담겨있는 객체를 반환하고, null이라면 인자로 넘겨준 member1이라는 객체를 반환합니다. 

Member member = optMember.orElse(member1);

- orElseGet(Supplier<? extends T> other) : Optional 내부에 담긴 객체가 null이 아니라면 담겨있는 객체를 반환하고, null이라면 인자로 넘겨준 함수형 인자를 통해 생성된 객체를 반환합니다.

 

 

 

 

 

* getOrCreate 메서드 작성을 하며 이해하게 된 orElse와 orElseGet의 차이

 

이 메서드들을 이용하여 조건문 없이 좀 더 깔끔하게 null일 수도 있는 객체를 제어할 수 있었습니다. 글 내용이 Optional 클래스 소개로 이어졌지만, 이 글을 쓰게 된 진짜 이유는 바로 위에 소개된 orElse와 orElseGet을 사용하면서 글로 정리하고 싶다는 생각이 들었기 때문입니다.

 

이 API를 이용하여 다음과 같이 getOrCreate 기능을 하는 메서드를 작성하고자 하였습니다.

PfmEntity getOrCreatePfm(String pfmId)

get : id에 해당하는 객체가 존재한다면 가져오고

create : 존재하지 않는다면 새로 객체를 생성하여 가져온다.

 

처음 저는 orElse와 orElseGet의 차이를 인지하지 못하고 orElse를 이용해서 다음과 같이 코드를 작성했습니다. (지금 생각하면 레퍼런스를 조금 더 꼼꼼히 볼 걸 그랬다고 느낍니다 ㅠ)

 

1 ) orElse(T other)

제가 의도한 것은 findByUserId를 통해 userId에 해당하는 PfmEntity를 조회하고 존재한다면 pfmEntity에 넣고, 존재하지 않는다면 repository.save()를 통해 pfmEntity를 저장하여 반환하는 것이었습니다.

 

getOrCreate 메서드를 '한 번' 수행하니 다음과 같이 잘 insert 되고, 잘 반환되었습니다.

 

 

 

테스트를 위해 한 번 더 수행했습니다. '두 번째' 호출에 있어 첫 번째 생성된 pfm을 반환하였으니 잘 동작하구나라고 생각했습니다.  

하지만 데이터베이스를 확인해보니 다음과 같이 동일한 userId로 두개의 pfm이 insert 되었습니다.

 

어이 없어서 한 번 더 호출해봤습니다 ㅎㅎ 세 번째 호출입니다.

반환하지 않고 읽고 씹네요 ㅠ_ㅠ 

하지만 뒤에서는 NonUniqueResultException이라는 에러가 발생하였습니다. Pfm 데이터베이스에 접근하는 PfmRepository에 저는 Optional<PfmEntity> findByUserId 의 쿼리 메서드를 작성하였고, userId는 unique하기 때문에 하나의 데이터만 반환할 것으로 생각하며 Optional<PfmEntity> 타입으로 리턴하도록 했습니다. 그런데 동일한 userId로 두 개의 pfm이 insert 되어 있는 상태에서 조회하려고 하니, unique한 결과를 반환할 수 없기 때문에 발생한 에러입니다.

 

orElse의 동작을 살펴본다면 다음과 같습니다.

// orElse를 사용한 코드
PfmEntity pfm = pfmRepository.findByUserId(userId)
			.orElse(pfmRepository.save(new PfmEntity(userId));

// orElse의 실제 동작
PfmEntity newPfm = pfmRepository.save(new PfmEntity(userId));
PfmEntity pfm = pfmRepository.findByUserId(userId)
			.orElse(newPfm);

즉, orElse에 인자로 넘겨준 객체를 '미리' 준비해 놓은 뒤에 만약 Optional에 담긴 객체가 null이라면 준비해 놓았던 객체를 반환하는 것입니다.

이 코드에서 다시 한번 설명하자면, save를 미리 해서 준비해 둔 newPfm이 있습니다. findByUserId를 통해 조회한 객체가 없다면 준비해 둔 newPfm을 반환합니다. 그리고 findByUserId를 통해 조회된 객체가 있다면 준비해 둔 newPfm은 버려질 것입니다.

하지만, 미리 준비해놓기 때문에 이미 save 메서드는 수행이 되었고 DB에는 insert된 상태인 것입니다.

 

반면, orElseGet은 orElse와 비슷하지만 좀 더 게으른 동작을 한다고 합니다. 그래서 orElseGet을 사용해보았습니다.

 

 

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

orElseGet은 다음과 같이 사용할 수 있습니다.

PfmEntity pfm = pfmRepository.findByUserId(userId)
			.orElseGet(() -> pfmRepository.save(new PfmEntity(userId));

Optional에 담긴 객체가 null일 때 반환하려는 객체를 미리 준비해 두는 orElse와 달리,

orElseGet은 함수형 인자를 넘겨주어 Optional에 담긴 객체가 null인 것을 알게 되었을 때가 되서야 반환할 객체를 생성하여 반환합니다.

orElseGet을 사용하지 않고 코드를 작성한다면 다음과 같습니다.

Optional<PfmEntity> optPfm = pfmRepository.findByUserId(userId);
PfmEntity pfm;
if(optPfm.isPresent()) {
	pfm = optPfm.get();
} else {
	pfm = pfmRepository.save(new PfmEntity(userId));
}

 

따라서, getOrCreate에 맞는 기능을 수행하기 위해서는 orElseGet을 사용해야 제가 의도한대로 동작하였습니다. 그래서 다음과 같은 코드를 작성하여 기능을 구현할 수 있었습니다.