Skip to content

develop 변경 내용 main으로 머지#41

Merged
hongjm0912 merged 17 commits into
mainfrom
develop
Dec 28, 2025
Merged

develop 변경 내용 main으로 머지#41
hongjm0912 merged 17 commits into
mainfrom
develop

Conversation

@ZaMan0806

Copy link
Copy Markdown
Collaborator

개요

develop의 최신 변경 내용을 main 브랜치로 머지합니다

@ZaMan0806 ZaMan0806 requested a review from hongjm0912 December 28, 2025 11:41
@ZaMan0806 ZaMan0806 self-assigned this Dec 28, 2025
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @ZaMan-O, 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은 develop 브랜치의 최신 변경 사항을 main 브랜치로 병합하는 것을 목적으로 합니다. 코드 베이스의 일관성을 높이고, 특정 기능의 동작 방식을 개선하며, 애플리케이션의 보안 정책을 유연하게 조정하는 여러 업데이트가 포함되어 있습니다. 전반적으로 코드의 유지보수성과 안정성을 향상시키는 데 기여합니다.

Highlights

  • JPA Repository 메서드명 변경: 연관 엔티티의 ID를 참조하는 JPA Repository 메서드(findByIdAndUserId, findByBlogId, findByPortfolioId)들이 findByIdAndUser_Id, findByBlog_Id, findByPortfolio_Id와 같이 일관된 명명 규칙으로 변경되었습니다. 이는 JPA의 연관 관계 매핑에 더 적합한 방식입니다.
  • 댓글 수 업데이트 로직 개선: 블로그 및 포트폴리오 댓글 서비스에서 댓글 생성 및 삭제 시, 엔티티의 댓글 수(increaseComment, decreaseComment)를 업데이트하는 로직이 실제 댓글 엔티티를 저장하거나 삭제하기 전에 수행되도록 순서가 변경되었습니다. 또한, BlogRepositoryPortfolioRepository에서 직접적인 댓글 수 업데이트를 위한 @Query 메서드가 제거되었습니다.
  • Minio 설정 속성 표준화: Minio 관련 설정 값(endpoint, access-key, secret-key, presigned-url-duration-minutes)을 주입받는 @Value 어노테이션의 속성 키가 minio.*에서 spring.minio.*로 변경되어 Spring Boot의 표준 설정 명명 규칙을 따르도록 수정되었습니다.
  • 신규 사용자 기본 역할 변경: 새로운 사용자가 시스템에 가입할 때 부여되는 기본 역할이 VERIFIED에서 UNVERIFIED로 변경되어, 초기 사용자에게 추가적인 인증 절차가 필요함을 나타냅니다.
  • JWT 필터 예외 경로 확장: JwtHeaderFiltershouldNotFilter 메서드가 추가되어, 인증 관련 API 엔드포인트(sign, login, verify, google, kakao), 블로그 및 포트폴리오의 공개 조회/댓글 GET 요청, 그리고 OPTIONS 요청은 JWT 인증 필터를 거치지 않도록 설정되었습니다.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/CI.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.

@hongjm0912 hongjm0912 merged commit 7b5e8d4 into main Dec 28, 2025
3 checks passed

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

develop 브랜치의 변경 내용을 main으로 병합하는 PR이군요. 전반적으로 코드의 일관성을 높이고 도메인 모델을 개선하는 좋은 리팩토링이 많이 포함되어 있습니다.

  • Spring Data JPA 쿼리 메서드 명명 규칙에 맞게 findByIdAndUser_Id 등으로 수정한 점이 좋습니다.
  • 댓글 수 증감 로직을 Repository의 @Modifying 쿼리에서 도메인 엔티티의 메서드로 옮겨, 객체지향적인 설계를 강화한 점도 훌륭합니다.
  • Minio 관련 설정 프로퍼티 키를 spring.minio.*로 통일하여 일관성을 확보했습니다.

다만, 몇 가지 중요한 개선점을 발견하여 리뷰 코멘트를 남겼습니다.

  • PortfolioServicedelete 메서드는 트랜잭션 처리와 권한 검사 순서에 심각한 문제가 있어 데이터 유실 위험이 있습니다. (critical)
  • BlogServicedelete 메서드는 트랜잭션 내에서 외부 서비스를 호출하여 데이터 불일치를 유발할 수 있습니다. (high)
  • JwtHeaderFilter에 추가된 경로는 SecurityConfig와 중복 관리될 수 있고, startsWith를 사용한 경로 비교는 잠재적인 위험을 가질 수 있어 개선이 필요합니다. (medium)

자세한 내용은 각 파일의 코멘트를 확인해주세요.

Comment on lines 93 to 103
public void delete(Long userId, Long portfolioId) {
List<PortfolioImageJpaEntity> images =
portfolioImageRepository.findByPortfolioId(portfolioId);
portfolioImageRepository.findByPortfolio_Id(portfolioId);

for (PortfolioImageJpaEntity img : images) {
imageService.deleteImage(img.getImageUrl());
}
PortfolioJpaEntity portfolio = portfolioRepository.findByIdAndUserId(portfolioId, userId)
PortfolioJpaEntity portfolio = portfolioRepository.findByIdAndUser_Id(portfolioId, userId)
.orElseThrow(() -> new ExpectedException(HttpStatus.NOT_FOUND, "존재하지 않는 포트폴리오입니다."));

portfolioRepository.delete(portfolio);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

delete 메서드에는 몇 가지 심각한 문제가 있습니다.

  1. 트랜잭션 부재: 메서드에 @Transactional 어노테이션이 없어, 이미지 삭제와 포트폴리오 삭제 작업이 원자적으로 처리되지 않습니다. 중간에 오류가 발생하면 데이터가 불일치 상태에 빠질 수 있습니다.
  2. 잘못된 작업 순서 및 권한 검사: 포트폴리오 삭제 권한을 확인하기 전에 외부 저장소의 이미지를 먼저 삭제하고 있습니다. 만약 사용자가 권한이 없거나 포트폴리오가 존재하지 않아 findByIdAndUser_Id에서 예외가 발생하면, 이미지는 삭제되었지만 포트폴리오는 그대로 남아있는 심각한 데이터 불일치가 발생합니다.

아래와 같이 수정하여 문제를 해결하는 것을 강력히 권장합니다.

  1. @Transactional 어노테이션을 추가하여 모든 DB 작업을 하나의 트랜잭션으로 묶습니다.
  2. 가장 먼저 포트폴리오 존재 여부와 사용자 권한을 확인합니다.
Suggested change
public void delete(Long userId, Long portfolioId) {
List<PortfolioImageJpaEntity> images =
portfolioImageRepository.findByPortfolioId(portfolioId);
portfolioImageRepository.findByPortfolio_Id(portfolioId);
for (PortfolioImageJpaEntity img : images) {
imageService.deleteImage(img.getImageUrl());
}
PortfolioJpaEntity portfolio = portfolioRepository.findByIdAndUserId(portfolioId, userId)
PortfolioJpaEntity portfolio = portfolioRepository.findByIdAndUser_Id(portfolioId, userId)
.orElseThrow(() -> new ExpectedException(HttpStatus.NOT_FOUND, "존재하지 않는 포트폴리오입니다."));
portfolioRepository.delete(portfolio);
@Transactional
public void delete(Long userId, Long portfolioId) {
PortfolioJpaEntity portfolio = portfolioRepository.findByIdAndUser_Id(portfolioId, userId)
.orElseThrow(() -> new ExpectedException(HttpStatus.NOT_FOUND, "존재하지 않는 포트폴리오입니다."));
List<PortfolioImageJpaEntity> images =
portfolioImageRepository.findByPortfolio_Id(portfolioId);
for (PortfolioImageJpaEntity img : images) {
imageService.deleteImage(img.getImageUrl());
}
portfolioRepository.delete(portfolio);
}

Comment on lines 95 to 110
public void delete(Long userId, Long blogId) {
List<BlogImageJpaEntity> images =
blogImageRepository.findByBlogId(blogId);
blogImageRepository.findByBlog_Id(blogId);

for (BlogImageJpaEntity img : images) {
imageService.deleteImage(img.getImageUrl());
}

BlogJpaEntity blog = blogRepository.findByIdAndUserId(blogId, userId)
BlogJpaEntity blog = blogRepository.findByIdAndUser_Id(blogId, userId)
.orElseThrow(() -> new ExpectedException(HttpStatus.NOT_FOUND, "존재하지 않는 블로그입니다."));
if (blog.getThumbnail() != null && !blog.getThumbnail().isBlank()) {
imageService.deleteImage(blog.getThumbnail());
}

for (BlogImageJpaEntity img : images) {
imageService.deleteImage(img.getImageUrl());
}

blogRepository.delete(blog);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

delete 메서드는 @Transactional로 관리되고 있지만, 데이터베이스 트랜잭션 내에서 외부 시스템(Minio)의 파일을 삭제하는 로직이 있어 데이터 불일치 위험이 있습니다.

imageService.deleteImage() 호출이 반복문 중간에 실패할 경우,

  • 데이터베이스 트랜잭션은 롤백되어 블로그와 이미지 정보는 DB에서 삭제되지 않습니다.
  • 하지만, 예외 발생 이전에 imageService.deleteImage()로 성공적으로 삭제된 파일들은 외부 저장소에서 영구적으로 사라지며 복구되지 않습니다.

이로 인해 DB에는 존재하지만 실제 파일은 없는 '깨진 데이터'가 발생할 수 있습니다.

개선 제안:
데이터베이스 작업과 외부 시스템 호출을 분리하는 것을 고려해 보세요. 예를 들어, 트랜잭션이 성공적으로 커밋된 후에만 파일을 삭제하도록 @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)를 사용하는 방법이 있습니다. 이렇게 하면 DB 작업의 원자성을 보장하면서 외부 시스템과의 상호작용으로 인한 부작용을 최소화할 수 있습니다.

Comment on lines +81 to +105
protected boolean shouldNotFilter(HttpServletRequest request) {
String uri = request.getRequestURI();

if (uri.startsWith("/api/v1/auth/sign")
|| uri.startsWith("/api/v1/auth/login")
|| uri.startsWith("/api/v1/auth/verify")
|| uri.startsWith("/api/v1/auth/google")
|| uri.startsWith("/api/v1/auth/kakao")) {
return true;
} else if (request.getMethod().equals("GET") && (
uri.startsWith("/api/v1/main/")
|| uri.startsWith("/api/v1/profile/")
|| uri.startsWith("/api/v1/portfolio/view/")
|| uri.startsWith("/api/v1/portfolio/comment/")
|| uri.startsWith("/api/v1/blog/view/")
|| uri.startsWith("/api/v1/blog/comment/"))) {
return true;
}

if(request.getMethod().equals("OPTIONS")) {
return true;
}

return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

새로 추가된 shouldNotFilter 메서드는 JWT 필터를 건너뛸 경로를 지정하여 최적화에 도움이 됩니다. 하지만 현재 구현에는 유지보수성 측면에서 몇 가지 개선점이 있습니다.

  1. 하드코딩된 URL 패턴: URL 경로들이 문자열로 하드코딩되어 있어, 향후 경로 변경 시 여러 곳을 수정해야 할 수 있고 오류 발생 가능성이 높습니다.
  2. startsWith 사용의 위험성: startsWithAntPathMatcher (예: /**)에 비해 정교하지 않습니다. 예를 들어, uri.startsWith("/api/v1/auth/sign")는 의도치 않게 /api/v1/auth/signup과 같은 다른 경로와도 일치할 수 있습니다.
  3. SecurityConfig와의 중복: 필터링 예외 경로는 SecurityConfig에 정의된 permitAll 경로와 거의 동일합니다. 두 곳에서 경로를 관리하면 동기화가 누락될 위험이 있습니다.

개선 제안:

  • 예외 경로 목록을 별도의 static final 리스트나 배열로 추출하여 한 곳에서 관리하고, SecurityConfigJwtHeaderFilter에서 함께 사용하도록 리팩토링하는 것을 권장합니다.
  • startsWith 대신 Spring에서 널리 사용되는 AntPathMatcher를 사용하여 SecurityConfig와 동일한 방식으로 경로를 검사하면 일관성과 안정성을 높일 수 있습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants