Skip to content

Commit fcd83b2

Browse files
authored
[Refactor] Presigned URL 캐싱
2 parents 7800c55 + a22fd5c commit fcd83b2

File tree

18 files changed

+294
-77
lines changed

18 files changed

+294
-77
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ dependencies {
5757
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
5858
implementation 'org.springframework.boot:spring-boot-starter-web'
5959
implementation 'org.springframework.boot:spring-boot-starter-validation'
60+
implementation 'org.springframework.boot:spring-boot-starter-cache'
6061

6162
// Lombok
6263
compileOnly 'org.projectlombok:lombok'
@@ -67,6 +68,7 @@ dependencies {
6768
runtimeOnly 'com.mysql:mysql-connector-j'
6869
implementation 'org.flywaydb:flyway-core'
6970
implementation 'org.flywaydb:flyway-mysql'
71+
implementation 'com.github.ben-manes.caffeine:caffeine' // in-memory cache
7072

7173
// JWT
7274
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package eatda.config;
2+
3+
import com.github.benmanes.caffeine.cache.Caffeine;
4+
import eatda.repository.CacheSetting;
5+
import java.util.Arrays;
6+
import java.util.List;
7+
import org.springframework.cache.CacheManager;
8+
import org.springframework.cache.caffeine.CaffeineCache;
9+
import org.springframework.cache.support.SimpleCacheManager;
10+
import org.springframework.context.annotation.Bean;
11+
import org.springframework.context.annotation.Configuration;
12+
13+
@Configuration
14+
public class CacheConfig {
15+
16+
@Bean
17+
public CacheManager cacheManager() {
18+
List<CaffeineCache> caches = Arrays.stream(CacheSetting.values())
19+
.map(this::createCaffeineCache)
20+
.toList();
21+
22+
SimpleCacheManager cacheManager = new SimpleCacheManager();
23+
cacheManager.setCaches(caches);
24+
cacheManager.afterPropertiesSet();
25+
return cacheManager;
26+
}
27+
28+
private CaffeineCache createCaffeineCache(CacheSetting cacheSetting) {
29+
return new CaffeineCache(
30+
cacheSetting.getName(),
31+
Caffeine.newBuilder()
32+
.expireAfterWrite(cacheSetting.getTimeToLive())
33+
.maximumSize(cacheSetting.getMaximumSize())
34+
.build());
35+
}
36+
}

src/main/java/eatda/service/common/ImageDomain.java renamed to src/main/java/eatda/domain/ImageDomain.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package eatda.service.common;
1+
package eatda.domain;
22

33
import lombok.Getter;
44
import lombok.RequiredArgsConstructor;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package eatda.repository;
2+
3+
import java.time.Duration;
4+
import lombok.Getter;
5+
6+
@Getter
7+
public enum CacheSetting {
8+
9+
IMAGE("image", Duration.ofMinutes(25), 1_000),
10+
;
11+
12+
private final String name;
13+
private final Duration timeToLive;
14+
private final int maximumSize;
15+
16+
CacheSetting(String image, Duration timeToLive, int maximumSize) {
17+
this.name = image;
18+
this.timeToLive = timeToLive;
19+
this.maximumSize = maximumSize;
20+
}
21+
}

src/main/java/eatda/service/article/ArticleService.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import eatda.controller.article.ArticleResponse;
44
import eatda.controller.article.ArticlesResponse;
55
import eatda.repository.article.ArticleRepository;
6-
import eatda.service.common.ImageService;
6+
import eatda.storage.image.ImageStorage;
77
import java.util.List;
88
import lombok.RequiredArgsConstructor;
99
import org.springframework.data.domain.PageRequest;
@@ -14,7 +14,7 @@
1414
public class ArticleService {
1515

1616
private final ArticleRepository articleRepository;
17-
private final ImageService imageService;
17+
private final ImageStorage imageStorage;
1818

1919
public ArticlesResponse getAllArticles(int size) {
2020
PageRequest pageRequest = PageRequest.of(0, size);
@@ -24,7 +24,7 @@ public ArticlesResponse getAllArticles(int size) {
2424
article.getTitle(),
2525
article.getSubtitle(),
2626
article.getArticleUrl(),
27-
imageService.getPresignedUrl(article.getImageKey())
27+
imageStorage.getPresignedUrl(article.getImageKey())
2828
))
2929
.toList();
3030

src/main/java/eatda/service/store/CheerService.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import eatda.controller.store.CheerRegisterRequest;
77
import eatda.controller.store.CheerResponse;
88
import eatda.controller.store.CheersResponse;
9+
import eatda.domain.ImageDomain;
910
import eatda.domain.member.Member;
1011
import eatda.domain.store.Cheer;
1112
import eatda.domain.store.Store;
@@ -14,8 +15,7 @@
1415
import eatda.repository.member.MemberRepository;
1516
import eatda.repository.store.CheerRepository;
1617
import eatda.repository.store.StoreRepository;
17-
import eatda.service.common.ImageDomain;
18-
import eatda.service.common.ImageService;
18+
import eatda.storage.image.ImageStorage;
1919
import java.util.List;
2020
import lombok.RequiredArgsConstructor;
2121
import org.springframework.data.domain.Pageable;
@@ -34,7 +34,7 @@ public class CheerService {
3434
private final MemberRepository memberRepository;
3535
private final StoreRepository storeRepository;
3636
private final CheerRepository cheerRepository;
37-
private final ImageService imageService;
37+
private final ImageStorage imageStorage;
3838

3939
@Transactional
4040
public CheerResponse registerCheer(CheerRegisterRequest request, MultipartFile image, long memberId) {
@@ -43,12 +43,12 @@ public CheerResponse registerCheer(CheerRegisterRequest request, MultipartFile i
4343

4444
List<StoreSearchResult> searchResults = mapClient.searchShops(request.storeName());
4545
StoreSearchResult result = storeSearchFilter.filterStoreByKakaoId(searchResults, request.storeKakaoId());
46-
String imageKey = imageService.upload(image, ImageDomain.CHEER);
46+
String imageKey = imageStorage.upload(image, ImageDomain.CHEER);
4747

4848
Store store = storeRepository.findByKakaoId(result.kakaoId())
4949
.orElseGet(() -> storeRepository.save(result.toStore())); // TODO 상점 조회/저장 동시성 이슈 해결
5050
Cheer cheer = cheerRepository.save(new Cheer(member, store, request.description(), imageKey));
51-
return new CheerResponse(cheer, imageService.getPresignedUrl(imageKey), store);
51+
return new CheerResponse(cheer, imageStorage.getPresignedUrl(imageKey), store);
5252
}
5353

5454
private void validateRegisterCheer(Member member, String storeKakaoId) {
@@ -69,7 +69,7 @@ public CheersResponse getCheers(int size) {
6969
private CheersResponse toCheersResponse(List<Cheer> cheers) {
7070
return new CheersResponse(cheers.stream()
7171
.map(cheer -> new CheerPreviewResponse(cheer, cheer.getStore(),
72-
imageService.getPresignedUrl(cheer.getImageKey())))
72+
imageStorage.getPresignedUrl(cheer.getImageKey())))
7373
.toList());
7474
}
7575
}

src/main/java/eatda/service/store/StoreService.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import eatda.domain.store.Store;
1212
import eatda.repository.store.CheerRepository;
1313
import eatda.repository.store.StoreRepository;
14-
import eatda.service.common.ImageService;
14+
import eatda.storage.image.ImageStorage;
1515
import java.util.List;
1616
import java.util.Optional;
1717
import lombok.RequiredArgsConstructor;
@@ -26,7 +26,7 @@ public class StoreService {
2626
private final StoreSearchFilter storeSearchFilter;
2727
private final StoreRepository storeRepository;
2828
private final CheerRepository cheerRepository;
29-
private final ImageService imageService;
29+
private final ImageStorage imageStorage;
3030

3131
// TODO : N+1 문제 해결
3232
public StoresResponse getStores(int size) {
@@ -38,7 +38,7 @@ public StoresResponse getStores(int size) {
3838

3939
private Optional<String> getStoreImageUrl(Store store) {
4040
return cheerRepository.findRecentImageKey(store)
41-
.map(imageService::getPresignedUrl);
41+
.map(imageStorage::getPresignedUrl);
4242
}
4343

4444
public StoreSearchResponses searchStores(String query) {

src/main/java/eatda/service/story/StoryService.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
import eatda.controller.story.StoryRegisterRequest;
88
import eatda.controller.story.StoryRegisterResponse;
99
import eatda.controller.story.StoryResponse;
10+
import eatda.domain.ImageDomain;
1011
import eatda.domain.member.Member;
1112
import eatda.domain.story.Story;
1213
import eatda.exception.BusinessErrorCode;
1314
import eatda.exception.BusinessException;
1415
import eatda.repository.member.MemberRepository;
1516
import eatda.repository.story.StoryRepository;
16-
import eatda.service.common.ImageDomain;
17-
import eatda.service.common.ImageService;
17+
import eatda.storage.image.ImageStorage;
1818
import java.util.List;
1919
import lombok.RequiredArgsConstructor;
2020
import org.springframework.data.domain.Page;
@@ -27,10 +27,11 @@
2727
@Service
2828
@RequiredArgsConstructor
2929
public class StoryService {
30+
3031
private static final int PAGE_START_NUMBER = 0;
3132

33+
private final ImageStorage imageStorage;
3234
private final MapClient mapClient;
33-
private final ImageService imageService;
3435
private final StoryRepository storyRepository;
3536
private final MemberRepository memberRepository;
3637

@@ -39,7 +40,7 @@ public StoryRegisterResponse registerStory(StoryRegisterRequest request, Multipa
3940
Member member = memberRepository.getById(memberId);
4041
List<StoreSearchResult> searchResponses = mapClient.searchShops(request.query());
4142
FilteredSearchResult matchedStore = filteredSearchResponse(searchResponses, request.storeKakaoId());
42-
String imageKey = imageService.upload(image, ImageDomain.STORY);
43+
String imageKey = imageStorage.upload(image, ImageDomain.STORY);
4344

4445
Story story = Story.builder()
4546
.member(member)
@@ -80,7 +81,7 @@ public StoriesResponse getPagedStoryPreviews(int size) {
8081
orderByPage.getContent().stream()
8182
.map(story -> new StoriesResponse.StoryPreview(
8283
story.getId(),
83-
imageService.getPresignedUrl(story.getImageKey())
84+
imageStorage.getPresignedUrl(story.getImageKey())
8485
))
8586
.toList()
8687
);
@@ -98,7 +99,7 @@ public StoryResponse getStory(long storyId) {
9899
story.getAddressDistrict(),
99100
story.getAddressNeighborhood(),
100101
story.getDescription(),
101-
imageService.getPresignedUrl(story.getImageKey())
102+
imageStorage.getPresignedUrl(story.getImageKey())
102103
);
103104
}
104105
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package eatda.storage.image;
2+
3+
import eatda.repository.CacheSetting;
4+
import java.util.Optional;
5+
import org.springframework.cache.Cache;
6+
import org.springframework.cache.CacheManager;
7+
import org.springframework.stereotype.Component;
8+
9+
@Component
10+
public class CachePreSignedUrlStorage {
11+
12+
private static final String CACHE_NAME = CacheSetting.IMAGE.getName();
13+
14+
private final Cache cache;
15+
16+
public CachePreSignedUrlStorage(CacheManager cacheManager) {
17+
this.cache = cacheManager.getCache(CACHE_NAME);
18+
}
19+
20+
public void put(String imageKey, String preSignedUrl) {
21+
cache.put(imageKey, preSignedUrl);
22+
}
23+
24+
public Optional<String> get(String imageKey) {
25+
return Optional.ofNullable(cache.get(imageKey, String.class));
26+
}
27+
}

src/main/java/eatda/service/common/ImageService.java renamed to src/main/java/eatda/storage/image/ExternalImageStorage.java

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
package eatda.service.common;
1+
package eatda.storage.image;
22

3+
import eatda.domain.ImageDomain;
34
import eatda.exception.BusinessErrorCode;
45
import eatda.exception.BusinessException;
56
import java.io.IOException;
67
import java.time.Duration;
78
import java.util.Set;
89
import java.util.UUID;
910
import org.springframework.beans.factory.annotation.Value;
10-
import org.springframework.lang.Nullable;
11-
import org.springframework.stereotype.Service;
11+
import org.springframework.stereotype.Component;
1212
import org.springframework.web.multipart.MultipartFile;
1313
import software.amazon.awssdk.core.sync.RequestBody;
1414
import software.amazon.awssdk.services.s3.S3Client;
@@ -17,8 +17,8 @@
1717
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
1818
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
1919

20-
@Service
21-
public class ImageService {
20+
@Component
21+
public class ExternalImageStorage {
2222

2323
private static final Set<String> ALLOWED_CONTENT_TYPES = Set.of("image/jpg", "image/jpeg", "image/png");
2424
private static final String DEFAULT_CONTENT_TYPE = "bin";
@@ -30,7 +30,7 @@ public class ImageService {
3030
private final String bucket;
3131
private final S3Presigner s3Presigner;
3232

33-
public ImageService(
33+
public ExternalImageStorage(
3434
S3Client s3Client,
3535
@Value("${spring.cloud.aws.s3.bucket}") String bucket,
3636
S3Presigner s3Presigner) {
@@ -75,12 +75,8 @@ private String getExtension(String filename) {
7575
return filename.substring(filename.lastIndexOf(EXTENSION_DELIMITER) + 1);
7676
}
7777

78-
@Nullable
79-
public String getPresignedUrl(@Nullable String key) {
80-
if (key == null) {
81-
return null;
82-
}
8378

79+
public String getPresignedUrl(String key) {
8480
try {
8581
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
8682
.bucket(bucket)

0 commit comments

Comments
 (0)