ํ”„๋กœ์ ํŠธ

[ํ”„๋กœ์ ํŠธ] ๋ฐ˜๋ ค๋™๋ฌผ ์ž๋ž‘ ํ† ์ดํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

sian han 2025. 6. 9. 13:57

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

“๋ชจ์—ฌ๋ผ PAW PAW” ๋Š” ๋ฐ˜๋ ค๋™๋ฌผ ์ž๋ž‘ ์ปค๋ฎค๋‹ˆํ‹ฐ์ž…๋‹ˆ๋‹ค.

ํšŒ์› ๊ฐ€์ž… ํ›„ ์ด์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋ฐ˜๋ ค๋™๋ฌผ์˜ ์‚ฌ์ง„๊ณผ ํ”„๋กœํ•„์„ ๋“ฑ๋กํ•ด ๋‹ค๋ฅธ ์‚ฌ๋žŒ๊ณผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์ข‹์•„ํ•˜๋Š” ๋‹จ์–ด, ์‹ซ์–ดํ•˜๋Š” ๋‹จ์–ด, ํ•œ์ค„ ์†Œ๊ฐœ ๋“ฑ์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž์™€ ๋ฐ˜๋ ค๋™๋ฌผ ๊ฐ„์˜ ๊ต๊ฐ์„ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ๋“ฑ๋ก๋œ ๋ฐ˜๋ ค๋™๋ฌผ ํ”„๋กœํ•„์„ ๋ณด๋ฉฐ ์ข‹์•„์š”๋ฅผ ๋‚จ๊ธธ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ธ๊ธฐ ๋žญํ‚น์ด ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.

๋ฐ˜๋ ค๋™๋ฌผ ์ด๋ฏธ์ง€ ๋“ฑ๋ก ์‹œ AI๋ฅผ ํ†ตํ•œ ๋ฐ˜๋ ค๋™๋ฌผ ํŒ๋ณ„ ๊ธฐ๋Šฅ์„ ์ ์šฉํ•˜์—ฌ, ๋“ฑ๋ก๋œ ์ด๋ฏธ์ง€๊ฐ€ ์‹ค์ œ ๋ฐ˜๋ ค๋™๋ฌผ์ธ์ง€ ์‚ฌ์ „์— ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

Github Repo : https://github.com/HAN-SEOHYUN/wedlessInvite/issues

โœ… ๋ชฉํ‘œ

  • ์•„ํ‚คํ…์ฒ˜ ๋ถ„๋ฆฌ, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋“ฑ์˜ ๊ตฌ์กฐ์ ์ธ ์„ค๊ณ„
  • ์‹ค์ „ ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ ๊ฒฝํ—˜
  • ๊ณต๋ถ€์ค‘์ธ ๋””์ž์ธ ํŒจํ„ด ์ ์šฉ
  • Docker, AWS ๊ธฐ์ˆ  ํ•™์Šต ๋ฐ ์ ์šฉ

 

๐Ÿ›  ์‚ฌ์šฉ ๊ธฐ์ˆ  ์Šคํƒ

Spring Boot ์„œ๋ฒ„์™€ Thymeleaf ํ…œํ”Œ๋ฆฟ ์—”์ง„์œผ๋กœ ๊ตฌ์„ฑ๋œ ๋ชจ๋†€๋ฆฌํ‹ฑ ์•„ํ‚คํ…์ฒ˜ ๊ธฐ๋ฐ˜์˜ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ž…๋‹ˆ๋‹ค.

Spring MVC ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ปจํŠธ๋กค๋Ÿฌ, ์„œ๋น„์Šค, ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๊ณ„์ธต์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌํ•˜๊ณ , Model-View-Controller ํŒจํ„ด์— ๋”ฐ๋ผ ํด๋ผ์ด์–ธํŠธ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. 

  • Database: MySQL 8.0
  • Infra: AWS EC2, S3, Docker
  • Language: Java 23
  • Framework: Spring Boot
  • ORM: JPA (Hibernate)
  • Database: MySQL
  • Template Engine: Thymeleaf
  • View Layer: HTML + CSS + JS
  • AI Service: PyTorch + Flask (ResNet ๊ธฐ๋ฐ˜ ์ด๋ฏธ์ง€ ํŒ๋ณ„)
  • ๊ธฐํƒ€: Intellij, Github
  • TEST : k6, Postman

K6 ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ ์•„ํ‚คํ…์ณ

 

 

๐Ÿš€ ์ฃผ์š” ์„ค๊ณ„ ํฌ์ธํŠธ

1. ๋ฐ˜๋ ค๋™๋ฌผ ์ด๋ฏธ์ง€ ํŒ๋ณ„ AI ๊ธฐ๋Šฅ

ImageController๋Š” ์ด๋ฏธ์ง€๋ฅผ MultipartFile๋กœ ๋ฐ›์•„์„œ Flask ๊ธฐ๋ฐ˜ AI ์„œ๋ฒ„๋กœ ์ „์†กํ•˜๊ณ , ํ•ด๋‹น ์ด๋ฏธ์ง€๊ฐ€ ๊ฐ•์•„์ง€/๊ณ ์–‘์ด์ธ์ง€ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค.
Flask ์„œ๋ฒ„๋Š” PyTorch์˜ ResNet-18 ๋ชจ๋ธ์„ ํ™œ์šฉํ•˜๋ฉฐ, ์„œ๋ฒ„์—์„œ ๋ถ„๋ฅ˜ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์•„, ์œ ํšจํ•œ ๋ฐ˜๋ ค๋™๋ฌผ์ธ์ง€ ์ฒดํฌ ํ›„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

 

2. ์ผ๊ด€๋œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ (Exception Handling)

๋ฐ˜๋ณต์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ์˜ˆ์™ธ ์ƒํ™ฉ์„ ๊ฐ„๊ฒฐํ•˜๊ณ  ๋ช…ํ™•ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด CustomException๊ณผ ErrorCode๋ฅผ ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค.

@Getter
public class CustomException extends RuntimeException {
    private final ErrorCode errorCode;

    public CustomException(ErrorCode errorCode) {
        super(errorCode.getErrorMsg());
        this.errorCode = errorCode;
    }
}

 

์•„๋ž˜์™€ ๊ฐ™์ด ์‚ฌ์šฉ์‹œ์ ์— ์ง๊ด€์ ์ด๊ณ  ๊ฐ„๋‹จํ•˜๊ฒŒ ์˜ˆ์™ธ์ฒ˜๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 

public void validateFileSize(MultipartFile file) {
        if (file.getSize() > MAX_FILE_SIZE) {
            throw new CustomException(EXCEED_MAX_FILE_SIZE);
        }
    }

 

 

3. DTO ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ

๋ชจ๋“  ๋„๋ฉ”์ธ์—์„œ ์š”์ฒญ๊ณผ ์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ DTO(Data Transfer Object)๋กœ ๋ถ„๋ฆฌํ•ด ์ฒ˜๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ DTO ํด๋ž˜์Šค๋Š” Lombok์˜ @Builder ํŒจํ„ด์„ ์ ์šฉํ•ด ๋ถˆ๋ณ€์„ฑ์„ ์œ ์ง€ํ•˜๋ฉด์„œ๋„ ๊ฐ€๋…์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ์ด๋ฏธ์ง€ ๋ชฉ๋ก ์‘๋‹ต์„ ์œ„ํ•œ DTO๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค:

@Getter
@Setter
public class ImageListResponseDto {
    private String orgFileName;
    private String s3Url;
    private Long fileSize;
    private LocalDateTime modTime;

    @Builder
    public ImageListResponseDto(String orgFileName, String s3Url, Long fileSize, LocalDateTime modTime) {
        this.orgFileName = orgFileName;
        this.s3Url = s3Url;
        this.fileSize = fileSize;
        this.modTime = modTime;
    }
}

 

 

4. Response<T>๋กœ ์‘๋‹ต ์ผ๊ด€์„ฑ ํ™•๋ณด

ํด๋ผ์ด์–ธํŠธ์™€์˜ ํ†ต์‹ ์—์„œ ์ผ๊ด€๋œ ์‘๋‹ต ํฌ๋งท์„ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด AbstractResponse, SuccessResponse<T> ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค. 

์‘๋‹ต์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ JSON ํ˜•ํƒœ๋กœ ์ œ๊ณต๋˜๋ฉฐ, ์„ฑ๊ณต๊ณผ ์‹คํŒจ ์—ฌ๋ถ€์— ๊ด€๊ณ„์—†์ด ๋™์ผํ•œ ๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. 

{
    "success": true,
    "statusCode": "OK",
    "message": "์š”์ฒญ์ด ์ •์ƒ์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
    "data": { ... }
}

 

๊ณตํ†ต ๋ถ€๋ชจ ํด๋ž˜์Šค์ธ AbstractResponse๋Š” ์„ฑ๊ณต ์—ฌ๋ถ€, ์ƒํƒœ ์ฝ”๋“œ, ๋ฉ”์‹œ์ง€๋ฅผ ์ •์˜ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ์ด๋ฅผ ์ƒ์†ํ•˜๋Š” SuccessResponse<T>์™€ FailureResponse๋Š” ๊ฐ๊ฐ ์„ฑ๊ณต๊ณผ ์‹คํŒจ์— ๋”ฐ๋ฅธ ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. SuccessResponse<T>๋Š” ์ œ๋„ค๋ฆญ ํƒ€์ž… T๋ฅผ ํ†ตํ•ด ๋‹ค์–‘ํ•œ ํ˜•ํƒœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์œ ์—ฐํ•˜๊ฒŒ ๋‹ด์„ ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. 

public class SuccessResponse<T> extends AbstractResponse {
    private final T data;

    public SuccessResponse(HttpStatus statusCode, String message, T data) {
        super(statusCode, message, true);  // success ๊ฐ’์€ ํ•ญ์ƒ true
        this.data = data;
    }

    public T getData() {
        return data;
    }
}

 

์˜ˆ๋ฅผ ๋“ค์–ด, ์ข‹์•„์š” ๋žญํ‚น ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” API๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑ๋ฉ๋‹ˆ๋‹ค.

@GetMapping("/rank")
public ResponseEntity<SuccessResponse<List<PetLikeRankingResponseDto>>> getPetLike() {
    List<PetLikeRankingResponseDto> dto = likeService.getTop3MostLikedInvitations();
    SuccessResponse<List<PetLikeRankingResponseDto>> successResponse = new SuccessResponse<>(
            HttpStatus.OK,
            LIKE_LIST_FETCH_SUCCESS_MESSAGE,
            dto
    );
    return ResponseEntity.status(HttpStatus.OK).body(successResponse);
}

 

5. ๋กœ๊ทธ์ธ ๊ฒ€์‚ฌ ์ธํ„ฐ์…‰ํ„ฐ (LoginCheckInterceptor)

๋น„ํšŒ์›์˜ ์ ‘๊ทผ์„ ์ œํ•œํ•˜๊ธฐ ์œ„ํ•ด LoginCheckInterceptor๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ธ์ฆ์ด ํ•„์š”ํ•œ ์š”์ฒญ์— ๋Œ€ํ•ด ์„ธ์…˜์— ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋˜๋ฉฐ, ํŠน์ • ๊ณต๊ฐœ URI๋Š” ์˜ˆ์™ธ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ ํ•„ํ„ฐ๋Š” WebConfig ์„ค์ •์„ ํ†ตํ•ด ์ปจํŠธ๋กค๋Ÿฌ ๊ฒฝ๋กœ์— ์ ์šฉ๋˜๋ฉฐ, ๋กœ๊ทธ์ธ ์ƒํƒœ๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ์—๋Š” ์ž๋™์œผ๋กœ /login ๊ฒฝ๋กœ๋กœ ์ด๋™์‹œ์ผœ ์‚ฌ์šฉ์ž ์ธ์ฆ์„ ์œ ๋„ํ•ฉ๋‹ˆ๋‹ค. ํŠน์ • API๋Š” ์˜ˆ์™ธ ๊ฒฝ๋กœ๋กœ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    HttpSession session = request.getSession(false);
    if (session == null || session.getAttribute("userMaster") == null) {
        response.sendRedirect("/login");
        return false;
    }
    return true;
}

 

 

โœ… ๋กœ๊ทธ ์ถ”์  ํ…œํ”Œ๋ฆฟ (AbstractLogTraceTemplate)

๊ฐ ์š”์ฒญ๋ณ„ ์‹คํ–‰ ํ๋ฆ„์„ ์ถ”์ ํ•˜๊ณ , ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ ์›์ธ์„ ์ •ํ™•ํžˆ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” LogTrace ๊ธฐ๋Šฅ์„ ๊ฐœ๋ฐœํ–ˆ์Šต๋‹ˆ๋‹ค. 

๊ณตํ†ต์ ์ธ ๋ถ€๊ฐ€๊ธฐ๋Šฅ์„ ํ•œ ํด๋ž˜์Šค์— ๋ชจ์„ ์ˆ˜ ์žˆ๋„๋ก ํ…œํ”Œ๋ฆฟ ๋ฉ”์„œ๋“œ ํŒจํ„ด์„ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. 

์ž์„ธํ•œ ๋‚ด์šฉ์€ ๋งํฌ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ ์š”์ฒญ๋ณ„ ์‹คํ–‰ํ๋ฆ„์— ๋Œ€ํ•œ Log ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค. 

[71701732] OrderController.createInvitation()
[71701732] |-->InvitationService.saveInvitationMaster
[71701732] |<--InvitationService.saveInvitationMaster time=34ms
[71701732] OrderController.createInvitation() time=40ms

 

 

 

๐Ÿ“š ์ง„ํ–‰ํ•˜๋ฉฐ ๋งˆ์ฃผํ•œ ์ด์Šˆ๋‚˜ ํ•™์Šต ๊ฒฝํ—˜์„ ์ •๋ฆฌํ•œ ๊ธ€๋“ค์„ ์•„๋ž˜์— ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค

 

๐Ÿ“• ์Šคํ† ๋ฆฌ๋ณด๋“œ