Skip to content

[김은애] sprint6#126

Open
Color-Marker wants to merge 37 commits intocodeit-bootcamp-spring:김은애from
Color-Marker:김은애-sprint6

Hidden character warning

The head ref may contain hidden characters: "\uae40\uc740\uc560-sprint6"
Open

[김은애] sprint6#126
Color-Marker wants to merge 37 commits intocodeit-bootcamp-spring:김은애from
Color-Marker:김은애-sprint6

Conversation

@Color-Marker
Copy link
Copy Markdown
Collaborator

요구사항

기본

  • 전부 완료

심화

  • 전부 완료

관계 매핑 정보

엔티티 관계 다중성 방향성 부모-자식 관계 연관관계의 주인
User : BianaryContent 1:1 User -> BinaryContent 부모: BinaryContent, 자식: User User
User : UserStatus 1:1 UserStatus -> User 부모: User, 자식: UserStatus UserStatus
Channel : ReadStatus 1:N ReadStatus -> Channel 부모: Channel, 자식: ReadStatus ReadStatus
User : ReadStatus 1:N ReadStatus -> User 부모: User, 자식: ReadStatus ReadStatus
Channel : Message 1:N Message -> Channel 부모: Channel, 자식: Message Message
User : Message 1:N Message -> User 부모: User, 자식: Message Message
BinaryContent : Attachment 1:N Attachment -> BinaryContent 부모: BinaryContent, 자식: Attachment Attachment
Message : Attachment 1:N Attachment -> Message 부모: Message, 자식: Attachment Attachment

오프셋 페이지네이션 vs. 커서 페이지네이션

일단 페이지네이션이라고 하면, 특정 정렬 기준과 필요 개수 조건에 맞춰 데이터를 가져오는 것이다.
기본 부분에서는 slice를 이용한 오프셋 페이지네이션을 이용하고,
심화 부분에서는 프론트에 cursor 관련 변수가 추가되어 커서 페이지네이션을 이용하게 리팩토링한다.

오프셋 커서
페이지 단위로 구분하여 요청/응답하게 구현하는 방식 클라이언트가 가져간 마지막 row의 순서상 다음 row들을 n개 요청/응답하게 구현
쉽게 말해 무조건 내부에서 미리 결정되어 있는 단위를 기준으로 잘라서 보내줌 보낸 데이터의 다음 데이터부터 보내주기 위한 구현이 되어있음. 그래서 어디서부터 보내줘야 하는지를 나타내는 지표로 cursor를 이용하는 방식
무조건 정해진 크기로 잘라서 보내줌 여기 다음부터 몇 개 주세요가 가능 -> 무한 스크롤 기능 구현 가능!
정해진 크기로 보내면 되니 구현이 간단함 cursor가 어딘지 확인해서 그 다음부터 n개 보내줘야 해서 오프셋 방식보다는 복잡함

주요 변경사항

  • n+1 문제를 해결하기 위해 메시지 조회 시 entityGraph에 attachments도 원래 추가해두었으나, attachments가 리스트 형식이다 보니 쿼리 길이가 너무
    길어지는 상황이 발생함.
  • 이에 이를 해결하기 위해 entityGraph에서 attachments는 제외하고 yaml에 batch 사이즈를 설정하는 방식으로 전환.
  • batch 크기는 페이지네이션 사이즈인 50으로 같게 설정.

멘토에게

  • n+1 문제가 전부 잘 해결된 것인지 궁금합니다.
  • 또한 페이지네이션 방식 중 페이지 방식을 이용할 때 오프셋 형식을 이용하고, slice를 이용할 시 커서 방식을 이용하면 되는 건지 궁금합니다.

우선 기본적으로 entitygraph 이용하여 user, message, readstatus,
userstatus 레포지토리에 적용함.
그런데 message에서 attachment는 리스트 형식이기에 entitygraph를 이용 시
쿼리가 너무 길어지는 상황이 발생.
이에 해결 방법을 찾아보다가 batch 라는 걸 알게 되었고 이를 yaml에 적용.
배치 사이즈는 message의 pageable 사이즈인 50에 맞췄습니다.
@Color-Marker Color-Marker requested a review from eedys1234 March 11, 2026 05:01
@eedys1234
Copy link
Copy Markdown
Collaborator

eedys1234 commented Mar 17, 2026

페이지네이션 방식 중 페이지 방식을 이용할 때 오프셋 형식을 이용하고, slice를 이용할 시 커서 방식을 이용하면 되는 건지 궁금합니다.

UI 구조상 페이지를 활용하는 경우 오프셋 방식을 사용합니다. Slice 이용도 내부적으로는 오프셋 형식을 사용하며 전체 수를 측정하는 쿼리만 제외되었다고 생각하시면 좋을것 같습니다.

@@ -0,0 +1,5 @@
package com.sprint.mission.discodeit.config;

public class SwaggerConfig {
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.

Suggested change
public class SwaggerConfig {
@Configuration
public class SwaggerConfig {

설정 클래스임을 표기하는 @configuration 애노테이션 붙여주는게 좋을것 같아요.


@Tag(name = "Binary Content API")
@RestController
@RequestMapping("/api/binaryContents")
Copy link
Copy Markdown
Collaborator

@eedys1234 eedys1234 Mar 17, 2026

Choose a reason for hiding this comment

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

요구사항에는 CamelCase 이네요..

가급적 현업에서는 URL에 CamelCase 보다는 kebab-case 사용 권장드려요~!

참고자료

}

private final BinaryContentService binaryContentService;
private final BinaryContentStorage binaryContentStorage;
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.

BinaryContentController -> BinaryContentService -> BinaryContentStorage 순으로 참조하는건 어떠실까요?

BinaryContentService는 BinaryContent에 관련된 서비스를 하는 객체를 의미하기도 해서요.

@Parameter(description = "채널 정보")
@RequestBody PublicChannelCreateRequest dto) {
Channel channel = channelService.create(dto);
ChannelDto result = channelService.find(channel.getId());
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.

혹시 Channel을 생성하고 생성된 ID를 통해 재조회한 이유가 있을까요?

생성된 Entity 객체를 Mapper를 활용하여 Dto 변환처리하는게 조회 쿼리를 1번 수행하지 않을 것 같아서요.

@Parameter(description = "채널 ID")
@RequestParam UUID channelId,
@RequestParam(required = false) Instant cursor,
@PageableDefault(size = 50, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
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.

👍👍👍

private UUID authorId;
//
private List<UUID> attachmentIds;
@Column(columnDefinition = "TEXT")
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.

columnDefinition은 DB에 종속적이라 @lob 사용을 권장드려요.

@lob을 사용한다면 JPA가 DB에 맞는 알맞는 데이터 형식으로 자동 매핑합니다.

this.updatedAt = Instant.now();
}
public void update(String newContent) {
if (newContent != null && !newContent.equals(this.content)) {
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.

Objects.equals(newContent, this.content) 사용하면 어떨까요?!?
내부적으로 NPE 없이 안전하게 비교해줍니다.


@Query("Select c FROM Channel c "
+ "WHERE c.type = 'PUBLIC' "
+ "OR c.id IN :subscribedIds")
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.

OR보다는 UNION을 사용해주세요.
OR의 경우 인덱스를 사용하지 않는 경우가 많습니다.

private User author;
//
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
@JoinColumn(name = "message_id")
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.

binary_contents 테이블에 외래키인 message_id가 존재하지 않나요?

binary_contents 테이블은 파일에 대한 메타데이터를 저장하는 테이블로 인지하고 있습니다.

User의 Profile을 저장하는 역할을 하며, 메시지에 포함되는 파일을 저장하는 역할도 혼용해서 사용하고 있는 것으로 인지하고 있어요.

직접 참조를 통한 연관관계보다는 간접 참조를 통해서 구현하는게 어떨까 싶어요.

직접객체를 참조하는 방식이 아닌 UUID를 통해 참조하는 방식입니다.

또한 의문점인건 테이블 스키마를 보면, message 테이블과 message_attachments 테이블 간의 연관관계로 보여요,

defer-datasource-initialization: true
properties:
hibernate:
default_batch_fetch_size: 50 # 추가함.
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.

잘하셨습니다. N+1을 방지하기 위한 방법 중 하나로 최대 조회할 수 있는 IN 갯수를 50개로 제한하여 처리한다는 의미입니다.

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