자바는 스위프트와 다르게 Null과 Optional 이 서로 연관된 개념이 아닌 분리된 개념입니다. 오히려 Null의 또 다른 대응책으로 Optional이 제안됩니다.
자바 8 전에는 메서드가 특정 조건에서 값을 반환할 수 없을 때 취할 수 있는 선택지가 두 가지 있었습니다.
두 방법 모두 허점이 있습니다.
자바 8 이후로는 또 하나의 선택지인 Optional<T>이 생겼습니다. Optional<T>는 null 이 아닌 T 타입 참조를 하나 담거나, 혹은 아무것도 담지 않을 수 있습니다. 옵셔널은 원소를 최대 1개 가질 수 있는 ‘불변’ 컬렉션입니다. Optional<T>가 Collection<T>를 구현하지는 않았지만, 원칙적으로 그렇다는 것입니다.
코드 55-1 컬렉션에서 최대값을 구한다(컬렉션이 비었으면 예외를 던진다).
public static <E extends Comparable<E>> E max(Collection<E> c) {
if (c.isEmpty()) {
throw new IllegalArgumentException("빈 컬렉션");
}
E result = null;
for(E e: c) {
if (result == null || e.compareTo(result) > 0) {
result = Objects.requireNonNull(e);
}
}
return result;
}
코드 55-2 컬렉션에서 최댓값을 구해 Optional
로 반환한다.
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
if (c.isEmpty()) {
return Optional.empty();
}
E result = null;
for(E e: c) {
if (result == null || e.compareTo(result) > 0) {
result = Objects.requireNonNull(e);
}
}
return Optional.of(result);
}
=> 빈 옵셔널은 Optional.empty()로 만들고, 값이 든 옵셔널은 Optional.of(value)로 생성했습니다. Optional.of(value)에 null을 넣으면 NullPointerException을 던지니 주의합시다.
코드 55-3 스트림 버전
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
return c.stream().max(Comparator.naturalOrder());
}
스위프트에서는 **Optional
let nonOptionalValue: Int = 6
print(nonOptionalValue) // 6
var optionalValue: Int? = nil
print(optionalValue) // nil
optionalValue = 6
print(optionalValue) // Optional(6)
Swift 에서는 특정 조건에서 값을 반환할 수 없을 때 주로 Optional 타입을 사용해 nil을 반환합니다. 예외(Error)는 정말 예외인 경우에 사용합니다.
다음은 위의 자바 코드에 대해 대응되는 적절한 예시의 Swift 코드입니다.
코드 55-2 를 Swift로 바꾼 코드 ```Swift static func max<T: Comparable>(collections: [T]) -> T? { if collections.isEmpty { return nil }
var result: T = collections.first!
for element in collections {
if element > result {
result = element
}
}
return result }
let maxValue = max(collections: [1, 2, 3]) print(maxValue) // Optional(3)
if let maxValue = max(collections: [1, 2, 3]) { print(maxValue) // 3 }
guard let maxValue = max(collections: [1, 2, 3]) else { return } print(maxValue) // 3
=> `if let` 구문이나 `guard let` 구문을 통해 Optional값을 unwrap 합니다. `max` 메서드의 반환값이 nil 인 경우 maxValue 는 값이 할당되지 않고, 반환값이 nil 이 아닌 경우 maxValue 에 값이 할당되어 print 됨을 알 수 있습니다. maxValue 의 타입은 Int, 즉 non-optional 합니다.
> 코드 55-3을 Swift로 바꾼 코드 (스트림 버전)
```swift
static func max<T: Comparable>(collections: [T]) -> T? {
return collections.max()
}
Java
String lastWordInLexicon = max(words).orElse("단어 없음...");
Swift
let maxValue: Int = Example.max(collections: [ ]) ?? 100
print(maxValue) // 100
=> 메소드 반환값이 nil 인 경우 ??
을 이용해 디폴트 값으로 설정된 100이 반환됩니다.
또는 상황에 맞는 예외를 던질 수 있습니다. 다음 코드에서 실제 예외가 아니라 예외 팩터리를 건넨것에 주목합시다. 이렇게 하면 예외가 실제로 발생하지 않는 한 예외 생성 비용은 들지 않습니다.
Java
Toy myToy = max(toys).orElseThrow(SometingException::new);
Swift
Swift 는 해당 기능이 없습니다. 따라서 똑같이 기능을 갖추려면 extension 을 이용해 기능 구현해야 합니다.
extension Optional {
func orThrow(_ errorExpression: @autoclosure () -> Error) throws -> Wrapped {
switch self {
case .some(let value):
return value
case .none:
throw errorExpression()
}
}
}
let file = try loadFile(at: path).orThrow(MissingFileError())
=> Swift by Sundell의 예시입니다.
Java
옵셔널에 항상 값이 채워져 있다고 확신한다며 그냥 곧바로 값을 꺼내 사용하는 선택지도 있습니다. 다만 잘못 판단한 것이라면 NoSuchElementException
이 발생합니다.
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
Swift Swift 에서는
!
을 이용하면 강제적으로 Optional 에서 값을 바로 꺼내 사용할 수 있습니다(Forced Unwrapping). 대신 마찬가지로 값이 없는 경우엔 런타임 에러가 발생합니다.
let lastNobleGas: Element = Example.max(collections: Elements.NobleGases)!
// ! 을 사용해서 타입은 옵셔널이 아닙니다.
Java
static Optional<String> getOptionalName() {
return Optional.of("Jason");
}
Optional<String> name = getOptionalName();
System.out.println("내 이름: " + (name.isPresent() ? name.get() : "이름 없음"));
Swift
if let
, guard let
)
if let maxValue = max(collections: [1, 2, 3]) {
print(maxValue) // 3
}
guard let maxValue = max(collections: [1, 2, 3]) else { return }
print(maxValue) // 3
반환 값으로 옵셔널을 사용한다고 해서 무조건 득이 되는 건 아닙니다.
컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안됩니다. 빈 Optional<List<T» 를 반환하기 보다는 빈 List
빈 컨테이너를 그대로 반환하면 클라이언트에 **옵셔널 처리 코드를 넣지 않아도 됩니다!**
BAD
func generateCards() -> [Card]? // 외부에서 옵셔널 처리를 해야 하는 번거로움이 있습니다.
GOOD ```Swift func generateCards() -> [Card] // 외부에서 옵셔널 처리를 하지 않아도 됩니다.
## 반환값으로 옵셔널을 사용해야 하는 경우
결과가 없을 수 있을 때 사용하고 **Swift의 경우, 제일 우선되는 선택지**입니다. Java는 null과 Optional이 분리된 개념이라 두 가지 선택지를 모두 고려해야 하지만, Swift는 옵셔널과 nil이 연관된 개념이기 때문에 옵셔널을 먼저 고려합니다.
## Java 맵 vs Swift 딕셔너리
> Java: Map
맵(Map)의 값으로 **옵셔널을 사용하면 절대 안됩니다.** 만약 그리 한다면 맵 안에 키가 없다는 사실을 나타내는 방법이 두 가지가 됩니다. 하나는 키 자체가 없는 경우이고, 다른 하나는 키는 있지만 그 키가 속이 빈 옵셔널(`Optional.empty()`)인 경우입니다. 쓸데없이 복잡성만 높여서 혼란과 오류 가능성을 키울 뿐입니다.
```java
Map<String, Optional<Integer>> map = Map.of("key", Optional.empty());
System.out.println(map.get("notKey")); // null
System.out.println(map.get("key")); // Optional.empty
=> 자바는 위와 같이 키가 없는 경우 null을 반환합니다.
Swift: Dictionary
딕셔너리(Dictionary)는 키와 대응하는 값이 있든 없든, 특정 키로 대응하는 값을 반환할 때 옵셔널 타입으로 반환합니다. 따라서 반환된 값은 키가 없으면 nil이 나오고, 키가 있으면 값이 옵셔널로 래핑되어 도출됩니다. 따라서 딕셔너리의 값 타입도 옵셔널타입은 지양해야 합니다. 자바의 Map과 마찬가지로 맵 안에 키가 없다는 사실이 키 자체가 없는 경우와 키는 있지만 값이 nil인 경우 총 2가지가 되어 복잡성만 높이기 때문입니다.
let three = dic["three"]
print(three) // nil
let four = dic["four"]
print(four) // Optional(4)
if let four = four { // unwrapping
print(four) // 4
}
=> 일반화해 이야기하면 옵셔널을 컬렉션의 키, 값, 원소나 배열의 원소로 사용하는 게 적절한 상황은 거의 없습니다.