해당 게시글에는
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard
의 섹션 5 데이터 접근 기술 - JPA을 수강하며 알게된 점들을 정리한 내용이 담겨있습니다.
1. JPQL이 SQL과 다른점
JPQL은 Java Persistence Query Language로 엔티티를 조회하기 위해 사용하는 쿼리 언어이다.
따라서 JPQL은 SQL과 90%이상 유사하지만 조회하는 대상이 분명 다르다.
간혹, JPQL로 작성된 코드를 볼 때 어색하고 낯선 느낌이 들기도 하지만 엔티티 객체를 조회하는 쿼리 언어라고 생각하면 쉽게 접근할 수 있다. (그래도 모르겠다면 SQL을 좀 더 공부해보는 것도 좋은 방법일 것 같다.)
// SQL
select * from ITEM;
// JPQL
select i from Item i;
두 쿼리언어는 같은 select문을 사용하고 있지만 형태가 살짝 다르다. 먼저, SQL은 테이블의 컬럼을 조회 대상으로 삼는다. (* 또는 특정 컬럼명 지정) 하지만 JPQL은 컬럼이 아닌 엔티티 객체를 조회 대상으로 삼는다.
또 다른 특징은 JPQL은 대소문자를 구분하기에 실제 엔티티 클래스의 이름과 동일하게 작성해야 하며, 해당 엔티티에 대한 별칭(위 예제에서는 i)을 꼭 사용해야 한다.
JPQL을 사용하면 where절, like절 등을 사용할 수 있고, 특정 파라미터로 바인딩할 수 있다. 그러나 이처럼 런타임시에도 동일한 쿼리 구조를 갖는 정적 쿼리가 아니라 런타임에 따라 쿼리가 변하는 동적 쿼리를 작성하려면 상당히 복잡하다.
@Override
public List<Item> findAll(ItemSearchCond cond) {
String jpql = "select i from Item i";
// 동적 쿼리 부분
if (StringUtils.hasText(itemName) || maxPrice != null) {
jpql += " where";
}
boolean andFlag = false;
List<Object> param = new ArrayList<>();
if (StringUtils.hasText(itemName)) {
jpql += " i.itemName like concat('%',:itemName,'%')";
param.add(itemName);
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
jpql += " and";
}
jpql += " i.price <= :maxPrice";
param.add(maxPrice);
}
...
}
특정 조건에 따라 jpql에 where절 혹은 like절을 추가하고 있다. 실무 환경에서는 이보다 훨씬 복잡할텐데 마치 JPA에서 JDBC의 냄새가 느껴지는 것 같다. 이러한 동적 쿼리를 깔끔하게 사용할 수 있는 Querydsl이라는 기술이 존재한다. 이 내용은 아직 배우지 않아서 강의를 더 들어보고 정리해야 할 것 같다.
2. JPA의 예외 변환
이제 JPA가 예외를 어떻게 변환하는지를 정리해보고자 한다. 그전에 우선 예외 로그를 확인하는 방법을 알아야 한다.
흔히 스택 트레이스라고 하며, 우리가 작성한 코드에서 예외가 발생했을 때, 빨간 글씨가 콘솔 창에 쭉 뜬다.
의존성이 복잡한 코드일 수록 예외가 발생했을 때, 스택 트레이스의 출력량은 상당하다.
따라서 스택 트레이스에서 중점적으로 봐야할 부분을 알지 못하면 원인을 분석하는 데 있어서 많은 시간을 소요할 수 있다.
필자는 해당 게시글로 배웠다. 해당 게시글에서는 스택 트레이스에서 어떠한 부분을 중점적으로 확인해야 하는지와
그 예시로 NPE가 발생한 스택 트레이스를 분석하는 방법을 소개하고 있다. 이 내용을 이해해야 JPA가 예외를 변환하는
부분을 쉽게 이해할 수 있다. 스택이라는 말에서 알 수 있듯이, 스택 트레이스의 로그는 밑에서 위로 올라가며 읽어야 한다.
즉, 문제의 근원이 되는 부분은 스택에 제일 먼저 들어가 있기에 밑에서 위로 올라가야 하는 것이다.
그렇다면, JPA의 예외가 변환되는 걸 이해할 것인데 먼저 JPA의 예외가 변환된다는 것이 무슨 뜻인지 확인해보자.
JPA에서 발생하는 예외는 스프링과 관련이 없다. 우리는 DB에 접근하기 위한 기술로 JDBC, MyBatis, JPA 등을 사용할 수 있다. 즉, 스프링 입장에서 JPA는 외부 기술이다. 따라서 이 둘은 관계가 없다. 즉, JPA는 JPA만의 예외를 발생시킬 수 밖에 없는 것이다.
JPA는 RuntimeException(언체크 예외)을 상속한 PersistenceException과 그 하위 예외를 발생시킨다.
(추가로 JPA는 IllegalStateException과 IllegalArgumentException을 발생시킬 수도 있다.)
스프링은 기본적으로 DB에서 발생한 예외를 DataAccessException으로 변환하여 추상화한다.
앞서 말했듯이, 스프링은 다양한 DB 기술들을 사용할 수 있다. 그런데 이들이 구현한 각각의 예외와 의존하게 되면
객체지향 원칙에도 위반된다.
- 서비스 계층은 구체화된 모듈이 아닌 추상화된 모듈에 의존해야 한다 : DIP
- 서비스 계층은 데이터 접근과 관련된 구현체(JPA 관련 예외)를 알 필요가 없으며, 비즈니스 로직에 대한 책임만 가져야 한다 : SRP
따라서 스프링은 JPA를 사용할 경우, JPA에서 발생한 예외를 스프링이 추상화한 DataAccessException으로 변환해야 하는 것이다.
예외를 추상화하지 않았을 때)
먼저, JPA를 사용할 때의 의존관계는 다음과 같다. Spring Data JPA를 사용하지 않았기 때문에 Repository 클래스가 EntityManager를 주입받아 사용하는 형태임을 참고하자. 그런데 JPA 예외가 비즈니스 로직을 처리하는 Service 클래스에까지 그대로 전달된다.
즉, PersistenceException을 Service 클래스에서 try-catch문으로 처리하게 된다면 Service 클래스는 JPA 관련 패키지와의 의존관계가 생긴다. (메서드 상에서 PersistenceException이 드러나기 때문에 import 발생! 만약, build.gradle에서 JPA 의존성을 제거한다면? Service 클래스에서 컴파일 에러가 발생할 것임)
그렇다면, 스프링은 JPA 관련 예외를 어떻게 변환할까?
잘 생각해보면 JPA에서 발생하는 예외를 스프링 예외로 변환하는 것은 어쩌면 어플리케이션의 비즈니스 로직 흐름에
중요하지 않을 수 있다. 스프링 AOP의 용어를 빌려쓰자면 예외를 변환하는 것은 공통 관심사(Cross-Cutting-Concern)에 해당한다. 따라서 스프링은 예외를 처리하는 로직에 대해 스프링 AOP 기술을 적용한다.
우리는 Repository를 컴포넌트 스캔 대상으로 등록하기 위해 @Repository를 사용한다. 이 때, @Repository가 붙으면 해당 클래스의 AOP 프록시 객체가 생성된다. 즉, @Repository를 붙임으로써 JPA 예외를 추상화할 수 있게 된다.
@Repository를 적용했을 때)
Repository 클래스에 @Repository가 붙게 되면, Repository와 연관관계가 맺어졌던 Service는 실제 객체가 아닌 AOP 프록시 객체와 연관관계를 맺는다. (코드 상에서는 드러나지 않음.)
그렇다면 @Repository를 붙였을 때와 붙이지 않았을 때 각각 예외가 어떻게 전달되는지를 스택 트레이스로 확인해보자.
Repository 클래스의 findAll 메서드에서 jpql 문법이 잘못 작성되었을 경우
해당 메서드를 실행하는 테스트 메서드에서 발생하는 스택 트레이스를 비교해보았다.
@Repository를 안 붙였을 때)
사진 밑에 있는 "Caused by"를 확인해보자. 일단 SyntaxException이라는 예외가 발생했고 이를 IllegalArgumentException으로 변환해서 던진 걸 확인할 수 있다. 앞서, JPA는 PersistenceException 이외에도 IllegalArgumentException으로 던질 수 있다는 걸 확인했다. 물론, 케이스에 따라 PersistenceException이라는 JPA 예외가 발생할 수도 있는 것이다.
@Repository를 붙였을 때)
@Repository를 붙임으로써 예외를 변환하는 AOP 프록시 객체가 생성되었고, JPA에서 발생한 예외를 DataAccessException을 상속한 InvalidDataAccessApiUsageException으로 변환하였음을 알 수 있다.
이전 게시글
'Spring Framework > Spring Boot' 카테고리의 다른 글
[JPA] 스프링 Data JPA 기술을 공부하며 배운 내용 정리 (0) | 2024.08.14 |
---|---|
[JPA] 스프링 JPA 기술을 공부하며 배운 내용 정리 (1) (0) | 2024.08.03 |
멋사 미션을 하며 스프링 JdbcTemplate에 대해 알게 된 내용 (0) | 2024.05.20 |
멋사 미션을 하며 스프링 부트 예외처리에 대해 알게 된 내용들 (0) | 2024.05.14 |
관심사의 분리, 의존관계 주입(DI) (0) | 2022.12.30 |