백엔드API
12

게시판 상세 화면에서 목록 순서 기준 이전글/다음글 구현하기

게시판 상세 화면에서 목록 순서 기준 이전글/다음글 구현하기

게시판 상세 화면에서 목록 순서 기준 이전글/다음글 구현하기

1. 들어가며

게시판 상세 화면을 구현하다 보면 현재 글의 상세 내용만 보여주는 것이 아니라, 하단에 이전글 / 다음글 이동 기능을 함께 제공해야 하는 경우가 많다.

처음에는 단순하게 현재 게시글 번호를 기준으로 이전글과 다음글을 찾으면 된다고 생각했다.

예를 들어 현재 게시글 번호가 10이면:

  • 이전글: 9
  • 다음글: 11

처럼 처리하면 될 것 같았다.

하지만 실제 게시판에서는 이렇게 단순하게 처리하면 문제가 생길 수 있다.

게시글 목록은 단순히 게시글 번호 순서대로만 조회되지 않기 때문이다.

예를 들어 다음과 같은 조건들이 목록 조회 순서에 영향을 줄 수 있다.

  • 공지글 우선 정렬
  • 공개 여부
  • 검색 조건
  • 작성일 정렬
  • 사용자 권한별 조회 조건
  • 특정 게시글 상단 고정
  • 정렬 기준 변경

그래서 이전글/다음글은 단순히 게시글 번호 기준이 아니라, 사용자가 보고 있던 목록의 조회 순서 기준으로 판단해야 했다.


2. 처음 생각했던 방식: MySQL LAG / LEAD 사용

처음에는 MySQL의 LAG, LEAD 함수를 사용해서 이전글과 다음글을 조회하려고 했다.

LAG, LEAD는 윈도우 함수라고 부르며, 특정 정렬 기준 안에서 현재 행을 기준으로 앞뒤 데이터를 가져올 수 있다.

2-1. LAG란?

LAG()는 현재 행을 기준으로 이전 행의 데이터를 가져올 때 사용한다.

LAG(컬럼명) OVER (ORDER BY 정렬기준)

예를 들어 게시글 목록이 작성일 내림차순으로 정렬되어 있다면:

SELECT
    POST_ID,
    TITLE,
    LAG(POST_ID) OVER (ORDER BY CREATED_AT DESC) AS PREV_POST_ID
FROM BOARD_POST;

이런 식으로 현재 행 기준 앞쪽 데이터를 가져올 수 있다.

2-2. LEAD란?

LEAD()는 현재 행을 기준으로 다음 행의 데이터를 가져올 때 사용한다.

LEAD(컬럼명) OVER (ORDER BY 정렬기준)

예를 들면:

SELECT
    POST_ID,
    TITLE,
    LEAD(POST_ID) OVER (ORDER BY CREATED_AT DESC) AS NEXT_POST_ID
FROM BOARD_POST;

이렇게 하면 현재 게시글 기준 다음 위치의 게시글 번호를 가져올 수 있다.


3. LAG / LEAD 방식의 장점

LAG, LEAD를 사용하면 DB에서 이전글/다음글을 바로 계산할 수 있다.

장점은 다음과 같다.

3-1. SQL 한 번으로 이전글/다음글을 구할 수 있다

게시글 목록 기준 정렬만 정확히 맞추면, DB에서 현재글 기준 이전글/다음글을 바로 조회할 수 있다.

SELECT *
FROM (
    SELECT
        POST_ID,
        TITLE,
        LAG(POST_ID) OVER (ORDER BY CREATED_AT DESC) AS PREV_POST_ID,
        LEAD(POST_ID) OVER (ORDER BY CREATED_AT DESC) AS NEXT_POST_ID
    FROM BOARD_POST
) A
WHERE A.POST_ID = #{postId};

3-2. Java에서 별도 반복문을 돌리지 않아도 된다

DB에서 이미 이전글/다음글을 계산해서 내려주기 때문에 Java에서는 결과만 받아서 화면에 전달하면 된다.

3-3. 데이터가 많은 경우 유리할 수 있다

전체 목록을 Java로 가져와서 직접 비교하지 않고 DB에서 계산하기 때문에, 조건이 잘 잡혀 있다면 효율적으로 처리할 수 있다.


4. 그런데 LAG / LEAD 방식에서 고민된 부분

처음에는 LAG, LEAD로 처리하려고 했지만, 구현 과정에서 몇 가지 고민이 생겼다.

4-1. 목록 조회 정렬 조건과 반드시 같아야 한다

이전글/다음글은 목록 화면에서 보던 순서 기준이어야 한다.

즉, 목록 조회 쿼리에서 사용하는 정렬 조건과 LAG, LEAD에서 사용하는 정렬 조건이 반드시 같아야 한다.

예를 들어 목록이 아래처럼 정렬된다면:

ORDER BY
    CASE WHEN NOTICE_YN = 'Y' THEN 0 ELSE 1 END,
    CREATED_AT DESC

LAG, LEAD 쿼리에서도 이 정렬 기준을 그대로 맞춰야 한다.

그렇지 않으면 사용자가 목록에서 보던 순서와 상세 화면의 이전글/다음글 순서가 달라진다.

4-2. CASE로 만든 정렬 컬럼을 바로 쓰면 오류가 날 수 있다

목록 조회에서 정렬용 컬럼을 CASE WHEN으로 만들고, 그 별칭을 바로 LAG, LEAD 안에서 사용하려고 하면 오류가 날 수 있다.

예를 들어 이런 식이다.

SELECT
    POST_ID,
    TITLE,
    CASE WHEN NOTICE_YN = 'Y' THEN 0 ELSE 1 END AS SORT_ORDER,
    LAG(POST_ID) OVER (ORDER BY SORT_ORDER, CREATED_AT DESC) AS PREV_POST_ID
FROM BOARD_POST;

이 경우 DB에서는 SORT_ORDER를 아직 인식하지 못해서 Unknown column 오류가 발생할 수 있다.

이럴 때는 서브쿼리나 CTE를 사용해서 먼저 정렬용 컬럼을 만든 뒤, 바깥 쿼리에서 LAG, LEAD를 적용해야 한다.

WITH POST_LIST AS (
    SELECT
        POST_ID,
        TITLE,
        CASE WHEN NOTICE_YN = 'Y' THEN 0 ELSE 1 END AS SORT_ORDER,
        CREATED_AT
    FROM BOARD_POST
)
SELECT
    POST_ID,
    TITLE,
    LAG(POST_ID) OVER (ORDER BY SORT_ORDER, CREATED_AT DESC) AS PREV_POST_ID,
    LEAD(POST_ID) OVER (ORDER BY SORT_ORDER, CREATED_AT DESC) AS NEXT_POST_ID
FROM POST_LIST;

4-3. 기존 목록 조회 쿼리와 중복이 생길 수 있다

이전글/다음글을 위해 별도의 쿼리를 만들면, 기존 목록 조회 쿼리와 비슷한 조건을 또 작성해야 한다.

예를 들어 목록 조회에 이런 조건들이 있다면:

  • 검색어
  • 공개 여부
  • 공지 여부
  • 사용자 권한
  • 정렬 조건

이 조건들을 이전글/다음글 조회 쿼리에도 동일하게 반영해야 한다.

그런데 나중에 목록 조회 조건이 바뀌면, 이전글/다음글 쿼리도 같이 수정해야 한다.

이 부분에서 유지보수 문제가 생길 수 있다고 판단했다.


5. 최종 선택: Java에서 목록 순서 기준으로 계산하기

결국 이번 구현에서는 DB의 LAG, LEAD를 사용하지 않고, Java에서 목록 데이터를 기준으로 이전글/다음글을 계산하는 방식으로 변경했다.

흐름은 다음과 같다.

1. 현재 게시글 번호를 받는다.
2. 목록 조회 기준과 동일한 조건으로 게시글 목록을 조회한다.
3. Java에서 현재 게시글의 위치를 찾는다.
4. 현재 위치 기준으로 앞/뒤 데이터를 구한다.
5. 이전글/다음글 정보를 상세 화면에 전달한다.

6. Java에서 처리하는 방식의 장점

6-1. 목록 조회 결과를 그대로 활용할 수 있다

이 방식의 가장 큰 장점은 목록 조회 순서와 이전글/다음글 순서를 맞추기 쉽다는 점이다.

이미 목록 조회 쿼리에서 정렬이 적용된 상태로 데이터를 가져온다면, Java에서는 그 리스트의 순서만 기준으로 삼으면 된다.

List<Map<String, Object>> postList = boardMapper.selectPostList(param);

이 리스트는 이미 목록 화면에서 보여줄 순서대로 정렬되어 있다고 볼 수 있다.

따라서 이전글/다음글은 이 리스트 안에서 현재 게시글의 앞뒤 요소를 찾으면 된다.

6-2. SQL 복잡도를 줄일 수 있다

LAG, LEAD, CTE, CASE WHEN, 정렬 별칭 문제 등을 SQL에서 처리하지 않아도 된다.

SQL은 목록 조회에 집중하고, 이전글/다음글 판단은 Java에서 처리한다.

이렇게 하면 쿼리가 지나치게 복잡해지는 것을 줄일 수 있다.

6-3. 디버깅이 쉽다

Java에서 처리하면 중간중간 로그를 찍어서 현재 상태를 확인하기 쉽다.

예를 들어:

logger.info("currentIndex: " + currentIndex);
logger.info("prevInfo: " + prevInfo);
logger.info("nextInfo: " + nextInfo);

이런 식으로 현재 게시글이 몇 번째 위치인지, 이전글/다음글이 어떤 데이터인지 바로 확인할 수 있다.

6-4. 조건이 복잡한 화면에서 적용하기 쉽다

목록 조회 조건이 복잡할수록 LAG, LEAD 쿼리도 복잡해질 수 있다.

반면 Java 방식은 목록 조회 결과만 정확하다면, 이후 로직은 단순하다.

특히 기존에 이미 목록 조회용 Mapper가 있다면 재사용하기 쉽다.


7. 구현 방식 정리

이번 구현에서는 다음과 같은 구조로 처리했다.

7-1. 현재 게시글 번호 받기

상세 화면 진입 시 현재 게시글 번호를 파라미터로 받는다.

String postId = (String) param.getOrDefault("postId", "");

여기서 postId는 현재 상세 화면에서 조회할 게시글의 일련번호다.

7-2. 현재 게시글 상세 조회

먼저 현재 게시글의 상세 정보를 조회한다.

Map<String, Object> postInfo = boardMapper.selectPostDetail(param);

상세 화면에는 현재 게시글의 제목, 내용, 작성일, 조회수 등이 필요하다.

7-3. 목록 조회

이전글/다음글을 찾기 위해 목록 조회 기준과 동일한 조건으로 전체 목록을 가져온다.

List<Map<String, Object>> postList = boardMapper.selectPostList(param);

중요한 점은 이 목록이 화면 목록과 같은 정렬 기준을 가져야 한다는 것이다.


8. 현재 게시글 위치 찾기

목록에서 현재 게시글이 몇 번째 위치에 있는지 찾아야 한다.

int currentIndex = -1;

for (int i = 0; i < postList.size(); i++) {
    Map<String, Object> item = postList.get(i);

    if (postId.equals(String.valueOf(item.get("postId")))) {
        currentIndex = i;
        break;
    }
}

여기서 currentIndex는 현재 게시글이 목록에서 몇 번째 위치인지 나타낸다.

초기값을 -1로 둔 이유는, 현재 게시글을 찾지 못한 상태를 구분하기 위해서다.


9. break를 사용하는 이유

현재 게시글을 찾으면 더 이상 반복문을 돌 필요가 없다.

그래서 조건을 만족하는 순간 break를 사용해서 반복문을 종료한다.

if (postId.equals(String.valueOf(item.get("postId")))) {
    currentIndex = i;
    break;
}

예를 들어 전체 게시글이 20개이고, 현재 게시글이 10번째에서 발견되면 반복문은 10번만 돌고 종료된다.

이렇게 하면 불필요하게 나머지 데이터를 계속 비교하지 않아도 된다.


10. 이전글/다음글 계산하기

현재 게시글 위치를 찾았다면, 이제 앞뒤 데이터를 가져오면 된다.

Map<String, Object> prevInfo = new HashMap<>();
Map<String, Object> nextInfo = new HashMap<>();

if (currentIndex != -1) {
    if (currentIndex > 0) {
        nextInfo = postList.get(currentIndex - 1);
    }

    if (currentIndex < postList.size() - 1) {
        prevInfo = postList.get(currentIndex + 1);
    }
}

여기서 중요한 점은 목록 정렬 방향이다.

보통 게시판은 최신글이 위에 오도록 정렬한다.

예를 들어 목록이 다음과 같다고 가정한다.

index 0: 최신글
index 1
index 2: 현재글
index 3
index 4: 오래된글

이 경우:

currentIndex - 1

은 화면에서 현재글보다 위쪽에 있는 글이다.

반대로:

currentIndex + 1

은 화면에서 현재글보다 아래쪽에 있는 글이다.

따라서 서비스나 화면에서 말하는 “이전글 / 다음글” 기준이 무엇인지 명확히 정해야 한다.


11. 인덱스 범위 체크가 필요한 이유

현재 게시글이 첫 번째 글이라면 currentIndex - 1은 존재하지 않는다.

if (currentIndex > 0) {
    nextInfo = postList.get(currentIndex - 1);
}

현재 게시글이 마지막 글이라면 currentIndex + 1은 존재하지 않는다.

if (currentIndex < postList.size() - 1) {
    prevInfo = postList.get(currentIndex + 1);
}

이 조건을 넣지 않으면 IndexOutOfBoundsException이 발생할 수 있다.


12. 최종 Java 코드 예시

public Map<String, Object> getPostDetail(Map<String, Object> param) {
    String postId = (String) param.getOrDefault("postId", "");

    // 현재 게시글 상세 조회
    Map<String, Object> postInfo = boardMapper.selectPostDetail(param);

    // 목록 조회 순서 기준으로 이전글/다음글을 찾기 위한 목록 조회
    List<Map<String, Object>> postList = boardMapper.selectPostList(param);

    Map<String, Object> prevInfo = new HashMap<>();
    Map<String, Object> nextInfo = new HashMap<>();

    int currentIndex = -1;

    // 현재 게시글 위치 찾기
    for (int i = 0; i < postList.size(); i++) {
        Map<String, Object> item = postList.get(i);

        if (postId.equals(String.valueOf(item.get("postId")))) {
            currentIndex = i;
            break;
        }
    }

    // 이전글/다음글 계산
    if (currentIndex != -1) {
        if (currentIndex > 0) {
            nextInfo = postList.get(currentIndex - 1);
        }

        if (currentIndex < postList.size() - 1) {
            prevInfo = postList.get(currentIndex + 1);
        }
    }

    postInfo.put("prevInfo", prevInfo);
    postInfo.put("nextInfo", nextInfo);

    return postInfo;
}

13. 화면에서 사용하는 데이터 구조

서버에서 상세 데이터와 이전글/다음글 데이터를 함께 내려준다.

예시는 다음과 같다.

{
  "postId": 10,
  "title": "현재 게시글 제목",
  "content": "현재 게시글 내용",
  "prevInfo": {
    "postId": 9,
    "title": "이전글 제목"
  },
  "nextInfo": {
    "postId": 11,
    "title": "다음글 제목"
  }
}

화면에서는 prevInfo, nextInfo가 비어 있는지 확인한 뒤 이전글/다음글 영역을 출력한다.


14. JSP / JavaScript 화면 처리

이전글 또는 다음글을 클릭하면 해당 게시글 번호를 가지고 상세 화면으로 다시 이동한다.

function moveDetail(postId) {
    location.href = "/board/detail.do?postId=" + postId;
}

템플릿 리터럴을 사용하면 다음과 같이 작성할 수도 있다.

function moveDetail(postId) {
    location.href = `/board/detail.do?postId=${postId}`;
}

단, 이때 postId 값이 undefined로 나오는 경우가 있다면 다음을 확인해야 한다.

  • 서버에서 내려주는 key 이름이 맞는지
  • JavaScript에서 접근하는 key 이름이 맞는지
  • 대소문자가 일치하는지
  • prevInfo, nextInfo가 빈 객체인지
  • 문자열 연결 시 따옴표나 백틱 사용이 올바른지

15. 구현하면서 주의했던 부분

15-1. 게시글 번호 타입 문제

화면에서 넘어온 postId는 문자열일 수 있다.

반면 DB에서 조회된 postId는 숫자 타입일 수 있다.

그래서 단순히 아래처럼 비교하면 실패할 수 있다.

postId.equals(item.get("postId"))

안전하게 비교하려면 String.valueOf()를 사용하는 것이 좋다.

postId.equals(String.valueOf(item.get("postId")))

15-2. 이전글/다음글 명칭 문제

목록이 최신순으로 정렬되어 있으면 currentIndex - 1이 화면상 위쪽 글이고, currentIndex + 1이 화면상 아래쪽 글이다.

하지만 서비스에서 말하는 이전글/다음글이 다음 중 무엇을 의미하는지에 따라 변수명이 달라질 수 있다.

  • 시간 기준 이전/다음
  • 화면 위치 기준 이전/다음
  • 게시글 번호 기준 이전/다음

이 부분은 헷갈리기 쉽기 때문에 변수명이나 주석을 명확히 작성해야 한다.

예를 들어 화면 위치 기준이라면 다음처럼 이름을 잡을 수도 있다.

Map<String, Object> upperInfo = new HashMap<>();
Map<String, Object> lowerInfo = new HashMap<>();

15-3. 목록 조회 조건 재사용

이전글/다음글은 반드시 목록 조회 순서와 같아야 한다.

따라서 상세 화면에서 이전글/다음글을 계산할 때도 목록 조회와 동일한 조건을 사용해야 한다.

검색 조건이나 정렬 조건이 빠지면 사용자가 목록에서 보던 순서와 상세 화면에서 이동되는 순서가 달라질 수 있다.


16. LAG / LEAD 방식과 Java 방식 비교

구분LAG / LEAD 방식Java 목록 기준 방식
처리 위치DBJava 서버
장점SQL에서 한 번에 계산 가능목록 조회 결과를 그대로 활용 가능
단점SQL이 복잡해질 수 있음목록 데이터를 가져와야 함
정렬 조건 관리쿼리 안에서 직접 맞춰야 함목록 조회 결과 기준으로 처리
디버깅SQL 중심Java 로그로 확인 쉬움
적합한 경우정렬 조건이 명확하고 쿼리 관리가 쉬울 때기존 목록 조회 로직을 재사용하고 싶을 때

17. 이번 구현에서 Java 방식을 선택한 이유

이번에는 Java에서 목록 기준으로 이전글/다음글을 계산하는 방식을 선택했다.

이유는 다음과 같다.

  1. 기존 목록 조회 쿼리를 그대로 활용할 수 있었다.
  2. 목록 정렬 기준과 이전글/다음글 기준을 맞추기 쉬웠다.
  3. LAG, LEAD를 위한 별도 쿼리를 복잡하게 만들 필요가 없었다.
  4. Java 로그로 현재 위치와 이전글/다음글 데이터를 확인하기 쉬웠다.
  5. 현재 프로젝트 구조상 Service 단에서 처리하는 것이 더 빠르게 적용 가능했다.

18. 보안 관점에서 예제 코드 작성 시 주의할 점

회사 프로젝트를 기준으로 블로그 글을 작성할 때는 실제 업무 정보를 그대로 노출하지 않도록 주의해야 한다.

특히 다음 정보들은 일반적인 예제명으로 바꿔 작성하는 것이 좋다.

  • 실제 DB명
  • 실제 테이블명
  • 실제 컬럼명
  • 실제 업무 화면명
  • 실제 Mapper명
  • 실제 Service명
  • 실제 Controller URL
  • 실제 패키지명
  • 사내 코드 체계
  • 운영 데이터 예시
  • 계정, 사용자 ID, 사번 등 식별 가능한 값

예를 들어 실제 프로젝트의 테이블명을 그대로 쓰기보다는 아래처럼 일반화할 수 있다.

실제 코드에서 나올 수 있는 정보블로그 예제에서 사용할 이름
업무 전용 테이블명BOARD_POST
업무 전용 게시글 번호 컬럼POST_ID
업무 전용 작성일 컬럼CREATED_AT
업무 전용 Mapper명boardMapper
업무 전용 상세 조회 메서드selectPostDetail
업무 전용 목록 조회 메서드selectPostList
업무 전용 URL/board/detail.do

블로그에서는 핵심 로직을 설명하는 것이 목적이므로, 실제 회사 코드와 동일한 이름을 사용할 필요는 없다.


19. 마무리

이번 구현을 통해 이전글/다음글 기능이 단순히 게시글 번호 앞뒤를 찾는 기능이 아니라는 것을 알게 되었다.

게시판에서 중요한 것은 사용자가 보고 있는 목록의 순서다.

따라서 이전글/다음글도 단순 일련번호 기준이 아니라, 목록 조회 조건과 정렬 기준을 그대로 반영해야 한다.

처음에는 MySQL의 LAG, LEAD를 사용하려고 했지만, 정렬 조건 관리와 쿼리 복잡도를 고려해서 Java에서 목록 기준으로 계산하는 방식으로 변경했다.

이 방식은 SQL을 단순하게 유지하면서도, 목록 조회 순서 기준으로 이전글/다음글을 자연스럽게 처리할 수 있다는 장점이 있었다.


20. 다음에 개선해볼 점

현재 방식은 목록 데이터를 Java로 가져온 뒤 현재 게시글의 위치를 찾는 구조다.

데이터가 많아지거나 검색 조건이 복잡해진다면 다음과 같은 개선도 고려할 수 있다.

  • LAG, LEAD를 사용한 DB 처리 방식으로 변경
  • 이전글/다음글 전용 Mapper 분리
  • 목록 조회 조건 공통화
  • 검색 조건을 상세 화면 이동 시에도 유지
  • 이전글/다음글 제목과 게시글 번호만 조회하도록 최적화
  • 화면 기준 이전/다음 명칭을 더 명확하게 정리

결론적으로 이전글/다음글 기능은 구현 자체보다 기준을 명확히 정하는 것이 더 중요했다.

댓글

(0)
게시판 상세 화면에서 목록 순서 기준 이전글/다음글 구현하기 | 강민석의 개발블로그