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

[4기 고예성] Spring Boot JPA로 게시판 구현 미션 제출합니다. #254

Open
wants to merge 11 commits into
base: Dev-Yesung/springboot-jpa-board
Choose a base branch
from

Conversation

Dev-Yesung
Copy link
Member

@Dev-Yesung Dev-Yesung commented Aug 5, 2023

📌 과제 설명

JPA로 게시판을 구현하고 API를 문서화를 했습니다.

👩‍💻 요구 사항과 구현 내용

  • SpringDataJPA 를 설정한다
    datasource : h2

  • 엔티티를 구성한다

    • 회원(User)
      - id (PK) (auto increment)
      - name
      - age
      - hobby
      - created_at
      - created_by
    • 게시글(Post)
      - id (PK) (auto increment)
      - title
      - content
      - created_at
      - created_by
  • 회원과 게시글에 대한 연관관계를 설정한다.

    • 회원과 게시글은 1:N 관계이다.
  • 게시글 Repository를 구현한다. (PostRepository)

  • API를 구현한다.
    - 게시글 조회
    - 페이징 조회 (GET "/posts")
    - 단건 조회 (GET "/posts/{id}")
    - 게시글 작성 (POST "/posts")
    - 게시글 수정 (POST "/posts/{id}")

  • REST-DOCS를 이용해서 문서화한다

✅ PR 포인트 & 궁금한 점

springboot-board-jpa setup
springboot-board-jpa setup
User, Post Entity 구현완료
gitignore 수정
JPA게시판 기능구현을 모두 완료했습니다.
게시판 테스트 코드 구현 완료 및 asciidoctor 적용
@Dev-Yesung Dev-Yesung changed the title [4기 공] Spring Boot JPA로 게시판 구현 미션 제출합니다. [4기 고예성] Spring Boot JPA로 게시판 구현 미션 제출합니다. Aug 5, 2023
@Dev-Yesung Dev-Yesung marked this pull request as draft August 5, 2023 14:23
@Dev-Yesung Dev-Yesung self-assigned this Aug 5, 2023
@Dev-Yesung Dev-Yesung marked this pull request as ready for review August 5, 2023 14:27
Copy link
Member

@kimihiqq kimihiqq left a comment

Choose a reason for hiding this comment

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

예성님 이번과제도 수고많으셨습니다~! 😁😁
예성님 코드 보면서 제가 미쳐 생각하지 못한 부분들이 많아 보면서 많이 배웠습니다!👍
잘 작성해주셔서 리뷰드릴부분이 많이 없었지만 몇가지 남겨 보았습니다!!
(혹시 리뷰 중 잘못된 부분이 있다면 지적해주시면 감사하겠습니다!)

Comment on lines +6 to +9
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
Copy link
Member

Choose a reason for hiding this comment

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

저 같은 경우엔 @EnableJpaAuditing을 메인 클래스에 직접 적용하였는데, 이렇게 별도의 설정 클래스를 사용하면 구조상의 구분이나 가독성을 높일 수 있겠군요!!

import java.util.Optional;

@Service
@Transactional(readOnly = true)
Copy link
Member

Choose a reason for hiding this comment

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

읽기 전용 트랜잭션을 기본으로 하시고 쓰기 작업이 필요한 메서드에 대해서만 @transactional을 재적용하셨군요! (저도 참고하겠습니다!)

Copy link
Member Author

Choose a reason for hiding this comment

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

제가 readOnly 관련해서 공부할 때 찾았던 글입니다~
https://hungseong.tistory.com/74
특히 JPA를 사용할 때 얻을 수 있는 이점이 많은 것 같습니다!

Comment on lines 44 to 45
@PostMapping("/{id}")
public ResponseEntity<PostUpdateResponse> updatePost(
Copy link
Member

Choose a reason for hiding this comment

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

리소스를 수정하는 경우 PUT 메서드가 더 적합할 것 같습니다! (혹시 PostMapping을 사용하신 다른 이유가 있으실까요??)

Copy link
Member Author

Choose a reason for hiding this comment

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

앗 요구사항에 POST "/posts/{id}"로 구현하라고 나와있어서 이렇게 구현했습니다.
하지만 저도 구현하면서 PUT이 더 적절하다고 생각했는데~ 바로 고치겠습니다.
감사합니다☺️

Comment on lines 41 to 42
@Column(nullable = false)
private Integer age;
Copy link
Member

Choose a reason for hiding this comment

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

저는 null을 가질 수 없는 int를 사용하는 것이 db의 not null 제약 조건과 더욱 분명한 일치가 이루어지지 않을까 생각했습니다! 혹시 age필드에 Integer를 사용하신 이유가 있으실까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

아아 맞습니다. 사실 객체로 만든게 습관적으로 예전에 원시값을 포장해서 써야한다는 글을 읽은 적 있었는데,
은연중에 큰 이유없이 코딩했었네요...! 성현님이 주신 리뷰를 읽고 생각해본 다음 아래의 글과 함께 학습했습니다!
예리한 지적 감사합니다~🫠 반영하겠습니다

https://warpgate3.tistory.com/m/entry/JPA-ENTITY-%EC%97%90%EC%84%9C-%EC%9B%90%EC%8B%9C%ED%83%80%EC%9E%85%EA%B3%BC-Wrapper-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%A4%91-%EB%AD%98-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%EB%90%98%EB%82%98

null값이 필요없는 필드이기 때문에 원시타입을 사용하는 것이 더 적절해서입니다.
좀 더 캡슐화가 되는 방향으로 DTO의 메서드를 변경했습니다.

BREAKING CHANGE: 서
Comment on lines +6 to +16
@Getter
public class IllegalPostDataException extends RuntimeException {

private final ErrorCode errorCode;
private final String className;

public IllegalPostDataException(ErrorCode errorCode, String className) {
this.errorCode = errorCode;
this.className = className;
}
}

Choose a reason for hiding this comment

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

@Getter
public class PostNotFoundException extends RuntimeException {
    private final ErrorCode errorCode;
    private final String className;

    public PostNotFoundException(ErrorCode errorCode, String className) {
        this.errorCode = errorCode;
        this.className = className;
    }
}
private final ErrorCode errorCode;
private final String className;

이 두 필드가 동일하게 계속 들어갈 것 같은데, 이렇게할거면 부모 예외를 만들고 상속 관계를 활용하는게 더 적절할 것 같습니다. :)

Copy link
Member Author

Choose a reason for hiding this comment

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

앗... ㅎㅎ 맞습니다 리팩토링하도록 하겠습니다!

Comment on lines +74 to +93
private void validateTitle(String title) {
if (!StringUtils.hasText(title) ||
title.length() > MAXIMUM_LENGTH) {
throw new IllegalPostDataException(ErrorCode.EMPTY_POST_TITLE, className);
}
}

private void validateContent(String content) {
if (!StringUtils.hasText(content)) {
throw new IllegalPostDataException(ErrorCode.EMPTY_POST_CONTENT, className);
}
}

private void updateCreatedBy(String createdBy) {
if (!StringUtils.hasText(createdBy) ||
createdBy.length() > MAXIMUM_LENGTH) {
throw new IllegalPostDataException(ErrorCode.INVALID_CREATED_BY_VALUE, className);
}
this.createdBy = createdBy;
}

Choose a reason for hiding this comment

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

예외를 던질 때 className를 같이 넘겨주는게 큰 의미가 있나용?!
로그를 찍기 위한 의도라던지..

Copy link
Member Author

Choose a reason for hiding this comment

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

로그찍을 용도로 만든거였는데, 생각해보니 로그에 클래스이름이 들어갈 필요는 없네요🧐
좀 더 의미있는 데이터로 바꿔보겠습니다...!

Comment on lines +63 to +72
public void updateUser(User user) {
if (Objects.nonNull(this.user)) {
this.user.getPosts().remove(this);
}

this.user = user;
user.getPosts().add(this);
String name = user.getName();
updateCreatedBy(name);
}

Choose a reason for hiding this comment

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

일반적으로 게시글(Post)의 유저를 다른 user로 바꾸는게 적절한건가요?

    @Transactional
    public PostSaveResponse savePost(PostSaveRequest request) {
        Post newPost = request.toEntity();

        Long userId = request.userId();
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new UserNotFoundException(
                        ErrorCode.USER_NOT_FOUND,
                        this.getClass().getName()
                ));
        newPost.updateUser(user);

        postRepository.save(newPost);

        return PostSaveResponse.of(newPost);
    }

물론 여기서 Post를 생성할 때 사용하고 있긴한데, updateUser를 따로 호출해서 넣어줘야하는건지...
지금 이 코드는 마치 User를 새로 업데이트 해줄 수 있는것처럼 보입니다.
그리고

    @Column(nullable = false, length = 50)
    private String createdBy;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

이렇게 게시글을 작성한 유저의 Name과 User에 대한 연관 관계를 둘다 가지고 있어서 더 혼란스러운 것 같아요.

Copy link
Member Author

Choose a reason for hiding this comment

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

아아 이해했습니다. update라는 메서드 자체도 유저를 변경해준다는 의미가 강하고
애초에 updateUser란 메서드도 Post에 있어 어색하네요...!
애초에 그 글을 작성하는 사람이 바뀔 이유가 없고요~
User를 Post를 생성할 때 주입하는 것으로 바꾸겠습니다.

createdBy는 요구사항에 있어서 그냥 집어넣은거 같은데 확실히 어색하네요!
연관관계만 집어넣고 삭제하는 걸로 하겠습니다~

+궁금한 점 : 원래는 updateUser가 아니라 setUser로 메서드를 변경하려고 했는데,
혹시 User를 Post메서드에 setUser(user)와 같은 방식으로 설정하는 것도
그렇게 바람직한 방식은 아닌건가요?!

Comment on lines +69 to +82
private void validateAge(Integer age) {
if (age < MIN_AGE || age > MAX_AGE) {
throw new IllegalUserDataException(ErrorCode.INVALID_USER_AGE);
}
}

private void validateName(String name) {
if (!StringUtils.hasText(name) ||
name.length() > MAXIMUM_LENGTH ||
!Pattern.matches(NAME_REGEX_PATTERN, name)
) {
throw new IllegalUserDataException(ErrorCode.INVALID_USER_AGE);
}
}

Choose a reason for hiding this comment

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

Post와는 다르게 User는 또 클래스 이름을 안넘겨주네요? ㅎㅎ

Copy link
Member Author

Choose a reason for hiding this comment

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

함께 리팩토링 하도록 하겠습니다!

Comment on lines +66 to +69
String postTitle = request.title();
String postContent = request.content();
post.updateTitle(postTitle);
post.updateContent(postContent);

Choose a reason for hiding this comment

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

title과 content를 함께 수정하는 메서드를 만드는게 더 낫지 않을까요 ㅎㅎ
이 둘이 따로따로 사용될 상황이 없다면..

Copy link
Member Author

Choose a reason for hiding this comment

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

앗 맞습니다...! 게시판 글을 수정하면 보통 둘 다 수정할 수 있도록 만드는데~
함께 수정하도록 하는게, 효율적이겠네요! 감사합니다

Comment on lines +61 to +74
@Test
@DisplayName("새 Post 생성 실패 테스트")
void savePostFailTest() {
// given
var request = new PostSaveRequest(
500L,
"이거 저장안됨ㅎ",
"제곧내 저장 안됨~"
);

// when, then
assertThatThrownBy(() -> postService.savePost(request))
.isInstanceOf(UserNotFoundException.class);
}

Choose a reason for hiding this comment

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

유저가 늘어나서 500번 유저가 생기면 어쩌죠?!

Copy link
Member Author

Choose a reason for hiding this comment

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

앗 맞습니다 아예 적절하지 않은 값을 넣어야하네요! 감사합니다~

import org.springframework.data.jpa.repository.JpaRepository;

public interface PostRepository extends JpaRepository<Post, Long> {
Page<Post> findAll(Pageable pageable);

Choose a reason for hiding this comment

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

이거 원래 정의되어 있지 않나요?

Copy link
Member Author

Choose a reason for hiding this comment

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

맞습니다! JPA에 익숙하지 않아서 몰랐는데, 원래 있더군요. 바로 삭제했습니다~

Comment on lines +49 to +54
@OneToMany(
mappedBy = "user",
cascade = CascadeType.ALL,
orphanRemoval = true
)
List<Post> posts = new ArrayList<>();
Copy link

@maenguin maenguin Aug 10, 2023

Choose a reason for hiding this comment

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

포스트가 유저에 전적으로 의존적이라면 해당 코드가 유의미할 수 있는데 아니라면 조금 위험해보이네요
유저가 삭제되면 유저가 작성한 모든 포스트도 같이 삭제될것 같은데.. 상황에 따라 조금 고민이 필요해보이지만.. 지금은 괜찮을것 같네요

Copy link
Member Author

Choose a reason for hiding this comment

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

넵 맹희님이 피드백 해주신 것을 토대로 한 번 생각해보았습니다.
서비스가 확장되면 문제가 생길거 같아 삭제하는게 낫다는 생각이 들었습니다.
또 굳이 양방향 연관관계로 조회를 할 필요도 없을거 같아 삭제했습니다!

Comment on lines +16 to +17
@RestController
@RequestMapping("api/v1")

Choose a reason for hiding this comment

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

여기도 뒤에 users를 붙여주시면 좋을것 같네요

Comment on lines +24 to +26
@SpringBootTest
@Transactional
class PostServiceTest {

Choose a reason for hiding this comment

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

슬라이스 테스트로 한번 바꿔보세요
Auditing 때문에 조금 시행착오는 있을거에요 ㅋㅋ

Copy link
Member Author

Choose a reason for hiding this comment

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

시도해보겠습니다! 감사합니다 :)

Comment on lines +40 to +44
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Transactional
class PostControllerTest {

Choose a reason for hiding this comment

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

현재 레스트 도큐먼트 구성 목적으로만 테스트 코드가 존재해서
@AutoConfigureRestDocs
@WebMvcTest(PostController.class)
위 코드만으로 될것 같아요(아마도?)

추가로, 실제 응답을 검증하는 테스트는 없는것 같은데요 이번에 한번 작성해보시면 좋을것 같네요

Copy link
Member Author

Choose a reason for hiding this comment

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

테스트 작성을 슬라이스 테스트로 작성하지 않아서 전체적으로 코드를 바꿔봐야겠네요!
실제 응답 검증 테스트도 진행해보겠습니다~ 감사합니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants