반응형
가독성 높이는 습관 - Optional 제대로 사용하기
[T] Wrapper Class -> Optional<T>
값이 없을 수도 있으면 Optional 사용하기
메서드
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findById(Long id);
}
@Entity(name = "user")
public class User {
@Id
private Long id;
@Column
private String name;
@Column
private String thumbnail;
publi Optional<String> getThumbnail() {
return Optional.ofNullabe(thumbnail);
}
//생략
메서드 (주의할점 - Optional 대신 null 리턴 절대금지)
// 절대 하면 안됨
public Optional<String> getThumbnail() {
if (thumbnail == null)
return null;
return Optional.of(thumbnail);
}
public static void main(String[] args) {
User user = new User();
user.getThumbnail() // NPE 발샐
.ifPresent(System.out::println);
}
- Thumbnail이 null일 때, null을 리턴
- user.getThumbnail().ifPresent()로 체이닝을 통해서 접근하는데 ifPresent()에서 NPE 발생
// 절대 하면 안됨
public Optional<String> getThumbnail() {
if (thumbnail == null)
return null;
return Optional.of(thumbnail);
}
public static void main(String[] args) {
User user = new User();
// 리턴 값을 믿을 수 없으니 null 체크
if (user.getThumbnail() != null)
// 결과 값이 Optional인 이상한 상황
user.getThumbnail()
.ifPresent(System.out::println);
}
- user의 getThumbnail이 null이 아닐 때, Optional이 리턴
- user.getThumbnail()을 통해서 Optional의 어떤 메서드들을 체이닝해서 접근
- null 회피를 위하여 Optional을 사용했지만, 또다시 Optional을 null check 하고 있음
- 이러한 문제를 피하기 위해서는 리턴 값에 null 대시 Optional.empty()를 사용해야 함
메서드 - 기존의 시스템의 리턴 값이 null을 리턴한다면?
public interface UserRepository extends JpaRepository<User, Long> {
User findByName(String name);
// 생략
static void main(String[] args) {
Optional.ofNullable(userRepository.findByName("name"))
.ifPresent(System.out::println);
}
- 올바른 코드였다면, User가 아닌 OptionalUser로 리턴했어야 함
- 만약 프로젝트의 레거시 버전이 부트 1.x 버전을 사용하고 있다면,
- CRUD 레파지토리에 아직 findById 메서드가 존재하지 않기 때문에 해당 메서드들을 직접만들어 써야함
- 이런 레거시 시스템이라면 아마도 Optional을 리턴하지 않고 위의 findBy 메서드처럼 직접 객체를 리턴했을 확률이 높음
- 이를 해결하기 위해서 가장 좋은 방법은 User 대신 OptionalUser를 리턴하는 방식으로 리팩터링하는 것
- 하지만 해당 메서드를 호출하는 코드가 많다면 선뜻 리팩터링하기가 굉장히 어려움
- NPE없이 잘 돌아가고 있다면, 우선은 우회하는 방식으로 코드를 수정
필드에 Optional은 지양하기 - Anti-Pattern
public class UserDto {
private Long id;
private String name;
@Getter
private Optional<String> thumbnail;
public static void main(String[] args) {
UserDtro user = UserDto;
user.getThumbnail()
.ifPresent(/* 비지니스 로직 추가 */);
}
- 1. Optional은 함수의 반환을 목적으로 만들어짐 (API note)
- 2. Serializable 구현하지 않았으므로 직렬화되지 않음
- 3. Optional.empty()를 누락할 가능성 높음 (휴먼 에러)
@Builder
@ToString
public class User {
private Long id;
private String name;
private Optional<String> thumbnail = Optional.empty();
public static void main(String[] args) {
System.out.println(User.builder().build());
}
- 빌더를 사용하게되면 default값이 들어가지 않게 됨
@Builder
@ToString
public class User {
private Long id;
private String name;
private Optional<String> thumbnail;
public User(Long id, String name, Optional<String> thumbnail) {
this.id = id;
this.name = name;
this.thumbnail = (thumbnail == null) ? Optional.empty() : thumbnail;
}
public static void main(String[] args) {
System.out.println(User.builder().thumbnail(null).build());
}
- 별도의 생성자를 만들고 매번 null check를 하고 Optional.empty()를 할당해야 함
@ToString
public class User {
private Long id;
private String name;
private Optional<String> thumbnail;
@Builder
public User(Long id, String name, String thumbnail) {
this.id = id;
this.name = name;
this.thumbnail = Optional.ofNullable(thumbnail);
}
public static void main(String[] args) {
System.out.println(User.builder().thumbnail(null).build());
}
- 최종적으로 생성자를 Optional이 아닌 원래 타입으로 받고,
- 이 값 자체를 Optional.ofNullable로 감싸서 Builder로 만들게 되면 원한는 값을 받을 수는 있음
Optional 과 Collection
비어있는 컬렉션을 표현할 때는 Collections.emptyList() 사용하기
// 절대 따라하지 말 것
Optional<List<User>> findAllByName(String name);
- Collection일 때는 굳이 Optional로 감싸거나 null로 리턴하지 않고 비어있는 Collection을 리턴하는 할 것
컬렉션의 요소에 Optional은 절대 금지
// 절대 따라하지 말 것
List<Optional<User>> findMaybeAllByName(String name);
Optional을 파라미터로 넘기지 않기
int countByUser(String name, Optional<Integer> age);
static void main(String[] args) {
userService.countByUser("이름", Optional.of(20));
// 사용하는 파라미터도 옵셔널로 감싸서 넘겨야함
userService.countByUser("이름", Optional.empty());
// 사용하지 않는 파라미터도 옵셔널을 넘겨야함
}
int countByUser(String name);
int countByUser(String name, int age);
static void main(String[] args) {
userService.countByUser("이름", 20);
userService.countByUser("이름");
}
Optional 값을 가져올 때 .get()은 되도록 지양하기
@Getter
@ToString
public class User {
private Long id;
private String name;
private Optional<String> thumbnail = Optional.empty();
public static void main(String[] args) {
System.out.println(user.getThumbnail().get());
}
}
Optional의 중간, 종단 메서드를 쓰기 위해서 불필요한 옵셔널은 사용하지 않기
public static UserDto convertDto(User user) {
return Optional.ofNullable(user)
.filter(it -> !user.isAdult());
.map(UserDto::convert)
.orElseThrow(() -> new RuntimeException("성인이 아닙니다."));
}
- Optional을 체이닝한 코드
public static UserDto convertDto(User user) {
if (!user.isAdult())
throw new RuntimeException("성인이 아닙니다.");
return UserDto.convert(user);
}
- 체이닝 없이 User객체에 직접 접근하여서 확인한 코드
정리
- 결과가 없을 때는 null 대신 Optional.empty()
- 어떠한 일이 있어도 Optional.empty()는 null로 리턴하면 안됨
- 중간 / 종단 메서드로 안전하게 처리 가능
- 기존 시스템이 null을 리턴하면 Optional.ofNullable로 감싸서 사용
- Optional은 함수의 반환타입을 위해 만들어짐
- 따라서, Optional을 필드에 사용할 때는 한번 더 고민 필요
- 컬렉션은 Optional 대신 비어있는 컬렉션 사용
- 컬렉션의 요소로 Optional 사용도 지양 할 것
반응형
'cs > java-spring-boot' 카테고리의 다른 글
[Zero-base] 9-13. 심플하게 구성하기 (0) | 2022.03.17 |
---|---|
[Zero-base] 9-12. Side effect 줄이기 (0) | 2022.03.17 |
[Zero-base] 9-10. Optional 살펴보기 + Java 8 : functional interface (0) | 2022.03.16 |
[Zero-base] 9-9. null 핸들링 (0) | 2022.03.16 |
[Zero-base] 9-8. null (0) | 2022.03.16 |