배경
JWT를 활용한 Spring Security를 작업하면서 Refresh Token을 저장하기 위해서 처음으로 Redis를 Spring에 적용해 봤다. Redis를 공부하면서 알게 된 Embedded Redis가 무엇인지 정리해 보고 적용했던 과정을 정리해 본다.
Embedded Redis
Embedded Redis는 로컬 개발 환경이나 테스트 환경에서 Redis를 쉽게 실행할 수 있도록 도와주는 도구이다. Embedded Redis를 사용하면 외부 Redis 서버를 설치하고 구성할 필요 없이 애플리케이션 내에서 Redis를 실행할 수 있다.
기존에는 Redis는 서버에서 실행되고 Redis 클라이언트가 Redis 서버에 연결해서 데이터를 조작한다. 하지만 Embedded Redis를 사용하면 애플리케이션 내에서 Redis를 실행하기 때문에 Redis가 애플리케이션에 의존하도록 할 수 있다.
테스트할 때 굉장히 유용한데, 마치 테스트를 할 때마다 Redis를 직접 껐다 킬 필요 없이 H2처럼 쉽게 Redis를 시작하고 중지할 수 있다.
이렇게 외부 설치 없이 실행할 수 있는 애플리케이션은 누가 어디서든 프로젝트를 cloen 받으면 별도 설정 없이 바로 개발을 시작할 수 있게 되는 장점이 있다.
구현
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
compile group: 'it.ozimov', name: 'embedded-redis', version: '0.7.2'
Embedded Redis는 0.7.3이 최신 버전이지만 해당 버전을 사용하면 에러가 발생한다.
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/C:/Users/ROGAL/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-simple/1.7.32/321ffafb5123a91a71737dbff38ebe273e771e5b/slf4j-simple-1.7.32.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/C:/Users/ROGAL/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.2.9/7d495522b08a9a66084bf417e70eedf95ef706bc/logback-classic-1.2.9.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See <http://www.slf4j.org/codes.html#multiple_bindings> for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.SimpleLoggerFactory]
Exception in thread "main" java.lang.IllegalArgumentException: LoggerFactory is not a Logback LoggerContext but Logback is on the classpath. Either remove Logback or the competing implementation (class org.slf4j.impl.SimpleLoggerFactory loaded from file:/C:/Users/ROGAL/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-simple/1.7.32/321ffafb5123a91a71737dbff38ebe273e771e5b/slf4j-simple-1.7.32.jar). If you are using WebLogic you will need to add 'org.slf4j' to prefer-application-packages in WEB-INF/weblogic.xml: org.slf4j.impl.SimpleLoggerFactory
Slf4J가 여러 번 바인딩돼서 발생하는 에러인데, 이 문제를 해결하는 풀리퀘스트가 있음에도 아직도 고쳐지지 않았다. 해결 방법으로 컴파일 시 Slf4j를 제외하는 방법이 있지만 나는 0.7.2 버전을 사용했다.
EmbeddedRedisConfig
EmbeddedRedis를 설정하는 클래스이다.
@Slf4j
@Profile("local")
@Configuration
public class EmbeddedRedisConfig {
@Value("${spring.redis.port}")
private int redisPort;
private RedisServer redisServer;
@PostConstruct
public void startRedis() throws IOException {
int port = isRedisRunning() ? findAvailablePort() : redisPort;
redisServer = new RedisServer(port);
redisServer.start();
}
@PreDestroy
public void stopRedis() {
redisServer.stop();
}
public int findAvailablePort() throws IOException {
for (int port = 10000; port <= 65535; port++) {
Process process = executeGrepProcessCommand(port);
if (!isRunning(process)) {
return port;
}
}
throw new BusinessLogicException(ExceptionCode.NOT_FOUND_AVAILABLE_PORT);
}
/**
* Embedded Redis가 현재 실행중인지 확인
*/
private boolean isRedisRunning() throws IOException {
return isRunning(executeGrepProcessCommand(redisPort));
}
/**
* 해당 port를 사용중인 프로세스를 확인하는 sh 실행
*/
private Process executeGrepProcessCommand(int redisPort) throws IOException {
String command = String.format("netstat -nat | grep LISTEN|grep %d", redisPort);
String[] shell = {"/bin/sh", "-c", command};
return Runtime.getRuntime().exec(shell);
}
/**
* 해당 Process가 현재 실행중인지 확인
*/
private boolean isRunning(Process process) {
String line;
StringBuilder pidInfo = new StringBuilder();
try (BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
while ((line = input.readLine()) != null) {
pidInfo.append(line);
}
} catch (Exception e) {
throw new BusinessLogicException(ExceptionCode.ERROR_EXECUTING_EMBEDDED_REDIS);
}
return StringUtils.hasText(pidInfo.toString());
}
}
- @Profile("local") : 프로파일이 local 환경일 때만 실행되도록 설정했다. 로컬 환경에서만 Embedded Redis를 사용하고, 실제 배포 환경에서는 외부 Redis 서버를 사용하기 때문이다.
- startRedis() : 해당 클래스가 로딩될 때, startRedis() 메서드가 자동으로 실행돼서 Embedded Redis를 실행한다.
- stopRedis() : PreDestroy 애너테이션으로 해당 클래스가 종료될 때 stopRedis() 메서드가 자동으로 실행되어 Embedded Redis를 종료한다.
- findAvailablePort() : 사용 가능한 포트를 찾는 메서드
- isRedisRunning() : 현재 Embedded Redis가 실행 중인지 확인하는 메서드
- executeGrepProcessCommand() : 해당 포트를 사용 중인 프로세스를 확인한다.
- isRunning() : 프로세스가 현재 실행중인지 확인하는 메서드
테스트
Embedded Redis가 잘 실행되는지 테스트해 보자. Redis 테스트 코드를 짜둔 게 있어서 그대로 사용했다.
@Slf4j
@SpringBootTest
class RedisCrudTest {
final String KEY = "key";
final String VALUE = "value";
final Duration DURATION = Duration.ofMillis(5000);
@Autowired
private RedisService redisService;
@BeforeEach
void shutDown() {
redisService.setValues(KEY, VALUE, DURATION);
}
@AfterEach
void tearDown() {
redisService.deleteValues(KEY);
}
@Test
@DisplayName("Redis에 데이터를 저장하면 정상적으로 조회된다.")
void saveAndFindTest() throws Exception {
// when
String findValue = redisService.getValues(KEY);
// then
assertThat(VALUE).isEqualTo(findValue);
}
@Test
@DisplayName("Redis에 저장된 데이터를 수정할 수 있다.")
void updateTest() throws Exception {
// given
String updateValue = "updateValue";
redisService.setValues(KEY, updateValue, DURATION);
// when
String findValue = redisService.getValues(KEY);
// then
assertThat(updateValue).isEqualTo(findValue);
assertThat(VALUE).isNotEqualTo(findValue);
}
@Test
@DisplayName("Redis에 저장된 데이터를 삭제할 수 있다.")
void deleteTest() throws Exception {
// when
redisService.deleteValues(KEY);
String findValue = redisService.getValues(KEY);
// then
assertThat(findValue).isEqualTo("false");
}
@Test
@DisplayName("Redis에 저장된 데이터는 만료시간이 지나면 삭제된다.")
void expiredTest() throws Exception {
// when
String findValue = redisService.getValues(KEY);
await().pollDelay(Duration.ofMillis(6000)).untilAsserted(
() -> {
String expiredValue = redisService.getValues(KEY);
assertThat(expiredValue).isNotEqualTo(findValue);
assertThat(expiredValue).isEqualTo("false");
}
);
}
}
에러 발생
테스트를 실행하자 아래와 같은 에러가 발생했다.
java.lang.RuntimeException: Can't start redis server. Check logs for details. Redis process log:
[30452] 24 Dec 13:36:00.257 #
The Windows version of Redis allocates a memory mapped heap for sharing with
the forked process used for persistence operations. In order to share this
memory, Windows allocates from the system paging file a portion equal to the
size of the Redis heap. At this time there is insufficient contiguous free
space available in the system paging file for this operation (Windows error
0x5AF). To work around this you may either increase the size of the system
paging file, or decrease the size of the Redis heap with the --maxheap flag.
Sometimes a reboot will defragment the system paging file sufficiently for
this operation to complete successfully.
Please see the documentation included with the binary distributions for more
details on the --maxheap flag.
Redis can not continue. Exiting.
로그를 읽어보면 Redis가 Window 운영 체제에서 메모리에 여유 공간이 부족하다고 한다. 그래서 —maxheap 플래그를 사용해서 Redis heap의 크기를 줄이라고 한다.
아래와 같이 코드를 수정하자
@Slf4j
@Profile("local")
@Configuration
public class EmbeddedRedisConfig {
...
@PostConstruct
public void startRedis() throws IOException {
int port = isRedisRunning() ? findAvailablePort() : redisPort;
redisServer = RedisServer.builder()
.port(port)
.setting("maxmemory 128M")
.build();
redisServer.start();
}
...
}
위 방식으로 끝낼 수 있지만 환경에 맞게 힙을 할당할 수 있도록 환경 변수를 사용하는 방식으로 변경했다.
spring:
redis:
maxmemory: 128M
@Slf4j
@Profile("local")
@Configuration
public class EmbeddedRedisConfig {
...
@Value("${spring.redis.maxmemory}")
private String redisMaxMemory;
@PostConstruct
public void startRedis() throws IOException {
int port = isRedisRunning() ? findAvailablePort() : redisPort;
redisServer = RedisServer.builder()
.port(port)
.setting("maxmemory " + redisMaxMemory)
.build();
redisServer.start();
}
...
}
다시 테스트를 실행해 보자
테스트 성공!
마무리
Embedded Redis를 적용하면서 꼬박 하루를 다 사용한 것 같다. 정말 에러가 많이 발생했다. 이렇게 테스트가 통과하면서 마무리되는 줄 알았지만…새로운 문제가 발생했다. 바로 M1(ARM)에서 Embedded Redis를 사용할 수 없었던 것. 해당 내용은 다음 포스팅에 이어서 다루겠다.
'Spring' 카테고리의 다른 글
Spring - 좋은 단위 테스트를 만드는 방법(JUnit) (2) | 2023.04.22 |
---|---|
Spring - 이메일 인증 구현해보기 (랜덤 인증번호 보내기) (9) | 2023.04.21 |
Spring - Redis를 사용해보자 (2) | 2023.04.15 |
Spring - Spring Security + JWT 4편: Access Token 재발급 (2) | 2023.04.14 |
Spring - Spring Security + JWT 적용기 3편: 로그아웃 (3) | 2023.04.14 |