JPA의 이념과 SQL의 간극

정정일·2024년 11월 28일
1

JPA를 사용하다보면 한번씩 "어라? 이걸 지원하지 않네?" 하는 순간을 마주하게 됩니다.

JPA(Java Persistence API)는 Java 애플리케이션에서 데이터베이스와 객체 간의 간극을 줄이고, 객체 지향적 개발을 돕기 위해 설계된 도구입니다. 그러나 JPA를 사용하며 얻는 장점에도 불구하고, 실무에서 SQL의 기능과 JPA의 기능 사이에서 간극을 느끼는 경우가 있습니다. 특히 JPQL(Java Persistence Query Language)을 사용하는 과정에서, SQL의 기능이 JPA의 설계 철학과 충돌하여 제한되는 경우들을 경험할 수 있습니다.

이 글에서는 제가 겪었던 JPA와 SQL 간의 간극을 보여주는 몇 가지 사례를 통해, JPA의 이념과 한계를 살펴볼까 합니다.

JPA의 설계 이념

JPA는 단순히 SQL을 객체로 감싸는 것이 아니라, 데이터베이스가 아닌 객체 중심의 설계를 목표로 하고있습니다.

  • 데이터베이스 독립성: 특정 데이터베이스에 종속되지 않고, 다양한 데이터베이스에서 동작하도록 설계.
  • 엔티티 중심: 테이블이 아닌 객체(Entity)를 중심으로 데이터 모델링.
  • 추상화 제공: 데이터 접근의 복잡성을 추상화하여 비즈니스 로직에만 집중 가능.
  • 관계 매핑 자동화: 객체 간의 연관 관계를 데이터베이스 테이블로 매핑.

이러한 이념에 개발 생산성이 크게 향상되었지만, 실무에서는 데이터베이스와 객체 간의 표현 차이로 인해 몇 가지 제약이 생기기도 합니다.

JPA와 SQL의 간극

1. UNION / UNION ALL 미지원

SQL에서 UNIONUNION ALL은 여러 쿼리 결과를 하나로 합치는 기능입니다. 하지만 JPQL은 UNION과 같은 연산을 지원하지 않습니다. 왜냐하면 JPA는 엔티티를 중심으로 설계되었고, UNION 같은 연산은 엔티티 모델링의 기본 이념과 충돌하기 때문이라고 생각합니다.

어떤 의미에서는 JPA가 이렇게 묻는 것 같습니다

"Entity 설계를 제대로 했다면 UNION이 필요할 일이 있나?"라고

하지만 요구사항에 의해 UNION을 사용해야하는 상황 또한 있을 수 있습니다.
그런 경우 결국 Native Query를 써야 하죠.

2. JPQL에서 INSERT 미지원

SQL에서 데이터를 삽입하는 INSERT 문은 기본적인 기능이지만, JPQL은 이를 지원하지 않습니다. JPA는 데이터를 삽입할 때 EntityManager.persist() 메서드를 사용하는 객체 지향적 방식을 채택했습니다. 사실 JPQL에서 INSERT를 할경우는 거의 없겠지만요.

3. 서브쿼리의 FROM 절 미지원

SQL에서는 서브쿼리를 FROM 절에 사용하여 임시 결과를 테이블처럼 사용할 수 있습니다. 하지만 JPQL에서는 FROM 절에서 서브쿼리를 지원하지 않습니다. JPQL은 엔티티를 기준으로 작동하기 때문에, SQL처럼 임시 결과를 FROM 절에서 사용하는 기능은 지원되지 않습니다.

결국 이 부분에서도 SQL로 돌아가야 하는 상황이 생깁니다.

4. 서브쿼리에서 LIMIT 미지원

SQL에서는 서브쿼리 내에서도 LIMIT을 사용할 수 있습니다.
그러나 JPQL에서는 메인 쿼리에서만 setMaxResults()setFirstResult()를 통해 Limit을 사용할 수 있습니다. setMaxResults()setFirstResult()는 단순 페이징(Pagination) 처리 이기때문에 지원하지만 서브쿼리의 LIMIT은 FROM절을 지원하지 않는것과 비슷한 이유이지 않을까 싶습니다.

위와 같은 JPA와 SQL의 간극들은 제게 Entity 설계부터 제대로 하라고 다그치는 것과 같이 느껴졌습니다.

그러나 요구사항은 언제든 변한다.

그러나 현실적인 개발 환경에서는 요구사항이 자주 변하며, 데이터베이스와 객체 간의 간극이 생기는 경우가 있습니다.

1. 내가 설계한 테이블이 아닐 때

프로젝트 초기부터 객체 지향적 설계를 기반으로 데이터베이스를 설계했다면, 간극은 좁아질 수 있습니다. 하지만 실무에서는 대부분 이미 설계된 테이블, 즉 레거시 데이터베이스를 다뤄야 할 때가 많습니다.

그럴때마다 단지 JPA를 사용하기 위해서 스키마를 변경하고 데이터를 마이그레이션 해야하는 걸까요?

2. 갑작스러운 요구사항 변경

요구사항은 항상 변합니다. 처음에는 단순했던 데이터 모델이 시간이 지나면서 복잡해지고, 비즈니스 로직이 변경되며 기존의 설계나 시스템에 영향을 미칠 수 있습니다. 이때 JPA가 제공하는 추상화 계층이 유연성을 확보하는 데 도움이 되기보다는 오히려 장애물이 되는 경우도 있다고 생각합니다.

만약 처음에는 단순히 "고객과 주문"이라는 두 개의 테이블만 처리하면 되었지만, 이후 주문 테이블에 "배송 상태", "결제 이력", "환불 처리" 등 추가 정보를 관리해야 하는 상황이 발생했다고 가정해보면 기존 설계를 유지하면서 데이터를 처리하기 위해 추가적인 엔티티나 관계 매핑이 필요하게 됩니다. 이러한 요구사항은 기존의 코드 전체에 영향을 미칠 수 있습니다. 특히, 연관 관계가 깊게 설정된 엔티티일수록 변경으로 인한 영향 범위가 기하급수적으로 커집니다.

3. 복잡한 쿼리 요구사항

기존의 단순한 데이터 조회에서 벗어나, 갑작스럽게 "가장 많이 구매한 고객", "특정 기간 내 주문 증가율" 등 비즈니스 인사이트를 위한 복잡한 집계 쿼리를 작성해야 할 때가 있습니다.

이럴때 JPQL은 위에 제약들로 복잡한 집계나 조건부 쿼리에 적합하지 않은 경우들이 있습니다. 특히, 서브쿼리, 조인, 그룹화, 윈도우 함수 등 SQL의 고급 기능을 활용하기 어려운 상황이 생깁니다.

이런 경우, 결국 다시금 Native Query로 돌아가야 합니다.

JPA와 SQL의 조화

JPA는 객체 중심 설계를 지원하지만, 완벽한 해결책은 아니라고 생각합니다.

JPA를 사용하면서 어떻게 보면 데이터베이스에게서는 독립했으나 JPA에 종속적이게 돼가는것 같습니다.

JPA만 고집하지 않고 Native Query, QueryDSL, 또는 SQL Mapper와 같은 도구를 적절히 조합하여 사용해 여러 방면으로 고민을 하는 것이 적절하다는 생각을 가지게되네요.

결국 중요한 건, 도구에 얽매이지 않고 주어진 문제를 가장 효과적으로 해결하는 방법을 찾는 것 아닐까싶습니다.

profile
하루하루 더 발전하는 사람이 되기 위해 노력하는 개발자가 되고자 합니다.

0개의 댓글