ํ์ : PM(1) / Design(1) / Frontend(2) / Backend(3)
๊ธฐ๊ฐ : 2024.03 ~ 2025.03
๋งํฌ : https://github.com/M-ung/MoodBuddy_Server
์๋น์ค ๋ด์ฉ : ์ฌ์ฉ์๊ฐ ์์ฑํ ์ผ๊ธฐ๋ฅผ ๋ฐํ์ผ๋ก ๊ฐ์ ๋ถ์ํ๋ ์น ์๋น์ค
์ํต : GitHub, Slack, Notion, Discord
์ฐ๋ฆฌ ๋ฌด๋๋ฒ๋๋ 2024.03 ~ 2024.08 ๊ธฐ๊ฐ ๋์ 1์ฐจ ๊ฐ๋ฐ์ ์๋ฃ ํ ๋ฐฐํฌํ์์ผ๋ฉฐ, ์ฌ์ฉ์๋ฅผ ๋ชจ์งํด์ ์๋น์ค๋ฅผ ์ด์ํ์๋ค.
์ด์์ ํ๋ฉด์ ์ฌ์ฉ์์ ํผ๋๋ฐฑ์ ๊พธ์คํ ๋ฐ์์ผ๋ฉฐ, ๊ฐ์ฅ ๋ง์ด ๋ฐ์ ํผ๋๋ฐฑ ์ค ํ๋๋ '์ผ๊ธฐ ์ ์ฅํ ๋ ์๋๊ฐ ๋๋ฌด ๋๋ ค์..' ์ด๋ค.
์์ ๊ฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ์ด์ ๋ ์๋์ ๊ฐ๋ค.
1. ์ผ๊ธฐ ์ ์ฅ API ํธ์ถ ์, DB์ ์ผ๊ธฐ ์ ๋ณด ์ ์ฅ
2. ์ผ๊ธฐ ์ ์ฅ API ํธ์ถ ์, S3์ ์ผ๊ธฐ ์ด๋ฏธ์ง ์
๋ก๋ ๋ฐ DB ์ ์ฅ
3. ์ผ๊ธฐ ์ ์ฅ API ํธ์ถ ์, Open AI๋ฅผ ํตํ ์ผ๊ธฐ ๋ถ์
๐ save
@Override
@Transactional
public DiaryResDetailDTO save(DiaryReqSaveDTO diaryReqSaveDTO) throws IOException {
final Long kakaoId = JwtUtil.getUserId();
DiaryUtil.validateExistingDiary(diaryRepository, diaryReqSaveDTO.getDiaryDate(), kakaoId);
String summary = gptService.summarize(diaryReqSaveDTO.getDiaryContent()).block();
DiarySubject diarySubject = classifyDiaryContent(diaryReqSaveDTO.getDiaryContent());
Diary diary = DiaryMapper.toDiaryEntity(diaryReqSaveDTO, kakaoId, summary, diarySubject);
diary = diaryRepository.save(diary);
DiaryUtil.saveDiaryImages(diaryImageService, diaryReqSaveDTO.getDiaryImgList(), diary);
checkTodayDiary(diaryReqSaveDTO.getDiaryDate(), kakaoId, false);
deleteDraftDiaries(diaryReqSaveDTO.getDiaryDate(), kakaoId);
return DiaryMapper.toDetailDTO(diary);
}
์ผ๊ธฐ ์ ์ฅํ ๋ ์ ์ธ ๊ฐ์ง๋ฅผ ํ ๋ฒ์ ์คํํ๊ธฐ ๋๋ฌธ์ ์ผ๊ธฐ ์ ์ฅ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฐ๋ค.
์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์๋ ์๋์ ๊ฐ์ด ์ผ๊ธฐ ์ ์ฅ API๋ฅผ ๋ฆฌํฉํ ๋งํด์ผ ํ๋ค.
1. ์ผ๊ธฐ ์ ์ฅ API ํธ์ถ ์ DB์ ์ผ๊ธฐ ์ ๋ณด ์ ์ฅ๋ง ํ๋๋ก ํ๋ค.
2. ์ผ๊ธฐ ์ด๋ฏธ์ง ์ ์ฅ์ ์ผ๊ธฐ ์ ์ฅ API์ ๋ถ๋ฆฌํ๋ค.
3. Open AI๋ฅผ ํตํ ์ผ๊ธฐ ๋ถ์ ๊ธฐ๋ฅ๋ ์ผ๊ธฐ ์ ์ฅ API์ ๋ถ๋ฆฌํ๋ค.
1. ์ผ๊ธฐ ์ ์ฅ API ํธ์ถ ์ DB์ ์ผ๊ธฐ ์ ๋ณด ์ ์ฅ๋ง ํ๋๋ก ํ๋ค.
์ผ๊ธฐ ์ ์ฅ์ ๊ธฐ์กด ์ฝ๋์์ ์ ์ฅ ๋ก์ง๋ง ๊ฐ์ ธ์ค๊ณ ๊น๋ํ๊ฒ ์ ๋ฆฌ๋ง ํ๋ค.
๐ DiaryApiController.java
@RestController
@RequestMapping("/api/v2/member/diary")
@Tag(name = "Diary", description = "์ผ๊ธฐ ๊ด๋ จ API")
@RequiredArgsConstructor
public class DiaryApiController {
private final DiaryFacade diaryFacade;
@PostMapping("/save")
@Operation(summary = "์ผ๊ธฐ ์์ฑ", description = "์๋ก์ด ์ผ๊ธฐ๋ฅผ ์์ฑํฉ๋๋ค.")
public ResponseEntity<DiaryResSaveDTO> saveDiary(@Parameter(description = "์ผ๊ธฐ ์ ๋ณด๋ฅผ ๋ด๊ณ ์๋ DTO")
@RequestBody @Valid DiaryReqSaveDTO requestDTO) {
return ResponseEntity.ok().body(diaryFacade.saveDiary(requestDTO));
}
}
๐ DiaryFacadeImpl.java
@Component
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DiaryFacadeImpl implements DiaryFacade {
private final DiaryService diaryService;
private final DraftDiaryService draftDiaryService;
private final DiaryImageService diaryImageService;
private final BookMarkService bookMarkService;
private final UserService userService;
private final RedisService redisService;
@Override
@Transactional
public DiaryResSaveDTO saveDiary(DiaryReqSaveDTO requestDTO) {
final var userId = JwtUtil.getUserId();
diaryService.validateExistingDiary(userId, requestDTO.diaryDate());
var diaryId = diaryService.saveDiary(userId, requestDTO);
saveDiaryImages(diaryId, requestDTO.diaryImageUrls());
checkTodayDiary(userId, requestDTO.diaryDate(), false);
deleteData(userId, requestDTO.diaryDate());
return new DiaryResSaveDTO(diaryId);
}
}
๐ DiaryServiceImpl.java
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class DiaryServiceImpl implements DiaryService {
private final DiaryRepository diaryRepository;
@Override
@Transactional
public Long saveDiary(final Long userId, DiaryReqSaveDTO requestDTO) {
return diaryRepository.save(Diary.of(
requestDTO,
userId)).getId();
}
}
2. ์ผ๊ธฐ ์ด๋ฏธ์ง ์ ์ฅ์ ์ผ๊ธฐ ์ ์ฅ API์ ๋ถ๋ฆฌํ๋ค.
์ฒ์ ์ด๋ฏธ์ง ์ ์ฅ์ ๋ฐ๋ก API๋ก ๋นผ๊ณ ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ก S3 ์
๋ก๋ ๋ฐ DB ์ ์ฅ์ ๊ตฌํํ๋ ค๊ณ ํ๋ค. ๋ ์๋ฒ์์ ์ธ๋ค์ผ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํด ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง์ ์ ์ฉํ๋ค.
ํ์ง๋ง ์๋ฒ์์ ๋ฆฌ์ฌ์ด์ง ,S3 ์
๋ก๋ ๋ฐ DB ์ ์ฅ์ ์ง์ ํ๋ ๊ฑด ์๊ฐ๋ ์ค๋ ๊ฑธ๋ฆฌ๊ณ ๋ณด์์์ผ๋ก ์์ ํ์ง ์๋ค๊ณ ์๊ฐํ๋ค.
โ๏ธ ์๋ฒ์์ ์ง์ ์ด๋ฏธ์ง ๋ฆฌ์ฌ์ด์ง์ ์ํํ ๊ฒฝ์ฐ
โ๏ธ S3 ์ ๋ก๋ ๋ฐ DB ์ ์ฅ์ ์๋ฒ์์ ์ง์ ์ฒ๋ฆฌํ ๊ฒฝ์ฐ
๊ทธ๋์ ์ ๋ฐฉ์์ ๋ฌธ์ ๋ฅผ ๋์ด์ค ์ ์๋ "Presigned URL" ๋ฐฉ์์ ๋ค์ ์ ์ฉํด ๋ณด๊ธฐ๋ก ํ๋ค.
Presigned URL ์ด๋?
Presigned URL์ ํด๋ผ์ด์ธํธ๊ฐ ์ธ์ฆ ์์ด ํน์ S3 ๊ฐ์ฒด(ํ์ผ)์ ์ ๊ทผํ ์ ์๋๋ก ์ผ์์ ์ผ๋ก ๊ถํ์ ๋ถ์ฌํ๋ URL์ด๋ค.
์ฆ, ์๋ฒ์์ ๋ฏธ๋ฆฌ ์๋ช ๋ URL์ ์์ฑํ์ฌ ํด๋ผ์ด์ธํธ์ ์ ๋ฌํ๊ณ , ํด๋ผ์ด์ธํธ๋ ํด๋น URL์ ์ฌ์ฉํ์ฌ S3์ ์ง์ ํ์ผ์ ์ ๋ก๋ํ๊ฑฐ๋ ๋ค์ด๋ก๋ํ๋ ๋ฐฉ์์ด๋ค.
๐ CloudController.java
@RestController
@RequestMapping("/api/v2/member/cloud")
@Tag(name = "Cloud", description = "ํด๋ผ์ฐ๋ ๊ด๋ จ API")
@RequiredArgsConstructor
public class CloudController {
private final CloudService cloudService;
@PostMapping("/generate-url")
@Operation(summary = "preSignedUrl ์์ฑ API", description = "preSignedUrl ์์ฑ API ์
๋๋ค.")
public ResponseEntity<CloudResUrlDTO> generatePreSignedUrl() {
return ResponseEntity.ok(cloudService.generatePreSignedUrl());
}
}
๐ CloudServiceImpl.java
@Service
@RequiredArgsConstructor
public class CloudServiceImpl implements CloudService{
private final AmazonS3 amazonS3;
private final String dot = ".";
@Value("${cloud.aws.s3.bucket}")
private String bucket;
@Value("${cloud.aws.s3.diary_images_folder}")
private String profileImagesFolder;
@Override
public CloudResUrlDTO generatePreSignedUrl() {
String fileName = UUID.randomUUID() + dot;
String uploadPath = String.format("%s/%s/%s", profileImagesFolder, JwtUtil.getUserId(), fileName);
GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucket, uploadPath)
.withMethod(HttpMethod.PUT)
.withExpiration(getExpirationTime());
return new CloudResUrlDTO(amazonS3.generatePresignedUrl(generatePresignedUrlRequest).toString());
}
private Date getExpirationTime() {
long expirationMillis = System.currentTimeMillis() + (1000 * 60 * 10);
return new Date(expirationMillis);
}
}
์ ๋ก์ง์ ํตํด Presigned URL
์ ํด๋ผ์ด์ธํธ์์ ์์ฒญํ๋ฉด ๋ฐ๊ธํ๋ค. ๊ทธ๋ฆฌ๊ณ ์ผ๊ธฐ ์ ์ฅ์ ํ ๋ ํด๋ผ์ด์ธํธ๊ฐ List์ ์ด๋ฏธ์ง URL์ ๋ด์์ ์ฃผ๋ฉด ๋ฐ๋ก DB์ ์ ์ฅํ๋ฉด ๋๋ค.
์ด๋ ๊ฒ ํ๋ฉด ์ผ๊ธฐ ์ ์ฅ์ ์ผ๊ธฐ ์ด๋ฏธ์ง ์ ์ฅ์ ํ์คํ๊ฒ ๋ถ๋ฆฌํ ์ ์๊ฒ ๋๋ค.
3. Open AI๋ฅผ ํตํ ์ผ๊ธฐ ๋ถ์ ๊ธฐ๋ฅ๋ ์ผ๊ธฐ ์ ์ฅ API์ ๋ถ๋ฆฌํ๋ค.
์ผ๊ธฐ ๋ถ์ ๊ธฐ๋ฅ์ Open AI๋ฅผ ํธ์ถํ๊ณ GPT์ ๋ต๋ณ์ ๊ธฐ๋ค๋ฆฌ๊ณ ๋ฐ๋ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ๊ฐ์ฅ ์ค๋ ์๊ฐ์ ์ฐจ์งํ๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ์ด๋ฅผ ์ผ๊ธฐ ์ ์ฅ API์ ํฌํจํ๋ ๊ฒ๋ณด๋จ ์์ ์ผ๊ธฐ ๋ถ์ API๋ฅผ ๋ง๋ค์ด์ ๋ฐ๋ก ์์ฒญํ๋ ๊ฒ์ด ๊ฐ์ฅ ์ข์ ๋ฐฉ์์ด์๋ค.
@RestController
@RequestMapping("/api/v2/member/diary")
@Tag(name = "Diary", description = "์ผ๊ธฐ ๊ด๋ จ API")
@RequiredArgsConstructor
public class DiaryAnalyzeApiController {
private final DiaryAnalyzeFacade diaryAnalyzeFacade;
@PostMapping("/analyze/{diaryId}")
@Operation(summary = "์ผ๊ธฐ ๋ถ์ (์์ฝ, ์ฃผ์ , ๊ฐ์ )", description = "์์ฑ๋ ์ผ๊ธฐ๋ฅผ ๋ถ์ํฉ๋๋ค. ์ผ๊ธฐ ์ ์ฅ, ์ผ๊ธฐ ์์ , ์์์ ์ฅ ์ผ๊ธฐ -> ์ผ๊ธฐ ์ ์ฅ ํ ๋ ํธ์ถํ๋ฉด ๋ฉ๋๋ค.")
public ResponseEntity<DiaryResAnalyzeDTO> analyzeDiary(@Parameter(description = "์ผ๊ธฐ ๊ณ ์ ์๋ณ์")
@PathVariable("diaryId") Long diaryId) {
return ResponseEntity.ok().body(diaryAnalyzeFacade.analyze(diaryId));
}
}
์ ๊ณผ์ ์ ํตํด 1์ฐจ ๋ฐฐํฌ ๋์ ๋นํด ์ด๋ ์ ๋ ์ผ๊ธฐ ์ ์ฅ ์๊ฐ์ ๋จ์ถํ๋์ง ํฌ์คํธ๋งจ์ ํตํด ํ์ธํด ๋ณด์๋ค.
postman ํ
์คํธ ๊ฒฐ๊ณผ [์ผ๊ธฐ ์ ์ฅ 3.78s + ๊ฐ์ ๋ถ์ 798ms = 4.578s]
postman ํ
์คํธ ๊ฒฐ๊ณผ [์ผ๊ธฐ ์ ์ฅ 233ms + ์ผ๊ธฐ ๋ถ์ 1.49s = 1.723s]
๊ฒฐ๊ณผ, 1์ฐจ ๋ฐฐํฌ ๋์ ๋นํด ์ผ๊ธฐ ์ ์ฅ 2.855์ด๋ฅผ ๋จ์ถํ ์ ์์๋ค.
์ ๊ฒฝํ์ ํตํด ํ ๊ธฐ๋ฅ์ ๊ตฌํํ๋๋ผ๋ ๊ธฐ๋ฅ์ ๋ถ๋ฆฌํ ์ ์์ผ๋ฉด ๋ถ๋ฆฌํด์ ์ฑ๋ฅ์ ๊ฐ์ ํ๋ ๊ฒ์ด ์ข๋ค๋ ์๊ฐ์ ํ๋ค.
๊ทธ๋ฆฌ๊ณ ์ฑ๋ฅ ๊ฐ์ ๋ณด๋ค ๋ ํฌ๊ฒ ๋ฐฐ์ด ์ ์ "์ฌ์ฉ์"์ ์ค์์ฑ์ด๋ค. ๋ง์ฝ ์ฌ์ฉ์์๊ฒ ์ง์ ๋ฐฐํฌํ์ง ์๊ณ ํ๋ก์ ํธ๋ฅผ ๋๋๋ค๋ฉด, ์ด๋ค ๊ธฐ๋ฅ์ด ๋ฌธ์ ๊ฐ ์๊ณ ์ด๋ค ๊ธฐ๋ฅ์ ๊ฐ์ ํด์ผ ํ๋์ง ๊นจ๋ฌ์ ์ ์์์ ๊ฒ์ด๋ค.
์์ผ๋ก ๊ฐ๋ฐ์ ์์ด์ ์ฌ์ฉ์ ๋ฐฐํฌ๋ ์์ด์๋ ์ ๋ ๊ณผ์ ์ด๋ผ๋ ๊ฒ์ ๋ฐฐ์ธ ์ ์์๊ณ , ์ฌ์ฉ์์ ํผ๋๋ฐฑ์ ์์ฉํ๋ ๊ฒ๋ ์์ด์๋ ์ ๋ ๊ณผ์ ์ด๋ผ๋ ๊ฒ์ ๋ฐฐ์ธ ์ ์์๋ค.