[๐Ÿ”ฅTroubleShooting - MoodBuddy๐Ÿ”ฅ] ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ ๐Ÿ”Š : ์ผ๊ธฐ ์ €์žฅ์ด ๋„ˆ๋ฌด ๋Š๋ ค์š”..๐Ÿ˜ญ๐Ÿ˜ญ

._mungยท2025๋…„ 3์›” 8์ผ
0

MoodBuddy

๋ชฉ๋ก ๋ณด๊ธฐ
2/9

๐Ÿ“Œ ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

ํŒ€์› : PM(1) / Design(1) / Frontend(2) / Backend(3)
๊ธฐ๊ฐ„ : 2024.03 ~ 2025.03
๋งํฌ : https://github.com/M-ung/MoodBuddy_Server
์„œ๋น„์Šค ๋‚ด์šฉ : ์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์„ฑํ•œ ์ผ๊ธฐ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๊ฐ์ • ๋ถ„์„ํ•˜๋Š” ์›น ์„œ๋น„์Šค
์†Œํ†ต : GitHub, Slack, Notion, Discord


๐Ÿ”ฅTroubleShooting๐Ÿ”ฅ

Problems

์šฐ๋ฆฌ ๋ฌด๋“œ๋ฒ„๋””๋Š” 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);
    }

์ผ๊ธฐ ์ €์žฅํ•  ๋•Œ ์œ„ ์„ธ ๊ฐ€์ง€๋ฅผ ํ•œ ๋ฒˆ์— ์‹คํ–‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ผ๊ธฐ ์ €์žฅ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฐ๋‹ค.


How

์œ„ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์ผ๊ธฐ ์ €์žฅ API๋ฅผ ๋ฆฌํŒฉํ† ๋งํ•ด์•ผ ํ•œ๋‹ค.

1. ์ผ๊ธฐ ์ €์žฅ API ํ˜ธ์ถœ ์‹œ DB์— ์ผ๊ธฐ ์ •๋ณด ์ €์žฅ๋งŒ ํ•˜๋„๋ก ํ•œ๋‹ค.
2. ์ผ๊ธฐ ์ด๋ฏธ์ง€ ์ €์žฅ์€ ์ผ๊ธฐ ์ €์žฅ API์™€ ๋ถ„๋ฆฌํ•œ๋‹ค.
3. Open AI๋ฅผ ํ†ตํ•œ ์ผ๊ธฐ ๋ถ„์„ ๊ธฐ๋Šฅ๋„ ์ผ๊ธฐ ์ €์žฅ API์™€ ๋ถ„๋ฆฌํ•œ๋‹ค.


Process

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 ์ €์žฅ์„ ์„œ๋ฒ„์—์„œ ์ง์ ‘ ์ฒ˜๋ฆฌํ•  ๊ฒฝ์šฐ

  • API ์‘๋‹ต ์‹œ๊ฐ„์ด ๊ธธ์–ด์ง€๋Š” ๋ฌธ์ œ
  • ์„œ๋ฒ„์—์„œ ํŒŒ์ผ์„ ์ง์ ‘ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์€ ๋ณด์•ˆ์ ์œผ๋กœ ์ทจ์•ฝํ•œ ๋ฌธ์ œ

๊ทธ๋ž˜์„œ ์œ„ ๋ฐฉ์‹์˜ ๋ฌธ์ œ๋ฅผ ๋œ์–ด์ค„ ์ˆ˜ ์žˆ๋Š” "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));
    }
}

Result

์œ„ ๊ณผ์ •์„ ํ†ตํ•ด 1์ฐจ ๋ฐฐํฌ ๋•Œ์— ๋น„ํ•ด ์–ด๋Š ์ •๋„ ์ผ๊ธฐ ์ €์žฅ ์‹œ๊ฐ„์„ ๋‹จ์ถ•ํ–ˆ๋Š”์ง€ ํฌ์ŠคํŠธ๋งจ์„ ํ†ตํ•ด ํ™•์ธํ•ด ๋ณด์•˜๋‹ค.

postman ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ [์ผ๊ธฐ ์ €์žฅ 3.78s + ๊ฐ์ • ๋ถ„์„ 798ms = 4.578s]

postman ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ [์ผ๊ธฐ ์ €์žฅ 233ms + ์ผ๊ธฐ ๋ถ„์„ 1.49s = 1.723s]

๊ฒฐ๊ณผ, 1์ฐจ ๋ฐฐํฌ ๋•Œ์— ๋น„ํ•ด ์ผ๊ธฐ ์ €์žฅ 2.855์ดˆ๋ฅผ ๋‹จ์ถ•ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.


Thoughts

์œ„ ๊ฒฝํ—˜์„ ํ†ตํ•ด ํ•œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋”๋ผ๋„ ๊ธฐ๋Šฅ์„ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ์œผ๋ฉด ๋ถ„๋ฆฌํ•ด์„œ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค๋Š” ์ƒ๊ฐ์„ ํ–ˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ์„ฑ๋Šฅ ๊ฐœ์„ ๋ณด๋‹ค ๋” ํฌ๊ฒŒ ๋ฐฐ์šด ์ ์€ "์‚ฌ์šฉ์ž"์˜ ์ค‘์š”์„ฑ์ด๋‹ค. ๋งŒ์•ฝ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ง์ ‘ ๋ฐฐํฌํ•˜์ง€ ์•Š๊ณ  ํ”„๋กœ์ ํŠธ๋ฅผ ๋๋ƒˆ๋‹ค๋ฉด, ์–ด๋–ค ๊ธฐ๋Šฅ์ด ๋ฌธ์ œ๊ฐ€ ์žˆ๊ณ  ์–ด๋–ค ๊ธฐ๋Šฅ์„ ๊ฐœ์„ ํ•ด์•ผ ํ•˜๋Š”์ง€ ๊นจ๋‹ฌ์„ ์ˆ˜ ์—†์—ˆ์„ ๊ฒƒ์ด๋‹ค.

์•ž์œผ๋กœ ๊ฐœ๋ฐœ์— ์žˆ์–ด์„œ ์‚ฌ์šฉ์ž ๋ฐฐํฌ๋Š” ์—†์–ด์„œ๋Š” ์•ˆ ๋  ๊ณผ์ •์ด๋ผ๋Š” ๊ฒƒ์„ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์—ˆ๊ณ , ์‚ฌ์šฉ์ž์˜ ํ”ผ๋“œ๋ฐฑ์„ ์ˆ˜์šฉํ•˜๋Š” ๊ฒƒ๋„ ์—†์–ด์„œ๋Š” ์•ˆ ๋  ๊ณผ์ •์ด๋ผ๋Š” ๊ฒƒ์„ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์—ˆ๋‹ค.


profile
๐Ÿ’ป ๐Ÿ’ป ๐Ÿ’ป

0๊ฐœ์˜ ๋Œ“๊ธ€

๊ด€๋ จ ์ฑ„์šฉ ์ •๋ณด