Conversation
|
실습 과제 수행하시느라 고생하셨습니다!
|
| this.title = title; | ||
| this.content = content; | ||
| this.createdDate = LocalDateTime.now(); | ||
| this.modifiedDate = null; |
There was a problem hiding this comment.
- LocalDateTime은 시간까지 나타내므로 변수 명은
~~Date보다는~~At이 더 적절해 보입니다. - 수정된 시기를 관리하는 변수는 보통 수정을 안했다면 null로 초기화하기 보다는 생성 시간을 넣어주는 경우가 더 많긴해요
There was a problem hiding this comment.
아 그렇군요! modifiedDate의 초기값을 null/현재시간 둘 중 무엇을 해야하나 도 고민을 했었는데, 말씀해주신 것 처럼 수정하도록 하겠습니다
| @RequestMapping("/articles") | ||
| public class ArticleController { | ||
|
|
||
| private final ArticleService articleService = new ArticleService(); |
There was a problem hiding this comment.
특별한 이유는 없습니다,,,
원래는 생성자 주입 방식을 일반적으로 사용한다고 하지만, 필드 주입을 하게 될 경우에도 동작은 되는지 확인해보고 싶어서 이렇게 했습니다..!
| model.addAttribute("name", name); | ||
| return "introduction"; | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
There was a problem hiding this comment.
개행이 별거 아니라고 생각됐었는데 중요한 이유가 있었군요..!
intellij 에서 설정할 수 있다는 것도 처음 알게 되었습니다 좋은 정보 감사합니다
|
|
||
| import com.example.bcsd.domain.Article; | ||
|
|
||
| public record ArticleCreateRequestDto( |
There was a problem hiding this comment.
DTO의 경우 이름 뒤에 DTO를 굳이 안넣으셔도 이름에 Request 또는 Response가 있으니 DTO라고 판단이 가능합니다!
There was a problem hiding this comment.
그렇군요! 다음 과제 때 수정하여 반영하도록 하겠습니다
| LocalDateTime createdDate, | ||
| LocalDateTime modifiedDate | ||
| ) { | ||
| public static ArticleResponseDto from(Article article) { |
There was a problem hiding this comment.
팩토리 메소드를 만들 때 from, of 두 개를 보통 사용하는 것으로 확인했는데,
of는 여러 개의 필드 또는 객체로 부터 dto를 생성할 때,
from 은 어떤 하나의 객체로부터 dto를 생성할 때 사용한다고 합니다.
때문에 여기서는 Article 객체 하나로부터 dto를 생성하므로, from 을 사용했습니다
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| public record ArticleResponseDto( |
There was a problem hiding this comment.
DTO를 클래스가 아닌 레코드로 지정한 이유가 궁금합니다!
그리고 레코드는 클래스와 무슨 차이가 있을까요??
There was a problem hiding this comment.
레코드는 불변 클래스에 속하는데, 매개변수에 필드 정보들이 주어지면 그를 기반으로 각각의 필드들과 그 필드들에 대한 생성자, getter 를 자동으로 생성해줍니다.
dto는 한 번 생성되면 내부 정보를 변경할 필요가 없으므로, record 로 작성하여 내부 정보들을 조회만 할 수 있도록 하였습니다.
|
|
||
| public static Long articleCount = 0L; | ||
|
|
||
| private Long id; |
There was a problem hiding this comment.
사실 이 부분에 대해서는 깊게 생각해본 적이 없었고, 단순히 Long을 사용하는 것이 관례구나 라고 생각했었습니다.
때문에 이 부분에 대해 조사해봤는데, 가장 큰 차이로는 Long은 Generic 이기 때문에 null 값을 받을 수 있고,
id는 생성하여 db에 저장하기 전에는 값이 없기 때문에 null을 저장할 수 있어야 한다고 합니다.
그리고 Long은 Generic 이기 때문에 JpaRepository 를 사용할 때 JpaRepository<Long, Article> 등으로 사용할 수 있다고 합니다.
이러한 이유로 Long 을 사용한다고 합니다
| return articles.stream() | ||
| .filter(article -> article.getId().equals(id)) | ||
| .findFirst(); |
There was a problem hiding this comment.
List형을 사용하면 특정 id를 지닌 Article을 조회하는데, N번의 반복이 필요할 것으로 보입니다!
Map 자료구조를 사용하는건 어떻게 생각하시나요?
There was a problem hiding this comment.
생각해보니 Map을 사용하는게 나을 것 같습니다
Map을 사용하면 O(1)에 원하는 데이터를 조회할 수 있기 때문에 성능 상의 이득을 볼 수 있다고 합니다
| return null; | ||
| } | ||
|
|
||
| public Article deleteById(Long id) { |
There was a problem hiding this comment.
게시글을 삭제하는데 응답으로 삭제한 게시글에 대한 데이터를 받는건 어색해보이는 것 같은데 어떻게 생각하시나요?
There was a problem hiding this comment.
어떤 게시글이 삭제되었는지에 대한 정보를 알 수 있도록 하면 좋을 것 같아서 이렇게 작성했었습니다
삭제된 게시글에 대한 정보를 반환받게 되면 이를 기록하여 어떤 게시글이 삭제됐었는지에 대한 기록들을 저장하면 좀 더 좋을 것 같다는 생각이 들었습니다
하지만 이미 삭제된 데이터를 반환해주는 코드를 다시 보니 말씀해주신대로 조금 어색한 느낌이 들기도 하는 것 같습니다
| @@ -0,0 +1,49 @@ | |||
| package com.example.bcsd.domain; | |||
There was a problem hiding this comment.
Article은 패키지 명이 domain보다 model이 더 적절해보여요!
그러면 domain, model, entity는 각각 무슨 차이가 있을까요??
There was a problem hiding this comment.
생각해보니 세 가지 모두 자주 듣긴 했었지만, 구체적으로 어떤 차이점이 있는가에 대해서는 제대로 알아보려고 하지 않았던 것 같습니다
이에 대해 다시 학습해보니, 우선 entity는 jpa에서 사용하는, db와 연결되는 객체를 의미하고, domain은 해당 객체의 업무 영역과 관련되는 모든 코드들을 의미하고, model은 어떤 domain의 특정 개념을 추상화한 객체를 의미한다고 합니다.
이에 따라, 제가 이번 과정에서 작성한 Article 클래스는 말씀해주신 것 처럼 model로 패키지명을 변경하는 것이 나을 것 같습니다
| @Table(name = "member") | ||
| @Builder | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @AllArgsConstructor |
There was a problem hiding this comment.
무분별한 객체 생성을 방지하지 위해 @NoArgsConstructor을 Protected로 지정했는데, 막상 @AllArgsConstructor는 제약이 없는 것으로 보여요!
그렇다면 무분별한 객체 생성이 가능하지 않을까요?
There was a problem hiding this comment.
말씀해주신 것도 조금 의문이 들었던 부분이었는데, 과제 수행하면서 다른 코드들을 참고했을 때 @AllArgsConstructor 뒤에는 저런 식으로 수식해주지 않는 이유가 무엇인지 궁금했습니다.
사실 AllArgsConstructor 는 Builder 활용을 위해 작성해줬었는데, AllAgrsConstructor의 accesslevel을 PRIVATE으로 해주어도 Builder가 정상적으로 작동된다는 것을 확인했습니다.
현재 상황에서는 접근 권한을 protected나 public으로 할 이유가 없어 보이는 것 같습니다. 무분별한 객체 생성을 막기 위해 AllArgsConstructor의 accessLevel 도 PRIVATE 로 하는 것이 맞는것 같습니다
There was a problem hiding this comment.
아니면 정적 팩토리 메소드를 활용하는 방법도 있을 것 같아요!
물론 이 역시 빌더 패턴을 사용하겠지만요.
| @Column(name = "name") | ||
| private String name; | ||
|
|
||
| @Column(name = "email") | ||
| private String email; | ||
|
|
||
| @Column(name = "password") | ||
| private String password; |
There was a problem hiding this comment.
보통 @Column에는 테이블에서 지정해둔 제약 조건(ex. not null, size, unique) 등을 지정해주는게 좋아요. 애플리케이션에서 만들어진 객체의 데이터가 잘못된 형식일 경우 DB로 넘어가서 발견되는 것이 아닌 애플리케이션 단에서 검증이 바로 가능하기 때문이에요.
There was a problem hiding this comment.
맞는 말씀입니다 다음 data jpa 과제에서는 칼럼마다의 제약 조건을 부여해서 강건성을 높일 수 있도록 하겠습니다
| private EntityManager em; | ||
|
|
||
| public List<Article> findAll() { | ||
| return em.createQuery("SELECT a FROM Article a", Article.class) |
There was a problem hiding this comment.
다른 곳도 마찬가지로 EntityManager에서 쿼리를 사용한 이유가 있으신가요??
There was a problem hiding this comment.
모든 데이터를 조회하는 메소드들은 저런 식으로 쿼리를 사용하는 것이 보다 직관적이고 쉬워보여서 우선 이렇게 구현했습니다
There was a problem hiding this comment.
엔티티 매니저의 find() 메소드를 호출하는게 더 쉬울 것으로 보여요!
ORM을 사용하는 이유 중 하나가 SQL 쿼리문을 작성하지 않기 위함도 포함되어 있기에 고려해보시면 좋을 듯 싶습니다
| } | ||
|
|
||
| public Article update(Long id, Article article) { | ||
| em.createQuery("UPDATE Article a SET a.title = :title, a.content = :content, a.modifiedAt = :modifiedAt WHERE a.id = :id") |
There was a problem hiding this comment.
Article article = articleRepository.findById(id)
.orElseThrow(() -> new CustomException(MEMBER_NOT_FOUND));
article.update(title, content);
이런 식으로 수정하면 괜찮을까요??
|
10주차 과제 완료했습니다.
|
|
| @OneToMany(mappedBy = "writer", orphanRemoval = true, cascade = {CascadeType.PERSIST, CascadeType.REMOVE}) | ||
| @Builder.Default | ||
| private List<Article> articles = new ArrayList<>(); |
There was a problem hiding this comment.
멤버 - 게시글 간의 관계를 양방향으로 잡은 이유가 있으신가요?
단방향으로도 충분해보이는데, 양방향 관계 매핑으로 무슨 이점을 얻으셨는지 궁금합니다!
There was a problem hiding this comment.
양방향 관계를 사용함으로써 양쪽 객체에서 서로를 조회할 수 있고, 이로 인해 Cascade 시 더 편리하다는 이점이 있는 것같습니다.
다만 다시 생각해보니, 말씀해주신 대로 단뱡항으로 개선하는 것이 더 좋을 것이라 판단됩니다.
양방향으로 매핑할 경우, 연관관계 동기화를 직접 해줘야 한다는 번거로움이 있고, 유지보수면에서도 단방향이 더 유리할 것 같습니다
| public interface ArticleRepository extends JpaRepository<Article, Long> { | ||
| List<Article> findAllByBoardId(Long boardId); | ||
|
|
||
| @Query("SELECT a FROM Article a " + "JOIN FETCH a.writer " + "JOIN FETCH a.board") |
There was a problem hiding this comment.
| @Query("SELECT a FROM Article a " + "JOIN FETCH a.writer " + "JOIN FETCH a.board") | |
| @Query(""" | |
| SELECT a | |
| FROM Article a | |
| JOIN FETCH a.writer | |
| JOIN FETCH a.board | |
| """) |
이런 식으로 자바의 텍스트 블록 기능을 활용하시면 가독성이 더 좋아질 것 같아요!
링크
There was a problem hiding this comment.
아 이런 기능도 있었군요! 참고하도록 하겠습니다 감사합니다
| @Query("SELECT a FROM Article a " + "JOIN FETCH a.writer " + "JOIN FETCH a.board") | ||
| List<Article> findAllWithWriterAndBoard(); | ||
|
|
||
| @Query("SELECT a FROM Article a " + "JOIN FETCH a.writer " + "WHERE a.board.id = :boardId") |
There was a problem hiding this comment.
모든 Repository 구현에 대한 질문입니다!
- 쿼리문을 직접 작성하신 이유가 있나요? 이러면 JDBC를 사용하는게 더 이점이 있을 것으로 보입니당.
- N+1 문제를 해결하기 위해
FETCH JOIN을 사용하셨는데, 실제로 N+1 문제가 발생해서 사용하신건가요?
There was a problem hiding this comment.
- 만약 JPA 메소드를 그대로 활용할 경우, 예를 들어 "어떤 게시글에서 특정 문자열을 포함하는 작성자의 게시글을 검색" 과 같은 복잡한 조건이 주어진다면, findByBoardIdAndWriterNameContaining() 이런 복잡한 이름의 메소드를 사용해야 했는데, 이보다는 쿼리문을 직접 사용하는 것이 더 깔끔하여 이를 사용했습니다. 그리고 후술할 N+1 문제 해결을 위한 Fetch join 사용을 위해서도 쿼리문을 직접 작성한 부분이 있습니다.
- 사실 직접 N+1 문제를 확인해보지는 않았습니다...! 특정 회원이나 특정 게시판 등 단일 객체 만을 조회하려고 할 때, entity 내부에 있는 List articles 등과 같은 필드 때문에 함께 조회하게 되는 것 아닌가? 하는 의심에 예방 차원에서 작성한 느낌이 있습니다..
| Member writer = memberRepository.findById(requestDto.writerId()) | ||
| .orElseThrow(() -> new CustomException(ErrorCode.INVALID_MEMBER_REFERENCE)); |
There was a problem hiding this comment.
이런 식으로 매 번 조회 후 Optional 내부 값의 존재 유무로 예외를 발생시키는 로직이 중복되는 것 같아요!
따로 분리를 하는건 어떻게 생각하시나요?
저는 인터페이스에서 default 메소드로 getBy~~를 만든 뒤 해당 로직을 분리하는 것을 선호하는 편입니다.
There was a problem hiding this comment.
저도 코드를 보면서 이 부분이 조금 겹친다고 생각되긴 했었는데, 말씀해주신대로 이 부분을 분리하는 것이 좀더 깔끔해보일 것 같습니다! 후에 반영해보도록 하겠습니다
| import java.util.List; | ||
|
|
||
| @Service | ||
| public class BoardService { |
There was a problem hiding this comment.
조회하는 로직을 위한 쿼리 서비스 라는 개념이 있다고해요!
굳이 적용은 안하셔도 이런 것도 있구나 참고 정도만 해두시면 좋을 듯 싶어요!
링크
|
11주차 보안 과제 완료했습니다.
|
|
|
추가로 유저가 로그아웃을 한 경우에는 발급받은 토큰을 어떻게 해야할 지 고민해보셔도 좋을 것 같아요! |
5주차 Hello SpringBoot 과제 완료했습니다.
이번 과제 수행 중에 고민했던 부분들에 대해 말씀드리겠습니다.