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 게시판 구현 미션 제출합니다. #278

Open
wants to merge 89 commits into
base: injae&hyeonwoo
Choose a base branch
from

Conversation

ASak1104
Copy link

@ASak1104 ASak1104 commented Nov 24, 2023

📌 과제 설명

  • SpringDataJPA 를 설정한다.
    • datasource : h2 or mysql
  • 엔티티를 구성한다
    • MappedSuperClass를 사용한다
    • 회원(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 관계이다.
    • mappedBy, orphanRemoval 등 배웠던 것을 활용
  • 게시글 Repository를 구현한다. (PostRepository)
    • Method query
  • 유저 Repository를 구현한다.
  • API를 구현한다.
    • 페이징 조회 (GET "/posts")
    • 단건 조회 (GET "/posts/{id}")
    • 게시글 작성 (POST "/posts")
    • 게시글 수정 (PUT "/posts/{id}")

👩‍💻 실행 방법

프로퍼티 설정

프로젝트 실행을 위해 maintest 하위에 있는 resources/application.yml 파일을 수정해주세요.

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: YOUR_DATABASE_URL
    username: YOUR_DB_USER_NAME
    password: YOUR_DB_PASSWORD

데이터베이스 설정

Dockerfile로 부터 이미지를 생성해주세요

$ docker build -t <IMAGE_NAME> .

아래 명령어를 통해 도커 컨테이너를 실행해주세요

$ docker run --name <CONTAINER_NAME> -e MYSQL_ROOT_PASSWORD=<PASSWORD> -e MYSQL_DATABASE=<DATABASE> -d -p <HOST_PORT>:<CONTAINER_PORT> <IMAGE_NAME>

✅ PR 포인트 & 궁금한 점

  • 다룬 내용이 적지 않은 것 같아 노션에서 확인해주시면 감사하겠습니다!
  • 가능하다면 코드 리뷰를 라이브로 진행하는 것도 좋을 것 같습니다!!
  • REST DOCS는 raw 파일로 보기 불편하기 때문에 해당 링크에서 보시는 걸 추천드립니다!!!

IjjS and others added 30 commits November 21, 2023 14:09
use that of hibernate instead of jpa
in order to avoid unexpected exception regarding of OS

Choose a reason for hiding this comment

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

PostRepository에서는 @repository annotation을 사용 안하셔서 둘 중 하나로 통일하는 게 좋을 것 같습니다.

Copy link

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.

@NoArgsConstructor(access = AccessLevel.PRIVATE) 로 private 생성자를 만드는 방법도 있어서 소개해 드릴려고요

Choose a reason for hiding this comment

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

Response로 나가는 데이터에 검증 절차를 넣는 건 어떻게 생각하시나요?
요청을 반활할 때도 저는 검증을 해주는 것도 나쁘지 않다고 생각해서요

Copy link

Choose a reason for hiding this comment

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

알 수 없는 문제나 예기치 못한 DB 조작으로부터 방지하는 차원에서 검증을 하는 것으로 생각하면 좋을까요?

Copy link
Author

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.

findAll 후 유저의 이름을 PostResponse에서 호출하게 되는데 이때 user의 정보를 가져오기 위해 쿼리가 한번 더 나가는 걸로 알고 있습니다. 이 요청 시에는 반드시 user의 이름을 가져와야 하니 한번에 join으로 호출하는 건 어떨가요?

Copy link

Choose a reason for hiding this comment

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

확실히 두번 나가는 것보단 join이라도 한번 나가는 것이 좋을 것 같습니다!!

Copy link
Author

@ASak1104 ASak1104 Nov 30, 2023

Choose a reason for hiding this comment

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

말씀해주신데로 Post 조회 후 각 Post에 대한 User를 찾기 위해 select 문이 추가로 호출되긴 합니다! 그런데 해당 쿼리는 JPA에서 자동으로 db에 날리는 쿼리라 join으로 호출하도록 커스텀 하는게 가능할 지 궁금하네요 🤔
단일 join 쿼리가 성능 상 더 빠를지도 궁금하고요!

Choose a reason for hiding this comment

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

혹시 builder 생성자를 숨기고 정적 스태택 메서드를 사용하는 방법은 어떻게 생각하시나요? 개인적으로는 생성자보다는 특정 상황에서 그에 맞는 생성자를 사용하자는 주의라 의견 남겨봤습니다.
(builder를 사용하는 순간 뭔가 서비스 메서드도 길어지는 느낌이 있어서요, 저는 생성할 때 필요한 최소한의 정보 dto 자체나 필요한 다른 정보만 넘기고 생성로직을 도메인에서만 사용하게 만드는 것을 선호합니다)

Copy link

Choose a reason for hiding this comment

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

두가지 방식의 장단점이 명확하다고 생각합니다. 롬복에서 빌더 패턴을 쉽게 제공해주니 한 번 써보자는 취지에서 써 본 것이 컸습니다. 이번 과제에 사용해보면서 장점도 느꼈고, 말씀대로 단점도 느꼈습니다.

체감했던 장점으로는

  • 도메인이 커지지 않으면서 서비스 메서드의 생성 로직이 생성자를 사용했을 때 보다 가독성이 괜찮다고 느껴졌고
  • 파라미터 순서를 실수하는 경우가 적어지겠다 느꼈습니다.

롬복과 jpa를 사용하면서 게터와 세터 사용에 대해 무뎌지게 되고 기본적으로 멤버 변수에 final을 못 붙이니 잘못하면 나중에 애플리케이션이 커지면 문제가 될 수 있겠다 싶습니다. 그래서 저도 명확하게 정적 팩토리 메서드 사용하는 것이 더 나을 때가 많은 것 같습니다!

Copy link
Author

@ASak1104 ASak1104 Nov 30, 2023

Choose a reason for hiding this comment

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

엔티티 자체가 리플렉션으로 만들어질 수 있어서 final 필드 적용이 불가하기 때문에 "롬복과 jpa" 그리고 빌더 패턴으로 인한 문제라기 보다 객체의 생성을 서비스에서 하는 것보단 엔티티에서 하는 것은 어떠한 가에 대해서 생각을 해볼 필요가 있는 것 같네요!

저는 개인적으로 엔티티가 DTO 객체의 존재 여부를 알 필요가 있을까? 라는 생각합니다! 엔티티는 각 필드에 대한 정보와 validation 정도를 포함하는 방식이 이상적인 것 같아서 서비스 로직의 코드가 길어지는 것이 엔티티 클래스의 코드가 길어지는 것 보다 훨씬 자연스러운 방향이 아닐까? 라고 생각합니다!

그리고 빌더 패턴을 사용하면 널 필드가 존재할 때 각 상황에 필요한 생성자를 모두 만들어야 되나? 아니면 전체 생성자에서 특정 필드에 null을 넣어야하나? 와 같은 고민을 해결할 수 있기 때문에 필드가 많고 상황에 따라 호출되는 생성자가 많다면 빌더 패턴을 적용하는 것이 좋은 선택지라고 생각합니다! 그리고 빌더 패턴을 사용하는 이유가 객체의 너무 많은 필드로 인해 코드 가독성을 해치는 것을 개행으로 해결하는 것이기 때문에 빌더 패턴으로 메서드가 길어지는 건 가독성과 큰 관련이 없다고 생각해요!

Choose a reason for hiding this comment

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

혹시 update 메서드에서 title과 content를 각각 수정하신 이유를 알 수 있을까요?
SRP로 해석하면 될까요?

Copy link
Author

@ASak1104 ASak1104 Nov 30, 2023

Choose a reason for hiding this comment

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

저는 개인적으로 title과 content를 동시에 수정하는 메서드가 있는 것 보다 각 필드에 대한 setter를 호출하는게 더 좋은 것 같아요!
개인적으로 가독성이 좋은 것 같고 메서드 호출할 때 필드 순서로 인한 오류도 방지할 수 있고 추후에 각각 사용할 수 있으니 범용성도 넓은 것 같고요!

Choose a reason for hiding this comment

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

생성하는 Response로직에서 생성한 데이터 정보 전체를 전달해주시는 이유를 알 수 있을까요?
프론트에서 점검하기 위한 용도인가요?

Copy link

Choose a reason for hiding this comment

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

넵! 프론트에서 원하는 정보가 있다고 가정하고 했습니다!

Copy link

@yenzip yenzip left a comment

Choose a reason for hiding this comment

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

테스트 코드가 굉장히 꼼꼼하게 작성되었네요! 👍
코드를 살펴보면서 미처 생각하지 못했던 부분에 대해 많이 배웠습니다.

private final PostRepository postRepository;
private final UserRepository userRepository;

public PostResponse create(CreatePostRequest request) {
Copy link

Choose a reason for hiding this comment

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

update에는 @transactional을 사용하셨는데, create에는 사용하지 않은 이유가 있을까요?

Copy link

Choose a reason for hiding this comment

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

update는 더티 체킹을 위해 트랜잭션이 필요하지만 create는 필요하지 않다고 판단해서 사용하지 않았습니다

Copy link
Author

Choose a reason for hiding this comment

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

게시글을 저장할 때 조회 문을 제외하면 단일 inseret 쿼리가 호출되기 때문에 따로 롤백이 필요없다고 생각했어요!

import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
Copy link

Choose a reason for hiding this comment

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

UserService에서는 @transactional을 사용하지 않은 이유가 있을까요?

Copy link

Choose a reason for hiding this comment

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

필요하지 않다고 생각해서 사용하지 않았습니다!

data:
web:
pageable:
default-page-size: 10
Copy link

Choose a reason for hiding this comment

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

application.yml에서는 기본 페이지 사이즈를 설정해주셨군요!
@PageableDefault(size = 10)을 사용하여 기본 페이지 크기를 설정해보는 것은 어떨까요?

Copy link

Choose a reason for hiding this comment

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

그것도 좋은 방법이라고 생각합니다!!

Copy link
Author

Choose a reason for hiding this comment

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

맞습니다! 코드로서 관리했을 때의 이점이 있어서 해당 어노테이션을 사용하는 것도 좋을 것 같아요!
저는 기본으로 페이징되는 사이즈를 변경하기 위해 코드를 다시 빌드할 필요는 없지 않을까 라고 생각해서 프로퍼티로 정의했습니다!

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

@Column(name = "title", nullable = false)
Copy link

Choose a reason for hiding this comment

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

title에서 글자수를 제한할 수 있는 설정을 추가해보는 것은 어떨까요?

Copy link

Choose a reason for hiding this comment

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

글자 수 제한을 이번 과제에선 고려하지 않는 방향으로 진행했는데 말씀대로 필요한 경우가 있을 수도 있다는 생각이 듭니다!

datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:33366/prod
username:

Choose a reason for hiding this comment

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

env로 관리해보면 좋을 것 같습니다.
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/${MYSQL_DATABASE}
username: ${MYSQL_USER}
password: ${MYSQL_PASSWORD}

이런 느낌으로 쓰는데 MYSQL_USER 환경변수들을 가진 env 파일을 두고 거기서 읽어오는 방식으로 바꾸고 env 파일은 git에 올리지 않으면 보안적으로 더 좋을 것 같습니다.

Choose a reason for hiding this comment

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

cmd 명령을 통해 환경변수를 받는 경우에도 위처럼 하면 yml 세팅도 따로 할 필요가 없을 것 같습니다.

private final PostService postService;

@GetMapping
public ResponseEntity<PageResponse<PostResponse>> find(Pageable pageable) {

Choose a reason for hiding this comment

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

offset, pageSize 값에 대한 validation은 필요없나요?? 현재는 컨텐츠의 총 개수를 넘는 요청이 오거나 음수의 페이지를 요청하는 데이터가 왔을 때 어떻게 동작하나요??

List<T> content
) {

public static <E> PageResponse<E> from(Page<E> page) {

Choose a reason for hiding this comment

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

Page도 추상체라서 의존성이 낮을 것 같고 반환되는 json 형태도 거의 유사할 것 같은데 PageResponse로의 변환을 해준 이유가 있을까요??

@NotNull(message = PostExceptionMessage.NULL_CONTENT)
String content,
@Positive(message = PostExceptionMessage.INVALID_USER_ID)
Long userId

Choose a reason for hiding this comment

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

Null인 경우도 validation 해주면 좋을 것 같습니다.

Choose a reason for hiding this comment

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

primitive로 id 값을 줘서 그런 거라면 dto에서도 primitive를 써도 될 것 같습니다.

import org.springframework.test.web.servlet.ResultActions;

@WebMvcTest(PostController.class)
class PostControllerTest {

Choose a reason for hiding this comment

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

개인적으론 해당 mockMvc를 이용한 테스트가 docs 테스트에서도 충분히 experct될 수 있다는 생각이 들어서 TestRestTemplate와 같은 방법들을 찾아보고 이를 이용하여 e2e 테스트를 짜보면 어떨까는 생각이 듭니다!

String content = faker.howIMetYourMother().catchPhrase();

// when
Post expected = Post.builder()

Choose a reason for hiding this comment

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

객체를 생성하는 부분들이 중복되니까 test용 util 함수로 빼서 테스트에서 사용해보면 좋을 것 같습니다.


// then
assertThat(postDetailResponse)
.hasFieldOrPropertyWithValue("id", post.id())

Choose a reason for hiding this comment

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

usingRecursiveComparision 이거 이용해보면 어떨까 생각합니다. property를 문자열로 직접 입력해야 한다는 부분이 좋게 느껴지진 않습니다.
https://ksh-coding.tistory.com/100#%F0%9F%8E%AF%203.%C2%A0%20AssertJ%EC%9D%98%20usingRecursiveComparison%20%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0-1

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

5 participants