SpringBoot 를 배워보자

MyBatis를 활용한 유연하고 효율적인 동적 SQL(Json&Pageing) 전략

_Blue_Sky_ 2024. 11. 30. 13:11
728x90
728x90

수백 개의 동적 SQL 조각을 효율적으로 관리하려면, 디렉토리 구조를 기능별로 구성하고, 공통 부분은 상위 디렉토리에 두는 방식이 적합합니다. 이를 MyBatis의 XML Mapper와 디렉토리 구조를 활용하여 설계하는 방안을 아래와 같이 제안드립니다.

 


2024.11.30 - [오라클 데이터베이스 강좌] - Oracle에서 JSON 형태로 데이터 생성하기

 

Oracle에서 JSON 형태로 데이터 생성하기

Oracle은 JSON 처리를 위한 다양한 내장 함수와 기능을 제공합니다. 비슷한 결과를 얻으려면 JSON_OBJECT, JSON_ARRAYAGG, 그리고 ROWNUM, OVER() 같은 SQL 기능을 활용할 수 있습니다. Oracle에서는 아래와 같은

notion4570.tistory.com

 


0. 디렉토리 구조 설계

다음은 SQL 조각과 매퍼 파일의 디렉토리 구조 예시입니다:

/resources
├── /mappers
│   ├── /common
│   │   ├── pagination.xml
│   │   ├── sorting.xml
│   │   ├── filters.xml
│   ├── /users
│   │   ├── userQueries.xml
│   │   ├── adminQueries.xml
│   ├── /products
│   │   ├── productQueries.xml
│   ├── /orders
│   │   ├── orderQueries.xml
  • /common: 공통적인 페이징, 정렬, 필터 로직이 포함된 SQL 조각.
  • 기능별 디렉토리 (/users, /products, /orders): 기능별로 SQL 조각 및 관련 매퍼를 배치.

 

728x90

 


1. 공통 페이징 템플릿 수정 (/common/pagination.xml)

JSON_OBJECT의 필드 구성도 동적으로 변경할 수 있도록 템플릿화합니다.

<sql id="basePagination">
    SELECT JSON_OBJECT(
       ${jsonFields}
    ) AS result
    FROM (
        <include refid="${dynamicQuery}" />
    ) sub
    WHERE ROWNUM BETWEEN (#{currentPage} - 1) * #{pageSize} + 1 AND #{currentPage} * #{pageSize}
</sql>
  • ${jsonFields}: JSON_OBJECT 내부의 필드들을 동적으로 정의합니다.
  • ${dynamicQuery}: 동적 SQL 조각을 삽입합니다.

2. 기능별 SQL 조각 구성 (/users/userQueries.xml, /products/productQueries.xml)

다양한 데이터 소스를 위한 SQL 조각과 JSON 필드 구성을 별도로 관리합니다.

/users/userQueries.xml

<mapper namespace="com.example.mapper.UserMapper">

    <!-- 사용자 목록 쿼리 -->
    <sql id="userList">
        SELECT id, name, age
        FROM users
        WHERE active = 1
        ORDER BY id
    </sql>

    <!-- 사용자 JSON 필드 구성 -->
    <sql id="userJsonFields">
        'id' VALUE id,
        'name' VALUE name,
        'age' VALUE age
    </sql>

</mapper>

/products/productQueries.xml

<mapper namespace="com.example.mapper.ProductMapper">

    <!-- 제품 목록 쿼리 -->
    <sql id="productList">
        SELECT id, product_name, stock
        FROM products
        WHERE available = 1
        ORDER BY product_name
    </sql>

    <!-- 제품 JSON 필드 구성 -->
    <sql id="productJsonFields">
        'id' VALUE id,
        'productName' VALUE product_name,
        'stock' VALUE stock
    </sql>

</mapper>

3. 동적 쿼리와 JSON 필드를 Mapper에서 주입

Mapper Interface에서 SQL 조각 IDJSON 필드 구성 ID를 전달받도록 설계합니다.

UserMapper.java

@Mapper
public interface UserMapper {

    List<Map<String, Object>> getPagedData(
        @Param("currentPage") int currentPage,
        @Param("pageSize") int pageSize,
        @Param("dynamicQuery") String dynamicQuery,
        @Param("jsonFields") String jsonFields
    );
}

4. Mapper XML에서 동적 쿼리와 JSON 필드 조합

Mapper XML에서 공통 템플릿을 호출하면서, JSON 필드와 동적 쿼리를 연결합니다.

<mapper namespace="com.example.mapper.UserMapper">

    <select id="getPagedData" resultType="map">
        <include refid="common.basePagination" />
    </select>

</mapper>

5. Service Layer에서 SQL 조각과 JSON 필드 전달

Service에서 필요한 SQL 조각과 JSON 필드 ID를 선택하여 Mapper에 전달합니다.

UserService.java

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public List<Map<String, Object>> getPagedUsers(int currentPage, int pageSize) {
        return userMapper.getPagedData(
            currentPage,
            pageSize,
            "userList",         // 동적 쿼리
            "userJsonFields"    // JSON 필드
        );
    }
}

ProductService.java

@Service
public class ProductService {

    @Autowired
    private ProductMapper productMapper;

    public List<Map<String, Object>> getPagedProducts(int currentPage, int pageSize) {
        return productMapper.getPagedData(
            currentPage,
            pageSize,
            "productList",        // 동적 쿼리
            "productJsonFields"   // JSON 필드
        );
    }
}

동작 과정 요약

  1. 공통 페이징 로직: basePagination은 고정된 페이징 로직을 제공하며, 동적으로 SQL과 JSON 필드를 삽입합니다.
  2. 기능별 SQL 및 JSON 필드: 각 기능별로 쿼리와 JSON 필드 구성을 별도의 XML 파일에서 정의.
  3. Mapper 동적 주입: 서비스 레이어에서 필요한 SQL 조각과 JSON 필드를 Mapper에 전달.
  4. 결과: 다양한 레이아웃에 따라 JSON_OBJECT가 동적으로 생성.

 

728x90

결과 예시

getPagedUsers 호출 결과 (Users)

{
  "currentPage": 1,
  "totalPages": 10,
  "totalItems": 100,
  "data": [
    { "id": 1, "name": "John", "age": 30 },
    { "id": 2, "name": "Jane", "age": 25 }
  ]
}

getPagedProducts 호출 결과 (Products)

{
  "currentPage": 1,
  "totalPages": 5,
  "totalItems": 50,
  "data": [
    { "id": 1, "productName": "Laptop", "stock": 15 },
    { "id": 2, "productName": "Mouse", "stock": 200 }
  ]
}

장점 및 추가 고려사항

장점

  1. 유지보수성:
    • JSON 필드와 SQL 쿼리가 분리되어 있어, 서로 독립적으로 관리 가능.
    • 공통 페이징 로직 수정 시 다른 부분을 변경할 필요 없음.
  2. 확장성:
    • 새로운 기능(테이블 또는 JSON 필드 추가)이 필요한 경우, 단순히 XML에 SQL 조각과 JSON 필드를 추가하면 됨.

고려사항

  1. SQL Injection 방지:
    • 동적 SQL과 JSON 필드는 사전 정의된 ID만 사용하도록 제한하여 보안 강화.
  2. 복잡성 증가:
    • 작은 규모의 프로젝트에는 오히려 복잡도를 높일 수 있으므로, 확장성이 필요한 경우에 적합.

이 방식이라면 동적 SQL과 JSON 필드를 완벽히 유연하게 제어할 수 있습니다. 😊

 

필자주:

퍼포먼스를 위해 먼저 페이징 처리한 후 조인을  해야 할 경우에는 이 방식은 수정되어야 함!

 

728x90
728x90