Redis

박태훈 ·2024년 10월 24일

면접복기

목록 보기
2/2

Redis??

Redis는 메모리 기반의 데이터 저장소이다(인메모리).
Key-Value 형태의 데이터로 이루어져있으며 모든 데이터를 메모리에 저장하고 필요에 따라 디스크에 백업한다.
매우 빠른 읽기/쓰기를 지원한다.
데이터 구조로는 문자열, 해시, 리시트, Set 을 지원한다.
Redis는 데이터 저장뿐 아니라 다양한 목적으로도 사용이 가능하다.
Redis의 빠른 처리 속도 때문에 ReadOnly데이터만 필요할때만 서브 데이터베이스로 사용이 가능하다. 또한 다양한 메소드를 통해 데이터를 다룰수있다.

내가 면접때 답변을 못한부분은 다음과 같다.
Q.Redis의 데이터 형식은 뭔가요??
A.Redis는 비정형데이터입니다. 테이블에 저장되지 않고 Key-Value형태로 저장되며, 쿼리문을 통해 조회하지 않습니다. 또한 속도가 매우 빠르다는 장점이 있습니다.
Q.아니 그거 말고 정형/비정형말고 어떤 타입의 데이터형태에요??
A. (정형/비정형 아니면 뭐고...) 어..... 메모리 위에서 동작하는 형태의 데이터베이스....입니다...
방식이 인메모리방식이고 인메모리 방식에 대해서 설명해보라는 꼬리질문의 의도를 파악하지 못했다.
그래서 다시 공부한다...

인메모리 방식

인메모리

  • 컴퓨터의 주기억장치인 RAM에 데이터를 올려서 사용하는 방법.
  • RAM에 데이터를 저장하게 되면 메모리 내부에서 처리가 되므로 데이터 저장/조회시 하드디스크를 오가는 과정을 거치지 않아도 되어 속도가 빠름
  • 전원이 꺼지면 가지고 있던 데이터가 삭제되는 특성을 가진다.

정형데이터와의 차이점

데이터베이스를 다룰때 많이 사용하는 관계형데이터베이스인 MYSQL, Oracle과 어떤 차이가 있을까??

위의 사진은 스키마사진의 일부이다.
정형데이터는 비정형데이터와 달리 데이터베이스가 테이블단위로 관리된다.
각각 다른 테이블들은 엄격한 규칙을 통한 관계로 이루어져있으며 쿼리를 통해 데이터를 처리, 가공한다.
하지만 Redis는 비정형데이터다.
정형데이터의 정반대의 데이터라고 볼수있다. Redis는 각 데이터가 Key-Value의 형태로 구성되어있다.

정형데이터 구조


현재 내 로컬DB에 저장되어있는 테이블의 데이터이다.
주식모의투자 프로젝트를 진행할때 사용했던 테이블인데 uuid는 배포가 끝났으니 알아가도 뭐 써먹을곳이 없을거임.
이렇게 정형데이터는 테이블이라는 구조안에 데이터가 저장된다.
쉽게 설명하자면 Column은 데이터베이스의 이름과 같은것이다. amount, member_stock_id 와 같은 항목들이 Column이다.
Row는 말그대로 행 즉 각 컬럼들의 해당하는 값이다.
데이터가 추가될수록 Row들이 한줄씩 쌓이면서 저장된다.

비정형데이터 구조

내가 실제로 Redis를 사용하면서 다룬 데이터중 하나이다.
stock이라는 디렉토리 안에 stock:005930이라는 Key안에 90000:1.87:91000:92000 라는 Value가 저장되는 형태이다. 이렇게 Key-Value형태로 저장이 되는데 이는 MongoDB도 마찬가지이다.


Redis왜쓸까?

서버부하 개선

부하랑 뭔상관??

  • 인메모리 방식이 아닌 DB는 저장장치에 저장된다.
  • DB는 서버가 다운되더라도 데이터가 손실되지 않음 하지만 저장장치에 액세스 해야하기 때문에 사용자가 많아질수록 부하가 걸려서 느려질 가능성이 있다.
  • 이때 캐시서버 를 이용해 부하를 줄일수있다.
  • 마치 컴퓨터의 캐시 메모리와 비슷한 맥락이라고 생각하면될거같다.
  • 그래서 Redis의 빠른 읽기/쓰기 속도를 이용해 캐시서버 전략을 사용한다.

Redis사용방법

개발환경

  • Java - 17
  • SpringBoot - 3.2.5
  • ORM - JPA

적용이유

  • User SignIn 시 Token이 발급된다
  • RefreshToken 을 저장하기 위해 Redis 사용
  • 회원이 로그인시 새로운 토큰을 발급해야하는데 Update문이 자주 일어나기 때문에 Redis를 이용해서 서버 부하를 줄이기 위해 사용

Redis Config파일 작성

@Configuration
@RequiredArgsConstructor
@Slf4j
public class RedisConfig {

    private final Environment environment;

    @Value("${spring.data.redis.port}")
    private int port; 

    @Value("${spring.data.redis.host}")
    private String host;

    @Value("${spring.data.redis.password}")
    private String password;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        log.info("host = {}", host);
        log.info("port = {}", port);
        log.info("password = {}", password);


        RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
        redisConfig.setHostName(host);
        redisConfig.setPort(port);
        redisConfig.setPassword(password);

        return new LettuceConnectionFactory(redisConfig);

    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() 
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        return redisTemplate;
    }

}

RedisConfig 파일을 등록해줍니다.
Config파일을 작성하는 이유는 SpringBoot 애플리케이션과 Redis의 연결을 설정하고, RedisTemplate을 통해 Redis에 데이터를 쉽게 저장하고 조회할수 있도록 하기 위함입니다.
데이터형식이 Key-Value로 되어있기 때문에 이를 Json파일로 변환하기 위한 역직렬화 과정이 필요합니다. 이를 하기 위해 RedisTemplate을 구성했습니다.

@Bean
public RedisTemplate<String, Object> redisTemplate() {
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setValueSerializer(new StringRedisSerializer());
    redisTemplate.setConnectionFactory(redisConnectionFactory());

    return redisTemplate;
}
  • RedisTemplate을 Bean으로 등록해줍니다.
  • Redis의 데이터를 직렬/역직렬화 하기 위해 선언한 메소드입니다.
  • KeySerializer & ValueSerializer: Redis에 데이터를 저장할 때 직렬 화/역직렬화를 담당합니다.
  • 기본적으로 Redis는 데이터를 바이너리 형태로 저장하기 때문에, 사람이 읽을 수 있도록 key와 value를 문자열로 변환하는 StringRedisSerializer를 설정합니다.
  • 이를 통해 Redis CLI에서 데이터를 확인할 때 알아볼 수 있는 형태로 출력됩니다.
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
  • setKeySerializer(new StringRedisSerializer()) :Redis의 Key를 문자열로 직렬/ 역직렬화 합니다. Redis의 Key가 문자열로 저장되며, 이를 조회할때도 문자열로 역 직렬화 됩니다.
  • setValueSerializer(new StringRedisSerializer()) : Redis의 Value를 문자열로 직렬화/역직렬화 합니다. 위와 똑같이 문자열로 저장됩니다.

적용부분

@Service
@RequiredArgsConstructor
@Slf4j
public class SignInService implements SignInUsecase {

    private final ModelMapper modelMapper;
    private final LoadSignInPort loadSignInPort;
    private final JWTUtil jwtUtil;
    private final AuthenticationManager authenticationManager;

    @Override
    public SignInResponseDto SigninService(SigninRequestDto signinRequestDto){
        SignIn signIn = modelMapper.map(signinRequestDto, SignIn.class);
        UuidDto Member = loadSignInPort.signIn(signIn);

        if(Member != null) {
            Authentication authentication = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            Member.getUuid(),
                            signIn.getPassword()
                    )
            );

            String accessToken = jwtUtil.createAccessToken(authentication); // 사용자 Access 토큰 생성
            String refreshToken = jwtUtil.createRefreshToken(authentication); // 사용자 Refresh 토큰 생성

            loadSignInPort.changeStatusLogIn(Member.getMemberId());

            return SignInResponseDto.builder()
                    .name(signIn.getName())
                    .accessToken(accessToken)
                    .refreshToken(refreshToken)
                    .build();

        } else {
            throw new CustomException(BaseResponseCode.SIGNIN_FAILED);

        }
    }
}
  • 로그인시 실행되는 서비스 로직입니다.
  • 토큰이 저장되는 방식만 블로그에 서술하기 때문에 해당 로직의 앞선 모든 과정들은 생략했습니다.
  • jwtUtil 클래스는 Token 을 생성하는 클래스입니다.
  • createAccessToken, createRefreshToken이 실행됩니다.

Toekn발급

@Component
@RequiredArgsConstructor
public class JWTUtil {

    private final RedisTemplate<String, String> redisTemplate;
    private final UserDetailsService userDetailsService;

    @Value("${JWT.access-expiration}")
    private long accessTokenExpiration;

    @Value("${JWT.refresh-expiration}")
    private long refreshTokenExpiration;

    @Value("${JWT.SECRET_KEY}")
    public String secretKey;


    public String createAccessToken(Authentication authentication){ //AccessToken 생성
        Claims claims = Jwts.claims().setSubject(authentication.getName());
        Date now = new Date();
        Date expireDate = new Date(now.getTime() + accessTokenExpiration);

        return Jwts.builder()  //AccessToken 리턴
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

    public String createRefreshToken(Authentication authentication){ //refreshToken 생성
        Claims claims  = Jwts.claims().setSubject(authentication.getName());
        Date now = new Date();
        Date expireDate = new Date(now.getTime() + refreshTokenExpiration);

        String refreshToken = Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();


        redisTemplate.opsForValue().  //redis에 refreshToken 저장
                set(authentication.getName(), refreshToken,
                refreshTokenExpiration, TimeUnit.MILLISECONDS);

        return refreshToken; //refreshToken 리턴
    }

    public String remakeAccessToken(String uuid){ //AccessToken 재발급
        Claims claims = Jwts.claims().setSubject(uuid);
        Date now = new Date();
        Date expireDate = new Date(now.getTime() + accessTokenExpiration);

        String AccessToken = Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();

        return AccessToken;
    }
}
  • 토큰이 생성되는 전체코드입니다.
  • RedisTemplate을 들고왔습니다. 직렬화/ 역직렬화 할때 필요하기 때문이죠.
public String createAccessToken(Authentication authentication){
    Claims claims = Jwts.claims().setSubject(authentication.getName());
    Date now = new Date();
    Date expireDate = new Date(now.getTime() + accessTokenExpiration);

    return Jwts.builder()  //AccessToken 리턴
            .setClaims(claims)
            .setIssuedAt(now)
            .setExpiration(expireDate)
            .signWith(SignatureAlgorithm.HS256, secretKey)
            .compact();
 }
  • JWT토큰을 발급 받기 위해서 User의 name을 넘겨줍니다.
  • 토큰 만료 시간을 설정하기 위해 현재 시각과 환경변수로 설정해준 만료 시간을 더합니다.
  • 해당 메소드는 AccessToken을 return해주기 위한 메소드입니다.
  • Redis에 저장하지는 않습니다.
public String createRefreshToken(Authentication authentication){
    Claims claims = Jwts.claims().setSubject(authentication.getName());
    Date now = new Date();
    Date expireDate = new Date(now.getTime() + refreshTokenExpiration);

    String refreshToken = Jwts.builder()
            .setClaims(claims)
            .setIssuedAt(now)
            .setExpiration(expireDate)
            .signWith(SignatureAlgorithm.HS256, secretKey)
            .compact();


    redisTemplate.opsForValue().set(
            authentication.getName(), refreshToken,
            refreshTokenExpiration, TimeUnit.MILLISECONDS
    );

    return refreshToken;
}
  • 자 드디어 Redis에 저장하는 메소드입니다.
  • 해당메소드는 RefreshToken을 저장하는 메소드입니다.
  • 토큰 발급 과정은 위의 메소드랑 똑같이 동작합니다.
  • redisTemplate.opsForValue().set(
    authentication.getName(), refreshToken,
    refreshTokenExpiration, TimeUnit.MILLISECONDS
    );
    • user의 name을 Key로 설정합니다.
    • 생성된 refreshToken을 value 로 저장합니다.
    • Redis에서는 TimeUnit 메소드를 이용해 데이터 저장시간을 설정할수있습니다.
    • refreshTokenExpiration의 시간이 지나면 자동으로 Redis에서 삭제됩니다. refreshToken의 만료시간은 일주일로 잡았습니다.
  • 제가 사용해본 Redis 경험을 토대로 작성했습니다.
  • 이거 말고 다른게 사용한것도 있는데 다음에 여유있을때 다뤄보도록하겠습니다.

정리

1.Redis는 인메모리방식의 데이터베이스이다.
2.Redis는 비정형데이터이다 Key-Value형태로 저장된다.
3.Redis는 캐싱작업을 할때 사용된다.
4.나는 로그인 기능 구현시 JWT토큰 저장소용도로 사용했다.
5.다른 서비스에서도 사용했지만 시간나면 한번더 다루겠다.


참고

https://devlog-wjdrbs96.tistory.com/374
https://aws.amazon.com/ko/elasticache/what-is-redis/
https://github.com/hoontaepark/TMT-BE-MemberServer

profile
아직말하는감자개발자

0개의 댓글