Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,3 @@ src/main/resources/*.yml
### VS Code ###
.vscode/

## local docker compose
docker/docker-compose.local.yml
42 changes: 42 additions & 0 deletions docker/docker-compose.local.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
version: "3.9"

services:
db:
image: postgis/postgis:16-3.4
container_name: sportize-postgis-local
restart: unless-stopped

environment:
POSTGRES_DB: sportize
POSTGRES_USER: angora
POSTGRES_PASSWORD: password
TZ: Asia/Seoul

ports:
- "5432:5432"

volumes:
- sportize_pgdata_local:/var/lib/postgresql/data

healthcheck:
test: ["CMD-SHELL", "pg_isready -U angora -d sportize"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7.2.4-alpine
container_name: sportize-redis-local
restart: unless-stopped
ports:
- "6379:6379"
command: ["redis-server", "--appendonly", "yes", "--requirepass", "password"]
volumes:
- sportize_redisdata_local:/data
healthcheck:
test: [ "CMD", "redis-cli", "-a", "password", "PING" ]
interval: 10s
timeout: 3s
retries: 5
volumes:
sportize_pgdata_local:
sportize_redisdata_local:
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.security.core.parameters.P;
import org.springframework.stereotype.Service;
Expand All @@ -23,6 +26,10 @@ public class LikeServiceImpl implements LikeService {

@Override
@Transactional
@Caching(evict = {
@CacheEvict(cacheNames = "likeCount", key = "#targetType + ':' + #targetId"),
@CacheEvict(cacheNames = "likeStatus", key = "#user.id + ':' + #targetType + ':' + #targetId")
})
public LikeResponse toggleLike(User user, LikeTargetType targetType, Long targetId) {

boolean liked = false; // 좋아요 여부 변수
Expand All @@ -39,18 +46,20 @@ public LikeResponse toggleLike(User user, LikeTargetType targetType, Long target
likeRepository.save(like);
liked = true;
}

long likeCount = getLikeCount(targetType, targetId); // 해당 타겟(게시물 or 댓글)의 좋아요 개수 저장 변수
// 토글 응답은 최신 값이 필요하므로 레포로 직접 count
long likeCount = likeRepository.countByTargetTypeAndTargetId(targetType, targetId);

return LikeResponse.of(liked, targetType, targetId, likeCount);
}

@Override
@Cacheable(cacheNames = "likeStatus", key = "#user.id + ':' + #targetType + ':' + #targetId")
public boolean isLiked(User user, LikeTargetType targetType, Long targetId) {
Comment on lines +56 to 57
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cd /repo && find . -name "LikeServiceImpl.java" -type f

Repository: Sportize/be

Length of output: 110


🏁 Script executed:

cd /repo && find . -name "*LikeController*.java" -type f 2>/dev/null || find . -name "*Like*Controller*.java" -type f 2>/dev/null | head -20

Repository: Sportize/be

Length of output: 204


🏁 Script executed:

cd /repo && head -80 src/main/java/com/be/sportizebe/domain/like/service/LikeServiceImpl.java

Repository: Sportize/be

Length of output: 110


🏁 Script executed:

head -80 src/main/java/com/be/sportizebe/domain/like/service/LikeServiceImpl.java

Repository: Sportize/be

Length of output: 2534


🏁 Script executed:

cat src/main/java/com/be/sportizebe/domain/like/controller/LikeController.java

Repository: Sportize/be

Length of output: 3197


🏁 Script executed:

grep -n "isLiked" src/main/java/com/be/sportizebe/domain/like/service/LikeServiceImpl.java

Repository: Sportize/be

Length of output: 137


비로그인 접근 시 SpEL NPE 위험이 실제로 발생할 수 있습니다.

getPostLikeStatusgetCommentLikeStatus 엔드포인트에서 @AuthenticationPrincipal User user는 인증이 없을 때 null을 주입합니다(required=true 미설정). 인증 없이 이 엔드포인트들에 접근 가능하므로, isLiked 메서드의 캐시 키 표현식 #user.id에서 NPE가 발생합니다.

null-safe 처리를 추가해 주세요:

💡 null-safe 처리 예시
-  `@Cacheable`(cacheNames = "likeStatus", key = "#user.id + ':' + `#targetType` + ':' + `#targetId`")
+  `@Cacheable`(
+      cacheNames = "likeStatus",
+      key = "#user.id + ':' + `#targetType` + ':' + `#targetId`",
+      condition = "#user != null"
+  )
   public boolean isLiked(User user, LikeTargetType targetType, Long targetId) {
+    if (user == null) {
+      return false;
+    }
     return likeRepository.existsByUserAndTargetTypeAndTargetId(user, targetType, targetId);
   }
🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/like/service/LikeServiceImpl.java`
around lines 56 - 57, The cache key in isLiked currently uses "#user.id" which
throws an NPE when `@AuthenticationPrincipal` User user is null for
unauthenticated calls (seen from getPostLikeStatus/getCommentLikeStatus); update
the `@Cacheable` key expression on isLiked to be null-safe (for example use a
ternary or null-check like "#user == null ? 'ANON' : `#user.id` + ':' +
`#targetType` + ':' + `#targetId`" or use SpEL null-safe operators) so that
unauthenticated requests produce a stable key instead of causing an exception.

return likeRepository.existsByUserAndTargetTypeAndTargetId(user, targetType, targetId);
}

@Override
@Cacheable(cacheNames = "likeCount", key = "#targetType + ':' + #targetId")
public long getLikeCount(LikeTargetType targetType, Long targetId) { // 좋아요 개수 추적 메서드
return likeRepository.countByTargetTypeAndTargetId(targetType, targetId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory)
new Jackson2JsonRedisSerializer<>(Long.class);
commentCountSerializer.setObjectMapper(objectMapper);

// likeStatus 캐시는 Boolean 타입으로 역직렬화
Jackson2JsonRedisSerializer<Boolean> likeStatusSerializer =
new Jackson2JsonRedisSerializer<>(Boolean.class);
likeStatusSerializer.setObjectMapper(objectMapper);


// 기본 캐시 설정
// TTL: 5분, Serializer: Object 기준
RedisCacheConfiguration defaultConfig =
Expand Down Expand Up @@ -91,6 +97,24 @@ public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory)
)
.entryTtl(Duration.ofSeconds(30))
);

cacheConfigs.put(
"likeCount",
RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(commentCountSerializer)
)
.entryTtl(Duration.ofSeconds(30))
);

cacheConfigs.put(
"likeStatus",
RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(likeStatusSerializer)
)
.entryTtl(Duration.ofSeconds(15))
);
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.be.sportizebe.domain.user.entity.User;
import com.be.sportizebe.domain.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
Expand All @@ -13,6 +14,8 @@
public class CustomUserDetailService implements UserDetailsService {
private final UserRepository userRepository;

@Cacheable(cacheNames = "userDetails", key = "#username", unless = "#result == null")
// result == null: 유저가 있으면- > 캐시에 저장, 유저가 없으면 -> 캐시에 저장 X
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
Expand Down