반응형

가독성 높이는 습관 - 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 사용도 지양 할 것

 

 


반응형

+ Recent posts