DataJpa의 Optional

KOSE·2022년 10월 24일
0

spring

목록 보기
3/9
post-thumbnail

안녕하세요. DataJpa를 사용할 때, 자주 접하는 Optional에 대해 정리하고자 글을 작성하게 되었습니다.

Optional

Java8에서는 Optional 클래스 제공하여 NullPointException을 방지할 수 있도록 돕습니다. Optional는 null이 올 수 있는 값을 감싸는 Wrapper 클래스로, 해당 값을 참조하더라도, NPE가 발생하지 않고, Optional에서 제공하는 null 값 처리, 예외 처리 등 메소드를 활용할 수 있습니다.

1. Optional 생성

//Optional.class

public final class Optional<T> {
  private static final Optional<?> EMPTY = new Optional();
  private final T value;

  private Optional() {
      this.value = null;
  }

  public static <T> Optional<T> empty() {
      Optional<T> t = EMPTY;
      return t;
  }
}

Optional은 Optional.empty()로 생성할 수 있습니다. 특징은 빈 객체를 여러 번 생성하더라도, EMPTY가 static final로 선언되어 있으므로, 1개의 EMPTY 객체를 공유하고 있습니다.

> Optional<String> optionalString = Optional.empty();

2. Optional.of()

Optioanl.of는 인스턴스가 null이 아닐 때 사용 가능합니다. 만약 인스턴스가 null이라면 NPE가 발생합니다.

3. Optional.ofNullable()

Optional.ofNullable()이 Optional를 사용하는 가장 큰 이유라고 생각합니다. ofNullable()을 활용하면, value가 null일 때 NPE를 발생시키지 않고 예외 처리를 할 수 있습니다. value에 값이 존재한다면, Optional의 value에 해당 value로 업데이트합니다.

> public static <T> Optional<T> ofNullable(T value) {
        return value == null ? empty() : of(value);
    }

4. 그 외의 메소드

isPresent(), isEmpty()는 Optional이 값이 있는지, null인지 판단하는 메소드이고, orElseThrow()는 해당 값이 null인경우 Exception을 발생시키는 메소드입니다. orElseThrow()는 자세히 다루도록 하겠습니다.

DataJpa의 Optional

DataJpa는 자바의 ORM인 Jpa를 편리하게 사용할 수 있도록 지원하는 프로젝트입니다.

1. DataJpa 특징

  • CRUD 처리를 위한 공통 인터페이스를 제공합니다 (save, findById, findAll, findBy***)
  • repository 개발 시 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 생성하여 스프링 빈에 주입합니다.
  • 만약 공통 메소드가 아니라면, 스프링 데이터 JPA가 메소드 이름을 분석하여 JPQL로 변환해서 실행합니다.

2. DataJpa를 활용한 MemberRepository 생성

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {

    @Override
    Optional<Member> findById(Long id);
    
}

스프링 데이터JPA는 JpaRepository를 확장하여 인터페이스로 설계합니다. 사용법은 제네릭에 repository를 구현하고 싶은 클래스와 키값의 타입을 선언하여 작성합니다 <Member, Long>

3. Member 클래스 생성

DataJpa를 활용하기 위해 Member 클래스를 선언하겠습니다.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {

    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String username;

    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    @Builder
    public Member(String username, int age) {
        this.username = username;
        this.age = age;
    }

    @Builder
    public Member(String username, int age, Team team) {
        this.username = username;
        this.age = age;
        if (team != null) {
            updateTeam(team);
        }
    }

    public void updateUsername(String username) {
        this.username = username;
    }

    public void updateAge(int age) {
        this.age = age;
    }

    public void updateTeam(Team team) {
        if (this.team != null) {
            this.team.getMembers().remove(this);
        }

        this.team = team;
        team.getMembers().add(this);
    }
}

코드 분석
Lombok을 활용하여, Getter, NoArgsConstructor(Access=AccessLevel.PROTECTED)를 선언하였습니다. NoArgsConstructor는 필드가 없는 기본 생성자를 생성하는 어노테이션입니다. Access는 해당 기본 생성자의 범위를 protected로 제한하는 키워드입니다.
@Builder 어노테이션으로 생성자를 선언하였고, 변경 가능한 필드는 update*** 메소드로 구현하여 Setter의 사용을 제한했습니다.

4. DataJpa를 활용한 예외 처리

DataJpa를 활용하여 데이터베이스로부터 데이터를 획득하면, 한가지 문제점이 존재합니다. 위에서 보신 바와 같이, Optional로 타입이 설정되어 있으므로, 이를 Member로 캐스팅하는 작업이 필요합니다. 하지만, Optional가 null일 수도 있으므로 단순하게 Member로 캐스팅하게 되면 NPE가 발생합니다. 따라서, 예외 처리하는 과정이 필요합니다.
데이터JPA를 활용하면 try catch로 작성했던 작업을 단순화 시킬 수 있습니다.

5. orElseThrow()

    private Optional<Member> findMemberOrThrowError(long id) {
        return Optional.ofNullable(memberRepository.findById(id)
                .orElseThrow(() -> {throw new IllegalArgumentException("member not found");}));
    }

이때, 사용하는 것이 orElseThrow입니다. 해당 객체가 null인 경우, 예외를 발생시키고, 문제가 없다면 그대로 리턴하는 방식입니다. 이를 활용하면, 값의 유무에 따라 해당 객체를 활용할 수 있습니다.

6. 테스트 코드 작성하기 - Junit5 예외 처리

junit5에서는 예외처리를 다양한 방식으로 제공합니다.
여기서 활용할 방법은 2가지로, 특정 상황의 발생 예외의 클래스를 판별하는 assertThrows()
해당 예외의 메세지를 판별하는 assertThrows() 두 가지 입니다.

  1. Assertions.assertThrows()
     org.junit.jupiter.api.Assertions.assertThrows(
                예외.class, () -> {
                    예외 상황 기술;
                }
        );
  1. Assertions.assertThrows() // 메세지 파악
        Throwable exception = org.junit.jupiter.api.Assertions.assertThrows(
                예외클래스.class, () -> {
                    예외 처리 상황
                });

        assertEquals("예외 발생시 메세지", exception.getMessage());

재밌는 점은, assertThrows의 예외.class에 부모 타입의 클래스를 넣어도 통과한다는 점입니다. 하지만, 테스트 결과 자식 클래스에는 해당되지 않는 점을 발견했습니다. (이 부분은 추후 포스팅으로 다루겠습니다.)

// NotMemberException

public class NotMemberException extends IllegalArgumentException{

    public NotMemberException() {
        super("member not found");
    }

}

// MemberRepository.test
    @Test
    public void 멤버없음_에러() throws Exception {
        //given
        Member member = Member.builder().
                username("ko")
                .build();

        //when

        org.junit.jupiter.api.Assertions.assertThrows(
                IllegalArgumentException.class, () -> {
                    findMemberOrThrowError(4L);
                }
        );

        Throwable exception = org.junit.jupiter.api.Assertions.assertThrows(
                IllegalArgumentException.class, () -> {
                    Optional<Member> findMember = findMemberOrThrowError(4L);
                });

        //then
        assertEquals("member not found", exception.getMessage());
    }

    private Optional<Member> findMemberOrThrowError(long id) {
        return Optional.ofNullable(memberRepository.findById(id)
                .orElseThrow(() -> {throw new NotMemberException();}));
    }

이는 테스트코드의 다형성을 구현하기 위한 방법이라고 생각이 듭니다...(개인적인 생각입니다...!)
만약, 해당 예외 처리에 대한 정책이 바뀌더라도, 상위 클래스를 상속받는 경우 해당 로직이 수행되는데 이상이 없도록 하는 방법이라고 생각합니다.

Optional은 DataJpa를 다룰 때 필수이자 꼭 숙지해야 하는 개념입니다.
추후 계속 Optional 처리를 공부하며 더 좋고 깔끔한 코드를 작성할 수 있도록 하겠습니다.

혹시 잘못된 부분이 있다면 댓글 작성 부탁드립니다.!!!
읽어주셔서 감사합니다.!!

참고자료:
인프런 김영한님 강의: 실전! 스프링 데이터 JPA
망나니 개발자님 tistory: https://mangkyu.tistory.com/70

profile
회사와 함께 성장하는 개발자가 되고싶습니다.

0개의 댓글