코드스테이츠

코드스테이츠 51일차

안형준 2022. 7. 7. 18:07

학습 목표

  • Spring Data JPA가 무엇인지 이해할 수 있다.
  • Spring Data JPA를 이용해서 데이터의 저장, 수정, 조회, 삭제 작업을 할 수 있다.
  • JPA의 JPQL을 Spring Data JPA에서 사용할 수 있다.

 

Spring Data JPA를 통한 데이터 액세스 계층 구현

Spring Data JPA란?

Spring Data 패밀리 기술 중 하나로써, JPA 기반의 데이터 액세스 기술을 좀 더 쉽게 사용할 수 있게 해주기때문에 데이터 액세스 계층의 구현에 있어 여러분의 개발 시간을 단축 시켜줄 것이다.

 

JPA vs Hibernate ORM vs Spring Data JPA

  • JPA의 경우 이름 자체는 Jakarta Persistence API(또는 Java Persistence API)라서 마치 API를 가져다 쓸 수 있는건가라는 생각이 들 수 있지만 JPA는 엔터프라이즈 Java 애플리케이션에서 관계형 데이터베이스를 사용하기 위해 정해 놓은 표준 스펙(사양 또는 명세, Specification)이다

       ’이 기술은 무엇이고, 이 기술은 이렇게 이렇게 구현해서 사용하면 돼’ 라고 적어 놓은 기술 명세라고 생각하자

  • Hibernate ORM은 JPA라는 표준 스펙을 구현한 구현체이다. 실제 우리가 사용할 수 있는 API라고 생각하자
  • Spring Data JPA는 JPA 스펙을 구현한 구현체의 API(일반적으로 Hibernate ORM)를 조금 더 쉽게 사용할 수 있도록 해주는 모듈이다. 우리는 이 Spring Data JPA를 사용하여 데이터 액세스 계층을 구현하면 된다.

 

엔티티 클래스 정의

Member 엔티티 클래스

// (1) 추가된 부분

    @Enumerated(value = EnumType.STRING)

    @Column(length = 20, nullable = false)

    private MemberStatus memberStatus = MemberStatus.MEMBER_ACTIVE;



    @Column(nullable = false)

    private LocalDateTime createdAt = LocalDateTime.now();



    @Column(nullable = false, name = "LAST_MODIFIED_AT")

    private LocalDateTime modifiedAt = LocalDateTime.now();



    @OneToMany(mappedBy = "member")

    private List<Order> orders = new ArrayList<>();



    public Member(String email) {

        this.email = email;

    }



    public Member(String email, String name, String phone) {

        this.email = email;

        this.name = name;

        this.phone = phone;

    }



    public void addOrder(Order order) {

        orders.add(order);

    }



    // (2) 추가 된 부분

    public enum MemberStatus {

        MEMBER_ACTIVE("활동중"),

        MEMBER_SLEEP("휴면 상태"),

        MEMBER_QUIT("탈퇴 상태");



        @Getter

        private String status;



        MemberStatus(String status) {

           this.status = status;

        }
  • (1)은 회원의 상태를 저장하기 위해 추가된 enum 필드이다. 처음 회원이 등록될 때의 기본 값은 MemberStatus.MEMBER_ACTIVE 이고, 일정 기간이 지나서 회원의 활동이 없거나 회원이 탈퇴를 원하면 ‘MEMBER_SLEEP(휴면 상태)’, ‘MEMBER_QUIT(탈퇴 상태)’ 등으로 변경될 것이다.
  • (2)는 (1)에서 정의된 memberStatus에서 사용하는 MemberStatus enum이다. 현재는 ‘MEMBER_ACTIVE(활동중)’, ‘MEMBER_SLEEP(휴면 상태)’, ‘MEMBER_QUIT(탈퇴 상태)’ 이렇게 총 세 개의 회원 상태를 사용할 수 있도록 구성되어 있다.

 

Coffee 엔티티 클래스

// (1) 추가된 부분

    @Enumerated(value = EnumType.STRING)

    @Column(length = 20, nullable = false)

    private CoffeeStatus coffeeStatus = CoffeeStatus.COFFEE_FOR_SALE;



    @Column(nullable = false)

    private LocalDateTime createdAt = LocalDateTime.now();



    @Column(nullable = false, name = "LAST_MODIFIED_AT")

    private LocalDateTime modifiedAt = LocalDateTime.now();



    // (2) 추가 된 부분

    public enum CoffeeStatus {

        COFFEE_FOR_SALE("판매중"),

        COFFEE_SOLD_OUT("판매중지");



        @Getter

        private String status;



        CoffeeStatus(String status) {

            this.status = status;

        }
  • (1)은 커피의 상태를 저장하기 위해 추가된 enum 필드이다. 처음 커피 정보가 등록될 때의 기본 값은 CoffeeStatus.COFFEE_FOR_SALE이다. 만일 해당 커피에 대한 재료가 떨어졌거나 기계 고장 등으로 해당 커피를 판매할 수 없을 경우에는 COFFEE_SOLD_OUT(판매중지) 으로 상태를 변경할 수 있다.
  • (2)는 (1)에서 정의된 coffeeStatus에서 사용하는 CoffeeStatus enum이다. 현재는 ‘COFFEE_FOR_SALE(판매중)’, ‘COFFEE_SOLD_OUT(판매중지)’ 이렇게 두 개의 커피 상태를 사용할 수 있도록 구성되어 있다.

 

리포지토리(Repository) 인터페이스 구현

MemberRepository

public interface MemberRepository extends JpaRepository<Member, Long> {//(1) 수정된 부분

    Optional<Member> findByEmail(String email);

}

위 코드는 JPA가 적용된 MemberRepository 인터페이스이다.

JpaRepository를 상속하지 않고, CrudRepository를 상속해도 되지만 JpaReposiroty가 JPA에 특화된 더 많은 기능들을 포함하고 있기 때문에 JpaReposiroty를 상속했다.

 

CoffeeRepository

public interface CoffeeRepository extends JpaRepository<Coffee, Long> {//(1) 수정된 부분

    Optional<Coffee> findByCoffeeCode(String coffeeCode);



    // (2) 수정된 부분

//    @Query(value = "FROM Coffee c WHERE c.coffeeId = :coffeeId")  // (2-1)

//    @Query(value = "SELECT * FROM COFFEE WHERE coffee_Id = :coffeeId", (2-2)

// nativeQuery = true)

    @Query(value = "SELECT c FROM Coffee c WHERE c.coffeeId = :coffeeId")  // (2-3)

    Optional<Coffee> findByCoffee(long coffeeId);

}
  • (2)에 대한 관찰

ㄴJPA에서는 복잡한 검색 조건을 지정하기 위한 몇 가지 방법을 제공한다.

ㄴJPQL을 통한 객체 지향 쿼리 사용

ㄴJPA에서는 JPQL이라는 객체 지향 쿼리를 통해 데이터베이스 내의 테이블을 조회할 수 있다.

ㄴJPQL은 데이터베이스의 테이블을 대상으로 조회 작업을 진행하는 것이 아니라 엔티티 클래스의 객체를 대상으로 객체를 조회하는 방법이다.

ㄴJPQL의 문법을 사용해서 객체를 조회하면 JPA가 내부적으로 JPQL을 분석해서 적절한 SQL을 만든 후에 데이터베이스를 조회하고, 조회한 결과를 엔티티 객체로 매핑한 뒤에 반환한다.

ㄴ(2-3)은 JPQL을 사용해서 coffeeId에 해당하는 커피 정보를 조회하고 있다. JPQL의 쿼리문을 보면 SQL과 유사하지만 차이점이 있으니 주의하자

ㄴJPQL은 객체를 대상으로 한 조회이기 때문에 COFFEE 테이블이 아니라 Coffee 클래스라는 객체를 지정해야하고, coffee_id라는 컬럼이 아닌 coffeeId 필드를 지정해야 한다.

ㄴ따라서 (2-3)의 “SELECT c FROM Coffee c WHERE c.coffeeId = :coffeeId”에서 Coffee는 클래스명이고, coffeeId는 Coffee 클래스의 필드명이다.

ㄴ‘c’는 Coffee 클래스의 별칭이기 때문에 “SELECT c FROM~” 와 같이 SQL에서 사용하는 ‘*’이 아니라 ‘c’로 모든 필드를 조회하는 것이다.

ㄴ(2-3)은 (2-1)과 같이 ‘SELECT c’를 생략한 형태로 사용이 가능하다.

ㄴ네이티브 SQL을 통한 조회

ㄴSpring Data JDBC에서와 마찬가지로 JPA 역시 네이티브 SQL 쿼리를 작성해서 사용할 수 있다.

ㄴ(2-2)의 nativeQuery 애트리뷰트의 값을 ‘true’로 설정하면 value 애트리뷰트에 작성한 SQL 쿼리가 적용된다.

 

Spring Data JDBC의 @Query vs Spring Data JPA의 @Query

Spring Data JDBC에서 사용하는 @Query 애너테이션과 Spring Data JDBC에서 사용하는 @Query 애너테이션은 이름은 같지만 패키지 자체가 다르기 때문에 만약에 Starter 모듈이 둘 다 의존 라이브러리에 포함이 되어 있는 경우에는 패키지 경로를 혼동하지 않도록 주의해야 한다.

 

Spring Data JDBC의 @Query 애너테이션 패키지 경로

import org.springframework.data.jdbc.repository.query.Query

 

Spring Data JPA의 @Query 애너테이션 패키지 경로

org.springframework.data.jpa.repository.Query

 

OrderRepository

import com.codestates.order.entity.Order;

import org.springframework.data.jpa.repository.JpaRepository;



public interface OrderRepository extends JpaRepository<Order, Long> { //(1) 수정된 부분

}

MemberRepository와 마찬가지로 JpaRepository를 상속하는 것 외에는 변경된 부분이 없다.

 

서비스(Service) 클래스 구현

MemberService

// (1) 추가된 부분

        Optional.ofNullable(member.getMemberStatus())

                .ifPresent(memberStatus -> findMember.setMemberStatus(memberStatus));



        // (2) 추가된 부분

        findMember.setModifiedAt(LocalDateTime.now());

Spring Data JDBC의 기술에서 Spring Data JPA의 기술로 변경했다고 해서 MemberService의 코드가 크게 변경되거나 하지 않았음을 알 수 있다.

 

CoffeeService

// (1) 추가된 부분

        Optional.ofNullable(coffee.getCoffeeStatus())

                .ifPresent(coffeeStatus -> findCoffee.setCoffeeStatus(coffeeStatus));

CoffeeService 클래스의 전체 코드에서도 변경된 부분은 (1)과 같이 커피 상태를 업데이트 하는 코드 정도이다.

 

OrderService

public Order createOrder(Order order) {

        // 회원이 존재하는지 확인

        memberService.findVerifiedMember(order.getMember().getMemberId());



        // (1) TODO 커피가 존재하는지 조회해야 됨



        return orderRepository.save(order);

    }



    // (2) 주문 상태 처리를 위한 updateOrder() 메서드 추가

    public Order updateOrder(Order order) {

        Order findOrder = findVerifiedOrder(order.getOrderId());



        Optional.ofNullable(order.getOrderStatus())

                .ifPresent(orderStatus -> findOrder.setOrderStatus(orderStatus));

        findOrder.setModifiedAt(LocalDateTime.now());

        return orderRepository.save(findOrder);

    }

위의 코드들을 보면서 Spring Data JDBC에서 Spring Data JPA로 바꿨다고 해서 실제로 코드 자체가 대폭 변경된 부분은 없다는 점을 알 수 있다.

이 말의 의미는 애플리케이션이 특정 기술에 강하게 결합되지 않도록 Spring이 추구하는 PSA(일관된 서비스 추상화)를 통해 개발자는 일관된 코드 구현 방식을 유지하도록 하고, 기술의 변경이 필요할 때 최소한의 변경만을 하도록 지원한다는 의미와 같다.

 

총정리

  • Spring Data JPA는 Spring Data 패밀리 기술 중 하나로써, JPA 기반의 데이터 액세스 기술을 좀 더 쉽게 사용할 수 있게 해준다.
  • JPA는 엔터프라이즈 Java 애플리케이션에서 관계형 데이터베이스를 사용하기 위해 정해 놓은 표준 스펙(사양 또는 명세, Specification)이다.
  • Hibernate ORM은 JPA라는 표준 스펙을 구현한 구현체이다.
  • Spring Data JPA는 JPA 스펙을 구현한 구현체의 API(일반적으로 Hibernate ORM)를 조금 더 쉽게 사용할 수 있도록 해주는 모듈이다.
  • Spring에서는 애플리케이션이 특정 기술에 강하게 결합되지 않도록 Spring이 추구하는 PSA(일관된 서비스 추상화)를 통해 개발자는 일관된 코드 구현 방식을 유지하도록 하고, 기술의 변경이 필요할 때 최소한의 변경만을 하도록 지원한다.
  • JpaRepository를 상속하면 CrudRepository 기능을 포함한 JPA에 특화된 확장 기능들을 사용할 수 있다.
  • JPQL은 JPA에서 지원하는 객체 지향 쿼리로써 데이터베이스의 테이블을 대상으로 조회 작업을 진행하는 것이 아니라 엔티티 클래스의 객체를 대상으로 객체를 조회한다.
  • JPQL의 문법을 사용해서 객체를 조회하면 JPA가 내부적으로 JPQL을 분석해서 적절한 SQL을 만든 후에 데이터베이스를 조회하고, 조회한 결과를 엔티티 객체로 매핑한 뒤에 반환한다.