2020. 10. 16. 23:49ㆍSpring/Spring Boot
1. Redis
Redis 서버가 설치되어 있다는 전제하에 진행된다.
[참고] Database - Practice - AWS EC2 인스턴스 Redis 설치
2. Redis 설정
1) build.gradle
Redis (Lettuce) 를 사용하기 위한 의존성을 작성해준다. Redis에 접근하기 위해서는 Spring Data Redis 라이브러리를 사용하며, Lettuce와 Jedis 방식이 있다. 별도의 의존성 설정이 필요한 Jedis와 달리 Lettuce는 별도의 설정없이 사용할 수 있다.
Spring Data Redis에서는 RedisTemplate를 이용하는 방식과 Redis Repository를 이용한 방식이 존재한다. 해당 글에서는 두 가지 방법을 혼용해서 진행한다. (테스트 케이스는 RedisTemplate, 저장 및 조회 기능은 Redis Repository)
compile('org.springframework.boot:spring-boot-starter-data-redis:2.3.1.RELEASE')
2) application.yml
spring:
redis:
host: [Redis 서버 IP]
port: 6379
3) RedisProperties.java
application.yml에서 읽어온 값을 객체 클래스로 제작한다.
package com.example.lettucetest.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Getter
@Setter
@ConfigurationProperties(prefix = "spring.redis")
@Component
public class RedisProperties {
private String host;
private int port;
}
4) RedisRepositoryConfig.java
Redis의 연결을 정의하는 클래스를 작성해준다. RedisConnectionFactory를 통해 내장 혹은 외부의 Redis를 연결한다. RedisTemplate을 통해 RedisConnection에서 넘겨준 byte 값을 객체 직렬화한다.
package com.example.lettucetest.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@RequiredArgsConstructor
@Configuration
@EnableRedisRepositories
public class RedisRepositoryConfig {
/*
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
*/
private final RedisProperties redisProperties;
// Lettuce
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
}
@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
Redis 외부 서버가 존재할 경우 필요는 없지만, 만약 내장 서버로 환경을 구성할 경우, Embedded Redis의 설정을 추가로 해주어야한다. Embedded Redis의 경우 H2와 같은 내장 Redis 데몬이다. 내장 Redis를 사용하기 위한 it.ozimov.embedded-redis는 의존성을 추가해주고, 내장 Redis를 사용할 때 설정 파일의 prifile이 local일 때만 동작하도록 @Profile 어노테이션을 사용하여 EmbeddedRedis 클래스를 생성해준다.
compile group: 'it.ozimov', name: 'embedded-redis', version: '0.7.2'
package com.example.lettucetest.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import redis.embedded.RedisServer;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@Slf4j
@RequiredArgsConstructor
@Profile("local")
@Configuration
public class EmbeddedRedisConfig {
private final RedisProperties redisProperties;
private RedisServer redisServer;
@PostConstruct
public void redisServer() {
redisServer = new RedisServer(redisProperties.getPort());
redisServer.start();
}
@PreDestroy
public void stopRedis() {
if(redisServer != null) {
redisServer.stop();
}
}
}
5) Redis 데이터 타입 테스트
Redis에서 사용되는 String, List, Set, Sorted Set, Hash 데이터 타입별로 테스트를 진행하여 동작 여부를 확인한다. 테스트를 진행하면서, 실제로 서버에 키-값이 저장이 되는지 직접 redis-cli에 명령어를 실행해보면서 확인한다.
메서드 | 설명 |
opsForValue | String을 쉽게 Serialize / Deserialize 해주는 인터페이스 |
opsForList | List를 쉽게 Serialize / Deserialize 해주는 인터페이스 |
opsForSet | Set을 쉽게 Serialize / Deserialize 해주는 인터페이스 |
opsForZSet | ZSet을 쉽게 Serialize / Deserialize 해주는 인터페이스 |
opsForHash | Hash를 쉽게 Serialize / Deserialize 해주는 인터페이스 |
package com.example.lettucetest.config;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.*;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisRepositoryConfigTest {
@Autowired
StringRedisTemplate redisTemplate;
// String
@Test
public void testString() {
final String key = "testString";
final ValueOperations<String, String> stringStringValueOperations = redisTemplate.opsForValue();
stringStringValueOperations.set(key, "1");
final String result_1 = stringStringValueOperations.get(key);
System.out.println("result_1 = " + result_1);
stringStringValueOperations.increment(key);
final String result_2 = stringStringValueOperations.get(key);
System.out.println("result_2 = " + result_2);
/*
redis-cli 명령어
- GET key
*/
}
// List
@Test
public void testList() {
final String key = "testList";
final ListOperations<String, String> stringStringListOperations = redisTemplate.opsForList();
stringStringListOperations.rightPush(key, "H");
stringStringListOperations.rightPush(key, "E");
stringStringListOperations.rightPush(key, "L");
stringStringListOperations.rightPush(key, "L");
stringStringListOperations.rightPush(key, "O");
stringStringListOperations.rightPushAll(key, " ", "W", "O", "L", "D");
final String character_1 = stringStringListOperations.index(key, 1);
System.out.println("character_1 = " + character_1);
final Long size = stringStringListOperations.size(key);
System.out.println("size = " + size);
final List<String> resultRange = stringStringListOperations.range(key, 0,9);
assert resultRange != null;
System.out.println("resultRange = " + Arrays.toString(resultRange.toArray()));
/*
redis-cli 명령어
- INDEX key index
- LRANGE key start stop
*/
}
// Set
@Test
public void testSet() {
final String key = "testSet";
SetOperations<String, String> stringStringSetOperations = redisTemplate.opsForSet();
stringStringSetOperations.add(key, "H");
stringStringSetOperations.add(key, "E");
stringStringSetOperations.add(key, "L");
stringStringSetOperations.add(key, "L");
stringStringSetOperations.add(key, "O");
Set<String> test = stringStringSetOperations.members(key);
assert test != null;
System.out.println("member = " + Arrays.toString(test.toArray()));
final Long size = stringStringSetOperations.size(key);
System.out.println("size = " + size);
Cursor<String> cursor = stringStringSetOperations.scan(key, ScanOptions.scanOptions().match("*").count(3).build());
while(cursor.hasNext()) {
System.out.println("cursor = " + cursor.next());
}
/*
redis-cli 명령어
- SMEMBERS key
*/
}
// Sorted Set
@Test
public void testSortedSet() {
final String key = "testSortedSet";
ZSetOperations<String, String> stringStringZSetOperations = redisTemplate.opsForZSet();
stringStringZSetOperations.add(key, "H", 1);
stringStringZSetOperations.add(key, "E", 5);
stringStringZSetOperations.add(key, "L", 10);
stringStringZSetOperations.add(key, "L", 15);
stringStringZSetOperations.add(key, "O", 20);
Set<String> range = stringStringZSetOperations.range(key, 0, 5);
assert range != null;
System.out.println("range = " + Arrays.toString(range.toArray()));
final Long size = stringStringZSetOperations.size(key);
System.out.println("size = " + size);
Set<String> scoreRange = stringStringZSetOperations.rangeByScore(key, 0, 13);
assert scoreRange != null;
System.out.println("scoreRange = " + Arrays.toString(scoreRange.toArray()));
/*
redis-cli 명령어
- ZRANGE key start stop [WITHSCORES]
- ZRANGEBYSCORE key min max [WITHSCORES]
*/
}
// Hash
@Test
public void testHash() {
final String key = "testHash";
HashOperations<String, Object, Object> stringObjectObjectHashOperations = redisTemplate.opsForHash();
stringObjectObjectHashOperations.put(key, "Hello", "testHash");
stringObjectObjectHashOperations.put(key, "Hello2", "testHash2");
stringObjectObjectHashOperations.put(key, "Hello3", "testHash3");
Object hello = stringObjectObjectHashOperations.get(key, "Hello");
System.out.println("hello = " + hello);
Map<Object, Object> entries = stringObjectObjectHashOperations.entries(key);
System.out.println("entries = " + entries.get("Hello2"));
Long size = stringObjectObjectHashOperations.size(key);
System.out.println("size = " + size);
/*
redis-cli 명령어
- HGET key field
*/
}
/*
redis-cli 키 전체 삭제 명령어
- flushAll
*/
}
3. API 제작
1) src 패키지 구조
config
ㄴ RedisProperties.java
ㄴ RedisRepositoryConfig.java
controller
ㄴ dto
ㄴ RedisCrudResponseDto.java
ㄴ RedisCrudSaveRequestDto.java
ㄴ RedisController.java
domain
ㄴ redis
ㄴ RedisCrud.java
ㄴ RedisCrudRepository.java
service
ㄴ redis
ㄴ RedisCrudService
2) RedisCrud.java
쉽게 생각한다면 Entity 클래스이다. Builder 어노테이션을 이용하였고, 수정이 가능한 메서드를 작성해준다.
package com.example.lettucetest.domain.redis;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import java.time.LocalDateTime;
@ToString
@Getter
@RedisHash("redisCrud")
public class RedisCrud {
@Id
private Long id;
private String description;
private LocalDateTime updatedAt;
@Builder
public RedisCrud(Long id, String description, LocalDateTime updatedAt) {
this.id = id;
this.description = description;
this.updatedAt = updatedAt;
}
public void update(String description, LocalDateTime updatedAt) {
if(updatedAt.isAfter(this.updatedAt)) {
this.description = description;
this.updatedAt = updatedAt;
}
}
}
3) RedisCrudRepository
RedisCrudRepository는 JpaRepository를 상속받을 때와 유사하게 작성된다.
package com.example.lettucetest.domain.redis;
import org.springframework.data.repository.CrudRepository;
public interface RedisCrudRepository extends CrudRepository<RedisCrud, Long> {
}
4) RedisCrudService.java
package com.example.lettucetest.service.redis;
import com.example.lettucetest.controller.dto.RedisCrudResponseDto;
import com.example.lettucetest.controller.dto.RedisCrudSaveRequestDto;
import com.example.lettucetest.domain.redis.RedisCrud;
import com.example.lettucetest.domain.redis.RedisCrudRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@Service
public class RedisCrudService {
private final RedisCrudRepository redisCrudRepository;
@Transactional
public Long save(RedisCrudSaveRequestDto requestDto) {
return redisCrudRepository.save(requestDto.toRedisHash()).getId();
}
public RedisCrudResponseDto get(Long id) {
RedisCrud redisCrud = redisCrudRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("Nothing saved. id=" + id));
return new RedisCrudResponseDto(redisCrud);
}
}
5) RedisCrudSaveRequestDto.java
저장할 때 데이터를 전달하기 위해 사용되는 Dto 클래스를 작성해준다.
package com.example.lettucetest.controller.dto;
import com.example.lettucetest.domain.redis.RedisCrud;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Getter
@NoArgsConstructor
public class RedisCrudSaveRequestDto {
private Long id;
private String description;
private LocalDateTime updatedAt;
@Builder
public RedisCrudSaveRequestDto(Long id, String description, LocalDateTime updatedAt) {
this.id = id;
this.description = description;
this.updatedAt = updatedAt;
}
public RedisCrud toRedisHash() {
return RedisCrud.builder()
.id(id)
.description(description)
.updatedAt(LocalDateTime.now())
.build();
}
}
6) RedisCrudResponseDto.java
조회할 때 Redis로 부터 값을 받아올 Dto 클래스를 작성한다.
package com.example.lettucetest.controller.dto;
import com.example.lettucetest.domain.redis.RedisCrud;
import lombok.Getter;
import java.time.LocalDateTime;
@Getter
public class RedisCrudResponseDto {
private Long id;
private String description;
private LocalDateTime updatedAt;
public RedisCrudResponseDto(RedisCrud redisHash) {
this.id = redisHash.getId();
this.description = redisHash.getDescription();
this.updatedAt = redisHash.getUpdatedAt();
}
}
7) RedisController.java
package com.example.lettucetest.controller;
import com.example.lettucetest.controller.dto.RedisCrudResponseDto;
import com.example.lettucetest.controller.dto.RedisCrudSaveRequestDto;
import com.example.lettucetest.service.redis.RedisCrudService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.Set;
@Slf4j
@RequiredArgsConstructor
@RestController
public class RedisController {
private final RedisCrudService redisCrudService;
private final StringRedisTemplate redisTemplate;
@GetMapping("/")
public String ok() {
return "ok";
}
@GetMapping("/keys")
public String keys() {
Set<String> keys = redisTemplate.opsForSet().members("*");
assert keys != null;
return Arrays.toString(keys.toArray());
}
@PostMapping("/save")
public Long save(@RequestBody RedisCrudSaveRequestDto requestDto) {
log.info(">>>>>>>>>>>>>>> [save] redisCrud={}", requestDto);
return redisCrudService.save(requestDto);
}
@GetMapping("/get/{id}")
public RedisCrudResponseDto get(@PathVariable Long id) {
return redisCrudService.get(id);
}
}
8) 테스트
RedisCrudRepository와 RedisController Unit Test를 진행한다.
package com.example.lettucetest.domain.redis;
import org.junit.After;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisCrudRepositoryTest {
@Autowired
private RedisCrudRepository redisCrudRepository;
@After
public void tearDown() throws Exception {
redisCrudRepository.deleteAll();
}
@Test
public void 기본_등록_조회() {
// given
Long id = 0L;
String description = "description";
LocalDateTime updatedAt = LocalDateTime.of(2020, 10, 16, 0, 0);
RedisCrud redisCrudSave = RedisCrud.builder()
.id(id)
.description(description)
.updatedAt(updatedAt)
.build();
// when
redisCrudRepository.save(redisCrudSave);
// then
RedisCrud redisCrudFind = redisCrudRepository.findById(id).get();
assertThat(redisCrudFind.getDescription()).isEqualTo("description");
assertThat(redisCrudFind.getUpdatedAt()).isEqualTo(updatedAt);
}
@Test
public void 기본_등록_수정() {
// given
Long id = 0L;
String description = "description";
LocalDateTime updatedAt = LocalDateTime.of(2020, 10, 16, 0, 0);
RedisCrud redisCrudSave = RedisCrud.builder()
.id(id)
.description(description)
.updatedAt(updatedAt)
.build();
redisCrudRepository.save(redisCrudSave);
// when
RedisCrud redisCrudUpdate = redisCrudRepository.findById(id).get();
redisCrudUpdate.update("updated description", LocalDateTime.of(2020,10, 17, 0, 0));
redisCrudRepository.save(redisCrudUpdate);
// then
RedisCrud redisCrudFind = redisCrudRepository.findById(id).get();
assertThat(redisCrudFind.getDescription()).isEqualTo("updated description");
}
}
package com.example.lettucetest.controller;
import com.example.lettucetest.controller.dto.RedisCrudSaveRequestDto;
import com.example.lettucetest.domain.redis.RedisCrudRepository;
import org.junit.After;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class RedisControllerTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private RedisCrudRepository redisCrudRepository;
@After
public void tearDown() throws Exception {
redisCrudRepository.deleteAll();
}
@Test
public void 기본() {
// given
String url = "http://localhost:" + port;
// when
ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isEqualTo("ok");
}
@Test
public void 기본_등록_조회() {
// given
RedisCrudSaveRequestDto requestDto = RedisCrudSaveRequestDto.builder()
.id(1L)
.description("description")
.updatedAt(LocalDateTime.now())
.build();
String url = "http://localhost:" + port + "/save";
// when
ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);
// then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isGreaterThan(0L);
}
}
9) 구현 화면
우측 Gradle 탭에서 전체 Test를 진행했을 때 문제 없이 동작하는 것을 확인할 수 있다.
실제로 Redis 서버와 통신을 하면서 저장과 조회가 정상적으로 동작하는 것을 확인할 수 있다.
'Spring > Spring Boot' 카테고리의 다른 글
[Spring Boot] MongoDB를 이용한 간단한 API 제작 (0) | 2020.12.21 |
---|---|
[Spring Boot] Redis (Jedis)를 이용한 간단한 API 제작 (0) | 2020.10.17 |
[Spring Boot] application 설정 파일 읽어오기 (0) | 2020.10.16 |
[Spring Boot] 참고 서비스 (0) | 2020.09.23 |