레벨 1 마지막 미션 <장기>를 구현하기 위해, 기물을 놓을 수 있는 장기판 위의 점을 의미하는 객체 Dot을 작성하였다.
Dot의 설계 의도는 기물(Piece)를 올려 놓을 수도 있고, 비워둘 수도 있는 객체이다. 그래서 piece를 필드로 갖고, 기물이 없는 경우는 null을 할당하여 일일히 null check를 해줘야 하나 싶었다.
public class Dot {
private Piece piece;
public Dot() {
piece = null;
}
public void place(Piece piece) {
this.piece = piece;
}
public boolean isPlaced() {
return piece != null;
}
public Piece getPiece() {
if (piece == null) {
throw new IllegalArgumentException("해당 점에는 장기말이 없습니다.");
}
return piece;
}
}
그런데 piece에 null을 넣는 것이 불편했다.
NPE 뜨면 어떡하지? NPE는 원래 개발자가 예상 못한 접근에서 발생하는 것 아닌가? 실무에서는 치명적인 예외 아닌가? 그냥 null이 싫다. 다른 방법은 없나?
그러다 문득 이럴 때 null 대신 쓰라고 Optional이 있는 건가 싶었다.
일단 써보자
public class Dot {
private Optional<Piece> piece;
public Dot() {
piece = Optional.empty();
}
public void place(Piece piece) {
this.piece = Optional.ofNullable(piece);
}
public boolean isPlaced() {
return piece.isPresent();
}
public Piece getPiece() {
if (!isPlaced()) {
throw new IllegalArgumentException("해당 점에는 장기말이 없습니다.");
}
return piece.get();
}
}
위 처럼 필드를 Optional로 감싸니 null을 안 봐도 됐다. 필드에 처음 보는 노란줄이 생기긴 했는데 목적을 달성했으니 별 신경 쓰지 않았다.
그런데 리뷰어와의 대화 도중 위 코드의 두 가지 문제점을 발견했다.
1. Optional 메서드 활용을 잘 못하고 있음.
2. Optional을 필드로 쓰는 것 자체를 지양해야 함.
Optional은 그 특성 상 값의 유무를 확인한 뒤 값을 사용하는 패턴이 자주 사용된다. 그럴 때 쓰라고 만들어놓은 다양한 메서드 orElse, orElseThrow 등을 활용하지 않고 If Present로 존재 여부를 확인하고 값을 get 할 거면 뭐하러 Optional을 쓰겠나.
get()은 아래처럼 깔끔해질 수 있다.
public Piece getPiece() {
return piece.orElseThrow(() -> new IllegalArgumentException("해당 점에는 장기말이 없습니다."));
}
마음이 한결 편안해진다. 보기에도 직관적이다.
훨씬 깔끔하고 좋은데?
그런데 두 번째 문제가 남아있었다. 그냥 넘겼던 필드의 노란줄에 관한 것이다. serialize.. 이게 뭘까?
결론부터 이야기하면 Optional은 직렬화(Serialize)가 불가능해 필드에 사용하지 않는다. 애초에 반환값 전용으로 설계된 녀석이었다.
실제로 Java 언어 설계자들도 optional 을 return value 전용으로 쓰길 권장하고 있습니다.
optional 을 필드나 파라미터로 쓸 수 없도록 직렬화 지원을 의도적으로 제외했다는 이야기도 있고요.
이게 맞는 방향인가에 대해서는 Java 커뮤니티 내에서도 꽤 논쟁이 있었는데, 가볍게 확인해보는 것도 재밌을 것 같네요 :)- 피케이의 리뷰 中
음... 그렇군.
근데 Serialize가 뭐지?

간단히만 알아보자. 직렬화란 객체를 메모리, 데이터베이스 혹은 파일에 쓰기 위해 byte stream으로 변환하는 것이다. 비유하자면 아래와 같다.

그런데 Optional은 직렬화를 지원하지 않는다.(java 개발자의 의도라고 한다.) 따라서 데이터의 유연한 이동을 위해 필드를 Optional로 작성하면 안되는 것이다.
그래서 필드는 일반 타입으로 선언하고 반환시에 Optional로 감싸줬다.
public class Dot {
private Piece piece;
public Dot() {
piece = null;
}
public void place(Piece piece) {
this.piece = piece;
}
public boolean isPlaced() {
return piece != null;
}
public Optional<Piece> findPiece() {
return Optional.ofNullable(piece);
}
public void clear() {
this.piece = null;
}
}
null 자체를 안보고 싶었지만 직렬화가 안된다니 어쩔 수 없지...
(함수 네이밍도 은근슬쩍 find로 바뀌었는데 일반적으로 Optional을 반환할 때는 find, 확정값은 get이라고 한다.)
이렇게 되면 findPiece에서는 Optional 객체를 넘겨주기 때문에 받아서 사용하는 측에서 null 처리를 해주어야 한다. 이 부분은 어쩔 수 없는 부분이라고 생각한다. 어쩌면 사용하는 측에서 null 체크를 하라고 Optional을 개발한 게 아닐까 싶기도 하다.
1. Optional은 직렬화가 안되기 때문에 일반적으로 필드에 사용하지 않는다. 반환용으로 만들어졌다고 한다.
2. Optional을 쓸 거면 If present - Get 보다는 orElse..를 사용해보자.