Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5기 남은찬, 박주한] Spring Boot JPA 게시판 구현 미션 제출합니다. #267

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: 도메인, 컨트롤러, 서비스, 레포, db셋팅 구현
ParkJuhan94 committed Nov 21, 2023
commit e40522f86112781bd7d701d4d38836c586c95158
25 changes: 25 additions & 0 deletions src/main/java/kdt/jpa/board/global/entity/BaseEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kdt.jpa.board.global.entity;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {

@CreatedDate
@Column(columnDefinition = "TIMESTAMP")
protected LocalDateTime createdDate;

Comment on lines +16 to +21

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인스턴스화하지 않을 객체이니 추상클래스로 변경해보는 것은 어때요 ??

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 맞는말인거같아요! 수정하겠습니다

@LastModifiedDate
@Column(columnDefinition = "TIMESTAMP")
private LocalDateTime updatedDate;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

질문) createdDateprotectedupdatedDateprivate으로 선언하신 이유가 있을까요?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

왜 이렇게 선언돼있는지 모르겠네요 .. 실수한 것 같아요 수정하겠습니다!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

질문) ZonedDateTime,Instant 대신 LocalDateTime을 사용하신 이유가 있을까요?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

솔직히 특별한 이유 없고, 자바에선 항상 LocalDateTime 만 사용해와서 사용했습니다

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지나가다가 발견해서 코멘트 남깁니다.
면접에서 자바8 기능에 대하여 종종 물어볼 수도 있어서 참고해보시면 좋을 것 같아요
https://min103ju.github.io/java/localdatetime/

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LocalDateTime 필드의 네이밍을 ~Date로 선언하신 이유가 있을까요?
LocalDate 필드를 사용할 일이 있을 때 변수명을 보고 datetime 값을 가지고 있는지 date 값을 가지고 있는지 확인하기 어려울 것 같습니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 정확한 네이밍 으로 수정하겠습니다!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요구사항은 created_atcreated_by를 만드는 걸로 기억하고 있는데 created_by는 빠진 이유가 있을까요?

Copy link
Author

@ParkJuhan94 ParkJuhan94 Nov 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 빠트린 created_by 필드도 추가해야겠습니다

Comment on lines +23 to +24

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개인적으로 모든 엔티티 필드에 컬럼 애너테이션을 달고 컬럼 이름을 명시하는 것을 선호해요

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 한번 적용해보겠습니다

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package kdt.jpa.board.global.exception;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class BoardApiExceptionHandler {

@ExceptionHandler(BoardException.class)
public ResponseEntity<ErrorResponse> handleCustomException(BoardException e) {
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}
}
11 changes: 11 additions & 0 deletions src/main/java/kdt/jpa/board/global/exception/BoardException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package kdt.jpa.board.global.exception;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class BoardException extends RuntimeException {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BoardException 네이밍이 어색한 것 같습니다.

  1. 이름만 보고 어떠한 용도로 쓰이는 지 알기 어렵습니다. 게시판 프로젝트라 Board 가 쓰였다 정도?
  2. 전역적으로 쓸 용도였다면 RuntimeException 대신에 사용할 이유를 모르겠습니다.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 네이밍은... 한번 더 고민해보겠습니다
  2. ExceptionHandler 에서 커스텀 예외에 대한 핸들링을 따로 하기위해서 만들었습니다


private final String message;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

질문) RuntimeException을 상속받았으면 message를 인자로 전달해줄 수 있는데 다시 정의하신 이유가 있을까요?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

생각해보니 없어도 되는 필드같네요! 수정하겠습니다

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package kdt.jpa.board.global.exception;

public record ErrorResponse(
String message
) {
}
48 changes: 48 additions & 0 deletions src/main/java/kdt/jpa/board/post/controller/PostController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package kdt.jpa.board.post.controller;

import kdt.jpa.board.post.service.PostService;
import kdt.jpa.board.post.service.dto.request.CreatePostRequest;
import kdt.jpa.board.post.service.dto.request.EditPostRequest;
import kdt.jpa.board.post.service.dto.response.PostListResponse;
import kdt.jpa.board.post.service.dto.response.PostResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/posts")
public class PostController {

private final PostService postService;

@PostMapping
public ResponseEntity<Long> savePost(@RequestBody CreatePostRequest request) {
Long postId = postService.createPost(request);

return ResponseEntity.ok(postId);
Comment on lines +29 to +32

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

응답 값만 보아서는 어떤 값이 리턴이 되는 지 알기 어렵습니다.
객체화해서 그 객체 필드에 postId 를 전달하는 것이 명확할 것 같아여

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분에 대해서 솔직히 id 를 응답할 필요가 없을거같아서 리턴 타입 자체를 불린으로 바꾸려 하는데, 불린에 대한 응답도 한번 객체화하는게 좋을까요?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

불린은 괜찬을듯

}

@GetMapping("/{id}")
public ResponseEntity<PostResponse> getPost(@PathVariable Long id) {
PostResponse postResponse = postService.getById(id);

return ResponseEntity.ok(postResponse);
}

@GetMapping
public ResponseEntity<PostListResponse> getPosts(@PageableDefault Pageable pageable) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PageableDefault 를 사용하면 어떤 효과가 있나여 ??

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상세한 sizepage 에 대해서 특별한 요구사항을 정하지 않아서 페이징 정보를 받지 않았을 때, 기본으로 @PageableDefault 에서 지정되는 디폴트 값으로 설정하려고 사용했습니다!

PostListResponse postListResponse = postService.getPosts(pageable);

return ResponseEntity.ok(postListResponse);
}

@PatchMapping
public ResponseEntity<Boolean> updatePost(@RequestBody EditPostRequest request) {
boolean result = postService.editPost(request);

return ResponseEntity.ok(result);
}
}
41 changes: 41 additions & 0 deletions src/main/java/kdt/jpa/board/post/domain/Post.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package kdt.jpa.board.post.domain;

import jakarta.persistence.*;
import kdt.jpa.board.global.entity.BaseEntity;
import kdt.jpa.board.user.domain.User;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor
@Table(name = "posts")
public class Post extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String title;

private String content;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

질문) @JoinColumnnullable 조건을 안걸어주셨는데 게시글에 유저가 없어도 된다는 의미로 구현하신거 맞을까요?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제약조건에 대해서 신경쓰지 않았던 것 같습니다.. 더 신경 써보겠습니다

private User user;

public Post(String title, String content, User user) {
this.title = title;
this.content = content;
this.user = user;
Comment on lines +27 to +30

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정적팩토리메서드 패턴을 바꿔도 좋을 것 같아요

}

public void edit(String title, String contents) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

필드명은 content인데 파라미터는 contents 로 넘겨주신 이유가 있을까요?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

부주의인거같아요 수정하겠습니다 🫡

this.title = title;
this.content = contents;
}

public String getUserName() {
return user.getName();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kdt.jpa.board.post.repository;

import kdt.jpa.board.post.domain.Post;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PostRepository extends JpaRepository<Post, Long> {
}
56 changes: 56 additions & 0 deletions src/main/java/kdt/jpa/board/post/service/PostService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package kdt.jpa.board.post.service;

import kdt.jpa.board.global.exception.BoardException;
import kdt.jpa.board.post.domain.Post;
import kdt.jpa.board.post.repository.PostRepository;
import kdt.jpa.board.post.service.dto.request.CreatePostRequest;
import kdt.jpa.board.post.service.dto.request.EditPostRequest;
import kdt.jpa.board.post.service.dto.response.PostListResponse;
import kdt.jpa.board.post.service.dto.response.PostResponse;
import kdt.jpa.board.post.service.utils.PostMapper;
import kdt.jpa.board.user.domain.User;
import kdt.jpa.board.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class PostService {

private final PostRepository postRepository;
private final UserRepository userRepository;

@Transactional
public Long createPost(CreatePostRequest request) {
User user = userRepository.findById(request.userId())
.orElseThrow(() -> new BoardException("존재하지 않는 회원입니다"));
Post post = new Post(request.title(), request.content(), user);

return postRepository.save(post).getId();
}

@Transactional(readOnly = true)
public PostResponse getById(long id) {
Post post = postRepository.findById(id)
.orElseThrow(() -> new BoardException("존재하지 않는 포스트입니다"));
return PostMapper.toPostResponse(post);
}

@Transactional(readOnly = true)
public PostListResponse getPosts(Pageable pageable) {
Page<Post> pagedPost = postRepository.findAll(pageable);
return PostMapper.toPostListResponse(pagedPost);
}

@Transactional
public boolean editPost(EditPostRequest request) {
Post post = postRepository.findById(request.postId())
.orElseThrow(() -> new BoardException("존재하지 않는 포스트입니다"));

post.edit(request.title(), request.content());
return true;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

질문) 항상 return true를 반환하는 거면 값을 반환할 필요 없지 않나요?
예외가 발생했을 때 잡아서 false를 반환하면 모르겠지만 true를 반환해주는 이유가 궁금합니다.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package kdt.jpa.board.post.service.dto.request;

public record CreatePostRequest(
String title,
String content,
long userId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package kdt.jpa.board.post.service.dto.request;

public record EditPostRequest (
long postId,
String title,
String content
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package kdt.jpa.board.post.service.dto.response;

import org.springframework.data.domain.Page;

public record PostListResponse(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반환은 Page<PostResponse> 인데 네이밍은 List 인 이유가 있을까요?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

페이지 안에 콘텐츠 자체는 List 라 이런식으로 네이밍을 했는데, 이런 경우는 Page 라고 명시를 해주는게 좋을까요?

Page<PostResponse> responses
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package kdt.jpa.board.post.service.dto.response;

public record PostResponse (
String title,
String content,
String userName
) {
}
21 changes: 21 additions & 0 deletions src/main/java/kdt/jpa/board/post/service/utils/PostMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package kdt.jpa.board.post.service.utils;

import kdt.jpa.board.post.domain.Post;
import kdt.jpa.board.post.service.dto.response.PostListResponse;
import kdt.jpa.board.post.service.dto.response.PostResponse;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.data.domain.Page;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class PostMapper {

public static PostResponse toPostResponse(Post post) {
return new PostResponse(post.getTitle(), post.getContent(), post.getUserName());
}

public static PostListResponse toPostListResponse(Page<Post> posts) {
Page<PostResponse> responses = posts.map(PostMapper::toPostResponse);
return new PostListResponse(responses);
}
}
29 changes: 29 additions & 0 deletions src/main/java/kdt/jpa/board/user/domain/User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package kdt.jpa.board.user.domain;

import jakarta.persistence.*;
import kdt.jpa.board.global.entity.BaseEntity;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor
@Table(name = "users")
public class User extends BaseEntity {
Comment on lines +11 to +12

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테이블 이름을 복수형으로 선언하신 이유가 있을까요???

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user 가 mysql 에서 이미 사용중인 네이밍이라 테이블 이름으로 사용이 안되는거로 알고있어서 복수형으로 선언했습니다!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 이미 사용중인 네임이였군여, 어쩐지 대부분 MEMBER, ACCOUNT 이런식으로 테이블 이름을 사용하더라고요 ㅎㅎㅎ


@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

private int age;

private String hobby;

public User(String name, int age, String hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kdt.jpa.board.user.repository;

import kdt.jpa.board.user.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
}
1 change: 0 additions & 1 deletion src/main/resources/application.properties

This file was deleted.

19 changes: 19 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/jpa_board?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul
username: root
password: 110811!!
jpa:
open-in-view: false
hibernate:
ddl-auto: create
show-sql: true
properties:
hibernate.format_sql: true
server:
servlet:
encoding:
charset: UTF-8
enabled: true
force: true