v5
★ MILESTONE · BOARD

최소 게시판

Create + Read 만 (인가 없음 — 의도적으로)

학습 목표

  • 게시판의 첫 두 기능 — 목록상세 보기 + 작성
  • JSP 의 JSTL + EL 로 데이터 출력
  • 5 부품(VO·Mapper·Service·Controller·JSP) 의 협업을 직접 짠다
  • v5 의 의도된 위험 — 인가 부재를 체감한다

⚠️ 처음부터 완벽한 게시판?

한 번에 다 만들려는 욕심

회원·로그인·작성·수정·삭제·페이징·댓글·첨부파일·검색·관리자 권한·... 한꺼번에 만들면 어디서 막혀도 원인을 알 수 없습니다.

가장 단순한 두 기능부터 시작 — Create 와 Read.

🛠️ 오늘 만들 것

CR (Create + Read)

가장 단순한 게시판 — 글 쓰기, 목록 보기, 상세 보기.

  • 글 작성 (Create) — 폼 + POST /board/write
  • 목록 보기 (Read All) — GET /board/list
  • 상세 보기 (Read One) — GET /board/view?id=3
  • ✗ 수정·삭제 — v6 에서 (인가와 함께)
  • ✗ 페이징 — v7 에서
  • ✗ 댓글 — v9 에서 (Ajax 와 함께)

① DB 테이블 (myboard 신설)


CREATE TABLE myboard (
    num     INT          PRIMARY KEY AUTO_INCREMENT,
    title   VARCHAR(100) NOT NULL,
    writer  VARCHAR(50)  NOT NULL,
    content TEXT         NOT NULL
);

👉 4 컬럼만. 작성일·조회수·첨부 같은 「있으면 좋은」 컬럼은 일부러 없습니다 — 다음 차시에서 ALTER 로 한 칸씩.

② Board 도메인 — 4 필드만


package com.smhrd.domain;

import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

@Data @AllArgsConstructor @NoArgsConstructor
public class Board {
    private int    num;
    private String title;
    private String writer;
    private String content;
}

Lombok @Data 가 getter/setter 자동 생성. 폼의 name="title" ↔ 필드 title 이 일치해야 자동 바인딩.

③ Mapper 인터페이스


package com.smhrd.mapper;

import com.smhrd.domain.Board;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface BoardMapper {

    List<Board> selectList();

    Board selectOne(int num);

    void insert(Board b);
}

③ Mapper XML — 조회 (SELECT)


<mapper namespace="com.smhrd.mapper.BoardMapper">

    <select id="selectList" resultType="com.smhrd.domain.Board">
        SELECT num, title, writer, content
        FROM myboard
        ORDER BY num DESC
    </select>

    <select id="selectOne" parameterType="int"
            resultType="com.smhrd.domain.Board">
        SELECT num, title, writer, content
        FROM myboard
        WHERE num = #{num}
    </select>

③ Mapper XML — 등록 (INSERT)


    <insert id="insert" parameterType="com.smhrd.domain.Board"
            useGeneratedKeys="true" keyProperty="num">
        INSERT INTO myboard(title, writer, content)
        VALUES(#{title}, #{writer}, #{content})
    </insert>

</mapper>

useGeneratedKeys + keyProperty="num" — INSERT 후 DB 가 만든 자동 증가 num 값을 VO 의 num 필드에 채워 돌려줍니다.

왜 작성자 이름이 따로 필요 없는가

v5 의 writer 컬럼은 로그인 ID 문자열 을 그대로 저장합니다. 닉네임 표시·JOIN 같은 「화려한 것」 은 일부러 미룸.


myboard
┌─────┬──────────┬─────────┬─────────┐
│ num │ title    │ writer  │ content │
├─────┼──────────┼─────────┼─────────┤
│ 1   │ 첫 글    │ user1   │ ...     │
│ 2   │ 두번째   │ user2   │ ...     │
└─────┴──────────┴─────────┴─────────┘

화면에는 ${b.writer} 그대로 출력

v5 는 「작동하는 최소형」이 목표. JOIN·표시명 분리 같은 기교는 후속 진화의 몫.

useGeneratedKeys — INSERT 후 자동 num 받기


<insert id="insert" parameterType="com.smhrd.domain.Board"
        useGeneratedKeys="true" keyProperty="num">
    INSERT INTO myboard(title, writer, content)
    VALUES(#{title}, #{writer}, #{content})
</insert>

DB 의 AUTO_INCREMENT 로 생성된 num 이 — INSERT 후 객체의 num 필드에 자동으로 채워집니다.


Board b = new Board();
b.setTitle("새 글");
b.setWriter("user1");
b.setContent("...");
mapper.insert(b);

System.out.println(b.getNum());   // ⭐ 자동 채워진 num (예: 7)

④ Service


@Service
public class BoardService {

    @Autowired
    private BoardMapper mapper;

    public List<Board> selectList() {
        return mapper.selectList();
    }

    public Board selectOne(int num) {
        Board b = mapper.selectOne(num);
        if (b == null) {
            throw new IllegalArgumentException("글이 없습니다: " + num);
        }
        return b;
    }

    public void insert(Board b) {
        mapper.insert(b);
    }
}

⑤ Controller — 조회


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

    @Autowired
    private BoardService service;

    @GetMapping("/list")
    public String list(Model model) {
        model.addAttribute("boards", service.selectList());
        return "board/list";
    }

    @GetMapping("/view")
    public String view(@RequestParam int num, Model model) {
        model.addAttribute("board", service.selectOne(num));
        return "board/view";
    }
    // ... (다음 슬라이드: 작성 폼 + 등록)
}

⑤ Controller — 작성


    @GetMapping("/write")
    public String writeForm() {
        return "board/write";
    }

    @PostMapping("/write")
    public String write(Board board, HttpSession session) {
        Member u = (Member) session.getAttribute("loginUser");
        board.setWriter(u.getId());
        service.insert(board);
        return "redirect:/board/list";
    }

작성자 ID 는 폼이 아니라 세션에서 꺼냅니다. 폼으로 받으면 「남의 이름으로 쓰기」가 가능해지기 때문.

⑥ JSP — 목록 헤더


<!-- /WEB-INF/views/board/list.jsp -->
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<body>
<%@ include file="/WEB-INF/views/common/header.jsp" %>

<h1>게시판</h1>
<a href="/board/write">새 글 쓰기</a>

⑥ JSP — 목록 테이블


<table>
    <thead>
        <tr><th>번호</th><th>제목</th>
            <th>작성자</th></tr>
    </thead>
    <tbody>
        <c:forEach var="b" items="${boards}">
            <tr>
                <td>${b.num}</td>
                <td><a href="/board/view?num=${b.num}">${b.title}</a></td>
                <td>${b.writer}</td>
            </tr>
        </c:forEach>
    </tbody>
</table>
</body></html>

「조회수」 컬럼이 없는 이유 — v5 의 myboard 에는 그 컬럼 자체가 없습니다. v10 에서 ALTER ADD.

JSTL c:forEach — List 반복

c:forEach 가 게시판 목록 같은 반복 출력의 핵심.


<c:forEach var="b" items="${boards}">
    <!-- ${b} 가 List 의 각 Board 객체 -->
    ${b.title}
</c:forEach>

Controller 에서:


model.addAttribute("boards", service.selectList());  // List<Board>

→ JSP 에서 ${boards} 가 그 List 를 가리킴. c:forEach 가 한 번씩 돌며 b 변수에 각 항목.

⑥ JSP — 상세 보기


<!-- /WEB-INF/views/board/view.jsp -->
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<body>
<%@ include file="/WEB-INF/views/common/header.jsp" %>

<h1>${board.title}</h1>
<p>작성자: ${board.writer}</p>
<hr/>
<div>${board.content}</div>

<a href="/board/list">목록으로</a>

<!-- v6 에서 추가될 자리:
     본인일 때만 수정·삭제 버튼 -->

</body></html>

⑥ JSP — 작성 폼


<!-- /WEB-INF/views/board/write.jsp -->
<%@ page contentType="text/html; charset=UTF-8" %>
<html>
<body>
<%@ include file="/WEB-INF/views/common/header.jsp" %>

<h1>새 글 쓰기</h1>

<form action="/board/write" method="post">
    <label>제목: <input name="title" required /></label><br/>
    <label>내용:<br/>
        <textarea name="content" rows="10" cols="50"></textarea>
    </label><br/>
    <button type="submit">저장</button>
</form>

</body></html>

폼 input 의 name 속성 (title, content) 이 Board 클래스 필드명과 같아서 자동 바인딩. writer 는 폼이 아니라 세션에서.

전체 흐름 — 글 작성

① 사용자 「새 글 쓰기」 클릭 ↓ GET /board/write ② Controller.writeForm() ↓ "board/write" 반환 ③ ViewResolver → write.jsp ↓ HTML 응답 ④ 사용자: 제목·내용 입력 후 「저장」 ↓ POST /board/write ⑤ Controller.write(Board board, HttpSession session) ↓ board 자동 바인딩 (title, content 채움) ↓ session 에서 사용자 가져와 writer 채움 ↓ service.insert(board) ⑥ Service.insert ↓ mapper.insert(b) — INSERT SQL ⑦ DB 에 새 행 추가 ↓ ⑧ "redirect:/board/list" ↓ 302 응답 ⑨ 브라우저: GET /board/list ↓ ⑩ 새 글이 목록에 보임

전체 흐름 — 목록 보기

① 사용자: GET /board/list ↓ ② Controller.list(Model model) ↓ service.selectList() ③ Service.selectList() ↓ mapper.selectList() ④ Mapper.selectList() ↓ SELECT num, title, writer, content FROM myboard ORDER BY num DESC ⑤ DB 응답 ↓ List<Board> 자동 매핑 ⑥ Service → Controller 로 ↓ model.addAttribute("boards", ...) ⑦ return "board/list" ↓ ⑧ JSP 의 c:forEach 가 ${boards} 반복 ↓ ⑨ 완성된 HTML 응답

v5 의 의도된 위험 — 인가 부재

⚠️ 의도적 위험

v5 는 「본인 확인」 이 없습니다. 즉 — 로그인만 했으면 누구나 누구의 글이든 수정·삭제 가능한 상태로 설계되어 있습니다 (수정·삭제는 v6 에서).

왜 일부러 위험하게? — 다음 차시 v6 에서 「본인 확인」 을 추가할 때 그 차이를 명확히 보기 위함. 「불편을 직접 느낀 다음에 도구를 만나는」 우리 학습 패턴.

v5 에서 빠진 것들 — 다음 차시들에서

기능v5 에서다음에
수정·삭제없음v6 (본인 확인 포함)
페이징전체 목록만v7
댓글없음v9 (Ajax)
조회수 자동 증가표시만 (0 고정)v10 (Ajax)
검색없음고급 (보강)
첨부파일없음v10

실행해보기

  1. 로그인
  2. http://localhost:8080/.../board/list — 빈 목록
  3. 「새 글 쓰기」 → 폼 작성 → 저장
  4. 목록에 등록된 글이 보임
  5. 제목 클릭 → 상세 화면

흔한 함정

증상원인
405 Method Not Allowedform method="post" 빠짐 / @PostMapping ↔ method 불일치
title·content 가 nullform input name ↔ VO 필드명 불일치
writer 가 빈칸세션에서 안 채워서 INSERT 됨 / NOT NULL 위반
새로고침 시 "재제출" 경고POST 후 redirect: 안 씀
목록은 비어있는데 DB 엔 있음테이블명 오타 (boardmyboard)

v5 의 의미

v5 는 우리 프로젝트의 「실제로 작동하는」 첫 게시판입니다.

  • v0 ~ v1 — Hello / 회원 한 명 보기
  • v2 ~ v4 — 회원·로그인 (인증 끝)
  • v5 — 작동하는 게시판 (오늘)
  • v6 — 안전한 게시판 (인가)
  • v8~v∞ — REST API 게시판

동작은 단순하지만, 5 부품이 모두 한 번에 협업하는 첫 산출물입니다.

🔄 Before / After

v4 끝

회원·로그인 깔끔. 게시판은 없음.

v5 끝

작동하는 게시판이 있다. 글 쓰고 목록 보고 상세 본다. 단 — 의도적으로 인가 없음.

이번 차시의 데이터 흐름

폼 / 목록 클릭
BoardController
BoardService
BoardMapper
myboard 테이블
JSP c:forEach
5 부품의 첫 완전한 협업 — 새 테이블 myboard 등장

정리

오늘 들고 가는 것

  • VO + Mapper(I/F + XML) + Service + Controller + JSP 5 부품 협업
  • JSTL c:forEach 로 List 반복
  • Board { num, title, writer, content } — 4 필드만
  • useGeneratedKeys 로 INSERT 후 num 받기
  • v5 = 작동하는 게시판 (의도적 인가 부재)

다음: ★ v6 안전한 게시판 — Update / Delete + 본인 확인.