Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d801e9d
refactor: Modify customException, memberService
comeevery-git Oct 19, 2024
c5d6486
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
e5ab6e1
fix: Apply openai 1.x.x
comeevery-git Oct 19, 2024
d13f89c
fix: Modify openai response format
comeevery-git Oct 19, 2024
f21922f
fix: Modify openai response format
comeevery-git Oct 19, 2024
ead8754
fix: Create gpt_review.txt
comeevery-git Oct 19, 2024
1330aeb
fix: Add log
comeevery-git Oct 19, 2024
a84d9d0
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
25ace9e
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
3dab92f
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
88c0f68
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
af7eeb2
fix: Add log code-review.yml
comeevery-git Oct 19, 2024
ddeb9de
fix: Add log code-review.yml
comeevery-git Oct 19, 2024
6f705f5
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
922112a
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
40bb9e9
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
47a60ff
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
231785a
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
70928d3
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
0d95077
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
3fe36db
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
a14ff3d
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
80e2723
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
87e2c6e
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
1aa7e9b
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
63fd053
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
4b3d9b4
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
48610ee
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
42e7419
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
035b5b1
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
456a8cf
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
7acad3f
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
d05eea0
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
6e45dda
fix: Modify code-review.yml
comeevery-git Oct 19, 2024
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
67 changes: 0 additions & 67 deletions .github/code-review.yml

This file was deleted.

148 changes: 148 additions & 0 deletions .github/workflows/code-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
name: Code Review with GPT

on:
pull_request:
types: [opened, synchronize]
workflow_dispatch:

jobs:
code_review:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'

- name: Install dependencies
run: |
pip install openai requests

- name: Generate diff for code review
id: diff
run: |
git fetch origin ${{ github.event.pull_request.base.ref }} --depth=1
git diff origin/${{ github.event.pull_request.base.ref }} ${{ github.sha }} > pr_diff.txt
shell: bash

- name: Perform Code Review with GPT and Comment on Changed Lines
id: gpt_review
run: |
python <<EOF
import openai
import requests
import json

# OpenAI API Key 설정
openai.api_key = "${{ secrets.OPENAI_API_KEY }}"

# 시스템 프롬프트 및 모델명 가져오기
system_prompt = """${{ secrets.SYSTEM_PROMPT }}"""
model_name = "${{ secrets.OPENAI_MODEL }}"

# 처리된 파일과 라인 기록 (중복 방지)
processed_files = set() # 각 파일을 기록
processed_lines = {} # 각 파일별로 라인을 기록하기 위한 딕셔너리로 수정

# GitHub API 호출 로그
print("Fetching changed files from GitHub API...")

# 변경된 파일과 라인 정보를 가져오기 위한 GitHub API 호출
files_url = f"https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files"
headers = {"Authorization": f"token ${{ secrets.GITHUB_TOKEN }}"}
try:
response = requests.get(files_url, headers=headers, timeout=10)
print(f"GitHub API status code: {response.status_code}") # 상태 코드 로그 출력
response.raise_for_status()
except requests.exceptions.RequestException as e:
print(f"Error fetching files: {e}")
exit(1)

if response.status_code == 200:
print("Files fetched successfully. Analyzing changes...")
files_changed = response.json()

# .java 파일만 필터링
java_files = [file for file in files_changed if file['filename'].endswith('.java')]

if not java_files:
print("No .java files to review.")
exit(0)

for file in java_files:
file_path = file['filename']

# 이미 처리한 파일은 건너뛰기
if file_path in processed_files:
print(f"Skipping already processed file: {file_path}")
continue

print(f"Processing file: {file_path}")
patch = file.get('patch', '')
print(f"Analyzing patch for file: {file_path}")

# 각 파일마다 중복 라인 처리를 위한 집합 생성
if file_path not in processed_lines:
processed_lines[file_path] = set()

# 전체 패치 내용을 GPT에게 전달
if patch: # 패치가 존재할 경우에만 처리
print(f"Calling GPT API for patch in file: {file_path}")
try:
gpt_response = openai.chat.completions.create(
model=model_name,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Here is the code diff for context:\n{patch}"}
],
timeout=30
)
except Exception as e:
print(f"Error in GPT request: {e}")
continue

review_comment = gpt_response.choices[0].message.content
print(f"GPT response received for file: {file_path}")
print(f"Review comment: {review_comment}")

# 변경된 파일과 라인에 리뷰 코멘트를 추가
commit_id = "${{ github.event.pull_request.head.sha }}"
line_number = file.get('patch').split('\n').index(next(line for line in file['patch'].split('\n') if line.startswith('+'))) + 1
comment_body = {
"body": review_comment,
"path": file_path,
"line": line_number,
"side": "RIGHT",
"commit_id": commit_id
}

# 코멘트를 추가하기 전 파일 경로와 위치를 로그로 출력
print(f"Commenting on file: {file_path}, line: {line_number}")

comment_url = f"https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments"
response = requests.post(comment_url, headers=headers, data=json.dumps(comment_body))

# 응답 상태 확인
if response.status_code == 201:
print(f"Comment posted successfully for file: {file_path}")
else:
print(f"Failed to post comment. Status code: {response.status_code}, Response: {response.text}")

# 각 파일을 처리한 후 파일 이름을 기록
processed_files.add(file_path)

else:
print(f"Unexpected status code: {response.status_code}")
exit(1)

# 최종 리뷰 요약 코멘트 추가
final_comment = "### 최종 리뷰 요약: .java 파일에 대한 모든 변경 사항을 검토 완료했습니다."
comment_url = f"https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments"
requests.post(comment_url, headers=headers, data=json.dumps({"body": final_comment}))
print("Final review comment posted.")
exit(0)
EOF
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
package app.common.infrastructure.exception;

import app.common.domain.model.common.ResponseCode;
import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
import java.io.Serializable;

@Getter
public class CustomException extends RuntimeException {
public class CustomException extends RuntimeException implements Serializable {

Choose a reason for hiding this comment

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

Line-by-Line Review Comments

  1. +import java.io.Serializable;

    • 코멘트: Serializable 인터페이스를 추가한 부분은 데이터 직렬화 및 역직렬화에 유용합니다. 하지만 추가적인 검토가 필요할 수 있는 부분입니다.
  2. public class CustomException extends RuntimeException implements Serializable {

    • 코멘트: Serializable을 구현함으로써 이 예외 클래스가 직렬화 가능하다는 점은 좋은 접근입니다. 그러나 예외가 직렬화될 때 추가적인 필드가 포함되는 경우 적절한 readObjectwriteObject 메서드를 구현하는 것이 필요할 수 있습니다.
  3. private final String additionalMessage;

    • 코멘트: 추가 메시지를 위한 변수를 정의한 것은 유용하지만, 이 값이 null인 경우가 있으므로 적절한 검증을 고려해야 합니다.
  4. public CustomException(ResponseCode responseCode) {

    • 코멘트: 생성자에서 기본 값으로 null을 전달하는 것은 괜찮지만, responseCodenull인 경우를 체크해야 합니다.
  5. this(responseCode, null, null);

    • 코멘트: 이것은 문법적으로 문제가 없지만 null 값이 의미하는 바에 대한 주석이 필요할 수 있습니다.
  6. public CustomException(ResponseCode responseCode, String additionalMessage) {

    • 코멘트: 이 생성자는 유용합니다. 그러나 responseCodenull일 경우 예외를 발생시키는 것이 좋습니다.
  7. public CustomException(ResponseCode responseCode, String additionalMessage, Throwable cause) {

    • 코멘트: 이 생성자에서 철자나 인수 검증이 필요하지 않다면, 괜찮습니다. 다만 추가 메시지가 null일 경우를 염두에 두어야 합니다.
  8. super(additionalMessage, cause);

    • 코멘트: additionalMessagenull일 경우 RuntimeException의 메시지에 영향을 줄 수 있으니 더 다루어야 할 필요가 있습니다.
  9. @Override public String getMessage() {...}

    • 코멘트: getMessage 메서드의 구현은 유용하나, 여기서 추가 메시지가 null일 경우를 신중하게 다루어야 합니다. 메시지가 올바르게 구성되어야만 더 깨끗한 출력이 가능합니다.
  10. return (additionalMessage != null) ? responseCode.getMessage() + ": " + additionalMessage : responseCode.getMessage();

    • 코멘트: 이 표현식은 읽기 쉽게 잘 작성되었습니다. 그러나 additionalMessage가 비어있지 않은 경우에 대해서도 추가적인 검증이 필요할 수 있습니다. 공백 메시지를 처리하기 위한 로직이 필요할 것입니다.

Final Review Comment


[Review Summary]

전체적으로 예외 처리 클래스를 개선하는 좋은 방향으로 진행되었습니다. 그러나 다음과 같은 몇 가지 문제가 발견되었습니다.

  • 사전 조건 검토: null 체크가 부족
  • 런타임 오류 체크: null 값으로 인한 예외 발생 가능성
  • 코딩 베스트 프랙티스: 입력 값 검증에 대한 개선 필요

[Issue Summary]

  1. 사전 조건 체크: responseCodeadditionalMessage에 대한 null 체크 부족
  2. 런타임 오류 체크: 생성자에 null 값 전달 가능성
  3. 코딩 베스트 프랙티스: 메시지가 비어 있거나 공백인 경우 처리 사항 미비

[Solutions]

  • responseCodeadditionalMessage에 대해 null이 아닌지 체크하는 로직을 추가하세요.
if (responseCode == null) {
    throw new IllegalArgumentException("Response code must not be null");
}
  • getMessage 메서드에서 additionalMessage가 빈 문자열일 경우를 나타내는 추가 로직을 도입하여 더 명확한 반환값을 제공하는 것이 좋습니다.
return (additionalMessage != null && !additionalMessage.trim().isEmpty())
       ? responseCode.getMessage() + ": " + additionalMessage
       : responseCode.getMessage();

이러한 개선을 통해 코드의 안정성과 가독성을 높일 수 있습니다.

private static final long serialVersionUID = 1L;

private final ResponseCode responseCode;
private final String additionalMessage;

public CustomException(ResponseCode responseCode) {
this(responseCode, null, null);
}

public CustomException(ResponseCode responseCode, String additionalMessage) {
this(responseCode, additionalMessage, null);
}

public CustomException(ResponseCode responseCode, String additionalMessage, Throwable cause) {
super(additionalMessage, cause);
this.responseCode = responseCode;
this.additionalMessage = additionalMessage;
}

@Override
public String getMessage() {
return (additionalMessage != null)
? responseCode.getMessage() + ": " + additionalMessage
: responseCode.getMessage();
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,86 @@
package app.common.infrastructure.exception;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Choose a reason for hiding this comment

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

// 로그를 추가하여 예외를 보다 효과적으로 추적할 수 있게 되었습니다. 잘 하셨습니다.
+    private static final Logger logger = LoggerFactory.getLogger(CustomExceptionHandler.class);

// 커스텀 예외 처리를 위한 로그 추가. 좋은 접근입니다.
+        logger.error("CustomException occurred: ", e);

// 잘못된 인자에 대한 예외 처리에서 구체적인 에러 메시지를 제공. 유용합니다.
+        logger.error("IllegalArgumentException occurred: ", e);

// 리소스가 발견되지 않았을 경우에 대한 에러 처리와 로그 추가. 필수입니다.
+        logger.error("NoHandlerFoundException occurred: ", e);

// 허용되지 않는 HTTP 메소드 호출에 대한 예외 처리. 명확한 응답을 제공하고 있습니다.
+        logger.error("HttpRequestMethodNotSupportedException occurred: ", e);

// 메소드 인자의 유효성 검사 실패에 대한 예외 처리. 좋은 실천입니다.
+        logger.error("MethodArgumentNotValidException occurred: ", e);

// 제약 조건 위반에 대한 예외 처리. 이 점도 잘 처리하고 있습니다.
+        logger.error("ConstraintViolationException occurred: ", e);

// 데이터 접근 예외를 처리하는 부분에서 로그 추가가 좋습니다.
+        logger.error("DataAccessException occurred: ", e);

// 접근 거부 예외에 대한 처리. 이 역시 중요한 부분입니다.
+        logger.error("AccessDeniedException occurred: ", e);

// 예상치 못한 예외 처리에서 로그를 추가하여 예외 추적이 용이해졌습니다.
+        logger.error("Unexpected exception occurred: ", e);

[Review Summary]

  • 사전 조건 체크: 0
  • 런타임 오류 체크: 0
  • 최적화: 0
  • 보안 문제: 0
  • 코딩 모범 사례: 0

[Issue Summary]

  • 없음

[Solutions]

  • 현재 코드에서의 모든 수정 사항이 잘 되어 있으며, 예외 처리를 위한 로그 추가로 인해 코드가 더욱 효과적으로 개선되었습니다. 예외 처리에서 제공하는 응답 메시지도 직관적이고 명확하여 향후 유지보수와 디버깅에 큰 도움이 될 것입니다. 추가적으로 예외가 발생한 경우의 리턴 코드를 통해 문제를 좀 더 체계적으로 파악할 수 있도록 잘 구성되었습니다.

결과적으로, 이 코드는 예외 처리를 잘 수행하고 있으며, 추가적인 개선 사항은 필요하지 않습니다.

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.dao.DataAccessException;
import org.springframework.security.access.AccessDeniedException;

import javax.validation.ConstraintViolationException;

import app.common.domain.model.common.BaseResponse;

@RestControllerAdvice
public class CustomExceptionHandler {
@ExceptionHandler(Exception.class)
protected BaseResponse handleException(Exception e) {
return BaseResponse.failResponse(e);
}

private static final Logger logger = LoggerFactory.getLogger(CustomExceptionHandler.class);

@ExceptionHandler(CustomException.class)
protected BaseResponse handleCustomException(CustomException e) {
return BaseResponse.failResponse(e.getResponseCode());
protected ResponseEntity<BaseResponse> handleCustomException(CustomException e) {
logger.error("CustomException occurred: ", e);
BaseResponse response = BaseResponse.failResponse(e.getResponseCode());
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(IllegalArgumentException.class)
protected ResponseEntity<BaseResponse> handleIllegalArgumentException(IllegalArgumentException e) {
logger.error("IllegalArgumentException occurred: ", e);
BaseResponse response = BaseResponse.failResponse("INVALID_ARGUMENT");
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(NoHandlerFoundException.class)
protected ResponseEntity<BaseResponse> handleNoHandlerFoundException(NoHandlerFoundException e) {
logger.error("NoHandlerFoundException occurred: ", e);
BaseResponse response = BaseResponse.failResponse("RESOURCE_NOT_FOUND");
return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
}

@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
protected ResponseEntity<BaseResponse> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
logger.error("HttpRequestMethodNotSupportedException occurred: ", e);
BaseResponse response = BaseResponse.failResponse("METHOD_NOT_ALLOWED");
return new ResponseEntity<>(response, HttpStatus.METHOD_NOT_ALLOWED);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
protected ResponseEntity<BaseResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
logger.error("MethodArgumentNotValidException occurred: ", e);
BaseResponse response = BaseResponse.failResponse("INVALID_INPUT");
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(ConstraintViolationException.class)
protected ResponseEntity<BaseResponse> handleConstraintViolationException(ConstraintViolationException e) {
logger.error("ConstraintViolationException occurred: ", e);
BaseResponse response = BaseResponse.failResponse("CONSTRAINT_VIOLATION");
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(DataAccessException.class)
protected ResponseEntity<BaseResponse> handleDataAccessException(DataAccessException e) {
logger.error("DataAccessException occurred: ", e);
BaseResponse response = BaseResponse.failResponse("DATABASE_ERROR");
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
}

@ExceptionHandler(AccessDeniedException.class)
protected ResponseEntity<BaseResponse> handleAccessDeniedException(AccessDeniedException e) {
logger.error("AccessDeniedException occurred: ", e);
BaseResponse response = BaseResponse.failResponse("ACCESS_DENIED");
return new ResponseEntity<>(response, HttpStatus.FORBIDDEN);
}

@ExceptionHandler(Exception.class)
protected ResponseEntity<BaseResponse> handleException(Exception e) {
logger.error("Unexpected exception occurred: ", e);
BaseResponse response = BaseResponse.failResponse("INTERNAL_SERVER_ERROR");
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
28 changes: 12 additions & 16 deletions src/main/java/app/member/domain/service/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,19 @@ public class MemberService {

@Transactional
public CreateMemberVo createMember(CreateMemberDto dto) {
try {
Member member = Member.builder()
.name(dto.getName())
.email(dto.getEmail())
.role(dto.getRole())
.build();
Member result = memberRepository.save(member);
log.info("### 회원 생성 결과: {}", result);
validateNewMember(dto);

return CreateMemberVo.toVo(result);
} catch (DataIntegrityViolationException e) {
throw new CustomException(ResponseCode.CONFLICT_DATA);
} catch (CustomException e) {
throw new CustomException(e.getResponseCode());
} catch (Exception e) {
throw e;
}
Member member = Member.builder()
.name(dto.getName())
.email(dto.getEmail())
.role(dto.getRole())
.build();

Member savedMember = memberRepository.save(member);

log.info("New member created with ID: {}", savedMember.getId());

return CreateMemberVo.toVo(savedMember);
}

@Transactional(readOnly = true)
Expand Down
Loading