Skip to content

Commit 8df1e5f

Browse files
authored
Merge pull request #34 from Namsoo315/남현수-sprint6
남현수 sprint6
2 parents 62ee80e + f92aefc commit 8df1e5f

29 files changed

+543
-1553
lines changed

README.md

Lines changed: 169 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,180 @@
1-
# 프로젝트 마일스톤 & 요구사항
1+
기본 요구사항
2+
API 명세
3+
이번 미션은 아래의 API 스펙과 비교하며 구현해보세요.
24

3-
## 🚀 프로젝트 마일스톤
5+
API 스펙 v1.1
6+
API 스펙을 준수한다면, 아래의 프론트엔드 코드와 호환됩니다.
47

5-
- RESTful API로 재설계 및 리팩토링
6-
- Swagger를 활용한 API 문서 자동화
7-
- 프론트엔드 연동
8-
- PaaS를 활용한 배포
8+
정적 리소스 v1.1.4
9+
소스 코드(참고용) v1.1.4
10+
프론트엔드 소스 코드는 참고용으로만 활용하세요. 수정하여 활용하는 경우 이어지는 요구사항 또는 미션을 수행하는 데 어려움이 있을 수 있습니다.
911

10-
---
12+
데이터베이스
13+
[ ] 아래와 같이 데이터베이스 환경을 설정하세요.
14+
데이터베이스: discodeit
15+
유저: discodeit_user
16+
패스워드: discodeit1234
17+
[ ] ERD를 참고하여 DDL을 작성하고, 테이블을 생성하세요.
18+
작성한 DDL 파일은 /src/main/resources/schema.sql 경로에 포함하세요. 9tj7wyc7q-image.png
19+
PK: Primary Key
20+
UK: Unique Key
21+
NN: Not Null
22+
FK: Foreign Key
23+
ON DELETE CASCADE: 연관 엔티티 삭제 시 같이 삭제
24+
ON DELETE SET NULL: 연관 엔티티 삭제 시 NULL로 변경
25+
Spring Data JPA 적용하기
26+
[ ] Spring Data JPA와 PostgreSQL을 위한 의존성을 추가하세요.
27+
[ ] 앞서 구성한 데이터베이스에 연결하기 위한 설정값을 application.yaml 파일에 작성하세요.
28+
[ ] 디버깅을 위해 SQL 로그와 관련된 설정값을 application.yaml 파일에 작성하세요.
29+
엔티티 정의하기
30+
[ ] 클래스 다이어그램을 참고해 도메인 모델의 공통 속성을 추상 클래스로 정의하고 상속 관계를 구현하세요.
1131

12-
## 📋 기본 요구사항
32+
이때 Serializable 인터페이스는 제외합니다.
1333

14-
1. **RESTful API 재설계**
15-
- 스프린트 미션#4에서 구현한 API를 RESTful API로 다시 설계
16-
- API 스펙을 확인하고 본인이 설계한 API와 비교
17-
- `oasdiff`를 활용하면 API 비교가 수월함
18-
- 제공된 API 스펙에 맞추어 구현 (심화 요구사항 프론트엔드 연동을 위해 필수)
34+
패키지명: com.sprint.mission.discodeit.entity.base
1935

20-
2. **API 테스트**
21-
- Postman을 활용하여 컨트롤러 테스트
22-
- 테스트 결과를 export하여 PR에 첨부
36+
클래스 다이어그램
2337

24-
3. **Swagger 기반 API 문서화**
25-
- `springdoc-openapi` 활용
26-
- Swagger-UI를 통해 API 테스트 가능
38+
[ ] JPA의 어노테이션을 활용해 createdAt, updatedAt 속성이 자동으로 설정되도록 구현하세요.
2739

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

30-
## ✨ 심화 요구사항
44+
클래스 다이어그램
45+
pq5iz92wt-image.png
3146

32-
1. **정적 리소스 서빙**
33-
- 제공된 `fe_1.0.0.zip` 활용
34-
- API 스펙을 준수하면 프론트엔드와 정상 연동
47+
화살표의 방향과 화살표 유무에 유의하세요.
3548

36-
2. **PaaS 배포 (Railway.app)**
37-
- Railway.app 가입 및 GitHub 레포지토리 연결
38-
- `Settings > Network` 섹션에서 Generate Domain 버튼으로 도메인 생성
39-
- 생성된 도메인 접속 후 애플리케이션 테스트
49+
[ ] ERD와 클래스 다이어그램을 토대로 연관관계 매핑 정보를 표로 정리해보세요.(이 내용은 PR에 첨부해주세요.)
50+
| A | B | 다중성 | 방향성 | 부모-자식 관계 | 연관관계의 주인 |
51+
| ------- | ------------- | ------ | ----------------------------------------- | ------------------------------ | ---------- |
52+
| User | UserStatus | 1:1 | 단방향 User → UserStatus | 부모: User, 자식: UserStatus | User |
53+
| User | ReadStatus | 1\:N | 단방향 ReadStatus → User | 부모: User, 자식: ReadStatus | ReadStatus |
54+
| User | Message | 1\:N | 단방향 Message → User (author) | 부모: User, 자식: Message |
55+
Message |
56+
| User | BinaryContent | 1:1 (Nullable) | 단방향 User → BinaryContent (profile) | 부모: User, 자식:
57+
BinaryContent | User |
58+
| Channel | ReadStatus | 1\:N | 단방향 ReadStatus → Channel | 부모: Channel, 자식: ReadStatus |
59+
ReadStatus |
60+
| Channel | Message | 1\:N | 단방향 Message → Channel | 부모: Channel, 자식: Message | Message |
61+
| Message | BinaryContent | 1\:N | 단방향 Message → BinaryContent (attachments) | 부모: Message, 자식:
62+
BinaryContent | Message |
63+
64+
예시
65+
엔티티 관계 다중성 방향성 부모-자식 관계 연관관계의 주인
66+
A:B 1:N B→A 단방향 부모: A, 자식: B B
67+
[ ] JPA 주요 어노테이션을 활용해 ERD, 연관관계 매핑 정보를 도메인 모델에 반영해보세요.
68+
@Entity, @Table
69+
@Column, @Enumerated
70+
@OneToMany, @OneToOne, @ManyToOne
71+
@JoinColumn, @JoinTable
72+
[ ] ERD의 외래키 제약 조건과 연관관계 매핑 정보의 부모-자식 관계를 고려해 영속성 전이와 고아 객체를 정의하세요.
73+
cascade, orphanRemoval
74+
레포지토리와 서비스에 JPA 도입하기
75+
[ ] 기존의 Repository 인터페이스를 JPARepository로 정의하고 쿼리메소드로 대체하세요.
76+
FileRepository와 JCFRepository 구현체는 삭제합니다.
77+
[ ] 영속성 컨텍스트의 특징에 맞추어 서비스 레이어를 수정해보세요.
78+
힌트: 트랜잭션, 영속성 전이, 변경 감지, 지연로딩
79+
DTO 적극 도입하기
80+
[ ] Entity를 Controller 까지 그대로 노출했을 때 발생할 수 있는 문제점에 대해 정리해보세요. DTO를 적극 도입했을 때 보일러플레이트 코드가 많아지지만, 그럼에도
81+
불구하고 어떤 이점이 있는지 알 수 있을거에요.(이 내용은 PR에 첨부해주세요.)
82+
힌트
83+
Entity와 API의 결합
84+
프로덕션 환경에서는 성능을 고려해 OSIV를 false로 설정하는 경우가 대부분
85+
양방향 연관관계 시 순환 참조
86+
민감한 데이터
87+
[ ] 다음의 클래스 다이어그램을 참고하여 DTO를 정의하세요.
88+
hd4c6g1of-image.png
89+
90+
[ ] Entity를 DTO로 매핑하는 로직을 책임지는 Mapper 컴포넌트를 정의해 반복되는 코드를 줄여보세요.
91+
92+
패키지명: com.sprint.mission.discodeit.mapper buo7cmjvp-image.png
93+
BinaryContent 저장 로직 고도화
94+
데이터베이스에 이미지와 같은 파일을 저장하면 성능 상 불리한 점이 많습니다. 따라서 실제 바이너리 데이터는 별도의 공간에 저장하고, 데이터베이스에는 바이너리 데이터에 대한 메타
95+
정보(파일명, 크기, 유형 등)만 저장하는 것이 좋습니다.
96+
97+
[ ] BinaryContent 엔티티는 파일의 메타 정보(fileName, size, contentType)만 표현하도록 bytes 속성을 제거하세요.
98+
99+
[ ] BinaryContent의 byte[] 데이터 저장을 담당하는 인터페이스를 설계하세요.
100+
101+
저장 매체의 확장성(로컬 저장소, 원격 저장소)을 고려해 인터페이스부터 설계합니다.
102+
103+
패키지명: com.sprint.mission.discodeit.storage
104+
105+
클래스 다이어그램
106+
nqt5zw2pk-image.png
107+
108+
BinaryContentStorage
109+
110+
바이너리 데이터의 저장/로드를 담당하는 컴포넌트입니다.
111+
UUID put(UUID, byte[])
112+
UUID 키 정보를 바탕으로 byte[] 데이터를 저장합니다.
113+
UUID는 BinaryContent의 Id 입니다.
114+
InputStream get(UUID)
115+
키 정보를 바탕으로 byte[] 데이터를 읽어 InputStream 타입으로 반환합니다.
116+
UUID는 BinaryContent의 Id 입니다.
117+
ResponseEntity<?> download(BinaryContentDto)
118+
HTTP API로 다운로드 기능을 제공합니다.
119+
BinaryContentDto 정보를 바탕으로 파일을 다운로드할 수 있는 응답을 반환합니다.
120+
[ ] 서비스 레이어에서 기존에 BinaryContent를 저장하던 로직을 BinaryContentStorage를 활용하도록 리팩토링하세요.
121+
122+
[ ] BinaryContentController에 파일을 다운로드하는 API를 추가하고, BinaryContentStorage에 로직을 위임하세요.
123+
124+
엔드포인트: GET /api/binaryContents/{binaryContentId}/download
125+
126+
요청
127+
128+
값: BinaryContentId
129+
방식: Query Parameter
130+
응답: ResponseEntity<?>
131+
132+
클래스 다이어그램
133+
134+
5qwe2kqno-image.png
135+
136+
[ ] 로컬 디스크 저장 방식으로 BinaryContentStorage 구현체를 구현하세요.
137+
138+
클래스 다이어그램
139+
140+
skptrmm5p-image.png
141+
142+
[ ] discodeit.storage.type 값이 local 인 경우에만 Bean으로 등록되어야 합니다.
143+
144+
Path root
145+
로컬 디스크의 루트 경로입니다.
146+
discodeit.storage.local.root-path 설정값을 정의하고, 이 값을 통해 주입합니다.
147+
void init()
148+
루트 디렉토리를 초기화합니다.
149+
Bean이 생성되면 자동으로 호출되도록 합니다.
150+
Path resolvePath(UUID)
151+
파일의 실제 저장 위치에 대한 규칙을 정의합니다.
152+
파일 저장 위치 규칙 예시: {root}/{UUID}
153+
put, get 메소드에서 호출해 일관된 파일 경로 규칙을 유지합니다.
154+
ResponseEntity<Resource> donwload(BinaryContentDto)
155+
get 메소드를 통해 파일의 바이너리 데이터를 조회합니다.
156+
BinaryContentDto와 바이너리 데이터를 활용해 ResponseEntity<Resource> 응답을 생성 후 반환합니다.
157+
페이징과 정렬
158+
[ ] 메시지 목록을 조회할 때 다음의 조건에 따라 페이지네이션 처리를 해보세요.
159+
50개씩 최근 메시지 순으로 조회합니다.
160+
총 메시지가 몇개인지 알 필요는 없습니다.
161+
[ ] 일관된 페이지네이션 응답을 위해 제네릭을 활용해 DTO로 구현하세요.
162+
패키지명: com.sprint.mission.discodeit.dto.response
163+
164+
클래스 다이어그램
165+
wj4q7nhn3-image.png
166+
167+
content: 실제 데이터입니다.
168+
169+
number: 페이지 번호입니다.
170+
171+
size: 페이지의 크기입니다.
172+
173+
totalElements: T 데이터의 총 갯수를 의미하며, null일 수 있습니다.
174+
175+
[ ] Slice 또는 Page 객체로부터 DTO를 생성하는 Mapper를 구현하세요.
176+
패키지명: com.sprint.mission.discodeit.mapper
177+
178+
x7qjncxm0-image.png
179+
180+
확장성을 위해 제네릭 메소드로 구현하세요.

build.gradle

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ dependencies {
3232
developmentOnly 'org.springframework.boot:spring-boot-devtools'
3333
runtimeOnly 'org.postgresql:postgresql'
3434
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
35-
3635
// https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-starter-webmvc-ui
3736
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9")
3837
}
Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,39 @@
11
package com.sprint.mission.discodeit.entity;
22

3+
import com.sprint.mission.discodeit.entity.base.BaseEntity;
4+
import jakarta.persistence.Column;
5+
import jakarta.persistence.Entity;
6+
import jakarta.persistence.Table;
37
import java.io.Serial;
48
import java.io.Serializable;
59
import java.time.Instant;
610
import java.util.UUID;
711

12+
import lombok.AccessLevel;
13+
import lombok.AllArgsConstructor;
814
import lombok.Getter;
15+
import lombok.NoArgsConstructor;
16+
import lombok.experimental.SuperBuilder;
917

18+
19+
@Table(name = "binary_contents")
20+
@Entity
1021
@Getter
11-
public class BinaryContent implements Serializable {
12-
13-
@Serial
14-
private static final long serialVersionUID = 1L;
15-
private final UUID id;
16-
17-
private final Instant createdAt;
18-
19-
private final String fileName;
20-
private final Long size;
21-
private final String contentType;
22-
private final byte[] bytes;
23-
// 수정 불가능 하기 때문에 updatedAt은 삭제
24-
25-
public BinaryContent(String fileName, String contentType, Long size, byte[] bytes) {
26-
this.id = UUID.randomUUID();
27-
this.createdAt = Instant.now();
28-
this.fileName = fileName;
29-
this.contentType = contentType;
30-
this.size = size;
31-
this.bytes = bytes;
32-
}
22+
@SuperBuilder
23+
@AllArgsConstructor(access = AccessLevel.PROTECTED)
24+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
25+
public class BinaryContent extends BaseEntity {
26+
27+
@Column(name = "file_name", nullable = false, length = 255)
28+
private String fileName;
29+
30+
@Column(nullable = false)
31+
private Long size;
32+
33+
@Column(name = "content_type", nullable = false, length = 100)
34+
private String contentType;
35+
36+
@Column(nullable = false)
37+
private byte[] bytes;
38+
3339
}
Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,58 @@
11
package com.sprint.mission.discodeit.entity;
22

3+
import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity;
4+
import jakarta.persistence.Column;
5+
import jakarta.persistence.Entity;
6+
import jakarta.persistence.EnumType;
7+
import jakarta.persistence.Enumerated;
8+
import jakarta.persistence.Table;
39
import java.io.Serial;
410
import java.io.Serializable;
511
import java.time.Instant;
612
import java.util.UUID;
713

14+
import lombok.AccessLevel;
15+
import lombok.AllArgsConstructor;
816
import lombok.Getter;
17+
import lombok.NoArgsConstructor;
18+
import lombok.experimental.SuperBuilder;
919

20+
@Table(name = "channels")
21+
@Entity
1022
@Getter
11-
public class Channel implements Serializable {
12-
13-
@Serial
14-
private static final long serialVersionUID = 1L;
15-
private final UUID id;
23+
@SuperBuilder
24+
@AllArgsConstructor(access = AccessLevel.PROTECTED)
25+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
26+
public class Channel extends BaseUpdatableEntity {
1627

28+
@Column(length = 100)
1729
private String name;
30+
31+
@Column(length = 500)
1832
private String description;
19-
private final ChannelType type;
20-
21-
private final Instant createdAt;
22-
private Instant updatedAt;
23-
24-
public Channel(ChannelType type, String name, String description) {
25-
this.id = UUID.randomUUID();
26-
this.type = type;
27-
this.name = name;
28-
this.description = description;
29-
this.createdAt = Instant.now();
30-
this.updatedAt = createdAt;
31-
}
32-
33-
public void update(String newName, String newDescription) {
34-
boolean anyValueUpdated = false;
35-
36-
if (newName != null && !newName.isEmpty()) {
37-
this.name = newName;
38-
anyValueUpdated = true;
39-
}
40-
41-
if (newDescription != null && !newDescription.isEmpty()) {
42-
this.description = newDescription;
43-
anyValueUpdated = true;
44-
}
45-
46-
if (anyValueUpdated) {
47-
this.updatedAt = Instant.now();
48-
}
49-
}
33+
34+
@Enumerated(EnumType.STRING)
35+
@Column(length = 10, nullable = false)
36+
private ChannelType type;
37+
38+
// public Channel(ChannelType type, String name, String description) {
39+
// this.type = type;
40+
// this.name = name;
41+
// this.description = description;
42+
// }
43+
//
44+
// public void update(String newName, String newDescription) {
45+
// boolean anyValueUpdated = false;
46+
//
47+
// if (newName != null && !newName.isEmpty()) {
48+
// this.name = newName;
49+
// anyValueUpdated = true;
50+
// }
51+
//
52+
// if (newDescription != null && !newDescription.isEmpty()) {
53+
// this.description = newDescription;
54+
// anyValueUpdated = true;
55+
// }
56+
// }
5057

5158
}

0 commit comments

Comments
 (0)