Java Optional, 도대체 왜 쓰는거야? (Feat. NPE 피하는 방법)
Java에서 Optional을 사용하게 되면, 어떤 메서드가 null을 반환할지 확신할 수 없거나, null 처리를 놓쳐서 발생하는 예외를 피할 수 있습니다.
예를 들어, 데이터베이스에서 특정 항목을 찾는 메서드를 호출할 때, 결과가 없을 수도 있습니다.
이때 Optional을 사용하면 코드를 더 명확하게 작성할 수 있고, 예외 처리를 더 쉽게 할 수 있어 코드 가독성이 높아지며, 유지보수도 더 편리해집니다.
만약 Optional이 없다면, 프로그램에서 더 자주 NPE(NullPointerException)가 발생하게 되고, 이로 인해 프로그램이 중단되는 등 사용자에게 문제를 야기할 수 있습니다.
그래서 Optional을 사용할 줄 아는 개발자와 아닌 개발자의 작업 효율에서 큰 차이가 발생합니다.
이랜서를 사용하는 개발자들의 업무 시간 단축을 위해 Optional의 올바른 사용 방법에 대해서 자세히 알려드리겠습니다.
Java Optional이란?
Optional은 Java8 버전부터 도입되었으며, ‘값이 없는 경우’를 표현하기 위한 용도로 사용되는 클래스입니다.
Optional 클래스는 Java 제네릭을 사용하여 만들어져 있으므로, 어떤 타입의 객체라도 값이 없을 수 있는 경우에 Optional을 사용하여 표현할 수 있습니다. 그러면, Optional이 왜 만들어졌고, 어떨 때 사용하면 되는지 알아보겠습니다.
Java Optional 사용목적
Optional은 메서드의 리턴 타입으로 ‘결과 없음’을 명확히 표현하는 용도로 제한적으로 사용하기 위한 용도로 제공됩니다.
Java8전에는 Optional이라는 클래스가 없었으므로, 메서드의 리턴 타입으로 ‘결과 없음’을 표현하기 위한 용도로 null이 사용되었습니다.
그러나 객체 값이 null인 경우에, null 체크 없이 해당 객체의 메서드를 호출하거나 하는 등, 부주의하게 사용하는 경우에 ‘NPE(NullPointerException)’이 발생하면서 프로그램이 죽어버리는 상황이 발생했습니다.
이런 상황을 방지하고 안전한 코딩을 위해서 Optional이 만들어지게 되었습니다.
null 값을 반환해야 하는 경우에 Optional로 감싼 객체로 empty 값을 표현하여 결괏값을 넘겨주면, null로 인한 오류 문제를 없앨 수 있습니다.
Java Optional을 사용했을 때의 이점
Optional은 ‘리턴 타입’의 용도로 제한적으로 사용하기 위해서 만들어졌기 때문에, 메서드의 ‘리턴 타입’에 사용하여 null 체크의 문제를 없애고, null-safe 한 코드를 제공할 수 있습니다.
Optional을 사용하지 않으면, 빈 결과를 반환해야 할 경우에, null이나 예외를 던지는 형태로 처리해야 합니다. 이럴 때 Optional을 반환값에 사용하게 되면,
1) 빈 결과를 명확히 반환할 수 있고,
2) null을 반환하는 메서드보다 오류 가능성이 적고,
3) null 일 때 예외를 던지는 메서드보다 더 사용이 용이한 코드를 만들 수 있습니다.
그러면, 이제부터 Optional의 기본 문법과 사용법을 차례로 살펴보겠습니다.
Optional 기본 문법
(1) Optional 생성 방법
메서드의 리턴 타입으로 Optional을 리턴하기 위해서 Optional을 생성해서 값을 리턴해줄 수 있습니다. 그러면, Optional을 어떻게 만드는지 알아보겠습니다.
Optional은 기본 생성자가 private로 직접적으로 생성자를 호출하여 Optional을 생성할 수 없고, Optional에서 제공하는 static 메서드를 사용하여 Optional을 만들어 낼 수 있습니다. 따라서, Optional 은 아래와 같은 방법으로 생성할 수 있습니다.
Optional<String> empty = Optional.empty(); // 값이 비어있는 Optional을 생성방법 Optional<String> opt = Optional.of(“Hello”); // “Hello”란 값이 담겨있는 Optional을 생성 String temp = null; Optional<String> opt = Optional.ofNullable(temp) // 인자(temp)가 null일 수도 있는 상황에서 사용 |
of() 메서드를 사용하는 경우, null을 인자로 사용하게 되면, NullPointerException 예외가 발생하므로 null 일 가능성이 있는 값을 인자로 받는 경우에는 ofNullable() 메서드를 사용해야 합니다.
(2) Optional이 제공하는 메서드와 사용방법
Optional을 리턴 값으로 받은 경우에 Optional의 값이 있는 경우와 없는 경우에 대해 어떻게 처리하는지 Optional이 제공하는 method들을 활용하여 코드를 작성할 수 있는데요. 각 메서드별 특징을 알려드리겠습니다.
isPresent()
Optional의 값이 존재하면 true, 해당 값이 없으면 false를 리턴합니다.
isEmpty()
isPresent() 메서드와는 반대로 체크하는 메서드로 Optional의 값이 비어있으면 true, 해당 값이 존재하면 false를 리턴합니다. 단, isEmpty() 메서드는 java11 버전부터 사용이 가능합니다.
get()
Optional이 감싸고 있는 실제 값을 얻기 위해서 사용하며, 만일 Optional의 값이 비어있는 경우, NoSuchElementException이 발생하게 됩니다.
그러므로, Optional의 값이 항상 있다고 판단되지 않는다면 isPresent() 또는 isEmpty()로 값의 존재 여부를 체크한 뒤, get() 메서드를 사용하는 형태로 사용해야 합니다.
Optional<String> optionalString = Optional.of("Hello"); if (optionalString.isPresent()) { // 값이 존재하는지 확인 System.out.println("Optional contains a value: " + optionalString.get()); } else { System.out.println("Optional is empty"); } |
ifPresent(), ifPresentOrElse()
ifPresent() 메서드를 이용해서 값이 있는 경우, 처리하는 함수를 직접 람다 함수로 사용할 수 있으며, ifPresentOrElse()를 사용해서 값이 있는 경우와 없는 경우를 동시에 처리할 수 있는 메서드도 제공합니다.
상기 예제를 Optional이 제공하는 ifPresentOrElse()를 사용해서 다시 작성하면 아래와 같이 사용할 수 있습니다.
Optional<String> optionalString = Optional.of("Hello"); optionalString.ifPresent( val -> System.out.println("Optional contains a value: " + val), () -> System.out.println("Optional is empty") ); |
위와 같이 작성하면 if 문을 사용하는 것보다 코드를 간략하게 작성 가능한 장점이 있는데요. Optional 클래스는 값이 없는 경우(empty), 대체 값이나 예외를 발생시킬 수 있는 orElse(), orElseGet(), orElseThrow() 메서드를 제공하고 있습니다.
orElse(), orElseGet(), orElseThrow()
Optional<String> optionalString = Optional.of("Hello"); String value = optionalString.orElse("Default Value"); // 대체값 지정
// 대체값을 람다함수로 지정 String result = optionalString.orElseGet(() -> "Value from Supplier");
// 값이 없을 때 예외 던지기 String str = emptyOptional.orElseThrow(() -> new RuntimeException("Optional is empty")); |
Java9부터 추가된 or() 메서드를 사용하면, orElseGet()이나 orElse()와 달리 Optional 객체를 리턴할 수 있습니다.
or()
Optional<String> firstOptional = Optional.empty(); Optional<String> secondOptional = Optional.of("Hello, world!"); // 두 Optional 객체 중 하나라도 값이 존재하는 경우 값을 반환하고, 모두 값이 없는 경우 "Default Value"를 반환 String result = firstOptional.or(() -> secondOptional).orElse("Default Value"); System.out.println("Result: " + result); |
위의 result 값은 firstOptional의 값이 없으므로, or() 메서드의 람다 함수가 실행되어 secondOptional이 리턴되고, secondOptional의 값은 존재하기 때문에, “Hello world!”가 됩니다.
filter()
Optional의 값에 filter를 적용하여 조건에 맞는 값인 경우에 사용할 경우, filter()를 사용할 수 있습니다.
Optional<String> optionalString = Optional.of("Hello World"); // filter를 사용하여 문자열의 길이가 5 이상인 경우에만 처리 optionalString.filter(s -> s.length() >= 5) .ifPresent(s -> System.out.println("Filtered Value: " + s)); |
map()
Optional의 값을 변환해서 사용해야 하는 경우, map()을 이용해 기존 값을 변환하여 값을 가져올 수 있습니다.
Optional<String> optionalString = Optional.of("Hello World"); // map을 사용하여 문자열을 대문자로 변환 Optional<String> upperCaseOptional = optionalString.map(String::toUpperCase); upperCaseOptional.ifPresent(s -> System.out.println("Uppercase Value: " + s)); |
flatMap()
마지막으로 flatMap()은 변환한다는 면에서는 map()과 동일하지만, map()이 인자로 받는 람다 함수는 리턴 값이 객체이며, 이 객체를 Optional로 감싸져서 return 하게 되지만, flatMap()이 인자로 받는 람다 함수는 리턴 값이 객체가 아니라 객체를 감싸는 Optional 객체를 리턴한다는 점에서 차이가 있습니다.
// flatMap을 사용하여 문자열을 Optional로 감싼 후 해제 Optional<String> optionalString = Optional.of("Java Optional"); Optional<String> flatMappedOptional = optionalString.flatMap(s -> Optional.of(s + " Tutorial")); flatMappedOptional.ifPresent(s -> System.out.println("FlatMapped Value: " + s)); |
즉, Optional 값 변환 시, 람다 함수의 리턴 값이 Optional인 경우에는 flatMap을 사용하고, 그렇지 않은 경우에 map()을 사용하면 됩니다.
Java Optional의 기본 사용법을 알아보았습니다. Java Optional은 '실제 값을 한번 감싸서 사용하기' 때문에 사용하지 않을 때보다 처리시간이 더 많이 소요될 수 있습니다.
따라서, Optional을 남용하지 않기 위해 올바른 사용 방법을 이해해야 하는데요. Optional을 올바로 사용하기 위해 알아야 할 주의사항을 알려드리겠습니다.
Optional 사용 시 주의사항
1) Optional 변수에 null을 할당하지 않는다.
Optional은 null을 사용하지 않기 위해서 만들어진 클래스인데, Optional을 null로 초기화하면 Optional의 사용 의도와 맞지 않게 됩니다. 빈 값으로 Optional을 만들려면 Optional.empty()로 객체를 생성해야 합니다.
2) Optional을 필드 타입으로 사용하지 않는다
Optional은 메서드의 리턴 타입으로 사용하기 위한 용도로 설계되었기 때문에, Optional을 클래스의 필드로 선언하는 것은 잘못된 사용 방식이며, 필드 타입은 객체를 그대로 사용하는 것이 올바른 사용 방식입니다.
3) 메서드의 인자로 Optional을 사용하지 않는다.
Optional은 메서드의 리턴 타입으로 사용하기 위한 용도이므로, Optional을 메서드의 인자로 사용하는 것도 잘못된 사용 방식으로 메서드의 인자도 역시 객체를 그대로 사용하는 것이 올바른 사용 방식입니다.
4) 빈 컬렉션이나 배열을 나타낼 때는 Optional을 사용하지 않는다.
빈 컬렉션을 리턴하고 자 하는 경우는 Optional을 사용할 것이 아니라, 빈 컬렉션을 리턴하는 것이 올바른 사용 방식입니다.
ArrayList나 HashMap() 등은 최초 객체를 생성하면 비어있는 컬렉션이 생성이 되므로 이를 사용해서 리턴하면 됩니다.
컬렉션이 ‘비어있음’을 나타내기 위해서 굳이 Optional을 사용하는 것은 적합한 사용 방식이 아닙니다. 마찬가지로 배열의 경우도 ‘비어있음’을 나타내려면 빈 배열을 생성하여 사용하면 됩니다.
5) 원시 타입을 값으로 가지는 Optional이 필요할 때는 원시 타입 용도로 만들어진 클래스를 사용한다.
원시 타입(primitive type)을 Optional로 사용해야 할 때는 Wrapper 클래스로 된 Optional 클래스를 사용하지 말고, 원시 타입 용도로 만들어진 OptionalInt, OptionalLong, OptionalDouble 형태의 클래스를 사용하는 것이 성능 측면에서 유리합니다.
또한 Optional
그러면, Optional의 기본 사용법과 올바른 사용방법에 대한 가이드를 알았으니, Optional이 개발 업무에서 주로 어떤 곳에 사용되는지 알아보고자 합니다.
Optional의 대표적인 사용처: Spring Data JPA
Java Spring 백엔드 개발을 하는 경우, DB 연동을 위해 ORM Framework인 JPA를 사용하게 되는데, 이때 Spring Data JPA에서 제공하는 JPA Repository의 메서드에서 return 타입이 Optional을 리턴하기 때문에 필수적으로 사용됩니다.
▶️ 연관 콘텐츠: JPA vs Mybatis, 현직 개발자는 이럴 때 사용합니다.
public interface CrudRepository<T, ID> extends Repository<T, ID> { Optional<T> findById(ID id); } |
해당 JPA 메서드는 아래와 같은 형태로 사용할 수 있습니다.
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Optional;
@Service public class UserService {
@Autowired private UserRepository userRepository;
public void getUserById(Long userId) { Optional<User> userOptional = userRepository.findById(userId); if (userOptional.isPresent()) { User user = userOptional.get(); System.out.println("User found: " + user.getName()); } else { System.out.println("User not found with ID: " + userId); } } } |
또한, Spring Data JPA에서는 Entity의 필드명을 이용한 특정 이름으로 Repository 인터페이스의 메서드의 이름을 정의하면 해당 메서드 이름에 해당하는 SQL을 자동으로 생성해 줍니다.
이때에도 Optional을 리턴 타입으로 사용하게 됩니다. 즉, Entity에 아래와 같이 user name이라는 필드가 있다고 가정하면,
@Entity @Data public class User { @Id @GeneratedValue(strategy = jakarta.persistence.GenerationType.IDENTITY) private Long id; private String username; private String email; } |
아래와 같이 Spring Data JPA의 메서드명 생성 규칙에 맞게 메서드를 선언하거나 또는 @Query 어노테이션을 사용하여 JPQL을 사용할 때도 Optioal을 리턴 값으로 사용할 수 있습니다.
@Repository public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String title);
@Query("SELECT u FROM User u WHERE u.email = :email") Optional<User> findByEmail(@Param("email") String email); } |
이렇게 Entity와 Repository를 만든 경우에, Optional을 사용하는 findByUsername() 메서드를 아래와 같이 사용할 수 있습니다.
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Optional;
@Service public class UserService { @Autowired private UserRepository userRepository;
public void getUserByUsername(String username) { Optional<User> userOptional = userRepository.findByUsername(username); userOptional.ifPresentOrElse( user -> System.out.println("User found: " + user.getUsername()), () -> System.out.println("User not found with username: " + username) ); } } |
이와 같이 Optional은 Spring 백엔드 개발자가 필수적으로 다룰 줄 알아야 하는 기능입니다.
Optional을 사용하면 코드를 명확하게 작성할 수 있고, NullPointerException 예외로부터 코드를 보호할 수 있습니다.
Optional을 사용하게 되면, null 체크에 대한 누락과 실수가 예방되어 값이 존재하지 않을 수 있는 상황에서 발생하는 NullPointerException의 예외를 방지하고, 코드의 안정성을 향상시키는 데 유용합니다.
프로그램 실행 시 NullPointerException 만큼 자주 발생하는 오류도 없을 겁니다. 이로 인해 불필요한 프로그램 중단과 사용자가 원하지 않는 오류메시지가 생성된 다면 서비스에 좋지 않은 영향을 끼칠 수 있고, 오류의 원인을 찾느라 시간을 낭비하게 됩니다.
특히 Spring Data JPA와 같은 프로젝트에서는 데이터베이스 조회 결과가 없는 경우가 많습니다. 이런 상황에서 Optional의 올바른 사용법을 적용해 보세요. NullPointerException로 인한 문제는 줄어들고, 관리 시간은 확연하게 줄어 작업 시간이 여유로워질 것입니다.
이랜서 선정, 모르면 야근하는 개발 업무 단축 시리즈
▶️ 개발자 도구, 현직 개발자가 웹 개발 생산성을 높이는 방법
▶️ 프롬프트 작성법, Chat GPT 제대로 활용하는 방법
▶️ Spring Framework, 개발 시간을 단축하고 싶으면 ‘3가지’를 확인하세요.
Optional로 NEP 문제를 잡고 관리 시간을 줄였다면,
이랜서 ‘오토폴리오’로 커리어 관리 시간도 줄여보세요.
좋은 기회는 예고 없이 찾아옵니다. 경쟁력 있는 프로젝트를 수주하기 위해 경력 기술서와 이력서 관리는 필수입니다. 하지만 개발 업무만으로도 바쁜 일상, 경력 기술서와 이력서를 완벽하게 관리하기란 쉽지 않은데요. 이럴 때 경력 기술서와 이력서를 자동으로 업데이트해주는 프로그램을 사용한다면 어떨까요?
억 단위 데이터가 경력 기술서와 이력서를
자동으로 업데이트해주는 이력서 자동관리 서비스
‘오토폴리오’
▶️ 이력서 자동관리 서비스 "오토폴리오" 시연 영상 보러가기
오토폴리오는 이랜서가 독자 개발한 경력 & 이력 자동 관리 서비스 프로그램입니다. 초기 1회만 표준 양식으로 업데이트하면 경력 기술서와 이력가 자동으로 업데이트되고, 워드 파일로 다운로드하거나 메일로 바로 보낼 수 있는 편리한 시스템입니다.
이미 40만 명의 IT 프리랜서들은
‘오토폴리오’를 통해 자동으로 경력을 관리하며
프로젝트 수주 확률을 높이고 있습니다.
시간만 오래 쓴다고 좋은 경력 기술서가 작성되지 않습니다. 이력서 자동관리 서비스 ‘오토폴리오’를 통해 경력 기술서와 이력서를 편하게 관리하세요. ‘억 단위’ 데이터를 활용한 오토폴리오가 합격률 높은 경력 관리를 도와드립니다.