환경

java 17
Spring-Jpa
Redis
MySQL

해당 환경에서 아주 간단한 캐시 공부 토이프로젝트를 생성할 것이다.
Redis 캐시에 대한 글이기 때문에 Spring이나 JPA에 대한 설명은 생략한다.

핵심 부분은 서비스이다.
해당 글에서는 서비스 부분의 코드만 중점적으로 다루고,
전체 코드는 참고 부분에 깃허브 레포지토리에 있다.

요구사항

학생은 학번, 이름, 나이 정보를 가지고 있다.
해당 프로젝트의 요구 기능은 다음과 같다.

  1. 학생 저장 기능
  2. 학생 조회 기능
  3. 특정 나이의 학생들 조회 기능
  4. 특정 나이와 특정 이름의 학생들 조회 기능

사용될 Entity 및 DTO

@Entity
@Data
@NoArgsConstructor
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String studentNumber;
    private String name;
    private int age;

    public Student(String studentNumber, String name, int age) {
        this.studentNumber = studentNumber;
        this.name = name;
        this.age = age;
    }
}
// Request
// 학생 정보를 저장하기 위한 Request이다.
@Data
@AllArgsConstructor
public class StudentRequest {
    private String studentNumber;
    private String name;
    private int age;
}

// 특정 나이와 이름을 가진 학생을 조회하기 위한 Request이다.
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AgeAndNameRequest {
    private int age;
    private String name;
}


// Response
// 조회한 학생 정보를 반환할 DTO이다.
@Data
@AllArgsConstructor
public class StudentResponse {
    @Id
    private String studentNumber;
    private String name;
    private int age;
}

// 학생 정보 리스트를 반환할 DTO이다.
@Data
@AllArgsConstructor
public class StudentListResponse {
    List<StudentResponse> studentList;
}

캐시 없는 서비스

아주 단순하게 조회하고, DB에 저장하는 기능이다.

@Service
@RequiredArgsConstructor
@Transactional
public class TestService {

    private final StudentRepository studentRepository;

	// 1. 학생 저장 기능
    public Long save(StudentRequest request) {
        System.out.println("===> SAVE 로직 실행");
        Student student = new Student(request.getStudentNumber(), request.getName(), request.getAge());
        studentRepository.save(student);
        return student.getId();
    }

	// 2. 학생 조회 기능
    public StudentResponse find(Long id) {
        System.out.println("===> FIND 로직 실행");
        Student student = studentRepository.findById(id).orElseThrow(IllegalArgumentException::new);
        return new StudentResponse(student.getStudentNumber(), student.getName(), student.getAge());
    }

	// 3. 특정 나이의 학생들 조회 기능
    public StudentListResponse findAge(int age) {
        System.out.println("===> FIND-AGE 로직 실행");
        List<Student> list = studentRepository.findByAge(age);

        List<StudentResponse> reponseList = list.stream().map(entity -> {
            return new StudentResponse(entity.getStudentNumber(), entity.getName(), entity.getAge());
        }).collect(Collectors.toList());
        return new StudentListResponse(reponseList);
    }
    
    // 4. 특정 나이와 특정 이름의 학생들 조회 기능
    public StudentListResponse findNameAndAge(AgeAndNameRequest request) {
        System.out.println("===> FIND NAME AND AGE 로직 실행");
        List<Student> list = studentRepository.findNameAndAge(request.getAge(), request.getName());

        List<StudentResponse> reponseList = list.stream().map(entity -> {
            return new StudentResponse(entity.getStudentNumber(), entity.getName(), entity.getAge());
        }).collect(Collectors.toList());
        return new StudentListResponse(reponseList);
    }

}

실행 로그

컨트롤러에서 실행될 때 로그를 남기도록 만들었다.
조회는 각각 2번씩 실행했다.

캐시를 적용한 서비스

YAML 설정

application.yml파일에 해당 설정을 추가한다.

spring:
  cache:
    type: redis
  redis:
    host: localhost # 로컬에서 실행
    port: 6379 # redis 기본 포트

Redis.Config 추가

@Configuration
@EnableCaching
public class RedisConfig {
//    @Value("${spring.redis.host}")
//    private String host;
//
//    @Value("${spring.redis.port}")
//    private int port;
//
//    @Bean
//    public RedisConnectionFactory redisConnectionFactory() {
//        return new LettuceConnectionFactory(host, port);
//    }

    @Bean
    public RedisCacheConfiguration cacheConfiguration() {
    	// 설정을 변경함.
        return RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(2)) // 캐시 만료시간을 2분으로 설정
                .disableCachingNullValues() // null 값을 캐시로 저장하지 않음
                .serializeValuesWith( // 캐시 값의 직렬화 방식 설정
                        RedisSerializationContext.SerializationPair.fromSerializer(
                                new GenericJackson2JsonRedisSerializer() // Jackson을 사용하여 객체를 JSON으로 변환하고 역직렬화
                        )
                );
    }
}

디폴트 설정
캐시된 데이터가 만료되지 않음. 즉 캐시된 데이터가 무제한으로 저장됨.
null 값을 캐시로 저장할 수 있음.
캐시 값의 직렬화 방식은 기본적으로 JDK Serialization을 사용. 이는 Java 객체를 직렬화하여 Redis에 저장하고 역직렬화하는 방식.

@EnableCaching

어노테이션을 사용하면 스프링 애플리케이션에서 캐싱을 활성화하고, @Cacheable, @CachePut, @CacheEvict 등의 캐시 관련 어노테이션을 사용하여 메서드 수준에서 캐시를 관리할 수 있습니다.

서비스에 @Cacheable 추가

@Cacheable 메소드에 대한 캐싱 동작을 활성화할 수 있다.
메소드에 @Cacheable를 붙혀서 캐싱 기능을 사용해보자.

@Service
@RequiredArgsConstructor
@Transactional
public class TestService {

    private final StudentRepository studentRepository;

	// 1. 학생 저장 기능
    public Long save(StudentRequest request) {
        System.out.println("===> SAVE 로직 실행");
        ...
    }

	// 2. 학생 조회 기능
    @Cacheable(value = "Single", key = "#id")
    public StudentResponse find(Long id) {
        System.out.println("===> FIND 로직 실행");
        ...
    }

	// 3. 특정 나이의 학생들 조회 기능
    @Cacheable(value = "Age", key = "#age")
    public StudentListResponse findAge(int age) {
        System.out.println("===> FIND-AGE 로직 실행");
        ...
    }
    
    // 4. 특정 나이와 특정 이름의 학생들 조회 기능
    @Cacheable(value = "Age", key = "#age")
    public StudentListResponse findNameAndAge(AgeAndNameRequest request) {
        System.out.println("===> FIND NAME AND AGE 로직 실행");
        ...
    }

}

실행 로그

저장 부분은 달라진 점이 없으니 놔두고 조회 부분만 2번씩 실행해보겠다.
컨트롤러는 2번씩 실행되지만, 서비스 로직이 1번씩만 실행되고 있는 것을 확인할 수 있다.

Redis 확인하기

다음 정보는 각각의 서비스 메소드에 파라미터로 들어간 값들이다.

find 메소드 파라미터: id=2
findAge 메소드 파라미터: age=20
findNameAndAge 메소드 파라미터: {age=33, name=장장장}

파라미터에 따라 Redis에 키값으로 저장된 것을 확인할 수 있다.

또한 요청으로 캐싱이 되자마자 TTL로 만료 시간을 확인해보면, 120초가 나오는 것을 확인할 수 있다.



참고

스프링 데이터 Redis 소개
Redis를 사용한 스프링 부트 캐시
Spring의 캐싱 가이드

https://github.com/Jang990/spring-redis

profile
공부한 내용을 적지 말고 이해한 내용을 설명하자

0개의 댓글

Powered by GraphCDN, the GraphQL CDN