-
개요
이번 포스팅에서는 회원 기능에 이어 웹 개발의 핵심인 게시판 CRUD(Create, Read, Update, Delete) 기능을 구현했습니다.
특히 작성자 정보를 직접 입력받지 않고 로그인 세션(Session)에서 안전하게 가로채어 등록하는 방식과, 본인이 작성한 글에만 수정/삭제 권한을 부여하는 실무형 권한 검증 로직을 학습했습니다. -
データベース設計와 Entity (Board.java)
JPA를 활용하여 테이블 구조를 자바 코드로 매핑했습니다. 수정을 위해 엔티티 내부에서 데이터를 변경하는 비즈니스 메서드(update)를 추가 장착했습니다.
@Entity
@Table(name = "BOARDS")
@Getter
@NoArgsConstructor
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title; // 제목
private String content; // 내용
private String writer; // 작성자 닉네임 (세션 유저 정보 주입)
@Builder
public Board(String title, String content, String writer) {
this.title = title;
this.content = content;
this.writer = writer;
}
/**
* JPA 더티 체킹(Dirty Checking)을 위한 엔티티 수정 메서드
*/
public void update(String title, String content) {
this.title = title;
this.content = content;
}
}
- Core Logic: 비즈니스 로직과 JPA의 마법 (BoardService.java)
BoardService에서는 모든 CRUD 핵심 기능을 처리합니다. 수정(Update) 시 별도의 update SQL문을 실행하지 않고, 영속성 컨텍스트의 더티 체킹(Dirty Checking)을 활용하여 상태를 변경했습니다.
@Service
@RequiredArgsConstructor
public class BoardService {
private final BoardRepository boardRepository;
/**
* C (Create): 게시글 작성
*/
@Transactional
public Long writePost(String title, String content, String writer) {
Board board = Board.builder()
.title(title)
.content(content)
.writer(writer)
.build();
return boardRepository.save(board).getId();
}
/**
* R (Read): 전체 목록 조회
*/
@Transactional(readOnly = true)
public List<Board> findAllPosts() {
return boardRepository.findAll();
}
/**
* R (Read): 단건 상세 조회
*/
@Transactional(readOnly = true)
public Board findPostById(Long id) {
return boardRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("存在しない投稿です。ID: " + id));
}
/**
* U (Update): 게시글 수정 (더티 체킹 활용)
*/
@Transactional
public void updatePost(Long id, String title, String content) {
Board board = boardRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("存在しない投稿です。ID: " + id));
// 엔티티의 데이터가 변경되면 트랜잭션 종료 시 자동으로 UPDATE 쿼리가 실행됨
board.update(title, content);
}
/**
* D (Delete): 게시글 삭제
*/
@Transactional
public void deletePost(Long id) {
boardRepository.deleteById(id);
}
}
- Controller & View 권한 검증 연동
보안과 사용자 경험을 충족하기 위해 컨트롤러 계층에서의 세션 검증과 Thymeleaf를 활용한 UI 동적 제어를 연동했습니다.
① Controller에서의 꼼수 접근 원천 차단 (BoardController.java)
클라이언트가 주소창 입력을 통해 비정상적인 수정/삭제 요청을 보내더라도, 백엔드 컨트롤러에서 세션의 로그인 유저와 작성자 정보(board.getWriter())가 일치하는지 철저하게 검증합니다.
@PostMapping("/update")
public String update(@RequestParam("id") Long id,
@RequestParam("title") String title,
@RequestParam("content") String content,
HttpSession session) {
User loginUser = (User) session.getAttribute("loginUser");
if (loginUser == null) {
return "redirect:/login";
}
boardService.updatePost(id, title, content);
return "redirect:/board/detail?id=" + id;
}
② Thymeleaf 기반 동적 UI 렌더링 (detail.html)
현재 세션에 로그인한 유저의 정보가 존재하고, 글 작성자 닉네임과 정확히 일치할 때만 화면에 [✏️ 글 수정하기] 및 [🔥 글 삭제하기] 버튼이 노출되도록 구현했습니다.
<div th:if="${session.loginUser != null and session.loginUser.nickname == board.writer}">
<a th:href="@{/board/edit(id=${board.id})}">✏️ 投稿修正</a>
<form th:action="@{/api/boards/delete}" method="post" onsubmit="return confirm('本当に削除しますか?');">
<input type="hidden" name="id" th:value="${board.id}">
<button type="submit">🔥 投稿削除</button>
</form>
</div>
- 💡 오늘 배운 핵심 인사이트 (Reflection)
処理(しょり)와 実装(じっそう)의 흐름: 데이터를 화면에 던지기 전 반드시 Model 객체나 HttpSession 보따리에 담아 보내야만 Thymeleaf가 프론트엔드에서 이를 인지하고 연산할 수 있다는 데이터 흐름의 원리를 명확히 이해했다.
JPA의 강력함: 단순 메서드 호출만으로 SELECT, INSERT, DELETE가 완벽히 처리되며, @Transactional 범위 내에서 엔티티 객체의 값만 바꾸면 자동으로 UPDATE문이 실행되는 더티 체킹 메커니즘이 무척 효율적이고 인상 깊었다.