Redis 란?

Redis 는 Key-Value 형태로 데이터를 관리하는 오픈 소스이다. Redis 는 빠른 속도와 간편한 사용법으로 인해 캐시, 인증 토큰, 세션 관리 등등 여러 용도로 사용되고 있다. Redis는 빠른 속도 대신 서버가 꺼지면 데이터가 휘발되는 특징이 있는데, 이번 프로젝트에선 Redis를 채팅룸 저장과 GameStartSet을 잠시 저장하는 DB로 사용하는 용도로 채택하였다. 

  • In-Memory Data Strucutre Store 
  • Key - Value 형태로 데이터 저장
  • 여러 가지 Value 타입 저장 가능 (String, Set, Hash, List 등등..)
  • Single Thread
  • 데이터 만료 시간 지정 가능 -> refresh토큰에 사용된다고 한다.

1. macOS에 Redis 설치

 

2. 전제 조건

Homebrew가 설치되어 있는지 확인하기위해 터미널에서 다음을 실행한다.

  • brew --version

 

3. 설치

터미널에서 다음을 실행한다. 시스템에 Redis가 설치된다.

 
  • brew install redis

 

4. Redis 시작 및 중지하기

1) Redis 설치를 테스트하기 위해 터미널창에 다음을 입력한다. 만약 정상적으로 실행이 된다면, log가 쭈루루룩 올라오는 것을 확인할 수 있다. 중지하려면 Ctrl-C를 입력한다.

  • redis-server

2) 위의 명령어를 대신해 아래와 같은 명령어로 실행할 수도 있다. (포그라운드에서 Redis를 실행하는 대신 launchd를 사용하여 프로세스를 백그라운드에서 시작할 수도 있다. 이렇게 하면 Redis가 시작되고 로그인 시 다시 시작된다.)

 
  • brew services start redis

3) 중지하려면 아래의 명령어를 입력한다.

  • brew services stop redis

 

5. Redis 연결

아래의 명령어로 레디스에 연결한다.

  • redis-cli

만약 한국어깨짐 현상이 있다면 아래 명령어를 입력해 준다.

  • redis-cli --raw

 

6. Redis 명령어

참고 링크 -> https://redis.io/commands/

1) String

  • 저장
    • set {key} {value} : key, value 를 저장
    • mset {key} {value} [{key} {value} ...] : 여러 개의 key, value 를 한번에 저장
    • setex {key} {seconds} {value} : key, seconds, value 저장 (설정한 시간 뒤에 소멸)
  • 조회
    • keys * : 현재 저장된 키값들을 모두 확인 (부하가 심한 명령어라 운영중인 서비스에선 절대 사용하면 안됨)
    • get {key} : 지정한 key 에 해당하는 value 를 가져옴
    • mget {key} [{key} ...] : 여러 개의 key 에 해당하는 value 를 한번에 가져옴
    • ttl {key} : key 의 만료 시간을 초 단위로 보여줌 (-1 은 만료시간 없음, -2 는 데이터 없음)
    • pttl {key} : key 의 만료 시간을 밀리초 단위로 보여줌
    • type {key} : 해당 key 의 value 타입 확인
  • 삭제
    • del {key} [{key} ...] : 해당 key 들을 삭제
  • 수정
    • rename {key} {newKey} : key 이름 변경
    • expire {key} {seconds} : 해당 키 값의 만료 시간 설정
  • 기타
    • randomkey : 랜덤한 key 반환
    • ping : 연결 여부 확인 ("ping" 만 입력하면 "PONG" 이라는 응답이 옴)
    • dbsize : 현재 사용중인 DB 의 key 의 갯수 리턴
    • flushall : 레디스 서버의 모든 데이터 삭제
    • flushdb : 현재 사용중인 DB 의 모든 데이터 삭제

 

2) Set

Redis 에서는 Set 에 포함된 값들을 멤버라고 표현한다. 여러 멤버가 모여 집합 (Set) 을 구성한다.

  • sadd {key} {member} [{member} ...]
    • key 에 새로운 멤버들을 추가. key 가 없으면 새로 만듬
  • smembers {key}
    • key 에 설정된 모든 멤버 반환
  • srem {key} {member [{member} ...]}
    • key 에 포함된 멤버들 삭제. 없는 멤버 입력하면 무시됨
  • scard {key}
    • key 에 저장된 멤버 수를 반환
  • sismember {key} {member}
    • member 가 해당 key 에 포함되는지 검사

 

3) Hash

Hash 자체를 나타내는 key 와 해당 key 에 포함된 field 까지 사용해서 값을 조회/저장할 수 있습니다.

  • hset {key} {field} {value} [{field} {value} ...]
    • key 를 이름으로 한 Hash 자료 구조에 field 와 value 값을 저장
  • hget {key} {field}
    • key Hash 값에 포함된 field 의 value 를 가져옴
  • hdel {key} {field} [{field} ...]
    • field 값으로 데이터 삭제
  • hlen {key}
    • Hash 가 갖고 있는 field 갯수 반환
  • hkeys {key}
    • Hash 가 갖고 있는 모든 field 출력
  • hvals {key}
    • Hash 가 갖고 있는 모든 value 출력
  • hgetall {key}
    • Hash 가 갖고 있는 모든 field 와 value 출력

 

7. Redis 사용시 발생한 문제, 해결 하지 못함

 

 

위와 같이 hgetall {Key} 로 조회했을 때, 내용이 깨져서 나오는 문제가 있다. 일단 한글은 redis-cli --raw 명령어를 사용해서 깨지지 않고 잘 나오고 있는데, 다른 부분들이 다 저렇게 알 수 없는 문자들로 나온다 ... 구글링을 반나절 한 결과 Medis 라는 Redis GUI 프로그램을 찾긴 했지만 $5 구독비가 있기 때문에 일단 사용을 보류하기로 했다. 해결 방법 아시는분 제발 댓글 좀 부탁드립니다.

아래는 Redis GUI 참고 블로그 

 

Mac M1에서 가능한 Redis GUI Client 프로그램 : Medis2

이제 좀 redis를 알아보려고 하는데, 계속 터미널에서 확인해야하니 명령어를 계속 찾아봐야해서 너무 번거로웠다.. 그래서 뭔가 gui툴이라던지 다른 프로그램이 없을까봐 찾아봤다. 그중에서 내

pearlluck.tistory.com


[ 참고자료 ]

 

Redis 설치 및 명령어

1. Redis 란? Redis 는 Key-Value 형태로 데이터를 관리하는 오픈 소스입니다. Redis 는 빠른 속도와 간편한 사용법으로 인해 캐시, 인증 토큰, 세션 관리 등등 여러 용도로 사용됩니다. In-Memory Data Strucutre

bcp0109.tistory.com

 

🎮 프로젝트 소개

프로젝트명: 나만 모른닭!

  • 내 머리에 있는 거 뭐야? 나만 몰라? 😭 ”양세찬 게임” 혹은 “콜마이네임”으로 불리는 키워드 맞추기 게임을 주제로 한 프로젝트

게임룰

  • 참여자 전원 머리 위에 키워드 공지 (본인 키워드는 확인할 수 없음)
  • 참여자 전원 한 명씩 돌아가며 발언권을 부여하고 본인 키워드에 대한 질문 음성으로 한다. (20초)
  • 발언권이 없는 인원들은 음성이 아닌 채팅으로 질문에 답을 해준다
  • 20초가 지난 후 발언권을 부여 받은 사람에게 정답을 입력하게 한다. (모르겠으면 넘어가기 버튼 클릭 유도)
  • 쭉 돌다가 정답자가 나오면 게임 종료

 

와이어 프레임

 

주요 기능

필수 기능 요소 

  • 로그인, 로그아웃 및 회원 가입
  • 카카오 로그인 기능
  • 게임 룰 설명
  • 게임 Room CRUD
  • 음성 채팅 기능
  • 실시간 채팅 기능
  • 20초 타이머 기능

부가 기능 요소 

  • 마이크 음량 조절
  • 게임 방 비밀번호 설정
  • 게임 방 인원수 조절
  • 게임 룰 추가 (3바퀴 이상 돌면 키워드 앞 글자 공개 등)
  • 게임 주제 직접 선택
  • 발언 타이머 시간 조절
  • 게임 페이지 내 방 정보 변경
  • 마이페이지 (닉네임 및 프로필 이미지 변경 / 업적은 있으면 좋고 없으면 말구,,,)
  • 라운드 수 사용자가 정할 수 있도록 혹은 게임 중지 가능 하도록

 

API 명세

https://www.notion.so/9793347b25e64b4e9b29a4ddd49b3606?v=8f9623e9b72d44da84f9253100adc102

 

ERD 

 

기술 스택

Backend

  • Spring boot , Spring Security
  • MySQL
  • JWT
  • WebSokcket ( Sockjs ,STOMP )
  • WEBRTC
  • Redis

Frontend

  • Styled-comonent
  • JavaScript
  • React ( React-cookie, React Router, ReactDOM )
  • Redux ( Redux toolkit )
  • Axios
  • Web Socket ( WebRtc, Stomp, SockJs )

Infrastructure

  • Amazon RDS
  • Amazon EC2
  • Amazon S3

Dev tools

  • Git, Github ( git flow 전략 사용 예정 )
  • Swagger

+ 추가 

 

실전때 사용하면 좋은 기능 : bucket4j, 웹소켓, Redis, 동시성제어, 무중단베포, builer패턴, CI/CD, Objectmetadata, quesryDSL, Docker

  • 챌린지 - 조회 성능 기능 (서비스팀도 가능하긴함 ) , 캐싱, 인메모리 db, 레디스 
  • 미니 프로젝트 - 추가기능 해보기 (대댓글 , 소셜 로그인, 페이징

1. 프로젝트 소개

 

 

 

2. 주요 기능

  • Spring Security, JWT를 이용한 회원가입/로그인
  • 이메일 인증을 통한 로그인 구현
  • AWS S3를 이용한 다중 이미지 업로드
  • JPA Pageable을 이용한 페이지 무한 스크롤
  • 타입별, 가격별 필터링 기능
  • 키워드 검색 기능
  • 숙소 좋아요 기능
  • swagger 적용
  • 다중 이미지 업로드 CRUD(조회 시 이미지 preview)

 

3. 서비스 아키텍쳐

 

4. 기술 스택

🎨 Front-end Stack

  • React , javascript
  • Redux
  • Redux Toolkit
  • mui , styled-components
  • axios

🛠 Back-end Stack

  • Spring boot
  • Spring Security, JWT
  • AWS S3, RDS(MySQL)
  • OAuth 2.0

🌐 Infrastructure

  • AWS EC2
  • AWS S3

🗂 Dev tools

  • Swagger
  • Git, Github

 

5. 트러블 슈팅

 

6. 팀 노션

7. 깃허브

8. 다음에 사용해보고싶은 기술

  • S3를 이용한 다중 이미지 업로드의 개별 이미지 수정
  • refresh 토큰
  • https 적용
  • logging
  • 소셜 로그인 ( google, naver )
  • bucket4j
  • 지도 api
  • 주소 찾기 api 기능
  • grid속성을 조절해 카드 컴포넌트 크기 키우기
  • 웹소캣 채팅 기능

@Enablejpaauditing

문제

  • 게시글을 수정할 때, CreatedAt/ModifiedAt 값이 null로 반환되는 문제

해결

  • @Enablejpaauditing 어노테이션 추가
@EnableJpaAuditing
@SpringBootApplication
public class HanghaebnbApplication {

    public static void main(String[] args) {
        SpringApplication.run(HanghaebnbApplication.class, args);
    }

}

+추가 자료)

  • Jpa Auditing 를 활성화시키는 annotation 입니다.
  • Spring Audit 기능 : Spring Data JPA 에서 시간에 대해서 자동으로 값을 넣어주는 기능입니다. 도메인을 영속성 컨텍스트에 저장하거나 조회를 수행한 후에 update를 하는 경우 매번 시간 데이터를 입력하여 주어야 하는데, audit을 이용하면 자동으로 시간을 매핑하여 데이터베이스의 테이블에 넣어주게 됩니다.

 

createdAt, modifiedAt 값이 String 타입이 아니라 배열로 반환됨

문제

  • createdAt, modifiedAt 값이 String 타입이 아니라 배열로 반환됨

해결

  • ResponseDto에 @JsonFormat(shape = JsonFormat.Shape.*STRING*, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") 추가

기술 매니저님 피드백)

경우에 따라 배열로 반환해 주는게 프론트엔드에서 값을 처리하기 편할 수도 있다. 따라서 이 부분은 상황에 맞게 반환타입을 주는게 좋다.

 

필터링 + 페이징 (with Spring Data JPA)

문제

  • 페이징과 카테고리별/가격별 조회를 한 번에 하려다보니 쿼리가 복잡해져서 Spring Data JPA의 Query Method만으로는 조회가 어려운 상황이었다.
  • @Query 어노테이션과 native query를 이용하여 해결하려 하였으나 native query와 페이징을 함께 사용하기가 까다로웠다.

해결

  • countQuery를 이용하여 query문을 작성하고, @Param 어노테이션을 함께 사용하여 메서드를 생성하여 해결하였다.
@Query(countQuery = "select count(*) from room r where (r.price between :minPrice and :maxPrice) and r.type = :type", nativeQuery = true)
Page<Room> findByPriceBetweenAndType(@Param("minPrice") int minPrice,
                                     @Param("maxPrice") int maxPrice,
                                     @Param("type") String type,
                                     Pageable pageable);

 

데이터 전달 시 타입 지정

문제

  • String type으로 매개변수를 받아올 때 공백문자가 섞이는 에러발생
  • 포스트맨에서 body - text로 놓고 {}없이 그냥 String 썼어야 함. 여태까지는 왜 이런 에러가 발생 안 했는지 생각해보니 여태까지는 dto로 받았었음.

해결

  • 이 문제를 해결하기 위해 제네릭스를 사용해서 해결했다가, 코드의 통일성위해 dto로 responsebody로 json형식으로 받아오는 방식으로 바꿈.

 

if-else와 try-catch

문제

  • if-else문 내부 throw → 특정 조건에서만 던져지는 exception

해결

  • try-catch문으로 변경.
  • catch시 try코드에서 어떤 exception이 터질지 알고있으니 그게 맞게 작성해주면 된다.
  • ? → 지금은 IOException이나 직접만든 CustomException 두개로 catch를 하고있지만 에러가 더욱 많아지면 계속 해서 catch문을 추가해서 해당하는 에러를 잡아야하나?
  • ! → 자바가 기본 제공하는 Exception중 해당 exception이 상속받는 상위 상위 상위 exception이 존재한다 초기에는 적절한 exception을 catch문으로 사용하여 잡으면 넓은 범위의 catch로 핸들링 할 수 있으며 이 범위는 최적화 과정에서 줄여나가면 된다.

CustomException

package com.cloneweek.hanghaebnb.util.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class CustomException extends RuntimeException{
    private final StatusMsgCode statusMsgCode;
}

 

StatusMsgCode

package com.cloneweek.hanghaebnb.util.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum StatusMsgCode {

    /* 400 BAD_REQUEST : 잘못된 요청 */
    USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "회원을 찾을 수 없습니다."),
    INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다."),
    BAD_ID_PASSWORD(HttpStatus.BAD_REQUEST, "아이디나 비밀번호 패턴이 맞지 않습니다."),
    ROOM_NOT_FOUND(HttpStatus.BAD_REQUEST, "숙소를 찾을 수 없습니다."),
    COMMENT_NOT_FOUND(HttpStatus.BAD_REQUEST, "댓글을 찾을 수 없습니다."),
    INVALID_USER(HttpStatus.BAD_REQUEST, "작성자만 삭제/수정할 수 있습니다."),
    FILE_UPLOAD_FAILED(HttpStatus.BAD_REQUEST, "파일 업로드 실패"),
    FILE_DELETE_FAILED(HttpStatus.BAD_REQUEST, "파일 삭제 실패"),


    /* 409 CONFLICT : Resource의 현재 상태와 충돌, 보통 중복된 데이터 존재 */
    DUPLICATE_RESOURCE(HttpStatus.CONFLICT, "데이터가 이미 존재합니다."),
    ALREADY_CLICKED_LIKE(HttpStatus.CONFLICT, "이미 좋아요를 눌렀습니다"),
    ALREADY_CANCEL_LIKE(HttpStatus.CONFLICT, "이미 좋아요 취소를 눌렀습니다"),
    EXIST_USER(HttpStatus.CONFLICT, "중복된 이메일입니다."),
    EXIST_NICK(HttpStatus.CONFLICT, "중복된 닉네임입니다."),
    UNMATCH_CODE(HttpStatus.BAD_REQUEST, "코드가 일치하지 않습니다."),


    /* 200 SUCCESS */
    SIGN_UP(HttpStatus.OK, "회원가입에 성공했습니다."),
    LOG_IN(HttpStatus.OK, "로그인에 성공했습니다"),
    LIKE(HttpStatus.OK, "좋아요 성공"),
    CANCEL_LIKE(HttpStatus.OK, "좋아요 취소"),
    DELETE_POST(HttpStatus.OK, "숙소를 삭제하였습니다"),
    DONE_POST(HttpStatus.OK, "숙소 등록 완료"),
    DELETE_COMMENT(HttpStatus.OK, "댓글을 삭제하였습니다"),
    NICKNAME(HttpStatus.OK, "사용 가능한 닉네임입니다."),
    EMAIL(HttpStatus.OK, "사용 가능한 이메일입니다."),
    EMAIL_CONFIRM(HttpStatus.OK, "해당 이메일로 회원가입 가능합니다."),
    UPDATE(HttpStatus.OK, "게시글 수정 완료"),
    MATCH_CODE(HttpStatus.OK, "코드가 일치합니다.");


    /* 401 UNAUTHORIZED : 인증되지 않은 사용자 */
//    INVALID_AUTH_TOKEN(HttpStatus.BAD_REQUEST, "토큰이 유효하지 않습니다."),
//    INVALID_AUTH_TOKEN(HttpStatus.UNAUTHORIZED, "권한 정보가 없는 토큰입니다."),
//    UNAUTHORIZED_USER(HttpStatus.UNAUTHORIZED, "존재하지 않는 유저입니다."),

    /* 404 NOT_FOUND : Resource를 찾을 수 없음 */
//    USER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저 정보를 찾을 수 없습니다."),
//    REFRESH_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "로그아웃 한 유저입니다."),
//    NOT_FOLLOW(HttpStatus.NOT_FOUND, "팔로우 중이지 않습니다"),

    /* 500 INTERNAL_SERVER_ERROR */
//    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버가 없습니다.");


    private final HttpStatus httpStatus;
    private final String detail;
}

 

GlobalExceptionHandler

package com.cloneweek.hanghaebnb.util.exception;

import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import static com.cloneweek.hanghaebnb.util.exception.StatusMsgCode.BAD_ID_PASSWORD;
import static com.cloneweek.hanghaebnb.util.exception.StatusMsgCode.DUPLICATE_RESOURCE;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = { ConstraintViolationException.class, DataIntegrityViolationException.class})
    protected ResponseEntity<ErrorResponse> handleDataException() {
        log.error("handleDataException throw Exception : {}", DUPLICATE_RESOURCE);
        return ErrorResponse.toResponseEntity(DUPLICATE_RESOURCE);
    }

    @ExceptionHandler(value = { CustomException.class })
    protected ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
        log.error("handleCustomException throw CustomException : {}", e.getStatusMsgCode());
        return ErrorResponse.toResponseEntity(e.getStatusMsgCode());
    }

    @ExceptionHandler(value = {MethodArgumentNotValidException.class})
    protected ResponseEntity<ErrorResponse> processValidationException(MethodArgumentNotValidException e) {
        log.error("handleCustomException throw CustomException : {}", e.getMessage());
        return ErrorResponse.toResponseEntity(BAD_ID_PASSWORD);
    }

}

 

ErrorResponse

package com.cloneweek.hanghaebnb.util.exception;

import lombok.Builder;
import lombok.Getter;
import org.springframework.http.ResponseEntity;

@Getter
@Builder
public class
ErrorResponse {

    private final int statusCode;
    private final String message;

    public static ResponseEntity<ErrorResponse> toResponseEntity(StatusMsgCode statusMsgCode) {
        return ResponseEntity
                .status(statusMsgCode.getHttpStatus())
                .body(ErrorResponse.builder()
                        .statusCode(statusMsgCode.getHttpStatus().value())
                        .message(statusMsgCode.getDetail())
                        .build());
    }
}

 

+ Recent posts