본문 바로가기
Develop Study/Spring 2025. 7. 16.

(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 구현도 한번더 점검할 수 있었다.