-
728x90반응형SMALL
프로젝트 구성에 앞서 Spring 프레임워크에서 백엔드에 접근하기 위해 JPA를 사용하게 되어서 JPA에 대해 정리를 해보려고 합니다.
JPA 구성요소 JPA에 대한 설명에 앞서 ORM에 대해 알아야 합니다. ORM이란 Object Relational Mapping으로 어플리케이션의 클래스와 RDB를 매핑시켜주는 것으로 어플리케이션의 객체를 RDB 테이블에 자동으로 영속화시켜주는 기술입니다. 정의한 객체를 사용하는 것만으로도 자연스럽게 데이터를 연결해 사용할 수 있습니다.
JPA는 Java Persistence Api의 약자로 Java의 ORM 표준 스펙으로 데이터베이스에 접근하기 위한 api 규격을 정의해 놓은 것입니다. orm이 전체적인 개념이라고 하면 jpa가 그 기능을 정의한 스펙이라고 볼 수 있습니다.
JPA에 대해 말할 때 hibernate도 빼놓을 수 없습니다. hibernate란 jpa에 대한 실제 구현체로 eclipse link와 같은 jpa provider입니다. java에서는 hibernate 구현체를 사용합니다.
JDBC란 java 프로그램이 데이터베이스와 연결되어 데이터를 주고 받을 수 있도록 도와주는 프로그래밍 인터페이스입니다.
결론적으로, jpa를 통해 entity 메니저에 접근하지 않고도 데이터에 대한 접근을 좀 더 쉽고 객체 지향적으로 처리할 수 있습니다.
spring에서 사용하는 spring data jpa란 spring에서 hibernate를 간편하게 사용할 수 있도록 추상 객체를 한 번 더 감싼 것입니다.
의존성 주입
다음과 같은 의존성을 build.gradle에 선언해줍니다.
dependencies{ implementation 'org.springframework.boot:spring-boot-starter-data-jpa' }
위에서 Java 객체와 RDB 테이블을 매핑해준다고 하였는데, 여기서의 java 객체가 Entity입니다.
... @Entity public class User{ @Id @GeneratedValue private Long id; ... }
RDB와 매핑해줄 객체에 @Entity 어노테이션을 추가해줍니다.
@Entity를 선언해줄 경우 멤버 변수 중 PK를 꼭 선언해주어야 합니다.
GeneratedValue의 경우 PK값을 자동적으로 값을 생성해주겠다는 뜻입니다.
자바 객체에서 JPA에 정의된 많은 메소드를 사용하기 위해 repository를 선언해줍니다.
public interface UserRepository extends JpaRepository<User, Long> { }
위와 같이 UserRepository 인터페이스를 선언해주는데, JpaRepository를 상속받는 것만으로도 JPA에서 제공해주는 메서드를 사용할 수 있게 됩니다.
제너릭의 첫번째에는 사용할 엔티티를 넣어주고, 두 번째에는 PK값의 타입을 넣어주면 됩니다.
JpaRepository interface에 정의된 메서드들을 살펴보도록 하겠습니다.
findAll
@Override List<T> findAll(); @Override List<T> findAll(Sort sort); // 활용 @Autowired UserRepository userRepository; @Test void test(){ List<User> users = userRepository.findAll(); // name 컬럼으로 정렬 List<User> users = userRepository.findAll(Sort.by(Direction.DESC,"name")); }
테이블의 전체 값을 가져오게 됩니다.
인자로 Sort가 들어올 수도 있습니다. 인자로 들어온 조건으로 정렬된 리스트를 반환받을 수 있습니다.
findAllById
@Override List<T> findAllById(Iterable<ID> ids); // 활용 @Autowired UserRepository userRepository; @Test void test(){ List<User> users = userRepository.findAllById(Lists.newArrayList(1L,3L,5L)); }
여러 개의 id에 해당되는 값을 찾아줍니다.
saveAll
@Override <S extends T> List<S> saveAll(Iterable<S> entities); // 활용 @Autowired UserRepository userRepository; @Test void test(){ User user1 = new User("user1","user1@gmail.com"); User user2 = new User("user2","user2@gmail.com"); userRepository.saveAll(Lists.newArrayList(user1,user2)); }
여러 개의 엔티티를 한 번에 db에 저장합니다.
sql의 insert에 해당합니다.
flush
void flush();
현재 jpa 컨텍스트에서 가지고 있는 db 값을 실제 db에 반영하도록 합니다. db 반영 시점을 조절할 수 있도록 해줍니다.
saveAndFlush
<S extends T> S saveAndFlush(S entity);
저장한 엔티티를 jpa 컨텍스트에 가지고 있지 말고 db에 반영해줍니다.
deleteInBatch
void deleteInBatch(Iterable<T> entities);
여러 개의 entity를 받아 delete해줍니다.
deleteAllInBatch
void deleteAllInBatch();
테이블의 값들을 모두 지웁니다.
getOne()
T getOne(ID id);
id값을 통해 엔티티 하나를 가져옵니다.
findAll() (Example, sort)
@Override <S extends T> List<S> findAll(Example<S> example); @Override <S extends T> List<S> findAll(Example<S> example, Sort sort); // 활용 @Autowired UserRepository userRepository; @Test void test(){ ExampleMatcher matcher = ExampleMatcher.matching() .withIgnorePaths("name") .withMatcher("email",endsWith()); Example<User> example = Example.of(new User("user","gmail.com"), matcher); userRepository.findAll(example); }
인자로 Example과 Sort를 추가할 수 있습니다.
Example은 like 검색을 처리할 수 있습니다.
위의 예시와 같은 경우 email의 뒷부분을 부분일치 하는 값들을 찾아줄 수 있도록 해줍니다. 즉 email 컬럼의 값이 "gmail.com"인 값을 모두 가져오게 됩니다.
JpaRepository가 상속받고 있는 PagingAndSortingRepository에 대해 살펴보도록 하겠습니다.
Iterable<T> findAll(Sort sort); Page<T> findAll(Pageable pageable); // 활용 @Autowired UserRepository userRepository; @Test void test(){ //PageRequst.of({page위치},{page크기}) Page<User> users = userRepository.findAll(PageRequest.of(1,3)); System.out.println(users.getTotalElements()); System.out.println(users.getTotalPages()); System.out.println(users.getNumberOfElements()); System.out.println(users.getSize()); users.getContent().forEach(System.out::println); }
Pageable 인자를 통해 paging을 쉽게 처리할 수도 있습니다.
위의 활용 예를 보면, 페이지의 크기는 3이고 그 중, 2번째 페이지를 가지고 오게 됩니다. (Page의 인덱스는 0부터 시작합니다.)
getTotalElement를 통해 총 엔티티의 개수를 알 수 있습니다.
getTotalPages를 통해 총 페이지 수를 알 수 있습니다.
getNumberOfElements를 통해 현재 페이지의 엔티티 개수를 알 수 있습니다.
getSize를 통해 페이지의 크기를 알 수 있습니다.
getContent를 통해 해당 페이지의 엔티티를 사용할 수 있습니다.
PagingAndSortingRepository가 상속받고 있는 CrudRepository는 기본적인 create, read, update, delete에 관련된 메서드를 정의하고 있습니다.
save(), saveAll()
<S extends T> S save(S entity); <S extends T> Iterable<S> saveAll(Iterable<S> entities);
엔티티 값을 저장해줍니다.
엔티티 리스트를 받아 한 번에 저장할 수도 있습니다.
save 코드
@Transactional @Override public <S extends T> S save(S entity) { Assert.notNull(entity, "Entity must not be null."); if (entityInformation.isNew(entity)) { em.persist(entity); return entity; } else { return em.merge(entity); } }
Entity가 null일 경우 오류가 발생합니다.
이 후, entity가 새로운 것이면 insert 쿼리를 수행하고, 이미 있는 것이라면 update 쿼리를 수행합니다.
즉, save는 isNew의 flag에 따라 insert와 update를 모두 수행할 수 있습니다. (isNew는 동일한 id값이 있는지 확인합니다.)
saveAll 코드
@Transactional @Override public <S extends T> List<S> saveAll(Iterable<S> entities) { Assert.notNull(entities, "Entities must not be null!"); List<S> result = new ArrayList<S>(); for (S entity : entities) { result.add(save(entity)); } return result; }
인자로 들어온 entity들에 대해 반복적으로 save를 실행하게 됩니다. (insert가 여러 번 발생하게 됩니다.)
findById()
Optional<T> findById(ID id);
이전의 getOne과 비슷하지만 반환되는 값이 Optional 객체로 매핑되어 있으므로 반환된 엔티티를 한 번 더 어떤 처리를 해서 사용하도록 합니다.
findById와 getOne 차이점
@Autowired UserRepository userRepository; @Test void test(){ User user = userRepository.getOne(1L); System.out.println(user); } // 에러! could not initialize proxy [org.example.model.UserEntity#1] - no Session org.hibernate.LazyInitializationException: could not initialize proxy [org.example.model.UserEntity#1] - no Session
위의 코드는 다음과 같은 에러가 납니다. 세션이 존재하지 않기 때문에 proxy를 초기화할 수 없다고 나옵니다.
getOne을 한 세션을 출력하는 시점까지 유지시켜주기 위해서는 @Transactional 어노테이션을 선언해주면 됩니다.
getOne은 Entity에 대한 Lazy loading을 지원해줍니다. lazy fetch에 대한 부분과 @Transactional 어노테이션에 대한 부분은 이후에 살펴보도록 하겠습니다.
@Autowired UserRepository userRepository; @Test void test(){ User user = userRepository.findById(1L).orElse(null); // Optional 처리 System.out.println(user); }
findById의 경우 Entity를 Optional로 매핑해서 반환하므로 따로 처리가 필요합니다. 위와 같은 경우 해당 id값이 존재하지 않는다면 orElse를 통해 null을 반환하도록 처리해주었습니다.
getOne, findById 코드
@Override public T getOne(ID id) { Assert.notNull(id, ID_MUST_NOT_BE_NULL); return em.getReference(getDomainClass(), id); } @Override public Optional<T> findById(ID id) { ... return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints)); }
getOne에서 em은 entity 메니저입니다. em의 getReference를 통해 도메인 클래스를 가져옵니다. lazy fetching시에는 reference만 가지고 있고 실제 값을 구하는 시점에 세션을 통해 조회를 하게 됩니다.
findById는 em의 find를 통해 직접 엔티티를 가져오게 됩니다.
위에서 살펴보았듯이 findById의 경우 eager fetching방식을 사용했고 getOne은 lazy fetching을 사용한 것을 확인할 수 있습니다.
existsById()
boolean existsById(ID id);
id에 해당하는 값이 존재하는지 알려줍니다. sql의 count 함수를 사용합니다.
findAll(), findAllById()
Iterable<T> findAll(); Iterable<T> findAllById(Iterable<ID> ids);
이전과 동일합니다.
count()
long count(); // 활용 @Autowired UserRepository userRepository; @Test void test(){ long count = userRepository.count(); System.out.println(count); }
전체 엔티티 갯수를 가져옵니다. sql count함수를 사용합니다.
deleteXxx()
void deleteById(ID id); void delete(T entity); void deleteAll(Iterable<? extends T> entities); void deleteAll(); // 활용 @Autowired UserRepository userRepository; @Test void test(){ userRepository.delete(userRepository.findById(1L).orElseThrow(RunttimeException::new))); }
조건에 따라 엔티티를 제거해줍니다.
delete와 deleteById의 경우 select 쿼리가 한 번 일어난 뒤 실제 delete 쿼리가 실행됩니다. delete를 실행하기 전에 해당 entity나 id의 값이 존재하는지 확인하기 위해서 select 쿼리가 한 번 동반이 됩니다.
deleteAll vs deleteInBatch
@Autowired UserRepository userRepository; @Test void test(){ userRepository.deleteAll(userRepository.findAllById(Lists.newArrayList(1L,3L)); userRepository.deleteInBatch(userRepository.findAllById(Lists.newArrayList(1L,3L)); }
deleteAll에 인자값을 넣어 여러 엔티티를 삭제할 경우 각 엔티티마다 delete 이전에 select 쿼리를 통해 해당 엔티티가 존재하는지 확인하한 뒤 delete를 실행합니다. 만약 더 많은 숫자의 엔티티를 제거하게 된다면 select, delete를 그 수만큼 실행하게 되어 성능에 문제가 생길 수 있습니다.
deleteInBatch의 경우 delete이전에 select쿼리가 존재하지 않고 delete 쿼리 한 번만 실행되게 됩니다.
deleteAll, deleteAllInBatch 코드
@Transactional @Override public void deleteAll() { for (T element : findAll()) { delete(element); } } @Transactional @Override public void deleteAllInBatch() { em.createQuery(getDeleteAllQueryString()).executeUpdate(); }
deleteAll의 경우 findAll을 실행한 뒤 반복문을 통해 delete를 실행하고, deletAll과 같은 경우 getDeleteAllQueryString을 통해 쿼리 스트링을 만들고 한 번의 쿼리 실행을 통해 delete를 실행하게 됩니다.
최상위 interface인 Repository에는 아무것도 존재하지 않습니다.
@Indexed public interface Repository<T, ID> { }
Jpa에서 사용하는 도메인 레포지토리 타입이라는 것을 알려주기 위한 용도로 사용됩니다.
JpaRepository가 상속받고 있는 QueryByExampleExecutor에 대해 살펴보도록 하겠습니다.
<S extends T> Optional<S> findOne(Example<S> example); <S extends T> Iterable<S> findAll(Example<S> example); <S extends T> Iterable<S> findAll(Example<S> example, Sort sort); <S extends T> Page<S> findAll(Example<S> example, Pageable pageable); <S extends T> long count(Example<S> example); <S extends T> boolean exists(Example<S> example);
이전에 살펴본 여러 메서드에 Example 인자가 추가된 메서드들이 정의되어 있습니다.
Entity를 선언한 뒤, JpaRepository를 상속받은 repository interface를 선언해주는 것만으로도 위에서 정의된 메서드들을 모두 활용할 수 있게 됩니다.
오늘은 jpaRepository에 정의된 메서드들을 살펴보았습니다. jpa에서 명세하고 있는 메서드들의 사용 원리에 대해 알게 되었지만, @Transactional 어노테이션, 영속석, lazy fetching 등 아직 애매하게 알고 있는 지식들이 아직 많은 것 같습니다.
이 후 포스팅을 통해 빠르게 습득할 수 있도록 하겠습니다.
반응형LIST'백엔드' 카테고리의 다른 글
JPA 연관관계 살펴보기 (1:1, 1:N, N:1) (4) 2023.08.21 Entity annotation, Listener (1) 2023.08.19 JPA 쿼리메서드 (0) 2023.08.18 Spring boot todo list 만들기 (2) 2023.07.21 mysql cli 모음 (+ docker) (0) 2022.12.09