728x90

Spring Boot에서 Redis를 이용한 Refresh Token 처리 방법은 다음과 같습니다.

  1. 의존성 추가
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. Redis 설정
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
spring.redis.database=0
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=-1
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=0
  1. Refresh Token 생성 후 Redis에 저장
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.UUID;

@Component
public class RefreshTokenService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private final String PREFIX_REFRESH_TOKEN = "refresh_token:";

    // Refresh Token 만료시간 (1주일)
    private final Duration REFRESH_TOKEN_EXPIRATION = Duration.ofDays(7);

    public String createRefreshToken() {
        String refreshToken = PREFIX_REFRESH_TOKEN + UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(refreshToken, "", REFRESH_TOKEN_EXPIRATION);
        return refreshToken;
    }
}
  1. 1. Access Token 만료 시, Refresh Token을 이용하여 새로운 Access Token을 생성.
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.Date;

@Component
public class AccessTokenService {

    @Value("${jwt.secret}")
    private String jwtSecret;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private final String PREFIX_REFRESH_TOKEN = "refresh_token:";

    // Access Token 만료시간 (1시간)
    private final Duration ACCESS_TOKEN_EXPIRATION = Duration.ofHours(1);

    public String createAccessToken(String refreshToken) throws Exception {
        if (!redisTemplate.hasKey(refreshToken)) {
            throw new Exception("Invalid Refresh Token");
        }

        String subject = ""; // Access Token의 subject
        Date now = new Date();
        Date expiredAt = new Date(now.getTime() + ACCESS_TOKEN_EXPIRATION.toMillis());

        redisTemplate.expire(refreshToken, Duration.ZERO);

        String accessToken = Jwts.builder()
                .setSubject(subject)
                .setIssuedAt(now)
                .setExpiration(expiredAt)
                .signWith(SignatureAlgorithm.HS256, jwtSecret)
                .compact();

        return accessToken;
    }
}

예시 코드는 Redis에 Refresh Token만 저장하고, Access Token은 JWT로 생성합니다.

이외에도 Redis를 이용하여 Access Token까지 저장할 수 있습니다.

  1. Redis에 Access Token과 Refresh Token을 저장합니다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.UUID;

@Component
public class TokenService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private final String PREFIX_ACCESS_TOKEN = "access_token:";
    private final String PREFIX_REFRESH_TOKEN = "refresh_token:";

    // Access Token 만료시간 (1시간)
    private final Duration ACCESS_TOKEN_EXPIRATION = Duration.ofHours(1);

    // Refresh Token 만료시간 (1주일)
    private final Duration REFRESH_TOKEN_EXPIRATION = Duration.ofDays(7);

    public String createAccessToken() {
        String accessToken = PREFIX_ACCESS_TOKEN + UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(accessToken, "", ACCESS_TOKEN_EXPIRATION);
        return accessToken;
    }

    public String createRefreshToken() {
        String refreshToken = PREFIX_REFRESH_TOKEN + UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(refreshToken, "", REFRESH_TOKEN_EXPIRATION);
        return refreshToken;
    }

    public void deleteToken(String accessToken) {
        String refreshToken = accessTokenToRefreshToken(accessToken);
        redisTemplate.delete(accessToken, refreshToken);
    }

    public String accessTokenToRefreshToken(String accessToken) {
        return accessToken.replace(PREFIX_ACCESS_TOKEN, PREFIX_REFRESH_TOKEN);
    }

    public boolean isValidRefreshToken(String refreshToken) {
        return redisTemplate.hasKey(refreshToken);
    }

    public boolean isValidAccessToken(String accessToken) {
        return redisTemplate.hasKey(accessToken);
    }
}
  1. Access Token의 만료시간을 Redis에서 확인합니다.
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;

@Component
public class JwtService {

    @Value("${jwt.secret}")
    private String jwtSecret;

    @Autowired
    private TokenService tokenService;

    // Access Token 만료시간 (1시간)
    private final long ACCESS_TOKEN_EXPIRATION_TIME = 3600000L;

    public String createAccessToken() {
        String accessToken = tokenService.createAccessToken();

        String subject = ""; // Access Token의 subject
        Date now = new Date();
        Date expiredAt = new Date(now.getTime() + ACCESS_TOKEN_EXPIRATION_TIME);

        String refreshToken = tokenService.createRefreshToken();

        // Redis에 Access Token과 Refresh Token 저장
        RedisTemplate<String, String> redisTemplate = tokenService.getRedisTemplate();
        redisTemplate.opsForValue().set(accessToken, refreshToken);
        redisTemplate.expire(accessToken, ACCESS_TOKEN_EXPIRATION_TIME);
        redisTemplate.opsForValue().set(refreshToken, accessToken);
        redisTemplate.expire(refreshToken, tokenService.getREFRESH_TOKEN_EXPIRATION_TIME());

        String jwt = Jwts.builder()
                .setSubject(subject)
                .setIssuedAt(now)
                .setExpiration(expiredAt)
                .signWith(SignatureAlgorithm.HS256, jwtSecret)
                .compact();

        return jwt;
    }

    public String refreshToken(String refreshToken) throws Exception {
        if (!tokenService.isValidRefreshToken(refreshToken)) {
            throw new Exception("Invalid Refresh Token");
        }

        String accessToken = tokenService.accessTokenToRefreshToken(refreshToken);
        if (!tokenService.isValidAccessToken(accessToken)) {
            throw new Exception("Invalid Access Token");
        }

        // Access Token의 만료시간을 Redis에서 확인
        RedisTemplate<String, String> redisTemplate = tokenService.getRedisTemplate();
        long expiration = redisTemplate.getExpire(accessToken);

        if (expiration < 0) {
            throw new Exception("Expired Access Token");
        }

        String newAccessToken = createAccessToken();

        // 기존 Access Token을 삭제하고 새로운 Access Token과 연결된 Refresh Token 저장
        redisTemplate.delete(accessToken);
        redisTemplate.opsForValue().set(newAccessToken, refreshToken);
        redisTemplate.expire(newAccessToken, ACCESS_TOKEN_EXPIRATION_TIME);
        redisTemplate.opsForValue().set(refreshToken, newAccessToken);
        redisTemplate.expire(refreshToken, tokenService.getREFRESH_TOKEN_EXPIRATION_TIME());

        return newAccessToken;
    }

}

위의 코드에서는 Redis에 저장된 Access Token의 만료시간을 확인하여 만료된 Access Token인 경우 새로운 Access Token을 생성합니다. 그리고 기존 Access Token을 삭제하고 새로운 Access Token과 연결된 Refresh Token을 Redis에 저장합니다.

위 코드를 참고하여 Redis를 이용하여 Access Token과 Refresh Token을 저장하고, Access Token의 만료시간을 확인하는 코드를 작성해보세요.


내저장소 바로가기 luxury515

728x90
  1. Redis를 Spring Framework 적용.

Spring Data Redis를 사용하여 Redis를 Spring Framework에 통합할 수 있습니다. 이를 위해 먼저 pom.xml 또는 build.gradle 파일에 필요한 의존성을 추가하고, Redis 구성을 설정해야 합니다.

pom.xml 파일에 아래와 같이 의존성을 추가할 수 있습니다.

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>2.6.0</version>
</dependency>

Redis 구성은 RedisConnectionFactory, RedisTemplate 및 RedisCacheManager를 포함하는 구성 클래스를 작성하여 구현할 수 있습니다. 이 구성 클래스는 Redis 호스트 및 포트와 같은 Redis 구성을 정의하고 Spring Framework에서 Redis를 사용할 수 있도록 합니다.

  1. 중복 요청을 방지할 Redis 키 생성

Redis를 사용하여 중복 요청을 방지하려면 각 요청에 대한 고유한 식별자를 생성해야 합니다. 이 식별자를 Redis 키로 사용하여 중복 요청을 방지합니다.

Spring Framework에서는 요청을 처리하는 데 사용되는 컨트롤러 메서드에 @RequestMapping 애너테이션을 추가할 수 있습니다. 이 애너테이션에는 요청 경로와 HTTP 메서드가 정의됩니다. 컨트롤러 메서드에서는 이 정보를 사용하여 고유한 요청 식별자를 생성할 수 있습니다.

다음과 같은 요청 식별자를 생성할 수 있습니다.

String requestId = request.getMethod() + ":" + request.getRequestURI();
  1. Redis에 중복 요청 키 저장

Redis에 중복 요청을 방지하기 위한 키를 저장합니다. 이를 위해 RedisTemplate을 사용하여 Redis에 키-값 쌍을 저장합니다.

ValueOperations<String, String> ops = redisTemplate.opsForValue();
Boolean success = ops.setIfAbsent(requestId, "true", Duration.ofSeconds(60));
if (!success) {
    throw new DuplicateRequestException("Duplicate request");
}

위 코드에서는 Redis에 requestId 키가 없으면 "true" 값을 저장하고, 만약 이미 존재한다면 DuplicateRequestException을 발생시킵니다. 이렇게 함으로써, 같은 요청이 중복으로 수신되는 것을 방지할 수 있습니다.

  1. key 만료 시키기.

Redis에 저장된 중복 요청 키를 만료시킴으로써 Redis의 메모리를 최적화할 수 있습니다. 이를 위해 RedisTemplate을 사용하여 키의 만료 시간을 설정합니다.

ops.expire(requestId, Duration.ofSeconds(60));

내저장소 바로가기 luxury515

728x90

들어가면서

Kafka는 대용량의 데이터를 처리하기 위한 분산 메시지 큐입니다. 이 문서에서는 Spring Boot 3.0에서 Kafka를 적용하는 방법과 기본적인 설정 코드를 제공합니다.

Kafka에 대한 기본적인 사항

Kafka는 Pub/Sub 모델을 사용하여 메시지를 처리합니다. 브로커(Broker)라고 불리는 Kafka 서버에서 메시지를 생성하고, 토픽(Topic)이라는 주제에 따라 메시지를 구분합니다. 프로듀서(Producer)는 메시지를 생성하고, 컨슈머(Consumer)는 메시지를 받아서 처리합니다.

Kafka 설정 코드

Kafka를 Spring Boot 3.0에서 적용하려면, spring-kafka 라이브러리를 추가해야 합니다. 예제 코드는 다음과 같습니다.

@Configuration
@EnableKafka
public class KafkaConfiguration {

    @Value("${spring.kafka.bootstrap-servers}")
    private String bootstrapServers;

    @Bean
    public Map<String, Object> consumerConfigs() {
        Map<String, Object> props = new HashMap<>();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        return props;
    }

}

위 코드에서 @Value 어노테이션을 사용하여 bootstrap-servers 값과 같은 Kafka 설정 정보를 application.yml 파일에서 가져올 수 있습니다.

RabbitMQ와의 비교

RabbitMQ는 메시지 큐를 위한 오픈소스 브로커입니다. Kafka와 비교하면 RabbitMQ는 메시지 브로커의 역할만 수행합니다. 반면에 Kafka는 메시지 브로커 역할과 분산 처리 역할을 수행합니다. RabbitMQ는 AMQP(Advanced Message Queuing Protocol) 프로토콜을 사용하며, Kafka는 Apache ZooKeeper를 사용합니다.

다른 메시지 큐와의 비교

다음 표는 Kafka와 유사한 메시지 큐와의 성능, 기능, 사용자 인기 등을 비교한 것입니다.

메시지 큐성능기능어떤 프로젝트에 더 어울리는지사용자 인기
ActiveMQ높음다양함중간높음
Redis높음캐시 용도작은 규모높음
RabbitMQ중간다양함중간높음

MySQL, JPA와 함께 사용하기

Spring Boot 3.0에서 Kafka를 사용하면서 MySQL과 JPA를 함께 사용할 수 있습니다. 아래는 application.yml 파일에서 MySQL과 Kafka 설정 정보를 가져오는 예제 코드입니다.

spring:
  datasource:
    url: {MySQL DB URL}
    username: {MySQL DB username}
    password: {MySQL DB password}
  kafka:
    bootstrap-servers: {Kafka bootstrap servers}

JUnit 6로 테스트하기

Spring Boot 3.0에서 Kafka를 사용하는 애플리케이션을 테스트하려면, spring-kafka-test 라이브러리를 추가해야 합니다. 아래는 예제 코드입니다.

@SpringBootTest
@EmbeddedKafka
class KafkaTest {

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    @Autowired
    private KafkaListenerEndpointRegistry kafkaListenerEndpointRegistry;

    @Test
    void testKafka() throws InterruptedException {
        String topic = "test-topic";
        kafkaTemplate.send(topic, "test-message");
        Thread.sleep(1000);
        assertThat(kafkaListenerEndpointRegistry.getListenerContainer("test-group").getRecordLatches().get(topic).getCount())
                .isEqualTo(1);
    }

}

위 코드에서 @EmbeddedKafka 어노테이션을 사용하여 내장된 Kafka 서버를 사용하여 테스트를 수행할 수 있습니다.

결론

Spring Boot 3.0에서 Kafka를 적용하는 방법과 기본적인 설정, RabbitMQ와의 비교, 다른 메시지 큐와의 비교, MySQL과 JPA와 함께 사용하는 방법, JUnit 6로 테스트하는 방법 등을 살펴보았습니다. 이를 통해 Kafka 사용 설정에 대한 내용을 이해하고, 적용할 수 있도록 도움이 되었기를 바랍니다.


내저장소 바로가기 luxury515

728x90

들어가면서

spring Boot 프로젝트에서 Ehcache 대신 Caffeine을 사용하면 성능을 향상시킬 수 있습니다.

Coffine이란 무엇인가?

Caffeine은 인기있는 오픈 소스 자바 캐시 라이브러리이며, Google이 개발하고 있습니다. Caffeine은 Java 8 이상을 지원하며, 최신 JVM의 기능을 활용하여 캐시 퍼포먼스를 높이고 메모리 사용을 최적화합니다.

Coffine은 Guava 캐시 라이브러리의 대체 제품입니다. Coffine은 Guava 캐시의 기능을 대부분 포함하고 있으며, 더 나은 쓰기 성능과 더 적은 메모리 사용량을 제공합니다.

어떤특징들이 있는가?

Caffeine은 다음과 같은 특징을 가지고 있습니다.

  1. 빠른 접근 및 응답 속도: Caffeine은 인-메모리 캐시를 사용하며, ConcurrentHashMap 및 ConcurrentLinkedHashMap과 같은 최신 자바 컬렉션을 활용하여 데이터에 대한 빠른 액세스 및 응답 속도를 제공합니다.
  1. 최신 JVM 최적화: Caffeine은 최신 JVM의 기능을 활용하여 캐시 퍼포먼스를 최적화합니다.
  1. 캐시 유효 기간 및 최대 크기 제한: Caffeine은 캐시 항목의 유효 기간 및 최대 크기 제한을 설정할 수 있습니다. 이러한 설정을 통해 메모리 사용을 최적화하고 캐시 데이터의 최신 상태를 유지할 수 있습니다.
  1. 스레드 안전성: Caffeine은 스레드 안전성을 보장하기 위해 synchronized 블록을 사용하지 않고 최신 자바 동시성 라이브러리를 활용합니다.

Caffeine 외 기타 library들에 대한 간단한 비교자료

라이브러리기능성능기타
Ehcache분산 캐싱, 대규모 데이터 처리높음Spring Boot에서 기본적으로 제공됨
Caffeine쓰기 성능이 뛰어나며, 메모리 사용량이 적음매우 높음Guava 캐시의 대체제
Redis분산 캐싱, Pub/Sub 메시징, Set 처리높음NoSQL 데이터베이스
Hazelcast분산 캐싱, 대규모 데이터 처리높음장애 복구 및 확장성이 뛰어남
Infinispan분산 캐싱, 대규모 데이터 처리높음JBoss에서 개발됨

Coffine의 장점

  • Guava 캐시의 대체제로서 Guava 캐시의 대부분의 기능을 포함하고 있음
  • 쓰기 성능이 더 뛰어나며, 메모리 사용량이 더 적음

Coffine의 단점

  • Guava 캐시와 달리, 캐시 다시 로드를 지원하지 않음

Coffine을 Spring Boot 3.0에 적용하는 코드 설정

1. Gradle 파일에 의존성 추가

dependencies {
    implementation("com.github.ben-manes.caffeine:caffeine:3.0.0")
    implementation("com.github.ben-manes.caffeine:caffeine-jcache:3.0.0")
}

2. Coffine 캐시 매니저 설정

@Configuration
@EnableCaching
public class CachingConfig extends CachingConfigurerSupport {

    @Bean
    @Override
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(caffeineCacheBuilder());
        return cacheManager;
    }

    Caffeine<Object, Object> caffeineCacheBuilder() {
        return Caffeine.newBuilder()
                .expireAfterWrite(5, TimeUnit.MINUTES)
                .maximumSize(100)
                .weakKeys()
                .recordStats();
    }
}

Coffine을 적용한 CRUD 작성하기

1. application.yml 파일에 MySQL 정보 기재

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/{database_name}?serverTimezone=UTC
    username: {username}
    password: {password}
  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true

2. JPA 적용

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name")
    private String name;

    @Column(name = "email")
    private String email;

    // getters and setters
}

3. Coffine을 적용한 Repository 구현

@Repository
public class UserRepositoryImpl implements UserRepository {

    @Autowired
    private EntityManager entityManager;

    @Cacheable(value = "users")
    public List<User> findAll() {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<User> cq = cb.createQuery(User.class);
        Root<User> root = cq.from(User.class);
        cq.select(root);
        TypedQuery<User> query = entityManager.createQuery(cq);
        return query.getResultList();
    }

    @Cacheable(value = "users", key = "#id")
    public User findById(Long id) {
        return entityManager.find(User.class, id);
    }

    @CachePut(value = "users", key = "#user.id")
    public User save(User user) {
        entityManager.persist(user);
        return user;
    }

    @CacheEvict(value = "users", key = "#id")
    public void deleteById(Long id) {
        User user = entityManager.find(User.class, id);
        entityManager.remove(user);
    }
}

JUnit6를 사용하여 테스트 코드 작성

@SpringBootTest
public class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    public void testFindAll() {
        List<User> userList1 = userRepository.findAll();
        List<User> userList2 = userRepository.findAll();
        assertEquals(userList1, userList2);
    }

    @Test
    public void testFindById() {
        User user1 = userRepository.save(new User("John", "john@example.com"));
        User user2 = userRepository.findById(user1.getId());
        assertEquals(user1.getName(), user2.getName());
        assertEquals(user1.getEmail(), user2.getEmail());
    }

    @Test
    public void testSave() {
        User user1 = new User("John", "john@example.com");
        userRepository.save(user1);
        User user2 = userRepository.findById(user1.getId());
        assertEquals(user1.getName(), user2.getName());
        assertEquals(user1.getEmail(), user2.getEmail());
    }

    @Test
    public void testDeleteById() {
        User user1 = userRepository.save(new User("John", "john@example.com"));
        userRepository.deleteById(user1.getId());
        assertNull(userRepository.findById(user1.getId()));
    }
}


내저장소 바로가기 luxury515

+ Recent posts