Spring boot로 CRUD 게시판 만들기 (2)

양현민·2025년 3월 24일
2

📌 지난 내용 요약
1편에서는 CRUD 기능을 중심으로 게시글을 작성하고, 수정하고, 삭제하는 기본 게시판을 만들어봤다.
이번 편에서는 아래 기능들을 추가로 구현하면서 실제 서비스처럼 다듬어 보기 위한 작업을 진행했다.

검색 기능
페이징 처리
파일 업로드 기능
⚠️ 그리고 구현하면서 혼동 되었던 URL 매핑 방식도 정리해보았다.


🔎 1. 게시글 검색 기능

✔️ 목적
사용자가 입력한 키워드(예: 제목)에 해당하는 게시글만 보여주도록 만들기

📁 BoardController.java

@GetMapping("/board/list")
public String boardList(Model model, @RequestParam(required = false) String searchKeyword) {
    List<Board> list;

    if (searchKeyword == null) {
        // 검색어 없으면 전체 목록
        list = boardService.boardList();
    } else {
        // 검색어 있으면 해당 키워드 포함 게시글만
        list = boardService.boardSearchList(searchKeyword);
    }

    model.addAttribute("list", list);
    return "boardlist";
}
  • @RequestParam 으로 쿼리 파라미터 받음
  • null 체크로 검색어 여부에 따라 로직 분기
    1. searchKeyword가 null인 경우 -> 전체 게시글
    2. searchKeyword가 null이 아닌 경우 -> 해당 키워드가 포함된 게시글

📁 BoardService.java

public List<Board> boardSearchList(String searchKeyword) {
    return boardRepository.findByTitleContaining(searchKeyword);
}

📁 BoardRepository.java

List<Board> findByTitleContaining(String title);
  • Containing 은 SQL에서 LIKE '%keyword%'와 같은 의미로, 제목 안에 검색어가 포함된 글을 자동으로 찾아준다.

📄 2. 페이징 처리

✔️ 목적
게시글이 많아졌을 때, 한 번에 모두 출력하지 않고 페이지 단위로 나누기

📁 BoardController.java

@GetMapping("/board/list")
public String boardList(Model model,
    @PageableDefault(page = 0, size = 10, sort = "id", direction = Sort.Direction.DESC)
    Pageable pageable) {

    Page<Board> list = boardService.boardList(pageable);

    // 현재 페이지, 시작/끝 페이지 계산 (페이지 버튼 만들 때 사용)
    int nowPage = list.getPageable().getPageNumber() + 1;
    int startPage = Math.max(nowPage - 4, 1);
    int endPage = Math.min(nowPage + 5, list.getTotalPages());

    model.addAttribute("list", list);
    model.addAttribute("nowPage", nowPage);
    model.addAttribute("startPage", startPage);
    model.addAttribute("endPage", endPage);

    return "boardlist";
}

📁 BoardService.java

public Page<Board> boardList(Pageable pageable) {
    return boardRepository.findAll(pageable);
}

📁 BoardService.java

Page<Board> findAll(Pageable pageable);

Pageable 은 Spring에서 페이징을 쉽게 도와주는 인터페이스


🗂️ 3. 파일 업로드 기능

📁 BoardService.java

public void write(Board board, MultipartFile file) throws Exception {
    // 저장 경로: 프로젝트 내부 static/files 폴더
    String projectPath = System.getProperty("user.dir") + "/src/main/resources/static/files";

    // UUID로 파일명 중복 방지
    UUID uuid = UUID.randomUUID();
    String fileName = uuid + "_" + file.getOriginalFilename();

    // 실제 경로에 파일 저장
    File saveFile = new File(projectPath, fileName);
    file.transferTo(saveFile);

    // DB에 저장할 파일 정보 설정
    board.setFilename(fileName);
    board.setFilepath("/files/" + fileName);

    boardRepository.save(board);
}
  • 실제 파일은 static/files에 저장되고, 그 경로를 DB에 저장해서 이미지로 보여준다.

⚠️ 4. 오류 해결 과정

게시글 상세보기 기능을 만들던 중, @RequestParam 으로 id를 받아오는 과정에서 오류가 났다.
오류 메세지는 아래와 같았다.

Required request parameter 'id' for method parameter type Long is not presen

다음과 같이 컨트롤러를 작성했다.

📁 java

@GetMapping("/board/view")
public String boardView(@RequestParam("id") Integer id, Model model) {
    model.addAttribute("board", boardService.boardView(id));
    return "boardview";
}

📁 html

<a th:href="@{/board/view(id=${board.id})}">상세보기</a>

-> /board/view?id=1 형식으로 호출되게 했다.


똑같이 썼음에도 계속 오류가 나서 @PathVariable 로 바꿨더니 오류가 해결 되었다.

📁 java

@GetMapping("/board/view")
public String boardView(@PathVariable("id") Integer id, Model model) {
    model.addAttribute("board", boardService.boardView(id));
    return "boardview";
}

📁 html

<a th:href="@{'/board/view/' + ${board.id}}">상세보기</a>

-> /board/view/1 형식으로 바꾸니깐 해결 되었다.

📌 정리

구조URL 예시Annotation
쿼리스트링 방식/board/view?id=1@RequestParam("id")
경로 변수 방식/board/view/1@PathVariable("id")

🌟5. 마무리

결국 상세 페이지를 구현하는 과정에서, 처음엔 @RequestParm을 사용해서 값을 받아오려 했지만 오류가 났었고, 구조가 잘못된 줄 알고 @PathVariable로 바꿔서 해결했었다.

그런데 나중에 보니 @RequestParam도 맞는 방식이었고, 내가 놓친 건 특정 상황에서 파라미터가 실제로 안 넘어왔던 부분이었다.
두 방식 모두 사용 가능하지만, 그 과정에서 다른 방법을 시도해본 것도 도움이 되었던 것 같다.

단지 강의를 보며 따라치는 프로젝트였지만, 생각보다 오류도 많이 나고 쉽지만은 않았지만, 그만큼 잘 작동했을 때는 뿌듯함이 컸다. 끗!

2개의 댓글

comment-user-thumbnail
2025년 3월 24일

애노테이션 마다 요구하는게 달라서 저도 애를 많이 먹었던 것 같아요 !! 정말 헷갈려요 … 그래도 잘 해결하셔서 다행입니다 ! 덕분에 저도 RequestParam과 PathVariable 둘 다 사용할 수 있다는 것을 알 수 있었던 것 같습니다! 고생 많으셨습니다

답글 달기
comment-user-thumbnail
2025년 3월 24일

파일 업로드 로컬에 하기엔 부담되니까 aws S3연동해서 꼭 해보시면 좋을 것 같아요!

답글 달기