본문 바로가기
server/🟥Redis

Redis와 Spring Scheduler를 사용해서 사이트 일일 방문자 저장하기

by 발개발자 2023. 2. 3.
반응형

Redis 어디다 쓰지...

AWS ElasticCache Redis 를 생성해놓고 어디에 쓸지 째려보기만한지 어언 한달...

드디어 결단의 시간이 왔다.

 

Redis를 사용해서 사이트 일일방문자수를 확인하는데 사용하려고 한다. 

왜 Redis를 사용하냐면, 실시간으로 접속하는 모든 방문자를 캐치하여 즉시 DB에 저장하는 것은 DB Connection과 I/O를 많이 발생시켜 비효율적으로 보였기 때문이다. 

따라서, Redis라는 캐시를 두어 실시간으로 접속하는 방문자를 캐시에 저장해두고 일정시간마다 DB에 저장하여 DB I/O를 줄여보고자 한다. 

 

로직은 다음과 같이 작성하려고 한다.

 

1. 특정 경로로 온 요청에 대해 방문자 Interceptor 작동.

2. Redis에 ip_date 를 key값으로 존재여부 확인 후 저장.

3. Scheduler를 통해 일정시간마다 Redis에 쌓여있는 방문자 데이터를 DB에 저장한 후 Redis clear.

 

간단명료하다.

 

자, 이제 시작해보자. ㄱㅈㅇ~

 

 

환경설정

 

먼저, 의존성 추가를 통해 redis 라이브러리를 사용할 수 있도록 하자.

 

build.gradle

    implementation 'org.springframework.boot:spring-boot-starter-data-redis:2.7.7'

 

 

그리고 본인이 설치한 Redis Server 접속정보를 작성하자.

 

 

application.yml

spring:
  redis:
    port: 6379
    host: your_ip

 

이제 RedisTemplate를 Bean에 등록하여 Spring환경에서 주입받아 사용할 수 있도록 셋팅하자.

 

RedisConfig.java

@Configuration
@EnableRedisRepositories
public class RedisConfig {

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

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

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisHost, redisPort);
    }

    @Bean
    public RedisTemplate<?, ?> redisTemplate() {
        RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        return redisTemplate;
    }

}

 

 

 

Interceptor 생성

 

이제 방문자들의 데이터를 Redis에 저장할 수 있는 Interceptor를 작성하자.

 

SingleVisitInterceptor.java

@Component
@RequiredArgsConstructor
public class SingleVisitInterceptor implements HandlerInterceptor {

    private final RedisTemplate<String, String> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        String userIp = request.getRemoteAddr();
        String userAgent = request.getHeader("User-Agent");
        String today = LocalDate.now().toString();
        String key = userIp + "_" + today;

        ValueOperations valueOperations = redisTemplate.opsForValue();

        if (!valueOperations.getOperations().hasKey(key)) {
            valueOperations.set(key, userAgent);
        }

        return true;
    }

}

 

 

*주의사항

여기서 주의해야할 사항이 있는데, 본인처럼 Nginx나 기타 Proxy를 사용할 경우 RemoteAddr이 127.0.0.1로 받아오게 된다. 아래와 같은 방어코드를 통해 Ip를 추출할 수 있다.

 

    public String getIp(HttpServletRequest request){
        String ip = request.getHeader("X-Forwarded-For");

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
             ip = request.getHeader("Proxy-Client-IP");
        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
             ip = request.getHeader("WL-Proxy-Client-IP");
        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
             ip = request.getHeader("HTTP_CLIENT_IP");
        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
             ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
             ip = request.getRemoteAddr();
        }

        return ip;
    }

 

 

본인은 Spring Security를 사용하기 때문에 아래의 Config에 Interceptor를 추가해줬다.

 

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig implements WebMvcConfigurer {

    private final SingleVisitInterceptor singleVisitInterceptor;

    ...

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(singleVisitInterceptor)
                .addPathPatterns("/mylocation/**");
    }
}

 

Redis-Cli를 통해 데이터가 제대로 저장되는지 확인까지 완료.

 

redis-cli    # cli가 없을 경우 설치 선행필요
keys *

 

 

 

 

 

Scheduler를 통해 Redis와 DB 반영

 

이제 Redis에 쌓인 방문자 데이터를 일정 시간마다 DB에 저장하여 동기화하는 작업을 하면 마무리가 된다.

방문자를 관리할 수 있는 Entity를 생성하여 50분마다 DB에 저장해보자.

 

VisitorEntity.java

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "tb_visitor")
public class VisitorEntity {
    @Id
    @GeneratedValue
    private Long id; // 기본키

    private String userIp;

    private String userAgent;

    private LocalDate date;

}

 

VisitorRepository.java

public interface VisitorRepository extends JpaRepository<VisitorEntity, Long> {

    boolean existsByUserIpAndDate(String userIp, LocalDate date);
}

 

 

VisitorScheduler.java

@Component
@Slf4j
@RequiredArgsConstructor
public class VisitorScheduler {

    private final RedisTemplate<String, String> redisTemplate;

    private final VisitorRepository visitorRepository;

    @Scheduled(initialDelay = 3000000, fixedDelay = 3000000)
    public void updateVisitorData() {
        Set<String> keys = redisTemplate.keys("*_*");

        for (String key : keys) {
            String[] parts = key.split("_");
            String userIp = parts[0];
            LocalDate date = LocalDate.parse(parts[1]);

            ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
            String userAgent = valueOperations.get(key);

            if(!visitorRepository.existsByUserIpAndDate(userIp, date)){
                VisitorEntity visitor = VisitorEntity.builder()
                        .userAgent(userAgent)
                        .userIp(userIp)
                        .date(date)
                        .build();

                visitorRepository.save(visitor);
            }

            redisTemplate.delete(key);
        }
    }
}

 

 

위와 같은 작업을 통해 50분마다 DB에 Redis의 방문자 데이터가 반영이 되고 Redis는 Clear가 된다.

 

이제 일일 테이블에 쌓인 데이터를 통해 일일방문자를 체크할 수 있게 되었다.

 

이번 작업을 통해 Redis를 아주 겉표면만 찍먹해볼 수 있는 좋은 기회였다.

점점 딥하게 사용하며 조금씩 지식도 쌓아나가보자.

 

끗!

반응형

댓글