Conversation
RedisConf, application.yml, WebMvcConfig 설정
비회원인 경우 GeustCookieInterceptor를 통해 발급된 쿠키에 있는 UUID를 통해 중복키 생성해서 10분간 조회 카운트 증가 방지 회원의 경우 jwt의 userId를 통해 중복키 생성해서 위 로직 수행 중복키에 10분 TTL 걸고 중복키가 있다면 로깅하고 종료 없다면 redis에 조회수 증분값 1증가
@mock + @Quailfier 조합은 mocking 안됨. 생성자 주입 사용
조회수 = DB 컬럼 + 레디스
테스트 정리(@import(redisTestConfig) + 조회수 증가(회원/비회원) 테스트 작성, readCountKey 변경, post 관련 테스트 중 레디스가 필요한 테스트 이동
5분마다 레디스에 있는 조회수 키를 DB에 반영해 주는 로직 추가 + 테스트 + Redisson을 통해 여러 인스턴스에서 동시에 로직을 실행하지 않도록 함.
조회수 값에 redis에서 읽어온 캐시값 추가해서 반환
1.페이지 조회 시 조회수 확인 2.페이지 상세 조회 시 조회수 확인
1.DTO를 하나 공유해서 쓰려고 하니까 특정 모듈에 종속적인 기능이 점점 많아져서 분리함. 2.대표적인 테스트에서 반환값의 모든 속성을 검증하여 수정 시 바로 테스트가 깨지도록 함.
|
""" Walkthrough이 변경사항은 게시글 조회수(조회 카운트) 기능을 백엔드와 프론트엔드에 통합적으로 추가합니다. Redis를 활용한 중복 방지 및 실시간 카운팅, 주기적 DB 동기화, 관련 서비스/DTO/테스트 코드, 프론트엔드 조회수 표시 등이 포함됩니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Guest/User as 게스트/사용자
participant Web as 프론트엔드
participant API as PostController
participant ViewSvc as ViewCountService
participant Redis as Redis
participant PostSvc as PostService
participant DB as DB
Guest/User->>Web: 게시글 상세 페이지 요청
Web->>API: GET /api/posts/details/{postId} (쿠키/토큰 포함)
API->>ViewSvc: readCountUp(userId/guestId, postId)
ViewSvc->>Redis: 중복키 조회/설정, 조회수 증가
Redis-->>ViewSvc: 현재 조회수 반환
API->>PostSvc: getPost(postId)
PostSvc->>DB: 게시글/좋아요 정보 조회
PostSvc-->>API: PostDetailResponse (DB readCount)
API->>API: Redis readCount를 응답에 누적
API-->>Web: PostDetailResponse (readCount 포함)
Web-->>Guest/User: 조회수 표시
sequenceDiagram
participant Scheduler as 스케줄러
participant SyncSvc as ViewCountSyncService
participant Redis as Redis
participant Redisson as RedissonLock
participant DB as DB
Scheduler->>SyncSvc: 5분마다 syncViewCounts() 호출
SyncSvc->>Redisson: 분산락 획득 시도
alt 락 획득 성공
SyncSvc->>Redis: post:viewCount:* 키스캔
loop 각 postId
SyncSvc->>Redis: 해당 postId 조회수 가져오기
SyncSvc->>DB: 게시글 readCount에 Redis값 누적 저장
SyncSvc->>Redis: 해당 Redis 키 삭제
end
SyncSvc->>Redisson: 락 해제
else 락 획득 실패
SyncSvc->>SyncSvc: 동기화 스킵
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Possibly related PRs
Suggested labels
Poem
""" Note ⚡️ Unit Test Generation is now available in beta!Learn more here, or try it out under "Finishing Touches" below. 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 19
🧹 Nitpick comments (15)
backend/src/test/resources/application-test.yml (1)
1-2: 불필요 공백 라인 정리 제안
테스트 설정 파일에 의미 없는 빈 줄이 다수 추가되었습니다. YAML 은 들여쓰기 의존적이므로 불필요한 공백은 줄이는 편이 관리·리뷰 측면에서 좋습니다. 기능에는 영향 없으나 다음 커밋에서 같이 정리해 주세요.Also applies to: 13-15, 30-30
frontend/src/pages/posts/PostListPage.jsx (1)
147-148:readCount미존재 시 오류 가능성 검증 필요
백엔드 DTO 에readCount가 포함되지 않는 경우undefined가 그대로 출력됩니다. 안전하게post.readCount ?? 0로 기본값을 주는 편이 좋습니다.- <th>조회수</th> + <th>조회수</th> ... - <td>{post.readCount}</td> + <td>{post.readCount ?? 0}</td>Also applies to: 176-176
backend/src/main/java/org/juniortown/backend/post/repository/PostRepository.java (1)
8-8: 불필요한 import 제거 필요
@Modifying어노테이션을 import했지만 실제로 사용되지 않고 있습니다.-import org.springframework.data.jpa.repository.Modifying;backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java (1)
223-223: Redis Mock 설정을 개선해주세요.Redis 관련 Mock 설정이 일부 테스트에만 적용되어 있습니다. 일관성을 위해 모든 관련 테스트 메서드에서 Redis Mock을 설정하는 것을 고려해보세요.
@BeforeEach메서드에서 공통 Mock 설정을 추가할 수 있습니다:@BeforeEach void setUp() { when(redisTemplate.opsForValue()).thenReturn(readCountValueOperations); }backend/src/main/java/org/juniortown/backend/post/service/ViewCountSyncService.java (2)
36-36: 오타 수정 필요"인스턴스에세" → "인스턴스에서"로 수정해주세요.
- log.info("동기화 락을 획득하지 못했습니다. 다른 인스턴스에세 이미 실행 중입니다."); + log.info("동기화 락을 획득하지 못했습니다. 다른 인스턴스에서 이미 실행 중입니다.");
28-57: 동시성 및 트랜잭션 격리 수준 고려사항현재 구현에서 다음 사항들을 고려해야 합니다:
- Redis 키를 읽고 삭제하는 사이에 새로운 조회수가 추가될 수 있습니다
- 여러 게시글을 업데이트하는 동안 트랜잭션이 길어질 수 있습니다
다음과 같은 개선을 고려해보세요:
- Redis의 GETDEL 명령어 사용을 고려하거나
- 각 게시글 업데이트를 별도 트랜잭션으로 처리
- 배치 업데이트를 위한 벌크 연산 사용
backend/src/main/java/org/juniortown/backend/config/RedisConfig.java (2)
26-33: RedisTemplate 설정 순서 일관성 필요
keyCheckRedisTemplate과readCountRedisTemplate의 설정 순서가 다릅니다. 일관성을 위해 통일하는 것이 좋습니다.
31-31: Jackson2JsonRedisSerializer 제네릭 타입 경고 해결
Jackson2JsonRedisSerializer에 raw type을 사용하면 컴파일 경고가 발생합니다.- redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Boolean.class)); + Jackson2JsonRedisSerializer<Boolean> serializer = new Jackson2JsonRedisSerializer<>(Boolean.class); + serializer.setObjectMapper(new ObjectMapper()); + redisTemplate.setValueSerializer(serializer);동일하게 Long 타입에도 적용:
- redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Long.class)); + Jackson2JsonRedisSerializer<Long> serializer = new Jackson2JsonRedisSerializer<>(Long.class); + serializer.setObjectMapper(new ObjectMapper()); + redisTemplate.setValueSerializer(serializer);Also applies to: 40-40
backend/src/main/java/org/juniortown/backend/post/service/ViewCountService.java (1)
22-25: 주석 형식 개선 및 상수 활용키 형식을 주석으로만 설명하고 있는데, 이를 메소드로 추출하면 더 안전합니다.
- // 조회수증분키 형태, post:viewCount:{postId}:{userId} public static final String VIEW_COUNT_KEY = "post:viewCount:"; - // 중복방지키 형태, postDup:key:{postId}:{userId} public static final String DUP_PREVENT_KEY = "postDup:key:"; + + private String buildViewCountKey(String postId) { + return VIEW_COUNT_KEY + postId; + } + + private String buildDupPreventKey(String postId, String userId) { + return DUP_PREVENT_KEY + postId + ":" + userId; + }backend/src/test/java/org/juniortown/backend/config/RedisTestConfig.java (2)
3-6: 사용하지 않는 import 제거다음 import들이 사용되지 않고 있습니다:
ViewCountSyncServiceTestMockitoRedissonClientConfigurationAlso applies to: 10-10
30-36: RedisTemplate 설정 순서 통일
RedisConfig와 동일한 순서로 설정하면 일관성이 향상됩니다.public RedisTemplate<String, Boolean> keyCheckRedisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String,Boolean> redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Boolean.class)); - redisTemplate.setConnectionFactory(redisConnectionFactory); return redisTemplate; }Also applies to: 39-45
backend/src/test/java/org/juniortown/backend/post/service/ViewCountServiceTest.java (1)
7-7: 사용하지 않는 import 제거
RedisTestConfig와Import어노테이션이 사용되지 않고 있습니다.Also applies to: 15-15
backend/src/main/java/org/juniortown/backend/post/dto/response/PostDetailResponse.java (1)
52-54: 가변 메서드 대신 불변 접근 방식 고려
addReadCount메서드가 객체 상태를 변경합니다. 불변 DTO 패턴을 따르려면 새로운 인스턴스를 반환하거나, 이 로직을 서비스 레이어로 이동하는 것을 고려해보세요.현재 구조를 유지하려면 최소한 null 체크를 추가하세요:
public void addReadCount(Long redisReadCount) { + if (redisReadCount == null) { + return; + } this.readCount += redisReadCount; }backend/src/main/java/org/juniortown/backend/post/controller/PostController.java (1)
8-10: 사용하지 않는 import 제거
PostWithLikeCount와PostWithLikeCountProjection은 사용되지 않습니다.-import org.juniortown.backend.post.dto.response.PostWithLikeCount; import org.juniortown.backend.post.dto.response.PostResponse; -import org.juniortown.backend.post.dto.response.PostWithLikeCountProjection;backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java (1)
129-133: 주석 처리된 코드 제거사용하지 않는 주석 코드는 제거하는 것이 좋습니다.
-// @AfterEach -// void clean() { -// userRepository.deleteAll(); -// postRepository.deleteAll(); -// }동기화 락 테스트도 필요하다면 수정하여 활성화하고, 그렇지 않다면 제거하세요.
Also applies to: 291-317
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (30)
backend/build.gradle(1 hunks)backend/src/main/java/org/juniortown/backend/BackendApplication.java(1 hunks)backend/src/main/java/org/juniortown/backend/config/RedisConfig.java(1 hunks)backend/src/main/java/org/juniortown/backend/config/SecurityConfig.java(1 hunks)backend/src/main/java/org/juniortown/backend/config/WebMvcConfig.java(1 hunks)backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java(1 hunks)backend/src/main/java/org/juniortown/backend/like/repository/LikeRepository.java(1 hunks)backend/src/main/java/org/juniortown/backend/like/service/LikeService.java(1 hunks)backend/src/main/java/org/juniortown/backend/post/controller/PostController.java(3 hunks)backend/src/main/java/org/juniortown/backend/post/dto/response/PostDetailResponse.java(1 hunks)backend/src/main/java/org/juniortown/backend/post/dto/response/PostResponse.java(2 hunks)backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCountProjection.java(1 hunks)backend/src/main/java/org/juniortown/backend/post/entity/Post.java(2 hunks)backend/src/main/java/org/juniortown/backend/post/repository/PostRepository.java(3 hunks)backend/src/main/java/org/juniortown/backend/post/service/PostService.java(3 hunks)backend/src/main/java/org/juniortown/backend/post/service/ViewCountService.java(1 hunks)backend/src/main/java/org/juniortown/backend/post/service/ViewCountSyncService.java(1 hunks)backend/src/main/resources/application.yml(2 hunks)backend/src/test/java/org/juniortown/backend/config/RedisTestConfig.java(1 hunks)backend/src/test/java/org/juniortown/backend/config/SyncConfig.java(1 hunks)backend/src/test/java/org/juniortown/backend/controller/AuthControllerTest.java(3 hunks)backend/src/test/java/org/juniortown/backend/controller/PostControllerPagingTest.java(4 hunks)backend/src/test/java/org/juniortown/backend/controller/PostControllerTest.java(3 hunks)backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java(1 hunks)backend/src/test/java/org/juniortown/backend/like/controller/LikeControllerTest.java(3 hunks)backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java(7 hunks)backend/src/test/java/org/juniortown/backend/post/service/ViewCountServiceTest.java(1 hunks)backend/src/test/resources/application-test.yml(3 hunks)frontend/src/pages/posts/PostDetailPage.jsx(1 hunks)frontend/src/pages/posts/PostListPage.jsx(2 hunks)
🧰 Additional context used
🧠 Learnings (15)
📓 Common learnings
Learnt from: Doncham
PR: Doncham/JuniorTown#10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
backend/src/main/java/org/juniortown/backend/like/repository/LikeRepository.java (1)
Learnt from: Doncham
PR: #10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCountProjection.java (1)
Learnt from: Doncham
PR: #10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
backend/src/test/java/org/juniortown/backend/controller/PostControllerTest.java (1)
Learnt from: Doncham
PR: #10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
backend/src/test/java/org/juniortown/backend/like/controller/LikeControllerTest.java (1)
Learnt from: Doncham
PR: #10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
backend/src/main/java/org/juniortown/backend/post/repository/PostRepository.java (1)
Learnt from: Doncham
PR: #10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
backend/src/test/java/org/juniortown/backend/controller/PostControllerPagingTest.java (1)
Learnt from: Doncham
PR: #10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java (1)
Learnt from: Doncham
PR: #10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
backend/src/main/java/org/juniortown/backend/post/service/ViewCountService.java (1)
Learnt from: Doncham
PR: #10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
backend/src/main/java/org/juniortown/backend/post/service/ViewCountSyncService.java (1)
Learnt from: Doncham
PR: #10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
backend/src/main/java/org/juniortown/backend/post/service/PostService.java (1)
Learnt from: Doncham
PR: #10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
backend/src/main/java/org/juniortown/backend/post/dto/response/PostResponse.java (1)
Learnt from: Doncham
PR: #10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
backend/src/main/java/org/juniortown/backend/post/dto/response/PostDetailResponse.java (1)
Learnt from: Doncham
PR: #10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
backend/src/main/java/org/juniortown/backend/post/controller/PostController.java (1)
Learnt from: Doncham
PR: #10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java (1)
Learnt from: Doncham
PR: #10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
🧬 Code Graph Analysis (2)
backend/src/main/java/org/juniortown/backend/config/RedisConfig.java (1)
backend/src/main/java/org/juniortown/backend/config/WebMvcConfig.java (1)
Configuration(11-26)
backend/src/test/java/org/juniortown/backend/config/RedisTestConfig.java (2)
backend/src/test/java/org/juniortown/backend/config/SyncConfig.java (1)
TestConfiguration(9-17)backend/src/main/java/org/juniortown/backend/config/RedisConfig.java (1)
Configuration(13-43)
🪛 ast-grep (0.38.6)
backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java
[warning] 31-31: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: response.addCookie(cookie);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application
(cookie-missing-samesite-java)
[warning] 31-31: A cookie was detected without setting the 'secure' flag. The 'secure' flag for cookies prevents the client from transmitting the cookie over insecure channels such as HTTP. Set the 'secure' flag by calling '.setSecure(true);'.
Context: response.addCookie(cookie);
Note: [CWE-614] Sensitive Cookie in HTTPS Session Without 'Secure' Attribute. [REFERENCES]
- https://owasp.org/www-community/controls/SecureCookieAttribute
(cookie-missing-secure-flag-java)
[warning] 31-31: A cookie was detected without setting the 'HttpOnly' flag. The 'HttpOnly' flag for cookies instructs the browser to forbid client-side scripts from reading the cookie. Set the 'HttpOnly' flag by calling 'cookie.setHttpOnly(true);
Context: response.addCookie(cookie);
Note: [CWE-1004] Sensitive Cookie Without 'HttpOnly' Flag. [REFERENCES]
- https://owasp.org/www-community/HttpOnly
(cookie-missing-httponly-java)
🔇 Additional comments (19)
backend/src/main/java/org/juniortown/backend/BackendApplication.java (1)
7-12: 스케줄링 활성화 추가 👍
@EnableScheduling추가로 스케줄 태스크가 구동됩니다. 문제 없습니다. 단,@Scheduled메서드 예외가 전파되면 다음 실행이 중단될 수 있으므로 서비스 단에서 예외 처리(try-catch & 로그) 확실히 해 주세요.backend/src/main/java/org/juniortown/backend/like/repository/LikeRepository.java (1)
12-12: Spring Data JPA 파생 쿼리 메서드가 올바르게 구현되었습니다.메서드명과 시그니처가 Spring Data JPA 규칙에 부합하며, 특정 게시글의 좋아요 개수를 조회하는 기능이 명확하게 표현되어 있습니다.
frontend/src/pages/posts/PostDetailPage.jsx (1)
114-116: 백엔드에서readCount필드 정상 확인됨
PostDetailResponse/PostResponseDTO에readCount가 선언되어 있으며, 엔티티 초기값이0L로 설정되어 null 반환이 불가능합니다.ViewCountService#readCountUp도 null 대신0L을 반환하므로, 프론트엔드에서는 항상 숫자 값을 안전하게 받아옵니다.UI에서는
<small className="text-muted ms-2"> 👁️ {post.readCount?.toLocaleString()}회 </small>로 optional chaining 및
toLocaleString()을 적용해, 카운트가 “0”일 때도👁️ 0회로 표시됩니다.필요에 따라 조회수가 0일 경우 UI에서 숨기고 싶다면, 아래처럼 조건부 렌더링을 추가하세요:
- {post.readCount?.toLocaleString() && ( - <small className="text-muted ms-2"> - 👁️ {post.readCount.toLocaleString()}회 - </small> - )} + {post.readCount > 0 && ( + <small className="text-muted ms-2"> + 👁️ {post.readCount.toLocaleString()}회 + </small> + )}backend/src/main/java/org/juniortown/backend/like/service/LikeService.java (1)
37-37: 코드 정리가 잘 되었습니다.Trailing whitespace 제거로 코드 품질이 개선되었습니다.
backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCountProjection.java (1)
13-13: PostWithLikeCountProjection에 readCount 프로젝션 검증 필요아래 두 가지를 확인해 주세요.
- PostRepository의 @query 문에
p.readCount AS readCount가 SELECT 절에 포함되어 있는지Post엔티티 클래스에readCount필드가 정의되어 있고, 실제 DB 테이블 컬럼과 매핑되어 있는지backend/src/main/java/org/juniortown/backend/config/SecurityConfig.java (1)
79-79: 게시글 상세 엔드포인트 공개 허용 검토 결과
- ViewCountService에서 Redis 기반 중복 방지 로직(
DUP_PREVENT_KEY, 10분 TTL) 및 증감 로직(VIEW_COUNT_KEYincrement)이 정상 구현되어 있습니다.- ViewCountSyncService를 통한 DB 동기화 로직도 스케줄러 및 락 방식으로 올바르게 작동합니다.
- Post 엔티티에는 ID, 제목, 본문(content), 작성자(user), 조회수(readCount) 등 외부 공개에 문제가 될 민감 필드가 없습니다.
따라서
/api/posts/details/**에 대한permitAll()설정은 합리적이며, 추가 조치 없이 승인합니다.backend/src/test/java/org/juniortown/backend/controller/PostControllerTest.java (1)
15-16: 테스트 설정 개선이 적절함Post 관련 테스트에 Redis 설정을 추가한 것은 조회수 기능 구현에 적합합니다. 또한 게시글 상세 조회 테스트를 별도의
PostRedisReadControllerTest로 분리한 것도 관심사 분리 측면에서 좋은 접근입니다.Also applies to: 41-41, 59-59
backend/src/test/java/org/juniortown/backend/config/SyncConfig.java (1)
1-17: 테스트용 Redis 설정이 적절하게 구현됨테스트 환경에서 실제 Redis 연결 없이 Redisson 기능을 테스트할 수 있도록 Mock 객체를 제공하는 설정이 잘 구현되었습니다.
@TestConfiguration과@Profile("test")사용으로 테스트 환경에서만 활성화되도록 한 것도 적절합니다.backend/src/main/java/org/juniortown/backend/post/repository/PostRepository.java (1)
18-18: 조회수 필드가 적절하게 추가됨
findAllWithLikeCount쿼리에readCount필드를 SELECT절과 GROUP BY절에 일관성 있게 추가한 것이 좋습니다. 조회수 기능 구현에 필요한 변경사항이 올바르게 적용되었습니다.Also applies to: 27-27
backend/src/main/java/org/juniortown/backend/post/entity/Post.java (1)
43-44: 조회수 필드 추가가 올바르게 구현되었습니다.
readCount필드가 적절한 JPA 어노테이션과 함께 추가되었으며, 기본값 0L 설정이 적합합니다.backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java (1)
16-35: 게스트 사용자 추적 로직이 올바르게 구현되었습니다.UUID를 사용한 게스트 ID 생성과 쿠키 존재 여부 확인 로직이 적절합니다.
backend/src/main/java/org/juniortown/backend/config/WebMvcConfig.java (1)
14-19: 인터셉터 등록이 올바르게 구현되었습니다.
GuestCookieInterceptor가 모든 경로에 대해 적절히 등록되어 게스트 사용자 추적이 가능합니다.backend/src/test/java/org/juniortown/backend/controller/PostControllerPagingTest.java (2)
12-13: Redis 테스트 설정이 적절히 추가되었습니다.
RedisTestConfig와SyncConfigimport가 Redis 기반 조회수 기능 테스트를 위해 올바르게 추가되었습니다.Also applies to: 35-35, 49-49
143-151: 테스트 검증 로직이 크게 향상되었습니다.게시글 상세 정보에 대한 포괄적인 검증이 추가되어
readCount,likeCount,isLiked등의 새로운 필드들이 올바르게 반환되는지 확인할 수 있습니다.backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java (2)
10-10: Redis 통합을 위한 Mock 객체들이 적절히 추가되었습니다.
LikeRepository,RedisTemplate,ValueOperationsMock이 새로운 Redis 기반 조회수 기능 테스트를 위해 올바르게 추가되었습니다.Also applies to: 34-35, 49-49, 58-60
225-225: 서비스 레이어 변경사항에 맞춰 반환 타입이 올바르게 업데이트되었습니다.기존 학습 내용에 따르면 이 프로젝트는 JPA 인터페이스 기반 프로젝션을 사용하므로,
PostResponse와PostDetailResponse로의 변경이 서비스 레이어의 실제 구현과 일치합니다.Also applies to: 238-238, 255-255, 286-286
backend/src/main/java/org/juniortown/backend/post/service/PostService.java (1)
102-102: Redis null 처리 잘됨Redis에서 가져온 값이 null일 경우를 적절히 처리하고 있습니다.
backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java (2)
77-79: 비표준 Redis 포트 사용 이유 확인Redis를 6380 포트로 실행하도록 설정되어 있습니다. 특별한 이유가 있나요?
표준 포트(6379)를 사용하는 것이 일반적입니다:
- .withCommand("redis-server --port 6380") - .withExposedPorts(6380); + .withExposedPorts(6379);
273-289: 조회수 동기화 테스트 잘 작성됨Redis에서 DB로의 동기화 로직을 잘 테스트하고 있습니다. 동기화 후 Redis 키가 삭제되는지 확인하는 것도 좋습니다.
|
|
||
| implementation 'org.springframework.boot:spring-boot-starter-data-redis' | ||
|
|
||
|
|
||
| testImplementation 'org.junit.jupiter:junit-jupiter' | ||
| testImplementation 'org.testcontainers:testcontainers' | ||
| testImplementation 'org.springframework.boot:spring-boot-testcontainers' | ||
| testImplementation 'org.testcontainers:junit-jupiter' | ||
| testImplementation 'com.redis:testcontainers-redis:2.2.2' | ||
|
|
||
| implementation 'org.redisson:redisson-spring-boot-starter:3.27.0' | ||
|
|
There was a problem hiding this comment.
중복·불필요 의존성 정리 필요
spring-boot-starter-test가 이미 JUnit Jupiter를 포함하므로testImplementation 'org.junit.jupiter:junit-jupiter'는 중복입니다.redisson-spring-boot-starter내부에spring-boot-starter-data-redis가 포함되어 있어 두 스타터를 동시 선언하면RedisConnectionFactory빈 충돌이 발생할 수 있습니다(실행 시BeanDefinitionOverrideException가능). 하나만 유지하거나redisson-spring-boot-starter에서exclude group: 'org.springframework.boot', module: 'spring-boot-starter-data-redis'로 중복을 제거해 주세요.- 필요하지 않은 테스트컨테이너 모듈이 포함됐는지 확인 바랍니다.
- implementation 'org.springframework.boot:spring-boot-starter-data-redis'
- ...
- testImplementation 'org.junit.jupiter:junit-jupiter'
+ // Redis
+ implementation 'org.redisson:redisson-spring-boot-starter:3.27.0' // 포함된 lettuce 혹은 jedis 중복 여부 확인
+ // 테스트
+ // spring-boot-starter-test 내부에 Jupiter 포함 → 중복 제거Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In backend/build.gradle around lines 51 to 62, remove the redundant
'org.junit.jupiter:junit-jupiter' dependency since 'spring-boot-starter-test'
already includes it. Also, avoid declaring both 'spring-boot-starter-data-redis'
and 'redisson-spring-boot-starter' together to prevent RedisConnectionFactory
bean conflicts; either keep only one or exclude 'spring-boot-starter-data-redis'
from 'redisson-spring-boot-starter' using the exclude directive. Finally, review
and remove any unnecessary testcontainers dependencies that are not used in the
project.
| Cookie cookie = new Cookie(COOKIE_NAME, guestId); | ||
| cookie.setPath("/"); | ||
| cookie.setMaxAge(60 * 60 * 24 * 365); // 1 year | ||
| response.addCookie(cookie); |
There was a problem hiding this comment.
쿠키 보안 설정을 강화해주세요.
정적 분석 도구에서 지적한 바와 같이 쿠키 보안 설정이 부족합니다.
다음과 같이 수정하여 보안을 강화하세요:
Cookie cookie = new Cookie(COOKIE_NAME, guestId);
cookie.setPath("/");
cookie.setMaxAge(60 * 60 * 24 * 365); // 1 year
+cookie.setHttpOnly(true);
+cookie.setSecure(true); // HTTPS 환경에서만
+cookie.setSameSite(Cookie.SameSite.STRICT.attributeValue());
response.addCookie(cookie);참고: HTTPS 환경이 아닌 경우 setSecure(false)로 설정하되, 프로덕션에서는 반드시 HTTPS를 사용하세요.
Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 ast-grep (0.38.6)
[warning] 31-31: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: response.addCookie(cookie);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application
(cookie-missing-samesite-java)
[warning] 31-31: A cookie was detected without setting the 'secure' flag. The 'secure' flag for cookies prevents the client from transmitting the cookie over insecure channels such as HTTP. Set the 'secure' flag by calling '.setSecure(true);'.
Context: response.addCookie(cookie);
Note: [CWE-614] Sensitive Cookie in HTTPS Session Without 'Secure' Attribute. [REFERENCES]
- https://owasp.org/www-community/controls/SecureCookieAttribute
(cookie-missing-secure-flag-java)
[warning] 31-31: A cookie was detected without setting the 'HttpOnly' flag. The 'HttpOnly' flag for cookies instructs the browser to forbid client-side scripts from reading the cookie. Set the 'HttpOnly' flag by calling 'cookie.setHttpOnly(true);
Context: response.addCookie(cookie);
Note: [CWE-1004] Sensitive Cookie Without 'HttpOnly' Flag. [REFERENCES]
- https://owasp.org/www-community/HttpOnly
(cookie-missing-httponly-java)
🤖 Prompt for AI Agents
In
backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java
around lines 29 to 32, the cookie security settings are insufficient. Enhance
security by setting the cookie's Secure flag to true when in a production HTTPS
environment, and set HttpOnly to true to prevent client-side script access.
Also, consider setting the SameSite attribute to Lax or Strict to mitigate CSRF
risks. If HTTPS is not used, explicitly set Secure to false, but ensure
production uses HTTPS.
| Long redisReadCount = 0L; | ||
| if(customUserDetails == null) { | ||
| // 비회원인 경우 | ||
| // 쿠키값을 레디스에 등록 | ||
| redisReadCount = viewCountService.readCountUp(guestId, String.valueOf(postId)); | ||
| } else { | ||
| // 회원인 경우 | ||
| // userId,postId를 조합해서 레디스에 등록 | ||
| Long userId = customUserDetails.getUserId(); | ||
| redisReadCount = viewCountService.readCountUp(String.valueOf(userId), String.valueOf(postId)); | ||
| } | ||
|
|
||
| PostDetailResponse postDetailResponse = postService.getPost(postId); | ||
| postDetailResponse.addReadCount(redisReadCount); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
컨트롤러의 비즈니스 로직을 서비스 레이어로 이동
조회수 증가 로직이 컨트롤러에 있습니다. 이는 서비스 레이어의 책임입니다. 또한 응답 DTO를 생성 후 수정하는 것은 좋지 않은 패턴입니다.
서비스 메서드가 사용자 정보를 받아 완전한 응답을 반환하도록 수정:
-Long redisReadCount = 0L;
-if(customUserDetails == null) {
- // 비회원인 경우
- // 쿠키값을 레디스에 등록
- redisReadCount = viewCountService.readCountUp(guestId, String.valueOf(postId));
-} else {
- // 회원인 경우
- // userId,postId를 조합해서 레디스에 등록
- Long userId = customUserDetails.getUserId();
- redisReadCount = viewCountService.readCountUp(String.valueOf(userId), String.valueOf(postId));
-}
-
-PostDetailResponse postDetailResponse = postService.getPost(postId);
-postDetailResponse.addReadCount(redisReadCount);
+String viewerId = customUserDetails != null
+ ? String.valueOf(customUserDetails.getUserId())
+ : guestId;
+Long currentUserId = customUserDetails != null ? customUserDetails.getUserId() : null;
+
+PostDetailResponse postDetailResponse = postService.getPostDetail(postId, currentUserId, viewerId);📝 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.
| Long redisReadCount = 0L; | |
| if(customUserDetails == null) { | |
| // 비회원인 경우 | |
| // 쿠키값을 레디스에 등록 | |
| redisReadCount = viewCountService.readCountUp(guestId, String.valueOf(postId)); | |
| } else { | |
| // 회원인 경우 | |
| // userId,postId를 조합해서 레디스에 등록 | |
| Long userId = customUserDetails.getUserId(); | |
| redisReadCount = viewCountService.readCountUp(String.valueOf(userId), String.valueOf(postId)); | |
| } | |
| PostDetailResponse postDetailResponse = postService.getPost(postId); | |
| postDetailResponse.addReadCount(redisReadCount); | |
| // 컨트롤러에서는 비즈니스 로직을 호출만 합니다. | |
| // viewCount 증가와 DTO 조립은 서비스 레이어로 위임 | |
| String viewerId = customUserDetails != null | |
| ? String.valueOf(customUserDetails.getUserId()) | |
| : guestId; | |
| Long currentUserId = customUserDetails != null ? customUserDetails.getUserId() : null; | |
| PostDetailResponse postDetailResponse = | |
| postService.getPostDetail(postId, currentUserId, viewerId); |
🤖 Prompt for AI Agents
In
backend/src/main/java/org/juniortown/backend/post/controller/PostController.java
around lines 78 to 91, move the read count increment logic from the controller
to the service layer. Refactor the service method to accept user information and
postId, handle the read count update internally, and return a fully constructed
PostDetailResponse with the updated read count included. This eliminates
business logic from the controller and avoids modifying the response DTO after
creation.
backend/src/main/java/org/juniortown/backend/post/dto/response/PostDetailResponse.java
Outdated
Show resolved
Hide resolved
backend/src/main/java/org/juniortown/backend/post/dto/response/PostResponse.java
Outdated
Show resolved
Hide resolved
backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java
Show resolved
Hide resolved
| import org.juniortown.backend.config.RedisTestConfig; | ||
| import org.juniortown.backend.config.SyncConfig; |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Like 기능에서 Redis 설정 필요성 확인
좋아요 기능 테스트에 Redis 관련 설정을 추가하셨는데, LikeController나 LikeService에서 실제로 Redis 기능을 사용하는지 확인이 필요합니다.
Also applies to: 29-29, 40-40
🏁 Script executed:
#!/bin/bash
# Like 관련 컨트롤러와 서비스에서 Redis 사용 여부 확인
rg -A 5 -B 5 "Redis|ViewCount" backend/src/main/java/org/juniortown/backend/like/
# LikeController에서 Redis 관련 의존성 주입 확인
ast-grep --pattern 'class LikeController {
$$$
}'Length of output: 137
추가로 Redis 설정의 실제 사용 여부를 확인하기 위해 아래 스크립트를 실행해 주세요.
🏁 Script executed:
#!/bin/bash
# 1) Like 패키지 내 Redis 언급 검색
echo "▶ Like 모듈 내 Redis 관련 코드 검색"
rg -i redis backend/src/main/java/org/juniortown/backend/like
# 2) RedisTestConfig 클래스 내용 확인
echo "▶ RedisTestConfig 클래스 내용"
rg -A5 -B5 'class RedisTestConfig' backend/src/main/java/org/juniortown/backend/config
# 3) SyncConfig 클래스 내용 확인
echo "▶ SyncConfig 클래스 내용"
rg -A5 -B5 'class SyncConfig' backend/src/main/java/org/juniortown/backend/configLength of output: 407
RedisTestConfig와 SyncConfig의 실제 경로 및 내용을 확인해 주세요.
🏁 Script executed:
#!/bin/bash
# 1) RedisTestConfig, SyncConfig 클래스 파일 경로 검색
echo "▶ Config 클래스 경로 찾기"
rg -l "class RedisTestConfig"
rg -l "class SyncConfig"
# 2) 찾은 파일 내용 상위 20줄 확인
echo "▶ RedisTestConfig 내용"
for f in $(rg -l "class RedisTestConfig"); do
echo "----- $f -----"
sed -n '1,20p' "$f"
done
echo "▶ SyncConfig 내용"
for f in $(rg -l "class SyncConfig"); do
echo "----- $f -----"
sed -n '1,20p' "$f"
done
# 3) LikeControllerTest 상단 imports 및 설정 확인
echo "▶ LikeControllerTest imports"
sed -n '1,30p' backend/src/test/java/org/juniortown/backend/like/controller/LikeControllerTest.javaLength of output: 4013
LikeControllerTest에서 실제로 @import와 @activeprofiles로 해당 설정을 불러오는지 확인해 주세요.
🏁 Script executed:
#!/bin/bash
echo "▶ LikeControllerTest 어노테이션 및 설정 확인 (1~80줄)"
sed -n '1,80p' backend/src/test/java/org/juniortown/backend/like/controller/LikeControllerTest.javaLength of output: 2974
LikeControllerTest에서 불필요한 RedisTestConfig·SyncConfig 제거
LikeController/LikeService 패키지 내에 Redis를 참조하는 코드는 없으므로, 테스트에 Redis 설정을 임포트할 필요가 없습니다. 아래 위치에서 관련 import 및 @Import 어노테이션을 제거해 주세요.
- 파일:
backend/src/test/java/org/juniortown/backend/like/controller/LikeControllerTest.java
• import 문(8–9행)
•@Import({RedisTestConfig.class, SyncConfig.class})어노테이션
- import org.juniortown.backend.config.RedisTestConfig;
- import org.juniortown.backend.config.SyncConfig;
...
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
- @Import({RedisTestConfig.class, SyncConfig.class})
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Transactional
class LikeControllerTest {📝 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.
| import org.juniortown.backend.config.RedisTestConfig; | |
| import org.juniortown.backend.config.SyncConfig; | |
| // … other import statements … | |
| @SpringBootTest | |
| @AutoConfigureMockMvc | |
| @ActiveProfiles("test") | |
| @TestInstance(TestInstance.Lifecycle.PER_CLASS) | |
| @Transactional | |
| class LikeControllerTest { | |
| // … | |
| } |
🤖 Prompt for AI Agents
In
backend/src/test/java/org/juniortown/backend/like/controller/LikeControllerTest.java
around lines 8 to 9, remove the unnecessary imports of RedisTestConfig and
SyncConfig since LikeController and LikeService do not reference Redis. Also,
locate and delete the @Import annotation that includes RedisTestConfig.class and
SyncConfig.class to clean up the test configuration.
backend/src/test/java/org/juniortown/backend/post/service/ViewCountServiceTest.java
Outdated
Show resolved
Hide resolved
backend/src/test/java/org/juniortown/backend/post/service/ViewCountServiceTest.java
Show resolved
Hide resolved
backend/src/test/java/org/juniortown/backend/post/service/ViewCountServiceTest.java
Show resolved
Hide resolved
1.Controller에서 처리하던 조회수 증가 로직을 서비스단으로 옮김. 2.PostDetailResponse에서 readCount 증가 로직 삭제 3.PostService에서 getPost의 자기가 좋아요 했는지 여부를 확인하기 위한 코드에 오류가 있어서 수정 4.ViewCountService에서 메서드를 싹 다 분리 5.PostService에서 viewerId가 Long인지 UUID인지에 따라서 다르게 동작
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java (1)
27-34: 쿠키 보안 설정을 강화해주세요.정적 분석 도구에서 지적한 바와 같이 쿠키 보안 설정이 부족합니다.
Secure플래그와SameSite속성을 추가하여 보안을 강화하세요.다음과 같이 수정을 권장합니다:
if(!hasGuestId) { String guestId = UUID.randomUUID().toString(); Cookie cookie = new Cookie(COOKIE_NAME, guestId); cookie.setPath("/"); cookie.setMaxAge(60 * 60 * 24 * 15); // 15 days cookie.setHttpOnly(true); + // 프로덕션 환경에서는 반드시 true로 설정 + cookie.setSecure(true); // HTTPS 환경에서만 + // Spring Boot 3.x 이상에서는 ResponseCookie 사용 권장 response.addCookie(cookie); }참고: 개발 환경에서 HTTPS를 사용하지 않는 경우 프로파일 기반으로 설정을 분리하는 것을 고려하세요.
backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java (1)
157-157: 중복된 테스트 이름을 수정하세요.두 번째 테스트의 DisplayName이 "회원"으로 되어 있지만, 실제로는 비회원 테스트입니다.
-@DisplayName("게시글 조회수 증가 성공(회원) - 중복키 존재 x") +@DisplayName("게시글 조회수 증가 성공(비회원) - 중복키 존재 x")
🧹 Nitpick comments (2)
backend/src/main/java/org/juniortown/backend/post/service/PostService.java (1)
139-146: 메서드명을 더 명확하게 변경하세요.
isLong메서드명은 의도를 명확히 전달하지 못합니다.-private boolean isLong(String str) { +private boolean isMemberId(String viewerId) { try { - Long.parseLong(str); + Long.parseLong(viewerId); return true; } catch (NumberFormatException e) { return false; } }또는 더 명시적으로:
private boolean isAuthenticatedUser(String viewerId) { // 회원 ID는 숫자, 게스트 ID는 UUID 형식 return viewerId != null && viewerId.matches("\\d+"); }backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java (1)
290-315: 주석 처리된 테스트 코드를 정리하세요.주석 처리된 동기화 락 실패 테스트는 다음 중 하나를 선택하세요:
- 테스트가 필요하다면 제대로 구현
- 불필요하다면 완전히 제거
현재 상태로는 코드베이스를 어지럽히기만 합니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
backend/build.gradle(1 hunks)backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java(1 hunks)backend/src/main/java/org/juniortown/backend/post/controller/PostController.java(3 hunks)backend/src/main/java/org/juniortown/backend/post/dto/response/PostDetailResponse.java(1 hunks)backend/src/main/java/org/juniortown/backend/post/dto/response/PostResponse.java(2 hunks)backend/src/main/java/org/juniortown/backend/post/service/PostService.java(3 hunks)backend/src/main/java/org/juniortown/backend/post/service/ViewCountService.java(1 hunks)backend/src/main/java/org/juniortown/backend/post/service/ViewCountSyncService.java(1 hunks)backend/src/test/java/org/juniortown/backend/config/RedisTestConfig.java(1 hunks)backend/src/test/java/org/juniortown/backend/controller/AuthControllerTest.java(2 hunks)backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java(1 hunks)backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java(8 hunks)backend/src/test/java/org/juniortown/backend/post/service/ViewCountServiceTest.java(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- backend/src/test/java/org/juniortown/backend/controller/AuthControllerTest.java
🚧 Files skipped from review as they are similar to previous changes (9)
- backend/src/main/java/org/juniortown/backend/post/service/ViewCountSyncService.java
- backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java
- backend/src/main/java/org/juniortown/backend/post/dto/response/PostDetailResponse.java
- backend/build.gradle
- backend/src/main/java/org/juniortown/backend/post/controller/PostController.java
- backend/src/main/java/org/juniortown/backend/post/dto/response/PostResponse.java
- backend/src/test/java/org/juniortown/backend/config/RedisTestConfig.java
- backend/src/main/java/org/juniortown/backend/post/service/ViewCountService.java
- backend/src/test/java/org/juniortown/backend/post/service/ViewCountServiceTest.java
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: Doncham
PR: Doncham/JuniorTown#12
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostResponse.java:18-20
Timestamp: 2025-07-26T02:32:29.833Z
Learning: PostResponse와 PostDetailResponse 클래스에서 readCount 필드는 Redis에서 실시간 조회수를 가져와 addReadCount() 메서드로 더해주는 로직 때문에 가변 필드로 유지되어야 한다. 단순한 불변성 일관성보다는 실제 비즈니스 로직의 필요성을 우선 고려해야 한다.
Learnt from: Doncham
PR: Doncham/JuniorTown#10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
backend/src/main/java/org/juniortown/backend/post/service/PostService.java (4)
Learnt from: Doncham
PR: #12
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostResponse.java:18-20
Timestamp: 2025-07-26T02:32:29.833Z
Learning: PostResponse와 PostDetailResponse 클래스에서 readCount 필드는 Redis에서 실시간 조회수를 가져와 addReadCount() 메서드로 더해주는 로직 때문에 가변 필드로 유지되어야 한다. 단순한 불변성 일관성보다는 실제 비즈니스 로직의 필요성을 우선 고려해야 한다.
Learnt from: Doncham
PR: #10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
Learnt from: Doncham
PR: #12
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostResponse.java:40-52
Timestamp: 2025-07-26T02:35:05.655Z
Learning: PostResponse.from() 메서드에 기본값을 설정하면 update 작업에서도 해당 메서드를 사용하기 때문에 likeCount가 0으로 초기화되어 문제가 발생한다. 대신 update 작업을 위한 별도의 응답 클래스를 만드는 것이 권장된다.
Learnt from: Doncham
PR: #10
File: backend/src/main/java/org/juniortown/backend/post/controller/PostController.java:58-65
Timestamp: 2025-07-08T15:09:35.134Z
Learning: 사용자 Doncham은 Spring Security와 인증 처리에 대해 별도로 공부하는 것을 선호하며, 당장 구현보다는 학습을 통해 제대로 이해한 후 적용하는 접근 방식을 취한다.
backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java (3)
Learnt from: Doncham
PR: #12
File: backend/src/test/java/org/juniortown/backend/controller/AuthControllerTest.java:7-8
Timestamp: 2025-07-26T12:44:23.551Z
Learning: Spring Boot 통합 테스트(@SpringBootTest)에서는 전체 애플리케이션 컨텍스트가 로드되므로, 특정 컨트롤러가 직접 사용하지 않는 의존성이라도 해당 의존성의 빈 생성 과정에서 외부 서비스(Redis, DB 등) 연결을 시도할 수 있다. 이런 경우 테스트용 mock 설정이 필요하다.
Learnt from: Doncham
PR: #12
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostResponse.java:18-20
Timestamp: 2025-07-26T02:32:29.833Z
Learning: PostResponse와 PostDetailResponse 클래스에서 readCount 필드는 Redis에서 실시간 조회수를 가져와 addReadCount() 메서드로 더해주는 로직 때문에 가변 필드로 유지되어야 한다. 단순한 불변성 일관성보다는 실제 비즈니스 로직의 필요성을 우선 고려해야 한다.
Learnt from: Doncham
PR: #10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
🧬 Code Graph Analysis (1)
backend/src/main/java/org/juniortown/backend/post/service/PostService.java (4)
backend/src/main/java/org/juniortown/backend/post/exception/PostDeletePermissionDeniedException.java (1)
PostDeletePermissionDeniedException(6-18)backend/src/main/java/org/juniortown/backend/post/exception/PostNotFoundException.java (1)
PostNotFoundException(5-15)backend/src/main/java/org/juniortown/backend/post/exception/PostUpdatePermissionDeniedException.java (1)
PostUpdatePermissionDeniedException(6-17)backend/src/main/java/org/juniortown/backend/user/exception/UserNotFoundException.java (1)
UserNotFoundException(6-15)
🪛 ast-grep (0.38.6)
backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java
[warning] 32-32: A cookie was detected without setting the 'secure' flag. The 'secure' flag for cookies prevents the client from transmitting the cookie over insecure channels such as HTTP. Set the 'secure' flag by calling '.setSecure(true);'.
Context: response.addCookie(cookie);
Note: [CWE-614] Sensitive Cookie in HTTPS Session Without 'Secure' Attribute. [REFERENCES]
- https://owasp.org/www-community/controls/SecureCookieAttribute
(cookie-missing-secure-flag-java)
[warning] 32-32: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: response.addCookie(cookie);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application
(cookie-missing-samesite-java)
🔇 Additional comments (4)
backend/src/main/java/org/juniortown/backend/post/service/PostService.java (3)
46-54: 팩토리 메서드 패턴 사용이 일관성 있게 적용되었습니다.
PostResponse.from()정적 팩토리 메서드를 사용하여 DTO 생성 방식이 통일되었습니다.
72-84: update 메서드도 동일한 팩토리 메서드 패턴을 사용합니다.
110-137: 좋아요 확인 로직이 올바르게 수정되었습니다.이전 리뷰에서 지적된 문제가 해결되어, 현재 조회하는 사용자의 ID(
viewerId)로 좋아요 여부를 확인하도록 수정되었습니다.backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java (1)
78-88: 테스트 컨테이너 설정이 잘 구성되었습니다.Redis Testcontainers를 사용하여 테스트 격리가 잘 이루어지고 있습니다. 커스텀 포트(6380) 사용은 기본 Redis 포트와의 충돌을 방지합니다.
backend/src/main/java/org/juniortown/backend/post/service/PostService.java
Show resolved
Hide resolved
mGet을 통해 레디스에게 10번 통신이 아닌 1번의 통신으로 10개의 게시글에 조회수 캐시를 받아온다.
1.레디스 연동
2.게시글 상세 조회 시 레디스의 키값 증가, 단시간 내의 중복 조회 막는 로직 구현
3.게시글 조회 시 조회수도 내려주기(DB값 + Redis 증분)
4.주기적으로 Redis 증분값을 DB에 동기화
closed #11
Summary by CodeRabbit
신규 기능
버그 수정
테스트
환경설정
UI 개선