소셜 커머스나 소셜 네트워크 사이트를 서핑하다 보면,
항상 특정 게시물에 대한 "좋아요" 기능이 있는 걸 볼 수 있다.
그럼 해당 "좋아요" 기능은 어떤식으로 구현할 수 있을까?
첫 번째로는 아래와 같이 RDBMS에 좋아요 정보를 저장하는 방법이 있다.
RDBMS로 좋아요를 구현할 때의 장점
1) 데이터 무결성과 일관성
RDBMS는 엄격한 데이터 무결성 규칙을 제공하며,
ACID(Atomicity, Consistency, Isolation, Durability)
속성을 통해 데이터의 정확성과 일관성을 보장한다.
이는 좋아요 수와 같이 정확한 계산이 필요한 기능에 적합하다.
2) 복잡한 쿼리와 조인 지원
관계형 데이터베이스는 복잡한 쿼리와 다양한 테이블 간의 조인(JOIN)을 지원한다.
사용자, 게시물, 좋아요 관계 등
여러 테이블의 데이터를 결합하여 분석하고 보고서를 생성하는 데 유리하다.
3) 표준화된 도구와 경험
SQL은 잘 정립된 쿼리 언어로, 많은 개발자가 익숙하다는 장점이 있다.
또한, RDBMS는 많은 표준화된 도구와 기술 지원을 받을 수 있는 장점이 있다.
RDBMS로 좋아요를 구현할 때의 단점
1) 확장성 문제
RDBMS는 수직 확장(서버 성능 향상)에는 유리하지만,
수평 확장(서버의 양적 증가) 이 어렵다.
대규모 분산 시스템에서는 데이터베이스 확장이 복잡하고 비용이 많이 들게 된다.
2) 쓰기 성능 문제
대규모 동시 사용자와 높은 쓰기 작업이 발생하는 서비스에서는
쓰기 지연이나 락 경합 문제가 발생할 수 있다.
예를 들어, 많은 사용자가 동시에 같은 게시물에 좋아요를 누르는 경우,
데이터베이스의 성능 저하가 발생할 수 있다.
3) 비용
고성능 RDBMS는 종종 라이선스 비용이 많이 들며, 하드웨어 비용도 무시할 수 없다.
대규모 트래픽을 처리하려면 고사양의 서버가 필요할 수 있어 비용이 증가한다.
여기서 가장 문제가 되는 것은 쓰기 성능 문제로 볼 수 있다.
일반적으로 우리가 많이 사용하고 있는
Facebook, Instagram 과 같은 SNS는 엄청난 규모의 사용자가 존재한다.
즉 시스템 자체가 대규모 시스템이라는 것인데,
방대한 "좋아요" 클릭 트래픽이 발생할 경우에
해당 데이터의 읽기, 쓰기 기능을 RDBMS를 통해 처리를 한다면,
데이터 무결성과 일관성을 보장을 받겠지만,
가장 중요한 사용자 경험을 떨어뜨릴 가능성이 높다.
더군다나 "좋아요" 깉은 데이터는
결제나 주문과 같이 무결성과 일관성이 꼭 필요한 작업과 다르게,
무결성, 일관성보다는 빠른 쓰기, 읽기 처리가 필요한 데이터이다.
그래서 대부분 대규모 시스템에서는 "좋아요" 기능을 RDBMS 가 아닌
NoSQL을 사용해서 구현한다.
대표적으로 Redis 를 사용하는데,
Redis는 디스크가 아닌 메모리에 직접 데이터를 저장한다.
RDBMS 에서 가장 빠르다고 알려진 NVMe ssd를 사용한다고 해도
Redis 의 메모리의 읽기 쓰기 속도가 10 ~ 100 배 정도까지 빠르다.
Redis를 사용하여 좋아요 기능을 구현하게 될 때 장점
1) 빠른 응답 시간
Redis는 모든 데이터를 메모리에 저장하기 때문에
디스크 I/O에 비해 훨씬 빠른 읽기 및 쓰기 속도를 제공한다.
이는 좋아요 버튼 같은 고응답성이 요구되는 기능에 이상적이다.
사용자의 좋아요 클릭은 거의 즉각적으로 처리되며,
사용자 경험을 크게 향상시킨다.
2) 간단한 스케일 아웃
Redis는 수평적으로 확장이 가능하여,
클러스터를 통해 더 많은 노드를 추가하고 부하를 분산시킬 수 있다.
이를 통해 더 높은 트래픽과 데이터량을 쉽게 처리할 수 있으며,
시스템의 가용성과 내결함성을 높일 수 있다.
3) 데이터 구조의 다양성
Redis는 문자열, 리스트, 세트, 정렬된 세트 등 다양한 데이터 구조를 지원한다.
좋아요 기능을 구현할 때, 정렬된 세트를 사용하여 사용자 ID와 타임스탬프를 저장하고,
빠르게 접근할 수 있다.
이는 복잡한 쿼리 없이도 높은 성능을 유지할 수 있게 해 준다.
4) 원자성 및 트랜잭션 지원
Redis의 모든 명령은 원자적으로 실행된다.
또한, 멀티 명령을 사용하여 여러 작업을 원자적으로 처리할 수 있어,
데이터의 일관성을 보장할 수 있다.
예를 들어, 사용자의 좋아요를 카운트하는 동시에
최신 좋아요 목록을 업데이트하는 작업을 동시에 수행할 수 있다.
5) 메모리 기반의 카운팅
Redis는 카운터 기능을 매우 효율적으로 지원한다.
INCR과 DECR 명령을 사용하여 좋아요 수를 즉시 증가시키거나 감소시킬 수 있다.
이 방법은 매우 빠르며, 대규모 분산 환경에서도 잘 작동한다.
6) 간편한 설정 및 유지 보수
Redis는 설정이 간단하고, 유지 보수가 쉬워서 관리 부담이 낮다.
개발자들은 Redis의 성능과 기능에 집중할 수 있으며,
복잡한 데이터베이스 최적화 작업을 크게 줄일 수 있다.
좋아요 구현
그럼 실제로 "좋아요" 기능을 구현해 보자.
backend 시스템으로는 RUST를 사용해서 구현해 보겠다.
일단 front단에서 유저가 특정 게시물의 "좋아요"를 눌러주면
아래와 같은 api를 호출해 준다고 가정하자.
https://127.0.0.1:8080/screen/likeTest
그리고 해당 부분 호출되는 소스코드는 아래와 같다.
아래는 라우팅 소스코드이다.
pub fn config(cfg: &mut web::ServiceConfig) {
let auth_scope = web::scope("/screen")
.route("/likeTest", web::post().to(like_test));
cfg.service(auth_scope);
}
아래는 핸들러 함수이다.
async fn like_test(di_container: web::Data<DIContainer>, json_request: web::Json<BestSeenInput>) -> impl Responder {
let pic_service = di_container.pic_service().as_ref();
let _ = match pic_service.set_like_pic_infos(*json_request.user_seq(), *json_request.pic_seq()).await {
Ok(_) => (),
Err(err) => {
error!("{:?}",err);
return HttpResponse::Unauthorized().finish();
}
};
let _ = match pic_service.get_like_pic_count(*json_request.pic_seq()).await {
Ok(_) => (),
Err(err) => {
error!("{:?}",err);
return HttpResponse::Unauthorized().finish();
}
};
HttpResponse::Ok().json("Like Success")
}
set_like_pic_infos()는 아래의 로직을 실행시키는 함수이다.
현재 유저가 해당 게시물에 이미 "좋아요"를 눌렀는지 확인.
ㄴ 좋아요를 이미 눌렀다면, 그냥 Ok(()) 리턴
ㄴ 좋아요를 누른 적이 없는 경우
ㄴ 해당 게시물에 좋아요 누르기
ㄴ 해당 게시물의 좋아요 카운트 늘리기.
async fn set_like_pic_infos(&self, user_seq_int: i64, pic_seq: i64) -> Result<(), anyhow::Error> {
let like_yn = self.repository.get_zscore_pic(&format!("likePhoto:{}",pic_seq), user_seq_int).await?;
if like_yn == -1 {
// When a specific user clicks on a specific photo, the information is stored in Redis.
let _ = self.repository.set_zadd_pic("likePhoto", user_seq_int, pic_seq).await?;
// Increase the click count of the photo and store the data in Redis.
let _ = self.repository.set_zincrby_pic("photoLikeCounts" ,pic_seq).await?;
}
Ok(())
}
get_zscore_pic() 함수의 형태는 아래와 같다.
해당 함수는 현재 유저가 해당 게시물에 이미 "좋아요"를
눌렀는지 확인해 주는 함수라고 보면 된다.
async fn get_zscore_pic(&self, key_str: &str, pic_seq: i64) -> Result<i32, anyhow::Error> {
let mut redis_conn: redis::cluster_async::ClusterConnection = self.redis_client.get_redis_conn().await?;
let res: Option<i32> = redis_conn.zscore(key_str, pic_seq).await?;
match res {
Some(res) => Ok(res),
None => Ok(-1)
}
}
해당 함수는 Redis에 아래의 명령어를 실행해서 응답을 가져오는 것이다.
ZSCORE key member
ZSCORE는 정렬된 집합(sorted set)에서
주어진 멤버(member)의 점수(score)를 반환하는 데 사용된다.
만약 특정 게시물 번호가 200이고
해당 게시물을 좋아요 한 유저의 고윳값이 120이라면,
아래와 같은 커맨드가 실행될 것이다.
ZSCORE likePhoto:200 120
만약 120번 유저가 200번 게시물에 좋아요를 눌렀을 경우에
nil을 반환할 것이고 위의 소스코드에서는 -1에 맵핑될 것이다.
set_zadd_pic() 함수는 아래와 같다.
해당 함수가 유저가 특정 게시물에 "좋아요"를 눌렀을 때,
해당 정보를 Redis에 저장해 주는 함수이다.
async fn set_zadd_pic(&self, key_str: &str, user_seq_int: i64, pic_seq: i64) -> Result<(), anyhow::Error> {
let mut redis_conn: redis::cluster_async::ClusterConnection = self.redis_client.get_redis_conn().await?;
let now_timestamp = Utc::now().timestamp();
let key_str = format!("{}:{}", key_str, pic_seq);
let _ = redis_conn.zadd(key_str, user_seq_int, now_timestamp).await?;
Ok(())
}
해당 함수는 Redis에서 아래의 명령어를 실행해서 응답을 가져와준다.
ZADD key score member
ZADD는 정렬된 집합(sorted set)에 하나 이상의 멤버와
그에 해당하는 점수를 추가하는 데 사용된다.
이 명령은 Redis의 정렬된 집합 데이터 타입을 활용하여
멤버를 점수에 따라 자동으로 정렬하고,
중복을 허용하지 않는 유니크한 멤버 관리를 가능하게 한다.
만약 120번 유저가 200번 게시물에 좋아요를 눌렀다면
ZADD 명령어는 아래와 같이 실행된다.
ZADD likePhoto:200 timestamp 120
여기서 timestamp는 현재의 시각을 의미한다.
set_zincrby_pic()는 특정 게시물의 좋아요 카운트를 늘려주는 함수이다.
async fn set_zincrby_pic(&self, key_str: &str, pic_seq: i64) -> Result<(), anyhow::Error> {
let mut redis_conn: redis::cluster_async::ClusterConnection = self.redis_client.get_redis_conn().await?;
let _: () = redis::cmd("ZINCRBY")
.arg(key_str)
.arg(1)
.arg(pic_seq)
.query_async(&mut redis_conn)
.await?;
Ok(())
}
해당 함수는 Redis에서 아래의 명령어를 실행해서 응답을 가져와준다.
ZINCRBY key increment member
ZINCRBY 명령은 정렬된 집합(sorted set)의
특정 멤버의 점수를 증가시키거나 감소시키는 데 사용된다.
이 명령은 멤버가 이미 존재하는 경우 그 점수에 주어진 값을 더하며,
멤버가 존재하지 않는 경우 새로운 멤버를 추가하고 주어진 값으로 시작한다.
특정 유저가 200번 게시물에 좋아요를 눌렀다면,
아래와 같은 명령어가 실행된다.
photoLikeCounts 는 특정 게시물 좋아요 정보 정렬집합 키라고 생각하면 된다.
ZINCRBY photoLikeCounts 1 200
get_like_pic_count()는 현재 해당 게시물의 좋아요 개수를 카운트 해주는 함수이다.
위에서 본 ZCOUNT 명령어를 사용해서,
photoLikeCounts 정렬키에서 특정 게시물정보에 대한 count 정보를 가져온다.
그럼 서버를 실행시키고 직접 결과를 확인해보자.
request 는 POSTMAN 을 통해 진행하였다.
10 번 유저가 77 번 게시물에 좋아요를 눌러준 것이다.
해당 요청을 보낸 다음 Redis 데이터를 확인해보자.
위와 같이 timestamp 값이 잘 들어간것을 알 수 있다.
그리고 해당 게시물의 좋아요 카운트도 확인해보자.
10번 유저가 77번 게시물에 처음으로 좋아요를 눌렀으니,
1이 나오는걸 볼 수 있다.
이번에는 30 번 유저가 77번 게시물에 좋아요를 눌렀다고 가정해보자.
그럼 위와 같이 77번 게시물에 대한 좋아요 카운트가 2로 늘어난 것을 볼 수 있다.
그런데, 이미 좋아요를 누른 30번 유저가 77번 게시물에
다시 좋아요를 누른다면 어떻게 될까?
위의 로직에서 중복으로 좋아요 처리를 막았으므로
해당 게시물의 좋아요 카운트는 더이상 늘지 않는것을 볼 수 있다.
'Redis' 카테고리의 다른 글
[Redis] key 삭제 api 구현 (0) | 2023.06.12 |
---|---|
[Redis] Key 삭제 (0) | 2023.05.24 |
[Redis] SCAN (2) | 2023.03.28 |
[Redis] Redis 설치 (0) | 2022.10.12 |
[Redis] Redis Cluster (0) | 2022.08.11 |