1. 개요
이번 구현에서 다루는 주요 기능은 다음과 같습니다:
- 게시글 CRUD: 사용자가 게시글을 생성, 조회, 수정, 삭제할 수 있도록 API를 설계.
- 댓글 기능: 각 게시글에 댓글을 달고, 댓글을 수정, 삭제하는 기능 구현.
- 태그 관리: 게시글에 태그를 추가하고, 태그를 수정 및 삭제하는 기능.
각 기능은 Controller, Service, Mapper, DTO를 사용해 계층 구조로 분리되었으며, MyBatis를 통해 데이터베이스와 상호작용합니다.
2. PostController: 게시글 관련 API 구현
PostController
는 게시글 관리 엔드포인트를 정의하고, 사용자의 요청을 처리하여 게시글의 생성, 조회, 수정, 삭제 기능을 제공합니다. 이를 통해 게시판의 핵심 기능인 게시글 작성과 조회를 관리할 수 있습니다.
2.1 게시글 등록
게시글 등록은 POST /posts
엔드포인트에서 처리됩니다. 사용자가 로그인한 상태에서 게시글 제목, 내용, 카테고리 ID 등을 입력하면 게시글이 데이터베이스에 저장됩니다.
@PostMapping
public ResponseEntity<CommonResponse<PostDTO>> registerPost(
@RequestBody @Valid PostRequest postRequest, HttpSession session) {
UserDTO user = getAuthenticatedUser(session);
if (user == null) {
return handleUnauthorized();
}
PostDTO result = postService.register(user.getId(), postRequest);
return createResponse(HttpStatus.CREATED, "POST_CREATED", "게시글이 성공적으로 등록되었습니다.", result);
}
- Session을 통한 인증 처리:
SessionUtil
을 사용하여 현재 로그인한 사용자의 ID를 가져와 게시글을 작성합니다. - 데이터 검증: 입력된 게시글 데이터는 서비스 계층에서 검증한 후, 유효할 경우 등록을 진행합니다.
- 결과 반환: 등록된 게시글의 정보를
PostDTO
로 클라이언트에 반환합니다.
2.2 게시글 조회
로그인한 사용자는 자신이 작성한 게시글 목록을 조회할 수 있습니다. GET /posts/my-posts
API는 페이지네이션을 통해 데이터를 제공합니다.
@GetMapping("/my-posts")
@LoginCheck(roles = {"DEFAULT", "ADMIN"})
public ResponseEntity<CommonResponse<Map<String, Object>>> myPostInfo(
@RequestParam(value = "page", defaultValue = "1") Long page,
@RequestParam(value = "size", defaultValue = "10") Long size,
HttpSession session) {
UserDTO user = getAuthenticatedUser(session);
if (user == null) {
return handleUnauthorized();
}
Map<String, Object> result = postService.getMyPosts(user.getId(), page, size);
return createResponse(HttpStatus.OK, "POSTS_RETRIEVED", "내 게시글 목록을 성공적으로 조회했습니다.", result);
}
- 페이지네이션 지원:
page
와size
파라미터를 통해 원하는 페이지의 게시글 목록을 가져옵니다. - 권한 검사:
LoginCheck
어노테이션을 사용하여 로그인된 사용자만 자신의 게시글 목록을 조회할 수 있도록 제어합니다.
게시글 수정과 삭제에 대한 설명을 추가합니다. 게시글을 수정하거나 삭제하는 작업은 게시판에서 매우 중요한 기능입니다. 이 기능들은 인증된 사용자만 접근할 수 있도록 보호되며, 사용자 권한에 따라 수정과 삭제가 가능합니다.
2.3 게시글 수정 (Update)
게시글 수정 기능은 PATCH /posts/{postId}
엔드포인트에서 처리됩니다. 사용자는 작성한 게시글을 수정할 수 있으며, 제목, 내용, 카테고리 등의 필드를 수정할 수 있습니다.
@PatchMapping("/{postId}")
@LoginCheck(roles = {"DEFAULT", "ADMIN"})
public ResponseEntity<CommonResponse<PostDTO>> updatePost(
@PathVariable(value = "postId") Long postId,
@RequestBody @Valid PostRequest postRequest,
HttpSession session) {
UserDTO user = getAuthenticatedUser(session);
if (user == null) {
return handleUnauthorized();
}
PostDTO postDTO = postService.updatePost(postId, user.getId(), postRequest);
return createResponse(HttpStatus.OK, "POST_UPDATED", "게시글이 성공적으로 수정되었습니다.", postDTO);
}
- API 설명:
- 이 엔드포인트는 로그인한 사용자가 특정 게시글의 ID를 기반으로 게시글의 일부 필드를 수정할 수 있게 합니다.
@LoginCheck
어노테이션을 통해 사용자가 로그인 상태인지 확인하며, 권한이 없는 사용자는 접근할 수 없습니다.
- Service 계층 처리:
updatePost(Long postId, Long userId, PostRequest postRequest)
메서드에서 수정할 게시글을 검색하고, 요청된 데이터로 필드를 업데이트합니다.
@Override
@Transactional
@CacheEvict(value = "getProducts", allEntries = true)
public PostDTO updatePost(Long postId, Long userId, PostRequest requestPostDTO) {
PostDTO existingPost = getAndValidatePost(postId, userId);
updatePostFields(existingPost, requestPostDTO);
try {
postMapper.updatePost(existingPost);
} catch (Exception e) {
log.error("Error updating post: postId={}", postId, e);
throw new RuntimeException("Failed to update post", e);
}
return postMapper.getPost(postId);
}
- 게시글 유효성 검사:
getAndValidatePost(Long postId, Long userId)
메서드를 사용하여 해당 게시글이 존재하는지 확인하고, 해당 사용자가 수정할 권한이 있는지 확인합니다. - 필드 업데이트: 필드가
null
이 아니거나 요청된 데이터가 있을 경우에만 기존 데이터를 덮어씁니다.
2.4 게시글 삭제 (Delete)
게시글 삭제는 DELETE /posts/{postId}
엔드포인트에서 처리됩니다. 사용자는 작성한 게시글을 삭제할 수 있으며, 삭제된 게시글은 데이터베이스에서 완전히 제거됩니다.
@DeleteMapping("/{postId}")
@LoginCheck(roles = {"DEFAULT", "ADMIN"})
public ResponseEntity<CommonResponse<Void>> deletePost(
@PathVariable(value = "postId") Long postId,
HttpSession session) {
UserDTO user = getAuthenticatedUser(session);
if (user == null) {
return handleUnauthorized();
}
postService.deletePost(user.getId(), postId);
return createResponse(HttpStatus.OK, "POST_DELETED", "게시글이 성공적으로 삭제되었습니다.", null);
}
- API 설명:
- 이 엔드포인트는 로그인한 사용자가 작성한 게시글을 삭제할 수 있도록 합니다.
- 역시
@LoginCheck
어노테이션을 사용하여 사용자가 로그인 상태인지, 삭제할 권한이 있는지를 확인합니다.
- Service 계층 처리:
deletePost(Long userId, Long postId)
메서드에서 게시글을 삭제합니다. 게시글에 연결된 댓글, 태그 등도 삭제하는 과정을 거칩니다.
@Override
@Transactional
@CacheEvict(value = "getProducts", allEntries = true)
public void deletePost(Long userId, Long postId) {
if (userId <= 0 || postId <= 0) {
throw new IllegalArgumentException("Invalid userId or postId");
}
try {
// 게시글 존재 여부 확인
PostDTO post = postMapper.getPost(postId);
if (post == null) {
throw new RuntimeException("Post not found with id: " + postId);
}
// 관련된 태그 및 댓글 삭제
postMapper.deletePostTagByPostId(postId);
commentMapper.deleteCommentsByPostId(postId);
// 게시글 삭제
postMapper.deletePost(postId);
log.info("Successfully deleted post with id: {}", postId);
} catch (DataAccessException e) {
log.error("Database error occurred while deleting post: postId={}", postId, e);
throw new RuntimeException("Database error occurred while deleting post", e);
} catch (Exception e) {
log.error("Error deleting post: postId={}", postId, e);
throw new RuntimeException("Failed to delete post", e);
}
}
- 연관 데이터 삭제:
- 게시글을 삭제하기 전에 해당 게시글과 연결된 댓글과 태그를 먼저 삭제합니다.
postMapper.deletePostTagByPostId(postId)
와commentMapper.deleteCommentsByPostId(postId)
를 통해 게시글과 연관된 데이터를 먼저 제거한 후, 게시글 자체를 삭제합니다.
3. 댓글(Comment) 기능 구현
댓글 기능은 게시글에 추가되는 사용자 간의 상호작용을 담당합니다. 사용자는 게시글에 댓글을 작성하고, 이를 수정 또는 삭제할 수 있습니다. 이 기능은 PostService
와 연계되어 있으며, CommentDTO
를 통해 데이터베이스에 저장됩니다.
3.1 댓글 등록
댓글은 특정 게시글에 달리는 형태로, 사용자가 댓글을 작성할 수 있도록 API를 제공합니다.
@PostMapping("/comments")
@LoginCheck(roles = {"DEFAULT", "ADMIN"})
public ResponseEntity<CommonResponse<CommentDTO>> registerComment(
@RequestBody @Valid CommentRequest commentRequest,
HttpSession session) {
UserDTO user = getAuthenticatedUser(session);
if (user == null) {
return handleUnauthorized();
}
CommentDTO result = postService.registerComment(user.getId(), commentRequest);
return createResponse(HttpStatus.CREATED, "COMMENT_CREATED", "댓글이 성공적으로 등록되었습니다.", result);
}
- 댓글 작성 처리: 댓글 요청 데이터는
CommentRequest
객체로 받아 서비스 계층으로 전달됩니다. - 결과 반환: 등록된 댓글 정보를 클라이언트에
CommentDTO
형태로 반환합니다.
3.2 댓글 수정 및 삭제
- 댓글은
PATCH /comments/{commentId}
로 수정할 수 있고,DELETE /comments/{commentId}
로 삭제할 수 있습니다. 각 API는LoginCheck
어노테이션을 통해 로그인한 사용자만 접근 가능하도록 설정됩니다.
4. 태그(Tag) 기능 구현
태그는 게시글을 분류하는 중요한 요소로, 게시글에 태그를 추가하거나 수정, 삭제하는 기능을 제공합니다. 각 게시글은 여러 개의 태그를 가질 수 있으며, 이를 통해 사용자는 원하는 주제에 맞는 게시글을 쉽게 찾아볼 수 있습니다.
4.1 태그 등록
@PostMapping("/tags")
@LoginCheck(roles = {"DEFAULT", "ADMIN"})
public ResponseEntity<CommonResponse<Void>> registerTag(
@RequestBody @Valid TagRequest tagRequest,
HttpSession session) {
UserDTO user = getAuthenticatedUser(session);
if (user == null) {
return handleUnauthorized();
}
postService.registerTag(user.getId(), tagRequest);
return createResponse(HttpStatus.CREATED, "TAG_CREATED", "태그가 성공적으로 등록되었습니다.", null);
}
- 태그 생성: 사용자가 입력한 태그 이름과 URL을 데이터베이스에 저장합니다.
- 중복 확인: 이미 등록된 태그가 있는지 확인 후, 없는 경우 새로운 태그를 등록합니다.
5. PostService와 서비스 계층 로직
PostService
는 게시글, 댓글, 태그와 관련된 비즈니스 로직을 처리합니다. 이 계층에서는 데이터 검증, 예외 처리, 그리고 데이터베이스 연동을 담당합니다.
5.1 게시글 등록 로직
게시글 등록 로직은 PostRequest
를 PostDTO
로 변환한 후 데이터베이스에 저장하는 과정을 거칩니다.
@Override
@Transactional
public PostDTO register(Long userId, PostRequest postRequest) {
validatePostRequest(postRequest);
PostDTO postDTO = PostDTO.fromRequest(userId, postRequest);
postMapper.register(postDTO);
processTagsForPost(postDTO); // 태그 처리 로직
return postDTO;
}
- 데이터 검증: 게시글 요청 데이터가 유효한지 확인합니다.
- 태그 처리: 게시글 등록 시 해당 게시글에 포함된 태그도 처리됩니다.
5.2 댓글 등록 로직
댓글 등록 로직은 CommentRequest
를 CommentDTO
로 변환하여 저장하는 형태로 구현됩니다.
@Override
public CommentDTO registerComment(Long userId, CommentRequest commentRequest) {
CommentDTO commentDto = CommentDTO.fromRequest(userId, commentRequest);
commentMapper.register(commentDto);
return commentDto;
}
- 검증 및 등록: 댓글 데이터가 유효한지 확인 후, 데이터베이스에 저장됩니다.
5.3 태그 처리 로직
태그는 게시글 등록 시 함께 처리되며, 태그가 이미 존재하는지 확인한 후 새로운 태그가 없을 경우 생성합니다.
@Override
public void registerTag(Long userId, TagRequest tagRequest) {
TagDTO existingTag = tagMapper.findTagByName(tagRequest.getName());
if (existingTag == null) {
TagDTO tagDTO = TagDTO.fromRequestForCreate(tagRequest);
tagMapper.register(tagDTO);
} else {
log.info("Tag with name {} already exists", tagRequest.getName());
}
}
- 태그 존재 여부 확인: 태그가 이미 존재하는지 검사하고, 없으면 새로 등록합니다.
6. 데이터베이스 매퍼: MyBatis 연동
MyBatis는 데이터베이스와의 상호작용을 위해 매퍼 인터페이스를 사용합니다. 각 매퍼는 필요한 쿼리를 정의하고, 이를 통해 데이터베이스와 통신합니다.
6.1 게시글 등록 쿼리
<insert id="register" parameterType="com
.example.boardserver.dto.PostDTO">
<selectKey keyProperty="id" resultType="Long" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO post(name, isAdmin, contents, createTime, views, categoryId, userId)
VALUES (#{name}, #{isAdmin}, #{contents}, #{createTime}, #{views}, #{categoryId}, #{userId})
</insert>
이 쿼리는 게시글을 데이터베이스에 등록한 후, 생성된 게시글 ID를 반환합니다.
6.2 게시글 수정 쿼리
<update id="updatePost" parameterType="com.example.boardserver.dto.PostDTO">
UPDATE post
SET name = #{name},
contents = #{contents},
views = #{views},
categoryId = #{categoryId},
userId = #{userId}
WHERE id = #{id}
</update>
게시글 수정 시 입력된 제목, 내용, 카테고리 등의 데이터를 수정합니다. 각 필드는 #{}
로 바인딩되어 요청된 데이터를 업데이트합니다.
6.3 게시글 삭제 쿼리
<delete id="deletePost">
DELETE FROM post
WHERE id = #{postId}
</delete>
이 쿼리는 해당 게시글 ID를 사용하여 게시글을 삭제합니다. 삭제되기 전에 게시글과 연결된 댓글 및 태그가 먼저 삭제됩니다.
6.4 댓글 및 태그 삭제 쿼리
<!-- 댓글 삭제 -->
<delete id="deleteCommentsByPostId">
DELETE FROM comment
WHERE postId = #{postId}
</delete>
<!-- 태그 삭제 -->
<delete id="deletePostTagByPostId">
DELETE FROM posttag
WHERE postId = #{postId}
</delete>
- 댓글 삭제: 게시글 삭제 시 해당 게시글에 달린 모든 댓글을 삭제합니다.
- 태그 삭제: 게시글과 연결된 태그를 posttag 테이블에서 삭제합니다.
6.5 태그 및 댓글 관련 쿼리
<!-- TagMapper -->
<insert id="register" parameterType="com.example.boardserver.dto.TagDTO">
<selectKey keyProperty="id" resultType="Long" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO tag(name, url)
VALUES (#{name}, #{url})
</insert>
<!-- CommentMapper -->
<insert id="register" parameterType="com.example.boardserver.dto.CommentDTO">
<selectKey keyProperty="id" resultType="Long" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO comment(postId, contents, subCommentId)
VALUES (#{postId}, #{contents}, #{subCommentId})
</insert>
각 매퍼는 태그 및 댓글을 등록하기 위한 쿼리를 정의하고, ID 생성 후 데이터를 저장합니다.
결론
이번 시리즈에서 우리는 게시글 CRUD, 댓글 기능, 태그 관리 기능을 성공적으로 구현했습니다. 각 기능은 잘 분리된 구조를 통해 확장 가능하며, MyBatis와의 연동을 통해 데이터베이스와 효과적으로 상호작용할 수 있도록 설계되었습니다. 앞으로도 더 많은 기능을 추가하면서, 대규모 트래픽을 처리할 수 있는 게시판을 완성해 나가겠습니다.
'프레임워크 > 자바 스프링' 카테고리의 다른 글
대규모 트래픽 게시판 구축 시리즈 #11: 로깅, 예외처리 (0) | 2024.09.07 |
---|---|
대규모 트래픽 게시판 구축 시리즈 #10: 게시판 검색 API (0) | 2024.09.07 |
대규모 트래픽 게시판 구축 시리즈 #8: 카테고리 API (0) | 2024.09.07 |
대규모 트래픽 게시판 구축 시리즈 #7: Spring AOP를 활용한 인증 및 인가 (0) | 2024.09.05 |
대규모 트래픽 게시판 구축 시리즈 #6: 유저 API (1) | 2024.09.05 |