어떤 프로그래밍 상황에서든 null checking은 필수다. 존재하지 않는 변수의 메소드나 필드를 참조해서 발생하는 NPE는 깔짝깔짝 나타나 버그를 일으켜 개발자를 짜증나게 한다. 메소드 시작점에서 입구컷을 하거나, @NonNull같은 어노테이션을 사용하거나, null일 경우 아예 직접 인스턴스를 만드는 등의 여러 방법을 사용할 수 있다.
오늘은 그 중 하나인 Optional 클래스를 살펴보겠다!!
1. Optional이 없는 상황
Student 클래스가 있다.
@RequiredArgsConstructor
public class Student {
private final String name;
private final int num;
public boolean hasSameName(String name) {
return name.equals(this.name);
}
@Override
public String toString() {
return "학생이름 : " + name + ", 번호 : " + num;
}
}
필드로 이름인 name, 출석번호인 num을 가지고 있다. 생성자로 각 필드값을 받을 것이다. toString() 메소드도 오버라이드했다. hasSameName()은 인자로 들어오는 String 값과 필드의 이름 값이 같은지 여부를 return한다.
이 Student 클래스에 대한 리스트를 담고있는 StudentRepository가 있다.
public class StudentRepository {
private final List<Student> attendance = new ArrayList<>();
AttendanceRepository() {
attendance.add(new Student("김부추",1));
attendance.add(new Student("박부추",2));
attendance.add(new Student("이부추",3));
attendance.add(new Student("전부추",4));
attendance.add(new Student("최부추",5));
attendance.add(new Student("한부추",6));
}
public Student findStudentByName(String name) {
for (Student student : attendance) {
if (student.hasSameName(name)) {
return student;
}
}
return null;
}
}
일단 생성자로 리스트 자체를 구성했다. 이름이 김부추~한부추, 출석번호가 1번~6번인데 이건 일단 동작을 위해서 만든 더미데이터!
public한 findStudentByName() 메소드가 있는데, 메소드 이름 그대로 인자값으로 전달한 name으로 student를 찾는 메소드다. for문으로 요소를 돌아 hasSameName()을 통해 이름이 일치하는 값을 찾고 있으면 그 요소값을 return, 모든 요소를 돌아도 찾을 수 없으면 null을 return한다.
이를 사용하는 클라이언트 코드를 보자. "김부추"라는 이름을 가진 학생과, "황부추"라는 이름을 가진 학생을 각각 찾고싶다. 각각 findStudentByName()을 호출했다.
public class Client {
public static void main(String[] args) {
StudentRepository studentRepository = new StudentRepository();
Student kimBuchu = studentRepository.findStudentByName("김부추");
if (kimBuchu!=null) {
System.out.println(kimBuchu);
} else {
System.out.println("김부추라는 학생은 없는데요?");
}
Student hwangBuchu = studentRepository.findStudentByName("황부추");
if (hwangBuchu!=null) {
System.out.println(hwangBuchu);
} else {
System.out.println("황부추라는 학생은 없는데요?");
}
}
}
보면 알겠지만, 참조변수에 대해 null checking이 이뤄짐을 알 수 있다. 아.. 보기싫다. findByName()을 호출할 때마다 이런식으로 null checking을 해줘야한다. 이 사실을 깜빡하고 이 메소드를 이리저리 쓰다가 NPE 한 번 뜨면 똑같이 if !=null 작성하는데 손가락 움직일거 생각하니 힘이 빠진다.
2. Optional<Student>
유틸 패키지에 있는 Optional 클래스는 이와같은 null 처리를 "FANCY"하게 해준다. Optional은 "썬크림은 필수, 비비크림은 옵션!" 할 때 옵션이다(썩은 비유 죄송합니다). 그러니까 꼭 필수로 있어야하는 값은 아니고 null이 있을 수도 있다~~ 를 고지하는 용도의 클래스라고 받아들이면 된다.
Optional 객체의 생성은 크게 다음의 세가지 방법이 있다.
- Optional.of(<null이 아닌 객체>)
- Optional.empty()
- Optional.ofNullable(<null일 수도 있는 객체>)
StudentRepository를 Optional 객체를 return하도록 바꿔보겠다.
public class StudentRepository {
private final List<Student> attendance = new ArrayList<>();
StudentRepository() {
attendance.add(new Student("김부추",1));
attendance.add(new Student("박부추",2));
attendance.add(new Student("이부추",3));
attendance.add(new Student("전부추",4));
attendance.add(new Student("최부추",5));
attendance.add(new Student("한부추",6));
}
public Optional<Student> findStudentByName(String name) {
for (Student student : attendance) {
if (student.hasSameName(name)) {
return Optional.of(student);
}
}
return Optional.empty();
}
}
원래 코드에서 Optional로 감싼 부분이 추가됐을 뿐이다. 그래서 뭐가 달라졌냐? 클라이언트 코드를 봐야한다.
public class Client {
public static void main(String[] args) {
StudentRepository studentRepository = new StudentRepository();
studentRepository.findStudentByName("김부추").ifPresentOrElse(
System.out::println,() -> {
System.out.println("김부추라는 학생은 없는데요?");
});
studentRepository.findStudentByName("황부추").ifPresentOrElse(
System.out::println,() -> {
System.out.println("황부추라는 학생은 없는데요?");
});
}
}
if (kimBuchu!=null) 부분이 없어지고, ifPresentOrElse()라는 메소드를 쓸 수 있게 되었다. 해당 메소드는 첫번째 인자로 Consumer, 두번째 인자로 Runnable FunctionalInterface를 받는다. 람다식을 쓰도록 하는 것이다. 람다식에 대해 모른다면.. 참고.
Consumer는 Optional 객체가 null이 아닐 때 실행된다. Consumer 인자로 null이 아닌 Student객체가 들어가게 되고 해당 로직이 그대로 수행되는 것이다. System.out::println 형식의 메소드 참조를 이용했다. empty Runnable은 Optional 객체가 null일때 실행된다. 그래서 위 코드는 Optional을 쓰기 전과 100% 동일한 기능을 제공한다. 값이 있으면 해당 값을 System.out.println()하고, 없으면 "~~라는 학생은 없는데요?"라고 출력하는 것이다.
Optional<T> 객체가 null을 반환할 때, 처리할 수 있는 방법이 몇 가지가 있다.
- orElseThrow()를 통해 예외를 던지게 할 수 있다. Supplier 람다식으로 Exception 객체를 반환하게 한다.
- orElse()의 인자로 default 객체를 넣어 Optional이 null일 경우 반환할 객체를 지정할 수 있다.
- orElseGet()의 Supplier 람다식 인자로 default T타입 객체를 반환하도록 할 수 있다. 2번과 차이점은 객체 자체가 아닌 "람다식"이 들어갔다는 점이다. 만약 Optional 객체가 null이라면 해당 람다식에서 return한 값이 반환된다.
어렵지 않은 내용이지만, 각 예시를 적용한 코드를 보자.
public class DealWithOptional {
public static void main(String[] args) {
StudentRepository studentRepository = new StudentRepository();
// 1. 에러 던지기
Student null1 = studentRepository.findStudentByName("존재하지않는학생이름")
.orElseThrow(()-> new RuntimeException("존재하지 않는 학생 이름입니다!"));
// 2. default 객체 반환하기
Student null2 = studentRepository.findStudentByName("존재하지않는학생이름")
.orElse(new Student("이제존재하게된학생이름",7));
// 3. 일정로직 수행 후 default 객체 반환하기
Student null3 = studentRepository.findStudentByName("존재하지않는학생이름")
.orElseGet(() -> {
System.out.println("존재하지 않을 때 수행되는 Supplier 람다식입니다.");
return new Student("이제존재하게된학생이름",7);
});
}
}
최초로 프로젝트다운 프로젝트를 했던 언어가 자바스크립트였고, Promise 관련한 콜백 기능이 재미있었기 때문에 이런 람다식 관련 기능을 살펴보는건 언제나 즐겁다 ㅋㅋ
3. Optional 주의.
Optional에서 사용하면 안되는 안티패턴이 있다.
public class Client {
public static void main(String[] args) {
StudentRepository studentRepository = new StudentRepository();
Optional<Student> kimBuchu = studentRepository.findStudentByName("김부추");
if (kimBuchu.isPresent()) {
System.out.println(kimBuchu);
} else {
System.out.println("김부추라는 학생은 없는데요?");
}
}
}
왜인지는 알겠지? Optional의 isPresent()는 해당 Optional 객체의 객체값이 null이 아닐때 true를 반환한다. 코드를 위같이 쓰면 결국 Optional을 쓰지 않은 최초의 코드랑 똑같아진다. isPresent()는 분명 유용하게 쓰일 때가 있겠지만, 적어도 위와 같은 상황은 아니다. null 처리는 2번에서 설명한 방법중 상황에 맞는 것을 사용하도록 하자.
그리고 사실, StudentRepository에서 Optional을 반환한 것도 어떻게보면 안티패턴이다. 메소드에서 직접 Optional.empty()를 반환하는 것은 null을 반환하는 것과 크게 다르지 않기 때문이다. 그런 상황이라면 그쪽에서 예외를 던지거나 default 객체를 만드는 편이 낫다. Repository 코드를 좀 더 괜찮게 수정해봤다.
public class StudentRepository {
private final List<Student> attendance = new ArrayList<>();
StudentRepository() {
attendance.add(new Student("김부추",1));
attendance.add(new Student("박부추",2));
attendance.add(new Student("이부추",3));
attendance.add(new Student("전부추",4));
attendance.add(new Student("최부추",5));
attendance.add(new Student("한부추",6));
}
public Optional<Student> findStudentByName(String name) {
return attendance.stream()
.filter(student -> student.hasSameName(name))
.findAny();
}
}
Steram API를 이용했다. filter()은 Predicate 람다식을 만족하는 객체만 다음 스트림으로 넘겨준다는 사실을 기억해라! findAny()를 통해 일치하는 값이 하나라도 있다면 그 값을 return하고, 없으면 null을 return할 것이다. 하는 일은 2번의 findStudnetByName() 메소드와 100% 동일하다. 그렇지만 더 짧고, 가독성있는 코드가 되었다!
REFERENCE
어떤 프로그래밍 상황에서든 null checking은 필수다. 존재하지 않는 변수의 메소드나 필드를 참조해서 발생하는 NPE는 깔짝깔짝 나타나 버그를 일으켜 개발자를 짜증나게 한다. 메소드 시작점에서 입구컷을 하거나, @NonNull같은 어노테이션을 사용하거나, null일 경우 아예 직접 인스턴스를 만드는 등의 여러 방법을 사용할 수 있다.
오늘은 그 중 하나인 Optional 클래스를 살펴보겠다!!
1. Optional이 없는 상황
Student 클래스가 있다.
@RequiredArgsConstructor
public class Student {
private final String name;
private final int num;
public boolean hasSameName(String name) {
return name.equals(this.name);
}
@Override
public String toString() {
return "학생이름 : " + name + ", 번호 : " + num;
}
}
필드로 이름인 name, 출석번호인 num을 가지고 있다. 생성자로 각 필드값을 받을 것이다. toString() 메소드도 오버라이드했다. hasSameName()은 인자로 들어오는 String 값과 필드의 이름 값이 같은지 여부를 return한다.
이 Student 클래스에 대한 리스트를 담고있는 StudentRepository가 있다.
public class StudentRepository {
private final List<Student> attendance = new ArrayList<>();
AttendanceRepository() {
attendance.add(new Student("김부추",1));
attendance.add(new Student("박부추",2));
attendance.add(new Student("이부추",3));
attendance.add(new Student("전부추",4));
attendance.add(new Student("최부추",5));
attendance.add(new Student("한부추",6));
}
public Student findStudentByName(String name) {
for (Student student : attendance) {
if (student.hasSameName(name)) {
return student;
}
}
return null;
}
}
일단 생성자로 리스트 자체를 구성했다. 이름이 김부추~한부추, 출석번호가 1번~6번인데 이건 일단 동작을 위해서 만든 더미데이터!
public한 findStudentByName() 메소드가 있는데, 메소드 이름 그대로 인자값으로 전달한 name으로 student를 찾는 메소드다. for문으로 요소를 돌아 hasSameName()을 통해 이름이 일치하는 값을 찾고 있으면 그 요소값을 return, 모든 요소를 돌아도 찾을 수 없으면 null을 return한다.
이를 사용하는 클라이언트 코드를 보자. "김부추"라는 이름을 가진 학생과, "황부추"라는 이름을 가진 학생을 각각 찾고싶다. 각각 findStudentByName()을 호출했다.
public class Client {
public static void main(String[] args) {
StudentRepository studentRepository = new StudentRepository();
Student kimBuchu = studentRepository.findStudentByName("김부추");
if (kimBuchu!=null) {
System.out.println(kimBuchu);
} else {
System.out.println("김부추라는 학생은 없는데요?");
}
Student hwangBuchu = studentRepository.findStudentByName("황부추");
if (hwangBuchu!=null) {
System.out.println(hwangBuchu);
} else {
System.out.println("황부추라는 학생은 없는데요?");
}
}
}
보면 알겠지만, 참조변수에 대해 null checking이 이뤄짐을 알 수 있다. 아.. 보기싫다. findByName()을 호출할 때마다 이런식으로 null checking을 해줘야한다. 이 사실을 깜빡하고 이 메소드를 이리저리 쓰다가 NPE 한 번 뜨면 똑같이 if !=null 작성하는데 손가락 움직일거 생각하니 힘이 빠진다.
2. Optional<Student>
유틸 패키지에 있는 Optional 클래스는 이와같은 null 처리를 "FANCY"하게 해준다. Optional은 "썬크림은 필수, 비비크림은 옵션!" 할 때 옵션이다(썩은 비유 죄송합니다). 그러니까 꼭 필수로 있어야하는 값은 아니고 null이 있을 수도 있다~~ 를 고지하는 용도의 클래스라고 받아들이면 된다.
Optional 객체의 생성은 크게 다음의 세가지 방법이 있다.
- Optional.of(<null이 아닌 객체>)
- Optional.empty()
- Optional.ofNullable(<null일 수도 있는 객체>)
StudentRepository를 Optional 객체를 return하도록 바꿔보겠다.
public class StudentRepository {
private final List<Student> attendance = new ArrayList<>();
StudentRepository() {
attendance.add(new Student("김부추",1));
attendance.add(new Student("박부추",2));
attendance.add(new Student("이부추",3));
attendance.add(new Student("전부추",4));
attendance.add(new Student("최부추",5));
attendance.add(new Student("한부추",6));
}
public Optional<Student> findStudentByName(String name) {
for (Student student : attendance) {
if (student.hasSameName(name)) {
return Optional.of(student);
}
}
return Optional.empty();
}
}
원래 코드에서 Optional로 감싼 부분이 추가됐을 뿐이다. 그래서 뭐가 달라졌냐? 클라이언트 코드를 봐야한다.
public class Client {
public static void main(String[] args) {
StudentRepository studentRepository = new StudentRepository();
studentRepository.findStudentByName("김부추").ifPresentOrElse(
System.out::println,() -> {
System.out.println("김부추라는 학생은 없는데요?");
});
studentRepository.findStudentByName("황부추").ifPresentOrElse(
System.out::println,() -> {
System.out.println("황부추라는 학생은 없는데요?");
});
}
}
if (kimBuchu!=null) 부분이 없어지고, ifPresentOrElse()라는 메소드를 쓸 수 있게 되었다. 해당 메소드는 첫번째 인자로 Consumer, 두번째 인자로 Runnable FunctionalInterface를 받는다. 람다식을 쓰도록 하는 것이다. 람다식에 대해 모른다면.. 참고.
Consumer는 Optional 객체가 null이 아닐 때 실행된다. Consumer 인자로 null이 아닌 Student객체가 들어가게 되고 해당 로직이 그대로 수행되는 것이다. System.out::println 형식의 메소드 참조를 이용했다. empty Runnable은 Optional 객체가 null일때 실행된다. 그래서 위 코드는 Optional을 쓰기 전과 100% 동일한 기능을 제공한다. 값이 있으면 해당 값을 System.out.println()하고, 없으면 "~~라는 학생은 없는데요?"라고 출력하는 것이다.
Optional<T> 객체가 null을 반환할 때, 처리할 수 있는 방법이 몇 가지가 있다.
- orElseThrow()를 통해 예외를 던지게 할 수 있다. Supplier 람다식으로 Exception 객체를 반환하게 한다.
- orElse()의 인자로 default 객체를 넣어 Optional이 null일 경우 반환할 객체를 지정할 수 있다.
- orElseGet()의 Supplier 람다식 인자로 default T타입 객체를 반환하도록 할 수 있다. 2번과 차이점은 객체 자체가 아닌 "람다식"이 들어갔다는 점이다. 만약 Optional 객체가 null이라면 해당 람다식에서 return한 값이 반환된다.
어렵지 않은 내용이지만, 각 예시를 적용한 코드를 보자.
public class DealWithOptional {
public static void main(String[] args) {
StudentRepository studentRepository = new StudentRepository();
// 1. 에러 던지기
Student null1 = studentRepository.findStudentByName("존재하지않는학생이름")
.orElseThrow(()-> new RuntimeException("존재하지 않는 학생 이름입니다!"));
// 2. default 객체 반환하기
Student null2 = studentRepository.findStudentByName("존재하지않는학생이름")
.orElse(new Student("이제존재하게된학생이름",7));
// 3. 일정로직 수행 후 default 객체 반환하기
Student null3 = studentRepository.findStudentByName("존재하지않는학생이름")
.orElseGet(() -> {
System.out.println("존재하지 않을 때 수행되는 Supplier 람다식입니다.");
return new Student("이제존재하게된학생이름",7);
});
}
}
최초로 프로젝트다운 프로젝트를 했던 언어가 자바스크립트였고, Promise 관련한 콜백 기능이 재미있었기 때문에 이런 람다식 관련 기능을 살펴보는건 언제나 즐겁다 ㅋㅋ
3. Optional 주의.
Optional에서 사용하면 안되는 안티패턴이 있다.
public class Client {
public static void main(String[] args) {
StudentRepository studentRepository = new StudentRepository();
Optional<Student> kimBuchu = studentRepository.findStudentByName("김부추");
if (kimBuchu.isPresent()) {
System.out.println(kimBuchu);
} else {
System.out.println("김부추라는 학생은 없는데요?");
}
}
}
왜인지는 알겠지? Optional의 isPresent()는 해당 Optional 객체의 객체값이 null이 아닐때 true를 반환한다. 코드를 위같이 쓰면 결국 Optional을 쓰지 않은 최초의 코드랑 똑같아진다. isPresent()는 분명 유용하게 쓰일 때가 있겠지만, 적어도 위와 같은 상황은 아니다. null 처리는 2번에서 설명한 방법중 상황에 맞는 것을 사용하도록 하자.
그리고 사실, StudentRepository에서 Optional을 반환한 것도 어떻게보면 안티패턴이다. 메소드에서 직접 Optional.empty()를 반환하는 것은 null을 반환하는 것과 크게 다르지 않기 때문이다. 그런 상황이라면 그쪽에서 예외를 던지거나 default 객체를 만드는 편이 낫다. Repository 코드를 좀 더 괜찮게 수정해봤다.
public class StudentRepository {
private final List<Student> attendance = new ArrayList<>();
StudentRepository() {
attendance.add(new Student("김부추",1));
attendance.add(new Student("박부추",2));
attendance.add(new Student("이부추",3));
attendance.add(new Student("전부추",4));
attendance.add(new Student("최부추",5));
attendance.add(new Student("한부추",6));
}
public Optional<Student> findStudentByName(String name) {
return attendance.stream()
.filter(student -> student.hasSameName(name))
.findAny();
}
}
Steram API를 이용했다. filter()은 Predicate 람다식을 만족하는 객체만 다음 스트림으로 넘겨준다는 사실을 기억해라! findAny()를 통해 일치하는 값이 하나라도 있다면 그 값을 return하고, 없으면 null을 return할 것이다. 하는 일은 2번의 findStudnetByName() 메소드와 100% 동일하다. 그렇지만 더 짧고, 가독성있는 코드가 되었다!