Skip to content

Commit

Permalink
[Release] 동시성 문제 해결 및 캐시를 적용하여 성능 개선을 한다. (#203)
Browse files Browse the repository at this point in the history
* feat: 골룸 노드 목록 조회 시 노드 설명과 이미지 반환하도록 수정

* [feat/CK-227] 로드맵 골룸 조회 시 최신순일때는 모든 상태를, 마감임박 순일 땐 모집 중인 상태만 반환한다 (#185)

* chore: 패키지 경로 수정

* feat: 조건에 따른 로드맵 골룸 조회 시 쿼리 수정

* refactor: 1:N 문제 방지를 위해 @batchsize 설정

* feat: 로드맵 골룸 응답에 골룸 상태 추가

* chore: 서브모듈 업데이트

* chore: 패키지 경로 수정

* refactor: 메서드 네이밍 수정

* refactor: BaseEntity의 CreatedAt 스프링 의존성 끊기 (#191)

* refactor: MemberIdentifierArgumentResolver에서 Authenticated 어노테이션 확인하도록 수정 (#193)

* [feat/CK-232] 전역적으로 사용하는 Exception을 분리하고 AOP로 예외를 변환하는 기능을 구현한다. (#194)

* refactor: exception 패키지를 service 패키지 하위로 이동

* feat: 도메인 Exception을 서비스 Exception으로 변환해주는 AOP 구현

* [feat/CK-222] Redis를 도입하고 Refresh Token을 Redis에 저장하도록 변경한다 (#190)

* build: spring data redis 및 testcontainers 의존성 추가

* feat: RefreshToken 레디스에 저장하도록 변경

* test: Redis 테스트 설정 및 RefreshTokenRedisRepository 테스트

* chore: 서브모듈 업데이트

* refactor: JwtTokenProvider에서 RefreshToken 생성해서 반환하도록 수정

* refactor: 리뷰 반영

* refactor: RedisTemplate으로 변경

* test: JwtTokenProvider 테스트 추가

* test: test config에 redis 설정 추가

* test: test 추가

* chore: 기존 refresh token table을 drop하는 쿼리 추가

* refactor: ttl을 초단위로 변경하는 로직 변수 추출

* refactor: 변수명 변경

* refactor: 레디스 테스트 간 격리

* chore: git submodule 업데이트

* [feat/CK-235] Redis 캐시를 적용한다 (#197)

* test: test container에 의존하지 않도록 변경

* feat: redis cache 적용

* feat: redis config 설정 수정

* fix: RedisConfig를 test에서 비활성화 시킨다

* test: CacheKeyGenerator test code 작성

* feat: 수정 요구사항 반영

* chore: 서브모듈 최신화

* chore: 서브모듈 최신화

* chore: flyway v4 파일 제거

* chore: flyway v4 파일 복구

* [feat/CK-237] 골룸 참여 시 발생하는 동시성 이슈를 해결한다 (#199)

* refactor: 골룸 참여 시 발생하는 동시성 이슈를 비관적 락으로 해결

* test: 테스트 코드 수정

* [feat/CK-236] JdbcTemplate을 이용하여 bulk insert를 적용한다 (#198)

* refactor: 기존 saveAll, deleteAll을 bulk insert로 개선

* refactor: Dao 대신 Repository 계층에 의존하도록 수정 - JdbcRepository 추상화

* [feat/CK-239] Amazon S3 접근 시 credential 정보를 이용하도록 수정한다 (#201)

* feat: amazon s3 접근 시 credentials 사용하도록 수정

* test: test application.yml에 credentials 추가

---------

Co-authored-by: Miseong Kim <[email protected]>
Co-authored-by: Ohjintaek <[email protected]>
Co-authored-by: Miseong Kim <[email protected]>
Co-authored-by: Ohjintaek <[email protected]>
  • Loading branch information
5 people authored Dec 3, 2023
1 parent 7088f17 commit c73aa21
Show file tree
Hide file tree
Showing 148 changed files with 1,094 additions and 807 deletions.
3 changes: 3 additions & 0 deletions backend/kirikiri/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ dependencies {
// flyway
implementation 'org.flywaydb:flyway-mysql'

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// test
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package co.kirikiri.common.config;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
Expand All @@ -11,15 +13,23 @@
public class AWSConfig {

private final Regions region;
private final String accessKey;
private final String secretKey;

public AWSConfig(@Value("${cloud.aws.region.static}") final String region) {
public AWSConfig(@Value("${cloud.aws.region.static}") final String region,
@Value("${cloud.aws.credentials.access-key}") final String accessKey,
@Value("${cloud.aws.credentials.secret-key}") final String secretKey) {
this.region = Regions.fromName(region);
this.accessKey = accessKey;
this.secretKey = secretKey;
}

@Bean
public AmazonS3 amazonS3() {
final BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
return AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package co.kirikiri.common.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.time.Duration;
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.context.annotation.Profile;
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.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableCaching
@Profile(value = {"prod", "dev", "local"})
public class RedisConfig {

@Bean
public RedisTemplate<String, String> redisTemplate(final RedisConnectionFactory redisConnectionFactory) {
final RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}

@Bean
public CacheManager redisCacheManager(final RedisConnectionFactory redisConnectionFactory) {
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.EVERYTHING);

final RedisSerializer<Object> serializer = new GenericJackson2JsonRedisSerializer(objectMapper);

final RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30L))
.disableCachingNullValues()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer));

return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package co.kirikiri.common.interceptor;

import co.kirikiri.exception.AuthenticationException;
import co.kirikiri.service.auth.AuthService;
import co.kirikiri.service.exception.AuthenticationException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package co.kirikiri.common.resolver;

import co.kirikiri.exception.AuthenticationException;
import co.kirikiri.common.interceptor.Authenticated;
import co.kirikiri.service.auth.AuthService;
import co.kirikiri.service.exception.AuthenticationException;
import co.kirikiri.service.exception.ServerException;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
Expand All @@ -21,6 +23,9 @@ public class MemberIdentifierArgumentResolver implements HandlerMethodArgumentRe

@Override
public boolean supportsParameter(final MethodParameter parameter) {
if (!parameter.hasMethodAnnotation(Authenticated.class)) {
throw new ServerException("MemberIdentifier는 인증된 사용자만 사용 가능합니다. (@Authenticated)");
}
return parameter.getParameterType().equals(String.class)
&& parameter.hasParameterAnnotation(MemberIdentifier.class);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package co.kirikiri.common.resolver;

import co.kirikiri.exception.BadRequestException;
import co.kirikiri.service.dto.roadmap.request.RoadmapNodeSaveRequest;
import co.kirikiri.service.dto.roadmap.request.RoadmapSaveRequest;
import co.kirikiri.service.exception.BadRequestException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
Expand All @@ -20,7 +21,6 @@
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import java.util.List;

@Component
@RequiredArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
package co.kirikiri.controller;

import co.kirikiri.exception.AuthenticationException;
import co.kirikiri.exception.BadRequestException;
import co.kirikiri.exception.ConflictException;
import co.kirikiri.exception.ForbiddenException;
import co.kirikiri.exception.NotFoundException;
import co.kirikiri.exception.ServerException;
import co.kirikiri.service.dto.ErrorResponse;
import co.kirikiri.service.exception.AuthenticationException;
import co.kirikiri.service.exception.BadRequestException;
import co.kirikiri.service.exception.ConflictException;
import co.kirikiri.service.exception.ForbiddenException;
import co.kirikiri.service.exception.NotFoundException;
import co.kirikiri.service.exception.ServerException;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;

@RestControllerAdvice
public class GlobalExceptionHandler {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import co.kirikiri.common.interceptor.Authenticated;
import co.kirikiri.common.resolver.MemberIdentifier;
import co.kirikiri.service.goalroom.GoalRoomCreateService;
import co.kirikiri.service.goalroom.GoalRoomReadService;
import co.kirikiri.service.dto.goalroom.GoalRoomMemberSortTypeDto;
import co.kirikiri.service.dto.goalroom.request.CheckFeedRequest;
import co.kirikiri.service.dto.goalroom.request.GoalRoomCreateRequest;
Expand All @@ -18,6 +16,8 @@
import co.kirikiri.service.dto.goalroom.response.GoalRoomTodoResponse;
import co.kirikiri.service.dto.member.response.MemberGoalRoomForListResponse;
import co.kirikiri.service.dto.member.response.MemberGoalRoomResponse;
import co.kirikiri.service.goalroom.GoalRoomCreateService;
import co.kirikiri.service.goalroom.GoalRoomReadService;
import jakarta.validation.Valid;
import java.net.URI;
import java.util.List;
Expand Down Expand Up @@ -50,25 +50,25 @@ public ResponseEntity<Void> create(@RequestBody @Valid final GoalRoomCreateReque
return ResponseEntity.created(URI.create("/api/goal-rooms/" + id)).build();
}

@Authenticated
@PostMapping("/{goalRoomId}/join")
@Authenticated
public ResponseEntity<Void> joinGoalRoom(@MemberIdentifier final String identifier,
@PathVariable final Long goalRoomId) {
goalRoomCreateService.join(identifier, goalRoomId);
return ResponseEntity.status(HttpStatus.OK).build();
}

@Authenticated
@PostMapping("/{goalRoomId}/todos")
@Authenticated
public ResponseEntity<Void> addTodo(@RequestBody @Valid final GoalRoomTodoRequest goalRoomTodoRequest,
@PathVariable final Long goalRoomId,
@MemberIdentifier final String identifier) {
final Long id = goalRoomCreateService.addGoalRoomTodo(goalRoomId, identifier, goalRoomTodoRequest);
return ResponseEntity.created(URI.create("/api/goal-rooms/" + goalRoomId + "/todos/" + id)).build();
}

@Authenticated
@PostMapping("/{goalRoomId}/todos/{todoId}")
@Authenticated
public ResponseEntity<GoalRoomToDoCheckResponse> checkTodo(@PathVariable final Long goalRoomId,
@PathVariable final Long todoId,
@MemberIdentifier final String identifier) {
Expand All @@ -77,8 +77,8 @@ public ResponseEntity<GoalRoomToDoCheckResponse> checkTodo(@PathVariable final L
return ResponseEntity.ok(checkResponse);
}

@Authenticated
@PostMapping(value = "/{goalRoomId}/checkFeeds", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
@Authenticated
public ResponseEntity<Void> createCheckFeed(@MemberIdentifier final String identifier,
@PathVariable("goalRoomId") final Long goalRoomId,
@ModelAttribute final CheckFeedRequest checkFeedRequest) {
Expand Down Expand Up @@ -116,8 +116,8 @@ public ResponseEntity<GoalRoomResponse> findGoalRoom(@PathVariable("goalRoomId")
return ResponseEntity.ok(goalRoomResponse);
}

@Authenticated
@GetMapping("/{goalRoomId}/members")
@Authenticated
public ResponseEntity<List<GoalRoomMemberResponse>> findGoalRoomMembers(
@PathVariable final Long goalRoomId,
@RequestParam(value = "sortCond", required = false) final GoalRoomMemberSortTypeDto sortType) {
Expand All @@ -126,17 +126,17 @@ public ResponseEntity<List<GoalRoomMemberResponse>> findGoalRoomMembers(
return ResponseEntity.ok(goalRoomMembers);
}

@Authenticated
@GetMapping("/{goalRoomId}/me")
@Authenticated
public ResponseEntity<MemberGoalRoomResponse> findMemberGoalRoom(
@MemberIdentifier final String identifier, @PathVariable final Long goalRoomId) {
final MemberGoalRoomResponse memberGoalRoomResponse = goalRoomReadService.findMemberGoalRoom(identifier,
goalRoomId);
return ResponseEntity.ok(memberGoalRoomResponse);
}

@Authenticated
@GetMapping("/me")
@Authenticated
public ResponseEntity<List<MemberGoalRoomForListResponse>> findMemberGoalRoomsByStatus(
@MemberIdentifier final String identifier,
@RequestParam(value = "statusCond", required = false) final GoalRoomStatusTypeRequest goalRoomStatusTypeRequest) {
Expand All @@ -150,8 +150,8 @@ public ResponseEntity<List<MemberGoalRoomForListResponse>> findMemberGoalRoomsBy
return ResponseEntity.ok(memberGoalRoomForListResponses);
}

@Authenticated
@GetMapping("/{goalRoomId}/todos")
@Authenticated
public ResponseEntity<List<GoalRoomTodoResponse>> findAllTodos(
@PathVariable final Long goalRoomId,
@MemberIdentifier final String identifier) {
Expand All @@ -160,8 +160,8 @@ public ResponseEntity<List<GoalRoomTodoResponse>> findAllTodos(
return ResponseEntity.ok(todoResponses);
}

@Authenticated
@GetMapping("/{goalRoomId}/nodes")
@Authenticated
public ResponseEntity<List<GoalRoomRoadmapNodeDetailResponse>> findAllNodes(
@PathVariable final Long goalRoomId,
@MemberIdentifier final String identifier
Expand All @@ -171,8 +171,8 @@ public ResponseEntity<List<GoalRoomRoadmapNodeDetailResponse>> findAllNodes(
return ResponseEntity.ok(nodeResponses);
}

@Authenticated
@GetMapping("/{goalRoomId}/checkFeeds")
@Authenticated
public ResponseEntity<List<GoalRoomCheckFeedResponse>> findGoalRoomCheckFeeds(
@MemberIdentifier final String identifier,
@PathVariable("goalRoomId") final Long goalRoomId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import co.kirikiri.common.interceptor.Authenticated;
import co.kirikiri.common.resolver.MemberIdentifier;
import co.kirikiri.service.roadmap.RoadmapCreateService;
import co.kirikiri.service.roadmap.RoadmapReadService;
import co.kirikiri.service.dto.CustomScrollRequest;
import co.kirikiri.service.dto.roadmap.RoadmapGoalRoomsOrderTypeDto;
import co.kirikiri.service.dto.roadmap.request.RoadmapCategorySaveRequest;
Expand All @@ -17,7 +15,11 @@
import co.kirikiri.service.dto.roadmap.response.RoadmapGoalRoomResponses;
import co.kirikiri.service.dto.roadmap.response.RoadmapResponse;
import co.kirikiri.service.dto.roadmap.response.RoadmapReviewResponse;
import co.kirikiri.service.roadmap.RoadmapCreateService;
import co.kirikiri.service.roadmap.RoadmapReadService;
import jakarta.validation.Valid;
import java.net.URI;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
Expand All @@ -31,8 +33,6 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.net.URI;
import java.util.List;

@RestController
@RequestMapping("/roadmaps")
Expand All @@ -42,14 +42,15 @@ public class RoadmapController {
private final RoadmapCreateService roadmapCreateService;
private final RoadmapReadService roadmapReadService;

@Authenticated
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Authenticated
public ResponseEntity<Void> create(final RoadmapSaveRequest request, @MemberIdentifier final String identifier) {
final Long roadmapId = roadmapCreateService.create(request, identifier);
return ResponseEntity.created(URI.create("/api/roadmaps/" + roadmapId)).build();
}

@PostMapping("/{roadmapId}/reviews")
@Authenticated
public ResponseEntity<Void> createReview(
@PathVariable("roadmapId") final Long roadmapId,
@MemberIdentifier final String identifier,
Expand Down Expand Up @@ -93,7 +94,8 @@ public ResponseEntity<List<RoadmapCategoryResponse>> findAllRoadmapCategories()
}

@PostMapping("/categories")
public ResponseEntity<Void> createRoadmapCategory(@RequestBody @Valid final RoadmapCategorySaveRequest roadmapCategorySaveRequest) {
public ResponseEntity<Void> createRoadmapCategory(
@RequestBody @Valid final RoadmapCategorySaveRequest roadmapCategorySaveRequest) {
roadmapCreateService.createRoadmapCategory(roadmapCategorySaveRequest);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
package co.kirikiri.domain;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PrePersist;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseCreatedTimeEntity extends BaseEntity {

private static final String TIME_FORMAT = "yyyy-MM-dd HH:mm:ss.SSSSSS";
protected static final String TIME_FORMAT = "yyyy-MM-dd HH:mm:ss.SSSSSS";

@CreatedDate
@Column(nullable = false, updatable = false)
protected LocalDateTime createdAt;

@PrePersist
public void prePersist() {
protected void prePersist() {
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(TIME_FORMAT);
final String formattedTime = createdAt.format(formatter);
final String formattedTime = LocalDateTime.now().format(formatter);
createdAt = LocalDateTime.parse(formattedTime, formatter);
}

Expand Down
Loading

0 comments on commit c73aa21

Please sign in to comment.