Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : 조회수 로직 추가 #239

Merged
merged 1 commit into from
Jan 3, 2025
Merged

feat : 조회수 로직 추가 #239

merged 1 commit into from
Jan 3, 2025

Conversation

urinaner
Copy link
Owner

@urinaner urinaner commented Jan 3, 2025

  • 💯 테스트는 잘 통과했나요?
  • 🏗️ 빌드는 성공했나요?
  • 🧹 불필요한 코드는 제거했나요?
  • 💭 이슈는 등록했나요?
  • 🏷️ 라벨은 등록했나요?

📘 게시판 조회수 증가 로직

🛠️ 구현 기능

1. 조회수 증가 로직

  • 동일 사용자가 동일 게시글을 반복 조회해도 조회수가 증가하지 않도록 쿠키를 활용한 중복 체크를 구현.
  • 동시성 문제를 방지하기 위해 데이터베이스 레벨에서 조회수 증가를 처리.

2. 동시성 문제 해결

  • 조회수 증가 로직에서 발생할 수 있는 동시성 문제를 해결하기 위해 데이터베이스 직접 업데이트 방식을 사용.
  • 이를 통해 정확한 조회수가 반영되도록 보장.

📋 조회수 증가 로직: 세션 vs 쿠키


🛠️ 조회수 증가 로직 구현 방식

1. 세션을 사용한 조회수 증가 로직

구현 방식

  • 서버에서 사용자의 세션에 조회한 게시글 ID를 저장하여 중복 조회를 방지.
  • 예: session.setAttribute("viewedPosts", Set<Long>);

장점

  • 사용자의 상태를 서버에서 관리하므로 높은 신뢰성을 보장.

단점

  • 서버 과부하: 많은 사용자가 접속하면 세션 데이터를 관리하기 위한 서버 메모리 사용량이 증가.
  • 스케일링 어려움: 서버가 분산 환경(클러스터링)에서 동작할 경우, 세션 동기화나 중앙 관리가 필요.

2. 쿠키를 사용한 조회수 증가 로직

구현 방식

  • 사용자 브라우저에 쿠키를 저장하여 이미 조회한 게시글을 확인합니다.
  • 예: Cookie("postView", "[1]_[2]_[3]")

장점

  • 서버 과부하 방지: 조회 상태를 클라이언트(브라우저)에서 관리하므로 서버 메모리를 사용하지 않습니다.

단점

  • 브라우저 의존적: 사용자가 쿠키를 차단하거나 삭제하면 동작이 제한됩니다.
  • 데이터 크기 제한: 브라우저에서 허용하는 쿠키 크기 제한(약 4KB)이 존재합니다.

🛡️ 왜 쿠키를 선택했는가?

  1. 서버 과부하 방지

    • 세션 기반 로직은 서버 메모리를 점유하기 때문에 많은 사용자가 접속하는 환경에서는 서버 과부하가 발생 가능.
    • 쿠키 기반 로직은 클라이언트에서 상태를 관리하므로 서버 부담이 감소.
  2. 개인정보 보호

    • 쿠키에는 게시글 ID와 같은 비식별 정보만 저장됩니다.
    • 사용자와 관련된 민감한 정보를 저장하지 않으므로 개인정보 유출 위험이 없습니다.
  3. 분산 서버 환경 지원

    • 쿠키는 클라이언트에 저장되므로 분산 서버 환경에서도 추가 설정 없이 동일하게 동작합니다.
    • 반면, 세션 기반 로직은 분산 환경에서 세션 동기화가 필요하여 복잡도가 증가합니다.

📂 코드 구성

1. Controller

BoardController는 클라이언트 요청을 처리하며, 상세 조회 시 조회수 증가 로직을 호출합니다.

@RestController
@RequestMapping("/api/boards")
public class BoardController {

    private final BoardService boardService;

    public BoardController(BoardService boardService) {
        this.boardService = boardService;
    }

    @GetMapping("/{boardId}")
    public ResponseEntity<BoardResDto> getBoard(@PathVariable Long boardId, HttpServletRequest request, HttpServletResponse response) {
        boardService.readCount(boardId, request, response); // 조회수 증가
        BoardResDto boardResDto = boardService.getBoard(boardId); // 게시글 상세 정보 반환
        return ResponseEntity.ok(boardResDto);
    }
}

2. Service

BoardService는 비즈니스 로직을 처리하며, 조회수 증가 로직과 상세 조회 로직을 제공.

@Service
public class BoardService {

    private final BoardRepository boardRepository;

    public BoardService(BoardRepository boardRepository) {
        this.boardRepository = boardRepository;
    }

    @Transactional
    public void readCount(Long boardId, HttpServletRequest request, HttpServletResponse response) {
        // 조회수 중복 체크 로직
        Cookie oldCookie = null;
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals("postView")) {
                    oldCookie = cookie;
                }
            }
        }

        if (oldCookie != null) {
            if (!oldCookie.getValue().contains("[" + boardId + "]")) {
                boardRepository.incrementViewCount(boardId); // 조회수 증가
                oldCookie.setValue(oldCookie.getValue() + "_[" + boardId + "]");
                oldCookie.setPath("/");
                oldCookie.setMaxAge(60 * 60 * 24);
                response.addCookie(oldCookie);
            }
        } else {
            boardRepository.incrementViewCount(boardId); // 조회수 증가
            Cookie newCookie = new Cookie("postView", "[" + boardId + "]");
            newCookie.setPath("/");
            newCookie.setMaxAge(60 * 60 * 24);
            response.addCookie(newCookie);
        }
    }

    public BoardResDto getBoard(Long boardId) {
        Board board = boardRepository.findById(boardId)
                .orElseThrow(() -> new IllegalArgumentException("게시글을 찾을 수 없습니다."));
        return new BoardResDto(board);
    }
}

3. Repository

BoardRepository는 데이터베이스와의 인터페이스로, 조회수 증가를 처리하는 쿼리를 제공.

public interface BoardRepository extends JpaRepository<Board, Long> {
    @Modifying
    @Query("UPDATE Board b SET b.viewCount = b.viewCount + 1 WHERE b.id = :boardId")
    void incrementViewCount(@Param("boardId") Long boardId);
}

4. Entity

Board 엔티티는 게시글 데이터를 표현하며, 조회수 필드를 포함.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "board")
public class Board extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "board_id", nullable = false)
    private Long id;

    @Column(name = "title")
    private String title;

    @Column(name = "content")
    private String content;

    @Column(name = "view_count")
    private int viewCount;

    @Column(name = "writer")
    private String writer;

    @Builder
    private Board(String title, String content, String writer) {
        this.title = title;
        this.content = content;
        this.writer = writer;
        this.viewCount = 0;
    }
}

🛡️ 동시성 문제 해결

선택한 방법: 데이터베이스 직접 업데이트

  • 쿼리: UPDATE Board b SET b.viewCount = b.viewCount + 1 WHERE b.id = :boardId
  • 이유: 데이터베이스에서 직접 처리하므로 동시성 문제를 효과적으로 방지 가능.

📝 참고 사항

  • 조회수 중복 방지는 쿠키를 기반으로 동작하며, 쿠키의 유효 기간은 24시간으로 설정되어 있습니다.
  • 동시성 문제가 발생하지 않도록 데이터베이스에서 직접 조회수를 증가시키는 방식으로 구현.

Closes #232

@urinaner urinaner added enhancement New feature or request 😄 BE labels Jan 3, 2025
@urinaner urinaner requested a review from 2Jin1031 January 3, 2025 09:24
@urinaner urinaner self-assigned this Jan 3, 2025
@urinaner urinaner merged commit 0d8df52 into main Jan 3, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
😄 BE enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[BE] 조회수 기능 구현
1 participant