Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions .github/workflows/cd-develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,19 @@ jobs:
run: |
echo "🧹 Cleaning up all existing containers"

# Stop and remove specific containers
# Stop and remove specific containers (로그는 볼륨에 보존됨)
sudo docker stop github-actions-demo || true
sudo docker rm github-actions-demo || true

sudo docker stop elasticsearch || true
sudo docker rm elasticsearch || true

echo "📋 Ensuring log directory exists on host"
sudo mkdir -p /var/log/wayble
sudo chmod 755 /var/log/wayble

echo "🧯 Cleaning up unused Docker networks"
sudo docker system prune -f || true
echo "🧯 Cleaning up unused Docker networks (excluding volumes)"
sudo docker system prune -f --volumes=false || true

- name: Create Docker network if not exists
run: |
Expand Down Expand Up @@ -184,6 +187,18 @@ jobs:
exit 1
fi

# 로그 파일 상태 확인
echo "=== Log Directory Status ==="
ls -la /var/log/wayble/ || echo "Log directory not found"

if [ -f "/var/log/wayble/wayble-error.log" ]; then
echo "✅ Error log file exists"
echo "📊 Error log file size: $(du -h /var/log/wayble/wayble-error.log | cut -f1)"
echo "📅 Last modified: $(stat -c %y /var/log/wayble/wayble-error.log)"
else
echo "ℹ️ No error log file yet (normal for new deployment)"
fi

# ✅ 배포 성공 알림 (Discord)
- name: Send success webhook to Discord
if: success()
Expand All @@ -204,7 +219,6 @@ jobs:




# on: #이 워크플로우가 언제 실행될지 트리거를 정의함.
# pull_request:
# types : [closed] #누군가가 Pull request를 닫았을 때 실행됨.
Expand Down
6 changes: 6 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ FROM openjdk:17
# 인자 설정 - JAR_File
ARG JAR_FILE=build/libs/*.jar

# 로그 디렉토리 생성 및 권한 설정
RUN mkdir -p /app/logs && chmod 755 /app/logs

# 작업 디렉토리 설정
WORKDIR /app

# jar 파일 복제
COPY ${JAR_FILE} app.jar

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class GlobalExceptionHandler {

@ExceptionHandler(ApplicationException.class)
public ResponseEntity<CommonResponse> handleApplicationException(ApplicationException e, WebRequest request) {
// 비즈니스 예외 로그 기록 (간결하게)

String path = ((ServletWebRequest) request).getRequest().getRequestURI();
String method = ((ServletWebRequest) request).getRequest().getMethod();

Expand All @@ -47,36 +47,31 @@ public ResponseEntity<CommonResponse> handleApplicationException(ApplicationExce

CommonResponse commonResponse = CommonResponse.error(e.getErrorCase());

HttpStatus status = HttpStatus.valueOf(e.getErrorCase().getHttpStatusCode());
//sendToDiscord(e, request, status);

return ResponseEntity
.status(e.getErrorCase().getHttpStatusCode())
.body(commonResponse);
}

@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseEntity<CommonResponse> handleValidException(BindingResult bindingResult,
MethodArgumentNotValidException ex,
WebRequest request) {
String message = bindingResult.getAllErrors().get(0).getDefaultMessage();
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<CommonResponse> handleValidException(MethodArgumentNotValidException ex, WebRequest request) {
String message = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
Comment on lines +55 to +57
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

검증 예외 메시지 추출 시 안전성 보강 (빈 에러/NPE 방지)

getAllErrors().get(0)는 드물게 비어 있을 수 있어 IndexOutOfBoundsException 가능성이 있습니다. 메시지가 null/blank인 경우도 대비해 기본 메시지를 두는 것이 안전합니다.

다음과 같이 방어 로직을 적용해 주세요:

-        String message = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
+        String message = "요청 검증에 실패했습니다.";
+        if (ex.getBindingResult() != null) {
+            if (!ex.getBindingResult().getAllErrors().isEmpty()) {
+                String m = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
+                if (m != null && !m.isBlank()) {
+                    message = m;
+                }
+            } else if (ex.getMessage() != null && !ex.getMessage().isBlank()) {
+                message = ex.getMessage();
+            }
+        } else if (ex.getMessage() != null && !ex.getMessage().isBlank()) {
+            message = ex.getMessage();
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<CommonResponse> handleValidException(MethodArgumentNotValidException ex, WebRequest request) {
String message = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<CommonResponse> handleValidException(MethodArgumentNotValidException ex, WebRequest request) {
// 기본 메시지 설정
String message = "요청 검증에 실패했습니다.";
if (ex.getBindingResult() != null) {
if (!ex.getBindingResult().getAllErrors().isEmpty()) {
String m = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
if (m != null && !m.isBlank()) {
message = m;
}
} else if (ex.getMessage() != null && !ex.getMessage().isBlank()) {
message = ex.getMessage();
}
} else if (ex.getMessage() != null && !ex.getMessage().isBlank()) {
message = ex.getMessage();
}
// 이후 로직 계속 (예: CommonResponse 생성 및 반환)
CommonResponse body = CommonResponse.error(message);
return ResponseEntity.badRequest().body(body);
}
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java
around lines 55-57, the current use of
ex.getBindingResult().getAllErrors().get(0).getDefaultMessage() can throw
IndexOutOfBoundsException when the error list is empty and may return
null/blank; update the handler to defensively retrieve the first error only
after checking that getAllErrors() is non-null and non-empty, extract
getDefaultMessage() safely (treating null/blank as absent), and fall back to a
sensible default message such as "Invalid request" (or a constant) if no usable
message exists; ensure no NPEs by null-checking the BindingResult and
DefaultMessage and consider trimming the message before returning it in the
ResponseEntity.


// 에러 로그 기록
String path = ((ServletWebRequest) request).getRequest().getRequestURI();
String method = ((ServletWebRequest) request).getRequest().getMethod();
String errorLocation = getErrorLocation(ex);

log.error("Validation Exception 발생 - Method: {}, Path: {}, Message: {}, Location: {}",
method, path, message, errorLocation, ex);
log.warn("Validation Exception - Method: {}, Path: {}, Message: {}, Location: {}",
method, path, message, errorLocation);

CommonResponse commonResponse = CommonResponse.error(400, message);

sendToDiscord(ex, request, HttpStatus.BAD_REQUEST);

return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(commonResponse);
}


/**
* 모든 예상하지 못한 예외 처리
*/
Expand All @@ -102,11 +97,17 @@ private void sendToDiscord(Exception ex, WebRequest request, HttpStatus status)
String path = ((ServletWebRequest) request).getRequest().getRequestURI();
String timestamp = Instant.now().toString();

if (!env.acceptsProfiles(Profiles.of("develop"))) {
if (!env.acceptsProfiles(Profiles.of("local"))) {
log.info("현재 active 프로파일이 develop가 아니므로 Discord 알림을 보내지 않습니다.");
return;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

프로파일 조건 검증 필요

현재 코드는 local 프로파일이 아닌 경우 Discord 알림을 보내지 않도록 되어 있으나, 로그 메시지는 여전히 "develop가 아니므로"라고 표시됩니다. 또한 PR 목적에서 언급된 대로 production 환경에서도 중요한 에러는 알림을 받아야 할 수 있습니다.

-        if (!env.acceptsProfiles(Profiles.of("local"))) {
-            log.info("현재 active 프로파일이 develop가 아니므로 Discord 알림을 보내지 않습니다.");
+        if (!env.acceptsProfiles(Profiles.of("develop", "production"))) {
+            log.info("현재 active 프로파일이 develop 또는 production이 아니므로 Discord 알림을 보내지 않습니다.");
             return;
         }
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java
around lines 100-103, the profile check is inverted and the log message is
wrong: change the condition to skip Discord notifications only when the active
profile is "local" (remove the negation), update the log message to reference
"local" instead of "develop", and ensure production and other non-local profiles
will proceed to send important error alerts; keep the check clear (e.g.,
env.acceptsProfiles(Profiles.of("local")) -> log and return).


// 특정 예외 타입 및 경로에 대한 Discord 알림 제외
if (shouldSkipDiscordNotification(ex, path)) {
log.debug("Discord 알림 제외 - Exception: {}, Path: {}", ex.getClass().getSimpleName(), path);
return;
}

// Embed 필드 구성
DiscordWebhookPayload.Embed embed = new DiscordWebhookPayload.Embed(
"🚨 서버 에러 발생",
Expand Down Expand Up @@ -136,6 +137,108 @@ private void sendToDiscord(Exception ex, WebRequest request, HttpStatus status)
}
}

/**
* Discord 알림을 보내지 않을 예외인지 판단
*/
private boolean shouldSkipDiscordNotification(Exception ex, String path) {
String exceptionName = ex.getClass().getSimpleName();
String message = ex.getMessage();

// 1. NoResourceFoundException 제외 (static resource 요청)
if ("NoResourceFoundException".equals(exceptionName)) {
return true;
}

// 2. 특정 경로 패턴 제외
if (isIgnoredPath(path)) {
return true;
}

// 3. 봇이나 크롤러 요청으로 인한 에러 제외
if (isBotOrCrawlerRequest(message)) {
return true;
}

// 4. 기타 불필요한 예외들
if (isIgnoredException(exceptionName, message)) {
return true;
}

return false;
}

/**
* 무시할 경로인지 확인
*/
private boolean isIgnoredPath(String path) {
String[] ignoredPaths = {
"/favicon.ico",
"/index.html",
"/robots.txt",
"/sitemap.xml",
"/apple-touch-icon",
"/.well-known/",
"/wp-admin/",
"/admin/",
"/phpmyadmin/",
"/xmlrpc.php",
"/.env",
"/config.php"
};

for (String ignoredPath : ignoredPaths) {
if (path.contains(ignoredPath)) {
return true;
}
}

return false;
}

Comment on lines +170 to +197
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

isIgnoredPath: 대소문자/널 안전성 보강

요청 경로가 null이거나 대문자 포함 시 매칭 누락 가능성이 있습니다. 소문자 변환 후 비교하도록 안전성 보강을 권장합니다.

-    private boolean isIgnoredPath(String path) {
-        String[] ignoredPaths = {
+    private boolean isIgnoredPath(String path) {
+        if (path == null) return false;
+        String lowerPath = path.toLowerCase();
+        String[] ignoredPaths = {
             "/favicon.ico",
             "/index.html", 
             "/robots.txt",
             "/sitemap.xml",
             "/apple-touch-icon",
             "/.well-known/",
             "/wp-admin/",
             "/admin/",
             "/phpmyadmin/",
             "/xmlrpc.php",
             "/.env",
             "/config.php"
         };
         
-        for (String ignoredPath : ignoredPaths) {
-            if (path.contains(ignoredPath)) {
+        for (String ignoredPath : ignoredPaths) {
+            if (lowerPath.contains(ignoredPath)) {
                 return true;
             }
         }
         
         return false;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 무시할 경로인지 확인
*/
private boolean isIgnoredPath(String path) {
String[] ignoredPaths = {
"/favicon.ico",
"/index.html",
"/robots.txt",
"/sitemap.xml",
"/apple-touch-icon",
"/.well-known/",
"/wp-admin/",
"/admin/",
"/phpmyadmin/",
"/xmlrpc.php",
"/.env",
"/config.php"
};
for (String ignoredPath : ignoredPaths) {
if (path.contains(ignoredPath)) {
return true;
}
}
return false;
}
private boolean isIgnoredPath(String path) {
if (path == null) return false;
String lowerPath = path.toLowerCase();
String[] ignoredPaths = {
"/favicon.ico",
"/index.html",
"/robots.txt",
"/sitemap.xml",
"/apple-touch-icon",
"/.well-known/",
"/wp-admin/",
"/admin/",
"/phpmyadmin/",
"/xmlrpc.php",
"/.env",
"/config.php"
};
for (String ignoredPath : ignoredPaths) {
if (lowerPath.contains(ignoredPath)) {
return true;
}
}
return false;
}
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java
around lines 170 to 197, the isIgnoredPath method is not null-safe and is
case-sensitive; update it to first handle a null path (return false immediately)
and normalize the incoming path to lowercase (and optionally trim) before
checking contains against the ignoredPaths entries (which can remain lowercase),
so comparisons succeed regardless of input case and avoid NPEs.

/**
* 봇이나 크롤러 요청인지 확인
*/
private boolean isBotOrCrawlerRequest(String message) {
if (message == null) return false;

String[] botIndicators = {
"No static resource",
"Could not resolve view",
"favicon",
"robots.txt"
};

for (String indicator : botIndicators) {
if (message.contains(indicator)) {
return true;
}
}

return false;
}

Comment on lines +198 to +219
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

봇/크롤러 감지: 대소문자 불일치 보완

메시지 비교를 소문자 기준으로 수행해 탐지 누락을 줄이는 것을 권장합니다.

-    private boolean isBotOrCrawlerRequest(String message) {
-        if (message == null) return false;
-        
-        String[] botIndicators = {
-            "No static resource",
-            "Could not resolve view",
-            "favicon",
-            "robots.txt"
-        };
-        
-        for (String indicator : botIndicators) {
-            if (message.contains(indicator)) {
-                return true;
-            }
-        }
-        
-        return false;
-    }
+    private boolean isBotOrCrawlerRequest(String message) {
+        if (message == null) return false;
+
+        String msg = message.toLowerCase();
+        String[] botIndicators = {
+            "no static resource",
+            "could not resolve view",
+            "favicon",
+            "robots.txt"
+        };
+
+        for (String indicator : botIndicators) {
+            if (msg.contains(indicator)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 봇이나 크롤러 요청인지 확인
*/
private boolean isBotOrCrawlerRequest(String message) {
if (message == null) return false;
String[] botIndicators = {
"No static resource",
"Could not resolve view",
"favicon",
"robots.txt"
};
for (String indicator : botIndicators) {
if (message.contains(indicator)) {
return true;
}
}
return false;
}
private boolean isBotOrCrawlerRequest(String message) {
if (message == null) return false;
String msg = message.toLowerCase();
String[] botIndicators = {
"no static resource",
"could not resolve view",
"favicon",
"robots.txt"
};
for (String indicator : botIndicators) {
if (msg.contains(indicator)) {
return true;
}
}
return false;
}
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java
around lines 198 to 219, the bot/crawler detection does a case-sensitive
contains check which can miss matches; normalize the input and indicators to a
consistent case (e.g., call message = message.toLowerCase(Locale.ROOT) and
compare against lower-cased indicator strings or call
message.toLowerCase(Locale.ROOT).contains(indicator.toLowerCase(Locale.ROOT)))
so all comparisons are case-insensitive and deterministic.

/**
* 무시할 예외인지 확인
*/
private boolean isIgnoredException(String exceptionName, String message) {
// 클라이언트 연결 종료 관련
if ("ClientAbortException".equals(exceptionName) ||
"BrokenPipeException".equals(exceptionName)) {
return true;
}

// 타임아웃 관련 (너무 빈번한 경우)
if (message != null && (
message.contains("Connection timed out") ||
message.contains("Read timed out") ||
message.contains("Connection reset")
)) {
return true;
}

return false;
}

Comment on lines +220 to +241
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

무시 예외 케이스 보강: broken pipe 등 메시지 기반 추가, 대소문자 보완

실무에서 “Broken pipe”, “Connection reset by peer” 형태의 메시지가 자주 발생합니다. 메시지 기반 케이스를 소문자로 통일해 보강하세요.

-    private boolean isIgnoredException(String exceptionName, String message) {
+    private boolean isIgnoredException(String exceptionName, String message) {
         // 클라이언트 연결 종료 관련
         if ("ClientAbortException".equals(exceptionName) || 
             "BrokenPipeException".equals(exceptionName)) {
             return true;
         }
         
         // 타임아웃 관련 (너무 빈번한 경우)
-        if (message != null && (
-            message.contains("Connection timed out") ||
-            message.contains("Read timed out") ||
-            message.contains("Connection reset")
-        )) {
-            return true;
-        }
+        if (message != null) {
+            String msg = message.toLowerCase();
+            if (msg.contains("connection timed out")
+                || msg.contains("read timed out")
+                || msg.contains("connection reset")
+                || msg.contains("connection reset by peer")
+                || msg.contains("broken pipe")) {
+                return true;
+            }
+        }
         
         return false;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 무시할 예외인지 확인
*/
private boolean isIgnoredException(String exceptionName, String message) {
// 클라이언트 연결 종료 관련
if ("ClientAbortException".equals(exceptionName) ||
"BrokenPipeException".equals(exceptionName)) {
return true;
}
// 타임아웃 관련 (너무 빈번한 경우)
if (message != null && (
message.contains("Connection timed out") ||
message.contains("Read timed out") ||
message.contains("Connection reset")
)) {
return true;
}
return false;
}
/**
* 무시할 예외인지 확인
*/
private boolean isIgnoredException(String exceptionName, String message) {
// 클라이언트 연결 종료 관련
if ("ClientAbortException".equals(exceptionName) ||
"BrokenPipeException".equals(exceptionName)) {
return true;
}
// 타임아웃 및 연결 종료 메시지 처리 (너무 빈번한 경우)
if (message != null) {
String msg = message.toLowerCase();
if (msg.contains("connection timed out")
|| msg.contains("read timed out")
|| msg.contains("connection reset")
|| msg.contains("connection reset by peer")
|| msg.contains("broken pipe")) {
return true;
}
}
return false;
}
🤖 Prompt for AI Agents
In src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java
around lines 220 to 241, the ignored-exception checks should be made
case-insensitive and extended to match common message variants (e.g., "broken
pipe", "connection reset by peer"). Change exceptionName comparisons to use
case-insensitive checks (equalsIgnoreCase) and normalize message to lower-case
(after null check) then check contains(...) for "broken pipe", "connection reset
by peer", "connection timed out", "read timed out", and "connection reset" so
all variants are caught reliably.

/**
* 예외의 스택트레이스에서 실제 에러 발생 위치를 추출
*/
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/logback-spring.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<maxFileSize>50MB</maxFileSize>
<maxHistory>90</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
<cleanHistoryOnStart>false</cleanHistoryOnStart>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n%ex{2}</pattern>
Expand Down