Ver2.0 ๐ Spring Cache๋ก ์ ํ๋ฆฌ์ผ์ด์ ์ฑ๋ฅ ์ต์ ํํ๊ธฐ ๐

๐ Spring Cache๋ก ์ ํ๋ฆฌ์ผ์ด์ ์ฑ๋ฅ ์ต์ ํํ๊ธฐ ๐
Java ๊ฐ๋ฐ์๋ฅผ ์ํ ์บ์ ํ์ฉ ์์ ์ ๋ณต! ์ฑ๋ฅ ๋ํ์ ๋๊ธฐ ๐ฏ
์๋ ํ์ธ์ ์ฌ๋ฌ๋ถ! ์ค๋์ ์ ๋ง ๊ฟ์ผ ์ฃผ์ ์ธ Spring Cache๋ก ์ ํ๋ฆฌ์ผ์ด์ ์ฑ๋ฅ ์ต์ ํํ๋ ๋ฐฉ๋ฒ์ ๋ํด ํจ๊ป ์์๋ณผ๊ฒ์! ๐ฅ
์ฌ๋ฌ๋ถ์ ์ ํ๋ฆฌ์ผ์ด์ ์ด ๊ฑฐ๋ถ์ด์ฒ๋ผ ๋๋ฆฌ๊ฒ ๋์ํ๋ค๋ฉด? DB ์ฟผ๋ฆฌ๊ฐ ๋งค๋ฒ ์คํ๋ผ์ ์ฌ์ฉ์๋ค์ด "์ด๊ฑฐ ๋ญ์? ์ ์ด๋ ๊ฒ ๋๋ฆผ?" ํ๊ณ ์๋ค๋ฉด? ์ด์ ์บ์๋ฅผ ํ์ฉํด์ ์ฌ๋ฌ๋ถ์ ์ฑ์ ์นํ๊ธ ์๋๋ก ๋ฐ๊ฟ๋ด ์๋ค! ๐ข โ ๐
์ฌ๋ฅ๋ท์์ Java ๊ฐ๋ฐ ์ฌ๋ฅ์ ๊ณต์ ํ๊ณ ์๋ ๊ฐ๋ฐ์๋ถ๋ค์ด๋ผ๋ฉด ์ด ๊ธ์ ํตํด ์ค๋ ฅ์ ํ ๋จ๊ณ ์ ๊ทธ๋ ์ด๋ํ ์ ์์ ๊ฑฐ์์. ์, ์ด์ ์์ํด๋ณผ๊น์? ๐
๐ ๋ชฉ์ฐจ
- ์บ์๊ฐ ๋ญ๊ธธ๋? ๊ธฐ๋ณธ ๊ฐ๋ ์ดํดํ๊ธฐ
- Spring Cache ์๊ฐ ๋ฐ ์ค์ ๋ฐฉ๋ฒ
- ์บ์ ์ด๋ ธํ ์ด์ ๋ง์คํฐํ๊ธฐ
- ๋ค์ํ ์บ์ ๋งค๋์ ์์๋ณด๊ธฐ
- ์ค์ ์์ ๋ก ๋ฐฐ์ฐ๋ Spring Cache
- ์บ์ ์ ๋ต ๋ฐ ํจํด
- ์ฑ๋ฅ ์ธก์ ๋ฐ ๋ชจ๋ํฐ๋ง
- ์ฃผ์์ฌํญ ๋ฐ ๋ฒ ์คํธ ํ๋ํฐ์ค
- ์ค๋ฌด์์ ์์ฃผ ๋ฐ์ํ๋ ๋ฌธ์ ํด๊ฒฐํ๊ธฐ
- ๋ง๋ฌด๋ฆฌ ๋ฐ ์ถ๊ฐ ํ์ต ์๋ฃ
1. ์บ์๊ฐ ๋ญ๊ธธ๋? ๊ธฐ๋ณธ ๊ฐ๋ ์ดํดํ๊ธฐ ๐ค
์บ์(Cache)๋ผ๋ ๋ง, ๋ง์ด ๋ค์ด๋ณด์ จ์ฃ ? ๊ทผ๋ฐ ์ ํํ ๋ญ์ง ์์๋์? ๊ฐ๋จํ ๋งํ๋ฉด ์์ฃผ ์ฌ์ฉํ๋ ๋ฐ์ดํฐ๋ฅผ ์์๋ก ์ ์ฅํด๋๋ ๊ณต๊ฐ์ด์์. ๋ง์น ์ฌ๋ฌ๋ถ์ด ์์ฃผ ์ฐ๋ ๋ฌผ๊ฑด์ ์ฑ ์ ์์ ์ฌ๋ ค๋๋ ๊ฒ์ฒ๋ผ์!
๐ช ํธ์์ ๋น์ ๋ก ์ดํดํ๋ ์บ์
์ฌ๋ฌ๋ถ์ด ํธ์์ ์ ์ฅ์ด๋ผ๊ณ ์์ํด๋ณด์ธ์. ์๋๋ค์ด ์์ฃผ ์ฌ๋ ๋ฌผ๊ฑด(๋ผ๋ฉด, ์๋ฃ์)์ ๊ณ์ฐ๋ ๊ทผ์ฒ์ ๋๊ณ , ๊ฐ๋ ์ฌ๋ ๋ฌผ๊ฑด์ ์์ชฝ ์ง์ด๋์ ๋์์์. ์ด๊ฒ ๋ฐ๋ก ์บ์์ ์๋ฆฌ์์! ์์ฃผ ์ ๊ทผํ๋ ๋ฐ์ดํฐ๋ ๋นจ๋ฆฌ ๊ฐ์ ธ์ฌ ์ ์๋ ๊ณณ(๋ฉ๋ชจ๋ฆฌ)์ ๋๊ณ , ๋๋จธ์ง๋ ๋๋ฆฐ ์ ์ฅ์(๋์คํฌ, DB)์ ๋๋ ๊ฑฐ์ฃ .
์บ์๊ฐ ํ์ํ ์ด์ ๐โโ๏ธ
์ ์บ์๊ฐ ํ์ํ ๊น์? ๋ค์ ์ํฉ์ ์๊ฐํด๋ณด์ธ์:
- DB ์ฟผ๋ฆฌ๋ ๋น์ธ์! (์๊ฐ์ด ๋ง์ด ๊ฑธ๋ฆผ)
- ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๋ณตํด์ ์์ฒญํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์์
- ๋คํธ์ํฌ ํธ์ถ์ ํญ์ ์ง์ฐ์๊ฐ์ด ๋ฐ์ํด์
- ์๋ฒ ๋ถํ๋ฅผ ์ค์ด๊ณ ์ถ์ด์
- ์ฌ์ฉ์์๊ฒ ๋น ๋ฅธ ์๋ต์ ์ ๊ณตํ๊ณ ์ถ์ด์
์บ์์ ์ข ๋ฅ ๐๏ธ
์บ์๋ ์ฌ๋ฌ ๋ ๋ฒจ์์ ๊ตฌํํ ์ ์์ด์:
- ์น ๋ธ๋ผ์ฐ์ ์บ์: ์ด๋ฏธ์ง, CSS, JS ํ์ผ ๋ฑ์ ๋ก์ปฌ์ ์ ์ฅ
- CDN ์บ์: ์ ์ธ๊ณ ์ฌ๋ฌ ์ง์ญ์ ์ฝํ ์ธ ๋ณต์ฌ๋ณธ ์ ์ฅ
- API ๊ฒ์ดํธ์จ์ด ์บ์: API ์๋ต์ ์บ์ฑ
- ์ ํ๋ฆฌ์ผ์ด์ ์บ์: ์ฐ๋ฆฌ๊ฐ ์ค๋ ๋ฐฐ์ธ Spring Cache!
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์บ์: DB ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅ
"์บ์๋ ์ปดํจํฐ ๊ณผํ์์ ๊ฐ์ฅ ์ด๋ ค์ด ๋ ๊ฐ์ง ๋ฌธ์ ์ค ํ๋์ ๋๋ค. ๋ค๋ฅธ ํ๋๋ ์ด๋ฆ ์ง๊ธฐ์ ์คํ-๋ฐ์ด-์ ์๋ฌ์ ๋๋ค." - ๋งํด ํ์ธ๋ฌ
ใ ใ ใ ์บ์๊ฐ ์ด๋ ต๋ค๊ณ ์? ๊ฑฑ์ ๋ง์ธ์! ์ด ๊ธ์ ๋ค ์ฝ์ผ๋ฉด ์ฌ๋ฌ๋ถ๋ ์บ์ ๋ง์คํฐ๊ฐ ๋ ์ ์์ด์! ๐
2. Spring Cache ์๊ฐ ๋ฐ ์ค์ ๋ฐฉ๋ฒ โ๏ธ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก Spring Cache์ ๋ํด ์์๋ณผ๊ฒ์! Spring Framework๋ ์บ์ฑ์ ์ํ ์ถ์ํ ๋ ์ด์ด๋ฅผ ์ ๊ณตํ๋๋ฐ, ์ด๊ฒ ๋ฐ๋ก Spring Cache ์ถ์ํ์์.
Spring Cache์ ํน์ง ๐
- ์ ์ธ์ ์บ์ฑ (์ด๋ ธํ ์ด์ ๊ธฐ๋ฐ)
- ๋ค์ํ ์บ์ ์ ๊ณต์(provider) ์ง์
- ํธ๋์ญ์ ๊ณผ์ ํตํฉ
- ์บ์ ์ถ์ํ๋ก ๊ตฌํ์ฒด ์ฝ๊ฒ ๊ต์ฒด ๊ฐ๋ฅ
๐ง ์ Spring Cache๋ฅผ ์ฌ์ฉํด์ผ ํ ๊น์?
์ฌ๋ฌ๋ถ์ด ์ง์ ์บ์ ๋ก์ง์ ๊ตฌํํ ์๋ ์์ง๋ง, ๊ทธ๋ฌ๋ฉด ๋น์ฆ๋์ค ๋ก์ง๊ณผ ์บ์ ๋ก์ง์ด ์์ฌ์ ์ฝ๋๊ฐ ์ง์ ๋ถํด์ ธ์. Spring Cache๋ฅผ ์ฌ์ฉํ๋ฉด AOP(๊ด์ ์งํฅ ํ๋ก๊ทธ๋๋ฐ) ๋ฐฉ์์ผ๋ก ๊น๋ํ๊ฒ ์บ์ ๊ธฐ๋ฅ์ ์ ์ฉํ ์ ์์ด์!
Spring Cache ์ค์ ํ๊ธฐ ๐ ๏ธ
Spring Boot ํ๋ก์ ํธ์์ ์บ์๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด ๋ค์๊ณผ ๊ฐ์ ๋จ๊ณ๋ฅผ ๋ฐ๋ฅด๋ฉด ๋ผ์:
1. ์์กด์ฑ ์ถ๊ฐํ๊ธฐ
๋จผ์ Maven์ด๋ Gradle์ ์์กด์ฑ์ ์ถ๊ฐํด์ผ ํด์.
Maven (pom.xml):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
Gradle (build.gradle):
implementation 'org.springframework.boot:spring-boot-starter-cache'
์ถ๊ฐ๋ก ์บ์ ๊ตฌํ์ฒด(์: Caffeine, Redis ๋ฑ)๋ ํ์ํด์:
// Caffeine ์บ์ ์ฌ์ฉ ์
implementation 'com.github.ben-manes.caffeine:caffeine'
// Redis ์บ์ ์ฌ์ฉ ์
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
2. ์บ์ ํ์ฑํํ๊ธฐ
์ ํ๋ฆฌ์ผ์ด์
ํด๋์ค์ @EnableCaching ์ด๋
ธํ
์ด์
์ ์ถ๊ฐํด์:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching // ์ด ์ด๋
ธํ
์ด์
์ผ๋ก ์บ์ ๊ธฐ๋ฅ ํ์ฑํ!
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
์ด @EnableCaching ์ด๋
ธํ
์ด์
์ด Spring์ ์บ์ ์ธํ๋ผ๋ฅผ ํ์ฑํํด์ค์. ์ด์ ์บ์ ๊ด๋ จ ์ด๋
ธํ
์ด์
๋ค์ ์ฌ์ฉํ ์ ์๊ฒ ๋์ด์! ๐
3. ์บ์ ๋งค๋์ ์ค์ ํ๊ธฐ
๊ธฐ๋ณธ์ ์ผ๋ก Spring Boot๋ ConcurrentMapCacheManager๋ฅผ ์ฌ์ฉํ์ง๋ง, ์ค์ ํ๋ก๋์
ํ๊ฒฝ์์๋ ๋ ๊ฐ๋ ฅํ ์บ์ ๋งค๋์ ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ์ข์์.
Caffeine ์บ์ ์ค์ ์์:
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(60, TimeUnit.MINUTES)
.initialCapacity(100)
.maximumSize(1000));
return cacheManager;
}
}
์ ์ค์ ์ ๋ค์์ ์๋ฏธํด์:
- ์บ์ ํญ๋ชฉ์ ์์ฑ ํ 60๋ถ ํ์ ๋ง๋ฃ๋ผ์
- ์ด๊ธฐ ์บ์ ์ฉ๋์ 100๊ฐ ํญ๋ชฉ์ด์์
- ์ต๋ 1000๊ฐ ํญ๋ชฉ๊น์ง ์ ์ฅํ ์ ์์ด์
์ด์ Spring Cache๋ฅผ ์ฌ์ฉํ ์ค๋น๊ฐ ๋๋ฌ์ด์! ๋ค์์ผ๋ก ์ค์ ๋ก ์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง ์์๋ณผ๊ฒ์. ๐
3. ์บ์ ์ด๋ ธํ ์ด์ ๋ง์คํฐํ๊ธฐ ๐
Spring Cache์ ํต์ฌ์ ์ด๋ ธํ ์ด์ ์ด์์! ์ด ์ด๋ ธํ ์ด์ ๋ค์ ์ ์ฌ์ฉํ๋ฉด ์ฝ๋ ๋ช ์ค๋ก ์บ์ ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์์ด์. ์ง์ง ๊ฐ๋ฐ์ ์ธ์์ด ํธํด์ง๋ค๋๊น์? ใ ใ ใ
์ฃผ์ ์บ์ ์ด๋ ธํ ์ด์ 5๊ฐ์ง ๐
1. @Cacheable
๋ฉ์๋ ํธ์ถ ๊ฒฐ๊ณผ๋ฅผ ์บ์์ ์ ์ฅํ๊ณ , ๋์ผํ ํ๋ผ๋ฏธํฐ๋ก ๋ค์ ํธ์ถ๋ ๋ ์บ์์์ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ ธ์์.
@Cacheable(value = "users", key = "#userId")
public User getUserById(Long userId) {
// ์ด ๋ฉ์๋๋ ์ฒ์ ํธ์ถ๋ ๋๋ง ์คํ๋๊ณ ,
// ๊ฐ์ userId๋ก ๋ค์ ํธ์ถํ๋ฉด DB ์ ๊ทผ ์์ด ์บ์์์ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํด์
return userRepository.findById(userId).orElse(null);
}
2. @CachePut
๋ฉ์๋ ์คํ ๊ฒฐ๊ณผ๋ฅผ ์บ์์ ์ ์ฅํ์ง๋ง, ์บ์ ํ์ธ์ ํ์ง ์์์. ์ฃผ๋ก ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ ์ ์ฌ์ฉํด์.
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
// ์ด ๋ฉ์๋๋ ํญ์ ์คํ๋๊ณ , ๊ฒฐ๊ณผ๊ฐ ์บ์์ ์ ์ฅ๋ผ์
// ๊ธฐ์กด ์บ์ ๋ด์ฉ์ ์๋ก์ด ๊ฐ์ผ๋ก ์
๋ฐ์ดํธํ ๋ ์ฌ์ฉํด์
return userRepository.save(user);
}
3. @CacheEvict
์บ์์์ ํญ๋ชฉ์ ์ ๊ฑฐํด์. ๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋๊ฑฐ๋ ์ญ์ ๋ ๋ ์ฌ์ฉํด์.
@CacheEvict(value = "users", key = "#userId")
public void deleteUser(Long userId) {
// ์ด ๋ฉ์๋๊ฐ ์คํ๋๋ฉด 'users' ์บ์์์ ํด๋น userId์ ๋ํ ํญ๋ชฉ์ด ์ ๊ฑฐ๋ผ์
userRepository.deleteById(userId);
}
// ์บ์ ์ ์ฒด ๋น์ฐ๊ธฐ
@CacheEvict(value = "users", allEntries = true)
public void clearAllUsersCache() {
// 'users' ์บ์์ ๋ชจ๋ ํญ๋ชฉ์ ์ ๊ฑฐํด์
System.out.println("๋ชจ๋ ์ฌ์ฉ์ ์บ์๊ฐ ์ง์์ก์ด์!");
}
4. @Caching
์ฌ๋ฌ ์บ์ ์์ ์ ๊ทธ๋ฃนํํ ์ ์์ด์.
@Caching(
evict = {
@CacheEvict(value = "users", key = "#user.id"),
@CacheEvict(value = "usersByName", key = "#user.name")
},
put = {
@CachePut(value = "userDetails", key = "#user.id")
}
)
public User updateUserWithMultipleCaches(User user) {
// ์ฌ๋ฌ ์บ์ ์์
์ ํ ๋ฒ์ ์ฒ๋ฆฌํด์
return userRepository.save(user);
}
5. @CacheConfig
ํด๋์ค ๋ ๋ฒจ์์ ๊ณตํต ์บ์ ์ค์ ์ ์ ์ํ ์ ์์ด์.
@Service
@CacheConfig(cacheNames = {"users"}) // ์ด ํด๋์ค์ ๋ชจ๋ ์บ์ ์์
์ 'users' ์บ์๋ฅผ ์ฌ์ฉํด์
public class UserService {
@Cacheable(key = "#userId") // cacheNames๋ฅผ ์ง์ ํ์ง ์์๋ 'users' ์บ์๋ฅผ ์ฌ์ฉํด์
public User getUser(Long userId) {
return userRepository.findById(userId).orElse(null);
}
@CacheEvict(key = "#userId") // ๋ง์ฐฌ๊ฐ์ง๋ก 'users' ์บ์๋ฅผ ์ฌ์ฉํด์
public void deleteUser(Long userId) {
userRepository.deleteById(userId);
}
}
SpEL ํํ์์ผ๋ก ์บ์ ํค ์ง์ ํ๊ธฐ ๐
Spring Cache๋ SpEL(Spring Expression Language)์ ์ฌ์ฉํด์ ๋์ ์ผ๋ก ์บ์ ํค๋ฅผ ์์ฑํ ์ ์์ด์. ์ด๊ฑด ์ง์ง ๊ฟํ์ด์์! ๐
์์ฃผ ์ฌ์ฉํ๋ SpEL ํํ์ ์์:
#paramName- ๋ฉ์๋ ํ๋ผ๋ฏธํฐ ์ด๋ฆ์ผ๋ก ์ ๊ทผ#p0,#a0- ์ฒซ ๋ฒ์งธ ํ๋ผ๋ฏธํฐ ์์น๋ก ์ ๊ทผ#root.method- ํ์ฌ ๋ฉ์๋ ์ด๋ฆ#root.target- ํ์ฌ ๋์ ๊ฐ์ฒด#result- ๋ฉ์๋ ์คํ ๊ฒฐ๊ณผ (@CachePut, @CacheEvict์์ ์ฌ์ฉ ๊ฐ๋ฅ)#user.id- ๊ฐ์ฒด์ ์์ฑ์ ์ ๊ทผT(ํจํค์ง.ํด๋์ค).์ ์ ๋ฉ์๋()- ์ ์ ๋ฉ์๋ ํธ์ถ
์๋ฅผ ๋ค์ด, ์ฌ์ฉ์ ID์ ์ญํ ๋ก ๋ณตํฉ ํค๋ฅผ ๋ง๋ค๊ณ ์ถ๋ค๋ฉด:
@Cacheable(value = "userPermissions", key = "#userId + '_' + #role")
public List<Permission> getUserPermissions(Long userId, String role) {
// ๋ณตํฉ ํค๋ก ์บ์ฑ: "123_ADMIN" ๊ฐ์ ํํ
return permissionRepository.findByUserIdAndRole(userId, role);
}
์กฐ๊ฑด๋ถ ์บ์ฑ ์ค์ ํ๊ธฐ ๐
ํญ์ ๋ชจ๋ ๊ฒฐ๊ณผ๋ฅผ ์บ์ฑํ๊ณ ์ถ์ง ์์ ์๋ ์์ด์. Spring Cache๋ condition๊ณผ unless ์์ฑ์ผ๋ก ์กฐ๊ฑด๋ถ ์บ์ฑ์ ์ง์ํด์.
// userId๊ฐ 100๋ณด๋ค ํฐ ๊ฒฝ์ฐ์๋ง ์บ์ฑ
@Cacheable(value = "users", key = "#userId", condition = "#userId > 100")
public User getUserById(Long userId) {
return userRepository.findById(userId).orElse(null);
}
// ๊ฒฐ๊ณผ๊ฐ null์ด ์๋ ๊ฒฝ์ฐ์๋ง ์บ์ฑ
@Cacheable(value = "users", key = "#userId", unless = "#result == null")
public User getUserById(Long userId) {
return userRepository.findById(userId).orElse(null);
}
// ๊ด๋ฆฌ์๊ฐ ์๋ ์ฌ์ฉ์๋ง ์บ์ฑ
@Cacheable(value = "users", key = "#user.id", condition = "!#user.admin")
public User processUser(User user) {
// ์ฒ๋ฆฌ ๋ก์ง
return user;
}
โ ๏ธ ์ฃผ์์ฌํญ
condition์ ๋ฉ์๋ ์คํ ์ ์ ํ๊ฐ๋๊ณ , unless๋ ๋ฉ์๋ ์คํ ํ์ ํ๊ฐ๋ผ์. ๋ฐ๋ผ์ unless์์๋ #result๋ฅผ ์ฌ์ฉํ ์ ์์ง๋ง, condition์์๋ ์ฌ์ฉํ ์ ์์ด์!
์ด์ Spring Cache ์ด๋ ธํ ์ด์ ์ ๊ธฐ๋ณธ์ ๋ง์คํฐํ์ด์! ๋ค์์ผ๋ก ๋ค์ํ ์บ์ ๋งค๋์ ์ ๋ํด ์์๋ณผ๊ฒ์. ๐ง
4. ๋ค์ํ ์บ์ ๋งค๋์ ์์๋ณด๊ธฐ ๐๏ธ
Spring์ ๋ค์ํ ์บ์ ๊ตฌํ์ฒด๋ฅผ ์ง์ํด์. ๊ฐ๊ฐ ์ฅ๋จ์ ์ด ์์ผ๋ ์ฌ๋ฌ๋ถ์ ํ๋ก์ ํธ ์๊ตฌ์ฌํญ์ ๋ง๋ ๊ฑธ ์ ํํ๋ฉด ๋ผ์! ์บ์ ๋งค๋์ ๋ ํ๋ก์ ํธ์ ์ฑ๋ฅ์ ํฐ ์ํฅ์ ๋ฏธ์น๋ ์ ์คํ๊ฒ ์ ํํ์ธ์!
์ฃผ์ ์บ์ ๋งค๋์ ๋น๊ต ๐
1. ConcurrentMapCacheManager
ํน์ง: Spring Boot์ ๊ธฐ๋ณธ ์บ์ ๋งค๋์ . ConcurrentHashMap์ ์ฌ์ฉํด ๋ฉ๋ชจ๋ฆฌ์ ์บ์๋ฅผ ์ ์ฅํด์.
์ฅ์ : ์ค์ ์ด ๊ฐ๋จํ๊ณ ์ถ๊ฐ ์์กด์ฑ์ด ํ์ ์์ด์.
๋จ์ : ์บ์ ๋ง๋ฃ ์ ์ฑ ์ด ์๊ณ , ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ ๊ธฐ๋ฅ์ด ์ ํ์ ์ด์์.
์ ํฉํ ๊ฒฝ์ฐ: ๊ฐ๋ฐ ํ๊ฒฝ์ด๋ ๊ฐ๋จํ ์ ํ๋ฆฌ์ผ์ด์ , ํ๋กํ ํ์ ์ ์ ํฉํด์.
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("users", "products");
}
2. CaffeineCacheManager
ํน์ง: Caffeine์ Java 8๋ก ์ฌ์์ฑ๋ Guava ์บ์๋ก, ๊ณ ์ฑ๋ฅ ์ธ๋ฉ๋ชจ๋ฆฌ ์บ์์์.
์ฅ์ : ๋ฐ์ด๋ ์ฑ๋ฅ, ๋ค์ํ ์บ์ ์ ์ฑ (์๊ฐ ๊ธฐ๋ฐ ๋ง๋ฃ, ํฌ๊ธฐ ์ ํ ๋ฑ), ํต๊ณ ์ ๊ณต
๋จ์ : ๋ถ์ฐ ํ๊ฒฝ์์๋ ๊ฐ ์๋ฒ๋ง๋ค ๋ ๋ฆฝ์ ์ธ ์บ์๋ฅผ ๊ฐ์ ธ์.
์ ํฉํ ๊ฒฝ์ฐ: ๊ณ ์ฑ๋ฅ์ด ํ์ํ ๋จ์ผ ์๋ฒ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ํฉํด์.
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(500)
.recordStats());
return cacheManager;
}
3. EhCacheCacheManager
ํน์ง: EhCache๋ ์ค๋๋ Java ์บ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก, ๋ง์ ๊ธฐ๋ฅ์ ์ ๊ณตํด์.
์ฅ์ : ๋์คํฌ ์ ์ฅ ์ง์, ๋ถ์ฐ ์บ์ฑ ์ต์ , ํ๋ถํ ์ค์ ์ต์
๋จ์ : Caffeine๋ณด๋ค ์ฑ๋ฅ์ด ์ฝ๊ฐ ๋จ์ด์ง ์ ์์ด์.
์ ํฉํ ๊ฒฝ์ฐ: ๋์คํฌ ๊ธฐ๋ฐ ์บ์ฑ์ด ํ์ํ๊ฑฐ๋ EhCache์ ํน์ ๊ธฐ๋ฅ์ด ํ์ํ ๊ฒฝ์ฐ์ ์ ํฉํด์.
@Bean
public CacheManager cacheManager() {
return new EhCacheCacheManager(ehCacheManager());
}
@Bean
public EhCacheManagerFactoryBean ehCacheManager() {
EhCacheManagerFactoryBean factory = new EhCacheManagerFactoryBean();
factory.setConfigLocation(new ClassPathResource("ehcache.xml"));
factory.setShared(true);
return factory;
}
4. RedisCacheManager
ํน์ง: Redis๋ ์ธ๊ธฐ ์๋ ์ธ๋ฉ๋ชจ๋ฆฌ ๋ฐ์ดํฐ ์ ์ฅ์๋ก, ๋ถ์ฐ ์บ์ฑ์ ์ ํฉํด์.
์ฅ์ : ๋ถ์ฐ ํ๊ฒฝ ์ง์, ์์์ฑ ์ต์ , ๋ค์ํ ๋ฐ์ดํฐ ๊ตฌ์กฐ ์ง์
๋จ์ : ๋ณ๋์ Redis ์๋ฒ ์ค์ ์ด ํ์ํ๊ณ , ๋คํธ์ํฌ ์ค๋ฒํค๋๊ฐ ์์ด์.
์ ํฉํ ๊ฒฝ์ฐ: ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ๋ ๋ถ์ฐ ์์คํ ์ ์ ํฉํด์.
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(config)
.build();
}
5. CompositeCacheManager
ํน์ง: ์ฌ๋ฌ ์บ์ ๋งค๋์ ๋ฅผ ํจ๊ป ์ฌ์ฉํ ์ ์์ด์.
์ฅ์ : ๋ค์ํ ์บ์ฑ ์ ๋ต์ ์กฐํฉํ ์ ์์ด์. ์๋ฅผ ๋ค์ด, ์์ฃผ ์ ๊ทผํ๋ ๋ฐ์ดํฐ๋ ๋ก์ปฌ ์บ์์, ๊ณต์ ๋ฐ์ดํฐ๋ Redis์ ์ ์ฅํ ์ ์์ด์.
๋จ์ : ์ค์ ์ด ๋ณต์กํ๊ณ ์บ์ ์ผ๊ด์ฑ ๊ด๋ฆฌ๊ฐ ์ด๋ ค์ธ ์ ์์ด์.
์ ํฉํ ๊ฒฝ์ฐ: ๋ค์ํ ์บ์ฑ ์๊ตฌ์ฌํญ์ด ์๋ ๋ณต์กํ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ํฉํด์.
@Bean
public CacheManager cacheManager(
CaffeineCacheManager caffeineCacheManager,
RedisCacheManager redisCacheManager) {
CompositeCacheManager compositeCacheManager = new CompositeCacheManager(
caffeineCacheManager,
redisCacheManager
);
compositeCacheManager.setFallbackToNoOpCache(true);
return compositeCacheManager;
}
์บ์ ๋งค๋์ ์ ํ ๊ฐ์ด๋ ๐งญ
์ด๋ค ์บ์ ๋งค๋์ ๋ฅผ ์ ํํด์ผ ํ ์ง ๊ณ ๋ฏผ๋๋ค๋ฉด, ๋ค์ ์ง๋ฌธ๋ค์ ๊ณ ๋ คํด๋ณด์ธ์:
- ์ ํ๋ฆฌ์ผ์ด์ ์ด ๋จ์ผ ์๋ฒ์์ ์คํ๋๋์, ์๋๋ฉด ์ฌ๋ฌ ์๋ฒ์์ ์คํ๋๋์?
- ์บ์ ๋ฐ์ดํฐ๊ฐ ์๋ฒ ์ฌ์์ ํ์๋ ์ ์ง๋์ด์ผ ํ๋์?
- ์บ์ ํฌ๊ธฐ๋ ์ผ๋ง๋ ๋ ๊ฒ์ผ๋ก ์์๋๋์?
- ์บ์ ํญ๋ชฉ์ ๋ง๋ฃ ์ ์ฑ ์ด ํ์ํ๊ฐ์?
- ์บ์ ํต๊ณ๋ ๋ชจ๋ํฐ๋ง์ด ํ์ํ๊ฐ์?
๐ก ์ถ์ฒ ๊ฐ์ด๋
- ๋จ์ผ ์๋ฒ, ๊ฐ๋จํ ์ ํ๋ฆฌ์ผ์ด์ : ConcurrentMapCacheManager ๋๋ CaffeineCacheManager
- ๋จ์ผ ์๋ฒ, ๊ณ ์ฑ๋ฅ ํ์: CaffeineCacheManager
- ๋ถ์ฐ ํ๊ฒฝ, ์ฌ๋ฌ ์๋ฒ ๊ฐ ๋ฐ์ดํฐ ๊ณต์ ํ์: RedisCacheManager
- ๋์คํฌ ์ ์ฅ์ด ํ์ํ ๋์ฉ๋ ์บ์: EhCacheCacheManager
- ๋ณต์กํ ์๊ตฌ์ฌํญ, ์ฌ๋ฌ ์บ์ฑ ์ ๋ต ํ์: CompositeCacheManager
์ฌ๋ฅ๋ท์์ Java ๊ฐ๋ฐ ๊ด๋ จ ์ฌ๋ฅ์ ๊ณต์ ํ๊ฑฐ๋ ๊ตฌ๋งคํ ๋, ์ด๋ฐ ์บ์ ์ต์ ํ ๊ธฐ์ ์ ์๋ ๊ฐ๋ฐ์๋ฅผ ์ฐพ์ผ๋ฉด ํ๋ก์ ํธ ์ฑ๋ฅ์ด ํ์คํ ๋ฌ๋ผ์ง ๊ฑฐ์์! ๐
5. ์ค์ ์์ ๋ก ๋ฐฐ์ฐ๋ Spring Cache ๐ป
์ด๋ก ์ ์ถฉ๋ถํ ๋ฐฐ์ ์ผ๋ ์ด์ ์ค์ ์์ ๋ฅผ ํตํด Spring Cache๋ฅผ ์ด๋ป๊ฒ ํ์ฉํ๋์ง ์์๋ณผ๊ฒ์! ์ง์ ์ฝ๋๋ฅผ ๋ณด๋ฉด ๋ ์ฝ๊ฒ ์ดํดํ ์ ์์ ๊ฑฐ์์.
์์ 1: ์ํ ์ ๋ณด ์บ์ฑํ๊ธฐ ๐
์จ๋ผ์ธ ์ผํ๋ชฐ์์ ์ํ ์ ๋ณด๋ฅผ ์บ์ฑํ๋ ์์ ๋ฅผ ๋ง๋ค์ด๋ณผ๊ฒ์.
1. ์ํฐํฐ ํด๋์ค
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private BigDecimal price;
private int stock;
// ์์ฑ์, getter, setter ์๋ต
}
2. ๋ ํฌ์งํ ๋ฆฌ
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByPriceGreaterThan(BigDecimal price);
}
3. ์๋น์ค ํด๋์ค (์บ์ ์ ์ฉ)
@Service
@CacheConfig(cacheNames = {"products"})
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Cacheable(key = "#id")
public Product getProductById(Long id) {
System.out.println("DB์์ ์ํ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ต๋๋ค: " + id);
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("์ํ์ ์ฐพ์ ์ ์์ต๋๋ค: " + id));
}
@Cacheable(key = "'premium_' + #price")
public List<Product> getPremiumProducts(BigDecimal price) {
System.out.println("DB์์ ํ๋ฆฌ๋ฏธ์ ์ํ ๋ชฉ๋ก์ ๊ฐ์ ธ์ต๋๋ค. ๊ฐ๊ฒฉ: " + price);
return productRepository.findByPriceGreaterThan(price);
}
@CachePut(key = "#product.id")
public Product updateProduct(Product product) {
System.out.println("์ํ ์ ๋ณด๋ฅผ ์
๋ฐ์ดํธํฉ๋๋ค: " + product.getId());
return productRepository.save(product);
}
@CacheEvict(key = "#id")
public void deleteProduct(Long id) {
System.out.println("์ํ์ ์ญ์ ํฉ๋๋ค: " + id);
productRepository.deleteById(id);
}
@CacheEvict(allEntries = true)
public void clearProductCache() {
System.out.println("๋ชจ๋ ์ํ ์บ์๋ฅผ ์ง์๋๋ค");
}
}
4. ์ปจํธ๋กค๋ฌ
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productService.getProductById(id);
}
@GetMapping("/premium")
public List<Product> getPremiumProducts(@RequestParam BigDecimal minPrice) {
return productService.getPremiumProducts(minPrice);
}
@PutMapping
public Product updateProduct(@RequestBody Product product) {
return productService.updateProduct(product);
}
@DeleteMapping("/{id}")
public void deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
}
@DeleteMapping("/cache")
public void clearCache() {
productService.clearProductCache();
}
}
์ ์์ ์์ getProductById ๋ฉ์๋๋ ์ฒ์ ํธ์ถ๋ ๋๋ง DB์ ์ ๊ทผํ๊ณ , ์ดํ ๊ฐ์ ID๋ก ํธ์ถํ๋ฉด ์บ์์์ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ ธ์์. ์ฝ์ ๋ก๊ทธ๋ฅผ ๋ณด๋ฉด ํ์ธํ ์ ์์ด์!
์์ 2: ์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด ์บ์ฑํ๊ธฐ ๐
์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด๋ฅผ ์บ์ฑํด์ ๋ก๊ทธ์ธ ์ฑ๋ฅ์ ๊ฐ์ ํ๋ ์์ ๋ฅผ ์ดํด๋ณผ๊ฒ์.
@Service
public class UserAuthService {
private final UserRepository userRepository;
public UserAuthService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Cacheable(value = "userAuth", key = "#username", condition = "#username != null")
public UserDetails loadUserByUsername(String username) {
System.out.println("DB์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ต๋๋ค: " + username);
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค: " + username));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.isEnabled(),
true, true, true,
user.getAuthorities()
);
}
@CacheEvict(value = "userAuth", key = "#username")
public void invalidateUserCache(String username) {
System.out.println("์ฌ์ฉ์ ์บ์๋ฅผ ๋ฌดํจํํฉ๋๋ค: " + username);
}
// ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ ์ ์บ์ ์
๋ฐ์ดํธ
@CacheEvict(value = "userAuth", key = "#username")
public void changePassword(String username, String newPassword) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค: " + username));
user.setPassword(newPassword);
userRepository.save(user);
// ์บ์๊ฐ ์๋์ผ๋ก ๋ฌดํจํ๋ฉ๋๋ค
System.out.println("๋น๋ฐ๋ฒํธ๊ฐ ๋ณ๊ฒฝ๋์์ต๋๋ค. ์บ์๊ฐ ๋ฌดํจํ๋ฉ๋๋ค.");
}
}
์ด ์์ ์์๋ ์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด๋ฅผ ์บ์ฑํด์ ๋ก๊ทธ์ธ ์ฑ๋ฅ์ ๊ฐ์ ํ์ด์. ๋น๋ฐ๋ฒํธ๊ฐ ๋ณ๊ฒฝ๋๋ฉด ์บ์๋ฅผ ๋ฌดํจํํด์ ์ต์ ์ ๋ณด๋ฅผ ์ฌ์ฉํ๋๋ก ํ์ด์.
์์ 3: ์กฐ๊ฑด๋ถ ์บ์ฑ๊ณผ ๋์ ํค ์์ฑ ๐
์ข ๋ ๋ณต์กํ ์บ์ฑ ์๋๋ฆฌ์ค๋ฅผ ์ดํด๋ณผ๊ฒ์.
@Service
public class ReportService {
private final ReportRepository reportRepository;
public ReportService(ReportRepository reportRepository) {
this.reportRepository = reportRepository;
}
// ๊ด๋ฆฌ์๋ ํญ์ ์ต์ ๋ฐ์ดํฐ๋ฅผ ๋ด์ผ ํ๋ฏ๋ก ์บ์ฑํ์ง ์์
@Cacheable(
value = "reports",
key = "#reportType + '_' + #period",
condition = "!#isAdmin",
unless = "#result == null || #result.isEmpty()"
)
public List<ReportData> generateReport(String reportType, String period, boolean isAdmin) {
System.out.println("๋ณด๊ณ ์ ์์ฑ ์ค: " + reportType + ", ๊ธฐ๊ฐ: " + period + ", ๊ด๋ฆฌ์: " + isAdmin);
// ์ค์ ๋ก๋ ๋ณต์กํ ์ฟผ๋ฆฌ์ ๋ฐ์ดํฐ ์ฒ๋ฆฌ๊ฐ ์์ ๊ฒ์
๋๋ค
return reportRepository.generateReport(reportType, period);
}
// ๋งค์ผ ์์ ์ ์ค์ผ์ค๋ฌ๋ก ํธ์ถ๋์ด ์บ์ ๊ฐฑ์
@CacheEvict(value = "reports", allEntries = true)
@Scheduled(cron = "0 0 0 * * ?")
public void clearReportCache() {
System.out.println("๋ณด๊ณ ์ ์บ์๋ฅผ ์ด๊ธฐํํฉ๋๋ค. ์๊ฐ: " + new Date());
}
// ํน์ ์ ํ์ ๋ณด๊ณ ์๋ง ์บ์ ๊ฐฑ์
@CacheEvict(value = "reports", key = "#reportType + '_*'", allEntries = false)
public void refreshReportCache(String reportType) {
System.out.println(reportType + " ์ ํ์ ๋ณด๊ณ ์ ์บ์๋ฅผ ๊ฐฑ์ ํฉ๋๋ค.");
}
}
์ด ์์ ์์๋ ๋ค์๊ณผ ๊ฐ์ ๊ณ ๊ธ ๊ธฐ๋ฅ์ ์ฌ์ฉํ์ด์:
- ์กฐ๊ฑด๋ถ ์บ์ฑ (
condition,unless) - ๋ณตํฉ ํค ์์ฑ (
reportType + '_' + period) - ์ค์ผ์ค๋ง์ ํตํ ์บ์ ์๋ ๊ฐฑ์
- ํน์ ํจํด์ ์บ์๋ง ์ ํ์ ์ผ๋ก ๊ฐฑ์
๐ก ์ค๋ฌด ํ
์ค์ ํ๋ก์ ํธ์์๋ ๋ค์ ์ฌํญ์ ๊ณ ๋ คํ์ธ์:
- ์บ์ ํค๋ ์ถฉ๋ถํ ๊ตฌ์ฒด์ ์ด์ด์ผ ํด์ (์ถฉ๋ ๋ฐฉ์ง)
- ์บ์ ๋ง๋ฃ ์๊ฐ์ ์ ์ ํ ์ค์ ํ์ธ์ (๋ฐ์ดํฐ ์ ์ ๋ vs ์ฑ๋ฅ)
- ์บ์ ํฌ๊ธฐ ์ ํ์ ์ค์ ํด์ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ ๊ด๋ฆฌํ์ธ์
- ์กฐ๊ฑด๋ถ ์บ์ฑ์ ํ์ฉํด ๋ถํ์ํ ์บ์ฑ์ ๋ฐฉ์งํ์ธ์
- ๋ก๊น ์ ์ถ๊ฐํด์ ์บ์ ๋์์ ๋ชจ๋ํฐ๋งํ์ธ์
์ด๋ฐ ์ค์ ์์ ๋ฅผ ํตํด Spring Cache๋ฅผ ์ด๋ป๊ฒ ํ์ฉํ ์ ์๋์ง ๊ฐ์ด ์ค์๋์? ์ฌ๋ฅ๋ท์์ Java ๊ฐ๋ฐ ๊ด๋ จ ์ฌ๋ฅ์ ์ฐพ๋๋ค๋ฉด, ์ด๋ฐ ์บ์ฑ ๊ธฐ์ ์ ์ ์๋ ๊ฐ๋ฐ์๋ฅผ ๋ง๋ ์ ์์ ๊ฑฐ์์! ๐
6. ์บ์ ์ ๋ต ๋ฐ ํจํด ๐งฉ
์บ์๋ฅผ ํจ๊ณผ์ ์ผ๋ก ์ฌ์ฉํ๋ ค๋ฉด ์ ์ ํ ์ ๋ต๊ณผ ํจํด์ ์ ์ฉํด์ผ ํด์. ์ด๋ค ๋ฐ์ดํฐ๋ฅผ ์ผ๋ง๋ ์ค๋ ์บ์ฑํ ์ง, ์ด๋ป๊ฒ ๋ฌดํจํํ ์ง ๋ฑ์ ๊ณ ๋ คํด์ผ ํด์.
์ฃผ์ ์บ์ ์ ๋ต ๐
1. Cache-Aside (Lazy Loading) ํจํด
๊ฐ์ฅ ์ผ๋ฐ์ ์ธ ํจํด์ผ๋ก, ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ ๋ ๋จผ์ ์บ์๋ฅผ ํ์ธํ๊ณ , ์์ผ๋ฉด DB์์ ๊ฐ์ ธ์ ์บ์์ ์ ์ฅํด์.
์ฅ์ : ํ์ํ ๋ฐ์ดํฐ๋ง ์บ์ฑ๋๊ณ , ๊ตฌํ์ด ๊ฐ๋จํด์.
๋จ์ : ์บ์ ๋ฏธ์ค ์ ์ง์ฐ์ด ๋ฐ์ํ๊ณ , ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๊ด๋ฆฌ๊ฐ ํ์ํด์.
Spring Cache์์: @Cacheable ์ด๋
ธํ
์ด์
์ด ์ด ํจํด์ ๊ตฌํํด์.
@Cacheable(value = "users", key = "#userId")
public User getUser(Long userId) {
// ์บ์์ ์์ผ๋ฉด ์ด ์ฝ๋๊ฐ ์คํ๋จ
return userRepository.findById(userId).orElse(null);
}
2. Write-Through ํจํด
๋ฐ์ดํฐ๋ฅผ ์ธ ๋ DB์ ์บ์๋ฅผ ๋์์ ์ ๋ฐ์ดํธํด์.
์ฅ์ : ์บ์์ DB์ ์ผ๊ด์ฑ์ด ์ ์ง๋ผ์.
๋จ์ : ์ฐ๊ธฐ ์์ ์ด ์ฝ๊ฐ ๋๋ ค์ง ์ ์์ด์.
Spring Cache์์: @CachePut ์ด๋
ธํ
์ด์
์ผ๋ก ๊ตฌํํ ์ ์์ด์.
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
// DB ์
๋ฐ์ดํธ ํ ๊ฒฐ๊ณผ๊ฐ ์๋์ผ๋ก ์บ์์ ์ ์ฅ๋จ
return userRepository.save(user);
}
3. Write-Behind (Write-Back) ํจํด
๋ฐ์ดํฐ๋ฅผ ๋จผ์ ์บ์์ ์ฐ๊ณ , ๋์ค์ ๋น๋๊ธฐ์ ์ผ๋ก DB์ ๊ธฐ๋กํด์.
์ฅ์ : ์ฐ๊ธฐ ์ฑ๋ฅ์ด ํฅ์๋ผ์.
๋จ์ : ๋ฐ์ดํฐ ์์ค ์ํ์ด ์๊ณ , ๊ตฌํ์ด ๋ณต์กํด์.
Spring Cache์์: ๊ธฐ๋ณธ์ ์ผ๋ก ์ง์ํ์ง ์์ง๋ง, ๋ฉ์์ง ํ์ ํจ๊ป ๊ตฌํํ ์ ์์ด์.
// ์บ์ ์
๋ฐ์ดํธ
@CachePut(value = "users", key = "#user.id")
public User updateUserInCache(User user) {
// ๋ฉ์์ง ํ์ ์
๋ฐ์ดํธ ์ด๋ฒคํธ ๋ฐํ
messageSender.send(new UserUpdateEvent(user));
return user;
}
// ๋น๋๊ธฐ ๋ฆฌ์ค๋
@Async
@EventListener
public void handleUserUpdateEvent(UserUpdateEvent event) {
User user = event.getUser();
userRepository.save(user);
}
4. Read-Through ํจํด
์บ์ ๋ฏธ์ค ์ ์บ์ ์์ฒด๊ฐ DB์์ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํด์.
์ฅ์ : ์ ํ๋ฆฌ์ผ์ด์ ์ฝ๋๊ฐ ๋จ์ํด์ง๊ณ , ์บ์ ๋ก์ง์ด ์บก์ํ๋ผ์.
๋จ์ : ์บ์ ๊ตฌํ์ด ๋ฐ์ดํฐ ์์ค์ ์์กด์ ์ด์์.
Spring Cache์์: ์บ์ ๋งค๋์ ๊ตฌํ์ฒด์ ๋ฐ๋ผ ๋ค๋ฅด์ง๋ง, @Cacheable๊ณผ ์ ์ฌํ ๋์์ ํด์.
5. Refresh-Ahead ํจํด
์์ฃผ ์ฌ์ฉ๋๋ ํญ๋ชฉ์ ๋ง๋ฃ๋๊ธฐ ์ ์ ๋ฏธ๋ฆฌ ๊ฐฑ์ ํด์.
์ฅ์ : ์บ์ ๋ฏธ์ค๋ฅผ ์ค์ด๊ณ ์ผ๊ด๋ ์ฑ๋ฅ์ ์ ๊ณตํด์.
๋จ์ : ์์ธก ๋ก์ง์ด ํ์ํ๊ณ , ๋ถํ์ํ ๊ฐฑ์ ์ด ๋ฐ์ํ ์ ์์ด์.
Spring Cache์์: ์ค์ผ์ค๋ฌ์ ํจ๊ป ๊ตฌํํ ์ ์์ด์.
@Scheduled(fixedRate = 600000) // 10๋ถ๋ง๋ค ์คํ
public void refreshPopularItems() {
List<Long> popularItemIds = analyticsService.getTopViewedItems(100);
for (Long itemId : popularItemIds) {
// ์บ์ ๊ฐ์ ๊ฐฑ์
Item item = itemRepository.findById(itemId).orElse(null);
if (item != null) {
cacheManager.getCache("items").put(itemId, item);
}
}
}
์บ์ ๋ฌดํจํ ์ ๋ต ๐
์บ์๋ ๋ฐ์ดํฐ๊ฐ ์ค๋๋๋ฉด ๋ฌธ์ ๊ฐ ๋ ์ ์์ด์. ๋ค์์ ์บ์๋ฅผ ๋ฌดํจํํ๋ ์ฃผ์ ์ ๋ต์ด์์:
1. TTL (Time-To-Live) ๊ธฐ๋ฐ ๋ง๋ฃ
์บ์ ํญ๋ชฉ์ ๋ง๋ฃ ์๊ฐ์ ์ค์ ํด์. ๊ฐ์ฅ ๊ฐ๋จํ๊ณ ๋๋ฆฌ ์ฌ์ฉ๋๋ ๋ฐฉ๋ฒ์ด์์.
// Caffeine ์บ์์ TTL ์ค์
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES));
return cacheManager;
}
// Redis ์บ์์ TTL ์ค์
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
2. ๋ช ์์ ๋ฌดํจํ
๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋ ๋ ๊ด๋ จ ์บ์ ํญ๋ชฉ์ ๋ช ์์ ์ผ๋ก ์ ๊ฑฐํด์.
@CacheEvict(value = "users", key = "#userId")
public void updateUserProfile(Long userId, UserProfileDto profileDto) {
User user = userRepository.findById(userId).orElseThrow();
user.updateProfile(profileDto);
userRepository.save(user);
}
3. ์ด๋ฒคํธ ๊ธฐ๋ฐ ๋ฌดํจํ
๋ฐ์ดํฐ ๋ณ๊ฒฝ ์ด๋ฒคํธ๋ฅผ ๋ฐํํ๊ณ , ์ด๋ฒคํธ ๋ฆฌ์ค๋์์ ์บ์๋ฅผ ๋ฌดํจํํด์.
// ์ด๋ฒคํธ ๋ฐํ
@Transactional
public void updateProduct(Product product) {
productRepository.save(product);
eventPublisher.publishEvent(new ProductUpdatedEvent(product.getId()));
}
// ์ด๋ฒคํธ ๋ฆฌ์ค๋
@EventListener
public void handleProductUpdated(ProductUpdatedEvent event) {
cacheManager.getCache("products").evict(event.getProductId());
cacheManager.getCache("productCategories").clear();
}
4. ๋ฒ์ ๊ธฐ๋ฐ ์บ์ฑ
์บ์ ํค์ ๋ฐ์ดํฐ ๋ฒ์ ์ ํฌํจ์์ผ, ๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋๋ฉด ์ ๋ฒ์ ์ผ๋ก ์บ์ํด์.
@Cacheable(value = "products", key = "#productId + '_' + @productVersionService.getVersion(#productId)")
public Product getProduct(Long productId) {
return productRepository.findById(productId).orElse(null);
}
// ๋ฒ์ ์
๋ฐ์ดํธ
@Transactional
public void updateProduct(Product product) {
productRepository.save(product);
productVersionService.incrementVersion(product.getId());
}
์บ์ ํค ์ค๊ณ ์์น ๐
ํจ๊ณผ์ ์ธ ์บ์ ํค ์ค๊ณ๋ ์บ์ ์ฑ๋ฅ๊ณผ ๊ด๋ฆฌ์ ์ค์ํด์:
- ๊ณ ์ ์ฑ: ์๋ก ๋ค๋ฅธ ๋ฐ์ดํฐ๋ ๋ค๋ฅธ ํค๋ฅผ ๊ฐ์ ธ์ผ ํด์.
- ๊ฐ๊ฒฐ์ฑ: ํค๋ ๊ฐ๋ฅํ ์งง๊ฒ ์ ์งํ์ธ์ (๋ฉ๋ชจ๋ฆฌ ํจ์จ์ฑ).
- ์๋ฏธ์ฑ: ํค๋ ๋ด์ฉ์ ์ฝ๊ฒ ์ดํดํ ์ ์์ด์ผ ํด์ (๋๋ฒ๊น ์ฉ์ด์ฑ).
- ์ผ๊ด์ฑ: ํค ์์ฑ ๊ท์น์ ์ผ๊ด๋๊ฒ ์ ์งํ์ธ์.
- ์ถฉ๋ ๋ฐฉ์ง: ๋ค์์คํ์ด์ค๋ ์ ๋์ฌ๋ฅผ ์ฌ์ฉํด ์ถฉ๋์ ๋ฐฉ์งํ์ธ์.
์บ์ ํค ์์:
// ๋์ ์: ๋๋ฌด ์ผ๋ฐ์
@Cacheable(value = "data", key = "#id")
// ์ข์ ์: ๊ตฌ์ฒด์ ์ด๊ณ ์๋ฏธ ์์
@Cacheable(value = "users", key = "'user_' + #id")
// ๋ณตํฉ ํค ์ฌ์ฉ
@Cacheable(value = "reports", key = "#reportType + '_' + #period + '_' + #format")
// ๊ฐ์ฒด ์์ฑ ์ฌ์ฉ
@Cacheable(value = "orders", key = "#order.customerId + '_' + #order.status")
// ํด์ ํจ์ ์ฌ์ฉ (๋ณต์กํ ๊ฐ์ฒด)
@Cacheable(value = "searchResults", key = "T(org.springframework.util.DigestUtils).md5DigestAsHex(#searchCriteria.toString().getBytes())")
๐ก ์บ์ ์ ๋ต ๋ฒ ์คํธ ํ๋ํฐ์ค
- ์ฝ๊ธฐ๊ฐ ๋ง๊ณ ์ฐ๊ธฐ๊ฐ ์ ์ ๋ฐ์ดํฐ๋ฅผ ์บ์ฑํ์ธ์.
- ์์ฃผ ๋ณ๊ฒฝ๋๋ ๋ฐ์ดํฐ๋ TTL์ ์งง๊ฒ ์ค์ ํ์ธ์.
- ์บ์ ํค์ ๋ฒ์ ์ด๋ ํ์์คํฌํ๋ฅผ ํฌํจํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์.
- ์บ์ ํฌ๊ธฐ๋ฅผ ๋ชจ๋ํฐ๋งํ๊ณ ์ ํํ์ธ์.
- ์บ์ ํํธ์จ์ ์ธก์ ํ๊ณ ์ต์ ํํ์ธ์.
- ์บ์ ๊ณ์ธต์ ์ฌ์ฉํ์ธ์ (L1/L2 ์บ์).
- ์บ์ ํญ๋ฐ(Cache Stampede)์ ๋ฐฉ์งํ๊ธฐ ์ํ ์ ๋ต์ ๋ง๋ จํ์ธ์.
์ ์ ํ ์บ์ ์ ๋ต์ ์ ํํ๋ฉด ์ ํ๋ฆฌ์ผ์ด์ ์ฑ๋ฅ์ ํฌ๊ฒ ํฅ์์ํฌ ์ ์์ด์. ์ฌ๋ฅ๋ท์์ Java ๊ฐ๋ฐ์๋ฅผ ์ฐพ์ ๋, ์ด๋ฐ ์บ์ ์ ๋ต์ ์ ์๋ ๊ฐ๋ฐ์๋ฅผ ์ ํํ๋ฉด ํ๋ก์ ํธ๊ฐ ํจ์ฌ ํจ์จ์ ์ผ๋ก ์งํ๋ ๊ฑฐ์์! ๐
7. ์ฑ๋ฅ ์ธก์ ๋ฐ ๋ชจ๋ํฐ๋ง ๐
์บ์๋ฅผ ๋์ ํ๋ค๋ฉด ๊ทธ ํจ๊ณผ๋ฅผ ์ธก์ ํ๊ณ ๋ชจ๋ํฐ๋งํ๋ ๊ฒ์ด ์ค์ํด์. ์บ์๊ฐ ์ ๋ง๋ก ์ฑ๋ฅ์ ๊ฐ์ ํ๊ณ ์๋์ง, ์ด๋ค ๋ถ๋ถ์ ๋ ์ต์ ํํ ์ ์๋์ง ์์์ผ ํด์.
์บ์ ์ฑ๋ฅ ์งํ ๐
๋ค์์ ์บ์ ์ฑ๋ฅ์ ์ธก์ ํ๋ ์ฃผ์ ์งํ๋ค์ด์์:
1. ์บ์ ํํธ์จ (Hit Rate)
์ ์ฒด ์บ์ ์์ฒญ ์ค ์บ์์์ ์ฑ๊ณต์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ฐพ์ ๋น์จ์ด์์.
๊ณ์ฐ: (์บ์ ํํธ ์) / (์ ์ฒด ์บ์ ์์ฒญ ์) ร 100%
๋ชฉํ: ์ผ๋ฐ์ ์ผ๋ก 80% ์ด์์ด ์ข์ ํํธ์จ๋ก ๊ฐ์ฃผ๋ผ์.
๊ฐ์ ๋ฐฉ๋ฒ: ์บ์ ํฌ๊ธฐ ์ฆ๊ฐ, TTL ์กฐ์ , ์บ์ ํค ์ต์ ํ
2. ์๋ต ์๊ฐ (Response Time)
์์ฒญ์ ์ฒ๋ฆฌํ๋ ๋ฐ ๊ฑธ๋ฆฌ๋ ์๊ฐ์ด์์.
์ธก์ : ์บ์ ํํธ ์ ์๋ต ์๊ฐ vs ์บ์ ๋ฏธ์ค ์ ์๋ต ์๊ฐ
๋ชฉํ: ์บ์ ํํธ ์ ์๋ต ์๊ฐ์ด ๋ฏธ์ค ์๋ณด๋ค ํ์ ํ ๋นจ๋ผ์ผ ํด์.
๊ฐ์ ๋ฐฉ๋ฒ: ์บ์ ๊ตฌํ์ฒด ๋ณ๊ฒฝ, ์บ์ ๊ณ์ธตํ, ๋คํธ์ํฌ ์ง์ฐ ์ต์ํ
3. ์บ์ ํฌ๊ธฐ ๋ฐ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋
์บ์๊ฐ ์ฌ์ฉํ๋ ๋ฉ๋ชจ๋ฆฌ ์์ด์์.
์ธก์ : ํญ๋ชฉ ์, ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ (MB/GB)
๋ชฉํ: ๋ฉ๋ชจ๋ฆฌ ํ๊ณ ๋ด์์ ์ต์ ์ ํํธ์จ ๋ฌ์ฑ
๊ฐ์ ๋ฐฉ๋ฒ: ์บ์ ํฌ๊ธฐ ์ ํ, ๋ง๋ฃ ์ ์ฑ ์ ์ฉ, ํญ๋ชฉ ํฌ๊ธฐ ์ต์ ํ
4. ์บ์ ์ ์ค ์ง์ฐ ์๊ฐ (Hit Latency)
์บ์์์ ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ํ๋ ๋ฐ ๊ฑธ๋ฆฌ๋ ์๊ฐ์ด์์.
์ธก์ : ๋ฐ๋ฆฌ์ด(ms) ๋จ์๋ก ์ธก์
๋ชฉํ: ๊ฐ๋ฅํ ๋ฎ๊ฒ ์ ์ง (์ผ๋ฐ์ ์ผ๋ก < 10ms)
๊ฐ์ ๋ฐฉ๋ฒ: ๋ ๋น ๋ฅธ ์บ์ ๊ตฌํ์ฒด ์ฌ์ฉ, ์บ์ ํค ๋จ์ํ
5. ์บ์ ํ์์จ (Eviction Rate)
์บ์ ๊ณต๊ฐ ํ๋ณด๋ฅผ ์ํด ์ ๊ฑฐ๋๋ ํญ๋ชฉ์ ๋น์จ์ด์์.
์ธก์ : ๋จ์ ์๊ฐ๋น ์ ๊ฑฐ๋ ํญ๋ชฉ ์
๋ชฉํ: ๋ฎ์ ํ์์จ (๋์ ํ์์จ์ ์บ์ ํฌ๊ธฐ๊ฐ ๋ถ์กฑํจ์ ์๋ฏธ)
๊ฐ์ ๋ฐฉ๋ฒ: ์บ์ ํฌ๊ธฐ ์ฆ๊ฐ, ๋ง๋ฃ ์ ์ฑ ์ต์ ํ, ์ค์ ํญ๋ชฉ ์ฐ์ ์์ ์ง์
Spring Boot Actuator๋ก ์บ์ ๋ชจ๋ํฐ๋ง ๐
Spring Boot Actuator๋ฅผ ์ฌ์ฉํ๋ฉด ์บ์ ํต๊ณ๋ฅผ ์ฝ๊ฒ ๋ชจ๋ํฐ๋งํ ์ ์์ด์.
1. ์์กด์ฑ ์ถ๊ฐ
// Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
// Gradle
implementation 'org.springframework.boot:spring-boot-starter-actuator'
2. ์ค์ ์ถ๊ฐ (application.properties ๋๋ application.yml)
# ์บ์ ๋ฉํธ๋ฆญ ํ์ฑํ
management.endpoints.web.exposure.include=health,info,metrics,caches
management.endpoint.metrics.enabled=true
management.endpoint.caches.enabled=true
3. ์บ์ ํต๊ณ ํ์ฑํ (Caffeine ์์)
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(500)
.recordStats()); // ํต๊ณ ๊ธฐ๋ก ํ์ฑํ
return cacheManager;
}
4. ์๋ํฌ์ธํธ ์ ๊ทผ
๋ค์ URL๋ก ์บ์ ์ ๋ณด์ ์ ๊ทผํ ์ ์์ด์:
http://localhost:8080/actuator/caches- ์บ์ ๋ชฉ๋กhttp://localhost:8080/actuator/metrics/cache.size- ์บ์ ํฌ๊ธฐhttp://localhost:8080/actuator/metrics/cache.gets- ์บ์ ์์ฒญ ์http://localhost:8080/actuator/metrics/cache.puts- ์บ์ ์ฝ์ ์
- ์บ์ ์ ๊ฑฐ ์http://localhost:8080/actuator/metrics/cache.evictions
Micrometer๋ก ์บ์ ๋ฉํธ๋ฆญ ์์งํ๊ธฐ ๐
Micrometer๋ Spring Boot์ ๋ด์ฅ๋ ๋ฉํธ๋ฆญ ์์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก, ๋ค์ํ ๋ชจ๋ํฐ๋ง ์์คํ ๊ณผ ํตํฉํ ์ ์์ด์.
1. Prometheus ํตํฉ ์์
// Maven
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
// Gradle
implementation 'io.micrometer:micrometer-registry-prometheus'
2. ์ค์ ์ถ๊ฐ
# Prometheus ์๋ํฌ์ธํธ ํ์ฑํ
management.endpoints.web.exposure.include=prometheus,health,info
3. ์ปค์คํ ์บ์ ๋ฉํธ๋ฆญ ์ถ๊ฐ
@Component
public class CacheMetricsCollector {
private final MeterRegistry meterRegistry;
private final CacheManager cacheManager;
public CacheMetricsCollector(MeterRegistry meterRegistry, CacheManager cacheManager) {
this.meterRegistry = meterRegistry;
this.cacheManager = cacheManager;
// 5์ด๋ง๋ค ์บ์ ํฌ๊ธฐ ์ธก์
Gauge.builder("custom.cache.size", cacheManager,
manager -> manager.getCacheNames().stream()
.map(name -> manager.getCache(name))
.filter(Objects::nonNull)
.count())
.description("Number of caches")
.register(meterRegistry);
}
}
์ฑ๋ฅ ํ ์คํธ ๋๊ตฌ ๐ ๏ธ
์บ์ ์ฑ๋ฅ์ ์ธก์ ํ๊ธฐ ์ํ ๋๊ตฌ๋ค์ด์์:
- JMeter: ๋ถํ ํ ์คํธ ๋ฐ ์ฑ๋ฅ ์ธก์
- Gatling: ๊ณ ์ฑ๋ฅ ๋ถํ ํ ์คํธ ๋๊ตฌ
- Spring Boot Actuator: ๊ธฐ๋ณธ ๋ฉํธ๋ฆญ ์ ๊ณต
- Prometheus + Grafana: ๋ฉํธ๋ฆญ ์์ง ๋ฐ ์๊ฐํ
- Micrometer: ์ ํ๋ฆฌ์ผ์ด์ ๋ฉํธ๋ฆญ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
JMeter๋ฅผ ์ฌ์ฉํ ์บ์ ์ฑ๋ฅ ํ ์คํธ ์์
- ์บ์๊ฐ ์๋ ์ํ์์ API ์๋ํฌ์ธํธ์ ๋ํ ๋ถํ ํ ์คํธ ์คํ
- ์๋ต ์๊ฐ, ์ฒ๋ฆฌ๋ ๋ฑ์ ๊ธฐ์ค ๋ฉํธ๋ฆญ ๊ธฐ๋ก
- ์บ์๋ฅผ ํ์ฑํํ๊ณ ๋์ผํ ํ ์คํธ ์คํ
- ๊ฒฐ๊ณผ ๋น๊ต ๋ฐ ๊ฐ์ ์ ๋ ์ธก์
- ์บ์ ์ค์ (ํฌ๊ธฐ, TTL ๋ฑ)์ ์กฐ์ ํ๋ฉฐ ์ต์ ๊ตฌ์ฑ ์ฐพ๊ธฐ
์ฑ๋ฅ ์ต์ ํ ์ฒดํฌ๋ฆฌ์คํธ โ
์บ์ ์ฑ๋ฅ์ ์ต์ ํํ๊ธฐ ์ํ ์ฒดํฌ๋ฆฌ์คํธ์์:
- ์ ์ ํ ์บ์ ํฌ๊ธฐ ์ค์ (๋๋ฌด ์์ผ๋ฉด ํ์๊ฐ ์์ฃผ ๋ฐ์, ๋๋ฌด ํฌ๋ฉด ๋ฉ๋ชจ๋ฆฌ ๋ญ๋น)
- ๋ฐ์ดํฐ ํน์ฑ์ ๋ง๋ ๋ง๋ฃ ์ ์ฑ ์ ์ฉ
- ์บ์ ํค ์ค๊ณ ์ต์ ํ (๋๋ฌด ๋ณต์กํ์ง ์๊ฒ)
- ์บ์ ํต๊ณ ๋ชจ๋ํฐ๋ง ์ค์
- ์ฃผ๊ธฐ์ ์ธ ์ฑ๋ฅ ํ ์คํธ ์คํ
- ์บ์ ์๋ฐ์ ์ ๋ต ๊ตฌํ (์๋ฒ ์์ ์ ์ฃผ์ ๋ฐ์ดํฐ ๋ฏธ๋ฆฌ ๋ก๋)
- ์บ์ ๊ณ์ธตํ ๊ณ ๋ ค (L1/L2 ์บ์)
- ๋ถ์ฐ ํ๊ฒฝ์์์ ์บ์ ์ผ๊ด์ฑ ๊ด๋ฆฌ
"์ธก์ ํ ์ ์์ผ๋ฉด ๊ฐ์ ํ ์ ์๋ค." - ํผํฐ ๋๋ฌ์ปค
์บ์ ์ฑ๋ฅ์ ์ง์์ ์ผ๋ก ๋ชจ๋ํฐ๋งํ๊ณ ์ธก์ ํจ์ผ๋ก์จ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฑ๋ฅ์ ๊ณ์ ๊ฐ์ ํ ์ ์์ด์. ์ฌ๋ฅ๋ท์์ Java ๊ฐ๋ฐ์๋ฅผ ์ฐพ์ ๋, ์ฑ๋ฅ ์ต์ ํ ๊ฒฝํ์ด ์๋ ๊ฐ๋ฐ์๋ฅผ ์ ํํ๋ฉด ํฐ ๋์์ด ๋ ๊ฑฐ์์! ๐
8. ์ฃผ์์ฌํญ ๋ฐ ๋ฒ ์คํธ ํ๋ํฐ์ค โ ๏ธ
์บ์๋ ๊ฐ๋ ฅํ ๋๊ตฌ์ง๋ง, ์๋ชป ์ฌ์ฉํ๋ฉด ์คํ๋ ค ๋ฌธ์ ๋ฅผ ์ผ์ผํฌ ์ ์์ด์. ์ฌ๊ธฐ์๋ Spring Cache ์ฌ์ฉ ์ ์ฃผ์ํด์ผ ํ ์ ๊ณผ ๋ฒ ์คํธ ํ๋ํฐ์ค๋ฅผ ์์๋ณผ๊ฒ์.
์ฃผ์์ฌํญ ๐จ
1. ์บ์ ์ผ๊ด์ฑ ๋ฌธ์
์บ์๋ ๋ฐ์ดํฐ์ ์ค์ ๋ฐ์ดํฐ ๊ฐ์ ๋ถ์ผ์น๊ฐ ๋ฐ์ํ ์ ์์ด์.
์ํ: ์ฌ์ฉ์์๊ฒ ์ค๋๋ ๋ฐ์ดํฐ๊ฐ ํ์๋๊ฑฐ๋, ์ค์ํ ์ ๋ฐ์ดํธ๊ฐ ๋๋ฝ๋ ์ ์์ด์.
ํด๊ฒฐ์ฑ :
- ์ ์ ํ TTL ์ค์
- ๋ฐ์ดํฐ ๋ณ๊ฒฝ ์ ๊ด๋ จ ์บ์ ํญ๋ชฉ ๋ช ์์ ๋ฌดํจํ
- ์ด๋ฒคํธ ๊ธฐ๋ฐ ์บ์ ๋ฌดํจํ ๊ตฌํ
// ๋ฐ์ดํฐ ๋ณ๊ฒฝ ์ ์บ์ ๋ฌดํจํ
@CacheEvict(value = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
2. ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ฆ๊ฐ
๊ณผ๋ํ ์บ์ฑ์ ๋ฉ๋ชจ๋ฆฌ ๋ถ์กฑ ๋ฌธ์ ๋ฅผ ์ผ์ผํฌ ์ ์์ด์.
์ํ: OutOfMemoryError ๋ฐ์, ์ ํ๋ฆฌ์ผ์ด์ ์ฑ๋ฅ ์ ํ
ํด๊ฒฐ์ฑ :
- ์บ์ ํฌ๊ธฐ ์ ํ ์ค์
- ์ ์ ํ ์บ์ ์ ๊ฑฐ ์ ์ฑ ์ฌ์ฉ
- ์บ์ํ ๋ฐ์ดํฐ ์ ๋ณ
// Caffeine ์บ์ ํฌ๊ธฐ ์ ํ
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)); // ์ต๋ 1000๊ฐ ํญ๋ชฉ์ผ๋ก ์ ํ
return cacheManager;
}
3. ์บ์ ํญ๋ฐ (Cache Stampede)
๋ง์ ์บ์ ํญ๋ชฉ์ด ๋์์ ๋ง๋ฃ๋์ด DB์ ๊ฐ์์ค๋ฌ์ด ๋ถํ๊ฐ ๋ฐ์ํ๋ ํ์์ด์์.
์ํ: ์๋ฒ ๊ณผ๋ถํ, ์๋ต ์๊ฐ ๊ธ์ฆ
ํด๊ฒฐ์ฑ :
- ์บ์ ๋ง๋ฃ ์๊ฐ์ ์ฝ๊ฐ์ ๋ฌด์์์ฑ ์ถ๊ฐ
- ๋ฐฑ๊ทธ๋ผ์ด๋ ๊ฐฑ์ ๊ตฌํ
- ์บ์ ๋ฝ(Lock) ๋ฉ์ปค๋์ฆ ์ฌ์ฉ
// ๋ง๋ฃ ์๊ฐ์ ๋ฌด์์์ฑ ์ถ๊ฐ
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(duration -> {
// ๊ธฐ๋ณธ 10๋ถ + ์ต๋ 2๋ถ์ ๋ฌด์์ ์๊ฐ ์ถ๊ฐ
return Duration.ofMinutes(10)
.plusSeconds(ThreadLocalRandom.current().nextLong(120));
}));
return cacheManager;
}
4. ์บ์ ์ค์ผ (Cache Pollution)
๊ฑฐ์ ์ฌ์ฉ๋์ง ์๋ ๋ฐ์ดํฐ๊ฐ ์บ์๋ฅผ ์ฐจ์งํ๋ ํ์์ด์์.
์ํ: ์บ์ ํจ์จ์ฑ ์ ํ, ์ค์ ๋ฐ์ดํฐ ์ ๊ฑฐ
ํด๊ฒฐ์ฑ :
- LRU(Least Recently Used) ๋๋ LFU(Least Frequently Used) ์ ๊ฑฐ ์ ์ฑ ์ฌ์ฉ
- ์ ์ ํ ์บ์ ํค ์ค๊ณ
- ์์ฃผ ์ฌ์ฉ๋๋ ๋ฐ์ดํฐ๋ง ์ ๋ณ์ ์ผ๋ก ์บ์ฑ
5. ๋ถ์ฐ ํ๊ฒฝ์์์ ๋ฌธ์
์ฌ๋ฌ ์๋ฒ ์ธ์คํด์ค๊ฐ ๊ฐ์์ ๋ก์ปฌ ์บ์๋ฅผ ๊ฐ์ง ๋ ๋ฐ์ํ๋ ๋ฌธ์ ์์.
์ํ: ์๋ฒ ๊ฐ ๋ฐ์ดํฐ ๋ถ์ผ์น, ์บ์ ํจ์จ์ฑ ์ ํ
ํด๊ฒฐ์ฑ :
- Redis์ ๊ฐ์ ๋ถ์ฐ ์บ์ ์ฌ์ฉ
- ์บ์ ๋ฌดํจํ ์ด๋ฒคํธ๋ฅผ ๋ชจ๋ ์๋ฒ์ ์ ํ
- ์ธ์ ์ดํผ๋ํฐ(Sticky Session) ๊ตฌํ
// Redis ์บ์ ๋งค๋์ ์ค์
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig())
.build();
}
๋ฒ ์คํธ ํ๋ํฐ์ค ๐
1. ์ ์ ํ ์บ์ ๋์ ์ ํ
๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ์บ์ฑํ๋ ๊ฒ์ ๋นํจ์จ์ ์ด์์. ๋ค์ ํน์ฑ์ ๊ฐ์ง ๋ฐ์ดํฐ๋ฅผ ์บ์ฑํ์ธ์:
- ์ฝ๊ธฐ๊ฐ ๋ง๊ณ ์ฐ๊ธฐ๊ฐ ์ ์ ๋ฐ์ดํฐ
- ๊ณ์ฐ ๋น์ฉ์ด ๋์ ๋ฐ์ดํฐ
- ์์ฃผ ์ ๊ทผํ๋ ๋ฐ์ดํฐ
- ํฌ๊ธฐ๊ฐ ์ ๋นํ ๋ฐ์ดํฐ
์: ์ํ ์นดํ๋ก๊ทธ, ์ฌ์ฉ์ ํ๋กํ, ์ค์ ์ ๋ณด ๋ฑ
2. ์บ์ ๊ณ์ธตํ
์ฌ๋ฌ ๋ ๋ฒจ์ ์บ์๋ฅผ ์ฌ์ฉํด ์ฑ๋ฅ๊ณผ ํ์ฅ์ฑ์ ๊ฐ์ ํ์ธ์:
- L1 ์บ์: ์ธ๋ฉ๋ชจ๋ฆฌ ๋ก์ปฌ ์บ์ (Caffeine)
- L2 ์บ์: ๋ถ์ฐ ์บ์ (Redis)
@Component
public class TwoLevelCacheService {
private final CaffeineCacheManager localCacheManager;
private final RedisCacheManager distributedCacheManager;
// ๋จผ์ ๋ก์ปฌ ์บ์ ํ์ธ, ์์ผ๋ฉด ๋ถ์ฐ ์บ์ ํ์ธ, ๋ ๋ค ์์ผ๋ฉด DB ์กฐํ
public User getUser(Long userId) {
String cacheKey = "user_" + userId;
// L1 ์บ์ ํ์ธ
Cache localCache = localCacheManager.getCache("users");
User user = localCache.get(cacheKey, User.class);
if (user != null) {
return user;
}
// L2 ์บ์ ํ์ธ
Cache distributedCache = distributedCacheManager.getCache("users");
user = distributedCache.get(cacheKey, User.class);
if (user != null) {
// L1 ์บ์์๋ ์ ์ฅ
localCache.put(cacheKey, user);
return user;
}
// DB ์กฐํ
user = userRepository.findById(userId).orElse(null);
if (user != null) {
// ์์ชฝ ์บ์์ ๋ชจ๋ ์ ์ฅ
localCache.put(cacheKey, user);
distributedCache.put(cacheKey, user);
}
return user;
}
}
3. ์บ์ ํค ์ค๊ณ
ํจ๊ณผ์ ์ธ ์บ์ ํค ์ค๊ณ ์์น:
- ๋ช ํํ ๋ค์ด๋ฐ ๊ท์น ์ฌ์ฉ
- ์ถฉ๋ถํ ์ ๋ณด ํฌํจ (์ถฉ๋ ๋ฐฉ์ง)
- ๋๋ฌด ๋ณต์กํ์ง ์๊ฒ ์ ์ง
- ํ์์ ํด์ ํจ์ ์ฌ์ฉ
// ์ข์ ์บ์ ํค ์์
@Cacheable(value = "products", key = "'product_' + #id + '_' + #language")
public ProductDto getProductDetails(Long id, String language) {
// ...
}
4. ์บ์ ์๋ฐ์
์ ํ๋ฆฌ์ผ์ด์ ์์ ์ ์ฃผ์ ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ์บ์์ ๋ก๋ํด ์ด๊ธฐ ์ฑ๋ฅ ์ ํ๋ฅผ ๋ฐฉ์งํ์ธ์:
@Component
public class CacheWarmer {
private final ProductService productService;
@EventListener(ApplicationReadyEvent.class)
public void warmUpCaches() {
log.info("์บ์ ์๋ฐ์
์์...");
// ์ธ๊ธฐ ์ํ ๋ฏธ๋ฆฌ ์บ์ฑ
List<Long> popularProductIds = getPopularProductIds();
for (Long id : popularProductIds) {
productService.getProductById(id);
}
log.info("์บ์ ์๋ฐ์
์๋ฃ!");
}
private List<Long> getPopularProductIds() {
// ์ธ๊ธฐ ์ํ ID ๋ชฉ๋ก ๋ฐํ ๋ก์ง
return List.of(1L, 2L, 3L, 4L, 5L);
}
}
5. ์บ์ ๋ชจ๋ํฐ๋ง ๋ฐ ํ๋
์บ์ ์ฑ๋ฅ์ ์ง์์ ์ผ๋ก ๋ชจ๋ํฐ๋งํ๊ณ ์ต์ ํํ์ธ์:
- ํํธ์จ, ๋ฏธ์ค์จ ๋ชจ๋ํฐ๋ง
- ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ถ์
- ์บ์ ๋ง๋ฃ ์ ์ฑ ํ๋
- ์ฃผ๊ธฐ์ ์ธ ์ฑ๋ฅ ํ ์คํธ ์คํ
@Scheduled(fixedRate = 3600000) // 1์๊ฐ๋ง๋ค ์คํ
public void logCacheStatistics() {
Cache usersCache = cacheManager.getCache("users");
if (usersCache instanceof CaffeineCache) {
CaffeineCache caffeineCache = (CaffeineCache) usersCache;
com.github.benmanes.caffeine.cache.Cache<Object, Object> nativeCache =
caffeineCache.getNativeCache();
CacheStats stats = nativeCache.stats();
log.info("์บ์ ํต๊ณ - ํํธ์จ: {}%, ์์ฒญ ์: {}, ์ ์ฌ ์: {}, ์ ๊ฑฐ ์: {}",
stats.hitRate() * 100,
stats.requestCount(),
stats.loadCount(),
stats.evictionCount());
}
}
์ค๋ฌด์์ ์์ฃผ ํ๋ ์ค์ ๐ โโ๏ธ
- ๋ชจ๋ ๊ฒ์ ์บ์ฑํ๋ ค๋ ์๋: ์บ์ฑ์ ์ ๋ณ์ ์ผ๋ก ํด์ผ ํจ๊ณผ์ ์ด์์.
- ์บ์ ๋ง๋ฃ ์ ์ฑ ๋ถ์ฌ: ์ ์ ํ TTL ์์ด ์บ์๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํด์.
- ์บ์ ํฌ๊ธฐ ์ ํ ์์: ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ ์ ํํ์ง ์์ผ๋ฉด OutOfMemoryError๊ฐ ๋ฐ์ํ ์ ์์ด์.
- ์บ์ ํค ์ถฉ๋: ๋๋ฌด ๋จ์ํ ํค๋ฅผ ์ฌ์ฉํ๋ฉด ๋ค๋ฅธ ๋ฐ์ดํฐ๊ฐ ์๋ก์ ์บ์๋ฅผ ๋ฎ์ด์ธ ์ ์์ด์.
- ์บ์ ๋ชจ๋ํฐ๋ง ๋ถ์ฌ: ์บ์ ์ฑ๋ฅ์ ๋ชจ๋ํฐ๋งํ์ง ์์ผ๋ฉด ๋ฌธ์ ๋ฅผ ์กฐ๊ธฐ์ ๋ฐ๊ฒฌํ ์ ์์ด์.
- ์บ์ ๋ฌดํจํ ๋ก์ง ๋๋ฝ: ๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋ ๋ ์บ์๋ฅผ ๊ฐฑ์ ํ์ง ์์ผ๋ฉด ์ค๋๋ ๋ฐ์ดํฐ๊ฐ ํ์๋ผ์.
- ์ง๋ ฌํ/์ญ์ง๋ ฌํ ๋น์ฉ ๋ฌด์: ๋ณต์กํ ๊ฐ์ฒด๋ฅผ ์บ์ฑํ ๋ ์ด ๋น์ฉ์ ๊ณ ๋ คํด์ผ ํด์.
๐ก ์บ์ ๋๋ฒ๊น ํ
์บ์ ๊ด๋ จ ๋ฌธ์ ๋ฅผ ๋๋ฒ๊น ํ ๋ ๋์์ด ๋๋ ํ์ด์์:
- ๋ก๊น ์ถ๊ฐ: ์บ์ ํํธ/๋ฏธ์ค๋ฅผ ๋ก๊น ํด ๋์ ํ์ธ
- ์บ์ ๋นํ์ฑํ ํ ์คํธ: ๋ฌธ์ ๊ฐ ์บ์ ๋๋ฌธ์ธ์ง ํ์ธ
- ์บ์ ๋ด์ฉ ๊ฒ์ฌ: ์บ์ ๋งค๋์ ๋ฅผ ํตํด ํ์ฌ ์บ์ ์ํ ํ์ธ
- ์บ์ ํค ๊ฒ์ฆ: ์๋ํ ๋๋ก ํค๊ฐ ์์ฑ๋๋์ง ํ์ธ
- ์กฐ๊ฑด๋ถ ์บ์ฑ ๊ฒ์ฌ: condition, unless ํํ์์ด ์์๋๋ก ๋์ํ๋์ง ํ์ธ
// ๋๋ฒ๊น
์ ์ํ ์บ์ ๋ด์ฉ ์ถ๋ ฅ
@Scheduled(fixedRate = 60000)
public void debugCache() {
Cache usersCache = cacheManager.getCache("users");
if (usersCache != null && usersCache instanceof ConcurrentMapCache) {
ConcurrentMapCache mapCache = (ConcurrentMapCache) usersCache;
Map<Object, Object> nativeCache = mapCache.getNativeCache();
log.debug("ํ์ฌ users ์บ์ ๋ด์ฉ:");
nativeCache.forEach((k, v) -> log.debug("ํค: {}, ๊ฐ: {}", k, v));
}
}
์ด๋ฐ ์ฃผ์์ฌํญ๊ณผ ๋ฒ ์คํธ ํ๋ํฐ์ค๋ฅผ ๋ฐ๋ฅด๋ฉด Spring Cache๋ฅผ ํจ๊ณผ์ ์ผ๋ก ํ์ฉํ ์ ์์ด์. ์ฌ๋ฅ๋ท์์ Java ๊ฐ๋ฐ์๋ฅผ ๊ตฌํ์ค ๋, ์ด๋ฐ ์บ์ฑ ๋ ธํ์ฐ๋ฅผ ๊ฐ์ง ๊ฐ๋ฐ์๋ฅผ ์ฐพ์ผ๋ฉด ํ๋ก์ ํธ ํ์ง์ด ํ์ธต ๋์์ง ๊ฑฐ์์! ๐
9. ์ค๋ฌด์์ ์์ฃผ ๋ฐ์ํ๋ ๋ฌธ์ ํด๊ฒฐํ๊ธฐ ๐ง
์ด๋ก ์ ์ด์ ์ถฉ๋ถํ ๋ฐฐ์ ์ผ๋, ์ค๋ฌด์์ ์์ฃผ ๋ง์ฃผ์น๋ ๋ฌธ์ ๋ค๊ณผ ๊ทธ ํด๊ฒฐ์ฑ ์ ์์๋ณผ๊ฒ์! ์ค์ ๊ฐ๋ฐ ํ์ฅ์์ ๊ฒช์ ์ ์๋ ์บ์ ๊ด๋ จ ๋ฌธ์ ๋ค์ด์์.
๋ฌธ์ 1: ์บ์๊ฐ ์ ๋๋ก ๋์ํ์ง ์์์ ๐ค
์ฆ์:
@Cacheable ์ด๋ ธํ ์ด์ ์ ์ถ๊ฐํ๋๋ฐ, ๋ฉ์๋๊ฐ ๊ณ์ ์คํ๋๊ณ ์บ์๊ฐ ๋์ํ์ง ์์์.
๊ฐ๋ฅํ ์์ธ:
- @EnableCaching ์ด๋ ธํ ์ด์ ๋๋ฝ
- ์์ฒด ํธ์ถ(self-invocation) ๋ฌธ์
- ์บ์ ํค ์์ฑ ์ค๋ฅ
- ํ๋ก์ ๊ด๋ จ ๋ฌธ์
ํด๊ฒฐ์ฑ :
// 1. @EnableCaching ํ์ธ
@SpringBootApplication
@EnableCaching // ์ด ์ด๋
ธํ
์ด์
์ด ์๋์ง ํ์ธ!
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
// 2. ์์ฒด ํธ์ถ ๋ฌธ์ ํด๊ฒฐ
@Service
public class ProductService {
@Autowired
private ApplicationContext context;
@Cacheable("products")
public Product getProduct(Long id) {
// ์บ์ ์ ์ฉ๋จ
return findProductById(id);
}
public Product updateAndGet(Long id) {
// ๋ฌธ์ : ์ง์ ํธ์ถํ๋ฉด ์บ์๊ฐ ๋์ํ์ง ์์
// return getProduct(id); // โ
// ํด๊ฒฐ: ํ๋ก์๋ฅผ ํตํด ํธ์ถ
ProductService proxy = context.getBean(ProductService.class);
return proxy.getProduct(id); // โ
}
private Product findProductById(Long id) {
// DB ์กฐํ ๋ก์ง
}
}
// 3. ์บ์ ํค ๋๋ฒ๊น
@Cacheable(value = "products", key = "#id", condition = "#id > 0")
public Product getProduct(Long id) {
System.out.println("์บ์ ํค: " + id); // ํค ๊ฐ ํ์ธ
return productRepository.findById(id).orElse(null);
}
๋ฌธ์ 2: ํธ๋์ญ์ ๊ณผ ์บ์ ๋์ ์์ ๋ฌธ์ ๐
์ฆ์:
ํธ๋์ญ์ ๋ด์์ ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ๊ณ ์บ์๋ฅผ ์ ๋ฐ์ดํธํ๋๋ฐ, ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋๋ฉด ์บ์์ DB๊ฐ ๋ถ์ผ์นํด์.
๊ฐ๋ฅํ ์์ธ:
์บ์ ์์ ์ด ํธ๋์ญ์ ๋ณด๋ค ๋จผ์ ์คํ๋์ด ํธ๋์ญ์ ๋กค๋ฐฑ ์ ์บ์๋ ์ ๋ฐ์ดํธ๋ ์ํ๋ก ๋จ์์์ด์.
ํด๊ฒฐ์ฑ :
// ๋ฌธ์ ๊ฐ ์๋ ์ฝ๋
@Transactional
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
// ํธ๋์ญ์
์ด ๋กค๋ฐฑ๋๋ฉด DB๋ ์๋๋๋ก ๋์๊ฐ์ง๋ง
// ์บ์๋ ์ด๋ฏธ ์
๋ฐ์ดํธ๋ ์ํ๋ก ๋จ์์์
return userRepository.save(user);
}
// ํด๊ฒฐ์ฑ
1: @Caching ์ด๋
ธํ
์ด์
๊ณผ ํธ๋์ญ์
๋ถ๋ฆฌ
@Transactional
public User updateUserInTransaction(User user) {
return userRepository.save(user);
}
@CachePut(value = "users", key = "#result.id")
public User updateUser(User user) {
try {
return updateUserInTransaction(user);
} catch (Exception e) {
// ํธ๋์ญ์
์คํจ ์ ์บ์ ์
๋ฐ์ดํธ ์ ํจ
throw e;
}
}
// ํด๊ฒฐ์ฑ
2: TransactionSynchronizationManager ์ฌ์ฉ
@Transactional
public User updateUser(User user) {
User savedUser = userRepository.save(user);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
// ํธ๋์ญ์
์ด ์ฑ๊ณต์ ์ผ๋ก ์ปค๋ฐ๋ ํ์๋ง ์บ์ ์
๋ฐ์ดํธ
cacheManager.getCache("users").put(user.getId(), savedUser);
}
});
return savedUser;
}
๋ฌธ์ 3: ์บ์๋ ๊ฐ์ฒด ์์ ์ ๋ถ์์ฉ ๐ฑ
์ฆ์:
์บ์์์ ๊ฐ์ ธ์จ ๊ฐ์ฒด๋ฅผ ์์ ํ๋๋ ๋ค๋ฅธ ์์ฒญ์๋ ์ํฅ์ ๋ฏธ์ณ์.
๊ฐ๋ฅํ ์์ธ:
์บ์๋ ๊ฐ์ฒด ์ฐธ์กฐ๋ฅผ ์ ์ฅํ๋ฏ๋ก, ์บ์๋ ๊ฐ์ฒด๋ฅผ ์์ ํ๋ฉด ์บ์์ ๋ด์ฉ๋ ๋ณ๊ฒฝ๋ผ์.
ํด๊ฒฐ์ฑ :
// ๋ฌธ์ ๊ฐ ์๋ ์ฝ๋
@Cacheable("products")
public Product getProduct(Long id) {
return productRepository.findById(id).orElse(null);
}
// ํด๋ผ์ด์ธํธ ์ฝ๋
Product product = productService.getProduct(123L);
product.setPrice(product.getPrice() * 0.9); // ํ ์ธ ์ ์ฉ
// ์ด์ ๋ค๋ฅธ ๋ชจ๋ ์์ฒญ๋ ํ ์ธ๋ ๊ฐ๊ฒฉ์ ๋ณด๊ฒ ๋จ! ๐ฑ
// ํด๊ฒฐ์ฑ
1: ๋ฐฉ์ด์ ๋ณต์ฌ
@Cacheable("products")
public Product getProduct(Long id) {
Product product = productRepository.findById(id).orElse(null);
if (product != null) {
// ๋ณต์ฌ๋ณธ ๋ฐํ
return new Product(product); // ๋ณต์ฌ ์์ฑ์ ์ฌ์ฉ
}
return null;
}
// ํด๊ฒฐ์ฑ
2: ๋ถ๋ณ ๊ฐ์ฒด ์ฌ์ฉ
@Cacheable("products")
public ProductDto getProduct(Long id) {
Product product = productRepository.findById(id).orElse(null);
if (product != null) {
// ๋ถ๋ณ DTO ๋ฐํ
return new ProductDto(product);
}
return null;
}
// ํด๊ฒฐ์ฑ
3: Jackson์ ์ฌ์ฉํ ์ง๋ ฌํ/์ญ์ง๋ ฌํ
@Bean
public CacheManager cacheManager(ObjectMapper objectMapper) {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES));
// Jackson์ ์ฌ์ฉํ ์ง๋ ฌํ/์ญ์ง๋ ฌํ๋ก ๊น์ ๋ณต์ฌ ๊ตฌํ
cacheManager.setAllowNullValues(true);
cacheManager.setCacheLoader(new CacheLoader<Object, Object>() {
@Override
public Object load(Object key) {
return null;
}
@Override
public Object reload(Object key, Object oldValue) {
try {
// ๊น์ ๋ณต์ฌ ๊ตฌํ
String json = objectMapper.writeValueAsString(oldValue);
return objectMapper.readValue(json, oldValue.getClass());
} catch (Exception e) {
return oldValue;
}
}
});
return cacheManager;
}
๋ฌธ์ 4: ์บ์ ํฌ๊ธฐ๊ฐ ๋๋ฌด ์ปค์ ธ์ ๐
์ฆ์:
์๊ฐ์ด ์ง๋ ์๋ก ์ ํ๋ฆฌ์ผ์ด์ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ด ๊ณ์ ์ฆ๊ฐํด์.
๊ฐ๋ฅํ ์์ธ:
์บ์ ํฌ๊ธฐ ์ ํ์ด ์๊ฑฐ๋, ์บ์ ํญ๋ชฉ์ด ๋๋ฌด ํฐ ๊ฒฝ์ฐ์์.
ํด๊ฒฐ์ฑ :
// ํด๊ฒฐ์ฑ
1: ์บ์ ํฌ๊ธฐ ์ ํ
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000) // ์ต๋ 1000๊ฐ ํญ๋ชฉ์ผ๋ก ์ ํ
.expireAfterWrite(1, TimeUnit.HOURS)); // 1์๊ฐ ํ ๋ง๋ฃ
return cacheManager;
}
// ํด๊ฒฐ์ฑ
2: ํฐ ๊ฐ์ฒด๋ ๋ณ๋ ์ฒ๋ฆฌ
@Cacheable(value = "largeObjects", key = "#id", condition = "#result != null && #result.size() < 1024")
public byte[] getLargeObject(String id) {
// ๊ฒฐ๊ณผ๊ฐ ๋๋ฌด ํฌ๋ฉด ์บ์ํ์ง ์์
return largeObjectRepository.findById(id);
}
// ํด๊ฒฐ์ฑ
3: ์บ์ ํต๊ณ ๋ชจ๋ํฐ๋ง
@Scheduled(fixedRate = 300000) // 5๋ถ๋ง๋ค ์คํ
public void monitorCacheSize() {
Cache usersCache = cacheManager.getCache("users");
if (usersCache instanceof CaffeineCache) {
CaffeineCache caffeineCache = (CaffeineCache) usersCache;
com.github.benmanes.caffeine.cache.Cache<Object, Object> nativeCache =
caffeineCache.getNativeCache();
long estimatedSize = nativeCache.estimatedSize();
log.info("ํ์ฌ users ์บ์ ํฌ๊ธฐ: {} ํญ๋ชฉ", estimatedSize);
if (estimatedSize > 10000) {
log.warn("์บ์ ํฌ๊ธฐ๊ฐ ๋๋ฌด ํฝ๋๋ค! ์ผ๋ถ ํญ๋ชฉ์ ์ ๊ฑฐํฉ๋๋ค.");
nativeCache.cleanUp(); // ๋ง๋ฃ๋ ํญ๋ชฉ ์ ๋ฆฌ
}
}
}
๋ฌธ์ 5: ์บ์์ ๋น๋๊ธฐ ์ฒ๋ฆฌ ๐
์ฆ์:
@Async์ @Cacheable์ ํจ๊ป ์ฌ์ฉํ๋๋ฐ ์บ์๊ฐ ์ ๋๋ก ๋์ํ์ง ์์์.
๊ฐ๋ฅํ ์์ธ:
@Async ๋ฉ์๋๋ ๋ณ๋์ ํ๋ก์๋ฅผ ํตํด ์คํ๋๋ฏ๋ก ์บ์ ํ๋ก์์ ์ถฉ๋ํ ์ ์์ด์.
ํด๊ฒฐ์ฑ :
// ๋ฌธ์ ๊ฐ ์๋ ์ฝ๋
@Async
@Cacheable("asyncData")
public CompletableFuture<Data> getDataAsync(String key) {
// ๋น๋๊ธฐ + ์บ์ ์กฐํฉ์ด ์ ๋๋ก ๋์ํ์ง ์์ ์ ์์
return CompletableFuture.supplyAsync(() -> dataService.fetchData(key));
}
// ํด๊ฒฐ์ฑ
1: ๋ฉ์๋ ๋ถ๋ฆฌ
@Cacheable("data")
public Data getData(String key) {
return dataService.fetchData(key);
}
@Async
public CompletableFuture<Data> getDataAsync(String key) {
// ์บ์ ๊ธฐ๋ฅ์ด ์๋ ๋๊ธฐ ๋ฉ์๋ ํธ์ถ
return CompletableFuture.completedFuture(getData(key));
}
// ํด๊ฒฐ์ฑ
2: ์๋ ์บ์ ๊ด๋ฆฌ
@Async
public CompletableFuture<Data> getDataAsync(String key) {
Cache cache = cacheManager.getCache("asyncData");
Data cachedData = cache.get(key, Data.class);
if (cachedData != null) {
return CompletableFuture.completedFuture(cachedData);
}
return CompletableFuture.supplyAsync(() -> {
Data newData = dataService.fetchData(key);
cache.put(key, newData);
return newData;
});
}
๋ฌธ์ 6: ์บ์ ์ง๋ ฌํ ์ค๋ฅ ๐ฆ
์ฆ์:
Redis ์บ์๋ฅผ ์ฌ์ฉํ ๋ "Cannot serialize" ์์ธ๊ฐ ๋ฐ์ํด์.
๊ฐ๋ฅํ ์์ธ:
์บ์์ ์ ์ฅํ๋ ค๋ ๊ฐ์ฒด๊ฐ ์ง๋ ฌํ ๊ฐ๋ฅํ์ง ์์์.
ํด๊ฒฐ์ฑ :
// ํด๊ฒฐ์ฑ
1: ์ง๋ ฌํ ๊ฐ๋ฅํ๊ฒ ๋ง๋ค๊ธฐ
@Entity
public class Product implements Serializable { // Serializable ๊ตฌํ
private static final long serialVersionUID = 1L;
// ํ๋, ๋ฉ์๋ ๋ฑ
}
// ํด๊ฒฐ์ฑ
2: JSON ์ง๋ ฌํ ์ฌ์ฉ (Redis)
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// Jackson2JsonRedisSerializer ์ฌ์ฉ
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(objectMapper);
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(serializer))
.entryTtl(Duration.ofMinutes(30));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
// ํด๊ฒฐ์ฑ
3: DTO ์ฌ์ฉ
@Cacheable("products")
public ProductDto getProduct(Long id) {
Product product = productRepository.findById(id).orElse(null);
// Entity ๋์ ์ง๋ ฌํ ๊ฐ๋ฅํ DTO ๋ฐํ
return product != null ? new ProductDto(product) : null;
}
๋ฌธ์ 7: ์กฐ๊ฑด๋ถ ์บ์ฑ์ด ์์๋๋ก ๋์ํ์ง ์์์ ๐คจ
์ฆ์:
condition์ด๋ unless ์์ฑ์ด ์์๋๋ก ๋์ํ์ง ์์์.
๊ฐ๋ฅํ ์์ธ:
SpEL ํํ์ ์ค๋ฅ๋ ์ปจํ ์คํธ ๋ฌธ์ ์ผ ์ ์์ด์.
ํด๊ฒฐ์ฑ :
// ๋ฌธ์ ๊ฐ ์๋ ์ฝ๋
@Cacheable(value = "users", key = "#id", condition = "#user.admin") // โ #user ๋ณ์ ์์
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
// ํด๊ฒฐ์ฑ
1: ์ฌ๋ฐ๋ฅธ ๋ณ์ ์ฐธ์กฐ
@Cacheable(value = "users", key = "#id", condition = "#id > 0") // โ
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
// ํด๊ฒฐ์ฑ
2: unless ์ฌ์ฉ ์ ์ฃผ์
@Cacheable(value = "users", key = "#id", unless = "#result == null || #result.admin")
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
// ํด๊ฒฐ์ฑ
3: ๋๋ฒ๊น
์ ์ํ ๋ก๊น
์ถ๊ฐ
@Cacheable(
value = "users",
key = "#id",
condition = "#id > 0",
unless = "#result == null || #result.admin"
)
public User getUser(Long id) {
System.out.println("์กฐ๊ฑด ํ๊ฐ: id > 0 = " + (id > 0));
User user = userRepository.findById(id).orElse(null);
if (user != null) {
System.out.println("unless ํ๊ฐ: result.admin = " + user.isAdmin());
}
return user;
}
๋ฌธ์ 8: ์บ์ ๋ฌดํจํ๊ฐ ์ ๋๋ก ๋์ํ์ง ์์์ ๐
์ฆ์:
@CacheEvict๋ฅผ ํธ์ถํ๋๋ฐ ์บ์๊ฐ ์ฌ์ ํ ์ด์ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํด์.
๊ฐ๋ฅํ ์์ธ:
์บ์ ํค๊ฐ ์ผ์นํ์ง ์๊ฑฐ๋, ๋ค๋ฅธ ์บ์ ์ ์ฅ์๋ฅผ ์ฌ์ฉํ๊ณ ์์ ์ ์์ด์.
ํด๊ฒฐ์ฑ :
// ๋ฌธ์ ๊ฐ ์๋ ์ฝ๋
@Cacheable(value = "users", key = "#userId")
public User getUser(Long userId) {
return userRepository.findById(userId).orElse(null);
}
@CacheEvict(value = "users", key = "#user.id") // ํค ๋ถ์ผ์น ๊ฐ๋ฅ์ฑ
public void updateUser(User user) {
userRepository.save(user);
}
// ํด๊ฒฐ์ฑ
1: ํค ์ผ๊ด์ฑ ์ ์ง
@Cacheable(value = "users", key = "#userId")
public User getUser(Long userId) {
return userRepository.findById(userId).orElse(null);
}
@CacheEvict(value = "users", key = "#user.id")
public void updateUser(User user) {
// ๋ก๊น
์ถ๊ฐ
System.out.println("์บ์ ๋ฌดํจํ: users::" + user.getId());
userRepository.save(user);
}
// ํด๊ฒฐ์ฑ
2: ๋ชจ๋ ํญ๋ชฉ ์ ๊ฑฐ (ํ์คํ ๋ฐฉ๋ฒ)
@CacheEvict(value = "users", allEntries = true)
public void updateUser(User user) {
userRepository.save(user);
}
// ํด๊ฒฐ์ฑ
3: ์ฌ๋ฌ ์บ์ ๋์ ๋ฌดํจํ
@Caching(evict = {
@CacheEvict(value = "users", key = "#user.id"),
@CacheEvict(value = "userProfiles", key = "#user.id"),
@CacheEvict(value = "userPermissions", key = "#user.id")
})
public void updateUser(User user) {
userRepository.save(user);
}
๐ก ์ค๋ฌด ๋๋ฒ๊น ํ
์บ์ ๊ด๋ จ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ๋ ๋์์ด ๋๋ ํ์ด์์:
- ์บ์ ๋ก๊น
ํ์ฑํ: Spring Boot์์
logging.level.org.springframework.cache=TRACE์ค์ - ์บ์ ๋งค๋์ ์ง์ ์ ๊ทผ: ๋ฌธ์ ํด๊ฒฐ์ ์ํด ์บ์ ๋ด์ฉ ์ง์ ํ์ธ
- ์บ์ ๋นํ์ฑํ ํ ์คํธ: ๋ฌธ์ ๊ฐ ์บ์ ๋๋ฌธ์ธ์ง ํ์ธํ๊ธฐ ์ํด ์ผ์์ ์ผ๋ก ๋นํ์ฑํ
- ๋จ์ ํ ์คํธ ์์ฑ: ์บ์ ๋์์ ๊ฒ์ฆํ๋ ํ ์คํธ ์์ฑ
- ์บ์ ํต๊ณ ๋ชจ๋ํฐ๋ง: ํํธ์จ, ๋ฏธ์ค์จ ๋ฑ์ ๋ชจ๋ํฐ๋งํด ๋ฌธ์ ํ์
์ค๋ฌด์์ ๋ฐ์ํ๋ ์ด๋ฐ ๋ฌธ์ ๋ค์ ๋ฏธ๋ฆฌ ์๊ณ ๋๋นํ๋ค๋ฉด, ์บ์ ๊ด๋ จ ์ด์๋ฅผ ๋น ๋ฅด๊ฒ ํด๊ฒฐํ ์ ์์ด์. ์ฌ๋ฅ๋ท์์ Java ๊ฐ๋ฐ์๋ฅผ ์ฐพ์ ๋, ์ด๋ฐ ์ค๋ฌด ๊ฒฝํ์ด ํ๋ถํ ๊ฐ๋ฐ์๋ฅผ ์ ํํ๋ฉด ํ๋ก์ ํธ๊ฐ ํจ์ฌ ์์ํ๊ฒ ์งํ๋ ๊ฑฐ์์! ๐ ๏ธ
10. ๋ง๋ฌด๋ฆฌ ๋ฐ ์ถ๊ฐ ํ์ต ์๋ฃ ๐
์ง๊ธ๊น์ง Spring Cache๋ฅผ ํ์ฉํ ์ ํ๋ฆฌ์ผ์ด์ ์ฑ๋ฅ ์ต์ ํ์ ๋ํด ์์๋ดค์ด์. ์ด์ ์ฌ๋ฌ๋ถ์ Spring Cache์ ๊ธฐ๋ณธ ๊ฐ๋ ๋ถํฐ ์ค๋ฌด์์ ๋ฐ์ํ๋ ๋ฌธ์ ํด๊ฒฐ๊น์ง ์ ๋ฐ์ ์ธ ๋ด์ฉ์ ์ดํดํ์ จ์ ๊ฑฐ์์!
๐ ํต์ฌ ์์ฝ
- ์บ์์ ๊ธฐ๋ณธ ๊ฐ๋ : ์์ฃผ ์ฌ์ฉํ๋ ๋ฐ์ดํฐ๋ฅผ ๋น ๋ฅด๊ฒ ์ ๊ทผํ ์ ์๋ ๊ณณ์ ์ ์ฅํ๋ ๊ธฐ์
- Spring Cache ์ค์ : ์์กด์ฑ ์ถ๊ฐ, @EnableCaching ํ์ฑํ, ์บ์ ๋งค๋์ ์ค์
- ์ฃผ์ ์ด๋ ธํ ์ด์ : @Cacheable, @CachePut, @CacheEvict, @Caching, @CacheConfig
- ์บ์ ๋งค๋์ : ConcurrentMapCacheManager, CaffeineCacheManager, RedisCacheManager ๋ฑ
- ์บ์ ์ ๋ต: Cache-Aside, Write-Through, Read-Through, Write-Behind, Refresh-Ahead
- ์ฑ๋ฅ ์ธก์ : ํํธ์จ, ์๋ต ์๊ฐ, ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ๋ฑ ๋ชจ๋ํฐ๋ง
- ์ฃผ์์ฌํญ: ์บ์ ์ผ๊ด์ฑ, ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋, ์บ์ ํญ๋ฐ, ์บ์ ์ค์ผ ๋ฑ
- ๋ฒ ์คํธ ํ๋ํฐ์ค: ์ ์ ํ ์บ์ ๋์ ์ ํ, ์บ์ ๊ณ์ธตํ, ํจ๊ณผ์ ์ธ ํค ์ค๊ณ ๋ฑ
- ์ค๋ฌด ๋ฌธ์ ํด๊ฒฐ: ์บ์ ๋์ ๋ฌธ์ , ํธ๋์ญ์ ์ด์, ๊ฐ์ฒด ์์ ๋ถ์์ฉ ๋ฑ
Spring Cache ์ฌ์ฉ ์๋๋ฆฌ์ค ๐ฏ
Spring Cache๊ฐ ํนํ ์ ์ฉํ ์๋๋ฆฌ์ค๋ค์ด์์:
- ์ฝ๊ธฐ ์์ฃผ ์ ํ๋ฆฌ์ผ์ด์ : ๋ธ๋ก๊ทธ, ๋ด์ค ์ฌ์ดํธ, ์ ํ ์นดํ๋ก๊ทธ ๋ฑ
- ๋ฐ๋ณต์ ์ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ: ์์ฃผ ์กฐํ๋์ง๋ง ๊ฑฐ์ ๋ณ๊ฒฝ๋์ง ์๋ ๋ฐ์ดํฐ
- ๋น์ฉ์ด ๋ง์ด ๋๋ ๊ณ์ฐ: ๋ณต์กํ ํต๊ณ, ๋ณด๊ณ ์ ์์ฑ ๋ฑ
- ์ธ๋ถ API ํธ์ถ: ์ 3์ ์๋น์ค ์๋ต ์บ์ฑ
- ์ธ์ ๋ฐ์ดํฐ: ์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด, ๊ถํ ๋ฑ
- ์ค์ ์ ๋ณด: ์์คํ ์ค์ , ์ ํ๋ฆฌ์ผ์ด์ ์์ฑ ๋ฑ
์ถ๊ฐ ํ์ต ์๋ฃ ๐
Spring Cache์ ๋ํด ๋ ๊น์ด ํ์ตํ๊ณ ์ถ๋ค๋ฉด ๋ค์ ์๋ฃ๋ค์ ์ฐธ๊ณ ํ์ธ์:
1. ๊ณต์ ๋ฌธ์
2. ์ฑ
- "Spring in Action" by Craig Walls (์บ์ฑ ๊ด๋ จ ์ฅ)
- "High-Performance Java Persistence" by Vlad Mihalcea
- "Spring Boot in Practice" by Somnath Musib
3. ์จ๋ผ์ธ ๊ฐ์
- Udemy: "Spring Framework 5: Beginner to Guru"
- Pluralsight: "Spring Framework: Spring Data"
- Baeldung: "Spring Caching"
4. ๋ธ๋ก๊ทธ ๋ฐ ํํ ๋ฆฌ์ผ
๋ค์ ๋จ๊ณ๋ก ๋ฐฐ์ธ ๋งํ ์ฃผ์ ๋ค ๐
Spring Cache๋ฅผ ๋ง์คํฐํ๋ค๋ฉด, ๋ค์ ์ฃผ์ ๋ค๋ ์ดํด๋ณด์ธ์:
- ๋ถ์ฐ ์บ์ฑ: Hazelcast, Infinispan ๋ฑ์ ํ์ฉํ ํด๋ฌ์คํฐ ์บ์ฑ
- ๋ฆฌ์กํฐ๋ธ ์บ์ฑ: Spring WebFlux์ ํจ๊ป ์ฌ์ฉํ๋ ๋ฆฌ์กํฐ๋ธ ์บ์ฑ
- ์บ์ ๊ณ์ธตํ: ๋ค์ค ๋ ๋ฒจ ์บ์ ์ํคํ ์ฒ ๊ตฌํ
- ์บ์ ํ๋ฆฌ์๋ฐ: ํจ๊ณผ์ ์ธ ์บ์ ์๋ฐ์ ์ ๋ต
- ์บ์ ์ผ๊ด์ฑ ํจํด: ๋ถ์ฐ ํ๊ฒฝ์์์ ์บ์ ์ผ๊ด์ฑ ์ ์ง ๋ฐฉ๋ฒ
- ์บ์ ๋ชจ๋ํฐ๋ง: Micrometer, Prometheus, Grafana๋ฅผ ํ์ฉํ ๊ณ ๊ธ ๋ชจ๋ํฐ๋ง
๋ง์น๋ฉฐ ๐ญ
Spring Cache๋ ์ ํ๋ฆฌ์ผ์ด์ ์ฑ๋ฅ์ ํฌ๊ฒ ํฅ์์ํฌ ์ ์๋ ๊ฐ๋ ฅํ ๋๊ตฌ์์. ํ์ง๋ง ๋ชจ๋ ๋๊ตฌ์ ๋ง์ฐฌ๊ฐ์ง๋ก, ์ฌ๋ฐ๋ฅด๊ฒ ์ฌ์ฉํ ๋ ์ต๊ณ ์ ๊ฒฐ๊ณผ๋ฅผ ์ป์ ์ ์์ด์.
์บ์ฑ์ ํญ์ ํธ๋ ์ด๋์คํ๊ฐ ์์ด์. ์๋์ ๋ฉ๋ชจ๋ฆฌ, ์ผ๊ด์ฑ๊ณผ ์ฑ๋ฅ ์ฌ์ด์์ ๊ท ํ์ ์ฐพ๋ ๊ฒ์ด ์ค์ํด์. ์ฌ๋ฌ๋ถ์ ์ ํ๋ฆฌ์ผ์ด์ ํน์ฑ๊ณผ ์๊ตฌ์ฌํญ์ ๋ง๋ ์บ์ฑ ์ ๋ต์ ์ ํํ๊ณ , ์ง์์ ์ผ๋ก ๋ชจ๋ํฐ๋งํ๊ณ ์ต์ ํํ์ธ์.
์ด ๊ธ์ด ์ฌ๋ฌ๋ถ์ Spring Cache ์ฌ์ ์ ๋์์ด ๋์๊ธฐ๋ฅผ ๋ฐ๋ผ์! ๋ ๋น ๋ฅด๊ณ ํจ์จ์ ์ธ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค์ด ์ฌ์ฉ์์๊ฒ ๋ ๋์ ๊ฒฝํ์ ์ ๊ณตํ์ธ์. ๐
Java ๊ฐ๋ฐ๊ณผ ๊ด๋ จ๋ ๋ ๋ง์ ์ ๋ณด์ ์ ๋ฌธ๊ฐ๋ฅผ ์ฐพ๊ณ ๊ณ์ ๋ค๋ฉด, ์ฌ๋ฅ๋ท(https://www.jaenung.net)์ ๋ฐฉ๋ฌธํด๋ณด์ธ์. ๋ค์ํ ๊ฐ๋ฐ ์ฌ๋ฅ์ ๊ฐ์ง ์ ๋ฌธ๊ฐ๋ค์ด ์ฌ๋ฌ๋ถ์ ํ๋ก์ ํธ๋ฅผ ๋์๋๋ฆด ์ค๋น๊ฐ ๋์ด ์์ด์! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.