Skip to content

[이규빈] Sprint6#187

Open
plzslp wants to merge 23 commits intocodeit-bootcamp-spring:이규빈from
plzslp:sprint6
Open

[이규빈] Sprint6#187
plzslp wants to merge 23 commits intocodeit-bootcamp-spring:이규빈from
plzslp:sprint6

Conversation

@plzslp
Copy link
Copy Markdown
Collaborator

@plzslp plzslp commented Mar 9, 2026

프로젝트 마일스톤

  • 데이터베이스 환경 설정 및 모델링
  • Spring Data JPA 환경 적용
  • Entity 연관 관계 매핑
  • 레포지토리와 서비스 계층에 JPA 적용
  • Transaction 처리
  • 페이지네이션과 정렬
  • DTO의 적극적인 도입과 MapStruct의 활용
  • BinaryContent 저장 로직 고도화
    • 메타정보와 바이너리 정보 분리
  • N+1 문제 해결

요구사항

기본

API 명세

  • 이번 미션은 아래의 API 스펙과 비교하며 구현해보세요.
    • [API 스펙 v1.1]
  • API 스펙을 준수한다면, 아래의 프론트엔드 코드와 호환됩니다.
    • [정적 리소스 v1.1.4]
    • [소스 코드(참고용) v1.1.4]

데이터베이스

  • 아래와 같이 데이터베이스 환경을 설정하세요.
    • 데이터베이스: discodeit
    • 유저: discodeit_user
    • 패스워드: discodeit1234
  • ERD를 참고하여 DDL을 작성하고, 테이블을 생성하세요.
    • 작성한 DDL 파일은 /src/main/resources/schema.sql 경로에 포함하세요.
u0ghedzoz-image
  • PK: Primary Key
  • UK: Unique Key
  • NN: Not Null
  • FK: Foreign Key
  • ON DELETE CASCADE: 연관 엔티티 삭제 시 같이 삭제
  • ON DELETE SET NULL: 연관 엔티티 삭제 시 NULL로 변경

Spring Data JPA 적용하기

  • Spring Data JPA와 PostgreSQL을 위한 의존성을 추가하세요.
  • 앞서 구성한 데이터베이스에 연결하기 위한 설정값을 application.yml 파일에 작성하세요.
  • 디버깅을 위해 SQL 로그와 관련된 설정값을 application.yml 파일에 작성하세요.

엔티티 정의하기

  • 클래스 다이어그램을 참고해 도메인 모델의 공통 속성을 추상 클래스로 정의하고 상속 관계를 구현하세요.
    • 이때 Serializable 인터페이스는 제외합니다.
    • 패키지명: com.sprint.mission.discodeit.entity.base
    • 클래스 다이어그램
xs6bzcvs6-image
  • JPA의 어노테이션을 활용해 createdAt, updatedAt 속성이 자동으로 설정되도록 구현하세요.

  • @CreatedDate, @LastModifiedDate

  • 클래스 다이어그램을 참고해 클래스 참조 관계를 수정하세요. 필요한 경우 생성자, update 메소드를 수정할 수 있습니다. 단, 아직 JPA Entity와 관련된 어노테이션은 작성하지 마세요.

    • 클래스 다이어그램
pq5iz92wt-image
  • 화살표의 방향과 화살표 유무에 유의하세요.

  • ERD와 클래스 다이어그램을 토대로 연관관계 매핑 정보를 표로 정리해보세요.(이 내용은 PR에 첨부해주세요.)

엔티티 관계 다중성 방향성 부모-자식 관계 연관관계의 주인
ReadStatus:User N대1 ReadStatus -> User 단방향 부모:User, 자식:ReadStatus ReadStatus
ReadStatus:Channel N대1 ReadStatus -> Channel 단방향 부모:Channel, 자식:ReadStatus ReadStatus
User:UserStatus 1대1 User <-> UserStatus 양방향 부모:User, 자식:UserStatus UserStatus
User:BinaryContent 1대1 User -> BinaryContent 단방향 부모:User, 자식:BinaryContent User
Message:BinaryContent 1대N Message -> BinaryContent 단방향 부모:Message, 자식:BinaryContent Message
Message:User N대1 Message -> User 단방향 부모:User, 자식:Message Message
MessageChannel N대1 Message -> Channel 단방향 부모:Channel, 자식:Message Message
  • JPA 주요 어노테이션을 활용해 ERD, 연관관계 매핑 정보를 도메인 모델에 반영해보세요.

    • @Entity, @Table
    • @Column, @Enumerated
    • @OneToMany, @OneToOne, @ManyToOne
    • @JoinColumn, @JoinTable
  • ERD의 외래키 제약 조건과 연관관계 매핑 정보의 부모-자식 관계를 고려해 영속성 전이와 고아 객체를 정의하세요.

    • cascade, orphanRemoval

레포지토리와 서비스에 JPA 도입하기

  • 기존의 Repository 인터페이스를 JPARepository로 정의하고 쿼리메소드로 대체하세요.
    • FileRepository와 JCFRepository 구현체는 삭제합니다.
  • 영속성 컨텍스트의 특징에 맞추어 서비스 레이어를 수정해보세요.
    • 힌트: 트랜잭션, 영속성 전이, 변경 감지, 지연로딩

DTO 적극 도입하기

  • Entity를 Controller 까지 그대로 노출했을 때 발생할 수 있는 문제점에 대해 정리해보세요. DTO를 적극 도입했을 때 보일러플레이트 코드가 많아지지만, 그럼에도 불구하고 어떤 이점이 있는지 알 수 있을거에요.(이 내용은 PR에 첨부해주세요.)

    • Entity와 API의 결합 : 엔티티의 필드가 변경되면 이에 따라 변경사항이 서비스부터 컨트롤러까지 전달된다. 이로 인해 API를 사용하던 다른 서비스의 코드까지 문제가 생긴다.
    • 프로덕션 환경에서는 성능을 고려해 OSIV를 false로 설정하는 경우가 대부분 : OSIV를 false로 적용시에 서비스 계층의 트랜잭션이 끝날시에 DB에 전송되므로 컨트롤러 계층에서 영속성 컨텍스트를 활용한 코드가 작동하지 않는다.
    • 양방향 연관관계 시 순환 참조 : 양방향 연관관계를 가진 엔티티들은 조회시에 순환참조로 인해 서로가 서로를 무한히 호출하는 문제가 생긴다.
    • 민감한 데이터 : 비밀번호같은 민감한 데이터가 노출될 수 있다.
  • 다음의 클래스 다이어그램을 참고하여 DTO를 정의하세요.

hd4c6g1of-image
  • Entity를 DTO로 매핑하는 로직을 책임지는 Mapper 컴포넌트를 정의해 반복되는 코드를 줄여보세요.
    • 패키지명: com.sprint.mission.discodeit.mapper
buo7cmjvp-image

BinaryContent 저장 로직 고도화
데이터베이스에 이미지와 같은 파일을 저장하면 성능 상 불리한 점이 많습니다. 따라서 실제 바이너리 데이터는 별도의 공간에 저장하고, 데이터베이스에는 바이너리 데이터에 대한 메타 정보(파일명, 크기, 유형 등)만 저장하는 것이 좋습니다.

  • BinaryContent 엔티티는 파일의 메타 정보(fileName, size, contentType)만 표현하도록 bytes 속성을 제거하세요.
  • BinaryContent의 byte[] 데이터 저장을 담당하는 인터페이스를 설계하세요.
    • 패키지명: com.sprint.mission.discodeit.storage
    • 클래스 다이어그램
nqt5zw2pk-image
  • BinaryContentStorage

    • 바이너리 데이터의 저장/로드를 담당하는 컴포넌트입니다.
    • UUID put(UUID, byte[])
      • UUID 키 정보를 바탕으로 byte[] 데이터를 저장합니다.
      • UUID는 BinaryContent의 Id 입니다.
    • InputStream get(UUID)
      • 키 정보를 바탕으로 byte[] 데이터를 읽어 InputStream 타입으로 반환합니다.
      • UUID는 BinaryContent의 Id 입니다.
    • ResponseEntity<?> download(BinaryContentDto)
      • HTTP API로 다운로드 기능을 제공합니다.
      • BinaryContentDto 정보를 바탕으로 파일을 다운로드할 수 있는 응답을 반환합니다.
  • 서비스 레이어에서 기존에 BinaryContent를 저장하던 로직을 BinaryContentStorage를 활용하도록 리팩토링하세요.

  • BinaryContentController에 파일을 다운로드하는 API를 추가하고, BinaryContentStorage에 로직을 위임하세요.

  • 엔드포인트: GET /api/binaryContents/{binaryContentId}/download

  • 요청

    • 값: BinaryContentId
    • 방식: Path Variable
  • 응답: ResponseEntity<?>

  • 클래스 다이어그램

5qwe2kqno-image
  • 로컬 디스크 저장 방식으로 BinaryContentStorage 구현체를 구현하세요.

  • 클래스 다이어그램

skptrmm5p-image
  • discodeit.storage.type 값이 local 인 경우에만 Bean으로 등록되어야 합니다.

  • Path root

    • 로컬 디스크의 루트 경로입니다.
    • discodeit.storage.local.root-path 설정값을 정의하고, 이 값을 통해 주입합니다.
  • void init()

    • 루트 디렉토리를 초기화합니다.
    • Bean이 생성되면 자동으로 호출되도록 합니다.
  • Path resolvePath(UUID)

    • 파일의 실제 저장 위치에 대한 규칙을 정의합니다.
      • 파일 저장 위치 규칙 예시: {root}/{UUID}
    • put, get 메소드에서 호출해 일관된 파일 경로 규칙을 유지합니다.
  • ResponseEntity<Resource> donwload(BinaryContentDto)

    • get 메소드를 통해 파일의 바이너리 데이터를 조회합니다.
    • BinaryContentDto와 바이너리 데이터를 활용해 ResponseEntity<Resource> 응답을 생성 후 반환합니다.

페이징과 정렬

  • 메시지 목록을 조회할 때 다음의 조건에 따라 페이지네이션 처리를 해보세요.
    • 50개씩 최근 메시지 순으로 조회합니다.
    • 총 메시지가 몇개인지 알 필요는 없습니다.
  • 일관된 페이지네이션 응답을 위해 제네릭을 활용해 DTO로 구현하세요.
    • 패키지명: com.sprint.mission.discodeit.dto.response
    • 클래스 다이어그램
wj4q7nhn3-image
  • content: 실제 데이터입니다.

  • number: 페이지 번호입니다.

  • size: 페이지의 크기입니다.

  • totalElements: T 데이터의 총 갯수를 의미하며, null일 수 있습니다.

  • Slice 또는 Page 객체로부터 DTO를 생성하는 Mapper를 구현하세요.

    • 패키지명: com.sprint.mission.discodeit.mapper
x7qjncxm0-image
  • 확장성을 위해 제네릭 메소드로 구현하세요.

심화

N+1 문제

  • N+1 문제가 발생하는 쿼리를 찾고 해결해보세요.

읽기전용 트랜잭션 활용

  • 프로덕션 환경에서는 OSIV를 비활성화하는 경우가 많습니다. 이때 서비스 레이어의 조회 메소드에서 발생할 수 있는 문제를 식별하고, 읽기 전용 트랜잭션을 활용해 문제를 해결해보세요.

  • OSIV 비활성화하기

spring:
  jpa:
    open-in-view: false

페이지네이션 최적화

  • 오프셋 페이지네이션과 커서 페이지네이션 방식의 차이에 대해 정리해보세요.

    • 오프셋 기반 페이지네이션은 페이지 번호(page number)와 크기(size)를 기준으로, 데이터베이스에서 해당 순서 앞의 데이터를 건너뛰고(Skip) 정해진 크기만큼의 데이터를 조회하여 제공한다
    • 커서 기반 페이지네이션은 정해진 size만큼의 정보를 제공하지만 모든 컨텐츠를 조회하지 않고 hasNext를 통해 다음 조회할 컨텐츠가 있는지 확인 후 cursor(마지막 데이터의 기준점)를 기준으로 다음 페이지를 반환하는 식으로 최적화 되어있다.
    • 오프셋 기반은 게시판 사이트의 게시물 목록에 주로 사용되며, 커서 기반은 주로 무한 스크롤에 주로 사용된다.
  • 기존에 구현한 오프셋 페이지네이션을 커서 페이지네이션으로 리팩토링하세요.

    • PageResponse는 다음과 같이 변경하세요.
73leqaemv-image
  • 다음의 API 명세를 준수하세요.

    • [API 스펙 v1.2]
  • API 스펙을 준수한다면, 아래의 프론트엔드 코드와 호환됩니다.

    • [정적 리소스 v1.2.4]
    • [소스 코드(참고용) v1.2.4]

MapStruct 적용

  • Entity와 DTO를 매핑하는 보일러플레이트 코드를 MapStruct 라이브러리를 활용해 간소화해보세요.

스크린샷

screanshot_sprint6

멘토에게

  • [feat: 프로젝트 초기 환경 설정 및 데이터베이스 연동] Mar 3, 2026 커밋부터 미션 6 내용입니다.
  • AI 사용 방법에 대한 질문입니다. 학습 시에는 질문을 한 뒤에 제가 제대로 이해한게 맞는지 되묻는 식으로 사용하고 있고, 과제 진행 시에는 요구사항을 체크리스트로 뽑아서 순차적으로 진행하거나 커밋 메시지 작성 정도에 사용하고 있는데 멘토님은 실무에서 주로 어떤 방식으로 사용하시는지 궁금합니다.

-모든 FileRepository 구현체에 ReentrantLock 적용

- SpringDoc OpenAPI 의존성 추가
- UserStatus 업데이트 로직 및 PATCH 메서드 수정

- UserController 엔드포인트 변경 및 RequestBody 적용

- ChannelDto.Response 필드명 수정 (lastMessageAt)
- ChannelController 및 Service의 채널 생성 로직 수정

- UserController의 생성, 수정 메서드 RequestPart 어노테이션 적용

- 제공된 정적 리소스 파일 추가
- MessageController, ReadStatusController, BinaryContentController 로직 수정

- 연관된 Service 및 Entity 비즈니스 로직 수정
- Update 요청 DTO 필드명 변경

- RequestParam 어노테이션에 키(value) 지정

- 프론트엔드 연동 및 API 일관성 확보를 위한 리팩토링
- ErrorResponse 및 BusinessLogicException 기반의 공통 예외 응답 규격 적용

- 서비스 계층 내 하드코딩된 예외를 커스텀 예외로 교체

- BasicChannelService 참여자 목록 동기화 누락 해결

- 미사용 Import 제거 및 코드 최적화 진행
- BasicChannelService 채널 전체 조회 시 lastMessageAt 필드 누락 문제 해결

- 공개 및 비공개 채널의 최근 메시지 시간(Instant) 매핑 로직 적용

- 프라이빗 메서드 추출(getParticipantIds, getLastMessageAt)을 통한 코드 가독성 개선
- 내부 클래스로 관리되던 DTO를 도메인별 개별 클래스로 추출

- 어노테이션을 통한 요청 데이터 유효성 검증 로직 적용

- DTO 클래스 분리에 따른 연관 Service 및 Controller 참조 로직 수정
- build.gradle: Spring Data JPA 및 MapStruct 의존성 추가

- application.yaml: 데이터베이스 연결 및 JPA/SQL 디버깅 로그 설정 추가

- schema.sql: 초기 테이블 생성을 위한 데이터베이스 스키마 정의 파일 추가
- BaseEntity 추상 클래스 생성 및 JPA Auditing 적용

- 엔티티 간 ID 참조 방식을 객체 참조 기반의 연관관계 매핑으로 개편

- ManyToOne, OneToMany 설정 및 영속성 전이(cascade), 고아 객체 제거 적용

- JpaRepository 기반 레포지토리 재작성 및 기존(JCF, File) 구현체 삭제
- BinaryContentStorage 인터페이스 설계 및 로컬 구현체 추가

- ConditionalOnProperty 기반 스토리지 자동 빈 등록 설정

- 파일 다운로드 API 구현 및 ResponseEntity 반환 타입 적용
- Pageable을 활용한 오프셋 방식 페이지네이션 로직 적용

- 공통 응답 규격 PageResponse DTO 도입 및 데이터 직렬화 최적화
- N+1 문제 해결 및 OSIV 비활성화에 따른 쿼리 효율성 개선

- Transactional 경계 설정 및 변경 감지(Dirty Checking) 기반 로직 리팩토링
- ReadStatus 엔티티 업데이트 로직 수정 및 요청 DTO 필드 추가

- Channel 엔티티 (name, description) Nullable 허용

- 파일 다운로드 Content-Disposition 헤더 추가 및 로그인 401 상태 코드 반영

- ErrorResponse 수정
@plzslp plzslp requested a review from jaeyeon518 March 9, 2026 08:18
@plzslp plzslp added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Mar 9, 2026
@plzslp plzslp changed the title [이규빈 Sprint6 [이규빈] Sprint6 Mar 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant