![[ํ๋ก์ ํธ] Redis๋ก ์บ์ฑ ๊ตฌํํ๊ธฐ](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyfPUy%2FbtsJDwuYz6M%2Fit9xZlzrSGKsspszo18Yz0%2Fimg.png)
๐ก ๊ฐ๋
Redis
- ์คํ ์์ค In-memory ๋ฐ์ดํฐ ์ ์ฅ์
- String / Hash / Set / List ๋ฑ์ ์ ์ฅ ๊ฐ๋ฅ
- ์ ํต์ ์ธ ๋์คํฌ ๋ฐฉ์๋ณด๋ค ํจ์ฌ ๋น ๋ฅด๋ฉฐ, ์ฃผ๋ก ์บ์(cache)์ฉ๋๋ก ์ฌ์ฉ
- ํ์ฌ๋ ๋จ์ ์บ์ ์ฉ๋ ์ด์ธ์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค, ๋ฉ์์ง ๋ธ๋ก์ปค, ์คํธ๋ฆฌ๋ฐ ์์ง์ผ๋ก๋ ์ฌ์ฉ
์บ์๋ก ์ฌ์ฉ?
- ํธ์ถํ ๋๋ง๋ค ๊ณ์ฐ์ ํด์ผํ๊ณ ๊ณ์ฐ์ด ์ค๋๊ฑธ๋ฆฌ๊ฑฐ๋, ์์ ์ ๋ฐ๋ผ ๊ฒฐ๊ณผ๊ฐ์ด ๋ฐ๋์ง ์๋๊ฒฝ์ฐ
(์์: ์ด์ ๊น์ง ๋ฐ์ํ ์๋น์ค์ ์ด ๋งค์ถ๊ฐ์ ํต๊ณ๊ฐ)
๐ฅ ๊ฐ๋ฐ ๋ฐฉํฅ
์ฌ์ฉ์ ์ ๋ณด๋ ํ์ฌ ํ๋ก์ ํธ์์ ๋ณ๊ฒฝ๋ ์ผ์ด ์์ผ๋ฏ๋ก ์ธ์ฆ ๋ถ๋ถ์์ ์บ์ฑ์ ์ ์ฉํ๋ ค๊ณ ํจ
๊ธฐ์กด
: ์ฌ์ฉ์ ์ธ์ฆ(๋ก๊ทธ์ธ)์ ์งํํ ๋, ํญ์ DB์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ด
๊ฐ๋ฐ ์งํ ๋ฐฉํฅ:
- ์ฒ์ ์ฌ์ฉ์ ์ธ์ฆ(๋ก๊ทธ์ธ) ์ DB select ํ, ์บ์์ ์ ์ฅ
- ์นด์นด์ค ๋ก๊ทธ์ธ ์ ์ธ์ฆ ํ์ ์นด์นด์ค ์ ์ ์ ๋ณด DB insert ํ๊ณ , ์บ์์ ์ ์ฅ
→ ์ดํ ๊ฐ์ userId๋ก ์ธ์ฆ์ ์บ์์์ ๋จผ์ ์กฐํ
1. Docker๋ฅผ ํตํด Redis ์คํ
docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latestโ
-p 6379:6379
:๋ก์ปฌ ์ปดํจํฐ์ 6379 ํฌํธ๋ฅผ ์ปจํ ์ด๋ ๋ด๋ถ์ 6379 ํฌํธ์ ์ฐ๊ฒฐํ์ฌ ํฌํธ๊ฐ ์ธ๋ถ์์ ์ ๊ทผ ๊ฐ๋ฅํ๋๋ก ์ค์ ํจ
(Redis ์๋ฒ๋ ๊ธฐ๋ณธ์ ์ผ๋ก 6379 ํฌํธ์์ ๋์)-p 8001:8001
:Redis Stack ์ ์น ์ธํฐํ์ด์ค(RedisInsight)์ ์ ์ํ ์ ์๋ ํฌํธ๋ฅผ ๋งคํํจ
http://localhost:8001/redis-stack/browser ๋ก RedisInsight์ ์ ์
2. ์์กด์ฑ ์ถ๊ฐ
๐พ build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis' //redis
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
3. redis host, port ์ค์
๐พ application.yaml
redis:
host: localhost
port: 6379
4. Cache ๊ด๋ จ ๋น ์ค์
๐พ CacheConfig.java
@Configuration
public class CacheConfig {
private static final ObjectMapper objectMapper =
new ObjectMapper().registerModule(new JavaTimeModule());
@Bean // redis์ ์ ์ํ๊ธฐ ์ํ ์ค์
RedisConnectionFactory redisConnectionFactory(
@Value("${redis.host}") String redisHost,
@Value("${redis.port}") int redisPort
) {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(redisHost);
config.setPort(redisPort);
return new LettuceConnectionFactory(config); // lettuce : ์ต์ , ์ฑ๋ฅ ์ข๋ค๊ณ ์๋ ค์ง ๊ตฌํ์ฒด
}
@Bean
public RedisTemplate<String, UserAccount> userAccountRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, UserAccount> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer()); //๋ฌธ์์ด๋ก ๋ณํํ์ฌ ํค๊ฐ ์ธํ
//value: ๋ฐ์ดํฐ๊ฐ ๋ณต์ก.. ํ๋ ๋ฐ์ดํฐ๋ ์๊ณ -> Jsonํํ์ ๋ฌธ์์ด๋ก
redisTemplate.setValueSerializer(
new Jackson2JsonRedisSerializer<>(objectMapper, UserAccount.class));
return redisTemplate;
}
}
5. ์บ์ฑ ๊ธฐ๋ฅ ๊ตฌํ
๐พ UserAccountService.java
@Transactional(readOnly = true)
public Optional<UserAccountDto> searchUser(String username) {
return userAccountCacheRepository.getUserAccountCache(username)
.or(() -> userAccountRepository.findById(username)
.map(userAccount -> {
userAccountCacheRepository.setUserAccountCache(userAccount);
return userAccount;
}))
.map(UserAccountDto::fromEntity);
}
public UserAccountDto saveUser(String username, String password, String email, String nickname, String memo) {
UserAccount userAccount = userAccountRepository.save(UserAccount.of(username, password, email, nickname, memo, username));
userAccountCacheRepository.setUserAccountCache(userAccount);
return UserAccountDto.fromEntity(userAccount);
}
searchUser
: ์ฌ์ฉ์ ๋ก๊ทธ์ธ ์ ์๋ Spring Security ๋ก์ง์์ DB์ ์ฌ์ฉ์๊ฐ ์๋์ง ํ์ธํ๋ ๋ก์ง
- redis์์ ์ฌ์ฉ์๊ฐ ์๋์ง ์กฐํํ๊ณ
- ์์ผ๋ฉด DB์์ ์กฐํ ํ, ์ํฐํฐ redis์ ์ ์ฅ
(map : ์นด์นด์ค ์ฒ์ ๋ก๊ทธ์ธ ์ redis, DB์ ๋ชจ๋ ์๊ธฐ๋๋ฌธ์ ์ํฐํฐ๊ฐ ์๋ ๊ฒฝ์ฐ์๋ง redis์ ์ ์ฅํจ)
SecurityConfig.java - userDetailsService
- ํผ ๋ก๊ทธ์ธ์ ํตํด ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ์ ์๋ํ ๋ ํธ์ถ๋จ
- username์ ๋ฐ์์ UserAccountService๋ฅผ ํตํด ์ฌ์ฉ์๋ฅผ ์กฐํํ๋ค
- UserDetailsService๋ Spring Security์์ ์ธ์ฆ ์ ๋ณด๋ฅผ ์กฐํํ ๋ ์ฌ์ฉ๋จ
@Bean public UserDetailsService userDetailsService(UserAccountService userAccountService) { return username -> userAccountService .searchUser(username) .map(SnsPrincipal::fromDto) .orElseThrow(() -> new UsernameNotFoundException("์ ์ ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค - username: " + username)); }โ
saveUser
: ์นด์นด์ค ๋ก๊ทธ์ธ ์ ์๋ OAuth2 ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ฒ๋ฆฌํ๊ณ DB์ ์ ์ฅํ๋ ๋ก์ง
- DB์ ์ ์ฅ ํ, redis์ ์ ์ฅ
SecurityConfig.java - oAuth2UserService
- OAuth2 ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ฒ๋ฆฌํ๋ ์๋น์ค ๊ฐ์ฒด๋ฅผ ์์ฑ
- ์ด ๋ฉ์๋๊ฐ ๋ฐํํ OAuth2User ๊ฐ์ฒด๊ฐ Spring Security์ ์ธ์ฆ ์ ๋ณด๋ก ์ฌ์ฉ๋จ
@Bean public OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService( UserAccountService userAccountService, PasswordEncoder passwordEncoder ) { final DefaultOAuth2UserService delegate = new DefaultOAuth2UserService(); return userRequest -> { OAuth2User oAuth2User = delegate.loadUser(userRequest); KakaoOAuth2Response kakaoResponse = KakaoOAuth2Response.from(oAuth2User.getAttributes()); String registrationId = userRequest.getClientRegistration().getRegistrationId(); String providerId = String.valueOf(kakaoResponse.id()); String username = registrationId + "_" + providerId; String dummyPassword = passwordEncoder.encode("{bcrypt}" + UUID.randomUUID()); return userAccountService.searchUser(username) .map(SnsPrincipal::fromDto) .orElseGet(() -> SnsPrincipal.fromDto( userAccountService.saveUser( username, dummyPassword, kakaoResponse.email(), kakaoResponse.nickname(), null ) ) ); }; }โ
๐พ UserAccountCacheRepository.java
@Slf4j
@Repository
@RequiredArgsConstructor
public class UserAccountCacheRepository {
private final RedisTemplate<String, UserAccount> userAccountRedisTemplate;
private final static Duration USER_CACHE_TTL = Duration.ofDays(3); //redis๊ณต๊ฐ์ ํจ์จ์ ์ผ๋ก ์ฌ์ฉํ๊ธฐ ์ํด 3์ผ๋์๋ง ์ ์ฅ
public void setUserAccountCache(UserAccount userAccount) {
log.info("Set User to Redis {}:{}", getRedisKey(userAccount.getUserId()), userAccount);
userAccountRedisTemplate.opsForValue().set(getRedisKey(userAccount.getUserId()), userAccount, USER_CACHE_TTL);
}
public Optional<UserAccount> getUserAccountCache(String username) {
String redisKey = getRedisKey(username);
UserAccount userAccount = userAccountRedisTemplate.opsForValue().get(redisKey);
log.info("Get User from Redis {}:{}", getRedisKey(username), userAccount);
return Optional.ofNullable(userAccount);
}
private String getRedisKey(String username) {
return "user:" + username;
}
}
๐ก ๊ฒฐ๊ณผ
๐ ์ผ๋ฐ form ๋ก๊ทธ์ธ ์ (์ฒ์)
- redis์์ ๋จผ์ ์กฐํ
- ์์ด์ DB์์ ์กฐํ ํ redis์ ์ ์ฅ
2024-09-18T23:04:38.633+09:00 INFO 99096 --- [nio-8080-exec-7] c.p.s.r.UserAccountCacheRepository : Get User from Redis user:ella:null
Hibernate:
select
ua1_0.user_id,
ua1_0.created_at,
ua1_0.created_by,
ua1_0.email,
ua1_0.memo,
ua1_0.modified_at,
ua1_0.modified_by,
ua1_0.nickname,
ua1_0.user_password
from
user_account ua1_0
where
ua1_0.user_id=?
2024-09-18T23:04:38.635+09:00 TRACE 99096 --- [nio-8080-exec-7] org.hibernate.orm.jdbc.bind : binding parameter (1:VARCHAR) <- [ella]
2024-09-18T23:04:38.636+09:00 INFO 99096 --- [nio-8080-exec-7] c.p.s.r.UserAccountCacheRepository : Set User to Redis user:ella:UserAccount(super=AuditingFields(createdAt=2024-01-01T16:47:05, createdBy=ella, modifiedAt=2024-01-01T16:47:05, modifiedBy=ella), userId=ella, userPassword={noop}qwer1234, email=ella@mail.com, nickname=EllaCoo, memo=I am Ella.)
๐ ์นด์นด์ค ๋ก๊ทธ์ธ ์ (์ฒ์)
- redis์์ ์กฐํํ์ผ๋ ์์
- DB์์ ์กฐํํ์ผ๋ ์์
- service์ saveUser ํตํด DB์ ์นด์นด์ค ์ธ์ฆ ์ ๋ณด ์ ์ฅ ๋ฐ redis ์ ์ ์ฅ
2024-09-18T23:07:01.215+09:00 INFO 99096 --- [nio-8080-exec-6] c.p.s.r.UserAccountCacheRepository : Get User from Redis user:kakao_1111:null
Hibernate:
select
ua1_0.user_id,
ua1_0.created_at,
ua1_0.created_by,
ua1_0.email,
ua1_0.memo,
ua1_0.modified_at,
ua1_0.modified_by,
ua1_0.nickname,
ua1_0.user_password
from
user_account ua1_0
where
ua1_0.user_id=?
2024-09-18T23:07:01.220+09:00 TRACE 99096 --- [nio-8080-exec-6] org.hibernate.orm.jdbc.bind : binding parameter (1:VARCHAR) <- [kakao_1111]
2024-09-18T23:07:01.238+09:00 INFO 99096 --- [nio-8080-exec-6] c.p.s.r.UserAccountCacheRepository : Set User to Redis user:kakao_1111:UserAccount(super=AuditingFields(createdAt=2024-09-18T23:07:01.230980, createdBy=kakao_1111, modifiedAt=2024-09-18T23:07:01.230980, modifiedBy=kakao_1111), userId=kakao_1111, userPassword={bcrypt}$2a$10$xMQ2EHRKE9T1eq8ZoVY5Uujwlltl7x5z2KDnyvn5.JvV3jWa9NBV6, email=null, nickname=์ด์์, memo=null)
Hibernate:
insert
into
user_account
(created_at, created_by, email, memo, modified_at, modified_by, nickname, user_password, user_id)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?)
์ดํ ๋์ผํ ์ ์ ์ธ์ฆ ์, redis์์ ๋จผ์ ์กฐํ ์งํ
์ถ๊ฐ ๊ณ ๋ฏผ์ฌํญ
- API ๊ฐ ๋จ์ผ ๋ ธ๋ ๊ตฌ์ฑ์ด๋ผ๋ฉด ์คํ๋ ค ๋ก์ปฌ ์บ์๊ฐ ๋ ๋น ๋ฅผ ์ ์๋ค.
(๊ตณ์ด ์๊ฒฉ์ redis์ ํต์ ํ ํ์๊ฐ ์์)
- ๋ฉ๋ชจ๋ฆฌ ๊ธฐ๋ฐ์ผ๋ก ์ฐ์ฐํ๋ ๊ฒ์ด ๋น ๋ฅธ ๊ฒ์ด์ง ๋คํธ์ํฌ๋ก ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ๋๊ฒ ๋น ๋ฅธ ๊ฒ์ ์๋
- ๋จ์ผ ๋ ธ๋๊ฐ ์๋ ๊ฒฝ์ฐ๋ก ๊ณ ๋ํ๋ฅผ ์งํํ ๋, ๊ฐ ๋ ธ๋ ๋ณ๋ก ๊ด๋ฆฌํ๋ ์บ์ ๋ฐ์ดํฐ๊ฐ ์๋ก ๋ฌ๋ผ์ง๋ค๋ ๋ฌธ์ ๋ฐ์ํ๋ ์ ์ ๊ณ ๋ คํด์ผ ํจ
โ ์ฑ๋ฅ ํ ์คํธ
DB - 10000๊ฐ
์บ์ - 10000๊ฐ
JMeter ๋ก ํ ์คํธ
์ฐ๋ ๋ ์(์ฌ์ฉ์ ์) : 5000
loop : 10
redis๋ฅผ ํตํด
- ๊ท์คํ DB ๋ฆฌ์์ค๋ฅผ ์๋ ์ ์๊ณ
- ํด๋ผ์ด์ธํธ์๊ฒ ์๋ต์ ๋ด๋ ค์ค ๋ ์๋ต์๋ ํฅ์