Skip to content

refactor: 아키텍처 수정#7

Merged
Dockerel merged 0 commit intodevfrom
refactor/architecture
Nov 2, 2025
Merged

refactor: 아키텍처 수정#7
Dockerel merged 0 commit intodevfrom
refactor/architecture

Conversation

@Dockerel
Copy link
Owner

@Dockerel Dockerel commented Nov 2, 2025

#️⃣ 연관된 이슈

x

📚 배경

x

📝 작업 내용

  • 아키텍처 수정 (단순 레이어드 아키텍처 → 계층 간 의존성을 최대한 줄인 레이어드 아키텍처 적용)
  • 인증 방식 변경 (세션 → JWT)
  • 엔티티 구조 변경 (멤버, 히스토리, 질문, 답변, 이미지 → 멤버, 히스토리, 채팅, 이미지)
  • ApiResponse 및 예외 처리 개선
  • Testcontainers 적용으로 테스트 간 격리성 향상

💬 리뷰 요구사항

x

✏ Git Close

x

@Dockerel Dockerel self-assigned this Nov 2, 2025
@Dockerel Dockerel added the enhancement New feature or request label Nov 2, 2025
@gemini-code-assist
Copy link

Summary of Changes

Hello @Dockerel, 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은 애플리케이션의 핵심 아키텍처를 재정비하고, 인증 시스템을 JWT 기반으로 전면 교체하며, 데이터 모델을 간소화하는 대규모 리팩토링을 포함합니다. 또한, API 응답 및 예외 처리 방식을 표준화하고, API 문서화 도구를 도입하여 개발 효율성과 코드 품질을 향상시키는 데 중점을 두었습니다. 이러한 변화는 향후 기능 확장 및 유지보수성을 크게 개선할 것입니다.

Highlights

  • 아키텍처 리팩토링: 단순 레이어드 아키텍처에서 계층 간 의존성을 최소화한 아키텍처로 전환하여 코드의 응집도를 높이고 결합도를 낮췄습니다. 이는 application, infrastructure, presentation 패키지 구조 변경 및 DTO 분리를 통해 이루어졌습니다.
  • 인증 방식 변경 (세션 -> JWT): 기존 세션 기반 인증 방식에서 JWT(JSON Web Token) 기반 인증으로 전환했습니다. 이를 위해 JwtProvider를 통한 토큰 생성 및 검증, AuthInterceptor를 통한 요청 가로채기, Redis를 활용한 Refresh Token 관리 로직이 추가되었습니다.
  • 엔티티 구조 변경: 기존 멤버, 히스토리, 질문, 답변, 이미지 엔티티 구조를 멤버, 히스토리, 채팅, 이미지로 변경하여 데이터 모델을 간소화하고 채팅 기능에 더 적합하도록 재구성했습니다.
  • API 응답 및 예외 처리 개선: 표준화된 ApiResponse 클래스를 도입하여 모든 API 응답의 일관성을 확보했습니다. 또한, KnuChatbotException, ErrorType, ErrorMessage를 포함하는 커스텀 예외 처리 시스템과 RestApiControllerAdvice를 통한 전역 예외 처리를 구현하여 견고한 오류 관리를 가능하게 했습니다.
  • 테스트 인프라 구축: Testcontainers를 활용한 MySQL 및 Redis 통합 테스트 환경(IntegrationTestSupport)과 Mockito를 사용한 컨트롤러 테스트(ControllerTestSupport)를 구축하여 코드 변경에 대한 안정성을 높였습니다.
  • API 문서화 (Swagger): Springdoc OpenAPI (Swagger)를 통합하여 API 문서를 자동 생성하고, JWT 인증 방식을 포함한 API 명세를 제공하여 개발 편의성을 향상시켰습니다.
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은 아키텍처를 개선하고 인증 방식을 JWT로 변경하는 등 많은 중요한 리팩토링을 포함하고 있습니다. 전반적으로 코드 구조가 개선되었고, 계층 분리, 예외 처리, 테스트 커버리지 등에서 많은 발전이 있었습니다. 특히 테스트 코드를 충실하게 작성해주신 점이 인상 깊습니다. 몇 가지 수정이 필요한 부분과 개선점을 제안드립니다.


// refreshToken 생성 및 저장
String newRefreshToken = jwtProvider.createRefreshToken(email);
memberRepository.saveRefreshToken(refreshToken, email, JwtProvider.REFRESH_TOKEN_EXPIRE_SECONDS);

Choose a reason for hiding this comment

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

critical

reissueTokens 메서드에서 새로운 리프레시 토큰을 생성한 후, Redis에 저장할 때 이전 리프레시 토큰(refreshToken)을 사용하고 있습니다. newRefreshToken을 사용해야 정상적으로 토큰 재발급이 계속 이루어질 수 있습니다.

Suggested change
memberRepository.saveRefreshToken(refreshToken, email, JwtProvider.REFRESH_TOKEN_EXPIRE_SECONDS);
memberRepository.saveRefreshToken(newRefreshToken, email, JwtProvider.REFRESH_TOKEN_EXPIRE_SECONDS);

Comment on lines +31 to +35
@ExceptionHandler(ServletRequestBindingException.class)
public ResponseEntity<ApiResponse<Object>> servletRequestBindingException(ServletRequestBindingException e) {
ApiResponse<Object> response = ApiResponse.error(new ErrorMessage(ErrorType.USER_LOGIN_REQUIRED_ERROR));
return new ResponseEntity<>(response, ErrorType.BINDING_ERROR.getStatus());
}

Choose a reason for hiding this comment

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

high

servletRequestBindingException 핸들러에서 USER_LOGIN_REQUIRED_ERROR를 사용하고 있는데, 이 에러 타입의 HTTP 상태는 UNAUTHORIZED(401)입니다. 하지만 응답을 생성할 때는 BINDING_ERROR의 상태인 BAD_REQUEST(400)를 사용하고 있어 불일치가 발생합니다. USER_LOGIN_REQUIRED_ERROR의 상태 코드를 사용하도록 수정해야 합니다.

Suggested change
@ExceptionHandler(ServletRequestBindingException.class)
public ResponseEntity<ApiResponse<Object>> servletRequestBindingException(ServletRequestBindingException e) {
ApiResponse<Object> response = ApiResponse.error(new ErrorMessage(ErrorType.USER_LOGIN_REQUIRED_ERROR));
return new ResponseEntity<>(response, ErrorType.BINDING_ERROR.getStatus());
}
@ExceptionHandler(ServletRequestBindingException.class)
public ResponseEntity<ApiResponse<Object>> servletRequestBindingException(ServletRequestBindingException e) {
ApiResponse<Object> response = ApiResponse.error(new ErrorMessage(ErrorType.USER_LOGIN_REQUIRED_ERROR));
return new ResponseEntity<>(response, ErrorType.USER_LOGIN_REQUIRED_ERROR.getStatus());
}

build.gradle Outdated
implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.7.0'

// security
implementation 'org.springframework.security:spring-security-crypto:6.0.3'

Choose a reason for hiding this comment

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

high

spring-security-crypto 의존성의 버전을 6.0.3으로 직접 지정하셨습니다. Spring Boot에서는 dependency-management 플러그인을 통해 의존성들의 호환되는 버전을 관리해주는 것이 일반적입니다. 버전을 직접 명시하면 다른 의존성과의 충돌이 발생할 수 있으니, 버전을 제거하여 Spring Boot의 BOM(Bill of Materials)이 관리하도록 하는 것을 권장합니다.

    implementation 'org.springframework.security:spring-security-crypto'

Comment on lines +73 to +75
} catch (Exception e) {
throw new KnuChatbotException(ErrorType.USER_INVALID_REFRESH_TOKEN_ERROR);
}

Choose a reason for hiding this comment

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

medium

validateToken 메서드에서 Exception으로 예외를 잡고 있습니다. 너무 광범위한 예외 처리이며, JWT 파싱과 관련 없는 다른 런타임 예외까지 잡을 수 있습니다. io.jsonwebtoken.JwtException과 같이 더 구체적인 예외를 잡도록 수정하는 것이 좋습니다. getClaimsFromAccessToken 메서드(85-87 라인)도 마찬가지입니다.

Suggested change
} catch (Exception e) {
throw new KnuChatbotException(ErrorType.USER_INVALID_REFRESH_TOKEN_ERROR);
}
} catch (io.jsonwebtoken.JwtException e) {
throw new KnuChatbotException(ErrorType.USER_INVALID_REFRESH_TOKEN_ERROR);
}

@@ -0,0 +1,12 @@
package knu_chatbot.application.util;

public abstract class AuthConst {

Choose a reason for hiding this comment

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

medium

상수만 포함하는 유틸리티 클래스는 abstract로 선언하기보다 final로 선언하고 private 생성자를 추가하여 상속과 인스턴스화를 모두 막는 것이 일반적입니다.

Suggested change
public abstract class AuthConst {
public final class AuthConst {
private AuthConst() {}

}

public static <T> ApiResponse<T> success(T data) {
return new ApiResponse(ResultType.SUCCESS, data, null);

Choose a reason for hiding this comment

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

medium

new ApiResponse(...)에서 raw type을 사용하고 있습니다. 타입 안정성을 위해 다이아몬드 연산자 <>를 사용하는 것이 좋습니다. error 메서드도 마찬가지입니다.

Suggested change
return new ApiResponse(ResultType.SUCCESS, data, null);
return new ApiResponse<>(ResultType.SUCCESS, data, null);

Comment on lines +18 to +22
return new OpenAPI()
.components(new Components())
.info(createApiInfo())
.addSecurityItem(new SecurityRequirement().addList(BEARER_AUTH))
.schemaRequirement(BEARER_AUTH, createSecurityScheme());

Choose a reason for hiding this comment

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

medium

schemaRequirement() 메서드는 deprecated 되었습니다. components()addSecuritySchemes()를 사용하여 보안 스키마를 등록하는 것이 좋습니다.

Suggested change
return new OpenAPI()
.components(new Components())
.info(createApiInfo())
.addSecurityItem(new SecurityRequirement().addList(BEARER_AUTH))
.schemaRequirement(BEARER_AUTH, createSecurityScheme());
return new OpenAPI()
.components(new Components()
.addSecuritySchemes(BEARER_AUTH, createSecurityScheme()))
.info(createApiInfo())
.addSecurityItem(new SecurityRequirement().addList(BEARER_AUTH));

Comment on lines +119 to +137
private String getRefreshTokenFromCookie(HttpServletRequest request) {
String refreshToken = null;

Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(REFRESH_TOKEN)) {
refreshToken = cookie.getValue();
break;
}
}
}

if (refreshToken == null) {
throw new KnuChatbotException(ErrorType.USER_COOKIE_REFRESH_TOKEN_ERROR);
}

return refreshToken;
}

Choose a reason for hiding this comment

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

medium

getRefreshTokenFromCookie 메서드에서 쿠키를 찾기 위해 직접 반복문을 사용하고 있습니다. java.util.Arrays.stream()을 사용하면 코드를 더 간결하고 선언적으로 만들 수 있습니다.

    private String getRefreshTokenFromCookie(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (cookies == null) {
            throw new KnuChatbotException(ErrorType.USER_COOKIE_REFRESH_TOKEN_ERROR);
        }

        return java.util.Arrays.stream(cookies)
                .filter(cookie -> REFRESH_TOKEN.equals(cookie.getName()))
                .findFirst()
                .map(Cookie::getValue)
                .orElseThrow(() -> new KnuChatbotException(ErrorType.USER_COOKIE_REFRESH_TOKEN_ERROR));
    }


@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "이메일 형태가 아닙니다.")
String email;

Choose a reason for hiding this comment

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

medium

email 필드가 private으로 선언되지 않았습니다. 캡슐화 원칙을 지키기 위해 필드는 private으로 선언하는 것이 좋습니다.

Suggested change
String email;
private String email;

DISCORD_ENVIRONMENT=dev No newline at end of file
DISCORD_ENVIRONMENT=dev

SECRET_KEY=hello_world_hello_world_hello_world No newline at end of file

Choose a reason for hiding this comment

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

medium

개발 환경이라도 hello_world...와 같이 예측하기 쉬운 시크릿 키를 사용하는 것은 안전하지 않습니다. 더 강력하고 무작위적인 문자열을 사용하고, Git에 커밋되지 않도록 환경 변수나 다른 secrets management 도구를 통해 관리하는 것을 권장합니다.

@Dockerel Dockerel merged commit 01a1a38 into dev Nov 2, 2025
@Dockerel Dockerel deleted the refactor/architecture branch November 2, 2025 13:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant