JavaMedium#63
Java의 Optional이란 무엇이며 올바른 사용법은?
#Java#Optional#NPE#Java8
힌트
NullPointerException 방지를 위한 래퍼 클래스입니다.
정답 및 해설
Java의 Optional이란 무엇이며 올바른 사용법은?
Optional<T>는 Java 8에서 도입된 컨테이너 클래스로, 값이 있거나 없을 수 있는 상황을 명시적으로 표현합니다. null 반환 대신 Optional을 사용하면 NullPointerException(NPE)을 명시적으로 다루도록 유도하여 코드의 안정성을 높입니다. 하지만 잘못 사용하면 오히려 코드가 복잡해지므로 올바른 사용 패턴을 이해하는 것이 중요합니다.
Optional이 필요한 이유
// 기존 null 처리 방식 - 잊어버리기 쉬움
public String getUserEmail(Long userId) {
User user = userRepository.findById(userId);
if (user == null) {
return null; // 호출자가 null 체크를 해야 하는데 강제되지 않음
}
return user.getEmail();
}
// 호출 코드에서 NPE 위험
String email = getUserEmail(1L);
System.out.println(email.toUpperCase()); // user가 없으면 NPE!
// Optional 사용 - null 가능성을 타입으로 명시
public Optional<String> getUserEmail(Long userId) {
// 반환 타입이 Optional이므로 호출자는 반드시 처리를 고려하게 됨
return userRepository.findById(userId)
.map(User::getEmail);
}
Optional 생성 방법
// 1. Optional.of(value): 값이 반드시 null이 아닐 때
Optional<String> opt1 = Optional.of("Hello");
Optional<String> opt2 = Optional.of(null); // NullPointerException 발생!
// 2. Optional.ofNullable(value): 값이 null일 수 있을 때
Optional<String> opt3 = Optional.ofNullable("Hello"); // Optional["Hello"]
Optional<String> opt4 = Optional.ofNullable(null); // Optional.empty
// 3. Optional.empty(): 명시적으로 빈 Optional 생성
Optional<String> opt5 = Optional.empty();
주요 메서드
값 확인 및 가져오기
Optional<String> opt = Optional.of("Hello");
Optional<String> empty = Optional.empty();
// isPresent(): 값이 있으면 true
System.out.println(opt.isPresent()); // true
System.out.println(empty.isPresent()); // false
// isEmpty(): Java 11+ 값이 없으면 true
System.out.println(opt.isEmpty()); // false
System.out.println(empty.isEmpty()); // true
// get(): 값 반환, 없으면 NoSuchElementException
System.out.println(opt.get()); // "Hello"
// empty.get(); // NoSuchElementException!
기본값 처리
Optional<String> opt = Optional.empty();
// orElse: 값이 없으면 기본값 반환 (항상 평가됨)
String result1 = opt.orElse("default");
System.out.println(result1); // "default"
// orElseGet: 값이 없을 때만 Supplier 실행 (지연 평가)
String result2 = opt.orElseGet(() -> generateDefault());
// opt가 비어있을 때만 generateDefault() 호출
// orElseThrow: 값이 없으면 예외 던짐 (Java 10+: 기본 NoSuchElementException)
String result3 = opt.orElseThrow(() -> new IllegalStateException("값이 없습니다."));
// orElse vs orElseGet 성능 차이
Optional<String> hasValue = Optional.of("value");
// orElse: "default"가 항상 생성됨
String r1 = hasValue.orElse(expensiveOperation()); // expensiveOperation 항상 실행
// orElseGet: 값이 있으면 람다 미실행
String r2 = hasValue.orElseGet(() -> expensiveOperation()); // 실행 안 됨
변환 및 필터링
Optional<String> opt = Optional.of(" hello world ");
// map: 값을 변환 (변환 결과가 null이면 Optional.empty 반환)
Optional<String> upper = opt.map(String::trim).map(String::toUpperCase);
System.out.println(upper.get()); // "HELLO WORLD"
// flatMap: Optional을 반환하는 함수를 적용 (중첩 Optional 방지)
Optional<Optional<String>> nested = opt.map(s -> Optional.of(s.trim())); // Optional<Optional<String>>
Optional<String> flat = opt.flatMap(s -> Optional.of(s.trim())); // Optional<String>
// filter: 조건을 만족하지 않으면 Optional.empty 반환
Optional<String> filtered = opt.map(String::trim)
.filter(s -> s.length() > 5);
System.out.println(filtered.isPresent()); // true ("hello world" 길이 11)
Optional<String> shortOpt = Optional.of("hi");
Optional<String> filtered2 = shortOpt.filter(s -> s.length() > 5);
System.out.println(filtered2.isPresent()); // false
동작 수행
Optional<String> opt = Optional.of("Hello");
// ifPresent: 값이 있을 때만 Consumer 실행
opt.ifPresent(System.out::println); // "Hello"
// ifPresentOrElse: Java 9+ 값이 있을 때와 없을 때 각각 처리
opt.ifPresentOrElse(
s -> System.out.println("값: " + s),
() -> System.out.println("값 없음")
);
// or: Java 9+ 값이 없을 때 대체 Optional 반환
Optional<String> result = Optional.<String>empty()
.or(() -> Optional.of("대체값"));
System.out.println(result.get()); // "대체값"
// stream: Java 9+ Optional을 Stream으로 변환
opt.stream()
.map(String::toUpperCase)
.forEach(System.out::println); // "HELLO"
올바른 사용법
권장: 반환 타입으로 사용
// 좋은 예: 조회 결과가 없을 수 있는 메서드의 반환 타입
public Optional<User> findUserById(Long id) {
return userRepository.findById(id);
}
public Optional<String> findConfigValue(String key) {
return Optional.ofNullable(configMap.get(key));
}
// 호출 코드
findUserById(1L)
.map(User::getName)
.ifPresent(name -> System.out.println("사용자: " + name));
비권장: 메서드 파라미터로 사용
// 나쁜 예: 파라미터에 Optional 사용
public void process(Optional<String> name) { // 안티패턴
// 호출자가 null을 Optional 대신 직접 전달 가능
}
// 좋은 예: 오버로딩 또는 @Nullable 사용
public void process(String name) { ... }
public void process() { ... } // 이름 없는 경우 별도 메서드
비권장: 필드로 사용
// 나쁜 예: 필드에 Optional 사용 (직렬화 불가, 메모리 낭비)
public class User {
private Optional<String> middleName; // 안티패턴
}
// 좋은 예: null 허용 필드로 선언하고 getter에서 Optional 반환
public class User {
private String middleName; // null 허용
public Optional<String> getMiddleName() {
return Optional.ofNullable(middleName);
}
}
비권장: isPresent() + get() 조합
// 나쁜 예: isPresent/get 조합은 기존 null 체크와 동일
Optional<User> userOpt = findUserById(1L);
if (userOpt.isPresent()) {
User user = userOpt.get();
System.out.println(user.getName());
}
// 좋은 예: ifPresent 또는 map 사용
findUserById(1L).ifPresent(user -> System.out.println(user.getName()));
// 또는
String name = findUserById(1L)
.map(User::getName)
.orElse("알 수 없음");
실전 패턴
체이닝으로 null-safe 처리
// 중첩 null 체크를 Optional 체이닝으로 단순화
// 기존 방식
String city = null;
if (user != null && user.getAddress() != null) {
city = user.getAddress().getCity();
}
// Optional 방식
String city = Optional.ofNullable(user)
.map(User::getAddress)
.map(Address::getCity)
.orElse("서울");
Stream과 함께 사용 (Java 9+)
List<Optional<String>> list = Arrays.asList(
Optional.of("A"),
Optional.empty(),
Optional.of("B"),
Optional.empty(),
Optional.of("C")
);
// flatMap으로 빈 Optional 제거 후 값만 추출
List<String> values = list.stream()
.flatMap(Optional::stream) // Java 9+
.collect(Collectors.toList());
System.out.println(values); // [A, B, C]
Repository 패턴에서의 활용
// Spring Data JPA는 Optional을 자연스럽게 지원
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
Optional<User> findByUsername(String username);
}
// 서비스 계층에서 활용
@Service
public class UserService {
private final UserRepository userRepository;
public UserDto getUser(Long id) {
return userRepository.findById(id)
.map(UserDto::from)
.orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다: " + id));
}
public void updateEmail(Long id, String newEmail) {
userRepository.findById(id)
.ifPresentOrElse(
user -> {
user.setEmail(newEmail);
userRepository.save(user);
},
() -> { throw new UserNotFoundException("사용자 없음: " + id); }
);
}
}
Optional 메서드 요약
| 메서드 | 설명 | 값 없을 때 |
|---|---|---|
of(value) | null 불가 Optional 생성 | NPE 발생 |
ofNullable(value) | null 가능 Optional 생성 | Optional.empty() |
empty() | 빈 Optional 생성 | - |
isPresent() | 값 존재 여부 확인 | false |
isEmpty() (Java 11+) | 값 없음 여부 확인 | true |
get() | 값 반환 | NoSuchElementException |
orElse(default) | 기본값 반환 (항상 평가) | default 반환 |
orElseGet(supplier) | 기본값 지연 평가 | supplier.get() 반환 |
orElseThrow(supplier) | 예외 던짐 | 예외 발생 |
map(function) | 값 변환 | Optional.empty() |
flatMap(function) | Optional 반환 함수 적용 | Optional.empty() |
filter(predicate) | 조건 필터링 | Optional.empty() |
ifPresent(consumer) | 값 있을 때 동작 수행 | 아무것도 안 함 |
ifPresentOrElse (Java 9+) | 있을 때/없을 때 각각 처리 | emptyAction 실행 |
or(supplier) (Java 9+) | 대체 Optional 반환 | supplier.get() 반환 |
stream() (Java 9+) | Optional → Stream 변환 | 빈 Stream |