Skip to content

Conversation

@yongjun0511
Copy link
Collaborator

@yongjun0511 yongjun0511 commented Sep 24, 2025

#️⃣ Issue Number

📝 요약(Summary)

  • 로깅 기능을 구현했습니다.

💬 공유사항 to 리뷰어

로깅 기능 구현 Details

logback-spring-xml

로그 공통 형식을 다음과 같이 설정했습니다.

  • [INFO] [http-nio-9000-ex] [reqId=123e4567] [com.eatssu.api.UserController] [Thread 번호] [RequestId] + ( 로그 내용 )
  • Thread를 통해 동시성 문제에 관한 부분을 확인할 수 있고 RequestId를 기준으로 필터링하면 작업 단위로 로그를 확인하실 수 있습니다.
  • RequestId를 수집하기 위해서 MDC 필터를 추가했습니다.
  • 유관 ref : https://mangkyu.tistory.com/266
스크린샷 2025-09-25 오후 7 03 07 - 다음과 같이 보이며 args 도입부에 userId(요청자)가 보이도록 설정했습니다.

래핑 비동기 처리 관련

  • AsyncAppender로 래핑하여 이유는 애플리케이션 쓰레드가 로그 I/O 때문에 지연되지 않도록 처리했습니다.
  • 이때 대기 Queue와 Discard Threshold는 Default (256, 20%)로 사용합니다.(문제 생기면 수정) ref : https://logback.qos.ch/manual/appenders.html
  • 트래픽의 규모에 따라서 큐가 터질 것을 대비해서 never block = true만 달아줬습니다 ref: https://mangkyu.tistory.com/429

로그는 이렇게 처리했어요

Controller Layer

컨르롤러의 로그는 대부분 비슷한 형식을 가지고 있을 것이라고 생각했습니다.

  • 따라서, 컨트롤러의 로그는 AOP로 처리됩니다.ControllerLogAspect에서 구현됩니다.
  • REQUEST : 누가 어떤 요청을 어떤 주소로 요청했는지 확인할 수 있습니다.

ex)

REQUEST GET /v2/reviews/my args=userId=51, lastReviewId=null, pageable={"sort":{"empty":false,"sorted":true,"unsorted":false},"offset":0,"pageNumber":0,"pageSize":20,"paged":true,"unpaged":false}
  • Response : 요청에 대한 결과와 소요 시간 + 결과 (600자까지만)를 확인할 수 있습니다. (어떤 쿼리가 나가고 이런 부분은 Metric 수집에서 하는 것이 적절해 보여요! 소요 시간 확인하고 직접 쿼리 보면 되서... 이정도 까지는 필요 없어 보입니다)

ex)

RESPONSE GET /v2/reviews/my (428 ms) result={"isSuccess":true,"code":1000,"message":"요청에 성공하였습니다.","result":{"numberOfElements":10,"hasNext":false,"dataList":[{"reviewId":169,"rating":null,"wri

Service Layer

  • 영속이 바뀌는 부분을 추가적으로 로깅해 주었습니다
  • 제가 보기에 비즈니스 로직상 추후에 문제를 일으킬 수도 있는 부분들은 WARN으로 두었습니다. ( Service Layer는 이 부분들 봐주시면 감사하겠습니다 + 추가적으로 로그가 필요할 수 있는 부분이 있다면 말씀해 주세요!)

⭐ 영속에 대한 Service Log 비동기 처리

  • 영속이 바뀌는 부분에서 바로 로그를 찍을 경우, 에러가 나서 트랜잭션이 롤백 된다면 로그와 실제 상황이 일치하지 않는 문제가 생깁니다.
  • 따라서, 이런 부분은 LogEvent를 발행하고 AfterCommit으로 비동기 처리했습니다.
  • 해당 과정에서 이벤트 마다 별도의 DTO를 만드는 구조는 너무 많은 리소스가 들어간다고 판단해서 String값에 로그 내용만 넣도록 LogEvent 내부를 구현했습니다.
  • Gemini 요구 사항 반영 -> 민감 정보 마스킹 어노테이션을 만듬

✅ PR Checklist

PR이 다음 요구 사항을 충족하는지 확인하세요.

  • 커밋 메시지 컨벤션에 맞게 작성했습니다.
  • 변경 사항에 대한 테스트를 했습니다.(버그 수정/기능에 대한 테스트).

@yongjun0511 yongjun0511 self-assigned this Sep 24, 2025
@yongjun0511 yongjun0511 added the feat 개발, 구현 label Sep 24, 2025
@yongjun0511 yongjun0511 linked an issue Sep 24, 2025 that may be closed by this pull request
@gemini-code-assist
Copy link

Summary of Changes

Hello @yongjun0511, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 애플리케이션에 강력한 로깅 인프라를 도입하고 이를 주요 서비스에 통합합니다. 변경 사항은 시스템의 관찰 가능성을 높여 애플리케이션 동작을 모니터링하고, 문제를 디버깅하며, 사용자 활동을 추적하는 것을 용이하게 합니다. 또한, 지속적인 로깅을 위한 Docker 배포를 포함하여 PLG 스택 관련 CI/CD 구성을 포함합니다.

Highlights

  • 포괄적인 로깅 시스템 구현: Logback, AOP(Aspect-Oriented Programming), MDC(Mapped Diagnostic Context) 및 Spring의 이벤트 발행 메커니즘을 활용하여 애플리케이션 전반에 걸쳐 중앙 집중식 로깅 시스템을 구축했습니다.
  • Docker 및 CI/CD 통합: 로그 지속성을 지원하고 PLG 스택 CI/CD 파이프라인을 준비하기 위해 Dockerfile 및 docker-compose.yml 파일이 업데이트 및 추가되었습니다.
  • 애플리케이션 주요 동작 로깅: 사용자 업데이트, 리뷰 생성, 좋아요, 회원 탈퇴 등 핵심 애플리케이션 동작에 대한 로깅 이벤트가 추가되어 추적 가능성과 디버깅 효율성이 향상되었습니다.
  • 성능 최적화: 비동기 로깅과 트랜잭션 이벤트 리스너를 도입하여 로깅 작업이 애플리케이션의 핵심 성능에 영향을 미치지 않도록 보장했습니다.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/deploy.yml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.


코드 흐름 따라, 로그는 길을 밝히네, 오류는 숨지 못해.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

이번 PR은 로깅 기능 구현과 CI/CD 관련 작업을 중심으로 진행되었습니다. 전반적으로 로깅 인프라(AOP, MDC, 이벤트 기반 로깅, Logback 설정)가 체계적으로 잘 구축되었습니다. 특히 트랜잭션 커밋 후에만 로그를 남기는 이벤트 리스너와 요청 추적을 위한 MDC 사용은 매우 좋은 접근 방식입니다. 다만, 몇 가지 중요한 보안 및 설정 관련 개선점이 보입니다. 민감한 사용자 정보(이메일, 문의 내용, 비밀번호 등)가 로그에 노출될 수 있는 부분이 있어 수정이 필요합니다. 또한, Logback 설정에서 로그 보관 기간과 파일 로그 포맷을 개선하여 운영 환경에서의 안정성과 추적 용이성을 높이는 것을 제안합니다. 아래에 자세한 리뷰를 남겼으니 확인 부탁드립니다.

Comment on lines 57 to 72
String otherArgsJson = IntStream.range(0, args.length)
.filter(i -> !(args[i] instanceof HttpServletRequest))
.filter(i -> !(args[i] instanceof CustomUserDetails))
.filter(i -> !(args[i] instanceof org.springframework.validation.BindingResult))
.mapToObj(i -> {
String name = (paramNames != null && i < paramNames.length) ? paramNames[i] : "arg" + i;
Object arg = args[i];
try {
String value = objectMapper.writeValueAsString(arg);
if (value.length() > 200) value = value.substring(0, 200) + "...(truncated)";
return name + "=" + value;
} catch (Exception e) {
return name + "=" + String.valueOf(arg);
}
})
.collect(Collectors.joining(", "));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

컨트롤러의 모든 요청 인자를 JSON으로 변환하여 로그에 기록하고 있습니다. 이 방식은 디버깅에 유용하지만, 회원가입이나 로그인 시 비밀번호, 또는 개인정보 수정 시 민감한 정보가 그대로 로그 파일에 노출될 수 있는 심각한 보안 취약점을 야기합니다. objectMapper.writeValueAsString(arg)를 호출하기 전에, 로깅할 객체에 민감한 정보가 포함되어 있는지 확인하고 마스킹 처리하는 로직이 반드시 필요합니다. 예를 들어, 특정 필드명(e.g., 'password', 'secret')을 기준으로 마스킹하거나, DTO 필드에 @LogMask와 같은 어노테이션을 추가하여 선택적으로 마스킹하는 방법을 고려해볼 수 있습니다. 지금 상태로는 운영 환경에서 심각한 정보 유출로 이어질 수 있습니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

커스텀 어노테이션 만들어서 마스킹 해주었어요

Comment on lines 34 to 36
eventPublisher.publishEvent(LogEvent.of(String.format(
"Inquiry created: id=%d, userId=%d, email=%s, content=%s",
saved.getId(), user.getId(), request.getEmail(), request.getContent())));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

사용자 문의 내용과 이메일 주소와 같은 민감한 개인정보를 로그에 직접 기록하고 있습니다. INFO 레벨 로그는 보통 수집 및 분석 대상이 되므로, 개인정보가 포함될 경우 정보 유출의 위험이 있습니다. 문의 내용(request.getContent())과 이메일(request.getEmail()) 대신, 문의 ID와 사용자 ID 같은 식별자만 로그에 남기는 것을 권장합니다. 만약 내용 로깅이 꼭 필요하다면, 길이를 제한하거나 일부를 마스킹 처리하는 방안을 고려해 보세요.

Suggested change
eventPublisher.publishEvent(LogEvent.of(String.format(
"Inquiry created: id=%d, userId=%d, email=%s, content=%s",
saved.getId(), user.getId(), request.getEmail(), request.getContent())));
eventPublisher.publishEvent(LogEvent.of(String.format(
"Inquiry created: id=%d, userId=%d",
saved.getId(), user.getId())));

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했슴다 이거

Comment on lines 88 to 93
eventPublisher.publishEvent(LogEvent.of(
String.format("User withdrawn: userId=%d, email=%s, nickname=%s",
user.getId(),
user.getEmail(),
user.getNickname())
));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

회원 탈퇴 시 사용자의 이메일과 닉네임을 로그에 기록하고 있습니다. 이메일은 중요한 개인정보(PII)에 해당하며, 이를 평문으로 로그에 남기는 것은 보안상 위험할 수 있습니다. 탈퇴 이벤트 로깅 시에는 이메일과 같은 민감 정보는 제외하고, 사용자 ID와 같이 식별자만 기록하는 것이 안전합니다.

        eventPublisher.publishEvent(LogEvent.of(
                String.format("User withdrawn: userId=%d",
                        user.getId())
        ));

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영완

<file>${LOG_PATH}/${APP_NAME}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${APP_NAME}-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>1</maxHistory>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

로그 파일의 maxHistory가 1로 설정되어 있어, 로그가 하루만 보관되고 삭제됩니다. 운영 환경에서 장애 추적이나 데이터 분석을 위해 최소 7일에서 30일 정도의 로그를 보관하는 것이 일반적입니다. 현재 설정은 과거 문제 발생 시 원인 분석을 어렵게 할 수 있으므로, 서비스 정책에 맞게 보관 기간을 늘리는 것을 검토해 보세요. 이 설정은 ERROR_FILE appender(38라인)에도 동일하게 적용하는 것이 좋습니다.

Suggested change
<maxHistory>1</maxHistory>
<maxHistory>30</maxHistory>

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 파일로 저장을 안하면서 없어졌어요~

<maxHistory>1</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%level] [%thread] %logger{36} - %msg%n</pattern>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

파일에 기록되는 로그(MAIN_FILE)의 패턴에 requestId가 빠져있습니다. 콘솔 로그에는 [reqId=%X{requestId}]가 포함되어 있어 요청 추적이 용이하지만, 파일 로그에는 이 정보가 없어 특정 요청과 관련된 로그들을 묶어서 확인하기 어렵습니다. 파일 로그 패턴에도 requestId를 추가하여 디버깅 및 모니터링 효율을 높이는 것을 권장합니다. ERROR_FILE appender(46라인)의 패턴에도 동일하게 적용해주세요.

Suggested change
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%level] [%thread] %logger{36} - %msg%n</pattern>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%level] [%thread] [reqId=%X{requestId}] %logger{36} - %msg%n</pattern>

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이것도 해결했어요.

@yongjun0511 yongjun0511 changed the title feat: 로깅 기능 구현 및 PLG 스택 관련 CI/CD 작업 feat: 로깅 기능 구현 및 Sep 25, 2025
@yongjun0511 yongjun0511 changed the title feat: 로깅 기능 구현 및 feat: 로깅 기능 구현 Sep 25, 2025
Copy link
Collaborator

@JjungminLee JjungminLee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다! 수고하셨어요! 망나니 개발자님 블로그랑 코드 같이 보니까 재밌네욤!! 굳굳

.average()
.orElse(0.0);

if (!reviews.isEmpty() && averageRating == 0.0) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rating이 0.0일때는 log.warn이 오는게 맞을까요..? 어제 회의에서는 0.0일때도 화면 그냥 보여주는거 같아서 뭐가 더 적절할지 고민이 되네요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 리뷰가 존재함에도 불구하고 rating이 0.0이 나온다면 "리뷰 데이터에 문제가 있지 않을까?" 라고 생각을 했어요!

리뷰를 달려면 별점을 입력해야 하는 것 같아서.. 혹시 맞을까요!?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네네 맞아요! 별점 입력해야 리뷰 달기 가능이에요!
두 조건 다 검증하는 것으로 가시쥬 지금 그대로 기기해도 좋습니다!

Dockerfile Outdated
WORKDIR /app

# 로그 디렉토리 미리 생성
RUN mkdir -p /app/logs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 로그 쌓는거 너무 좋은데요! 근데 로그가 너무 많이 쌓이면 ec2자체가 터져버릴 수도 있는 상황도 생각해본다면! 혹시 cron으로 주기적으로 로그를 청소해보는건 어떨까 하는 생각을 해봤어요.! 근데 나중에 결정적일때 로그가 없어져버리는 상황도 생각해야겠네요 ㅋㅎㅋㅎ

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헛 이 부분은 로그를 출력하는 방식을 변경해서 함께 삭제를 해야하는데 반영을 못했네욥! 삭제하겠습니다!

로그 청소에 대해서 조금 알아봤는데요,
지금 처럼 따로 로그에 대한 설정을 하지 않는 경우 도커 데몬의 json-file 드라이버 의해 관리됩니다.

  • (모든 컨테이너의 로그에 대한 json을 도커 엔진이 관리하게 됨.)

보통은 docker run을 할 때 롤링 정책을 설정할 수 있는 것 같아서 그 방법으로 용량을 관리해도 좋아 보입니다.

스크린샷 2025-09-30 오후 5 45 34 https://docs.docker.com/engine/logging/drivers/local/
  • Cloud Front/Promtail 같은 외부 수집기를 사용할 경우는 로컬에 많은 용량을 저장하고 있을 필요는 없어보이고 로컬 엔진에서 20M* 5로테이션 = 100 MB 정도만 저장하고 있어서 저렇게 최근 로그만 가지고 있어도 괜찮아 보입니다! ( 20GB 정도의 용량이 남아있어서 사용하면서 불편한 점이 있다면 수정해도 좋을 것 같습니다.)
--log-driver=json-file \
--log-opt max-size=20m \
--log-opt max-file=5 \       

이 부분을 Docker Run에 추가해 두었습니다 !

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 좋아요!!


@Target({ElementType.FIELD, ElementType.RECORD_COMPONENT})
@Retention(RetentionPolicy.RUNTIME)
public @interface LogMask {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오왕 이거 진짜 만드신거구나 짱짱!

@JjungminLee
Copy link
Collaborator

영속이 바뀌는 부분에서 바로 로그를 찍을 경우, 에러가 나서 트랜잭션이 롤백 된다면 로그와 실제 상황이 일치하지 않는 문제가 생깁니다. 따라서, 이런 부분은 LogEvent를 발행하고 AfterCommit으로 비동기 처리했습니다.

=> 이 부분은 결국 비동기처리 된다면 로그가 시간순으로 쌓일수 없는것으로 이해했는데요!

ReviewServiceV2.java

eventPublisher.publishEvent(LogEvent.of(
                String.format("MealReview created: reviewId=%d, mealId=%d, userId=%d, images=%d, menuLikes=%d",
                        review.getId(),
                        meal.getId(),
                        user.getId(),
                        request.getImageUrls().size(),
                        request.getMenuLikes().size())
        ));


=> 혹시 괜찮으시다면 시간도 찍어주실수 있나요? 소요시간 말고 요청한 시간입니다 이를테면 2025-09-29:13:49:21 이런식으로요~! 혹시 이 부분이 불요하거나 이미 관리하고 있다면 말씀해주시어요

@yongjun0511
Copy link
Collaborator Author

영속이 바뀌는 부분에서 바로 로그를 찍을 경우, 에러가 나서 트랜잭션이 롤백 된다면 로그와 실제 상황이 일치하지 않는 문제가 생깁니다. 따라서, 이런 부분은 LogEvent를 발행하고 AfterCommit으로 비동기 처리했습니다.

=> 이 부분은 결국 비동기처리 된다면 로그가 시간순으로 쌓일수 없는것으로 이해했는데요!

ReviewServiceV2.java

eventPublisher.publishEvent(LogEvent.of(
                String.format("MealReview created: reviewId=%d, mealId=%d, userId=%d, images=%d, menuLikes=%d",
                        review.getId(),
                        meal.getId(),
                        user.getId(),
                        request.getImageUrls().size(),
                        request.getMenuLikes().size())
        ));

=> 혹시 괜찮으시다면 시간도 찍어주실수 있나요? 소요시간 말고 요청한 시간입니다 이를테면 2025-09-29:13:49:21 이런식으로요~! 혹시 이 부분이 불요하거나 이미 관리하고 있다면 말씀해주시어요

스크린샷 2025-09-30 오후 6 10 18

logback-spring.xml에서 모든 로그에 이런식으로 (시간 , 쓰레드ID, 요청ID)가 뜨도록 해놔서 편도로 추가할 필요는 없슴다 😄

.average()
.orElse(0.0);

if (!reviews.isEmpty() && averageRating == 0.0) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네네 맞아요! 별점 입력해야 리뷰 달기 가능이에요!
두 조건 다 검증하는 것으로 가시쥬 지금 그대로 기기해도 좋습니다!

Dockerfile Outdated
WORKDIR /app

# 로그 디렉토리 미리 생성
RUN mkdir -p /app/logs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 좋아요!!

@JjungminLee
Copy link
Collaborator

로그 시간까지 다 확인했습니다-! dev로 머지해도 될것같아요! 수고하셨어요~!!

@yongjun0511 yongjun0511 merged commit 507108c into develop Sep 30, 2025
@yongjun0511 yongjun0511 deleted the feature/#243-plg-logging-stack branch September 30, 2025 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 개발, 구현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feature: 로깅 기능 구현

3 participants