Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
Tags
- IntelliJ IDEA
- break문
- 인텔리제이 한글 깨짐 해결법
- 접근제어지시자
- 생성자
- 시스템 환경 변수 편집
- for문
- this예약어
- JAVA객체지향
- 메서드
- 메서드 오버로딩
- continue문
- 형 변환
- JAVA기초
- 자바 멀티스레딩
- While
- 집합관계
- Thread
- multi-threading
- 포함관계
- function
- Java
- java변수
- OPP개념
- 반복문
- 컴파일
- 인텔리제이 기초 설정
- Java데이터 타입
- 상수
- 연관관계
Archives
- Today
- Total
최원종의 개발 블로그
V7-2 댓글 작성하기 기능 추가 본문


reply_tb 생성(SQL)
create table reply_tb(
id int auto_increment primary key,
comment varchar(500) not null,
created_at timestamp default current_timestamp,
user_id int not null,
board_id int not null,
foreign key(user_id) references user_tb(id),
foreign key(board_id) references board_tb(id)
);
Reply 엔티티 ( 1단계 작업)
Reply 코드 요약
댓글 도메인(Reply) 신규 구축 : 테이블 reply_tb와 매핑되는 새로운 엔티티를 추가하여 블로그의 핵심 기능 확장
@Column 제약 조건 설정 : 댓글 내용(comment) 필드에
글자 수 제한(500자) 및 필수값(nullable = false) 옵션을 주어 데이터 무결성 확보
다대일(@ManyToOne) 단방향 이중 매핑 : 하나의 댓글이 '어떤 사용자'가 '어떤 게시글'에 썼는지
추적할 수 있도록 두 개의 외래키(FK) 관계를 정석대로 연결
성능 최적화 지연 로딩(LAZY) 적용 : User와 Board를 조회할 때 불필요한 즉시 로딩을 방지하여
성능 낭비를 원천 차단
Reply에서 알아야 할 개념
Reply ➔ User (N : 1): 한 명의 유저는 여러 개의 댓글을 달 수 있지만,
하나의 댓글은 오직 한 명의 유저만 작성할 수 있다.
Reply ➔ Board (N : 1): 하나의 게시글에는 수많은 댓글이 달릴 수 있지만,
특정 댓글 하나가 여러 게시글에 동시에 존재할 수는 없다.
이 두 개의 외래키(user_id, board_id)가 reply_tb에 물리적으로 생성되면서
중심을 단단히 잡아주는 구조가 완성.
Reply 코드
package com.tenco.blog.reply;
import com.tenco.blog.board.Board;
import com.tenco.blog.user.User;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import java.sql.Timestamp;
@Data
@NoArgsConstructor
@Entity
@Table(name = "reply_tb")
public class Reply {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length = 500 , nullable = false)
private String comment;
@CreationTimestamp // pc --> db 자동 주입
private Timestamp createdAt;
// Reply -> User 연관관계 설정 (FK -> 자바에서 표현하는 개념)
// 1 : N, N : 1 , M:N <- 중 선택
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
// 하나의 게시글에는 여러개의 댓글이 작성될 수 있다. 1 : N
// N : 1
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "board_id")
private Board board;
}
Reply 댓글 소유자 확인 로직 추가 코드 ( 2단계 작업)
package com.tenco.blog.reply;
import com.tenco.blog._core.errors.Exception404;
import com.tenco.blog.board.Board;
import com.tenco.blog.user.User;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import java.sql.Timestamp;
@Data
@NoArgsConstructor
@Entity
@Table(name = "reply_tb")
public class Reply {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length = 500 , nullable = false)
private String comment;
@CreationTimestamp // pc --> db 자동 주입
private Timestamp createdAt;
// Reply -> User 연관관계 설정 (FK -> 자바에서 표현하는 개념)
// 1 : N, N : 1 , M:N <- 중 선택
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
// 하나의 게시글에는 여러개의 댓글이 작성될 수 있다. 1 : N
// N : 1
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "board_id")
private Board board;
@Builder
public Reply(String comment, User user, Board board) {
this.comment = comment;
this.user = user;
this.board = board;
}
/**
* 댓글 소유자 확인 로직 (세션정보, DB 작성된 user_id)
* @return
*/
public boolean isOwner(Integer userId) {
if(this.user == null || userId == null) {
throw new Exception404("잘못된 요청입니다.");
}
// 본인이 작성한 댓글이 아님
if(this.user.getId() != userId) {
return false;
}
return true;
}
}
WebMvcConfig 코드 ( reply 인증검사 공통 처리)
package com.tenco.blog._core.config;
import com.tenco.blog._core.interceptor.LoginInterceptor;
import com.tenco.blog._core.interceptor.SessionInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// 자바 코드로 스프링 부트 설정 파일을 다둘 수 있다.
// @Component
@Configuration // IoC 대상 - 하나 이상의 IoC 처리를 하고 싶을 때 사용 한다.
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired // DI 처리
private LoginInterceptor loginInterceptor;
@Autowired // DI 처리
private SessionInterceptor sessionInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 화면에 SessionUser 정보를 내려줄 사용 됨.
registry.addInterceptor(sessionInterceptor)
.addPathPatterns("/**"); // 모든 URL 요청서 동작 함
// 인증 처리 인터셉터 동작 함
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/board/**", "/user/**", "/reply/**")
.excludePathPatterns(
// 로그인 관련(인증이 필요 없는 페이지)
"/login-form", // 로그인 화면 요청 시
"/join-form", // 회원 가입 화면 요청 시
"/logout", // 로그아웃
// 게시글 조회 관련 (인증 없이도 볼 수 있는 페이지)
"/board/list", // 게시글 목록 화면 요청 시
"/" , // 메인 페이지
"/index" , // 메인 페이지
"/board/{id:\\d+}", // 게시글 상세보기( 숫자 ID만 허용)
// 정적 리소스 (CSS, JS, 이미지 등)
"/css/**", // CSS 파일 제외
"/js/**", // JS 파일 제외
"/images/**", // 이미지 파일 제외
"/favicon.ico", // 파비콘 제외
// H2 데이터베시스 콘솔( 개발 환경용)
"/h2-console/**" // H2 콘솔 접근
);
}
}
board/detail.mustache ( 댓글 등록 폼 hidden 태그에 board pk 추가)
<!-- 댓글 등록 폼 -->
<div class="card-body">
<!--
댓글 등록 폼: POST 방식으로 댓글 저장
향후 댓글 기능 구현시 사용할 예정
현재는 정적 HTML로만 구성
-->
<form action="/reply/save" method="post">
<input type="hidden" name="boardId" value="{{board.id}}">
<textarea class="form-control" rows="2" name="comment" placeholder="댓글을 입력하세요"></textarea>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-outline-primary mt-1">댓글등록</button>
</div>
</form>
ReplyController (댓글 등록 기능)
코드 요약
도메인 제어기 구축 : 댓글(Reply) 도메인의 생성 및 처리를 전담하는
ReplyController를 새로 추가하여 아키텍처 응집도 향상
동적 리다이렉트(Redirect) 구현 : 댓글 작성 완료 후
saveDTO.getBoardId()를 활용해 원래 읽던 게시글 상세 화면으로 자연스럽게 복귀 처리
유효성 검사 1차 방어 : 로직 진입 직후 DTO의 validate()를 즉시 실행하여
빈 댓글이나 비정상 요청을 서비스 레이어 전 단계에서 컷
세션 연동 및 가벼운 컨트롤러 유지 : 복잡한 인가 처리나 비즈니스 로직은
ReplyService.댓글작성()으로 위임하고, 컨트롤러는 흐름 제어에만 집중
ReplyController 코드
package com.tenco.blog.reply;
import com.tenco.blog.user.User;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
@Controller // IoC
@RequiredArgsConstructor // DI 처리
public class ReplyController {
private final ReplyService replyService;
// 댓글 등록 기능 요청
@PostMapping("/reply/save")
public String saveProc(ReplyRequest.SaveDTO saveDTO, HttpSession session) {
// 1. 인증검사 --> LoginInterceptor 처리
User sessionUser = (User) session.getAttribute("sessionUser");
// 2. 유효성 검사
saveDTO.validate();
replyService.댓글작성(saveDTO, sessionUser.getId());
// 해당 게시글에 댓글 작성후 리다이렉션 처리 (해당 게시글로)
return "redirect:/board/" + saveDTO.getBoardId();
}
// 댓글 삭제 기능 요청
// @PostMapping("/reply/delete")
}
ReplyService
Service 계층에서는 여러 Repository를 조합해서 비즈니스 규칙을 완성한다
즉, 서비스 계층이 필요한 이유 중 하나이다.
코드 요약
서비스 계층의 본질 구현 : 하나의 서비스에서 Reply, Board, User 총 3개의 리포지토리를
유기적으로 조합하여 복잡한 비즈니스 규칙 완성
선제적 데이터 검증 (Defensive Code) : 댓글을 달기 전, 대상 게시글과 작성자가
실제로 DB에 존재하는지 findById와 orElseThrow로 철저히 상호 검증
도메인 객체 간의 연관관계 바인딩 : 조회해온 영속 객체(userEntity, boardEntity)를
saveDTO.toEntity()의 인자로 전달해 정석적인 FK 관계 맵핑 완료
@Transactional 어노테이션 전술적 활용 : 데이터 삽입(INSERT)이 일어나는 메서드 특성에 맞게
쓰기 트랜잭션을 명시하여 데이터 무결성 보장
ReplyService 코드
package com.tenco.blog.reply;
import com.tenco.blog._core.errors.Exception404;
import com.tenco.blog.board.Board;
import com.tenco.blog.board.BoardRepository;
import com.tenco.blog.user.User;
import com.tenco.blog.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
// Service 계층에서는 여러 Repository를 조합해서 비즈니스 규칙을 완성한다
// 즉, 서비스 계층이 필요한 이유 중 하나이다.
@Service // IoC
@RequiredArgsConstructor // DI 처리
public class ReplyService {
private final ReplyRepository replyRepository;
private final BoardRepository boardRepository;
private final UserRepository userRepository;
@Transactional
public Reply 댓글작성(ReplyRequest.SaveDTO saveDTO, Integer id) {
// 게시글 조회
Board boardEntity = boardRepository.findById(saveDTO.getBoardId()).orElseThrow(
() -> new Exception404("해당 게시글을 찾을 수 없습니다"));
// id 로 사용자 조회
User userEntity = userRepository.findById(id).orElseThrow(
() -> new Exception404("사용자를 찾을 수 없습니다")
);
Reply reply = saveDTO.toEntity(userEntity, boardEntity);
replyRepository.save(reply);
return reply;
}
}
ReplyRepository 코드
package com.tenco.blog.reply;
import org.springframework.data.jpa.repository.JpaRepository;
// @Repository - 부모의 클래스에 정의 되어 있음()
public interface ReplyRepository extends JpaRepository<Reply, Integer> {
// 기본적인 CRUD 자동 완성 및 추가 편의 기능 자동 생성
}'Spring boot 입문' 카테고리의 다른 글
| V7-4 댓글 삭제 기능 추가 (0) | 2026.05.21 |
|---|---|
| V7-3 댓글 조회 기능 추가 (0) | 2026.05.21 |
| 스프링부트 V7 댓글 기능 추가 (User 도메인 리팩토링 ) (0) | 2026.05.20 |
| V6-2 expose-session-attributes 의 false 사용 (0) | 2026.05.20 |
| 스프링부트 V6- OSIV (0) | 2026.05.20 |
