티스토리 뷰

728x90
반응형

 

 

 

지난 글까진 순수 JPA를 이용해 엔티티와 테이블 간의 매핑을 구현했다.

 

이번 글에선 Spring Data JPA를 이용해 JPA를 조금 더 편하게 사용하는 방법에 대해 살핀다.

 

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

 

Spring Data JPA

 

Spring Data JPA는 앞서 Spring Data JDBC에 대해 알아볼 때 한 번 등장했었다.

 

2022.08.26 - [개발/Spring] - [Spring]Spring Data JDBC, Spring Data JDBC 사용법

 

[Spring]Spring Data JDBC, Spring Data JDBC 사용법

지난 글에선 SQL Mapper와 ORM의 차이에 대해서 알아봤다. 2022.08.26 - [개발/Spring] - [Spring]SQL Mapper vs. ORM [Spring]SQL Mapper vs. ORM 지난 글에선 웹 앱의 계층 구조와 JDBC Workflow에 대해 알아봤..

gnidinger.tistory.com

 

요약하면 Spring Data는 DB의 특성을 유지하며 데이터에 접근하기 위한 프로젝트로,

 

Spring Data JPA는 그 라이브러리, 즉 표준 스펙 중 하나이다.

 

주로 JPA를 위한 스프링 데이터 저장소(JpaRepository 인터페이스)를 지원하며, 이 기능을 가장 많이 사용한다.

 

또한 JPA를 살피며 아래와 같은 그림을 본 적이 있는데,

 

2022.08.31 - [개발/Spring] - [Spring]JPA(Java Persistence API)

 

[Spring]JPA(Java Persistence API)

지난 글에서 JDBC의 단점을 극복하며 나온 ORM에 대해 다루었다. 2022.08.26 - [개발/Spring] - [Spring]SQL Mapper vs. ORM [Spring]SQL Mapper vs. ORM 지난 글에선 웹 앱의 계층 구조와 JDBC Workflow에 대해..

gnidinger.tistory.com

 

 

여기서 Spring Data JPA는 JPA를 한 단계 더 추상화한 기술이며,

 

바꿔말하면 Hibernate가 JPA를 구현하듯이 JPA는 Spring Data JPA를 구현한다고 볼 수 있다.

 

쉽게 말하자면 Spring Data JPA는 JPA를 구현한 Hibernate의 API를 쉽게 사용할 수 있게 해주는 모듈인 것이다.

 

실제로 애플리케이션이 데이터베이스에 접근하는 순서는 아래와 같다.

 

이 글에서는 이전에 Spring Data JDBC를 적용했던 예제를 불러와 Spring Data JPA를 다시 적용한다.

 

2022.08.30 - [개발/Spring] - [Spring]Spring Data JDBC - Service, Repository 구현

 

[Spring]Spring Data JDBC - Service, Repository 구현

지난 글까지 JDBC 및 Spring Data JDBC의 소개와 데이터 연동을 위한 도메인 엔티티를 설계 및 구성했다. 2022.08.26 - [개발/Spring] - [Spring]JDBC(Java DataBase Connectivity) 2022.08.26 - [개발/Spring] -..

gnidinger.tistory.com

시작하기 전에, build.gradle과 application.yml에 JPA 의존성을 추가하자.

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
spring:
  h2:
    console:
      enabled: true
      path: /h2    // Context path 설정
  datasource:
    url: jdbc:h2:mem:test    // JDBC URL 설정
  jpa:
    hibernate:
      ddl-auto: create // 테이블 스키마 자동 생성
    show-sql: true // 로그에 SQL 쿼리 출력

 

Entity Classes

 

Spring Data JDBC를 걷어내고 Spring Data JPA의 애너테이션을 추가한다.

 

Member Entity

 

package com.gnidinger.member.entity;

import com.gnidinger.order.entity.Order;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@NoArgsConstructor
@Getter
@Setter
@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long memberId;

    @Column(nullable = false, updatable = false, unique = true)
    private String email;

    @Column(length = 100, nullable = false)
    private String name;

    @Column(length = 13, nullable = false, unique = true)
    private String phone;
    
    @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);
    }
    
    public enum MemberStatus { // 회원 상태 구성
        MEMBER_ACTIVE("활동중"),
        MEMBER_SLEEP("휴면 상태"),
        MEMBER_QUIT("탈퇴 상태");

        @Getter
        private String status;

        MemberStatus(String status) {
            this.status = status;
        }
    }
}

회원 상태 저장을 위한 enum 변수 memberstatus와 사용할 상태를 구성하는 MemberStatus enum이 추가되었다.

 

JPA 애너테이션을 통해 엔티티 매핑을 하는 코드는 이전 글에 설명했으므로 생략한다.

 

2022.08.31 - [개발/Spring] - [Spring]JPA - Entity ↔ DB Table Mapping

2022.09.01 - [개발/Spring] - [Spring]JPA - Entity ↔ Entity Mapping

 

Coffee Entity

 

package com.gnidinger.coffee.entity;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;
import java.time.LocalDateTime;

@NoArgsConstructor
@Getter
@Setter
@Entity
public class Coffee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long coffeeId;

    @Column(length = 100, nullable = false)
    private String korName;

    @Column(length = 100, nullable = false)
    private String engName;

    @Column(length = 5, nullable = false)
    private int price;

    @Column(length = 3, nullable = false, unique = true)
    private String coffeeCode;
    
    @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();
    
    public enum CoffeeStatus { // 추가
        COFFEE_FOR_SALE("판매중"),
        COFFEE_SOLD_OUT("판매중지");

        @Getter
        private String status;

        CoffeeStatus(String status) {
            this.status = status;
        }
    }
}

Member와 마찬가지로 enum 변수와 CoffeeStatus enum이 추가되었다.

 

Order Entity

 

package com.gnidinger.order.entity;

import com.gnidinger.member.entity.Member;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;
import java.time.LocalDateTime;

@NoArgsConstructor
@Getter
@Setter
@Entity(name = "ORDERS")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long orderId;

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus = OrderStatus.ORDER_REQUEST;

    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();

    @Column(nullable = false, name = "LAST_MODIFIED_AT")
    private LocalDateTime modifiedAt = LocalDateTime.now();

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    public void addMember(Member member) {
        this.member = member;
    }

    public enum OrderStatus {
        ORDER_REQUEST(1, "주문 요청"),
        ORDER_CONFIRM(2, "주문 확정"),
        ORDER_COMPLETE(3, "주문 처리 완료"),
        ORDER_CANCEL(4, "주문 취소");

        @Getter
        private int stepNumber;

        @Getter
        private String stepDescription;

        OrderStatus(int stepNumber, String stepDescription) {
            this.stepNumber = stepNumber;
            this.stepDescription = stepDescription;
        }
    }
}

 

Repository Interfaces

 

글 초입에도 적었지만 Spring Data JPA에서 가장 많이 쓰이는기능은 JpaRepository 인터페이스이다.

 

JpaRepository는 위와 같은 인터페이스를 상속받아 CRUD, Paging, Sorting등의 기능과 더불어

 

Spring Data에 속한 QueryByExampleExecutor를 상속받아 검색 조건을 포함하는 Example 객체를 생성하고 

 

이를 쿼리 메서드에 전달함으로써 원하는 값을 조회할 수 있게 된다.

 

이를 바탕으로 각 레포지토리 클래스를 수정해 보자.

 

MemberRepository

 

package com.gnidinger.member.repository;

import com.gnidinger.member.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> { // Crud -> Jpa
    Optional<Member> findByEmail(String email);
}

기존에 CrudRepository를 상속받던 부분을 JpaRepository로 교체했다.

 

CoffeeRepository

 

package com.gnidinger.coffee.repository;

import com.gnidinger.coffee.entity.Coffee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.Optional;

public interface CoffeeRepository extends JpaRepository<Coffee, Long> { // Crud -> Jpa
    Optional<Coffee> findByCoffeeCode(String coffeeCode);

    @Query(value = "SELECT c FROM Coffee c WHERE c.coffeeId = :coffeeId")

//    @Query(value = "FROM Coffee c WHERE c.coffeeId = :coffeeId")  // ‘SELECT c' 생략 가능
//    @Query(value = "SELECT * FROM COFFEE WHERE coffee_Id = :coffeeId", nativeQuery = true) // (1)

    Optional<Coffee> findByCoffee(long coffeeId);
}

JpaRepository 상속과 함께 JPQL을 이용해 coffeeId에 대한 정보를 조회하는 것을 확인할 수 있다.

 

JPQL(Java Persistence Query Language)

 

  • SQL을 추상화한, 특정 DB SQL에 의존하지 않는 객체지향 쿼리
  • SQL은 DB 테이블(COFFEE)을, JPQL은 엔티티(Coffee)를 대상으로 쿼리를 보냄
  • SQL과 문법이 유사하고, SELECT, FROM, WHERE 등을 지원
  • JPA는 JPQL을 분석한 후 SQL을 생성해 DB 조회, 결과를 엔티티 객체로 매핑한 뒤 반환

 

주석에 쓰인 대로 'SELECT c'는 생략이 가능하며, SQL을 이용해 조회하려면 (1)과 같이 하면 된다.

 

OrderRepository

 

package com.gnidinger.order.repository;

import com.gnidinger.order.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderRepository extends JpaRepository<Order, Long> {
}

 

Service Classes

 

MemberService

 

...

@Service
public class MemberService {
    ...

    public Member updateMember(Member member) {
        ...

        Optional.ofNullable(member.getMemberStatus())
                .ifPresent(memberStatus -> findMember.setMemberStatus(memberStatus));


        findMember.setModifiedAt(LocalDateTime.now());

        return memberRepository.save(findMember);
    }

    ...
}

회원의 상태 정보와 등록 시간을 조회하는 기능을 추가했다.

 

CoffeeService

 

...

public class CoffeeService {
    ...

    public Coffee updateCoffee(Coffee coffee) {
        ...

        // (1) 추가된 부분
        Optional.ofNullable(coffee.getCoffeeStatus())
                .ifPresent(coffeeStatus -> findCoffee.setCoffeeStatus(coffeeStatus));

        return coffeeRepository.save(findCoffee);
    }

    ...
}

커피 상태를 업데이트하는 코드를 작성했다.

 

OrderService

 

...

@Service
public class OrderService {
    ...

    public Order updateOrder(Order order) { // // updateOrder() 메서드 추가
        Order findOrder = findVerifiedOrder(order.getOrderId());

        Optional.ofNullable(order.getOrderStatus())
                .ifPresent(orderStatus -> findOrder.setOrderStatus(orderStatus));
        findOrder.setModifiedAt(LocalDateTime.now());
        return orderRepository.save(findOrder);
    }

    ...
}

updateOrder() 메서드를 추가했다.

 

코드를 바꾸면서 Spring Data JDBC -> Spring Data JPA의 체감이 크진 않았다.

 

그만큼 Spring이 추구하는 PSA(일관된 서비스 추상화)가 구현되어 있고

 

개발자는 일관된 코드 구현 방식을 유지하며 기술의 변경이 필요할 때 최소한의 변경만을 하도록 만들어져 있다는 뜻이겠다.

 

이번 코드는 컨트롤러나 DTO 클래스도 손을 봤는데,



하나하나 올릴 수 없으니 누덕누덕 기워 만든 내 코드 원본을 올려본다.

 

https://github.com/gnidinger/gnidinger-spring

 

GitHub - gnidinger/gnidinger-spring

Contribute to gnidinger/gnidinger-spring development by creating an account on GitHub.

github.com

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/06   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
글 보관함