diff --git a/settings.gradle b/settings.gradle index 14b6fd8b..e0a6020a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -12,3 +12,5 @@ include 'smeem-output-notification:firebase' include 'smeem-output-oauth' include 'smeem-output-oauth:apple' include 'smeem-output-oauth:kakao' +include 'smeem-output-cache' +include 'smeem-output-cache:redis' diff --git a/smeem-application/src/main/java/com/smeem/application/domain/member/MemberService.java b/smeem-application/src/main/java/com/smeem/application/domain/member/MemberService.java index 910370f7..47f25b1e 100644 --- a/smeem-application/src/main/java/com/smeem/application/domain/member/MemberService.java +++ b/smeem-application/src/main/java/com/smeem/application/domain/member/MemberService.java @@ -3,7 +3,6 @@ import com.smeem.application.domain.badge.Badge; import com.smeem.application.domain.trainingtime.DayType; import com.smeem.application.domain.trainingtime.TrainingTime; -import com.smeem.application.domain.visit.Visit; import com.smeem.application.port.input.MemberUseCase; import com.smeem.application.port.input.dto.request.member.UpdateMemberHasPushAlarmRequest; import com.smeem.application.port.input.dto.request.member.UpdateMemberRequest; @@ -13,6 +12,7 @@ import com.smeem.application.port.input.dto.response.member.UpdateMemberResponse; import com.smeem.application.port.input.dto.response.member.UsernameDuplicatedResponse; import com.smeem.application.port.input.dto.response.plan.RetrieveMemberPlanResponse; +import com.smeem.application.port.output.cache.CachePort; import com.smeem.application.port.output.persistence.*; import com.smeem.common.logger.HookLogger; import com.smeem.common.logger.LoggingMessage; @@ -21,6 +21,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; @Service @@ -31,10 +33,10 @@ public class MemberService implements MemberUseCase { private final TrainingTimePort trainingTimePort; private final BadgePort badgePort; private final DiaryPort diaryPort; - private final VisitPort visitPort; private final GoalPort goalPort; private final PlanPort planPort; private final HookLogger hookLogger; + private final CachePort cachePort; @Transactional public UpdateMemberResponse updateMember(long memberId, UpdateMemberRequest request) { @@ -89,12 +91,16 @@ public RetrievePerformanceResponse retrieveMemberPerformance(long memberId) { } @Transactional - public void checkAttendance(long memberId) { - val foundMember = memberPort.findById(memberId); - if (!visitPort.isExistByMemberAndToday(foundMember.getId())) { + public void visit(long memberId) { + Member foundMember = memberPort.findById(memberId); + String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String key = "visit:" + today; + + if (!cachePort.getBit(key, foundMember.getId())) { + System.out.println("test"); foundMember.visit(); memberPort.update(foundMember); - visitPort.visit(new Visit(foundMember.getId())); + cachePort.setBit(key, foundMember.getId(), true); } } diff --git a/smeem-application/src/main/java/com/smeem/application/domain/visit/Visit.java b/smeem-application/src/main/java/com/smeem/application/domain/visit/Visit.java deleted file mode 100644 index 2095ba27..00000000 --- a/smeem-application/src/main/java/com/smeem/application/domain/visit/Visit.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.smeem.application.domain.visit; - -import lombok.Getter; - -import java.time.LocalDate; - -@Getter -public class Visit { - Long id; - long memberId; - LocalDate visitedAt; - - public Visit(long memberId) { - this.memberId = memberId; - this.visitedAt = LocalDate.now(); - } -} diff --git a/smeem-application/src/main/java/com/smeem/application/port/input/MemberUseCase.java b/smeem-application/src/main/java/com/smeem/application/port/input/MemberUseCase.java index f5145c89..087e675b 100644 --- a/smeem-application/src/main/java/com/smeem/application/port/input/MemberUseCase.java +++ b/smeem-application/src/main/java/com/smeem/application/port/input/MemberUseCase.java @@ -15,7 +15,7 @@ public interface MemberUseCase { UsernameDuplicatedResponse checkUsernameDuplicated(String username); void updateMemberHasPush(long memberId, UpdateMemberHasPushAlarmRequest request); RetrievePerformanceResponse retrieveMemberPerformance(long memberId); - void checkAttendance(long memberId); + void visit(long memberId); void updatePlan(long memberId, UpdateMemberPlanRequest request); RetrieveMemberPlanResponse retrieveMemberPlan(long memberId); } diff --git a/smeem-application/src/main/java/com/smeem/application/port/output/cache/CachePort.java b/smeem-application/src/main/java/com/smeem/application/port/output/cache/CachePort.java new file mode 100644 index 00000000..f5d500e9 --- /dev/null +++ b/smeem-application/src/main/java/com/smeem/application/port/output/cache/CachePort.java @@ -0,0 +1,6 @@ +package com.smeem.application.port.output.cache; + +public interface CachePort { + void setBit(String key, long offset, boolean value); + boolean getBit(String key, long offset); +} diff --git a/smeem-application/src/main/java/com/smeem/application/port/output/persistence/VisitPort.java b/smeem-application/src/main/java/com/smeem/application/port/output/persistence/VisitPort.java deleted file mode 100644 index 63c9d9e4..00000000 --- a/smeem-application/src/main/java/com/smeem/application/port/output/persistence/VisitPort.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.smeem.application.port.output.persistence; - -import com.smeem.application.domain.visit.Visit; - -public interface VisitPort { - void visit(Visit visit); - boolean isExistByMemberAndToday(long memberId); -} diff --git a/smeem-bootstrap/build.gradle b/smeem-bootstrap/build.gradle index 2e69f09d..14c6ab5e 100644 --- a/smeem-bootstrap/build.gradle +++ b/smeem-bootstrap/build.gradle @@ -9,6 +9,8 @@ dependencies { implementation project(':smeem-output-oauth') implementation project(':smeem-output-oauth:apple') implementation project(':smeem-output-oauth:kakao') + implementation project(':smeem-output-cache') + implementation project(':smeem-output-cache:redis') implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-actuator' diff --git a/smeem-bootstrap/src/main/resources/application-dev.yml b/smeem-bootstrap/src/main/resources/application-dev.yml index 76834bf1..84548e8e 100644 --- a/smeem-bootstrap/src/main/resources/application-dev.yml +++ b/smeem-bootstrap/src/main/resources/application-dev.yml @@ -7,6 +7,7 @@ spring: - classpath:smeem-config/application-dev.yml - classpath:notification-config/application-dev.yml - classpath:common-config/application-dev.yml + - classpath:redis-config/application-dev.yml logging.level: org.hibernate.SQL: debug diff --git a/smeem-bootstrap/src/main/resources/application-local.yml b/smeem-bootstrap/src/main/resources/application-local.yml index d85a1e7b..c4efcc01 100644 --- a/smeem-bootstrap/src/main/resources/application-local.yml +++ b/smeem-bootstrap/src/main/resources/application-local.yml @@ -7,6 +7,7 @@ spring: - classpath:smeem-config/application-local.yml - classpath:notification-config/application-local.yml - classpath:common-config/application-local.yml + - classpath:redis-config/application-local.yml logging.level: org.hibernate.SQL: debug diff --git a/smeem-bootstrap/src/main/resources/application-prod.yml b/smeem-bootstrap/src/main/resources/application-prod.yml index 6a718839..bf3eb50b 100644 --- a/smeem-bootstrap/src/main/resources/application-prod.yml +++ b/smeem-bootstrap/src/main/resources/application-prod.yml @@ -7,6 +7,7 @@ spring: - classpath:smeem-config/application-prod.yml - classpath:notification-config/application-prod.yml - classpath:common-config/application-prod.yml + - classpath:redis-config/application-prod.yml logging.level: org.hibernate.SQL: debug diff --git a/smeem-input-http/src/main/java/com/smeem/http/controller/MemberApi.java b/smeem-input-http/src/main/java/com/smeem/http/controller/MemberApi.java index bdb52f8b..7d414d31 100644 --- a/smeem-input-http/src/main/java/com/smeem/http/controller/MemberApi.java +++ b/smeem-input-http/src/main/java/com/smeem/http/controller/MemberApi.java @@ -76,9 +76,9 @@ public SmeemResponse retrievePerformance(Principal @ResponseStatus(HttpStatus.OK) @PatchMapping("/visit") - public SmeemResponse checkAttendance(Principal principal) { + public SmeemResponse visit(Principal principal) { val memberId = smeemConverter.toMemberId(principal); - memberUseCase.checkAttendance(memberId); + memberUseCase.visit(memberId); return SmeemResponse.of(SmeemMessage.UPDATE_MEMBER); } } diff --git a/smeem-input-http/src/main/java/com/smeem/http/controller/docs/MemberApiDocs.java b/smeem-input-http/src/main/java/com/smeem/http/controller/docs/MemberApiDocs.java index a12e8a4b..14a57a5b 100644 --- a/smeem-input-http/src/main/java/com/smeem/http/controller/docs/MemberApiDocs.java +++ b/smeem-input-http/src/main/java/com/smeem/http/controller/docs/MemberApiDocs.java @@ -93,5 +93,5 @@ SmeemResponse updateMemberHasPushAlarm( responseCode = "200", description = "OK success") }) - SmeemResponse checkAttendance(@Parameter(hidden = true) Principal principal); + SmeemResponse visit(@Parameter(hidden = true) Principal principal); } diff --git a/smeem-output-cache/build.gradle b/smeem-output-cache/build.gradle new file mode 100644 index 00000000..0ee5b3c3 --- /dev/null +++ b/smeem-output-cache/build.gradle @@ -0,0 +1,27 @@ +project(':smeem-output-cache') { + dependencies { + } +} + +project(':smeem-output-cache:redis') { + dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + } +} + +allprojects { + dependencies { + implementation project(':smeem-common') + implementation project(':smeem-application') + + implementation 'org.springframework.boot:spring-boot-starter-web' + } + + tasks.bootJar { + enabled = false + } + + tasks.jar { + enabled = true + } +} diff --git a/smeem-output-cache/redis/src/main/java/com/smeem/output/cache/redis/adapter/CacheAdapter.java b/smeem-output-cache/redis/src/main/java/com/smeem/output/cache/redis/adapter/CacheAdapter.java new file mode 100644 index 00000000..0bc1b77f --- /dev/null +++ b/smeem-output-cache/redis/src/main/java/com/smeem/output/cache/redis/adapter/CacheAdapter.java @@ -0,0 +1,22 @@ +package com.smeem.output.cache.redis.adapter; + +import com.smeem.application.port.output.cache.CachePort; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; + +@Configuration +@RequiredArgsConstructor +public class CacheAdapter implements CachePort { + private final RedisTemplate redisTemplate; + + @Override + public void setBit(String key, long offset, boolean value) { + redisTemplate.opsForValue().setBit(key, offset, value); + } + + @Override + public boolean getBit(String key, long offset) { + return Boolean.TRUE.equals(redisTemplate.opsForValue().getBit(key, offset)); + } +} diff --git a/smeem-output-cache/redis/src/main/java/com/smeem/output/cache/redis/config/RedisConfig.java b/smeem-output-cache/redis/src/main/java/com/smeem/output/cache/redis/config/RedisConfig.java new file mode 100644 index 00000000..1ae4a1d1 --- /dev/null +++ b/smeem-output-cache/redis/src/main/java/com/smeem/output/cache/redis/config/RedisConfig.java @@ -0,0 +1,72 @@ +package com.smeem.output.cache.redis.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisPassword; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@Configuration +@EnableRedisRepositories +@EnableCaching +@EnableConfigurationProperties(RedisProperties.class) +@RequiredArgsConstructor +public class RedisConfig { + + @Bean + public LettuceConnectionFactory lettuceConnectionFactory(RedisProperties redisProperties) { + RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(); + redisConfig.setHostName(redisProperties.host()); + redisConfig.setPort(redisProperties.port()); + redisConfig.setPassword(RedisPassword.of(redisProperties.password())); + + LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() + .useSsl() + .build(); + + return new LettuceConnectionFactory(redisConfig, clientConfig); + } + + @Bean + public RedisTemplate redisTemplate( + RedisConnectionFactory redisConnectionFactory, + ObjectMapper objectMapper + ) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)); + return redisTemplate; + } + + @Bean + public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { + RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofDays(7)) + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new GenericJackson2JsonRedisSerializer())); + + return RedisCacheManager.builder(redisConnectionFactory) + .cacheDefaults(redisCacheConfiguration) + .build(); + } +} diff --git a/smeem-output-cache/redis/src/main/java/com/smeem/output/cache/redis/config/RedisProperties.java b/smeem-output-cache/redis/src/main/java/com/smeem/output/cache/redis/config/RedisProperties.java new file mode 100644 index 00000000..b001ad68 --- /dev/null +++ b/smeem-output-cache/redis/src/main/java/com/smeem/output/cache/redis/config/RedisProperties.java @@ -0,0 +1,11 @@ +package com.smeem.output.cache.redis.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "redis") +public record RedisProperties( + String host, + int port, + String password +) { +} diff --git a/smeem-output-cache/redis/src/main/resources/redis-config/application-dev.yml b/smeem-output-cache/redis/src/main/resources/redis-config/application-dev.yml new file mode 100644 index 00000000..c9637c51 --- /dev/null +++ b/smeem-output-cache/redis/src/main/resources/redis-config/application-dev.yml @@ -0,0 +1,5 @@ +redis: + host: ${REDIS_HOST} + port: 6379 + password: ${REDIS_PASSWORD} + ssl: true diff --git a/smeem-output-cache/redis/src/main/resources/redis-config/application-local.yml b/smeem-output-cache/redis/src/main/resources/redis-config/application-local.yml new file mode 100644 index 00000000..c9637c51 --- /dev/null +++ b/smeem-output-cache/redis/src/main/resources/redis-config/application-local.yml @@ -0,0 +1,5 @@ +redis: + host: ${REDIS_HOST} + port: 6379 + password: ${REDIS_PASSWORD} + ssl: true diff --git a/smeem-output-cache/redis/src/main/resources/redis-config/application-prod.yml b/smeem-output-cache/redis/src/main/resources/redis-config/application-prod.yml new file mode 100644 index 00000000..c9637c51 --- /dev/null +++ b/smeem-output-cache/redis/src/main/resources/redis-config/application-prod.yml @@ -0,0 +1,5 @@ +redis: + host: ${REDIS_HOST} + port: 6379 + password: ${REDIS_PASSWORD} + ssl: true diff --git a/smeem-output-persistence/postgresql/src/main/java/com/smeem/persistence/postgresql/adapter/MemberAdapter.java b/smeem-output-persistence/postgresql/src/main/java/com/smeem/persistence/postgresql/adapter/MemberAdapter.java index b83b7bff..70a46b46 100644 --- a/smeem-output-persistence/postgresql/src/main/java/com/smeem/persistence/postgresql/adapter/MemberAdapter.java +++ b/smeem-output-persistence/postgresql/src/main/java/com/smeem/persistence/postgresql/adapter/MemberAdapter.java @@ -24,7 +24,6 @@ public class MemberAdapter implements MemberPort { private final DeletedDiaryRepository deletedDiaryRepository; private final DiaryRepository diaryRepository; private final TrainingTimeRepository trainingTimeRepository; - private final VisitRepository visitRepository; private final WithdrawRepository withdrawRepository; @Override @@ -55,7 +54,6 @@ public void deleteById(long id) { deletedDiaryRepository.deleteByMemberId(id); diaryRepository.deleteByMemberId(id); trainingTimeRepository.deleteByMemberId(id); - visitRepository.deleteByMemberId(id); memberRepository.deleteById(id); } diff --git a/smeem-output-persistence/postgresql/src/main/java/com/smeem/persistence/postgresql/adapter/VisitAdapter.java b/smeem-output-persistence/postgresql/src/main/java/com/smeem/persistence/postgresql/adapter/VisitAdapter.java deleted file mode 100644 index eb32c57a..00000000 --- a/smeem-output-persistence/postgresql/src/main/java/com/smeem/persistence/postgresql/adapter/VisitAdapter.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.smeem.persistence.postgresql.adapter; - -import com.smeem.application.domain.visit.Visit; -import com.smeem.application.port.output.persistence.VisitPort; -import com.smeem.persistence.postgresql.persistence.entity.VisitEntity; -import com.smeem.persistence.postgresql.persistence.repository.VisitRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; - -import java.time.LocalDate; - -@Repository -@RequiredArgsConstructor -public class VisitAdapter implements VisitPort { - private final VisitRepository visitRepository; - - @Override - public void visit(Visit visit) { - visitRepository.save(new VisitEntity(visit)); - } - - @Override - public boolean isExistByMemberAndToday(long memberId) { - return visitRepository.existsByMemberIdAndVisitedAt(memberId, LocalDate.now()); - } -} diff --git a/smeem-output-persistence/postgresql/src/main/java/com/smeem/persistence/postgresql/persistence/entity/VisitEntity.java b/smeem-output-persistence/postgresql/src/main/java/com/smeem/persistence/postgresql/persistence/entity/VisitEntity.java deleted file mode 100644 index 06c1c716..00000000 --- a/smeem-output-persistence/postgresql/src/main/java/com/smeem/persistence/postgresql/persistence/entity/VisitEntity.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.smeem.persistence.postgresql.persistence.entity; - -import com.smeem.application.domain.visit.Visit; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.LocalDate; - -@Entity -@Table(name = "visit", schema = "smeem") -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Getter -public class VisitEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - @Column(nullable = false) - private long memberId; - @Column(nullable = false) - private LocalDate visitedAt; - - public VisitEntity(Visit visit) { - this.memberId = visit.getMemberId(); - this.visitedAt = visit.getVisitedAt(); - } -} diff --git a/smeem-output-persistence/postgresql/src/main/java/com/smeem/persistence/postgresql/persistence/repository/VisitRepository.java b/smeem-output-persistence/postgresql/src/main/java/com/smeem/persistence/postgresql/persistence/repository/VisitRepository.java deleted file mode 100644 index 6a68ce06..00000000 --- a/smeem-output-persistence/postgresql/src/main/java/com/smeem/persistence/postgresql/persistence/repository/VisitRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.smeem.persistence.postgresql.persistence.repository; - -import com.smeem.persistence.postgresql.persistence.entity.VisitEntity; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.time.LocalDate; - -public interface VisitRepository extends JpaRepository { - boolean existsByMemberIdAndVisitedAt(long memberId, LocalDate visitedAt); - void deleteByMemberId(long memberId); -} diff --git a/smeem-output-persistence/postgresql/src/main/resources/postgres-config/application-dev.yml b/smeem-output-persistence/postgresql/src/main/resources/postgres-config/application-dev.yml index c438bec7..da69532b 100644 --- a/smeem-output-persistence/postgresql/src/main/resources/postgres-config/application-dev.yml +++ b/smeem-output-persistence/postgresql/src/main/resources/postgres-config/application-dev.yml @@ -16,4 +16,5 @@ spring: format_sql: true default_batch_fetch_size: 1000 auto_quote_keyword: true - show-sql: true +# generate_statistics: true + show-sql: false diff --git a/smeem-output-persistence/postgresql/src/main/resources/postgres-config/application-local.yml b/smeem-output-persistence/postgresql/src/main/resources/postgres-config/application-local.yml index 77e42305..38278cc3 100644 --- a/smeem-output-persistence/postgresql/src/main/resources/postgres-config/application-local.yml +++ b/smeem-output-persistence/postgresql/src/main/resources/postgres-config/application-local.yml @@ -16,4 +16,5 @@ spring: format_sql: true default_batch_fetch_size: 1000 auto_quote_keyword: true - show-sql: true + generate_statistics: true + show-sql: false diff --git a/smeem-output-persistence/postgresql/src/main/resources/postgres-config/application-prod.yml b/smeem-output-persistence/postgresql/src/main/resources/postgres-config/application-prod.yml index f9148cb4..27ec4472 100644 --- a/smeem-output-persistence/postgresql/src/main/resources/postgres-config/application-prod.yml +++ b/smeem-output-persistence/postgresql/src/main/resources/postgres-config/application-prod.yml @@ -16,4 +16,5 @@ spring: format_sql: true default_batch_fetch_size: 1000 auto_quote_keyword: true - show-sql: true +# generate_statistics: true + show-sql: false