Skip to content

이용일-sprint6#128

Open
lyi980403-arch wants to merge 30 commits intocodeit-bootcamp-spring:이용일from
lyi980403-arch:이용일

Hidden character warning

The head ref may contain hidden characters: "\uc774\uc6a9\uc77c"
Open

이용일-sprint6#128
lyi980403-arch wants to merge 30 commits intocodeit-bootcamp-spring:이용일from
lyi980403-arch:이용일

Conversation

@lyi980403-arch
Copy link
Copy Markdown
Collaborator

요구사항

기본

  • 아래와 같이 데이터베이스 환경을 설정하세요.
    데이터베이스: discodeit
    유저: discodeit_user
    패스워드: discodeit1234
  • ERD를 참고하여 DDL을 작성하고, 테이블을 생성하세요.
  • Spring Data JPA와 PostgreSQL을 위한 의존성을 추가하세요.
  • 앞서 구성한 데이터베이스에 연결하기 위한 설정값을 application.yaml 파일에 작성하세요.
  • 디버깅을 위해 SQL 로그와 관련된 설정값을 application.yaml 파일에 작성하세요.
  • 클래스 다이어그램을 참고해 도메인 모델의 공통 속성을 추상 클래스로 정의하고 상속 관계를 구현하세요.
  • JPA의 어노테이션을 활용해 createdAt, updatedAt 속성이 자동으로 설정되도록 구현하세요.
  • 클래스 다이어그램을 참고해 클래스 참조 관계를 수정하세요. 필요한 경우 생성자, update 메소드를 수정할 수 있습니다. 단, 아직 JPA Entity와 관련된 어노테이션은 작성하지 마세요.
  • ERD와 클래스 다이어그램을 토대로 연관관계 매핑 정보를 표로 정리해보세요.
  • JPA 주요 어노테이션을 활용해 ERD, 연관관계 매핑 정보를 도메인 모델에 반영해보세요.
  • ERD의 외래키 제약 조건과 연관관계 매핑 정보의 부모-자식 관계를 고려해 영속성 전이와 고아 객체를 정의하세요.
  • 기존의 Repository 인터페이스를 JPARepository로 정의하고 쿼리메소드로 대체하세요.
  • 영속성 컨텍스트의 특징에 맞추어 서비스 레이어를 수정해보세요.
  • Entity를 Controller 까지 그대로 노출했을 때 발생할 수 있는 문제점에 대해 정리해보세요.
  • 강한 결합 : 엔티티 구조 변경 = API 변경
  • 보안 문제 : password 같은 민감 정보 노출
  • 무한 참조 : 양방향 연관관계 직렬화 문제
  • Lazy Loading 문제 : LazyInitializationException 발생
  • 성능 문제 : 불필요한 데이터 조회
  • 다음의 클래스 다이어그램을 참고하여 DTO를 정의하세요.
  • Entity를 DTO로 매핑하는 로직을 책임지는 Mapper 컴포넌트를 정의해 반복되는 코드를 줄여보세요.
  • BinaryContent 엔티티는 파일의 메타 정보(fileName, size, contentType)만 표현하도록 bytes 속성을 제거하세요.
  • BinaryContent의 byte[] 데이터 저장을 담당하는 인터페이스를 설계하세요.
  • 서비스 레이어에서 기존에 BinaryContent를 저장하던 로직을 BinaryContentStorage를 활용하도록 리팩토링하세요.
  • BinaryContentController에 파일을 다운로드하는 API를 추가하고, BinaryContentStorage에 로직을 위임하세요.
  • 로컬 디스크 저장 방식으로 BinaryContentStorage 구현체를 구현하세요.
  • discodeit.storage.type 값이 local 인 경우에만 Bean으로 등록되어야 합니다.
  • 메시지 목록을 조회할 때 다음의 조건에 따라 페이지네이션 처리를 해보세요.
  • 일관된 페이지네이션 응답을 위해 제네릭을 활용해 DTO로 구현하세요.
  • Slice 또는 Page 객체로부터 DTO를 생성하는 Mapper를 구현하세요.

심화

  • N+1 문제가 발생하는 쿼리를 찾고 해결해보세요.
  • 프로덕션 환경에서는 OSIV를 비활성화하는 경우가 많습니다. 이때 서비스 레이어의 조회 메소드에서 발생할 수 있는 문제를 식별하고, 읽기 전용 트랜잭션을 활용해 문제를 해결해보세요.
  • 오프셋 페이지네이션과 커서 페이지네이션 방식의 차이에 대해 정리해보세요.
    -오프셋 페이지네이션 : 데이터의 위치(offset)를 기준으로 특정 구간을 가져오는 방식
    -커서 페이지네이션 : 마지막으로 조회한 데이터의 값(cursor)을 기준으로 다음 데이터를 가져오는 방식
  • 기존에 구현한 오프셋 페이지네이션을 커서 페이지네이션으로 리팩토링하세요.

주요 변경사항

스크린샷

image

멘토에게

  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

@lyi980403-arch lyi980403-arch requested a review from hagyutae March 12, 2026 09:44
@lyi980403-arch lyi980403-arch changed the title 이용일-sprint6(2) 이용일-sprint6 Mar 12, 2026
Copy link
Copy Markdown
Collaborator

@hagyutae hagyutae left a comment

Choose a reason for hiding this comment

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

피드백 확인하고 수정해주세요~

@Query("SELECT DISTINCT c FROM Channel c " +
"LEFT JOIN FETCH c.messages " +
"LEFT JOIN FETCH c.readStatuses")
List<Channel> findAllWithMessagesAndReadStatuses();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

두 개의 컬렉션(messagesreadStatuses)을 동시에 LEFT JOIN FETCH하고 있습니다. Hibernate에서 List 타입의 컬렉션 두 개를 동시에 FETCH JOIN하면 MultipleBagFetchException이 발생합니다.

@Query("SELECT DISTINCT c FROM Channel c " +
        "LEFT JOIN FETCH c.messages " +
        "LEFT JOIN FETCH c.readStatuses")
List<Channel> findAllWithMessagesAndReadStatuses();

해결 방법으로는 다음 중 하나를 추천합니다:

  1. 컬렉션 중 하나를 Set으로 변경
  2. 두 번의 쿼리로 분리 (첫 번째 쿼리에서 messages FETCH, 두 번째에서 readStatuses FETCH)
  3. default_batch_fetch_size 설정을 활용하여 FETCH JOIN 없이 조회 (현재 100으로 설정되어 있으므로 FETCH JOIN 제거만으로도 가능)

);


CREATE TABLE user_statuses
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

user_statuses 테이블이 DDL 파일 내에서 두 번 정의되어 있습니다.

private List<ReadStatus> readStatuses = new ArrayList<>();

@OneToMany(mappedBy = "author")
private List<Message> messages = new ArrayList<>();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

클래스 다이어그램에서 Message -> User 관계는 단방향(Message에서 User로)으로 정의되어 있으나, User 엔티티에 @OneToMany(mappedBy = "author") List<Message> messages가 추가되어 양방향으로 구현되었습니다. 마찬가지로 readStatuses도 User 쪽에 양방향으로 설정되어 있습니다.

클래스 다이어그램 기준에서 User -> Message 역방향 참조가 명시되어 있지 않으므로 messages 필드는 제거하는 것이 다이어그램 준수에 더 부합합니다. readStatuses는 cascade 처리를 위해 유지할 수 있지만, 꼭 필요한 경우가 아니라면 불필요한 양방향 관계는 피하는 것이 좋습니다.

ChannelType type,
String name,
String description,
List<UserDto> participantIds,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

클래스 다이어그램에서 ChannelDto의 participants 필드는 List<UserDto> 타입이지만, 실제 필드명이 participantIds로 되어 있습니다. ChannelMapper에서도 participantIds 타겟에 List<UserDto>를 매핑하고 있어 필드명이 혼란스럽습니다.

이런 DTO 정의가 API 스펙과 맞지 않으면, FE 동작에 문제가 생길 수 있습니다.

try {
return Files.newInputStream(filePath);
} catch (IOException e) {
e.printStackTrace();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

stack trace 출력은 부적절!

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