개발인생/Project

페이징처리 프로젝트 구현(Oracle)

forri 2025. 3. 17. 19:29

BoardMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.susuma.board.model.BoardMapper">

	<!-- [1] 게시물 목록 -->
	<select id="selectBoards" parameterType="map" resultType="com.susuma.board.model.BoardDTO">
		SELECT *
		FROM (
		SELECT A.*, ROWNUM AS RN
		FROM (
		SELECT B.BO_NO, B.ME_NO, B.TYPE, B.TITLE, B.CONTENT, B.INSERT_TIME, B.UPDATE_TIME, B.ANSWER, B.ANSWER_INSERT_TIME, B.ANSWER_UPDATE_TIME
		, M.NAME
		FROM BOARD B
		JOIN MEMBER M
		ON M.ME_NO = B.ME_NO
		<choose>
			<when test=' type.equals("myask") '>
				WHERE B.TYPE = 'ask'
				AND B.ME_NO = #{meNo}
			</when>
			<otherwise>
				WHERE B.TYPE = #{type}
			</otherwise>
		</choose>
		<if test='answerCheck != NULL and answerCheck.equals("yes")'>
			AND B.ANSWER IS NOT NULL
		</if>
		<if test='answerCheck != NULL and answerCheck.equals("no")'>
			AND B.ANSWER IS NULL
		</if>
		<if test="title != null and title != ''">
	        AND title LIKE '%' || #{title} || '%'
	    </if>
		ORDER BY B.${sortField} ${sortOrder} NULLS LAST
		) A
		)
		WHERE RN BETWEEN #{startRow} and #{endRow}
	</select>

	<!-- [1-2] 게시물 목록 수(페이징 처리) -->
	<select id="countBoards" parameterType="map" resultType="int">
		SELECT COUNT(*)
		FROM BOARD B
		JOIN MEMBER M
		ON M.ME_NO = B.ME_NO
		<choose>
			<when test=' type.equals("myask") '>
				WHERE B.TYPE = 'ask'
				AND B.ME_NO = #{meNo}
			</when>
			<otherwise>
				WHERE B.TYPE = #{type}
			</otherwise>
		</choose>
		<if test="answerCheck != NULL and answerCheck.equals('yes')">
			AND B.ANSWER IS NOT NULL
		</if>
		<if test="answerCheck != NULL and answerCheck.equals('no')">
			AND B.ANSWER IS NULL
		</if>
		<if test="title != null and title != ''">
	        AND title LIKE '%' || #{title} || '%'
	    </if>
	</select>

	<!-- [2] 게시물 -->
	<select id="selectBoard" resultType="com.susuma.board.model.BoardDTO">
		SELECT * FROM BOARD WHERE BO_NO = #{boNo}
	</select>

	<!-- [3] 게시물 등록 -->
	<insert id="insertBoard" parameterType="com.susuma.board.model.BoardDTO">
		INSERT INTO BOARD(BO_NO, ME_NO, TYPE, TITLE, CONTENT)
		VALUES(BOARD_SEQ.NEXTVAL, #{meNo}, #{type}, #{title}, #{content})
	</insert>

	<!-- [4] 게시물 수정 -->
	<update id="updateBoard" parameterType="com.susuma.board.model.BoardDTO">
		UPDATE BOARD SET TITLE=#{title},CONTENT=#{content},UPDATE_TIME = SYSDATE
		WHERE BO_NO=#{boNo}
	</update>

	<!-- [5] 게시물 삭제 -->
	<delete id="deleteBoard" parameterType="string">
		DELETE FROM BOARD
		WHERE BO_NO = #{boNo}
	</delete>

	<!-- [6] 1:1문의 등록 -->
	<insert id="askWrite" parameterType="com.susuma.board.model.BoardDTO">
		INSERT INTO BOARD(BO_NO, ME_NO, TITLE, CONTENT, TYPE)
		VALUES(BOARD_SEQ.NEXTVAL, #{meNo}, #{title}, #{content}, #{type})
	</insert>

	<!-- [7] 1:1문의 -->
	<select id="askView" resultType="com.susuma.board.model.BoardDTO">
		SELECT * FROM BOARD WHERE BO_NO = #{boNo}
	</select>

	<!-- [8] 이전글 다음글 -->
	<select id="getPreNext" resultType="com.susuma.board.model.BoardDTO">
		select * from(SELECT
		BO_NO,
		LEAD(BO_NO,1,9999) OVER(ORDER BY BO_NO) AS next,
		LAG(BO_NO,1,9999) OVER(ORDER BY BO_NO) AS last,
		TITLE,
		LEAD(TITLE,1,9999) OVER(ORDER BY BO_NO) AS nexttitle,
		LAG(TITLE,1,9999) OVER(ORDER BY BO_NO) AS lasttitle

		FROM BOARD
		WHERE TYPE = 'notice'
		order by BO_NO DESC)
		WHERE BO_NO = #{boNo}

	</select>
	<!-- [9] 1:1답변 등록 -->
	<update id="insertReply" parameterType="com.susuma.board.model.BoardDTO">
		UPDATE BOARD
		SET
		ANSWER = #{answer},
		ANSWER_INSERT_TIME = CASE
		WHEN ANSWER_INSERT_TIME IS NULL THEN SYSDATE
		ELSE ANSWER_INSERT_TIME
		END,
		ANSWER_UPDATE_TIME = CASE
		WHEN ANSWER_INSERT_TIME IS NOT NULL THEN SYSDATE
		ELSE ANSWER_UPDATE_TIME
		END
		WHERE BO_NO = #{boNo}
	</update>

	<!-- [10] 1:1문의 삭제 -->
	<delete id="askDelete" parameterType="string">
		DELETE FROM BOARD
		WHERE BO_NO = #{boNo}
	</delete>



</mapper>

 

  • ROWNUM 기반 페이징
    • 게시글 목록 조회시 내부 서브쿼리에서 조회된 결과에 대해 ROWNUM AS RN을 부여하고, 외부 쿼리에서 WHERE RN BETWEEN #{startRow} and #{endRow}로 원하는 범위의 행을 선택하는 구조를 썼습니다.
  • 동적 SQL 조건 처리
    • <choose>, <when>, <otherwise>, <if> 태그들을 사용해 검색 조건을 동적으로 구성합니다. 예를 들어, type이 "myask"인 경우와 그렇지 않은 경우에 따른 조건문, 답변 여부(answerCheck) 및 제목 검색(title LIKE) 조건을 유연하게 처리합니다.
  • 정렬 동적 처리
    • ORDER BY B.${sortField} ${sortOrder} NULLS LAST 구문을 사용하여 클라이언트에서 전달하는 정렬 필드와 정렬 순서에 따라 동적으로 정렬합니다.
  • Oracle 전용 기능 사용
    • 삽입 쿼리에서 BOARD_SEQ.NEXTVAL을 사용해 자동으로 기본 키를 생성하고, 업데이트 쿼리에서는 SYSDATE를 이용해 현재 시간을 기록하는 등 Oracle DB의 고유 기능을 활용하고 있습니다.
  • 분석 함수 사용
    • <select id="getPreNext"> 쿼리에서는 LEAD와 LAG 함수로 이전 및 다음 게시물을 조회하는 기능을 제공하여, 상세 조회 화면에서 인접한 게시물로의 네비게이션을 지원합니다.

 

📍추가적인 성능 최적화 방법

1. 인덱스 최적화 (검색 속도 개선)

- 문제: WHERE 조건이나 JOIN을 사용할 때, 데이터베이스가 전체 테이블을 조회하면 시간이 오래 걸릴 수 있음.

- 해결 방법:

  • 자주 검색하는 컬럼(예: TYPE, ME_NO, TITLE)인덱스를 추가하면 검색 속도가 더 빨라질 수 있음.
  • 인덱스는 책의 목차와 같은 개념임. 목차가 있으면 원하는 내용을 바로 찾을 수 있는 것처럼, 인덱스를 활용하면 DB도 원하는 데이터를 빠르게 찾을 수 있음.

2. 파티셔닝 (데이터를 여러 개로 분리)

- 문제: 데이터가 많아지면 한꺼번에 처리하는 게 부담이 될 수 있음.

- 해결 방법:

  • 데이터를 기간별(예: INSERT_TIME) 또는 유형별(예: TYPE)로 나누는 방법이 있음.
  • 예를 들어, 게시판 데이터를 "한 달 단위"로 나누면, 최근 데이터만 빠르게 조회할 수 있음.
  • 파일을 "2023년", "2024년" 폴더별로 정리하면 필요한 데이터를 쉽게 찾을 수 있는 것과 같은 원리임.

3. Materialized View (미리 계산된 데이터 저장)

- 문제: 같은 복잡한 쿼리를 여러 번 실행하면 성능 저하가 발생할 수 있음.

- 해결 방법:

  • 자주 조회되는 데이터를 미리 계산해서 저장하는 방법이 있음.
  • 예를 들어, "게시판에서 최신 100개의 글"을 계속 보여줘야 한다면, 매번 쿼리를 실행하는 대신 미리 저장해두고 꺼내 쓰면 조회 속도가 훨씬 빨라짐.
  • 즐겨찾기 기능과 비슷한 개념임.

4. 쿼리 실행 계획 분석 & 힌트 사용 (SQL 실행 방식 최적화)

- 문제: 데이터베이스가 쿼리를 비효율적으로 처리할 수도 있음.

- 해결 방법:

  • EXPLAIN PLAN을 사용하면 쿼리가 얼마나 효율적으로 실행되는지 확인 가능함.
  • 불필요한 전체 검색을 방지하고, 원하는 데이터만 빠르게 찾도록 유도할 수 있음.
  • 내비게이션을 사용해 가장 빠른 경로를 찾는 것과 같은 방식임.

5. 캐싱 (자주 쓰는 데이터 미리 저장)

- 문제: 같은 데이터를 계속 요청하면 DB에 부담이 커질 수 있음.

- 해결 방법:

  • 자주 조회하는 데이터는 서버나 메모리에 미리 저장해서 사용하는 방법이 있음.
  • 예를 들어, "인기 게시글 목록"을 10분마다 갱신하고, 그동안은 DB에 다시 요청하지 않도록 하면 부하를 줄일 수 있음.
  • Redis 같은 캐시 시스템을 활용하면 속도를 더 빠르게 만들 수 있음.
  • 브라우저의 "임시 저장" 기능과 비슷한 개념임.

6. 전송 데이터 최소화 (불필요한 데이터 줄이기)

- 문제: SELECT * 를 사용하면 모든 데이터를 가져와서 네트워크 비용이 커지고, 응답 속도가 느려질 수 있음.

- 해결 방법:

  • 진짜 필요한 컬럼만 선택해서 조회하도록 수정하면 속도를 개선할 수 있음.
  • 예를 들어, 게시판 리스트에서 TITLE, INSERT_TIME만 필요하면 다른 컬럼은 조회하지 않도록 하면 됨.
  • 필요 없는 데이터를 전송하지 않으면 응답 시간이 단축될 수 있음.

💡 정리

문제 방법 예시
검색이 느림 인덱스 추가 TYPE, ME_NO, TITLE에 인덱스 추가
데이터가 많음 파티셔닝 1년치 데이터를 월별로 분할
같은 쿼리를 계속 실행 미리 계산된 데이터 저장 최신 100개 글을 미리 저장
비효율적인 쿼리 실행 실행 계획 분석 & 힌트 사용 EXPLAIN PLAN으로 실행 방식 확인
DB 부하가 많음 캐싱 적용 인기 게시글 목록을 10분마다 갱신
데이터 전송 부담 필요한 컬럼만 SELECT SELECT title, insert_time FROM board