JPA와 프로시저
저는 현재 스프링부트와 MSSQL을 사용해서 프로젝트를 진행하고 있는데요.
여러 개의 테이블에서 작업을 진행할 때 프로시저를 주로 사용하고 있습니다.
그러던 중에 궁금한 점이 생겼습니다.
"이렇게 프로시저를 통해 처리하는 작업을 JPA에서는 어떻게 처리할까?" 였습니다.
JPA를 몇 번 찍먹으로 사용해본적만 있을 뿐, 제대로 프로젝트에 적용해본적은 없어서 의문점이 들었습니다.
결론부터 얘기하면 3가지 정도의 방법이 있는데요.
1. @Transactional 사용
각각의 엔티티를 정의하고 해당 엔티티를 처리하는 Repository를 서비스 계층에서 동시에 사용합니다.
이 때, @Transactional을 통해 오류 발생 시 모든 작업이 롤백되도록 합니다.
이렇게 하면 프로시저에서 TRY~CATCH 문을 통해 예외를 체크하는거랑 동일하게 동작시킬 수 있습니다.
@Entity
public class User {
@Id
private Long id;
private String name;
private int age;
// getters and setters
}
@Entity
public class Order {
@Id
private Long id;
private String itemName;
private int quantity;
// getters and setters
}
public interface UserRepository extends JpaRepository<User, Long> {
// 추가적인 사용자 관련 쿼리 정의 가능
}
public interface OrderRepository extends JpaRepository<Order, Long> {
// 추가적인 주문 관련 쿼리 정의 가능
}
@Service
public class MyService {
private final UserRepository userRepository;
private final OrderRepository orderRepository;
@Transactional
public void processUserAndOrder(User user, Order order) {
userRepository.save(user); // User 엔터티를 저장 (User 테이블에 데이터 추가)
orderRepository.save(order); // Order 엔터티를 저장 (Order 테이블에 데이터 추가)
}
}
2. JPA 커스텀 쿼리
@Query 어노테이션으로 JPQL이나 네이티브 sql 쿼리를 사용해 여러 테이블을 동시에 업데이트하거나 삭제합니다.
여기서 JPQL (Java Persistence Query Language)은 JPA에서 사용하는 객체 지향 쿼리 언어입니다.
SQL과 유사하지만, SQL이 데이터베이스 테이블에서 직접 쿼리를 실행하는 반면, JPQL은 엔티티 객체를 대상으로 쿼리를 실행합니다.
덕분에 JPQL은 데이터베이스에 독립적이어서 다양한 DBMS에서도 동일하게 사용할 수 있습니다.
JPQL에 대해서 좀 더 알아보겠습니다.
JPQL의 특징으로는 3가지 정도를 얘기할 수 있습니다.
1. 엔티티 기반
2. DBMS 독립성
3. 일반적인 쿼리 작업 지원: JPQL은 `SELECT`, `UPDATE`, `DELETE`, `JOIN`, `GROUP BY` 등 다양한 SQL 구문을 사용할 수 있습니다.
예제를 조금 살펴보겠습니다.
1. 데이터 조회 (SELECT)
`User`라는 엔티티에서 특정 나이 이상의 사용자를 조회하는 쿼리입니다.
String jpql = "SELECT u FROM User u WHERE u.age > :age";
List<User> users = entityManager.createQuery(jpql, User.class)
.setParameter("age",20)
.getResultList();
여기서 `User`는 엔티티 클래스 이름이고, `u.age`는 User 엔티티의 필드입니다.
2. 데이터 수정 (UPDATE)
String jpql = "UPDATE User u SET u.status = :status WHERE u.id = :id";
entityManager.createQuery(jpql)
.setParameter("status", "INACTIVE")
.setParameter("id", 1L)
.executeUpdate();
3. 데이터 삭제 (DELETE)
String jpql = "DELETE FROM User u WHERE u.status = :status";
entityManager.createQuery(jpql)
.setParameter("status", "INACTIVE")
.executeUpdate();
하지만 여기서 한가지 의문점이 들었습니다.
"그러면 JPA보다 그냥 JPQL을 쓰면 가장 이상적인거 아닌가?"
하지만 언제나 그렇듯 저의 지나친 일반화였습니다.
JPA와 JPQL을 섞어 사용할 때 각각의 장점을 살리면서도 효율적인 코드 작성이 가능합니다.
CRUD와 같이 표준적인 작업은 JPA 기본 기능으로 처리하고, 복잡한 쿼리 작업은 JPQL로 해결하는 방식이 가장 효율적입니다.
그리고 고유한 DBMS의 기능이 필요하거나 성능 최적화가 필요할 때 네이티브 SQL을 사용하기도 합니다.
@Query(value = "SELECT * FROM users WHERE status = :status", nativeQuery = true)
List<User> findUsersByStatus(@Param("status") String status);
하지만 네이티브 SQL을 사용하면 그만큼 DBMS에 종속되기 때문에 마이그레이션에 있어서 까다로워집니다.
3. Stored Procedure와 JPA 통합
흐름이 조금 끊겼지만 JPA에서 프로시저를 대체하는 방식 중 마지막 세번째는 기존의 프로시저를 `@Procedure`를 통해 JPA에서 직접 호출하는 방법입니다.
일반적으로 마이바티스에서 프로시저를 호출하는 방법과 크게 차이는 없습니다.
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.query.Procedure;
import org.springframework.data.repository.query.Param;
public interface UserRepository extends JpaRepository<User, Long> {
@Procedure(name = "update_user_age")
void updateUserAge(@Param("user_id") Long userId, @Param("new_age") int newAge);
}
이렇게 Mybatis에서 프로시저로 처리하던 작업을 JPA에서 어떻게 처리하는지 알아봤는데요.
그동안 제가 Mybatis와 프로시저를 사용하면서 느낀 단점들은 명확했습니다.
그 중에서도 가장 크게 느낀건 유지보수가 너무 어렵다는 점입니다.
프로시저의 변경에 영향을 받는데 변경 이력이 관리가 안되다보니깐 기능이 수정되었을 때, 특히나 CICD를 통해 반영하려고 해도 프로시저는 따로 수정해줘야 하는 번거로움이 있었습니다.
그리고 개인 프로젝트를 진행할 때 아직 서버가 없는 경우가 많은데, 그럴 때 여러 군데서 작업을 하면 항상 데이터베이스의 내용을 맞춰주는게 까다로웠습니다.
근데 JPA를 사용하면서 그런 부분들을 해결해보고 나니깐 JPA의 편리성을 확실히 더 느끼게 되었습니다.
제가 느꼈던 유지 보수에 대한 어려움과 소규모 프로젝트의 증가로 인해 JPA가 더 유용하게 쓰임받는 상황이 되지 않았나 생각이 듭니다.
하지만 컴퓨터의 세계에서 모든 것은 trade-off 입니다.
범용성을 갖추면 결국 성능은 떨어지기 마련입니다.
성능을 최적화하고 DBMS에 특화된 기능을 사용하려면 결국 SQL을 쓸 수 밖에는 없습니다.