Spring Boot - 동시 접속 유저수 제한(feat.Redis, 세션 ) (2)

Yunny.Log ·2022년 8월 22일
1

Spring Boot

목록 보기
77/80
post-thumbnail
  • 세션을 저장할 레디스를 설치하고 연동해야 합니다.
  • 레디스 설치 글을 참고해 연동 작업을 진행하면 됩니다.

1.스프링-Redis 연동

1) 의존성 추가

build.gradle


// spring에서 redis에 대한 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// spring에서 redis를 session storage로 사용하기 위한 의존성
implementation 'org.springframework.session:spring-session-data-redis'

1) spring-boot-starter-data-redis는 spring

  • redis를 사용하기 위한 의존성을 추가합니다.

2) spring-session-data-redis

  • spring의 session storage로써 기존의 메모리가 아닌 redis를 이용해 세션 값들을 저장 하기 위한 의존성입니다.

2) 설정 파일

application.yml

spring :
	session :
    	storage-type : redis
	redis :
    	host : localhost
        password: password
        port : 6379
  • spring.session의 하위 항목은 spring의 session storage 타입을 redis로 바꾸어 준 것입니다.
  • spring.redis의 하위 항목은 어딘가에서 실행되고 있는 redis와 연결하기 위한 설정입니다.

3) @EnableRedisHttpSession

  • main-Application@EnableRedisHttpSession 설정을 추가합니다.

4) redis config 파일


@Configuration
    public class RedisConfig {
        @Value("${spring.redis.host}")
        public String host;

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

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


        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
            RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
            redisTemplate.setConnectionFactory(connectionFactory);
            return redisTemplate;
        }

        @Bean
        public RedisConnectionFactory redisConnectionFactory() {
            RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
            configuration.setHostName(host);
            configuration.setPassword(password);
            // password 설정을 추가합니다. 
            configuration.setPort(port);
            return new LettuceConnectionFactory(configuration);
        }
    }

(+) 발생한 에러

method failed; nested exception is org.springframework.data.redis.redisconnectionfailureexception: unable to connect to redis; nested exception is io.lettuce.core.redisconnectionexception: unable to connect to localhost:6379

  • 해결책 : password 설정을 추가하면 해결됩니다.
configuration.setPassword(password);

2. 기능 구현

  • 사용자가 로그인 시 생성되는 세션을 redis 에 등록시켜주고 없애주어야 합니다.
  • 저의 설계는 아래와 같습니다.

2-3-RedisConfig

  • redis 연동을 위한 config 파일을 작성합니다.

@Configuration
public class RedisConfig {
    @Value("${spring.redis.host}")
    public String host;

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

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

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

    // stringRedisTemplate bean은 일반적인 String 값을 key, value로 사용하는 경우 사용합니다.
    @Bean
    public StringRedisTemplate stringRedisTemplate() {
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        stringRedisTemplate.setKeySerializer(new StringRedisSerializer());
        stringRedisTemplate.setValueSerializer(new StringRedisSerializer());
        stringRedisTemplate.setConnectionFactory(redisConnectionFactory());
        return stringRedisTemplate;
    }

    // Redis 서버와의 통신을 위한 low-level 추상화를 제공합니다. 
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
        configuration.setHostName(host);
        configuration.setPassword(password);
        configuration.setPort(port);
        return new LettuceConnectionFactory(configuration);
    }
}

2-4 RedisService (Refactoring 전)


@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class RedisService {

    private final StringRedisTemplate stringRedisTemplate;

    /**
     * id 포함 여부를 판단합니다. 
     * @param chkId : 포함 여부를 판단할 id 
     * @return : 포함될 시 true , 아니라면 false 
     */
    public boolean ifIdInLoginList(Long chkId) {
        boolean if_in_login_list = false;
        Set<byte[]> keys = stringRedisTemplate.getConnectionFactory().getConnection().keys("*".getBytes());
        Iterator<byte[]> it = keys.iterator();
        while (it.hasNext()) {
            byte[] data = it.next();
            if (chkId.toString().equals(new String(data, 0, data.length))) {
                if_in_login_list = true;
                break;
            }
        }

        return if_in_login_list;
    }


    /**
     * 현재 유저 수 (key 갯수) 를 count 합니다.
     * @return 현재 유저 수 (key 갯수)
     */
    public int getAllRedisStringValue() {

        // key의 Set을 가져옵니다.
        Set<byte[]> keys = stringRedisTemplate.getConnectionFactory().getConnection().keys("*".getBytes());

        // 현재 유저 수 (key 갯수) 를 count 합니다.
        Iterator<byte[]> it = keys.iterator();
        int simultaneous_user_count = 0;
        while (it.hasNext()) {
            simultaneous_user_count += 1;
        }

        return simultaneous_user_count;
    }

    public boolean isUserMoreThan50() {
        return getAllRedisStringValue() > 49;
    }


    /**
     * key = member id
     * 세션 id를 생성하고, 값을 세션에 저장합니다.
     * @param key = member id
     */
    public void setRedisStringValue(String key) {
        // 세션 id를 생성하고, 값에 세션을 저장한 후 Redis의 key-value 형태로 저장합니다.
        String sessionId = UUID.randomUUID().toString();
        // opsForValue() = String 을 쉽게 Serialize / Deserialize 해주는 Interface
        ValueOperations<String, String> stringValueOperations = stringRedisTemplate.opsForValue();
        // key 로 사용자 id, 값으로 session id 를 set 합니다.
        stringValueOperations.set(key, sessionId);
    }

    /**
     * key = member id to be deleted
     * 로그아웃 시 멤버의 id key값을 제거합니다.
     * @param key = member id
     */
    public void removeKey(String key) {
        stringRedisTemplate.delete(key);
    }

}

2-5 접속자조회 & 레디스 get

  • 모든 키 값을 데려오는 메소드를 사용합니다.

  Set<byte[]> keys = RedisTemplate.getConnectionFactory().getConnection().keys("*".getBytes());
  

  • 제가 작성한 키 값의 갯수 = 접속해있는 유저의 수 입니다. 따라서 키의 갯수를 count 해주면 됩니다.

    /**
     * 현재 유저 수 (key 갯수) 를 count 합니다.
     * @return 현재 유저 수 (key 갯수)
     */
    public int getAllRedisStringValue() {

        // key의 Set을 가져옵니다.
        Set<byte[]> keys = stringRedisTemplate.getConnectionFactory().getConnection().keys("*".getBytes());

        // 현재 유저 수 (key 갯수) 를 count 합니다.
        Iterator<byte[]> it = keys.iterator();
        int simultaneous_user_count = 0;
        while (it.hasNext()) {
            simultaneous_user_count += 1;
        }

        return simultaneous_user_count;
    }

    public boolean isUserMoreThan50() {
        return getAllRedisStringValue() > 49;
    }
  • 만약 키 값이 지정된 사용자 수인 50명을 넘는지 판단 후, 넘는다면 true / 넘지 않는다면 false를 return 해주는 메소드를 추가해줍니다.

    // 키 값이 50명 이상이면 false 반환
    public boolean isUserMoreThan50(){
        if(getAllRedisStringValue()>50){
            return true;
        }else{
            return false;
        }
    }

2-6 isUserMoreThan50() 메소드

  • 로그인 시 해당 유저 아디 키값 : 세션아이디 를 레디스에 set 해줍니다.
  • 그러나 로그인 허용 경우는 위에서 지정된 사용자 수 (여기선 50명)가 넘는지 여부를 검사해서 false가 반환될 때만 허용해줍니다.
  • 따라서 sign in 시 에 isUserMoreThan50 문으로 검사 진행해주고,
    • 지정된 사용자 수 (여기선 50명) 넘으면 에러를 던져줍니다.
    • 아니라면 setRedisStringValue 메소드를 통해 레디스에 유저 id를 set 해줍니다.

   @Transactional(readOnly = true)
        public SignInResponse signIn(SignInRequest req) {

        if (redisService.isUserMoreThan50()) {
		// 50명 넘으면 에러 던지며 로그인을 허용하지 않습니다. 
            throw new MemberSoManyException();
        }
    /**
     * key = member id
     * 세션 id를 생성하고, 값을 세션에 저장합니다.
     * @param key = member id
     */
    public void setRedisStringValue(String key) {
        // 세션 id를 생성하고, 값에 세션을 저장한 후 Redis의 key-value 형태로 저장합니다.
        String sessionId = UUID.randomUUID().toString();
        // opsForValue() = String 을 쉽게 Serialize / Deserialize 해주는 Interface
        ValueOperations<String, String> stringValueOperations = stringRedisTemplate.opsForValue();
        // key 로 사용자 id, 값으로 session id 를 set 합니다.
        stringValueOperations.set(key, sessionId);
    }
}

2-7 로그아웃 시 처리 (레디스 delete)

  • 로그아웃 api가 동작할 시에 레디스에서 key를 삭제해줘야 합니다.
  • 따라서 api 가 작동할 때 RedisServiceremoveKey(로그아웃할 id)를 추가함으로써 redis에서 유저 id로 된 key값을 제거합니다.

    @CrossOrigin(origins = "https://localhost:3000")
    @PostMapping("/logout/{id}")
    @ResponseBody
    public Response signOut(@PathVariable Long id) {
        redisService.removeKey(id.toString());
        return Response.success(id);
    }
  • 서버와 연결된 Redis 의 key를 제거하는 removeKey() 메소드를 추가해줍니다.

    /**
     * key = member id to be deleted
     * 로그아웃 시 멤버의 id key값을 제거합니다.
     * @param key = member id
     */
    public void removeKey(String key) {
        stringRedisTemplate.delete(key);
    }

출처

레디스 설치 과정

스프링-Redis 연동

redis 에 데이터 set, get 하는 방법

REDIS-Key-Evict

0개의 댓글