Spring/개인 공부

2. CRUD 게시판 프로젝트 - 글 목록부터 글 상세보기까지.

코딩하는냥이 2025. 7. 16. 22:01
반응형

저번 글에서는 게시판에서 글을 작성하는 것까지 진행했었습니다.

이번 글에서는 Read를 맡고 있는 R에 대해서 제작해보려고 합니다.


1. 게시판 목록을 띄워보자!

먼저 시작하기 전 필요한 파일을 세팅한 장면

 

일단 저는 게시판 목록을 만들기 전, 게시판 목록을 표시할 board_list.html을 먼저 만들어뒀습니다.

 

BoardController.java

@Controller
@RequiredArgsConstructor
@RequestMapping("/board")
public class BoardController {

    // 이전 내용 생략하고
    // 아래 내용을 추가
    @GetMapping("/list")
    public String getListBoard() {
        return "board_list";
    }
}

 

먼저 @GetMapping("/list") 를 만들어, 매핑이 제대로 되었는지 확인할 것입니다.

그럼 서버를 켜고, 해당 주소인 127.0.0.1:8080/board/list 로 접속해보겠습니다.

 

127.0.0.1:8080/board/list

 

무척이나 잘 나오는 모습!

하지만 저희가 이 곳을 들어올 때, 계속 주소를 전부 치고 들어오기도 힘들어서

지금이라도 바로 들어올 다이렉트 주소를 만들어주고자 합니다.

 

다이렉트 주소가 뭔가요?

그냥 제가 다이렉트로 들어오고 싶어서 다이렉트 주소라고 했는데요.

 

MainController 생성 위치

 

MainController 클래스를 만드는 모습

 

package com.mysite.sbb;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MainController {
	@GetMapping("/")
	public String me() {
		return "redirect:/board/list";
	}
}

 

만들면서 이름으로 쓸 것은 상황에 맞게 만드는 편인데,

main 적으려다가 나중에 오류가 생길 것 같아 급한대로 me 라고 메서드를 생성해두었습니다.

 

이 컨트롤러를 만들어 줌으로서, 저희는 127.0.0.1:8080만 쳐도 바로 /board/list 가 뒤에 생기는 것을 볼 수 있습니다.

그러면 이제 board_list.html 보내줄 게시글 목록을 위해

다시 BoardController에게 정보를 보내줄

 

BoardService로 가보겠습니다!!

 

저희는 Repository → Service → Controller → html 순으로 출력 정보를 전송하기 때문에,

Repository는 DB에서 정보를 받는 것을 따로 수정하고 싶을 때 혹은 간단하게 미리 받고싶은 정보로 바꿀 때 빼고는

Service에서 대부분의 기능을 먼저 만들어주고 Controller에서 정교하게 처리해줄 것입니다.

 

그럼 BoardService로 가볼까요?

 

BoardService.java

package com.mysite.sbb.board;

import java.time.LocalDateTime;

// Page 관련해서 import를 할 때에는 잘 보고 import 할 것.
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class BoardService {
    private final BoardRepository boardRepository;
	
    // 이전 내용 생략
    
    // 새로 추가한 내용
    public Page<Board> getListBoard(int page) {
        // 1. Pageable이라는 객체를 만든다.
        Pageable pageable = PageRequest.of(page, 10);
        
        // 2. boardRepository에서 findAll(pageble)로 게시글을 꺼낸다.
        // PageRequest.of(page, 10) 이라고 설정했기 때문에
        //    -> 한 페이지에 10개씩만!
        return this.boardRepository.findAll(pageable);
    }
}

 

getListBoard(int page)

메소드를 가져올 때, 페이지를 적습니다.

몇 번째 페이지를 볼 것인지 결정하는 숫자입니다.

( 예 : page가 0이면 1페이지, 1이면 2페이지..)

 

PageRequest.of(page, 10)

한 페이지에 10개씩, 원하는 페이지 번호(page)에 해당하는 데이터만 가져와 달라는 뜻입니다.

 

findAll(pageable)

전체 게시글 중에서 한 페이지에 해당하는 게시글 10개만 꺼내줍니다.

 

그리고 저희는 Service에서 준비를 끝냈기 때문에,

이제 Controller로 가보겠습니다.

 

BoardController.java

package com.mysite.sbb.board;

import org.springframework.data.domain.Page;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
@RequiredArgsConstructor
@RequestMapping("/board")
public class BoardController {
    private final BoardService boardService;

    @GetMapping("/create")
    public String createBoard() {
        return "board_form";
    }

    @PostMapping("/create")
	public String createBoard(@RequestParam("subject") String subject, @RequestParam("content") String contet) {
        this.boardService.createBoard(subject, contet);
        return "redirect:/board/create";
    }
    
    // 수정된 메서드
    @GetMapping("/list")
    public String getListBoard(Model model, @RequestParam(value = "page", defaultValue = "0") int page) {
        // 1. page 번호를 파라미터로 받음 (없으면 기본값 0)
        Page<Board> paging = this.boardService.getListBoard(page);

        // 2. 모델(model)에 'paging'이란 이름으로 결과를 담는다
        model.addAttribute("paging", paging);

        // 3. board_list라는 이름의 HTML 파일(템플릿)을 보여줌
        return "board_list";
    }
}

 

@RequestParam(value = "page", defaultValue = "0") int page

주소 뒤에 ?page=숫자 가 오면 그 값을 받고, 안 오면 자동으로 0(첫 페이지)로 처리합니다.

 

Page<Board> paging = this.boardService.getList(page)

BoardService에서 원하는 페이지의 글 목록을 10개씩 꺼내옵니다.

전체가 아닌, 요청한 한 페이지에 해당하는 글만 꺼내옵니다.

Page<Board>는 글 목록뿐만 아니라 전체 글 수, 페이지 수, 현재 페이지 등 다양한 정보를 담고 있습니다.

 

model.addAttribute("paging", paging);

HTML에서 사용할 수 있게, 글 목록과 페이징 정보를 모델에 담아 전달합니다.

그냥 간단하게 HTML에서 사용할 수 있게,

"받아 내 정보!!!! paging가 너에게 닿기를!!!!!!"

 

정보 던져주는겁니다.

 

return "board_list";

결과를 board_list.html로 연결해주는 겁니다.

아까 공백을 출력해보셔서 아시겠지만, 만약 오타가 있을 경우 경로에는 404 오류가 뜨게됩니다.

 

그럼 이러면 목록이 출력되는 건가요?

 

아쉽게도 이렇게 해도 저희는 아직 화면을 확인할 수 없습니다.

왜냐하면 저흰 아직 HTML을 수정해준 적이 없기 때문에,

아직 똑같은 흰 화면이 뜰 뿐입니다.

시무룩

 

그럼 저희는 이제 board_list.html로 이동합니다.

 

board_list.html

<table>
    <thead>
        <tr>
            <th>번호</th>
            <th>제목</th>
            <th>작성일자</th>
        </tr>
    </thead>
</table>

 

먼저 안에 있는 모든 내용을 없애버리고, 표의 틀을 간단하게 잡고 출력해봅니다.

 

달라진 127.0.0.1:8080/board/list

 

좀 초라하긴 하지만 잘 나오는 것을 확인할 수 있습니다.

그러면 이제 저희가 등록했던 내용을 출력해야겠죠?

 

board_list.html

<table>
    <thead>
        <tr>
            <th>번호</th>
            <th>제목</th>
            <th>작성일자</th>
        </tr>
    </thead>
    <!-- 추가되는 내용-->
    <tbody>
        <tr th:each="board : ${paging}">
            <td th:text="${board.id}"></td>
            <td th:text="${board.subject}"></td>
            <td th:text="${board.createDate}"></td>
        </tr>
    </tbody>
</table>

 

이제 저희가 먼저 만들어둔 열에 맞춰서 내용을 넣어줍니다.

 

th:each="board:${paging}"

저희가 다른 언어로 썼던 For문을 이용하여 배열을 출력하는 것입니다.

저흰 아까 스프링 프레임워크에서 제공하는 컬렉션인 Page를 사용해 정보를 보냈었습니다.

 

Controller에 있는 getBoardList에서 정보를

model.addAttribute("paging", paging)를 통해서 던졌던 것을 확인할 수 있는데요?

 

그 정보를 여기에서 받아서 쓴다고 보면 될 것 같습니다.

 

th:each="board:${paging}" 는 사실 for(Board board : paging) 와 같이 사용한 것이라고 볼 수 있습니다.

그럼 나머지 그 안에 있는 정보들은 무엇일까요?

 

board.id / board.subject / board.createDate

각각 게시물의 id와 제목, 생성일자입니다.

저희가 첫 게시물에서 DB에 테이블을 만들 때, 미리 만들어두었던 필드인데

혹시 기억나시나요?

 

저희는 그 정보를 가져와 그냥 보여주는 것 뿐입니다.

그럼 이제 정보도 가져왔겠다.

게시물의 제목을 누르면 상세보기 화면으로 이동되는 폼을 만들어줘야겠죠?

 

언제나 하던 작업대로 저는 먼저 매핑을 해줍니다.

 

BoardController.java

// Board Contrller에 추가되는 새로운 매핑 메서드
@GetMapping("/detail")
public String getBoard() {
    return "board_detail";
}

 

코드도 간단히 추가했겠다.

 

board_detail.html이 생겨난 모습

 

board_detail.html도 잘 만들었겠다.

그러면 주소로 이동해볼까요?

 

127.0.0.1:8080/board/detail

 

404 오류(페이지를 찾을 수 없음)가 뜨지 않고 빈 공백 화면이 잘 뜨는 모습!!

하지만 여기서 의문일겁니다..

 

"아니, 이제 게시판이라면서요? 게시물도 많아지고 할 텐데 그 게시물들을 다 어떻게 나타내죠?"

 

걱정하지마세요!

어차피 이 글을 읽고계시는 당신이 하실 일입니다.

걱정하지 말고 잘 따라오세요!

 

BoardService.java

// 새로 추가한 내용
public Board getBoard(Integer id) {
    Optional<Board> board = this.boardRepository.findById(id);
	
    if(!board.isPresent())
        ; // 오류 발생시 적을 클래스!!
    return board.get();
}

 

Board(게시글 하나)를 가져올 코드를 작성합니다.

하지만 여기서 저희가 만약 해당 id를 가진 게시글이 없을 때 작성할 것을 하나 대비해둬야 하는데요?

 

"왜 그런 일이 생기죠? 어차피 링크타고 들어오면 걱정 없는거 아닌가요?"

 

그런데 그 때, 링크를 넘어다니는 닌자가 나타났다...

아쉽게도 세상은 그렇게 호락호락하지 않습니다.

흔히 주소 창에서 id값을 직접 조작해서 게시물을 옮겨다니는 사람도 있기 마련이거든요.

 

그렇기 때문에 저희는 오류가 발생할 경우, 해당 오류를 처리해줄 클래스를 하나 만들어 줄 것입니다.

 

새로 생겨난 DataNotFoundException

 

저희는 새로운 클래스를 하나 만들어 줄 겁니다.

 

그 이름은 매우 무서운 "데이터를 찾을 수 없음 예외" 영어버전..!

 

DataNotFoundException.java

package com.mysite.sbb;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "객체를 찾을 수 없습니다.")
public class DataNotFoundException extends RuntimeException{
	private static final long seriaVersionUID = 1L;
	public DataNotFoundException(String message) {
		super(message);
	}
}

 

이 클래스가 무슨 역할을 하길래 따로 만들어주는 걸까요?

웹 사이트에서 찾으려는 데이터가 없을 때 에러를 알려주는 용도로 만드는 예외 클래스입니다.

 

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "객체를 찾을 수 없습니다.")

이 예외가 발생하면, 웹 서버는 404를 자동으로 응답하고 에러 메세지로 "객체를 찾을 수 없습니다." 를 표시합니다.

 

public class DataNotFoundException extends RuntimeException

예외의 일종인 DataNotFoundException 클래스를 만들면서,

RuntimeException을 상속받아, 특별한 예외 상황에서 사용하게 됩니다.

 

private static final long seriaVersionUID = 1L;

이 부분은 사실 저도 잘 모릅니다.

자바의 직렬화 기능을 위한 고유 번호라고 하는데, 없으면 오류날 때가 있어서 있어야 좋다고 합니다.

나도 모르니까 공부하지

 

super(message)

예외가 발생할 때, "찾을 수 없습니다" 같은 메세지를 전달받아서 부모 클래스(RunTimeException)에게 넘겨줍니다.

즉, 예외 발생 시 에러 메세지를 함께 기록할 수 있게 해준다고 볼 수 있습니다.

 

이렇게 완성한 클래스는 다시 Service로 가서 넣어주게 됩니다.

 

BoardService.java

if(!board.isPresent())
    throw new DataNotFoundException("해당 게시물을 찾을 수 없습니다."); // 오류 발생시 적을 클래스!!
return board.get();

 

아까 비워뒀던 자리! 찜 해뒀던 자리!

그 곳에 이 클래스를 당당하게 끼워줍니다.

이렇게 하면 404 Not Found 에러와 함께

화면에 "해당 게시물을 찾을 수 없습니다." 메세지가 표시되게 될 겁니다.

 

와, 정말 힘들었다!

하지만 이걸로 끝이 아니라는 것을 여러분은 아실겁니다.

 

Service를 했으면 Controller를..

Controller를 했으면 html을 해야 끝이라는 것을!!

 

바로 이동합니다.

 

BoardController.java

// 아까 만들어 두었던 getBoard 메서드를 수정!!
@GetMapping("/detail/{id}")
public String getBoard(Model model, @PathVariable("id") Integer id) {
    Board board = this.boardService.getBoard(id);
    model.addAttribute("board", board);
    return "board_detail";
}

 

Controller에서 할 것은 간단합니다.

제목에 맞는 게시글의 번호를 찾아서 게시글을 보내준다!

말만 하면 정말 쉬운거 같은데..

 

여기서 처음 보는 친구가 등장합니다.

 

@PathVariable("id")

아마 @GetMapping를 보시면 아시겠지만, datail 뒤에 새로운 친구{id}가 생긴 것을 볼 수 있습니다.

이것은 URL 경로 변수! 즉, 경로 변수라는 것입니다..!!

이제 여러분의 주소에 변수가 침범하기 시작한 겁니다.

 

/detail/5 라면 id = 5가,

/detail/10 이라면 id = 10이 되는 것입니다.

 

그럼 감이 슬슬 오실 것입니다.

이 id가 getBoard에서 받는 게시글 번호라는 것을!

 

그래서 해당 번호를 받은 변수를 this.boardService.getBoard(id) 안에 넣어서,

Board board 변수 안에 게시글을 넣어줍니다.

그리고 addAttribute를 이용해서 던져주기까지 했다면?

 

이제 HTML만 만져주면 끝이 아닐까요?

 

board_list.html

<table>
    <thead>
        <tr>
            <th>번호</th>
            <th>제목</th>
            <th>작성일자</th>
        </tr>
    </thead>
    <tbody>
        <tr th:each="board : ${paging}">
            <td th:text="${board.id}"></td>
            <!-- 기존에 있던 값이 달라진 것을 확인할 수 있는 타이틀 -->
            <td><a th:text="${board.subject}" th:href="@{|/board/detail/${board.id}|}"></a></td>
            <td th:text="${board.createDate}"></td>
        </tr>
    </tbody>
</table>

 

board_detail.html로 가기 전! 저희는 링크를 걸어줘야 합니다.

detail을 완성하고 나서, 막상 확인할 수단이 없어 저희가 주소를 직접 치고 들어갈 순 없잖아요?

먼저 하이퍼링크를 잘 걸어주고, 저장! 새로고침! 클릭!

 

127.0.0.1:8080/board/detail/1

 

잘 나오는 것 까지 확인!

이것까지 확인했다면 드디어! board_detail.html로 이동하면 됩니다.

 

board_detail.html

<h2 th:text="${board.subject}"></h2>
<div th:text="${board.content}"></div>

 

"헉, 모야모야. 단 두줄만 올려주면 어떻게해요??"

"이건 처음 만드는 건데 전체를 올려줘야죠?"

 

아뇨, 이 두 줄이 전체인데요?

 

이 두 줄만으로 제목과 내용이 나오는 것을 확인할 수 있습니다.

 

아하!

 

일단 이번 글은 이것으로 마치고, 저희가 게시판을 만듬에 있어 이제 UI가 필요하다는 것을 느끼실겁니다.

처음 사용하는 사람들은 주소를 직접 치면서 왔다갔다 할 수 없기 때문이죠.

 

다음 글에서는 bootstrap을 가져오는 방법을 확인하고, UI를 꾸미는 것을 해보겠습니다.

 

 

 

여담 / 요즘 떠오르는 UI, UX는 무엇인가요?

더보기

예전에 한국에서는 UX라는 내용이 크게 뜨지 않았습니다.

솔직히 어릴적했던 게임들을 보면 UX는 신경쓰지 않았던 것 같기도 하구요.

일단 잡담은 그만두고,

 

UI → User Interface 라는 단어의 줄임말입니다.

사용자 인터페이스는 사람이 컴퓨터의 시스템을 사용함에 있어서,

편하고 쉽게 사용할 수 있게 제작하는 것이 목표입니다.

 

저희가 이번에 폼을 만든 것을 보면

게시글 목록이 먼저 뜨고, 링크를 누르면 상세 보기로 넘어가는 것을 알 수 있습니다.

이것이 UI의 기본이라고 볼 수 있습니다.

 

그럼 기본이 되어있지 않은 UI는?

저희가 글을 쓰기 위해서 127.0.0.1:8080/board/create 를 주소에 직접 치고 들어가야 합니다.

이것이 단 한 개 뿐일 때에는, "별 거 아닌데?" 싶겠지만..

2개... 6개... 12개... 56개... 203개...

 

사용하는 기능이 늘어나면 늘어날수록 어릴적 전화기에 누르던 전화번호처럼

주소에 적어야 할 주소의 종류를 전부 기억해야하는 불상사가 생기게 됩니다.

그것을 해결하기 위한 것이 바로 UI!!!

 

간단하게 이해가 되었을까요?

 

그렇다면 UX는 무엇일까요?

 

UX → User eXperience 의 줄임말입니다.

넓게는 공학 및 산업/시각적 디자인 분야에서 제품, 서비스, 시스템을 사용하면서 체험하는

전반적인 사용자 경험을 개선하기 위한 설계라고 하는데..

 

그냥 간단하게 보면 저희가 만든 프로그램(웹 사이트)을 쓰는 사람들의 편안함? 이라고 볼 수 있을 거 같습니다.

 

"게시글에서 제목을 누르니까 상세보기로 넘어가잖아?"

"리스트나 상세보기에서 글 작성 버튼이 보이네?"

"상세보기를 들어왔는데 돌아가기 버튼은 없고 왜 제목이랑 내용만 있어? 나보고 어쩌라는거야?"

 

이런식으로 사용함에 있어 사용감?이 좋게 만드는 것을 생각하는 일을 UX라고 생각합니다.

솔직히 자세한 정의는 내리지 못하지만 아마 이정도면 비슷하지 않을까 싶네요.