조회수를 레디스에 저장,조회하고 RDB에 batch를 활용하여 동기화 하는 방식
1. RedisConfig 클래스 -우선 레디스 연결 객체를 만든다.
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
//yml에 기입해둔 Redis 호스트명과 port번호를 Value어노테이션을 통해 가지고와 속성으로 선언해둔다.
//왜냐 레디스의 설정을 담당하는 RedusConnectionFactory객체를 만드는데 우리가 연결할
//레디스 서버의 host명과 port번호가 필요하니까
//조회수 관련
@Bean
@Qualifier("viewCount")
public RedisConnectionFactory redisConnectionFactoryForClickCount(){
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
configuration.setHostName(host);
configuration.setPort(port);
configuration.setDatabase(4); //레디스에 사용할 데이터베이스 인덱스
return new LettuceConnectionFactory(configuration); //위에서 설정한 configuration을 기반으로 레디스에 연결할 객체
}
//먼저, 레디스의 설정을 담당하는 RedisStandaloneConfiguration 객체를 생성하고,
//설정객체에 연결될 Redis의 호스트명, 포트번호, 그리고 데이터베이스 번호까지 설정해준다.
//이 설정객체를 배개변수로 하여 LettuceConnentionFactory라는 레디스와 물리적으로 연결하는 객체를 생성한다.
@Bean
@Qualifier("viewCount")
public RedisTemplate<String,String> redisTemplateForClickCount(@Qualifier("viewCount") RedisConnectionFactory redisConnectionFactoryForClickCount){
RedisTemplate<String,String> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactoryForClickCount);
return redisTemplate;
}
//RedisConnectionFactory가 레디스의 물리적인 연결을 하는 객체라면, RedisTemplate는 물리적인
//연결을 기반으로 레디스 내의 데이터를 읽고/쓰는 기능을 제공하는 객체다. 그래서 매개변수로 위에서 설정한 물리적 연결객체가 필요한 것!
//그리고 레디스는 기본적으로 데이터를 바이너리 데이터를 저장하고 꺼내는데,Key와 Value를
//사람이 읽을 수 있는 String 형태로 변환해주는 설정이다.
}
2.RedisServiceForViewCount클래스 - 조회수 기능과 관련한 메서드를 구현한다(조회수 증가, 조회수 조회, RDB 동기화)
-여기서는 어뷰징 방지를 막기위해 한 사람이 계속 조회하여 조회수를 증가시키는 것을 막는다.
-그래서 redis에 TTL설정을 통해 어뷰징 방지용 로직을 조회수 증가 메서드에 걸어둔다.
@Service
@Transactional
public class RedisServiceForViewCount {
//"post:viewCount"를 아래 코드를 보면 중복으로 사용하는 곳이 많아, 상수로 선언하여 편하게 사용(나중에 키값이 바뀌더라도 한번에 수정하기 용이해서 상수선언하였음)
private static final String VIEW_COUNT_PREFIX = "post-viewCount-"; // 레디스에 저장 될 조회수 키
private static final String VIEW_LOCK_PREFIX = "post-viewLock-"; // 레디스에 저장될 어뷰징 방지용 키
@Qualifier("viewCount")
private final RedisTemplate<String,String> redisTemplate;
public RedisServiceForViewCount(@Qualifier("viewCount") RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
//RedisTemplate의존성 주입
//레디스에서 조회수 증가시키는 함수
public void increaseViewCount(Long postId,String userId){
String key = VIEW_COUNT_PREFIX + postId;
String lockKey = VIEW_LOCK_PREFIX + postId +"-" + userId;
if(Boolean.TRUE.equals(redisTemplate.hasKey(lockKey))){
return;
} //유저아이디와 아이디가 본 게시물id를 조합한 키값이 이미 있다면 그 유저는 그 게시물을 1분이내에 봐서 조회수를 증가시켰다는 의미로 아래 로직을 타지않고 리턴시킴
//그런데 redisTemplate.hasKey(lockKey).equals(true)를 하지 않는 이유는 앞에서 null값이면 에러가 터지니까 반대로 적용하였음
redisTemplate.opsForValue().increment(key); //레디스에 해당 해당 게시물id키값에 대해 조회수를 1증가시킴
redisTemplate.opsForValue().set(lockKey,"1", Duration.ofMinutes(1)); // 유저아이디와 아이디가 본 게시물id를 조합한 키값에 대한 밸류를 1로 설정하고 1분뒤 삭제
}
//레디스에서 조회수 가지고 오는 함수
public int getViewCount(Long postId){
String key = VIEW_COUNT_PREFIX + postId;
String countStr = redisTemplate.opsForValue().get(key); //레디스에 해당 키가 없으면 countStr = null이 됨
return countStr == null ? 0 : Integer.parseInt(countStr);
}
//레디스에 저장되어 있는 데이터 rdb반영하기 위한 작업
//rdb에 어떤 포스트가 몇개의 조회수가 있는지를 동기화 해야하니까, postId와 해당 조회수가 필요하다.
//그래서 postID:조회수로, 키/밸류 형태로으로 값을 저장하기 편하니 Map클래스를 사용한다.
public Map<Long,Integer> getAllViewCountForRdb(){
Set<String>keys = redisTemplate.keys(VIEW_COUNT_PREFIX+"*"); //.keys는 특정패턴을 가진 모든 키 목록을 가지고 오는 것.우리는 post-viewCount-로 시작되는 모든 키를 가지고 온다.
Map<Long,Integer> viewCounts = new HashMap<>(); //게시글의 ID와 조회수가 들어갈 맵
if(keys !=null){
for(String key :keys){ //현재 키는 prefix가 붙어있는 post-viewCount+게시글ID형태로 되어있다.
Long postId = Long.parseLong(key.replace(VIEW_COUNT_PREFIX,""));//그래서 키값에서 prefix(접두사)인 post-viewCount-를 없애면 id값만 남는다.
String countStr = redisTemplate.opsForValue().get(key) //조회수를 가지고 오고
Integer count = (countStr == null) ? 0: Integer.parseInt(countStr) // 만약에 조회수가 null이면 0 조회수가 있다면 정수화
viewCounts.put(postId, count == null);
}
}
return viewCounts;
}
}
3. PostService클래스 - 게시물 상세조회 메서드 로직
-게시글 보기가 일어날때 조회수가 1증가해야하니까 여기서 위에서 구현한 조회수 증가메서드를 사용한다.
이때 post를 컨트롤러 단에 반환할 때 해당 게시글의 조회수가 담겨서 반환하도록 DTO를 구현해놓았기 때문에 조회수 조회메서드도 사용되었음.
public PostDetailDto findById(Long id){
Post post = postRepository.findByIdAndDelYN(id,DelYN.N).orElseThrow(()->new EntityNotFoundException("없는 게시글입니다"));
String userId = SecurityContextHolder.getContext().getAuthentication().getName();
redisServiceForViewCount.increaseViewCount(post.getId(),userId); //해당 포스트에 대해 조회수 1증가시킴
return post.toDetailDto(redisTemplate, redisServiceForViewCount.getViewCount(id));
}
4.Batch클래스- 일정한 주기로 레디스에 있는 조회수 값을 RDB에 동기화
@Service
@Transactional
public class ViewCountBatchService {
private final RedisServiceForViewCount redisServiceForViewCount;
private final PostRepository postRepository;
public ViewCountBatchService(RedisServiceForViewCount redisServiceForViewCount, PostRepository postRepository) {
this.redisServiceForViewCount = redisServiceForViewCount;
this.postRepository = postRepository;
}
//게시글 DB에 조회수 데이터를 넣어줘야 하니까 위의 2개의 의존성 주입
@Scheduled(fixedRate = 600000) //10분마다 실행
public void syncViewCounts(){
Map<Long,Integer> viewCounts = redisServiceForViewCount.getAllViewCountForRdb();
// 내 방식 => 더티체킹을 활용하는 것.쉬운코드.
// for(Long key :viewCounts.keySet()){ -->keySet은 map자료에서 키 목록만 뽑아오는 것
// Post post = postRepository.findById(key).orElseThrow(()->new EntityNotFoundException("없는 게시물입니다"));
// post.updateViewCounts(viewCounts.get(key));
// }
// gpt방식 => 대량의 데이터 업데이트에 유리(네이티브 쿼리 사용하여 성능에 주안점을 둔 커뮤니티 사이트에 맞는 방식이라고 함)
for(Map.Entry<Long, Integer> entry : viewCounts.entrySet()){
Long postId = entry.getKey();
Integer views = entry.getValue();
postRepository.increaseViewCountByValue(postId,views);
// 지금 increseViewCountByValue메서드는 레디스에 있는 조회수로 업데이트 하는 걸로 했는데 원래라면 레디스에 있는 조회수 만큼 증가시키는 걸로 바꾸고
// 여기에 레디스에 있는 조회수값을 삭제하는 코드가 추가되었어야함
// 그런데 우리 프로젝트에선 게시물의 조회수를 db에서 갖고 오는게 아니라 레디스에서 계속 가지고 오는 걸로 세팅했기 때문에 이렇게 코드를 짬.(실제 커뮤니티에서 조회수는 그냥 db에서 가지고 오는게 나을듯)
}
5.PostRepository 클래스 - 네이티브 쿼리를 이용하여 어떤 게시글의 조회수를 변경하여 저장함.
@Repository
public interface PostRepository extends JpaRepository<Post, Long>{
@Modifying // 업데이트,딜리트 쿼리를 실행하기 위한 JPA어노테이션
@Query("Update Post p SET p.viewCount = :newview WHERE p.id = :postId") //매개변수로 준 게시물 id에 해당하는 게시물의 조회수칼럼을 매개변수로 준 newview로 업데이트한다는 말
@Transactional
// 여기서 Param은 쿼리 내의 :postId와 :increment와 매핑되기 위해 사용한다.
public void increaseViewCountByValue(@Param("postId")Long postId, @Param("newview")Integer newview);
}
'커뮤니티 플랫폼 프로젝트' 카테고리의 다른 글
[AWS]백엔드 배포 오토 스케일링 정리-2 (0) | 2025.03.20 |
---|---|
[AWS]백엔드 배포 흐름 정리-1 (1) | 2025.03.17 |
[AWS] 프론트 배포 흐름 정리 (0) | 2025.03.17 |
[Spring] 좋아요 기능(Redis,RabbitMQ활용) (0) | 2025.02.28 |