Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 레디스 도입 및 알림 개수를 캐쉬로 관리 #402

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
59293b7
chore: build.gradle에 Redis 의존성 추가
bugoverdose Sep 29, 2022
627479c
test: CouponEvent와 CouponHistory에 대한 Fixture 구현
bugoverdose Sep 29, 2022
3b11ce0
feat: HashMap 기반의 읽지 않은 알림 개수에 대한 캐쉬 저장소 구현
bugoverdose Sep 29, 2022
fba58da
feat: 읽지 않은 알림 개수 캐쉬 값을 증가시키고 초기화하는 기능 구현
bugoverdose Sep 29, 2022
e105c38
feat: 읽지 않은 알림 개수 캐쉬를 수정하는 메서드 구현
bugoverdose Sep 29, 2022
c2098e3
feat: 캐시를 통해 읽지 않은 알림 개수 계산
bugoverdose Sep 29, 2022
c1f0743
fix: gradle 의존성 형식 수정
bugoverdose Sep 29, 2022
3ff4203
feat: 내장 레디스 구동 및 연동하도록 셋업
bugoverdose Sep 29, 2022
dad0a10
feat: Redis에 연동되는 알림 개수 캐시 저장소 구현
bugoverdose Sep 29, 2022
b0bd2a4
feat: HashMap에서 Redis 기반의 캐시 저장소로 마이그레이션
bugoverdose Sep 29, 2022
8a3b26e
refactor: NoticeCacheRepository 클래스로 이름 수정
bugoverdose Sep 29, 2022
1ac55c3
fix: 캐시가 기존에 없던 경우 올바른 값으로 생성되도록 서비스 로직 수정
bugoverdose Sep 29, 2022
33e89e3
feat: 캐시가 존재하지 않는 경우 굳이 갱신하지 않도록 수정
bugoverdose Sep 29, 2022
f5ebd94
refactor: private 메서드 기반 중복 로직 제거
bugoverdose Sep 29, 2022
b85caa5
refactor: SonarCloud의 code smell 해결
bugoverdose Sep 30, 2022
9cc3ea3
feat: DBMS 접근 작업에 대해 @Transactional 추가
bugoverdose Sep 30, 2022
92d308c
feat: Redis 연동시 비밀번호 설정하도록 설정 추가
bugoverdose Oct 11, 2022
fcab300
chore: dev, prod 프로파일별 Redis 서버 연동 정보 추가
bugoverdose Oct 11, 2022
35efff3
fix: develop 브랜치 머지 후 충돌 해결
bugoverdose Oct 11, 2022
2294a7e
fix: develop 브랜치 머지 후 충돌 해결
bugoverdose Oct 12, 2022
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
4 changes: 4 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ dependencies {
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'

// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation group: 'it.ozimov', name: 'embedded-redis', version: '0.7.2'

asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,14 @@ public ResponseEntity<TokenResponse> login(@RequestParam String code) {
}

@PostMapping("/install/bot")
public ResponseEntity<Void> installSlackApp(
@RequestBody InstallSlackAppRequest installSlackAppRequest) {
public ResponseEntity<Void> installSlackApp(@RequestBody InstallSlackAppRequest installSlackAppRequest) {
authService.installSlackApp(installSlackAppRequest.getCode());

return ResponseEntity.ok().build();
}

@PostMapping("/signup/token")
public ResponseEntity<MemberCreateResponse> save(
@RequestBody MemberCreateRequest memberCreateRequest) {
public ResponseEntity<MemberCreateResponse> save(@RequestBody MemberCreateRequest memberCreateRequest) {
Long id = authService.signUp(memberCreateRequest);
MemberCreateResponse memberCreateResponse = authService.loginByMemberId(id);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.woowacourse.kkogkkog.common.config;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import redis.embedded.RedisServer;

@Slf4j
@Profile("default")
@Configuration
public class EmbeddedRedisConfig {

private final int redisPort;

private RedisServer redisServer;

public EmbeddedRedisConfig(@Value("${spring.redis.port}") final int redisPort) {
this.redisPort = redisPort;
}

@PostConstruct
public void redisServer() {
try {
redisServer = new RedisServer(redisPort);
redisServer.start();
} catch (RuntimeException e) {
log.info("{} 포트에 내장 레디스 서버를 구동하는 데 실패하였습니다.", redisPort);
}
}

@PreDestroy
public void stopRedis() {
if (redisServer != null) {
redisServer.stop();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.woowacourse.kkogkkog.common.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

@Configuration
public class RedisConfig {

private final String redisHost;
private final int redisPort;
private final String redisPassword;

public RedisConfig(@Value("${spring.redis.host}") final String redisHost,
@Value("${spring.redis.port}") final int redisPort,
@Value("${spring.redis.password}") final String redisPassword) {
this.redisHost = redisHost;
this.redisPort = redisPort;
this.redisPassword = redisPassword;
}

@Bean
public RedisConnectionFactory redisConnectionFactory() {
final var redisConfiguration = new RedisStandaloneConfiguration(redisHost, redisPort);
redisConfiguration.setPassword(redisPassword);
return new LettuceConnectionFactory(redisConfiguration);
}

@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.woowacourse.kkogkkog.coupon.domain.UnregisteredCouponEventType;
import com.woowacourse.kkogkkog.coupon.domain.repository.CouponHistoryRepository;
import com.woowacourse.kkogkkog.coupon.domain.repository.CouponRepository;
import com.woowacourse.kkogkkog.coupon.domain.repository.NoticeCacheRepository;
import com.woowacourse.kkogkkog.coupon.domain.repository.UnregisteredCouponRepository;
import com.woowacourse.kkogkkog.coupon.exception.CouponNotAccessibleException;
import com.woowacourse.kkogkkog.coupon.exception.UnregisteredCouponNotFoundException;
Expand All @@ -39,6 +40,7 @@ public class CouponService {
private final CouponRepository couponRepository;
private final UnregisteredCouponRepository unregisteredCouponRepository;
private final CouponHistoryRepository couponHistoryRepository;
private final NoticeCacheRepository noticeCacheRepository;
private final PushAlarmPublisher pushAlarmPublisher;

@Transactional(readOnly = true)
Expand Down Expand Up @@ -137,6 +139,7 @@ private List<Member> findReceivers(List<Long> memberIds) {

private void saveCouponHistory(CouponHistory couponHistory) {
couponHistory = couponHistoryRepository.save(couponHistory);
noticeCacheRepository.increment(couponHistory.getHostMember());
pushAlarmPublisher.publishEvent(couponHistory);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,16 @@ public static CouponHistory ofNew(Coupon coupon) {
return new CouponHistory(historyHostMember, coupon.getSender(), coupon, initEvent, null);
}

public static CouponHistory of(Member loginMember, Coupon coupon, CouponEvent event, String message) {
public static CouponHistory of(Member loginMember, Coupon coupon, CouponEvent event,
String message) {
Member historyHostMember = coupon.getOppositeMember(loginMember);
return new CouponHistory(historyHostMember, loginMember, coupon, event, message);
}

public boolean isRead() {
return isRead;
}

public void updateIsRead() {
isRead = true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.woowacourse.kkogkkog.coupon.domain.repository;

import com.woowacourse.kkogkkog.member.domain.Member;
import java.util.concurrent.TimeUnit;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public class NoticeCacheRepository {

private static final String KEY_FORMAT = "unreadNoticeCount:%d";
private static final int CACHE_VALIDITY = 600;

private final RedisTemplate<String, Long> redisTemplate;
private final CouponHistoryRepository couponHistoryRepository;

public NoticeCacheRepository(RedisTemplate<String, Long> redisTemplate,
CouponHistoryRepository couponHistoryRepository) {
this.redisTemplate = redisTemplate;
this.couponHistoryRepository = couponHistoryRepository;
}

@Transactional(readOnly = true)
public Long get(Member member) {
Long unreadCountCache = getCache(member);
if (unreadCountCache != null) {
return unreadCountCache;
}
Long unreadCount = couponHistoryRepository.countByHostMemberAndIsReadFalse(member);
setCache(member, unreadCount);
return unreadCount;
}

public void increment(Member member) {
Long unreadCount = getCache(member);
if (unreadCount != null) {
setCache(member, unreadCount + 1);
}
}

public void decrement(Member member) {
Long unreadCount = getCache(member);
if (unreadCount != null && unreadCount > 0) {
setCache(member, unreadCount - 1);
}
}

public void reset(Member member) {
setCache(member, 0L);
}

private Long getCache(Member member) {
ValueOperations<String, Long> valueOperations = redisTemplate.opsForValue();
String cacheKey = String.format(KEY_FORMAT, member.getId());
return valueOperations.get(cacheKey);
}

private void setCache(Member member, Long unreadCount) {
ValueOperations<String, Long> valueOperations = redisTemplate.opsForValue();
String cacheKey = String.format(KEY_FORMAT, member.getId());
valueOperations.set(cacheKey, unreadCount, CACHE_VALIDITY, TimeUnit.SECONDS);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.woowacourse.kkogkkog.auth.application.dto.MemberUpdateResponse;
import com.woowacourse.kkogkkog.coupon.domain.CouponHistory;
import com.woowacourse.kkogkkog.coupon.domain.repository.CouponHistoryRepository;
import com.woowacourse.kkogkkog.coupon.domain.repository.NoticeCacheRepository;
import com.woowacourse.kkogkkog.infrastructure.dto.GoogleUserDto;
import com.woowacourse.kkogkkog.infrastructure.dto.SlackUserInfo;
import com.woowacourse.kkogkkog.member.application.dto.MemberHistoryResponse;
Expand Down Expand Up @@ -33,6 +34,7 @@ public class MemberService {
private final MemberRepository memberRepository;
private final WorkspaceUserRepository workspaceUserRepository;
private final CouponHistoryRepository memberHistoryRepository;
private final NoticeCacheRepository noticeCacheRepository;

public boolean existsMember(SlackUserInfo userInfo) {
String email = userInfo.getEmail();
Expand Down Expand Up @@ -99,11 +101,10 @@ private MemberUpdateResponse integrateNewWorkspaceUser(SlackUserInfo userInfo,

@Transactional(readOnly = true)
public MyProfileResponse findById(Long memberId) {
Member findMember = memberRepository.get(memberId);
Long unreadHistoryCount = memberHistoryRepository
.countByHostMemberAndIsReadFalse(findMember);
Member member = memberRepository.get(memberId);
Long unreadCount = noticeCacheRepository.get(member);

return MyProfileResponse.of(findMember, unreadHistoryCount);
return MyProfileResponse.of(member, unreadCount);
}

@Transactional(readOnly = true)
Expand Down Expand Up @@ -142,7 +143,10 @@ public List<MemberHistoryResponse> findHistoryById(Long memberId) {

public void updateIsReadMemberHistory(Long memberHistoryId) {
CouponHistory memberHistory = memberHistoryRepository.findCouponHistory(memberHistoryId);
memberHistory.updateIsRead();
if (!memberHistory.isRead()) {
memberHistory.updateIsRead();
noticeCacheRepository.decrement(memberHistory.getHostMember());
}
}

public void updateAllIsReadMemberHistories(Long memberId) {
Expand All @@ -152,5 +156,6 @@ public void updateAllIsReadMemberHistories(Long memberId) {
for (CouponHistory couponHistory : couponHistories) {
couponHistory.updateIsRead();
}
noticeCacheRepository.reset(foundMember);
}
}
4 changes: 4 additions & 0 deletions backend/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ spring:
password:
flyway:
enabled: false
redis:
host: localhost
port: 6379
password: ""

logging:
level:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.woowacourse.kkogkkog.infrastructure.application.SlackClient;
import com.woowacourse.kkogkkog.support.common.DatabaseCleaner;
import com.woowacourse.kkogkkog.support.common.RedisStorageCleaner;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -19,12 +20,16 @@ public class AcceptanceTest {
@Autowired
private DatabaseCleaner databaseCleaner;

@Autowired
private RedisStorageCleaner redisStorageCleaner;

@MockBean
protected static SlackClient slackClient;

@BeforeEach
void setUp() {
RestAssured.port = port;
databaseCleaner.execute();
redisStorageCleaner.execute();
}
}
Loading