1. Docker Redis 이미지 다운 후 Spring Boot와 연결 & 실행
1. Redis란?
Redis는 Remote Dictionary Server의 약자로 메모리 기반의 Key-Value 데이터 저장소이다.
데이터를 메모리에 저장하여 매우 빠른 속도로 읽기/쓰기 작업을 처리할 수 있다.
NoSQL 데이터베이스 중 하나로 영속성을 제공하여 데이터가 영구적으로 유지될 수 있다.
1. Redis가 뭐에요?
Redis는 메모리 기반의 Key-Value 데이터 저장소로 데이터를 메모리에 저장해서 매우 빠르게 읽기/쓰기 작업을 처리할 수 있는 NoSQL DB입니다.
2. 특징이 뭐에요?
Redis의 특징은 In-Memory 데이터베이스로 모든 데이터를 메모리에 저장해서 빠르게 읽고 쓸 수 있고 데이터를 디스크에 저장하여 재시작 후에도 데이터 복구가 가능합니다. 또한 Pub-Sub 기능과 TTL, Lua 스크립트 등의 유용한 기능을 제공하고 있습니다.
3. 사용 사례에는 뭐가 있어요?
Redis는 캐싱이나 세션 관리, 실시간 랭킹, Pub-Sub, 큐 시스템 등에 사용됩니다.
4. Redis 사용 시점
내 프로젝트에서는 Hash를 사용해 사용자 프로필 데이터 저장할 때나 토큰 저장 같은 상황에 필요하지 않을까 싶다.
2. Docker에 이미지 다운 받기
docker pull redis
docker run --name mysql-container -e MYSQL_ROOT_PASSWORD=1234 -p 3306:3306 -d mysql:8.0
docker run --name redis-container -p 6379:6379 -d redis
3. Spring Boot에 Redis 연결하기
build.gradle에 아래 Redis 관련 의존성을 추가한다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
application.properties 파일에 Redis 설정을 추가한다.나는 Spring Boot 3.x이기 때문에 아래와 같이 추가하였다.
# Redis
spring.data.redis.database=0
spring.data.redis.host=localhost
spring.data.redis.port=6379
docker에서 redis를 실행시킨 후 아래 명령어로 redis-cli를 실행해서 데이터를 확인해보았다.
docker exec -it redis-container redis-cli
test-key와 Hello Redis를 연결해서 추가한 후 확인한 결과이다.
Redis는 MySQL과 달리 테이블, 컬럼 개념이 없다.
데이터를 비우려면 Redis의 Key 삭제 또는 전체 데이터 초기화를 실행해야한다.
아래와 같이 RedisTemplate을 RedisConfig에 만든 후 Bean으로 등록한다.
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
}
RedisTemplate은 Spring Data Redis에서 제공하는 클래스로 Redis와 통신하는 데 필요한 다양한 메서드를 제공해서 Redis 데이터를 읽고 쓰는 작업을 쉽게 처리할 수 있다.java 객체를 Redis의 key-value 형식으로 변환하여 저장하거나 조회할 수 있게 도와준다.Redis는 Key와 Value를 직렬화하여 저장하기 때문에 RedisTemplate은 데이터를 Redis에 저장하거나 읽을 때 직렬화/역직렬화 과정을 처리한다.두 번 호출 시 Redis에서 데이터 조회되는 것을 확인할 수 있다.
getUser에만 우선적으로 redis를 적용해보았다.
// redis 적용
public UserDto getUser(Long userId) {
String redisKey = "user:"+userId;
User cachedUser = (User) redisTemplate.opsForValue().get(redisKey);
if(cachedUser != null){
System.out.println("Redis에서 데이터 조회");
return UserDto.mapToDto(cachedUser);
}
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found"));
redisTemplate.opsForValue().set(redisKey, user);
return UserDto.mapToDto(user);
}
LocalDateTime 관련해서 에러가 발생한다.
redis 직렬화 과정에서 LocalDateTime 수정이 필요하다하는데 수정한 건 아래와 같다.
(해달라는 대로 해줘서 해결은 됐는데 좀 찝찝한..)
1) JacksonConfig 추가
package com.fortune.app.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.hibernate5.jakarta.Hibernate5JakartaModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new Hibernate5JakartaModule());
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return objectMapper;
}
}
2) RedisConfig 수정
package com.fortune.app.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper));
return redisTemplate;
}
}
3) 서비스 부분 수정
// redis 적용
public UserResponseDto getUser(Long userId) {
String redisKey = "user:" + userId;
Object cachedUser = redisTemplate.opsForValue().get(redisKey);
if (cachedUser != null) {
System.out.println("Redis에서 데이터 조회");
// LinkedHashMap을 UserResponseDto로 변환
UserResponseDto userDto = objectMapper.convertValue(cachedUser, UserResponseDto.class);
return userDto;
}
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found"));
redisTemplate.opsForValue().set(redisKey, UserResponseDto.mapToDto(user));
return UserResponseDto.mapToDto(user);
}
2. JPARepository에 JPQL 활용해서 작성해보기
1. JPQL이란?
JPQL은 Java Persistence Query Language의 약자로 JPA에서 제공하는 객체 지향 쿼리 언어다.
SQL과 유사하지만 SQL이 데이터베이스 테이블을 대상으로 한다면 JQPL은 엔티티 객체를 대상으로 한다.객체와 관계형 데이터베이스를 매핑하여 객체 중심으로 데이터를 조회할 수 있도록 설계된 쿼리 언어다.SQL은 특정 DBMS에 종속적이지만 JPQL은 JPA가 자동으로 SQL로 변환하여 실행하기 때문에 특정 DB에 종속되지 않는다.단점은 SQL보다 성능 최적화가 어렵고 JOIN이 많거나 복잡한 경우 불리하다. ex) 윈도우 함수
2. JPQL 쿼리 작성
package com.fortune.app.user;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.name = :name")
User findByName(@Param("name") String name);
@Query("SELECT u FROM User u WHERE u.nickname = :nickname")
Optional<User> findUserByNickname(@Param("nickname") String nickname);
@Query("SELECT u FROM User u JOIN FETCH u.oauth WHERE u.nickname = :nickname")
Optional<User> findUserWithOauthByNickname(@Param("nickname") String nickname);
@Query("SELECT u FROM User u JOIN FETCH u.oauth WHERE u.userId = :userId")
Optional<User> findUserWithOauthById(@Param("userId") Long userId);
@Query("SELECT u FROM User u JOIN FETCH u.oauth WHERE u.createdAt >= :startDate")
List<User> findRecentUsersWithOauth(@Param("startDate") LocalDateTime startDate);
}
3. 테스트 코드
package com.fortune.app.user;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@Transactional
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
private OauthRepository oauthRepository;
@PersistenceContext
private EntityManager entityManager;
@Test
void testFindUserWithOauthById() {
Oauth oauth = Oauth.builder()
.provider("google")
.providerUid("123456")
.accessToken("asdf")
.refreshToken("qwer")
.build();
oauthRepository.save(oauth);
User user = User.builder()
.name("사용자")
.birth(new Date())
.nickname("testUser")
.oauth(oauth)
.build();
userRepository.save(user);
entityManager.flush();
entityManager.clear();
Optional<User> foundUser = userRepository.findUserWithOauthById(user.getUserId());
assertThat(foundUser).isPresent();
assertThat(foundUser.get().getOauth()).isNotNull();
assertThat(foundUser.get().getOauth().getProvider()).isEqualTo("google");
}
}
'dev > 프로젝트' 카테고리의 다른 글
[프로젝트] Redis & OAuth2 적용 (6) | 2025.01.21 |
---|---|
[프로젝트] Custom Exception & Redis (6) | 2025.01.19 |
[프로젝트] JPA 영속성 Context & AOP (2) | 2025.01.16 |
[프로젝트] ERD & Docker 적용 (9) | 2025.01.15 |
[프로젝트] MVC 아키텍처 & JPA (6) | 2025.01.14 |