JPA 기반 데이터 액세스 계층(4) - Spring Data JPA를 통한 데이터 엑세스 계층 구현

Backend kwon·2023년 9월 19일
0

⚫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의 기술을 적용

1. 엔티티 클래스 정의

@Getter
@Setter
@NoArgsConstructor
@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;
        }
    }
}
  • 엔티티에 JPA에 맞는 애너테이션을 새로 추가해야 합니다.

 

2. 레포지토리 인터페이스 구현

public interface CoffeeRepository extends JpaRepository<Coffee, Long> {
    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", nativeQuery = true) // (2-2) 
    @Query(value = "SELECT c FROM Coffee c WHERE c.coffeeId = :coffeeId")  // (2-3)
    Optional<Coffee> findByCoffee(long coffeeId);
}
  • Spring Data JDBC에서의 Repository와 비교했을 때 변경된 부분은 CrudRepository를 상속하는 대신 JpaRepository를 상속한 것 입니다.

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

 

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

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

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

    JPQL의 문법을 사용해서 객체를 조회하면 JPA가 내부적으로 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 애너테이션 패키지 경로
import org.springframework.data.jdbc.repository.query.Query

Spring Data JPA의 @Query 애너테이션 패키지 경로
org.springframework.data.jpa.repository.Query

 

3. 서비스 클래스 구현

@Service
public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public Member createMember(Member member) {
        verifyExistsEmail(member.getEmail());

        return memberRepository.save(member);
    }

    public Member updateMember(Member member) {
        Member findMember = findVerifiedMember(member.getMemberId());

        Optional.ofNullable(member.getName())
                .ifPresent(name -> findMember.setName(name));
        Optional.ofNullable(member.getPhone())
                .ifPresent(phone -> findMember.setPhone(phone));
        // (1) 추가된 부분
        Optional.ofNullable(member.getMemberStatus())
                .ifPresent(memberStatus -> findMember.setMemberStatus(memberStatus));

        // (2) 추가된 부분
        findMember.setModifiedAt(LocalDateTime.now());

        return memberRepository.save(findMember);
    }

    public Member findMember(long memberId) {
        return findVerifiedMember(memberId);
    }

    public Page<Member> findMembers(int page, int size) {
        return memberRepository.findAll(PageRequest.of(page, size,
                Sort.by("memberId").descending()));
    }

    public void deleteMember(long memberId) {
        Member findMember = findVerifiedMember(memberId);

        memberRepository.delete(findMember);
    }

    public Member findVerifiedMember(long memberId) {
        Optional<Member> optionalMember =
                memberRepository.findById(memberId);
        Member findMember =
                optionalMember.orElseThrow(() ->
                        new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
        return findMember;
    }

    private void verifyExistsEmail(String email) {
        Optional<Member> member = memberRepository.findByEmail(email);
        if (member.isPresent())
            throw new BusinessLogicException(ExceptionCode.MEMBER_EXISTS);
    }
}
  • 액세스 기술을 Spring Data JDBC에서 Spring Data JPA로 바꿨다고 해서 실제로 코드 자체가 대폭 변경된 부분은 없습니다.
    즉, 애플리케이션이 특정 기술에 강하게 결합되지 않도록 Spring이 추구하는 PSA(일관된 서비스 추상화)를 통해 개발자는 일관된 코드 구현 방식을 유지하도록 하고, 기술의 변경이 필요할 때 최소한의 변경만을 하도록 지원한다는 의미입니다.

 

4. 기타 기능 추가로 인해 수정 및 추가되는 코드

@Getter
public class MemberPatchDto {
    private long memberId;

    @NotSpace(message = "회원 이름은 공백이 아니어야 합니다")
    private String name;

    @NotSpace(message = "휴대폰 번호는 공백이 아니어야 합니다")
    @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$",
          message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다")
    private String phone;

    // 회원 상태 값을 사전에 체크하는 Custom Validator를 만들수도 있다.
    private Member.MemberStatus memberStatus;

    public void setMemberId(long memberId) {
        this.memberId = memberId;
    }
}
profile
백엔드개발자를 향해서

0개의 댓글