우테코 레벨1 웹체스 미션에서 일급 컬렉션에서 특정 요소를 Optional<T>로 꺼내는 메서드를 작성했다. 그런데 해당 요소를 그냥 꺼내는 메서드가 따로 있었고, 이름이 겹치고 싶지 않아 생각하다 find를 붙였다. 레벨2에서 DB를 본격적으로 사용하며 Dao객체에서 데이터 조회를 할 때, findget 중 무엇을 쓸지 고민했다. getgetter처럼 느껴져서 find를 사용했는데, DDDRepository 패턴을 보니 get을 쓰는 경우가 많았다. 그래서 둘의 차이를 찾아봤다.

find vs get

스프링 Data의 CRUD repository 인터페이스는 Optional<T>를 반환한다.

public interface CrudRepository<T,ID> {
    ...
    Optional<T> findById(ID id);
    Iterable<T> findAll();
    ...
}

반면 같은 스프링 Data의 JPA repository에서는 T를 반환한다.

public interface JpaRepository<T,ID> {
    ...
    T getOne(ID id);
    List<T> findAll();
    ...
}

CrudRepository는 찾는 값이 없을 경우 빈 Optional을, JpaRepository는 찾는 값이 없을 경우 예외를 발생한다는 차이가 있다. 여기서 볼 수 있듯이 일반적으로 반환 값이 없을 수 있는 경우 find을, 항상 있어야 하는 경우 get을 붙인다. find를 쓰면 api 사용자에게 이름을 통해 반환값이 없을 수 있음을 알려 줄 수 있는 것이다.
반면, 반환형이 List같은 컬렉션이라면 값이 없어도 null이 아닌 빈 컬렉션을 반환하면 된다.

Optional

cat

대표적 Optional인 슈뢰딩거의 고양이

Optional에 대해 한 번 정리하려 했었는데, find~ 메서드에서 Optional을 쓰는 김에 이펙티브 자바를 참고해 작성했다.

Optional이란

Optional<T>은 null이 아닌 T 참조를 하나 담거나 아무것도 담지 않을 수 있는 클래스이다. 보통은 T를 반환해야 하지만 특정 조건에서는 아무것도 반환하지 않아야 할 때 T대신 Optional<T>를 반환하도록 선언하면 된다. Optional은 정적 팩토리 메서드로 생성할 수 있다.

  • Optional.empty() : 빈 옵셔널 생성
  • Optional.of(T) : T가 든 옵셔널 생성
  • Optional.ofNullable(T) : T가 든 옵셔널 생성, null값이 들어갈 수도 있을 때 사용

그냥 of(T)ofNullable(T)를 구분해 사용하는 것이 중요하다. Optional을 쓰는데는 NullPointerException 체크의 번거로움을 생략하는 목적이 있는데, of(T)null을 넣으면 NullPointerException이 발생해 하나마나가 되기 때문이다.

효과적으로 사용하기

처음 Optional을 알게 되었을 때는 isPresent()isEmpty()로 분기문을 만들어 처리했다.

if (optionalPiece.isEmpty()) {
    throw new IllegalArgumentException("해당 말은 존재하지 않습니다.");
}

그런데 Optional이 제공하는 메서드를 이용해서 효율적으로 처리할 수 있는 방법이 있었다. 위 예시처럼 분기문으로 처리하면 사실 try~catch로 예외처리 하는 것과 다를 바가 없다고 생각한다.

  • orElse로 기본값 정하기
String name = optionalName.orElse("익명");
  • orElseGet으로 기본값 정하기
String name = optionalName.orElseGet(generateRandomName());

orElse에 메서드를 넣으면 Optional안에 실제로 값이 있건 없건 일단 실행되는 것에 유의해서 사용하자. orElseGetOptional에 값이 없어야만 실행된다.

  • orElseThrow로 예외 던지기
String name = optionalName.orElseThrow(IllegalArgumentException::new);

주의사항

  • 컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입을 옵셔널로 감싸기 금지

그냥 빈 컬렉션이나 배열을 사용하는 것이 낫다.

  • 박싱된 기본 타입을 옵셔널로 다시 감싸기 금지

값을 두 겹이나 감싸기 보다 OptionalInt, OptionalLong, OptionalDouble을 사용하자.

  • 컬렉션의 키, 값, 원소나 배열의 원소로 사용 금지

복잡성만 높여서 혼란과 오류 가능성만 키운다.

  • 메서드 파라미터나 인스턴스 필드로 사용 금지

메서드 내에서 옵셔널 분기 처리를 하는 건 생산성이 저하되며, 필드로 쓰기엔 선택형 반환값으로 설계되었기에 직렬화가 구현되지 않았다.

참고자료