Skip to content

[김진우] Sprint6#191

Open
zinuzanu wants to merge 53 commits intocodeit-bootcamp-spring:김진우from
zinuzanu:sprint6
Open

[김진우] Sprint6#191
zinuzanu wants to merge 53 commits intocodeit-bootcamp-spring:김진우from
zinuzanu:sprint6

Conversation

@zinuzanu
Copy link
Copy Markdown
Collaborator

@zinuzanu zinuzanu commented Mar 10, 2026

기본 요구사항

API 명세

프론트엔드 소스 코드는 참고용으로만 활용하세요. 수정하여 활용하는 경우 이어지는 요구사항 또는 미션을 수행하는 데 어려움이 있을 수 있습니다.

데이터베이스

  • 아래와 같이 데이터베이스 환경을 설정하세요.
    • 데이터베이스: 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.yaml 파일에 작성하세요.
  • 디버깅을 위해 SQL 로그와 관련된 설정값을 application.yaml 파일에 작성하세요.

엔티티 정의하기

  • 클래스 다이어그램을 참고해 도메인 모델의 공통 속성을 추상 클래스로 정의하고 상속 관계를 구현하세요.
    • 이때 Serializable 인터페이스는 제외합니다.
    • 패키지명: com.sprint.mission.discodeit.entity.base
    • 클래스 다이어그램
xs6bzcvs6-image
  • JPA의 어노테이션을 활용해 createdAt, updatedAt 속성이 자동으로 설정되도록 구현하세요.
    • @CreatedDate@LastModifiedDate
  • 클래스 다이어그램을 참고해 클래스 참조 관계를 수정하세요. 필요한 경우 생성자, update 메소드를 수정할 수 있습니다. 단, 아직 JPA Entity와 관련된 어노테이션은 작성하지 마세요.
    • 클래스 다이어그램
pq5iz92wt-image
- 화살표의 방향과 화살표 유무에 유의하세요.
  • ERD와 클래스 다이어그램을 토대로 연관관계 매핑 정보를 표로 정리해보세요.(이 내용은 PR에 첨부해주세요.)

연관관계 매핑 정리

엔티티 관계 다중성 방향성 부모-자식 관계 연관관계의 주인
User : UserStatus 1:1 양방향 부모: User, 자식: UserStatus UserStatus
Channel : Message 1:N 양방향 부모: Channel, 자식: Message Message
User : Message 1:N 단방향 (User ← Message) - Message
Channel : ReadStatus 1:N 양방향 부모: Channel, 자식: ReadStatus ReadStatus
User : ReadStatus 1:N 단방향 (User ← ReadStatus) - ReadStatus
User : BinaryContent (profile) 1:0..1 단방향 (User → BinaryContent) - User
Message : BinaryContent (attachments) 1:N 단방향 (Message → BinaryContent) 부모: Message, 자식: BinaryContent Message

  • JPA 주요 어노테이션을 활용해 ERD, 연관관계 매핑 정보를 도메인 모델에 반영해보세요.
    • @Entity@Table
    • @Column@Enumerated
    • @OneToMany@OneToOne@ManyToOne
    • @JoinColumn@JoinTable
  • ERD의 외래키 제약 조건과 연관관계 매핑 정보의 부모-자식 관계를 고려해 영속성 전이와 고아 객체를 정의하세요.
    • cascadeorphanRemoval

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

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

DTO 적극 도입하기

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

DTO를 도입한 이유

  • Entity를 그대로 노출할 경우 발생할 수 있는 문제

    • API 스펙 변경 시 Entity 구조까지 함께 변경될 수 있어 API와 도메인이 강하게 결합될 수 있음
    • 양방향 연관관계에서 무한 순환 참조가 발생할 가능성
    • OSIV가 비활성화된 환경에서 Lazy Loading이 발생할 경우 LazyInitializationException 발생 가능
    • Entity 내부의 민감한 데이터가 외부 API로 노출될 위험
  • DTO 도입의 장점

    • API 응답 구조와 도메인 모델을 명확하게 분리
    • Entity 구조 변경이 API에 직접적인 영향을 주지 않음
    • 필요한 데이터만 전달하여 응답 구조를 단순화
    • 직렬화 과정에서 순환 참조 문제를 방지

  • 다음의 클래스 다이어그램을 참고하여 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}
      • putget 메소드에서 호출해 일관된 파일 경로 규칙을 유지합니다.
    • 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

페이지네이션 최적화

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

오프셋 페이지네이션 (Offset Pagination)

  • 특징

    • 페이지 번호 기반 조회 방식
    • SQL에서 LIMITOFFSET을 활용하여 구현
  • 장점

    • 구현이 비교적 단순함
    • 페이지 번호 기반 UI 구현이 쉬움
  • 단점

    • 데이터가 많아질수록 OFFSET 스캔 비용이 증가
    • 대용량 데이터 환경에서 성능 저하 발생

커서 페이지네이션 (Cursor Pagination)

  • 특징

    • 마지막으로 조회한 데이터의 식별자를 기준으로 다음 데이터를 조회
  • 장점

    • 대용량 데이터 환경에서도 일관된 조회 성능 유지
    • OFFSET 스캔이 없어 성능이 안정적
  • 단점

    • 구현 복잡도가 증가
    • 특정 페이지 번호로 바로 이동하는 기능 구현이 어려움

  • 기존에 구현한 오프셋 페이지네이션을 커서 페이지네이션으로 리팩토링하세요.
    • PageResponse는 다음과 같이 변경하세요.
73leqaemv-image
- 다음의 API 명세를 준수하세요.
    - [API 스펙 v1.2](https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=12179&version=1&directory=/%E1%84%86%E1%85%B5%E1%84%89%E1%85%A7%E1%86%AB%20%E1%84%8B%E1%85%A1%E1%86%AB%E1%84%82%E1%85%A2%20API%20%E1%84%86%E1%85%AE%E1%86%AB%E1%84%89%E1%85%A5%201.2.json&name=%E1%84%86%E1%85%B5%E1%84%89%E1%85%A7%E1%86%AB%20%E1%84%8B%E1%85%A1%E1%86%AB%E1%84%82%E1%85%A2%20API%20%E1%84%86%E1%85%AE%E1%86%AB%E1%84%89%E1%85%A5%201.2.json)
- API 스펙을 준수한다면, 아래의 프론트엔드 코드와 호환됩니다.
    - [정적 리소스 v1.2.4](https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=12179&version=1&directory=/Release%201.2.4%20dist.zip&name=Release%201.2.4%20dist.zip)
    - [소스 코드(참고용) v1.2.4](https://bakey-api.codeit.kr/api/files/resource?root=static&seqId=12179&version=1&directory=/0%20Sprint%20Mission%20Front%201.2.4.zip&name=0%20Sprint%20Mission%20Front%201.2.4.zip)

프론트엔드 소스 코드는 참고용으로만 활용하세요. 수정하여 활용하는 경우 이어지는 요구사항 또는 미션을 수행하는 데 어려움이 있을 수 있습니다.

MapStruct 적용

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

[변경사항 요약]

- 제공 받은 프론트엔드 빌드 산출물을 static/ 디렉토리에 추가하여 Spring 기반 정적 리소스 서빙 구성
- OpenAPI 스펙 요구사항에 맞춰 Swagger 설정 및 DTO 필드명을 camelCase 기준으로 정리
- ChannelDto.Response의 memberIds → participantIds로 변경하여 프론트엔드 응답 필드명과 일치
- UserStatusDto의 lastActiveAt 관련 필드명을 newLastActiveAt으로 수정
- UserDto.Response의 isOnline → online으로 변경하여 프론트엔드 응답 파싱 오류 방지
- PRIVATE 채널에서 참여자가 1명만 남을 경우 프론트 크래시 방지를 위한 임시 필터링 로직 추가

※ 정적 리소스 연동 후 화면 기반 테스트를 통해 API 응답 구조 불일치로 발생하는 런타임 오류를 보완함
- Dockerfile 제거
- Railway 환경변수(RAILPACK_JDK_VERSION)를 통한 JDK 버전 설정
- plain.jar 생성 방지를 위해 Gradle jar task 비활성화
- bootJar만 생성되도록 설정하여 Railway 실행 아티팩트 충돌 방지

※ Docker 이미지 기반 배포에서 Railway 기본 Railpack 배포 방식으로 전환
[변경사항 요약]

- BaseEntity를 entity/base 패키지로 이동하고 BaseUpdatableEntity 도입
- 엔티티 공통 필드 상속 구조 정립
- UUID 기반 참조 제거 후 객체 참조(User, Channel 등) 방식으로 전환
- Serializable 인터페이스 제거 (JPA 도입 대비)
- 엔티티 구조 변경에 따른 서비스 및 Repository 계층 전반 리팩터링
[변경사항 요약]

- ERD 기반으로 도메인 엔티티에 JPA 매핑 정보 반영
  - @entity, @table 적용
  - @column, @Enumerated 적용
  - @manytoone, @OnetoOne, @onetomany 연관관계 매핑
  - @joincolumn, @jointable 설정

- 부모-자식 관계 및 생명주기 경계를 고려하여 영속성 전이 정책 정의
  - Aggregate 내부 관계에 cascade 적용
  - 고아 객체 제거(orphanRemoval) 설정

- DB 외래키 제약 조건과 객체 그래프 간 책임 경계 정리
  - 단순 참조 관계는 DB FK에 위임
  - 강한 생명주기 종속 관계는 JPA 레벨에서 관리
[변경사항 요약]
- FileRepository, JCFRepository 삭제 및 관련 의존성 제거
- JpaRepository 상속 및 도메인별 쿼리 메서드 적용
- 영속성 컨텍스트 특징(변경 감지, 지연 로딩) 반영 및 서비스 로직 리팩터링
- @transactional을 활용한 트랜잭션 경계 설정 및 데이터 일관성 보장"
[변경사항 요약]
- 기존 Inner Record 기반 DTO 구조를 Flat 형식으로 분리 및 재구성
- MapStruct 라이브러리 도입을 통한 Entity-DTO 매핑 자동화 및 간소화
- 매퍼 컴포넌트 도입과 DTO 구조 재구성에 따른 Service, Controller 리팩터링 수행
[변경사항 요약]
- BinaryContent에서 bytes 제거 및 로컬 파일 시스템 저장 방식 도입
- BinaryContentStorage 인터페이스 및 Local 디스크 구현체 추가
- Message, ReadStatus 엔티티 생성자 기반 리팩토링 (assign 메서드 제거)
- .gitignore 수정: .idea 폴더 전체 제외 및 uploads 경로 추가
- 파일 다운로드 API 구현 (GET /api/binaryContents/{id}/download)"
[변경사항 요약]
- API v1.1 명세 및 정적 리소스 v1.1.4 도입에 따른 도메인 로직 및 컨트롤러 일부 리팩터링
- 유저 상태(UserStatus) 매핑 오류 수정 및 API 규격에 맞는 데이터 응답 구조 정합성 확보
[주요 변경사항]
- N+1 문제 해결: Fetch Join 및 @EntityGraph를 활용하여 연관 엔티티 일괄 조회 최적화
- 페이지네이션 리팩토링: 기존 오프셋 방식에서 커서 기반 페이지네이션으로 전환 (v1.2 스펙 준수)
- OSIV 비활성화 대응: open-in-view: false 설정 및 서비스 레이어 조회 메서드 전용 트랜잭션 적용
- 정적 리소스 v1.2.4 호환성 이슈 보고:
    * 현상: 메시지 생성 시 응답 DTO의 lastMessageAt이 null로 반환되어 프론트엔드 읽음 처리 하이라이트가 무한 반복되는 현상 식별
    * 원인 분석: 메시지 저장 시점과 매퍼의 최신 메시지 조회 시점 간의 영속성 컨텍스트 불일치 의심
    * 특이사항: 해당 이슈는 현재 진행 중이며, 임시 조치 대신 구조적인 해결을 위해 추가 분석 예정
- API v1.2 사양 준수: PageResponse 구조 변경 및 관련 Controller/Service/Repository 전면 리팩토링
@zinuzanu zinuzanu requested a review from jaeyeon518 March 10, 2026 08:40
@zinuzanu zinuzanu added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Mar 10, 2026
@zinuzanu zinuzanu self-assigned this Mar 10, 2026
[주요 변경사항]
1. DTO 구조 개선: ReadStatus/Message 응답 시 객체 대신 UUID 식별자 반환하도록 수정 (정적 리소스 규격 준수)
2. 연관관계 정규화: Message-BinaryContent 관계를 @manytomany에서 1:N(@onetomany) 단방향 매핑으로 변경
3. N+1 문제 해결: ChannelMapper 내 DB 조회 로직 제거 및 Service 레이어에서 Map을 활용한 Batch Fetching 적용
4. 데이터 정합성 보장: saveAndFlush() 적용으로 JPA Auditing 시점 동기화 (createdAt null 이슈 방지)
5. 페이지네이션 교정: PageResponse 내 nextCursor 값을 페이지 번호에서 마지막 요소의 createdAt(Instant)으로 수정
[주요 변경사항]
1. 커서 기반 페이징 정합성 확보
   - 요청(cursor)과 응답(nextCursor) 기준을 모두 Instant로 통일하여 정합성 해결

2. N+1 문제 추가 해결 및 최적화
   - 채널 조회: Channel-ReadStatus-User 연관 객체 일괄 조회를 통해 지연 로딩 방지
   - 메시지 조회: Author Fetch Join 및 Attachments @batchsize 적용으로 DTO 변환 시 쿼리 폭탄 방어
   - 메모리 페이징 방지: 컬렉션 Fetch Join을 제거하여 실제 DB 레벨 페이징(Limit) 활성화

3. 도메인 로직 및 매핑 규격 수정
   - 채널 수정 API: Dirty Checking이 정상 동작하도록 수정하여 기능 복구
   - 매핑 준수: Message-BinaryContent 관계를 조인 테이블 기반 1:N 단방향 매핑으로 유지
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