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 |
'개발인생 > Project' 카테고리의 다른 글
| 페이징처리 프로젝트 구현(React) (0) | 2025.03.17 |
|---|---|
| 페이징처리 - Pagination.js + Pageable (0) | 2025.03.17 |
| Spring Security 설정 완벽 분석 - 보안 설정 가이드 (2) | 2025.03.05 |
| 프로젝트 코드 리뷰: JPQL vs QueryDSL 최적화 및 리팩토링 (0) | 2025.02.21 |
| JSP/Servlet 프로젝트와 React+Spring Boot 프로젝트 비교 (1) | 2025.02.19 |