Skip to content

Commit

Permalink
refactor: redis를 활용한 조회 성능 최적화 (#480)
Browse files Browse the repository at this point in the history
* chore: redis 설정 추가

* chore: redis 관련 의존성 추가

* feat(CoLeaderReader): 캐싱을 위한 CoLeaderReader 레이어 구현

* refactor(CoLeaderRepository): CoLeader user 관련 데이터 페치조인 구현

* refactor(ControllerExceptionAdvice): 서버 에러 트레이스를 위해 트레이스 스택 출력하도록 개선

* chore(ExecutionLoggingAop): redis 관련 세팅 로깅 aop 에서 제외

* chore(UserActivityVO): redis에 저장되어 있는 객체 역직렬화 하기 위한 코드 수정

* chore(ImageUrlVO): redis에 저장되어 있는 객체 역직렬화 하기 위한 코드 수정

* chore(Meeting):
1. mStartDate, mEndDate 역직렬화 문제를 위해 getter 선언
2. 기존에 사용되었던 getter 수정

* chore(BaseTimeEntity): LocalDateTime 직렬화 및 역직렬화 문제로 인해 코드 추가

* chore(Meeting): LocalDateTime 직렬화 및 역직렬화 문제로 인해 코드 추가

* feat(UserReader): 캐싱을 위한 UserReader 레이어 구현

* feat(MeetingReader): 캐싱을 위한 MeetingReader 레이어 구현

* chore: LazyLoading 객체 저장 오류 해결

* add(RedisConfig): redis 관련 설정 추가

1. 직렬화, 역직렬화 관련된 설정 추가
2. 캐시 TTL 5시간 설정

* refactor(MeetingV2ServiceImpl): 캐시를 활용환 성능 최적회

1. 모임 조회 : 모임, 모임장, 공동모임장 관련 데이터 캐싱
2. 모임 수정 : 1번에서 캐싱한 데이터 초기화

* test(yml): redis 설정 추가

* chore(User, Meeting): lazyLoading 객체 redis에 저장하지 않도록 구현

* chore(RedisConfig): host와 port 명시적으로 설정

* feat(AbstractContainerBaseTest): redis 테스트 컨테이너 추가

* fix(AbstractContainerBaseTest): Property 수정

* chore(yml): dev redis 경로 변경

* chore(cd-dev): 배포 테스트

* chore(cd-dev): 배포 테스트

* chore(cd-dev): 배포 테스트

* chore(cd-dev): 배포 테스트

* chore(UserReader): 레디스에 캐싱하는 데이터를 User 에서 MeetingCreatorDto 로 변경

* rename(redisContainerBaseTest): redisContainerBaseTest 으로 파일 이름 변경

* chore(redisContainerBaseTest): 싱글톤으로 수정

* chore: redis 추가

* chore: redis localhost로 변경

* chore: readonly 옵션 추가

* del(User): 주석삭제

* chore(UserReader): readonly 옵션 추가

* chore(RedisConfig): 디폴트 ttl 설정, 이후에 다른 ttl 주기의 캐시가 필요하면 추가할 수 있도록 구현

* chore(ImageUrlVO): 필드 final 로 변경 및 직렬화 설정

* chore(Meeting): 기존 상태로 원상복구

* feat(MeetingReader): Meeting 반환이 아닌 MeetingRedisDto 를 사용으로 수정

* 🚨feat(CoLeaderReader): CoLeaders 가 아닌 CoLeadersRedisDto 반환으로 수정

1. User 생성자에 불가피하게 id 추가
2. CoLeader 에 Meeting 필드를 불가피하게 null 로 저장

* chore(RedisConfig): objectMapper 커스텀화하여 공통적으로 사용할 수 있게 구현

* chore(MeetingV2ServiceImpl): 로직 변경으로 인한 수정

* chore(MeetingV2GetMeetingByIdResponseDto): Meeting 생성자 변경없이 하기 위해 모임 id 반환 방식을 변경

* chore: redis 커넥션 풀 설정

* chore: redis 관련 기능들만 커스텀 objectMapper 사용하도록 수정

* chore(UserActivityVO): UserActivityVO final 추가

* chore(RedisProperties): RedisProperties 를 따로 정의하여 사용

* fix: 잘못된 코드 원상복구

* chore(CoLeaderReader): readonly 추가

* chore(ImageUrlVO): static -> 인스턴스 메서드로 수정

* chore(cd): cd 트리거 수정

* chore(UserActivityVO): 검증 로직 추가

* chore(CoLeadersRedisDto): 기본 생성자 추가

* fix: 기존 코드 복구

* feat(AuthV2ServiceImpl): 회원 정보 수정된 경우에 캐시 초기화

* feat(User): updateIfChanged 메서드 구현, withUserIdForRedis 메서드 구현

* chore(yml): 커넥션 풀 설정 제거

* chore(CoLeaderRedisDto): User 생성 로직 수정

* chore(CoLeadersRedisDto): 기본 해시맵 생성으로 변경

* test: 파트 및 기수가 잘못됐을 경우 예외 발생

* fix: NPE 문제 해결

* chore: 검증로직 private 메서드화
  • Loading branch information
mikekks authored Dec 15, 2024
1 parent 2398c9b commit 2e4f7f3
Show file tree
Hide file tree
Showing 39 changed files with 899 additions and 242 deletions.
78 changes: 67 additions & 11 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ services:
caddy.log.output: stdout
caddy.log.format: json
caddy.log.include: http.log.access.localhost
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "10"

swagger:
image: swaggerapi/swagger-ui
Expand All @@ -42,9 +47,23 @@ services:
labels:
caddy.route: /docs*
caddy.route.reverse_proxy: "{{ upstreams 8080 }}"
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "10"

redis:
image: redis:alpine
container_name: redis
hostname: redis
ports:
- 6379:6379
networks:
- caddy

nestjs-green:
image: makerscrew/server:latest
image: makerscrew/server:develop
container_name: nestjs-green
ports:
- 3001:3000
Expand Down Expand Up @@ -107,17 +126,28 @@ services:
caddy.route_14.reverse_proxy: "{{ upstreams 3000 }}"

spring-green:
image: makerscrew/main:latest
image: makerscrew/main:develop
environment:
- TZ=Asia/Seoul
TZ: Asia/Seoul
container_name: spring-green
ports:
- 4001:4000
- 5556:5555
restart: unless-stopped
depends_on:
- nestjs-green
- pinpoint-agent
- redis
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "100"
networks:
- caddy
volumes:
- data-volume:/pinpoint-agent

labels:
caddy.log: "localhost"
# for Swagger spec
Expand Down Expand Up @@ -152,9 +182,17 @@ services:
caddy.route_13.reverse_proxy: "{{ upstreams 4000 }}"
caddy.route_14: /auth/v2/*
caddy.route_14.reverse_proxy: "{{ upstreams 4000 }}"
caddy.route_15: /8da2d7e6-72aa-4120-9e84-8f459a2584a1/*
caddy.route_15.reverse_proxy: "{{ upstreams 5555 }}"
caddy.route_16: /internal/*
caddy.route_16.reverse_proxy: "{{ upstreams 4000 }}"

nestjs-blue:
image: makerscrew/server:latest
image: makerscrew/server:develop
container_name: nestjs-blue
ports:
- 3002:3000
restart: unless-stopped
env_file:
- ./.env
environment:
Expand All @@ -170,10 +208,11 @@ services:
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- AWS_REGION=${AWS_REGION}
- JWT_SECRET=${JWT_SECRET}
container_name: nestjs-blue
ports:
- 3002:3000
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "10"
networks:
- caddy
labels:
Expand Down Expand Up @@ -212,17 +251,27 @@ services:
caddy.route_14.reverse_proxy: "{{ upstreams 3000 }}"

spring-blue:
image: makerscrew/main:latest
image: makerscrew/main:develop
environment:
- TZ=Asia/Seoul
TZ: Asia/Seoul
container_name: spring-blue
ports:
- 4002:4000
- 5557:5555
restart: unless-stopped
depends_on:
- nestjs-blue
- pinpoint-agent
- redis
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "100"
networks:
- caddy
volumes:
- data-volume:/pinpoint-agent
labels:
caddy.log: "localhost"
# for Swagger spec
Expand Down Expand Up @@ -257,7 +306,14 @@ services:
caddy.route_13.reverse_proxy: "{{ upstreams 4000 }}"
caddy.route_14: /auth/v2/*
caddy.route_14.reverse_proxy: "{{ upstreams 4000 }}"
caddy.route_15: /8da2d7e6-72aa-4120-9e84-8f459a2584a1/*
caddy.route_15.reverse_proxy: "{{ upstreams 5555 }}"
caddy.route_16: /internal/*
caddy.route_16.reverse_proxy: "{{ upstreams 4000 }}"

volumes:
data-volume:

networks:
caddy:
external: true
external: true
7 changes: 7 additions & 0 deletions main/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ dependencies {

// Slack Webhook
implementation 'com.github.maricn:logback-slack-appender:1.4.0'

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate6'

}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
package org.sopt.makers.crew.main.auth.v2.service;

import java.util.Optional;

import org.sopt.makers.crew.main.auth.v2.dto.request.AuthV2RequestDto;
import org.sopt.makers.crew.main.auth.v2.dto.response.AuthV2ResponseDto;
import org.sopt.makers.crew.main.entity.meeting.CoLeaderRepository;
import org.sopt.makers.crew.main.global.jwt.JwtTokenProvider;
import org.sopt.makers.crew.main.entity.user.User;
import org.sopt.makers.crew.main.entity.user.UserRepository;
import org.sopt.makers.crew.main.external.playground.PlaygroundService;
import org.sopt.makers.crew.main.external.playground.dto.request.PlaygroundUserRequestDto;
import org.sopt.makers.crew.main.external.playground.dto.response.PlaygroundUserResponseDto;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -23,42 +24,69 @@
public class AuthV2ServiceImpl implements AuthV2Service {

private final UserRepository userRepository;
private final CoLeaderRepository coLeaderRepository;

private final PlaygroundService playgroundService;
private final JwtTokenProvider jwtTokenProvider;

@Override
@Transactional
public AuthV2ResponseDto loginUser(AuthV2RequestDto requestDto) {
PlaygroundUserResponseDto responseDto = fetchPlaygroundUser(requestDto);
User curUser = userRepository.findByOrgId(responseDto.getId())
.orElseGet(() -> signUpNewUser(responseDto));

// 플그 서버로의 요청
PlaygroundUserResponseDto responseDto = playgroundService.getUser(PlaygroundUserRequestDto.of(requestDto.authToken()));
Optional<User> user = userRepository.findByOrgId(responseDto.getId());

/**
* @note: 회원가입 경우
*
* */
if (user.isEmpty()) {
User newUser = responseDto.toEntity();
userRepository.save(newUser);

log.info("new user signup : {} {}", newUser.getId(), newUser.getName());
String accessToken = jwtTokenProvider.generateAccessToken(newUser.getId(), newUser.getName());
return AuthV2ResponseDto.of(accessToken);
if (updateUserIfChanged(curUser, responseDto)) {
clearCacheForUser(curUser.getId());
}

/**
* @note: 로그인 경우 : 기존 정보에서 변화있는 부분은 업데이트 한다.
*
* */
User curUser = user.get();
curUser.updateUser(responseDto.getName(), responseDto.getId(), responseDto.getUserActivities(),
responseDto.getProfileImage(), responseDto.getPhone());

String accessToken = jwtTokenProvider.generateAccessToken(curUser.getId(), curUser.getName());
log.info("accessToken : {}", accessToken);

log.info("Access token generated for user {}: {}", curUser.getId(), accessToken);
return AuthV2ResponseDto.of(accessToken);
}

private PlaygroundUserResponseDto fetchPlaygroundUser(AuthV2RequestDto requestDto) {
return playgroundService.getUser(PlaygroundUserRequestDto.of(requestDto.authToken()));
}

private User signUpNewUser(PlaygroundUserResponseDto responseDto) {
User newUser = responseDto.toEntity();
User savedUser = userRepository.save(newUser);
log.info("New user signup: {} {}", savedUser.getId(), savedUser.getName());
return savedUser;
}

private boolean updateUserIfChanged(User curUser, PlaygroundUserResponseDto responseDto) {
User playgroundUser = responseDto.toEntity();
boolean isUpdated = curUser.updateIfChanged(playgroundUser);

if (isUpdated) {
log.info("User updated: {}", curUser.getId());
}

return isUpdated;
}

private void clearCacheForUser(Integer userId) {
clearCacheForLeader(userId);

coLeaderRepository.findAllByUserIdWithMeeting(userId).forEach(
coLeader -> clearCacheForCoLeader(coLeader.getMeeting().getId())
);
log.info("Cache cleared for user: {}", userId);
}

@Caching(evict = {
@CacheEvict(value = "meetingLeaderCache", key = "#userId")
})
public void clearCacheForLeader(Integer userId) {

}

@Caching(evict = {
@CacheEvict(value = "coLeadersCache", key = "#meetingId")
})
public void clearCacheForCoLeader(Integer meetingId) {

}
}
Original file line number Diff line number Diff line change
@@ -1,41 +1,38 @@
package org.sopt.makers.crew.main.entity.apply;

import static jakarta.persistence.GenerationType.IDENTITY;
import static jakarta.persistence.GenerationType.*;
import static org.sopt.makers.crew.main.global.exception.ErrorStatus.*;

import java.time.LocalDateTime;

import org.sopt.makers.crew.main.entity.apply.enums.ApplyStatusConverter;
import org.sopt.makers.crew.main.entity.apply.enums.ApplyTypeConverter;
import org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus;
import org.sopt.makers.crew.main.entity.apply.enums.EnApplyType;
import org.sopt.makers.crew.main.entity.common.BaseTimeEntity;
import org.sopt.makers.crew.main.entity.meeting.Meeting;
import org.sopt.makers.crew.main.entity.user.User;
import org.sopt.makers.crew.main.global.exception.BadRequestException;
import org.springframework.data.annotation.CreatedDate;

import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;

import java.time.LocalDateTime;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import org.sopt.makers.crew.main.entity.common.BaseTimeEntity;
import org.sopt.makers.crew.main.global.exception.BadRequestException;
import org.sopt.makers.crew.main.entity.apply.enums.ApplyStatusConverter;
import org.sopt.makers.crew.main.entity.apply.enums.ApplyTypeConverter;
import org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus;
import org.sopt.makers.crew.main.entity.apply.enums.EnApplyType;
import org.sopt.makers.crew.main.entity.meeting.Meeting;
import org.sopt.makers.crew.main.entity.user.User;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "apply")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Apply extends BaseTimeEntity {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
Expand All @@ -16,10 +21,14 @@ public abstract class BaseTimeEntity {

@CreatedDate
@Column(name = "createdTimestamp")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
public LocalDateTime createdTimestamp;

@LastModifiedDate
@Column(name = "modifiedTimestamp")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
public LocalDateTime modifiedTimestamp;

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public class CoLeader extends BaseTimeEntity {

@Builder
private CoLeader(Meeting meeting, User user) {
if (Objects.equals(meeting.getUserId(), user.getId())) {
if (meeting != null && Objects.equals(meeting.getUserId(), user.getId())) {
throw new BadRequestException(LEADER_CANNOT_BE_CO_LEADER_APPLY.getErrorCode());
}
this.meeting = meeting;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.sopt.makers.crew.main.entity.meeting;

import java.util.List;

import org.sopt.makers.crew.main.meeting.v2.dto.redis.CoLeadersRedisDto;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CoLeaderReader {
private final CoLeaderRepository coLeaderRepository;

@Cacheable(value = "coLeadersCache", key = "#meetingId")
public CoLeadersRedisDto getCoLeaders(Integer meetingId) {
List<CoLeader> coLeaders = coLeaderRepository.findAllByMeetingId(meetingId);

return new CoLeadersRedisDto(coLeaders);
}
}
Loading

0 comments on commit 2e4f7f3

Please sign in to comment.