[영상후기] [10분 테코톡] 조이썬의 Java Optional

박철현·2024년 8월 12일
0

영상후기

목록 보기
150/160

movie

Java8 이전의 개발

첫 번째 - Null 가능성을 인지하지 못한 경우

public class Deck {
	private final Deque<Card> value;
    
    public Deck(final Deque<Card> cards) {
    	this.value = cards;
    }
    
    public Card draw() {
        Card card = this.value.pollLast();
        // pollLast()의 특징(큐가 비워있으면 null 반환)을 알지 못한 경우에는 null 예외를 만나게 된다.
        // 확인하지 인지 못하여 예외 처리를 미리 하지 못함
        if(card == null) {
            throw new IllegalStateException("카드덱에 카드가 없습니다!");
        }
        return card;
    }
}
  • deque의 메서드 중 pollLast() : 큐의 마지막 요소 추출

    the tail of this deque, or null if this deque is empty

    • queue가 비워있는 상태면 null을 반환한다.
  • Deque의 pollLast() 메서드의 주석을 보기 전까지 null 반환하는지 여부를 몰라 나중에 null인 경우를 만나고 나서야 예외 처리를 함

두 번째 - 중첩 참조를 통해 특정 객체가 null일 수도 있을 때 예외를 막기 위해 if문 지옥

관계 설명

  • 사람은 자동차가 있음
    • 자동차엔 또 보험이 있음
      • 보험엔 정보가 있음
        • 정보 안에 name을 가짐
public class Person {
	private final String name;
    // 1. 사람 안에 자동차
	private final Car car;

	public Person(final String name, final Car car) {
		this.name = name;
		this.car = car;
	}

	public String getGetCarInsuranceName() {
		return car.getInsurance()
			.getInfo()
			.getName();
	}

	static class Car {
    	// 2. 자동차 안에 보험
		private final Insurance insurance;

		public Car(final Insurance insurance) {
			this.insurance = insurance;
		}

		public Insurance getInsurance() {
			return insurance;
		}
	}

	static class Insurance {
    	// 3. 보험 안에 정보
		private final Info info;

		public Insurance(final Info info) {
			this.info = info;
		}

		public Info getInfo() {
			return info;
		}
	}

	static class Info {
    	// 4. 정보 안에 이름
		final String name;

		public Info(final String name) {
			this.name = name;
		}

		public String getName() {
			return name;
		}
	}
}

getCarInsuranceName을 사용해볼까?

public String getGetCarInsuranceName() {
	return car.getInsurance()
		.getInfo()
		.getName();

문제 상황 1 - 조이썬은 차가 없어 Car null로 객체 생성

@Test
@DisplayName("1번 id에 있는 회원의 이름은 조이썬이고, 자동차 보험명은 삼성 자동차 보험 이다.")
void test() {
	Person person = repository.findById(1);
    assertThat(person.getName()).isEqualTo("조이썬");
    assertThat(person.getGetCarInsuranceName()).isEqualTo("삼성 자동차 보험");
}

"this.car " is null -> NullPointerException 발생

  • 문제 상황 : 조이썬은 차가 없어서 null을 넣어둔듯
persons.add(new Person("조이썬", null));

문제 상황 2 - 차가 있지만 보험이 없어 NullPointException 발생

@Test
@DisplayName("조이썬의 차는 벤츠이고, 자동차 보험명은 현대 자동차 보험 이다.")
void test2() {
	Person person = repository.findByName("조이썬");
    assertThat(person.getCarName()).isEqualTo("벤츠");
    assertThat(person.getGetCarInsuranceName()).isEqualTo("현대 자동차 보험");
}

Cannot Invoke "Insurance.getName() " because the return value of "Car.getInsurance()" is null

  • 문제 상황 : 차가 있지만 보험이 null이기 때문에 NullPointerException 발생

아오 화딱지 ! - if 문 지옥 Hi

  • 이는 Insurance, Info, Name 이 각각 null 여부를 다 검사해야 한다.
    public String getCarInsuranceName() {
    	if (car != null) {
      	if (car.getInsurance() != null) {
          	if (car.getInsurance().getInfo() != null) {
              	return car.getInsurance().getInfo().getName();
              }
              throw new IllegalStateExceptipn("보험 정보가 없습니다");
          }
          throw new IllegalStateExceptipn("보험이 없습니다");
      }
      throw new IllegalStateExceptipn("차가 없습니다!");
    }

Java 8 이전 문제점

  • null을 반환할수 있는걸 알려줄수없을까?
  • if문으로 계속 null이 아닌지 확인해야할까?

Optional이란?

  • Java 8에서 도입되어 값을 감싸는 래퍼 클래스

    Cat -> Optional<Cat>

  • 고양이가 있으면? -> 고양이를 꺼내서 밥을 준다

  • 고양이가 없으면? -> 고양이가 없다고 경고(예외) 한다 or 새로운 고양이를 가져온다.

Optional의 정적 팩토리 메소드

메소드의미
of(T t)값이 null이 될 수 없는 Optional 생성, null이 들어오면 NullPointerException 발생
ofNullable(T t)값이 null일 수 있는 Optional 생성
empty()값이 없는 Optional 생성

of - 생성 단계에서 빠른 감지가 가능

  • null이 들어오면 NullPointerException이 터지기 때문에 빠른 감지 가능
@Test
void ofWhenNpeTest() {
	Person person = null;
    Assertions.assertThatThrownBy(() -> {
    	Optional.of(person);
        })
        .isInstanceOf(NullPointerException.class);
}

ofNullable - null 포인터 함수 호출 시 발생

  • null도 감쌀 수 있는 Optional 객체를 생성하기에 null에 접근할 때 예외 발생
@Test
void ofWhenNpeTest() {
	Person person = null;
    assertThatCode(() -> {
    	Optional.ofNullable(person);
        }).doseNotThrowAnyException();
    assertThatThrownBy(() -> person.getCarName())
    	.isInstanceOf(NullPointerException.class);
}

Optional 메서드

메소드의미
get()값을 반환, 존재하지 않으면 NoSuchElementException 발생
isPresent() / isEmpty()값이 존재하면, 존재하지않으면 참을 반환, 그렇지 않으면 거짓을 반환
ifPresent(Consumer<? super T> action)값이 존재하면 주어진 동작을 값과 함께 실행
orElse(T other)값이 존재하면 해당 값 반환, 존재하지 않으면 다른 값 반환
orElseGet(Supplier<? extends T> supplier)값이 존재하면 해당 값 반환, 존재하지 않으면 제공된 함수로 생성된 값 반환
orElseThrow() (Supplier<? extends X> exceptionSupplier)값이 존재하면 해당 값 반환, 존재하지 않으면 예외 발생

Optional 잘 사용하기? 개발자의 의도대로 쓰는것이 best긴해

  • 람다 표현식의 사양 책임 개발자
    • 반환 결과값이 없음을 명확하게 표현할때
    • null을 반환할 때 발생하는 위험을 처리할 때

안티 패턴1. 불필요한 Optional

public class Deck {
	private final Deque<Card> value;
	
	public Deck(final Deque<Card> cards) {
		this.value = cards;
	}

	public Optional<Card> draw() {
		// null이 될 수도 있다고 반환
		return Optional.ofNullable(this.value.pollLast());
	}
}
deck.draw()
	.orElseThrow(() -> new IllegalStateException("카드가 없네?"));
  • draw() 하는 부분에서는 단순히 card 한장을 원했는데
    • 불필요한 메모리 낭비
    • 불필요한 책임 전파
  • null에 대한 책임을 어디서 수행해야 하는지 & null을 전파 해야만 하는지를 고려해야 한다.
public Card draw() {
        Card card = this.value.pollLast();
        if(card == null) {
            throw new IllegalStateException("카드덱에 카드가 없습니다!");
        }
        return card;
    }
  • Null인지 확인하고 Null에 대한 책임을 전파하지 않아 위 코드가 올바른 패턴

안티 패턴 2 - isPresent-get을 피하자

@Test
void avoidPattern() {
	final Optional<Person> optionalPerson = personMemoryRepository.findById(0);
	if(optionalPerson.isPresent()) {
		final Person person = optionalPerson.get();
		assertThat(person.getName()).isEqualTo("제우스");
	}
}
  • 불필요한 지역 변수들 선언 + 변수명 고민..
    • optionalPerson 과 person 두개의 변수가 필요해짐

ifPresent로 해결 가능

@Test
void avoidPattern() {
personMemoryRepository.findById(0)
						.ifPresent(person -> 
							assertThat(person.getName()
								.isEqualTo("제우스"));
  • optionalPerson 과 person 이라는 불필요 지역변수 미선언 -> 지역변수명 고민 줄임

없을 시 예외를 던지고 싶다면?

@Test
void avoidPattern() {
	final int id = 0;
	final Person person = personMemoryRepository.findById(id)
						.orElseThrow(() -> new NotExistPersonException(id));

안티 패턴3 - orElse, orElseGet 구분

  • orElse(T other) : 존재하지 않으면 다른 값 반환
  • orElseGet(Supplier<? extends T> supplier) : 존재하지 않으면 제공된 함수로 생성된 값 반환

orElse 함수를 넣으면?

public Post create(int humanId, String content) {
	Optional<String> optionalName = getHumanNameById(humanId);
	String name = optionalName.orElse(randomName());
	return new Post(name, content);
}

private String randomName() {
	log.info("random 이름이 생성되었습니다");
	return "randomName";
}
  • 코드 실행 : 값이 있든 없는 항상 함수 실행!
    • 상수, 변수 값이 필요할 때는 orElse 사용
public Post create(int humanId, String content) {
	Optional<String> optionalName = getHumanNameById(humanId);
	String name = optionalName.orElse(DEFAULT_NAME);
	return new Post(name, content);
}

private String randomName() {
	log.info("random 이름이 생성되었습니다");
	return "randomName";
}
  • 메서드 호출이 필요할 때는 orElseGet 사용
public Post create(int humanId, String content) {
	Optional<String> optionalName = getHumanNameById(humanId);
	String name = optionalName.orElseGet(this::randomName);
	return new Post(name, content);
}

private String randomName() {
	log.info("random 이름이 생성되었습니다");
	return "randomName";
}

안티 패턴4. Optional은 return 타입으로만

public class OptionalPerson {
	private final String name;
	// car가 있을수도 없을수도 있으니 Optional 매핑
	private final Optional<OptionalCar> car;

	public OptionalPerson(String name, Optional<OptionalCar> car) {
		this.name = name;
		this.car = car;
	}

	public Optional<OptionalCar> getCar() {
		return car;
	}

	public String getCarName() {
		return car.orElseThrow( () -> new IllegalStateException("차가 없습니다.") )
	.getName();
  • 생성자에 Optional 넘겨주고 Get할때만 return 하면 되지 않냐?
@Test
void avoidPattern() {
	OptionalPerson person = new OptionalPerson("제리",
		Optional.ofNullable(new OptionalCar("스타렉스", Optional.ofNullable(null))));
}
  • 호출할때 불필요한 Optional 생성해 전달해야 함
    • 생성자 & 메서드 매개변수로 사용하지 말자
  • Optional은 직렬화 인터페이스 구현X
    • 멤버 변수로 사용하지 말자!!
  • return type에만 사용하는 것이 바람직함
public class Person {
	private final String name;
	private final Car car; // null 일 수 있음

	public Person(final String name, final Car car) {
		this.name = name;
		this.car = car;
	}

	public Optional<Car> getCar() {
		return Optional.ofNullable(car);
	}
}

안티 패턴5. API를 잘 사용하자

  • Car, Insurance 모두 null이 될 수 있으니 Optional 래핑을 해볼까?
public class Person {
	private final String name;
	private final Car car; // Null 일 수 있음

	public Person(final String name, final Car car) {
		this.name = name;
		this.car = car;
	}

	public Person(String name) {
		this.name = name;
		this.car = null;
	}

	public Optional<Car> getCar() {
		return Optional.ofNullable(car);
	}

getCarInsuranceName을 다시 구현하자

public String getCarInsuranceName() {
	Optional<Car> car = getCar();
	if(car.isPresent()) {
		Optional<Insurance> insurance = car.get().getInsurance();
		if(insurance.isPresent()) {
			Optional<Info> info = insurance.get().getInfo();
			if(info.isPresent()) {
				return info.get().getName();
			}
		}
	}
	throw new IllegalStateException("조건에 충족하지 못합니다.");
}
  • 계속 있는지 여부 확인하는 불필요 로직 반복

flatMap과 Map을 통해 깔끔하게 정렬 가능

public String getGetCarInsuranceName() {
	return Optional.ofNullable(car)
				.flatMap(Car::getInsurance)
				.flatMap(Insurance::getInfo)
				.map(Info::name)
				.orElseThrow( () -> new IllegalArgumentException("조건에 충족하지 못합니다") );
  • 둘 다 Optional을 통해 값을 변환하는 경우 사용

    • flatMap : 함수가 Optional을 반환하는 경우
    • map : 함수가 객체를 반환하는 경우
  • Stream API와 Method Chaining 역시 가능

public int findJockerCardValue() {
	return value.stream()
				.filter(Card::isJoker)
				.findFirst()
				.map(card -> card.value())
				.orElseThrow( () -> new IllegalStateException("조커 카드가 없습니다") );
profile
비슷한 어려움을 겪는 누군가에게 도움이 되길

0개의 댓글

관련 채용 정보