From 1088294500085f7ddf191749803909b7b6202ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=ED=9B=88=EA=B8=B0?= Date: Tue, 3 Feb 2026 02:19:39 +0900 Subject: [PATCH 1/3] =?UTF-8?q?:sparkles:Feat:=20isLiked=EC=BA=90=EC=8B=9C?= =?UTF-8?q?=20likeCount=EC=BA=90=EC=8B=9C=20=EC=A0=81=EC=9A=A9=20&=20toggl?= =?UTF-8?q?eLike=EC=8B=9C=20=EC=BA=90=EC=8B=9C=EB=AC=B4=ED=9A=A8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/like/service/LikeServiceImpl.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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); } From 46445b10c206b0ba0960a484ea3b933784cc00e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=ED=9B=88=EA=B8=B0?= Date: Tue, 3 Feb 2026 02:20:13 +0900 Subject: [PATCH 2/3] =?UTF-8?q?:sparkles:Feat:=20UserDetails=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=EC=BA=90=EC=8B=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/RedisCacheConfig.java | 24 +++++++++++++++++++ .../security/CustomUserDetailService.java | 3 +++ 2 files changed, 27 insertions(+) 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) From d6e333a501620e7dce9d4108db378564db015916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=ED=9B=88=EA=B8=B0?= Date: Tue, 3 Feb 2026 14:56:11 +0900 Subject: [PATCH 3/3] :wrench:Settings: local docker-compose.yml --- .gitignore | 2 -- docker/docker-compose.local.yml | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 docker/docker-compose.local.yml 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