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

[Seminar3] 필수 과제 구현 #10

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open

Conversation

jjuny-won
Copy link
Collaborator

@jjuny-won jjuny-won commented Oct 31, 2024

🔥 Pull requests

⛳️ 작업한 브랜치

👷 작업한 내용

  • Diary CRUD 수정
  • 로그인, 회원가입 구현
  • 패키지 구조 변경
  • Diaries 분리
  • 예외 처리
  • (선택) 페이징 처리

Entity와 도메인의 차이

  • VO: 단순히 데이터 값을 전달하기 위한 용도로 사용되는 객체입니다.
    • DTO와 혼용해서 사용되는 경우가 있으며, 데이터 전송 목적으로만 사용될 때는 DTO와 의미가 유사합니다.
    • 도메인 주도 설계(DDD)에서는 식별자가 필요 없는 불변의 객체로 사용되며, 값 자체가 중요한 경우에 활용됩니다.
  • DTO: 클라이언트와 서버 간 데이터 전송을 위해 설계된 객체입니다.
    • 컨트롤러와 서비스 계층 사이, 또는 서비스 계층과 리포지토리 계층 사이의 데이터 교환을 명확하게 하기 위해 필수적입니다.
    • 비즈니스 로직을 포함하지 않고 순수한 데이터 전달 목적으로 사용됩니다.
  • Repository: 엔티티 객체를 보관하고 관리하는 저장소로, 엔티티의 생명 주기를 관리하며 데이터를 조회하거나 저장하는 역할을 합니다.
    • 도메인 주도 설계(DDD)에서는 엔티티와 값 객체(VO)를 저장하고 관리하는 저장소로 정의됩니다.
    • 실무에서는 DAO와 구분 없이 Repository가 사용되기도 합니다.
  • DAO: 데이터베이스 접근과 관련된 로직을 모아둔 객체로, 데이터베이스와의 CRUD 작업을 수행합니다.
  • Entity: 데이터베이스에 저장되는 객체로, 보통 고유 식별자(ID)를 가지고 하나의 테이블 레코드와 매핑됩니다.
    • 엔티티는 도메인 모델에서 주요 개념을 표현하는 역할을 합니다.
  • Domain: 비즈니스 로직을 캡슐화하고 상태를 관리하는 역할을 합니다.
    • 엔티티와 값 객체(VO)를 포함합니다.
    • 유효성 검사, 상태 관리, 비즈니스 규칙의 실행 등 비즈니스 로직을 포함하고 있으며, 데이터 변경 시 해당 규칙을 관리하고 적용하는 역할을 합니다.

패키지 구조

  1. 계층형: 각 계층을 대표하는 디렉터리를 기준으로 만들어집니다.

    • 장점: 해당 프로젝트에 대한 이해도가 낮아도 전체적인 구조를 빠르게 파악할 수 있습니다.
    • 단점: 디렉터리에 클래스들이 너무 많이 모이게 됩니다.
  2. 도메인형: 도메인 디렉터리를 기준으로 구분합니다.

    • 장점: 관련된 코드가 응집되어 있습니다.
    • 단점: 프로젝트 이해도가 낮을 경우 전체적인 구조 파악이 어렵습니다.

제가 선택한 방법은 계층형입니다. 그 이유는, 도메인으로 나눌 만큼 구조가 복잡하지 않아서 계층형으로도 충분하다고 생각했기 때문입니다. 제가 생각한 패키지 및 클래스 구조는 아래와 같습니다.
Screenshot 2024-10-31 at 3 11 17 AM
Screenshot 2024-11-03 at 7 01 04 PM

DTO를 사용한다고 했을 때, RequestResponse는 어떻게 구분해야 할까요? RequestDTO, ResponseDTO도 DTO의 일종입니다. 제가 이해한 DTO는 엔티티가 외부에 노출되지 않도록 하고, 역할을 명확하게 분리하기 위해 Controller와 Service 사이에서 중간 다리 역할을 해주는 객체입니다. 따라서 세 가지를 따로 구분할지 고민하던 중, DTO를 InnerCase로 관리하는 방법을 알게 되었습니다. DTO를 Response, Request 마다 생성하면 너무 많아질 수 있지만, InnerCase로 관리하면 이를 줄일 수 있습니다. 그러나 코드의 가독성을 위해 최종적으로는 따로 분리하여 구현했습니다.

목록 구현 방법

  • 전체 목록 조회 시, 상세 조회 시 내가 쓴 일기라면 isPrivate이어도 보이도록 구현했습니다. 처음에는 이러한 조건이 없어서 JPQL과 Spring Data JPA를 사용해 구현했으나, 코드의 가독성이 떨어졌고 더 효율적인 구현 방법을 찾고자 UML 다이어그램을 작성하여 구체화했습니다.
Screenshot 2024-11-03 at 1 34 29 AM

목록 조회를 할 때 생각해 본 방안은 세 가지입니다. 최종적으로 선택한 것은 3번 방법입니다.

  1. JPA 메서드를 사용하여 세부적으로 필터링해서 가져오기 → 메서드가 너무 많아지고 코드가 복잡해짐
  2. findAll()로 가져와서 서비스 레이어에서 필터링 및 정리하기 → 성능 이슈 발생
  3. 최소한의 조건을 사용하여 나누고, 필터링 및 정리는 Stream을 사용하여 구현하기 → 3개의 메서드 + Stream을 통한 구현
  • /my로 들어오는 요청 → findByUserId
  • /로 들어오지만 userId가 없는 요청 (유저의 일기인지 필터링할 필요 없음) → findAllByIsPrivateFalse
  • /로 들어오는데 userId가 있는 요청 (유저의 일기인지 필터링할 필요 있음) → findByUserIdOrIsPrivateFalse

그 외에 공통적으로 처리해야 하는 sortType 관련 부분은 Comparator를 이용해 처리하고, limit() 함수를 사용해 개수를 제한하며, filter()를 통해 카테고리를 필터링했습니다.

Comparator, Comparable

객체를 비교하는 방법으로 두 가지 인터페이스인 ComparatorComparable을 사용할 수 있습니다. 이 두 인터페이스의 가장 큰 차이는 자기 자신과 비교할 수 있는지 여부입니다.

Comparable

  • 자기 자신과 매개변수 객체를 비교하는 기능을 제공합니다.
  • compareTo(T o) 메서드를 사용합니다.
  • ClassName.compareTo(o)로 호출할 수 있으며, 들어온 파라미터 o가 비교할 객체가 됩니다.
  • 자기 자신을 기준으로 상대 객체와의 차이를 반환합니다.

Comparator

  • 두 매개변수 객체를 비교하는 기능을 제공합니다.
  • compare(T o1, T o2) 메서드를 사용합니다.
  • java.util 패키지에 포함되어 있으므로 import가 필요합니다.

🚨 참고 사항

노션 페이지

  • @Transactional에 대한 개념이 아직 완전히 잡히지 않아서 추가적으로 공부가 필요할 것 같습니다. 이 부분에 대해 조금 더 피드백을 주시면 감사하겠습니다
  • Exception 부분도 추가로 구현을 해보았습니다. @Valid 에 대한 따로 처리를 하는 Exception class 도 찾았는데 @NotNull 이 어떤 레이어에서 처리해주는지에 대한 확인이 다시 필요할 것 같습니다!
  • 단방향적으로 구현했는지에 대한 리뷰도 해주세용

Copy link
Member

@gahyuun gahyuun left a comment

Choose a reason for hiding this comment

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

고생하셨습니다! 어떤 구조로 가져갈지 고민하시고 잘 분리하신 것 같아 많이 배울 수 있었습니다 🤗

Copy link
Member

Choose a reason for hiding this comment

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

이런 enum은 어디에 분리할지 고민을 했었는데 entity에 분리하셨군요!!

diaryRepository.save(
new DiaryEntity(diaryRequest.title(),
diaryRequest.content(),
diaryRequest.category(),
Copy link
Member

Choose a reason for hiding this comment

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

diaryRequest의 category가 Category타입이면 all도 category로 저장될 수 있을 것 같은데 이 부분에 대해선 의도하신걸까요!?

import java.util.List;
import java.util.stream.Collectors;

@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에 readOnly true가 어떤 역할인가요!?

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Component
Copy link
Member

Choose a reason for hiding this comment

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

DiaryFormatter에 Component 어노테이션을 붙인 이유가 있을까요!?

}


public DiaryListRes getDiariesResponse(Category category, SortType sortType, boolean isMine, Long userId) {
Copy link
Member

Choose a reason for hiding this comment

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

세미나 시간에 getDiariesResponse가 if else가 너무 많다고 하ㅅ셨는데, /diaries/my 와 /diaries에서 둘다 호출되고 있는 경우때문에 그렇군용!
두 api에서 하나의 함수를 사용하면 중복되는 코드가 줄여지는 장점이 있겠지만, 코드가 너무 길어져서 함수의 역할이 많아질 수도 있지 않을까요?!


@GetMapping("/my")
public ResponseEntity<DiaryListRes> getMyDiaries(
@Valid @RequestHeader("userId") long userId,
Copy link
Member

Choose a reason for hiding this comment

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

혹시 존재하지 않는 유저 아이디가 들어왔을 때는 어떻게 처리하나용?

import java.util.Optional;

public interface UserRepository extends JpaRepository<UserEntity,Long> {
Optional<UserEntity> findByLoginIdAndPassword(String loginId, String password);
Copy link
Member

Choose a reason for hiding this comment

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

저는 findByLoginId 이후 해당 user의 비번을 비교하는 과정을 거쳤는데 해당 메서드를 사용하는게 훨 깔끔할 것 같네요 !! 👍🏻

BAD_REQUEST(HttpStatus.BAD_REQUEST, "USER004", "로그인 정보가 올바르지 않습니다."),

//diary
INVALID_INPUT_LENGTH(HttpStatus.LENGTH_REQUIRED, "diary001", "글자수를 다시 확인해주세요"),
Copy link
Member

Choose a reason for hiding this comment

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

title이나 content의 글자수는 특정 필드의 규칙이므로 400 error가 좀 더 적합하지 않을까요?! 저도 411 Error에 대해서 잘 알진 못하지만 Content-Length header가 잘못되었을 때 보내는 에러 같아서요!
https://www.holisticseo.digital/technical-seo/status-code/400s/411/

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

Successfully merging this pull request may close these issues.

2 participants