(25.07.16) Java Spring Boot 환경에서 CTE 동작 확인 (MyBatis 와 JPA 활용)
2025.05.13 - [Develop Study/Database] - (25.05.13) SQL 재귀 WITH RECURSIVE
(25.05.13) SQL 재귀 WITH RECURSIVE
SQLD 자격증을 공부하면서 습득했던 구문들과 문법들을 나름 다 활용하고 있다고 생각했는데,Java 기반의 알고리즘 프로그래밍에서나 볼 수 있었던 "재귀" 에대해서 SQL구문을 활용을 하는 예제를
andrew75313.tistory.com
SQL의 서브 쿼리를 간편하게 작성될 수 있도록 CTE(Common Table Expression)를 활용한 쿼리를 공부하고 많이 활용하고자 하고 있다.
PostgreSQL 과 MySQL에서 직접 쿼리를 날려서 데이터를 Fetch 해올 때는 정상적으로 작동을 하지만, 이러한 코드를 실제로 Spring Boot 에서 활용할 수 있을지 직접 확인하고자 했다.
Spring Data JPA 의 하나의 단점이 CTE 처럼 긴 쿼리일 경워 @Query 를 통한 커스텀 쿼리로 작성하면서 직관적으로 볼 수 없다는 단점이 있을 수 있기 때문에, 비교적 Legacy 기술인 MyBatis를 활용해 Mapping해서 확인하고자 했다.
MyBatis
MyBatis
- 반복적인 JDBC 프로그래밍을 단순화 시켜주는 Java Persistence Framework
- SQL 쿼리들을 XML 파일에 작성 → 코드와 SQL을 분리하여 관리를 목적
XML을 통한 쿼리 코드 작성
- 결과값을 맵핑하는 객체는 JPA 에 비해 존재 X
- 간단한 설정 (Mapper의 이름을 통한 설정)
- 관심사를 분리 = SQL 문을 따로 관리할 수 있음 → 유지보수 효율
- 복잡한 쿼리나 다이나믹하게(동적쿼리) 변경되는 쿼리 작성
- XML 안에있는 SQL을 Java의 메소드에 매핑이 가능(Mapper)
MyBatis 캐시 계층
- 1차 캐시 (Session Cache)
- SqlSession 단위에서 동작
- 같은 SqlSession 안에서 같은 쿼리와 파라미터를 사용할 경우, DB를 다시 조회하지 않고 캐시된 결과를 사용하는 특징
- 트랜잭션 단위로 사용되고, 세션 종료 시 캐시도 사라짐
- 2차 캐시 (Mapper Cache)
- Mapper 단위로 공유되는 캐시
- 특정 Mapper에서 수행한 결과가 전체 애플리케이션 내에서 재사용 가능
- 단, 사용하려면 @CacheNamespace 어노테이션 또는 XML 설정 필요
- JPA 일 경우, 1차 캐시 (EntityManager 단위), 2차 캐시 (Hibernate 기준으로는 @Cacheable 사용하여 설정) 과 구분이 됨
MyBatis 를 통한 CTE 쿼리 실행하기
MyBatis 설정
dependencies {
...
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
}
- Spring Boot 기준으로 mybatis spring boot starter을 주입
Mapper와 XML 설정
http://mybatis.org/dtd/mybatis-3-mapper.dtd>">
WITH invalid_user AS (
SELECT id
FROM users
WHERE status = 'DEACTIVATED'
AND role = 'USER'
)
SELECT
id
FROM invalid_user
- 실행시킬 CTE 쿼리를 포함한 UserMapper.xml을 src/main/resources/mapper/ 경로에 생성을 했음
- 경로는 resources 에 작성하는 것이 일반적
- 위의 CTE는 직접 USER 중에 회원탈퇴 → DEACTIVATED 상태가 된 사용자의 아이디만을 반환하는 쿼리를 임의로 작성, DB(PostgreSQL)에서 작동하는지 확인하는 것이 목적
@Mapper
public interface UserMapper {
List<UUID> selectInvalidUserIds();
}
- XML에서는 UserMapper 클래스의 메서드 이름 = selectInvalidUserIds 그리고, Mapper 클래스의 위치 = com.example.boilerplatepractice.domain.users.mapper.UserMapper 가 명시가 되어있어야함
- UserMapper 에서는 XML 에 적혀진 이름, 위치 외에 반환 타입(List<UUID>) 가 모두 일치해야함 → 매우 중요
spring:
mybatis:
mapper-locations: classpath:mapper/*.xml
- application.yml 기준 spring 이 mybatis의 XML 파일의 위치를 파악할 수 있도록 mapper-location을 위와 같이 명시적으로 설정
Controller, Service 설정
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
...
@GetMapping("/invalid")
public ResponseEntity<DataResponseDTO<?>> getInvalidUsers() {
DataResponseDTO<List<UUID>> response = userService.getInvalidUserIds();
return new ResponseEntity<>(response ,HttpStatus.OK);
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserMapper userMapper;
...
public DataResponseDTO<List<UUID>> getInvalidUserIds() {
List<UUID> invalidIds = userMapper.selectInvalidUserIds();
return new DataResponseDTO<List<UUID>>(invalidIds);
}
}
- /api/users/invalid 의 GET 요청 시 service 단에서 UserMapper의 selectInvalidUserIds 메서드가 미리 UserMapper.xml 에 작성된 CTE를 포함하 SQL을 실행 → UUID 타입의 사용자 아이디를 반환할 수 있도록 간단하게 작성
실행 결과 - 오류 발생
미리 작성된 DB에서의 DEACTIVATED된 USER의 ID인 baa19744-af95-4004-96e2-4903ca9e6d29 가 Response 되도록 해야함
- 모든 XML, Mapper클래스, application.yml 설정을 모두 다시 한 번 확인을 해도, 위의 Invalid bount statement (not found) 발생
- 이는 MyBatis 가 정상적으로 주입된 상태일 때, 메서드 이름이 명확하게 일치하지 않는 경우에 발생하는 에러
복합 기술 스택에 의한 에러
- MyBatis와 JPA(Hibernate)를 동시에 사용하는 Spring Boot 프로젝트에서, MyBatis Mapper가 동작하지 않거나 쿼리가 실행되지 않는 문제
- 위의 상황에서는 이미 Repository를 통해 Spring Data JPA가 자동으로 적용되고 있는 개발 구조
- MyBatis와 JPA는 각각의 생명주기와 초기화 우선순위가 다르므로, 자동 설정만 믿고 함께 쓰면 충돌이 다양한 방면에서 발생
- 기술 일관성과 유지보수의 효율성을 위해 하나만 채택하는 것이 일반적
- JPA 는 EntityManager, MyBatis는 SqlSession 기반이기 때문에 서로 독립적으로 작동 → 각각 작동이 잘 되더라도, Transaction 전파, 동기화에서 반드시 이슈가 발생할 가능성이 너무 높음
- → 두 개의 설정을 수동으로 분리 하거나, 혼용하는 것을 지양(Legacy 코드의 MyBatis와 혼용되어 개발 시 제외)
- 분리를 위해 MyBatis용 Config 클래스를 직접 지정
이슈 해결 → CTE 실행을 확인하는 것이 목적이므로 JPA 활용
@Repository
public interface UserRepository extends JpaRepository<User, UUID> {
@Query(value = """
WITH invalid_user AS (
SELECT id
FROM users
WHERE status = 'DEACTIVATED'
AND role = 'USER'
)
SELECT id
FROM invalid_user
""",
nativeQuery = true
)
List<UUID> selectInvalidUserIds();
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserMapper userMapper;
...
public DataResponseDTO<List<UUID>> getInvalidUserIds() {
List<UUID> invalidIds = userRepository.selectInvalidUserIds();
return new DataResponseDTO<List<UUID>>(invalidIds);
}
}
- Repository의 selectInvalidUserIds 메서드를 nativeQuery를 사용해서 작성
- “”” 를 사용해 띄어쓰기 텍스트 까지 포함 할 수 있도록 함
- nativeQuery를 사용해야지 DB 내 CTE를 실행시킬 수 있음
- 정상적으로 baa19744-af95-4004-96e2-4903ca9e6d29아이디가 반환
참고
https://yjkim-dev.tistory.com/66
JPA 와 Mybatis 같이 사용하기
안녕하세요. JPA 는 ORM 으로 단순 CRUD 처리에 대해 편리하게 해주는데요. 실무에서 정산 쿼리나, 복잡한 여러 조인이 필요한 쿼리들이 있을 때에는, JPA 보단 mybatis를 혼용해서 쓴다고 합니다. 그래
yjkim-dev.tistory.com
https://m.blog.naver.com/kimtaru2/221789017884
[SpringBoot] JPA & Mybatis를 혼용한 SpringBoot 개발 환경 구축
JPA는 클래스간의 관계(일대일, 일대다 등)를 모델링하여 쿼리작성 없이 데이터를 가져올 수 있지만, 이...
blog.naver.com
MyBatis를 활용하기 위해서는 @Configuration 클래스를 하나 작성을 해야하나, 이미 BoilerPlate가 Spring Data Jpa로 자동으로 작동될 수 있도록 구성되어있어 추가로 더 기술스택을 얹지는 않고자 했다.
그러나 Legacy 코드를 다루는건 포기할 수 없는 부분이기 때문에 CTE 활용을 적용하면서 MyBatis 구현도 한번더 점검할 수 있었다.