diff --git a/.gitignore b/.gitignore index d24fcb6..630165c 100644 --- a/.gitignore +++ b/.gitignore @@ -39,5 +39,3 @@ src/main/resources/*.yml ### VS Code ### .vscode/ -## local docker compose -docker/docker-compose.local.yml \ No newline at end of file diff --git a/docker/docker-compose.local.yml b/docker/docker-compose.local.yml new file mode 100644 index 0000000..dc5445a --- /dev/null +++ b/docker/docker-compose.local.yml @@ -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: \ No newline at end of file diff --git a/src/main/java/com/be/sportizebe/domain/like/service/LikeServiceImpl.java b/src/main/java/com/be/sportizebe/domain/like/service/LikeServiceImpl.java index ae90c2b..ed6cd91 100644 --- a/src/main/java/com/be/sportizebe/domain/like/service/LikeServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/like/service/LikeServiceImpl.java @@ -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; @@ -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; // 좋아요 여부 변수 @@ -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) { 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); } diff --git a/src/main/java/com/be/sportizebe/global/config/RedisCacheConfig.java b/src/main/java/com/be/sportizebe/global/config/RedisCacheConfig.java index c55dc49..7bc269e 100644 --- a/src/main/java/com/be/sportizebe/global/config/RedisCacheConfig.java +++ b/src/main/java/com/be/sportizebe/global/config/RedisCacheConfig.java @@ -53,6 +53,12 @@ public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) new Jackson2JsonRedisSerializer<>(Long.class); commentCountSerializer.setObjectMapper(objectMapper); + // likeStatus 캐시는 Boolean 타입으로 역직렬화 + Jackson2JsonRedisSerializer likeStatusSerializer = + new Jackson2JsonRedisSerializer<>(Boolean.class); + likeStatusSerializer.setObjectMapper(objectMapper); + + // 기본 캐시 설정 // TTL: 5분, Serializer: Object 기준 RedisCacheConfiguration defaultConfig = @@ -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) diff --git a/src/main/java/com/be/sportizebe/global/security/CustomUserDetailService.java b/src/main/java/com/be/sportizebe/global/security/CustomUserDetailService.java index 6e67b8c..bdf61ae 100644 --- a/src/main/java/com/be/sportizebe/global/security/CustomUserDetailService.java +++ b/src/main/java/com/be/sportizebe/global/security/CustomUserDetailService.java @@ -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; @@ -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)