Spring Boot JPA 적용 1. JpaRepository 사용

최민길(Gale)·2023년 7월 21일
1

Spring Boot 적용기

목록 보기
41/46

안녕하세요 이번 시간에는 현재 서비스에 JPA를 적용하기 위한 기본 설정 작업을 진행하는 부분에 대해서 포스팅해보도록 하겠습니다.

우선 JPA란 Java Persistence API의 약자로 자바를 사용하여 데이터베이스와 상호작용하는데 사용되는 ORM(Object-Relational Mapping) 기술입니다. ORM이란 객체 지향 프로그래밍 언어와 관계형 데이터베이스 간의 데이터 변환 및 매핑을 처리해주는 기술로 개발자가 별도로 쿼리를 작성하지 않고도 DB와 상호작용이 가능하도록 해줍니다.

따라서 JPA를 도입하게 되면 DB와 관련된 복잡한 작업을 최소화하고, 객체 지향적인 코드를 작성하여 유지 보수성을 향상시킬 수 있습니다. 또한 실제 DB 구성이 바뀌어도 매핑된 엔티티를 기준으로 진행하기 때문에 독립적으로 사용할 수 있다는 장점 역시 존재합니다. 하지만 학습 곡선이 가파르며 매핑 작업을 잘못 수행할 경우 성능이 저하될 수 있는 이슈가 존재합니다. 저는 현재 서비스에 사용되는 쿼리 중 크게 복잡한 쿼리가 없고 ORM 기술을 통해 DB 변경에 독립적으로 반응한다는 장점으로 인해 JPA 방식을 도입하기로 하였습니다.

JPA 적용 방식은 크게 2가지 방식이 존재합니다.

  1. persistence.xml 파일을 통한 EntityManagerFactory 생성 방식
  2. JpaRepository를 통한 DB 접근 방식

첫 번째 방식의 경우 resource 폴더 내에 META-INF 폴더 내에 위치해야 하며, xml 파일에 db 정보를 추가하여 연동합니다. 이 후 EntityManagerFactory를 persistence.xml 파일을 바탕으로 생성하여 이를 이용해 생성한 EntityManager에서 영속성 컨텍스트에 접근하는 방식으로 동작합니다. 이 방식의 경우 직접 트랜잭션을 열고 닫아줘야 하며 CRUD 구현 역시 직접 해주어야 한다는 번거로움이 존재합니다.

두 번째 방식인 JpaRepository의 경우 Spring Data JPA의 일부로, 명시적 구현 없이 CRUD 작업을 수행하기 위한 상위 수준의 데이터 엑세스 방법을 제공합니다. 즉 CRUD 구현 코드를 작성하지 않고도 동적 프록시를 사용하여 런타임에 자동으로 구현 클래스를 생성해줍니다.

이 때 Spring Data JPA는 application.properties에서 DB 속성을 우선적으로 참조하여 EntityManagerFactory를 설정합니다. 따라서 persistence.xml 파일로 EntityManagerFactory를 별도로 생성할 경우 DB 정보가 중복되어 런타임 시 충돌하여 오류가 발생합니다. 따라서 JpaRepository를 사용하기 위해서는 persistence.xml 파일을 생성하지 않고 application.properties에만 DB 정보를 추가해야 합니다.

DB 정보를 통해 생성된 EntityManagerFactory에서 EntityManager를 생성합니다. 이 때 EntityManagerFactory는 thread-safe하기 때문에 여러 스레드에서 접근해도 괜찮지만, EntityManager는 thread-safe하지 않기 때문에 재사용 등을 신중히 고려해야 합니다.

J2SE 환경에서는 EntityManager를 생성하면 내부에는 영속성 컨텍스트가 함께 만들어집니다. JPA는 이 영속성 컨텍스트를 이용하여 DB 정보와 매핑을 진행합니다.

조회(select)의 경우 영속성 컨텍스트 내의 1차 캐시에 찾는 엔티티가 있다면 DB를 조회하지 않고 메모리에 올라가 있는 엔티티를 조회합니다. 1차 캐시에 없다면 DB에 접근한 데이터를 바탕으로 엔티티를 생성하여 1차 캐시에 저장합니다. 이 때 최초 상태를 복사한 스냅샷을 함께 저장합니다.

삽입(insert)의 경우 트랜잭션 커밋 직전까지 내부 저장소에 쿼리를 저장하다가 트랜잭션 커밋 시 지금까지 모인 쿼리들을 한번에 DB로 전송(flush)합니다.(= 쓰기 지연)

수정(update)의 경우 트랜잭션 커밋 시 EntityManager 내부에서 영속성 컨텍스트의 변경 내용을 DB와 동기화하는 작업인 flush 메서드를 호출합니다. 이 때 flush 시점에서 처음에 저정한 스냅샷과 현재 엔티티를 비교해서 변경된 부분을 찾아 수정 쿼리를 생성 해서 내부 저장소에 쿼리를 전달합니다. 이후 삽입 과정에서처럼 내부 저장소의 쿼리를 DB로 전송한 후 DB에서 트랜잭션을 커밋합니다.

삭제(delete)의 경우 엔티티 삭제 시 영속성 컨텍스트에서 우선적으로 제거된 후 삭제 쿼리를 내부 저장소에 등록합니다. 이후 flush 호출 시 DB에 전달됩니다.

JpaRepository를 사용하면 위의 CRUD를 따로 구현하지 않고 자체 제공 인터페이스(구현 클래스)를 이용합니다. 아래는 User 엔티티(테이블과 매핑한 객체)에 접근하는 TestRepository 코드입니다. 인터페이스 형식으로 JapRepository<엔티티 타입, 아이디 타입> 인터페이스를 상속받으며 상속받게 되면 런타임에 CRUD 구현 클래스가 생성됩니다.

여기서 사용자 정의 CRUD를 원한다면 아래처럼 메서드를 정의해주면 됩니다. 기본적으로 JPA는 엔티티에 @Id로 설정된, 즉 PK를 기준으로 조회를 진행합니다. 하지만 서비스 쿼리는 PK 외에 다른 컬럼을 기준으로 조회를 해야 할 필요가 생깁니다. 이 때는 findBy{엔티티 속성 이름} 구성으로 메서드를 생성하면 해당 메서드를 사용 시 그 컬럼을 기준으로 탐색합니다. 아래의 경우 testRepository.findByEmail(email) 메서드를 호출하면 엔티티에 정의된 email을 기준으로 탐색합니다.(= where email like ?) 이 때 리턴값을 리스트로 하면 전체 목록을 가져오며 리턴값을 단일 클래스로 하면 만족하는 목록 중 가장 상위의 1개만 가져옵니다.(=limit)

또한 테이블의 여러 컬럼 중 특정 컬럼만 조회할 필요가 있습니다. 이 경우 별도 인터페이스를 생성한 후 이를 리턴 타입으로 던져줍니다. 이 경우 클라이언트에서 해당 메서드를 사용할 때 받은 인터페이스 내에 존재하는 getter 메서드를 통해 값을 가져옵니다. 아래의 코드를 보시면 아이디와 코드, 이름과 이메일만을 원할 때 이 정보들의 getter를 생성한 인터페이스를 리턴 타입으로 설정합니다. 클라이언트에서는 testRepository.findByUserName(userName)을 통해 별도 생성한 UserInfoMappings 타입으로 결과를 받을 수 있으며 각 값들은 UserInfoMappings 내에 있는 getter 메서드로 가져올 수 있습니다.

public interface TestRepository extends JpaRepository<User, Long>{
    User findByEmail(String email);
    List<UserInfoMappings> findByUserName(String userName);

}
@Entity
@Table(name="User")
@Getter
@Setter
@ToString
@RequiredArgsConstructor
public class User {
    // PK 설정
    @Id
    
    // PK를 자동으로 생성(Auto Increment) 기능 on
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    
    @Column(name="ID", nullable = false)
    private long userID;

    @Column(name="email", nullable = false)
    private String email;

    @Column(name="password", nullable = false)
    private String password;

    @Column(name="code", nullable = false)
    private String userCode;

    @Column(name="status", nullable = false)
    private int appPassword;

    @Column(name="name", nullable = false)
    private String userName;
}
package com.toda.api.TODASERVERSPRINGBOOT.models.mappings;

public interface UserInfoMappings {
    Long getUserID();
    String getUserCode();
    String getEmail();
    String getUserName();
}
profile
저는 상황에 맞는 최적의 솔루션을 깊고 정확한 개념의 이해를 통한 다양한 방식으로 해결해오면서 지난 3년 동안 신규 서비스를 20만 회원 서비스로 성장시킨 Software Developer 최민길입니다.

0개의 댓글