JPA 와 Spring Data JPA

보통 사용 가능 기술에 둘 모두 작성되어 있는 백엔드 개발자들이 많을 것이다. 

 

그럼 JPA 와 Spring Data JPA의 차이는 무엇일까?

 

자신있게 답변하기 쉽지 않았다. 

 

내가 알고 있던 차이점

나는 김영한님의 스프링 입문 강의에서 두 개념에 대해 처음 접한 것 같다.

 

내가 알고 있던 둘의 개념은

JPA : Java Persistence Api 로 sql문을 따로 작성해주지 않아도 repository를 통해 자바로 db작업까지 함께 할 수 있는 기능(EntityManager 사용)

Spring Data JPA : JPA가 더 간편화된 방법으로 repository 에서 많은 코드를 축약해서 쓸 수 있다. 그런데 너무 많은 내용이 축약되므로 나중에 코드를 한번에 이해하기 어려울 수 있다.

 

따라서 Spring Data JPA 보다는 JPA를 사용할 것, 부끄럽지만 이렇게 알고 있었다.

각각 개념에 대해 먼저 알아보자

 

 

JPA

객체-관계 매핑(ORM)을 위한 표준 명세인 인터페이스

자바 객체를 데이터베이스 테이블에 저장하거나 조회하는 규칙을 정해둔 것

JPA는 인터페이스이므로 직접 사용할 수 없고, 이를 구현한 라이브러리를 사용해야 한다.

 

 


Spring Data JPA 

JPA의 기능을 더 쉽게 사용할 수 있도록 도와주는 Spring 프레임워크의 확장 라이브러리

JPA 구현체 위에서(구현체를 통해), 반복적인 코드 작성을 줄이고 더 간편하게 데이터베이스와 상호작용할 수 있도록 도와준다.

 


그럼 그동안 내가 사용했던 EntityManager는 무엇일까? JPA의 구현체일까?

EntityManager

데이터베이스에 대해 자바 객체를 저장, 조회, 수정, 삭제하는 등의 작업을 수행하는 JPA의 핵심 인터페이스 중 하나로, 구현체는 아니다.

실제 동작을 위해서는 EntityManager의 구현체가 필요하다. 대표적인 구현체가 Hibernate 이다.

* 개발자는 JPA 표준을 사용하지만, 실제로는 Hibernate가 그 표준을 기반으로 내부적으로 동작한다.

 

이번 프로젝트에서 JPA를 사용했다.

=> 이번 프로젝트에서 JPA의 구현체인 Hibernate를 사용했다 .

 

 

Hibernate와 Spring Data JPA의 차이

 

Hibernate

 Spring Data JPA

목적 JPA의 구현체로서 ORM 기능 제공 Hibernate 위에서 동작, 더 간편한 추상화 기능 제공
세부 제어 복잡한 쿼리와 동작을 세밀하게 제어 가능 기본적인 CRUD 작업과 페이징, 정렬을 자동으로 처리
복잡도 높은 수준의 제어와 기능 제공, 복잡한 설정 필요 자동화된 기능을 통해 코드 간소화, 단순 작업에 최적화
기본 제공 기능 고급 캐싱, 배치 처리, 지연 로딩, 원시 SQL 쿼리 지원 CRUD, 페이징, 정렬, 메서드 쿼리 생성 등의 기능 자동 제공
확장성 JPA 표준 외에도 고급 기능 제공 대부분의 고급 기능은 Hibernate를 통해 제공

 

 

 

어떤 경우에 어느 방법을 사용하는 것이 효율적일까?

Hibernate를 사용하는 경우:

  1. 복잡한 데이터베이스 작업이 필요할 때: 복잡한 쿼리나 성능 최적화, 고급 페치 전략을 사용해야 하는 경우
  2. JPA 표준 이상의 기능을 사용하고 싶을 때: 캐싱 전략, 배치 처리, 고급 로딩 전략 등 JPA 표준에 포함되지 않은 기능을 이용하고 싶은 경우
  3. 세부적인 쿼리와 데이터 조작이 필요한 경우: 쿼리를 직접 관리하거나 데이터 처리 로직에 대한 세밀한 제어가 필요한 경우

Spring Data JPA를 사용하는 경우:

  1. 단순 CRUD, 페이징, 정렬 작업이 주된 경우: 반복적인 CRUD 코드를 작성하는 대신, Spring Data JPA가 제공하는 자동화된 기능을 사용하는 것이 효율적이다
  2. 생산성을 높이고자 할 때: 기본적인 기능을 자동으로 처리해주므로, 복잡하지 않은 애플리케이션에서는 Spring Data JPA가 훨씬 빠르고 생산적이다
  3. 간단한 프로젝트나 빠른 프로토타이핑이 필요할 때: 기본적인 데이터베이스 연동 작업을 빠르게 처리할 수 있기 때문에 간단한 애플리케이션에서는 Spring Data JPA가 적합하다

 궁금증에 대한 시작점

치아 건강 관리 서비스인 '투스데이'를 진행할 때, 나를 포함한 백엔드 팀원 3명은 나-EntityManager 사용, 다른 2분-Spring Data JPA 사용 이렇게 나누어져있었다.

따로 정한건 아니었지만 다른 팀원분들께서 각각 aws 배포, 구글 연동 로그인을 맡으시고 나는 조금 더 많은 api 분량을 담당하여 비교적 먼저 개발을 시작하였다. 이 과정에서 나는 기존에 사용하던 EntityManager 를 사용하여 개발을 진행하였고, 다른 팀원 두분은 치과 진료 기록에 대한 부분을 같이 담당하셔서 같이  Spring Data JPA를 사용하신 것 같다! 

프로젝트를 진행하는데 문제는 없었으니 계속 진행하였는데 후반부쯤 페이징 처리 부분에서 이슈가 발생했다!

이 부분은 따로 정리한 페이징과도 연관되어 있는데, 기본 JPA에서 사용하는 offset, limit 처리와 Spring Data JPA 에서 지원하는 Pageable의 offset, limit 처리 기준이 달라 api 마다 페이징 처리가 다르게 되는 문제가 발생한 것이었다.

 

이 과정에서 Spring Data JPA의 Pageable이 지원하는 방식이 데이터 용량이 큰 경우 더 효율적이라는 글을 보고 Spring Data JPA가 더 안좋은 방식인 줄 잘못 알고 있었던건가?!? 싶어서 둘의 차이점과 장단점, 어떤 경우에 어떤 방법을 사용하는게 더 효과적인 지 공부해보고 싶었다.

 

이상 끝~!

* JPA (Java Persistence API)

* JPQL(Java Persistence Query Language) : SQL과 유사한 구문을 사용하여 데이터베이스에서 엔티티 객체를 조회하는 쿼리 언어

 

1. setFirstResult, setMaxResults 로 offset, limit 처리

setFirstResult(int offset)

: 결과 리스트에서 시작할 첫 번째 행의 위치를 설정, 데이터베이스 쿼리 결과에서 특정 위치부터 데이터를 가져올 수 있다.

 

setMaxResults(int limit)

: 반환할 최대 값을 설정, 이 값을 이용해 페이지 당 데이터를 얼마나 가져올지 제어할 수 있다.

 

public List<Post> findByKeywordIdPaging(int keywordId, int limit, int offset) {
        //keywordId로 Post 조회하여 결과 목록 전달
        String queryStr = "SELECT pk.post FROM PostKeyword pk WHERE pk.keyword.id = :keywordId order by pk.post.createDate DESC";
        TypedQuery<Post> query = em.createQuery(queryStr, Post.class)
                .setParameter("keywordId", keywordId)
                .setFirstResult(offset) 
                .setMaxResults(limit);   

        return query.getResultList();
    }

 

장점 :

1) 쿼리 제어가 가능하여, 복잡한 쿼리나 커스텀 쿼리에 대해 유연하게 페이징 적용 가능

 

단점 :

1) 매번 쿼리를 작성해야 한다

2) ( 전체 페이지 수, 총 데이터 수)와 같은 추가적인 정보를 얻기 어렵다

 

 

 

 

2. Spring Data JPA의 Pageable, Page 인터페이스

Spring Data JPA의 Pageable 인터페이스는 조금 더 높은 수준의 페이징 기능을 제공한다

PageRequest.of(page, size)를 사용해 페이지 번호와 페이지 크기 지정, 내부적으로는 이를 기반으로 offset, limit 값을 계산

 

offset : page*size 로 계산. 예를 들어, 3번째 페이지를 요청하고 페이지 당 size가 10이라면 offset=3*10=30.

limit : size로 설정됨, 페이지 당 가져올 데이터의 수

 

Pageable pageable = PageRequest.of(2, 10); // 3번째 페이지 (0-indexed), 페이지당 10개의 데이터
Page<Post> postsPage = postRepository.findByTitleContaining("example", pageable);

 

장점 :

1) 코드 간소화(쿼리를 직접 작성하지 않고도 메서드 이름 기반으로 페이징 처리 가능

2) 부가적인 페이징 정보를 얻는데 유리하다(전체 페이지 수, 전체 데이터 개수 등)

3) 확장성(페이징, 정렬, 검색 기능을 쉽게 추가할 수 있다)

 

단점 :

1) 내부적으로 동작하는 쿼리 제어가 어려우므로, 복잡한 쿼리를 처리할 때는 한계가 있을 수 있다

 

 

 


추가로 사용할 수 있는 페이징 방법들

3. Native Query 를 사용한 페이징 처리

복잡한 SQL 쿼리를 직접 작성하고 페이징을 적용할 수 있다.

복잡한 데이터베이스 구조나 성능 최적화를 위해 사용될 수 있다.

@Query(value = "SELECT * FROM post WHERE title LIKE %:title% ORDER BY create_date DESC LIMIT :limit OFFSET :offset", nativeQuery = true)
List<Post> findByTitleNative(@Param("title") String title, @Param("limit") int limit, @Param("offset") int offset);

 

장점 : 고도화된 SQL 제어 가능, 복잡한 쿼리에 대해 성능 최적화 가능

단점 : 유지 보수가 어렵고, 데이터베이스에 종속적일 수 있다.

 


 

 

 

그렇다면 각각의 페이징 기법은 어떤 경우에 사용하면 좋을까?

 

단순한 페이징을 사용하는 경우에는 Pageable 과 Page를 사용하는 Spring Data JPA 방식이 효율적이고

복잡한 쿼리 제어가 필요하다면 setFirstResult 혹은 Native Query 방식을 사용하는 것이 좋을 듯하다.

 

그러면 복잡한 쿼리 제어가 필요한 경우의 기준이 어떻게 되는 지 헷갈려서 책 gpt에게 물어보았다.

 

복잡한 쿼리 제어가 필요하다는 것은,

데이터베이스 단순 조회 작업을 넘어 더 많은 조건, 조인, 서브쿼리 등을 포함해 성능 최적화와 같은 고려가 필요한 경우를 말한다고 한다. 수백만개 이상의 데이터를 처리할 때는 페이징, 정렬 등의 최적화가 필수적이고 이런 경우에도 쿼리 제어 방법을 사용한다고 한다. 

 

 


 

이렇게 총 3가지 페이징 기법들에 대해 학습한 후 내 개인 프로젝트인 배달 서비스 '마잇'에 어떤 기법을 적용하면 좋을 지 고민해보았다. 

 

마잇은 내가 개발중인 배달 애플리케이션 서버이다. 개인 프로젝트로 진행 중이라 프론트 영역은 시각적 이해를 위해 보여주기 위한 부분만 타임리프로 개발하고 주로 백엔드 개발 위주로 진행하고 있다.

 

다음과 같은 기능들을 목표로 개발 중이다.

나는 이 프로젝트를 활용해서 Redis 캐싱 기법 혹은 스프링 부트의 비동기 프로세싱을 활용한 성능 최적화에 관하여 졸업 논문을 작성할 예정이라 이 부분을 염두에 두고 확장성에 유리한 Spring Data JPA 의 Pageable을 활용하는 방법으로 정하였다.

 

 

그동안 프로젝트를 하거나 스프링 공부를 하면서 엔티티 간의 관계를 설정할 때 cascade 등과 같은 옵션을 설정할 수 있다는 걸 알고 있었지만 자세히 이해하지 못했었다. 이번에 투스데이 프로젝트를 진행하면서 여러 테이블을 구현하고 연관관계를 맺다 보니 이런 관계 설정에 있어 자세히 공부하고 필요한 옵션만 설정해 주어야겠다는 생각이 들어서 급한 api 개발을 마치고 바로 cascade에 대해 공부해 보았다. 

Cascade

  • JPA에서 엔티티 간의 관계를 설정할 때 사용하는 중요한 개념 중 하나이다.
  • cascade 옵션을 사용하여 부모 엔티티에 대한 작업이 자식 엔티티에도 자동으로 전파된다. 

장점

  • 코드의 간결성 유지 가능
  • 데이터의 일관성 보장

 

유형

  • CascadeType.PERSIST

부모 엔티티가 저장될 때, 자식 엔티티도 함께 저장

 

  • CascadeType.MERGE

부모 엔티티가 병합될 때, 자식 엔티티도 함께 병합

 

  • CascadeType.REMOVE

부모 엔티티가 삭제될 때, 자식 엔티티도 함께 삭제

 

  • CascadeType.REFRESH

부모 엔티티를 새로 고칠 때 자식 엔티티도 함께 새로 고침

 

  • CascadeType.DETACH

부모 엔티티가 영속성 컨텍스트에서 분리될 때, 자식 엔티티도 함께 분리된다

 

  • CascadeType.ALL

위의 모든 CascadeType 옵션을 적용한다

부모 엔티티의 모든 생명주기 변경에 대해 자식 엔티티에 동일하게 적용된다

 

주의사항

  • CascadeType.ALL 을 무분별하게 사용하는 것은 좋지 않다. 실제로 필요한 옵션을 선택해서 사용하는 것이 좋다
  • cascade 설정은 부모 엔티티의 관계 매핑 코드 내에서 작성해야 한다.
    • ex)  
    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Image> imageList= new ArrayList<>();
    
    ※ @OneToMany로 설정한 내용은 부모 엔티티의 db 테이블에 값이 생기지는 않는다. 부모 엔티티와 자식 엔티티 사이의 관계만 연결해준다. 나는 개인적으로 이 부분이 헷갈렸었는데 cascade를 공부하면서 이 내용도 확실히 짚고 넘어갈 수 있었다.
  • orphanRemoval = true : 부모 엔티티에서 자식 엔티티를 리스트에서 제거할 때, db에서 해당 자식 엔티티가 삭제되도록 설정하여 고아 객체 제거

이렇게 엔티티 간의 연관관계를 설정할 때 CascadeType 옵션에 대해 공부해보았다. 그동안 엔티티에 CascadeType 설정을 해주는 경우에는 거의 대부분 ALL을 사용했던 것 같다. 하지만 무분별하게 ALL을 사용하는 것도 좋지 않으니 추후 이 부분을 수정하여야겠다.

이 포스팅은 인프런 김영한님의 스프링입문 강의 중 섹션1 빌드하고 실행하기 중 발생한 에러에 대한 내용입니다!

 

빌드하고 실행하기!

인텔리제이를 사용해서 실행하지 말고 cmd 창에서 빌드하고 실행해보는 방법

 

 

<윈도우 환경>

 

1. 코드가 있는 경로로 cmd 창 열기

2. gradlew 입력  (gradlew 를 입력하면 gradlew.bat가 실행된다고 한다)

3. gradlew build 입력

  -> 문제 발생!

다음과 같이 Execution failed for task ':test'  그리고 파일의 build폴더안의 ~~~~경로의 index.html에서 문제가 발생했다고 한다. 흠.. 저 경로의 해당 파일은 만진적도 없는데 말이다. 강의를 그대로 따라하고 있었는데 왜 에러가 뜬건지 알 수 없지만

 

여기 저기 구글링을 해본 결과

gradlew build 를 입력할 때 --debug 를 추가하면 해결된다고 한다. 

 

3. gradlew --debug build 입력!

수많은 디버깅 코드들과 함께 빌드가 정상 작동되고

 

4. cd build/libs  입력(libs 폴더로 이동하기)

5. java -jar hello.spring-0.0.1-SNAPSHOT.jar 입력하면 실행 성공!

  (참고로 리눅스에서 내가 습관적으로 사용하는 ls 는 윈도우에서 dir 이다)

localhost:8080 으로 이동해보면 성공적으로 실행되는 것을 확인할 수 있다.!!!

 

+ Recent posts