๐ ๋จผ์ ์ฝ์ผ๋ฉด ์ข์ ๊ธ
๋ก์ปฌ ์บ์ & ๋ถ์ฐ ์บ์ ์ดํดํ๊ธฐ
๐ ๋จผ์ ์ฝ์ผ๋ฉด ์ข์ ๊ธ ์บ์(Cache) ๊ฐ๋ ์ฝ๊ฒ ์ ๋ฆฌํ๊ธฐ๐ ์บ์(Cache)๋?์ง๊ธ๊น์ง ๋ฐ์ดํฐ๊ฐ ํ์ํ ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ง์ ์กฐํํด์์ต๋๋ค.๊ฐ๋ฐ์ ๊ณต๋ถํด๋ดค๋ค๋ฉด ์ ํ๋ฆฌ์ผ์ด์ ์ฑ๋ฅ์ I/O ์
syleeblog.tistory.com
์คํ๋ง ํ๋ก์ ํธ์์ ์ฑ๋ฅ์ ๊ฐ์ ํ๋ ๋ฐ ์์ฃผ ์ฌ์ฉ๋๋ ๊ธฐ์ ์ค ํ๋๊ฐ ์บ์์ ๋๋ค.
์ง๋ ๊ธ๋ค์์ ์บ์ ๊ฐ๋ ์ ๋ํด ์์๋ณด์์ต๋๋ค. ์ด๋ฒ ๊ธ์์๋ ์คํ๋ง ํ๋ก์ ํธ์ ๋ก์ปฌ ์บ์๋ฅผ ์ ์ฉํด๋ณด๋ ค ํฉ๋๋ค.
๋ํ์ ์ธ ๋ก์ปฌ ์บ์ Caffeine์ ์ ์ฉํ๊ณ , @Cacheable, @CachePut, @CacheEvict์ ๊ฐ์ ์คํ๋ง์ ์บ์ ์ด๋
ธํ
์ด์
์ ๋ํด ์ ๋ฆฌํด๋ด
์๋ค.
โ๏ธ ํ๊ฒฝ
- SpringBoot 3.4.0
- build.gradle์ dependency ์ถ๊ฐ
// Cache
implementation 'org.springframework.boot:spring-boot-starter-cache'
์ด ๊ธ์์๋ ๋ก์ปฌ ์บ์ ์ค ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉ๋๋ ์บ์ ์ค ํ๋์ธ Caffeine ์บ์๋ฅผ ์ฌ์ฉํฉ๋๋ค.
โ Caffeine Cache
์นดํ์ธ ์บ์๋ Java์์ ์ฌ์ฉํ๋ ๊ณ ์ฑ๋ฅ ๋ก์ปฌ ์บ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก
๋น ๋ฅธ ์๋์ ๋์ ์ ์ค๋ฅ , ๊ทธ๋ฆฌ๊ณ ๋ค์ํ ์ถ๊ฐ ๊ธฐ๋ฅ์ ์ง์ํ๋ฉฐ Spring์์๋ ๊ณต์์ ์ผ๋ก ์ง์ํ๋ ์ข์ ์บ์์ ๋๋ค.
์นดํ์ธ ์บ์๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์ ๋ค์๊ณผ ๊ฐ์ ์์กด์ฑ์ ์ถ๊ฐํด์ฃผ์ธ์.
build.gradle
implementation 'com.github.ben-manes.caffeine:caffeine'
๊ทธ๋ฆฌ๊ณ Configuration ํ์ผ์ ์ถ๊ฐํด์ค์๋ค.
Cache Configuration
@Configuration
@EnableCaching
@Slf4j
class CacheConfig {
@Bean
public CaffeineCacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("users", "notifications");
cacheManager.setCaffeine(caffeineCacheBuilder());
return cacheManager;
}
@Bean
public Caffeine<Object, Object> caffeineCacheBuilder() {
return Caffeine.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES) // ์ฐ๊ณ 30๋ถ ํ ๋ง๋ฃ
.expireAfterAccess(10, TimeUnit.MINUTES) // ๋ฌด์ฌ์ฉ 10๋ถ ํ ๋ง๋ฃ
.maximumSize(5000) // ์ต๋ ์ํธ๋ฆฌ ์
.recordStats() // ํต๊ณ ์์ง ํ์ฑํ
.removalListener((key, value, cause) -> // ์ ๊ฑฐ ์ด๋ฒคํธ ๋ฆฌ์ค๋
log.info("Cache removed: key={}, cause={}", key, cause));
}
}
๐งโ๐ผ ์บ์ ๋งค๋์ (Cache Manager)๋?
์บ์ ๋งค๋์ ๋ ์คํ๋ง์์ ์ฌ๋ฌ ์บ์ ์ธ์คํด์ค๋ฅผ ํตํฉ ๊ด๋ฆฌํ๋ ํต์ฌ ์ปดํฌ๋ํธ์ ๋๋ค.
์ด๋ค ์บ์๋ฅผ ์ด๋ค ์ด๋ฆ์ผ๋ก ์์ฑํ ์ง, ์ ์ฑ ์ ์ด๋ป๊ฒ ์ ์ฉํ ๊ฑด์ง ๊ฒฐ์ ํ์ฃ .
CaffeineCacheManager๋ CacheManager์ ๊ตฌํ์ฒด ์ค ํ๋๋ก, ์ฐ๋ฆฌ๋ ์นดํ์ธ ์บ์๋ฅผ ์ฌ์ฉํ๋ ์ด ๋งค๋์ ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
์บ์๋ฅผ ๊ตฌ์ฒด์ ์ผ๋ก ์ค์ ํ์ฌ setCaffeine() ๋ฉ์๋๋ฅผ ํตํด ์ค์ ์ ์ ์ฉํฉ๋๋ค.
โ ์นดํ์ธ ์บ์ ์์ธ ์ค์
- ๋ง๋ฃ ์ ์ฑ
expireAfterWrite: ๋ฐ์ดํฐ๊ฐ ์์ฑ/์์ ๋ ํ 30๋ถ์ด ์ง๋๋ฉด ์บ์์์ ์๋ ์ญ์ ๋ฉ๋๋ค. ์บ์์ ์ ์ฅํ์ง ์ค๋๋ ๋ฐ์ดํฐ๋ ํ๋ฆด ๊ฐ๋ฅ์ฑ์ด ๋๊ธฐ ๋๋ฌธ์ ๋๋ค.expireAfterAccess: 10๋ถ ๋์ ์ ๊ทผ์ด ์์ผ๋ฉด ๋ถํ์ํ ์บ์ฑ์ด๋ผ ๊ฐ์ฃผํ์ฌ ์ญ์ ๋ฉ๋๋ค.
- ์ฉ๋ ๊ด๋ฆฌ
maximumSize(5000): ์บ์์ ๋ฐ์ดํฐ๊ฐ ๋ฌดํ์ผ๋ก ์ ์ฅ๋์ง ์๋๋ก ์ต๋ 5000๊ฐ์ ๋ฐ์ดํฐ๋ง ์ ์ฅํ๋๋ก ์ ํํฉ๋๋ค.- ๋ก์ปฌ ์บ์๋ ์๋ฒ ๋ฉ๋ชจ๋ฆฌ ๋ด์ ์์นํ๋ฏ๋ก ์ฉ๋์ด ๋๋ฌด ํฌ๋ฉด ์๋ฒ ์ฑ๋ฅ์ ์ํฅ์ ์ค ์ ์์ต๋๋ค.
- ๋ฐ์ดํฐ ์ด๊ณผ๋ก ์ธํด ์ญ์ ์ LRU(Least Recently Used) ์๊ณ ๋ฆฌ์ฆ์ ๋ฐ๋ผ ์ฌ์ฉํ์ง ๊ฐ์ฅ ์ค๋๋ ๋ฐ์ดํฐ๋ถํฐ ์ญ์ ํฉ๋๋ค.
- ๋ชจ๋ํฐ๋ง
recordStats(): ์บ์ ์ ์ค๋ฅ ํต๊ณ๋ฅผ ์์งํฉ๋๋ค.cacheManager.getCache("users").getStats()๋ก ์ ์ค๋ฅ ํ์ธ ๊ฐ๋ฅํฉ๋๋ค.
removalListener: ์บ์ ์ญ์ ์์ธ(๋ง๋ฃ, ์๋ ์ญ์ ๋ฑ)์ ๋ก๊น
ํ ์ ์์ต๋๋ค.
์บ์ ์ด๋ ธํ ์ด์ ์ ์ด์ฉํด ์์ฝ๊ฒ ์บ์ ๊ธฐ๋ฅ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
โ @Cacheable
@Cacheable ์ด๋
ธํ
์ด์
์ ๋ฉ์๋ ์์ ๋ถ์ฌ ๋ฐ์ดํฐ๋ฅผ ์บ์์ ์ ์ฅํฉ๋๋ค.
๋ฉ์๋ ํธ์ถ ๊ฒฐ๊ณผ๋ฅผ ์บ์์ ์ ์ฅํ๊ณ ,
๊ฐ์ ์ธ์๋ก ์ฌํธ์ถ ์ ๋ฉ์๋ ๋ด๋ถ ๋ก์ง์ด ์คํ๋์ง ์๊ณ , ์บ์์์ ๋ฐ๋ก ๋ฐํํฉ๋๋ค.
// ์ ์ ๋ณ๋ก ์๋ฆผ ๋ชฉ๋ก์ ์กฐํํ๋ ์๋น์ค ๋ฉ์๋
// userId๋ฅผ ํค๋ก ์ ์ฅ๋์ด, userId๋ง๋ค ๋ชฉ๋ก ์กฐํ ๊ฒฐ๊ณผ๊ฐ ์บ์ฑ๋จ
@Cacheable(
cacheNames = "notifications",
key = "#userId"
)
public List<NotificationDto> findAll(UUID userId) {
List<Notification> notifications = notificationRepository.findAllByReceiverId(userId);
return notificationMapper.toDto(notifications);
}
์ฃผ์ ์์ฑ
| cacheNames | ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ์บ์ ์ ์ฅ์ ์ด๋ฆ |
| key | ์บ์ ํค ์ง์ (๊ธฐ๋ณธ์ ์ผ๋ก ํ๋ผ๋ฏธํฐ ์กฐํฉ์ผ๋ก ๋ง์ด ํจ) |
| condition | ์บ์ฑ ์ ์ฉ ์กฐ๊ฑด(๋ฉ์๋ ์คํ ์ ์ ์กฐ๊ฑด์ ๋ถํฉํ๋์ง ํ๊ฐํจ) |
| unless | ์บ์ฑ ์ ์ธ ์กฐ๊ฑด(๋ฉ์๋ ์คํ ํ ๊ฒฐ๊ณผ๋ฅผ ์บ์ฑํ ์ง ํ๊ฐํจ) |
| sync | ๋๊ธฐํ ์บ์ ์กฐํ ๊ธฐ๋ฅ ํ์ฑํ
|
๋ ๋ค์ํ ์์ฑ์ ์จ๋ณด๋ฉด ์๋์ ๊ฐ์ต๋๋ค.
SpEL(Spring Expression Language) ๋ฌธ๋ฒ์ ๋ฐ๋ผ ์์ฑํ๋๋ก ์ฃผ์ํด์ผ ํฉ๋๋ค.
@Cacheable(
cacheNames = "notifications",
key = "#userId"
condition= "#userId!=null" // userId๊ฐ null์ด ์๋ ๊ฒฝ์ฐ์๋ง ์บ์ฑ ์ ์ฉ
unless = "#result.getSize() == 0" // ์กฐํํ ๊ฒฐ๊ณผ ์๋ฆผ ๊ฐ์๊ฐ 0๊ฐ์ธ ๊ฒฝ์ฐ ์บ์ฑ ์ ์ธ
)
public List<NotificationDto> findAll(UUID userId) {
List<Notification> notifications = notificationRepository.findAllByReceiverId(userId);
return notificationMapper.toDto(notifications);
}
โ @CachePut
๋ฉ์๋ ์คํ ๊ฒฐ๊ณผ๋ฅผ ๋ฌด์กฐ๊ฑด ์บ์์ ์ ์ฅ(๊ฐฑ์ )ํ๋ ์ด๋ ธํ ์ด์ ์ ๋๋ค.
@Cacheable์ ๋ฉ์๋๋ฅผ ์คํํ๊ธฐ ์ , ์บ์์ ๋ฐ์ดํฐ๊ฐ ์์ผ๋ฉด ๋ฉ์๋๋ฅผ ์คํํ์ง ์๊ณ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํ์ง๋ง
@CachePut์ ํญ์ ๋ฉ์๋๋ฅผ ์คํํ๋ ์ฐจ์ด๊ฐ ์์ต๋๋ค.
์ ์ ๋ณ๋ก ๋ง์ดํ์ด์ง๋ฅผ ๋น ๋ฅด๊ฒ ์กฐํํ๊ธฐ ์ํด
์ ์ ์ ๋ณด DTO๋ฅผ ์กฐํํ๋ ๋ฉ์๋์ @Cacheable์ ์ ์ฉํ๋ค๊ณ ํฉ์๋ค.
@Cacheable(
cacheNames = "users",
key = "#userId"
)
public UserDto find(UUID userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(Map.of("userId", userId)));
return userMapper.toDto(user);
}
๊ทธ๋ ๋ค๋ฉด ํ๋กํ ์ด๋ฏธ์ง๋ ์ฐ๋ฝ์ฒ๋ฅผ ๋ณ๊ฒฝํ๋ค๋ ์ง ์ ์ ์ ๋ณด๊ฐ ์ ๋ฐ์ดํธ ๋๋ฉด ์ด๋จ๊น์?
์บ์์ ์๋ ์ ๋ณด๋ ์ค๋๋ ๋ฐ์ดํฐ๊ฐ ๋์ด ํ์์๊ฒ ๋๊ณ , ๊ฐฑ์ ์ด ํ์ํด์ง๋๋ค.
๊ทธ๋ผ ์ ์ ์ ๋ณด๊ฐ ์
๋ฐ์ดํธ๋ ๋๋ง๋ค ์บ์์์ ํด๋น ํค๊ฐ ์
๋ฐ์ดํธ๋๋๋ก @CachePut์ ์ด์ฉํฉ์๋ค.
๊ฒฐ๊ณผ๋ก ๋ฐํ๋๋ ์ ์ ์ ๋ณด๊ฐ ์บ์์ ์ ์ฅ๋ฉ๋๋ค.
@CachePut(
cacheNames = "users",
key = "#userUpdateRequest.userId"
)
public UserDto update(UUID userId, UserUpdateRequest userUpdateRequest,
// User ์ํฐํฐ ์
๋ฐ์ดํธ ๋ก์ง
return userMapper.toDto(user);
}
โ @CacheEvict
์บ์์ ์ ์ฅ๋ ๋ฐ์ดํฐ๋ฅผ ์ ๊ฑฐํ๋ ๋ฐฉ๋ฒ์ ๋๋ค.
๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค ์ค์ ๋ฐ์ดํฐ์ ์บ์์ ์ ์ฅ๋ ๋ฐ์ดํฐ๊ฐ ๋ถ์ผ์น๊ฐ ์ผ์ด๋ ์ ์์ผ๋ฏ๋ก
์ ์ ํ๊ฒ @CacheEvict๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐฉ์งํด์ผ ํฉ๋๋ค.
์๊น ์ ์ ๋ณ ์ ๋ณด๋ฅผ ๋น ๋ฅด๊ฒ ์กฐํํ๊ธฐ ์ํด ์บ์ฑํ์์ต๋๋ค.
๋ง์ฝ ๊ทธ ์ ์ ๊ฐ ์ญ์ ๋๋ค๋ฉด ์บ์์์๋ ๋ ์ด์ ํ์์๋ ๋ฐ์ดํฐ๊ฐ ๋๊ฒ ์ฃ ?
@CacheEvict๋ฅผ ํตํด ์บ์์์ ํด๋น ์ํธ๋ฆฌ๋ฅผ ์ญ์ ํฉ๋๋ค.
@CacheEvict(cacheNames = "users", key = "#userId")
public void delete(UUID userId) {
User user = userRepository.findById(userId).orElseThrow(() -> {
log.warn("[Updating User Failed: User with id {} not found]", userId);
return new UserNotFoundException(Map.of("userId", userId));
});
userRepository.delete(user);
}
์ฃผ์ ์์ฑ
| cacheNames | ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ ์บ์ ์ ์ฅ์ ์ด๋ฆ |
| key | ์บ์ ํค ์ง์ (๊ธฐ๋ณธ์ ์ผ๋ก ํ๋ผ๋ฏธํฐ ์กฐํฉ์ผ๋ก ๋ง์ด ํจ) |
| allEnrites = true | ํน์ ์ํธ๋ฆฌ๊ฐ ์๋๋ผ ์บ์ ๋ด ๋ชจ๋ ์ํธ๋ฆฌ ์ญ์ |
| beforeInvocation | true์ด๋ฉด ๋ฉ์๋ ์คํ ์ ์ ์บ์์์ ์ญ์ false์ด๋ฉด ๋ฉ์๋ ์คํ ์ฑ๊ณต ํ์ ์ญ์
|
| condition | ์บ์์์ ์ ๊ฑฐํ๋ ์กฐ๊ฑด |
์ด๋ฒ ๊ธ์์๋ Spring์์ ๋ก์ปฌ ์บ์๋ก Caffeine์ ์ค์ ํ๊ณ ,
์บ์ ๊ด๋ จ ์ด๋
ธํ
์ด์
๋ค์ ์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง ๊ธฐ๋ณธ์ ์ธ ๋ด์ฉ์ ์ ๋ฆฌํด๋ณด์์ต๋๋ค.
์์ผ๋ก์ ํ๋ก์ ํธ์์ ์์ฃผ ์บ์๋ฅผ ์ ์ฉํ ๊ฒ ๊ฐ์์!
์ฐธ๊ณ ์๋ฃ
Spring boot ์ caffeine ์บ์๋ฅผ ์ ์ฉํด๋ณด์ - ์ด๋ป๊ฒํ๋ฉด ์ผ์ ์ ํ ๊น?
Spring Boot ์์ Cache ์ฌ์ฉํ๊ธฐ
'๐ฟSpring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| [Spring] DI(Dependency Injection) ์์กด์ฑ ์ฃผ์ (4) | 2025.08.14 |
|---|---|
| [Spring] IoC ์ ์ด์ ์ญ์ (1) | 2025.08.12 |
| JWT(Json Web Token) ๊ตฌ์กฐ ์ดํดํ๊ธฐ (0) | 2025.05.25 |
| [Spring Security] ์น ๋ณด์ ๊ณต๊ฒฉ #2 ์ธ์ ๊ณ ์ ๊ณต๊ฒฉ, JWT ํ ํฐ ํ์ทจ + ๋์ ์ ๋ต (0) | 2025.05.23 |
| [Spring Security] ์น ๋ณด์ ๊ณต๊ฒฉ #1: CSRF๊ณต๊ฒฉ๊ณผ XSS ๊ณต๊ฒฉ + ๋์ ์ ๋ต (1) | 2025.05.23 |
