배경
JWT를 활용한 Spring Security를 작업하면서 Refresh Token을 저장하기 위해서 처음으로 Redis를 Spring에 적용한 후, 로컬 환경과 테스트에서 사용할 Embedded Redis를 힘들게 적용했다. 테스트도 무사히 통과해서 홀가분한 마음으로 풀 리퀘스트를 날리고 머지를 했는데, 조금 뒤 Mac(M1)을 사용하는 팀원이 말했다.
“에러나는데?”
Caused by: java.lang.RuntimeException: Can't start redis server. Check logs for details.
Mac M1(ARM)에서 Embedded Redis가 실행하지 못하는 이유
M1에서 Embedded Redis를 실행하지 못하는 이유는 Redis가 M1의 ARM 프로세서 아키텍처에서 실행되는 것을 지원하지 않기 때문이다. Embedded Redis는 애플리이션이 실행될 때 자동으로 시작되고, 애플리케이션이 종료될 때 Redis도 종료된다.
하지만 Redis가 ARM 프로세서 아키텍처에서 실행되지 않기 때문에 M1에서 Embedded Redis를 실행할 수 없다.
해결 방법
Embedded Redis 라이브러리를 봐도 mac_arm64 바이너리가 없다. 문서에서는 직접 바이너리를 지정해 사용하는 방법을 권장한다.
RediseServer(File executable, int port) 생성자를 통해 문제를 해결해보자
Redis 소스 코드 컴파일
- Redis 다운로드
$ wget <https://download.redis.io/releases/redis-6.0.10.tar.gz>
- 다운 받은 Redis 파일의 압출을 해제한다.
$ tar -xzf redis-6.0.10.tar.gz
- 압축을 해제한 Redis 디렉토리로 이동한다.
$ cd redis-6.0.10
- Redis를 컴파일한다. make 는 소스 코드에서 실행 파일을 만드는 명령어다
$ make
- Redis 서버를 시작한다.
$ src/redis-server
- src/redis-server에 생성된 바이너리의 이름을 변경해서 아래 프로젝트 경로에 추가한다.
- src/main/resources/binary/redis/{redis mac arm 바이너리 파일
- 아키텍처에 따라 다르게 실행될 수 있도록 EmbeddedRedisConfig 클래스를 수정한다.
EmbeddedRedisConfig
@Slf4j
@Configuration
public class EmbeddedRedisConfig {
@Value("${spring.redis.port}")
private int redisPort;
@Value("${spring.redis.maxmemory}")
private String redisMaxMemory;
private RedisServer redisServer;
@PostConstruct
public void startRedis() throws IOException {
int port = isRedisRunning() ? findAvailablePort() : redisPort;
if (isArmArchitecture()) {
log.info("ARM Architecture");
redisServer = new RedisServer(Objects.requireNonNull(getRedisServerExecutable()), port);
} else {
redisServer = RedisServer.builder()
.port(port)
.setting("maxmemory " + redisMaxMemory)
.build();
}
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);
}
private boolean isRedisRunning() throws IOException {
return isRunning(executeGrepProcessCommand(redisPort));
}
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);
}
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());
}
private File getRedisServerExecutable() {
try {
return new File("src/main/resources/binary/redis/redis-server-6.2.5-mac-arm64");
} catch (Exception e) {
throw new BusinessLogicException(ExceptionCode.REDIS_SERVER_EXCUTABLE_NOT_FOUND);
}
}
private boolean isArmArchitecture() {
return System.getProperty("os.arch").contains("aarch64");
}
}
- new RedisServer(Objects.requireNonNull(getRedisServerExecutable()), port) : RedisServer(excutable, port)로 RedisServer를 생성할 수 있다. excutable 은 Redis를 실행하는데 필요한 실행 파일을 지정하는 매개변수이다.
- getRedisServerExecutable() : ARM 아키텍처에서 Redis Server를 실행할 때 사용할 Redis Server 실행 파일을 가져온다. 가져올 파일이 없는 경우 예외를 던진다.
- isArmArchitecture() : 현재 시스템이 ARM 아키텍처인지 확인한다.
테스트
위 코드를 적용한 후 내 PC에서 테스트하는 것은 의미 없기 때문에 M1을 사용하는 팀원분이 실행하도록 했고 에러 없이 깔끔하게 실행됐다.
마무리
Can't start redis server. Check logs for details.
이 문구를 너무 많이 봐서 노이로제 걸릴 것 같다. 그래도 Embedded Redis를 적용한 덕에 다른 팀원도 별도 설치 과정 없이 편하게 Redis를 사용하고 테스트할 수 있어서 고생한 보람은 있었다. 이번 경험으로 이후 적용할 때는 훨씬 빠르게 적용할 수 있을 것 같다.
Redis와 Embedded Redis를 적용했던 글은 아래 링크에서 확인할 수 있다.