INFLEARN

[스프링 입문] 3. Optimal 타입

ch010104 2026. 3. 3. 19:02

1. 왜 사용하는가?

  • NullPointerException(NPE) 방지: 값이 null일 때 발생하는 치명적인 오류를 막아줍니다.
  • 명시적인 표현: 반환 타입이 Optional<Member>라면, "이 결과는 값이 없을 수도 있으니 반드시 체크해라"라는 강력한 메시지를 개발자에게 전달합니다.

2. 주요 사용 패턴 (1개 조회 시)

보통 결과가 0개 또는 1개인 조회 메서드(findById, findByName)에서 주로 사용합니다.

  • 값이 있을 때만 실행: ifPresent()를 사용하여 값이 존재할 때만 특정 로직(예: 중복 체크)을 수행합니다.
  • 값이 없으면 예외 발생: orElseThrow()를 사용하여 값이 없을 경우 커스텀 에러를 던집니다.
  • 값이 없으면 기본값 반환: orElse()를 사용하여 빈 상자일 때 대신 내보낼 값을 지정합니다.

3. 왜 findAll에는 안 쓰는가?

  • 리스트 자체가 비어있음: List<Member>는 데이터가 없으면 null이 아니라 **빈 리스트([])**를 반환하는 것이 관례입니다.
  • 불필요한 중복: 리스트 자체가 이미 "데이터가 없음"을 표현할 수 있으므로, 굳이 Optional로 또 감쌀 필요가 없어 사용하지 않습니다.

4. 테스트 코드에서의 주의점

  • 상자 열기: Optional은 '상자'와 같아서 안에 든 알맹이(Member)와 직접 비교할 수 없습니다.
  • .get() 사용: 테스트 검증 시에는 .get() 메서드를 호출하여 상자 안의 실제 객체를 꺼낸 뒤 비교해야 합니다.

1. 단건 조회: Optional 사용

결과가 없거나 하나일 때는 Optional로 감싸서 반환합니다.

// 특정 ID로 회원 찾기 (0개 또는 1개)
public Optional<Member> findById(Long id) {
    // store.get(id) 결과가 null이어도 Optional 상자에 담아서 반환하므로 안전합니다.
    return Optional.ofNullable(store.get(id));
}

// 특정 이름으로 회원 찾기 (0개 또는 1개)
public Optional<Member> findByName(String name) {
    return store.values().stream()
            .filter(member -> member.getName().equals(name))
            .findAny(); // 찾은 즉시 Optional로 감싸서 반환, 없으면 빈 Optional 반환
}

2. 전체 조회: List 사용

결과가 여러 개이거나 아예 없을 수 있는 목록 조회는 Optional을 쓰지 않고 빈 리스트를 반환합니다.

// 모든 회원 목록 가져오기
public List<Member> findAll() {
    // 데이터가 하나도 없어도 null이 아니라 빈 리스트([])를 반환합니다.
    return new ArrayList<>(store.values());
}

3. 테스트 코드에서 비교하는 법 (차이점)

테스트 코드(MemoryMemberRepositoryTest)에서 검증할 때 두 방식의 차이를 확인해 보세요.

@Test
void 조회_테스트() {
    Member member = new Member();
    member.setName("spring");
    repository.save(member);

    // 1. Optional인 경우: .get()으로 꺼내야 비교 가능
    Optional<Member> resultOpt = repository.findByName("spring");
    assertThat(resultOpt.get()).isEqualTo(member);

    // 2. List인 경우: 리스트 통째로 비교하거나 사이즈로 검증
    List<Member> resultList = repository.findAll();
    assertThat(resultList).contains(member); // member가 리스트에 포함되어 있는지 확인
    assertThat(resultList.size()).isEqualTo(1); // 개수로 확인
}

💡 핵심 요약

  1. findById, findByName: "회원이 없을 수도 있으니 Optional 상자에 넣어서 줄게. 꺼내 쓰거나(get), 없으면 어떻게 할지 정해(orElse)."
  2. findAll: "데이터가 없으면 **빈 바구니(List)**를 줄게. 굳이 상자에 또 넣을 필요는 없어."