티스토리 뷰

SPRING 공부

스프링 캐시

CodingDreamTree 2023. 11. 5. 11:10

캐시

미래 요청 혹은 반복되는 요청을 위해 데이터를 일시적으로 저장해놓고 원본 소스 접근 없이 엑세스할 수 있도록 도와주는

하드웨어 혹은 소프트웨어 구성 요소를 가리킨다.

 

데이터 요청을 할 때 원하는 데이터 캐시 저장소에 있다면 해당 데이터로 빠르게 반환되는데, 이를 캐시 히트라고 하며

캐시에 없는 경우는 캐시 미스라고 한다.

 

 

캐시 사용 유의점

 

● 캐시 설계

 

데이터 결과가 빈번하게 변경되거나 저장공간이 제한적인 경우 캐시를 사용하지 않는것이 좋다.

반복적이고 데이터 결과가 변경없는 요청에 대해서 사용하는 것이 좋다.

시스템의 요구 사항과 성능 목표를 고려해야한다.

 

● 캐시 키

 

키값에 의해서 캐시가 처리되는데, 해당 키가 정말 유일한 키인지 확인이 필요하다.

중복된 키값에 의해서 원하지 않는 다른 결과를 캐시해올 수 있기 때문이다.

 

● 만료 정책

 

캐시 데이터의 유효기간을 정해야 하는데, 상황에 따라 길게 혹은 짧게 가져갈 수 있도록 해야한다.

너무 짧게 설정하는 경우 캐시 효율이 떨어질 수 있으며 길게 가져가는 경우 데이터의 변화가

정말 없는건지 확인해보아야 한다.

 

● 메모리

 

메모리 캐시를 사용할 때, 메모리 사용량을 확인하고 제한해야한다. 모든 데이터를 메모리 캐시를

사용하면 메모리 사용량이 초과될 수 있기 때문에 유의해야한다.

 

● 캐시 폭탄(Cache Stampede)

 

모든 요청에 대해 캐시를 사용하거나 캐시 항목이 동시에 만료되면 많은 요청에 의해 동시에 원본 데이터를 다시 캐싱

하려고 할때 폭발적인 서버 부하가 일어날 수 있다. 

 

이를 방지하려면 랜덤화된 만료시간, 캐시 항목 갱신전략(미리 캐시항목 갱신 혹은 비동기 갱신),

워크큐 사용 등의 방법이 있다.

 

● 일관성 유지

 

캐시 데이터와 원본 데이터의 일관성 유지가 필요하다 원본 데이터가 업데이트 되는 경우 캐시데이터도 같이 

업데이트 되어야한다.

 

● 보안

 

상황에 따라 다르겠지만, 민감한 데이터가 캐시에 노출되는 상황을 방지해야한다.

 

● 갱신 전략

 

데이터 변화가 있는 경우 캐시 데이터를 일괄 갱신, 비동기 갱신, 부분 갱신 전략을 선택하여야 한다.

 

● 모니터링 및 로깅

 

캐시 동작 및 성능에 대해서 모니터링 해보고 문제 식별 및 처리 할 줄 알아야한다.

 

● 캐시 무효화(삭제)

 

캐시에서 무효화 되지않은 데이터는 오래된 데이터가 될 수 있으므로 적절한 무효화 처리를 해야한다.

 

● 테스트

 

캐시 사용전 후 테스트 수행하여 캐시가 의도한 대로 동작하고 문제가 없는지 확인해보아야 한다.

 

 

 

스프링 캐시

스프링 프레임 워크의 기능중 하나로 메서드 호출 결과나 데이터를 캐시에 저장하고 재사용하는데 사용된다.

스프링 캐시를 사용하면 빈번하게 사용되는 데이터나 계산결과를 메모리나 다른 영속성 저장소에 저장하여 

반복적 계산, 데이터 검색을 피하고 애플리케이션 응답 시간을 개선할 수 있다.

 

 

 

스프링 캐시 구성요소

 

● 캐시 관리자 (Cache Manager)

 

캐시 생성 및 구성, 캐시 엑세스, 캐시 업데이트 및 삭제, 캐시 적중률(캐시 히트) 관리, 캐시 클리어 및 인벤토리,

캐시 실패와 예외 처리 등 전반적인 캐시 관리를 할 수 있는 구성 요소이다.

 

● 캐시 어노테이션

 

@Cacheable: 메서드 호출 결과를 캐시에 저장하고, 동일한 인수로 재호출 될 때 캐시에서 결과를 반환한다.

@CachePut: 메서드 호출 결과를 강제로 저장한다. 기존 캐시 엔트릴르 갱신한다.

@CacheEvict: 캐시에서 특정 데이터를 삭제한다.

이 외에도 많은 어노테이션이 있지만 사용하면서 알아보도록 하자.

 

 

● 캐시

 

사용할 캐시를 지정해야한다. 각 캐시 관리자의 고유한 캐시 이름을 사용해야하며 키 / 값에 대한 정의

만료시간, 스토리지 위치등을 설정하고 사용한다.

 

 

 

카페인 캐시

카페인 캐시는 최적의 적중률(hit rate)를 제공하는 고성능 Java 캐싱 라이브러리이다.

ConcurrentMap과 유사하지만 완전히 동일하지 않으며 가장 큰 차이점은 ConcurrentMap은 모든 요소가 명시적으로

제거될 때까지 지속되지만 해당 캐시는 메모리를 제한하기 위해 항목을 자동적으로 제거하도록 구성된다.

경우에 따라서 자동 캐시 로딩으로 항목을 제거하지 않더라도

LodingCache(동기 캐싱) , AsyncLoadingCache(비동기 캐싱)가 사용될 수 있다.

 

카페인 캐시가 제공하는 기능들은 다음과 같다.

■ 선택적 비동기식으로 캐시 항목 자동 로드

빈도 및 최근 사용도를 기준으로 최대치에 가까울 정도로 크기 기반 퇴출(eviction)

마지막 접근 혹은 마지막 쓰기 이후 측정된 항목의 시간 기반 만료

항목에 대한 첫 번째 오래된 요청이 발생할 때 비동기식으로 새로 고침

약한 참조(week references)로 자동 래핑된 키

약한 참조 또는 소프트 참조로 자동 래핑된 값

퇴거 혹은 제거 알림

외부 리소스로 쓰기 전파

캐시 접근 통계치

 

약한 참조(week references)

객체 참조 유형의 하나로 가비지 컬렉션의 동작에 영향을 미치는 참조의 강도를 나타내는데 사용된다.

약한 참조의 경우 해당 객체를 가비지 컬렉션 대상으로 표시하고 더이상 강력한 참조를 가지고 있지 않을 때

메모리에서 제거될 수 있도록 허용한다. 주로 캐시 구현과 객체 감시에 사용된다.

 

소프트 참조(soft references)

가비지 컬렉터가 메모리 부족 상황에서만 수거할 수 있도록 허용하는 참조.

메모리 부족 시스템에서 응용 프로그램의 완전한 성능을 유지하면서 중요한 객체를 유지하고 복구할 때 유용하다.

 

 

 

카페인 캐시 설계 분석

출처:  Design · ben-manes/caffeine Wiki (github.com)

 

 

 


 

 

스프링 캐시 사용해보기

공식 문서와 GPT를 사용해 캐싱 처리를 해보자

문서는 Getting Started | Caching Data with Spring  와 IO (spring.io) 에서 에서 확인할 수 있다.

 

캐시의 구현체는 Caffeine 캐시(깃허브: ben-manes/caffeine: A high performance caching library for Java (github.com)) 를 활용한다.

공식문서에서는 해당 캐시 라이브러리가 존재하는경우 Caffeine Manager를 자동으로 등록해준다고 한다. (테스트 해보자)

 

의존성

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-cache'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
	runtimeOnly 'com.h2database:h2'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

 

property 설정

spring:
  sql:
    init:
      mode: always
  datasource:
    url: jdbc:h2:mem:mydb
    username: sa
    driver-class-name: org.h2.Driver
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    show-sql: true
    hibernate:
      ddl-auto: create
      naming:
        physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
    defer-datasource-initialization: true
    properties:
      hibernate:
        format_sql: true
        show_sql: true
        use_sql_comments: true
  h2:
    console:
      enabled: true

logging:
  level:
    org:
      springframework:
        cache: TRACE
      hibernate:
        sql: DEBUG
      type:
        descriptor:
          sql:
            BasicBinder: TRACE

 

 

스프링 캐시 의존성과 카페인 캐시 의존성을 추가하였다.

 

우선 캐싱을 사용할 수 있게 캐싱 기능을 활성화하도록한다.

 

SpringBootApplication 클래스

@EnableCaching
@SpringBootApplication
public class SpringcacheApplication {

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

}

 

 

@EnableChing: Spring의 주석 기반 캐시 관리기능을 활성화 한다.

 

 

 

 

Config 클래스

@Configuration
public class CacheConfig {

    /**
     * 캐시매니저로 카페인 캐시 매니저를 등록한다.
     * */
    @Bean
    public CacheManager caffeineCachemanager() {
        CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
        caffeineCacheManager.setCaffeine(caffeineConfig());  // 캐시매니저가 관리하는 캐시들의 공통 설정을 진행한다.
        caffeineCacheManager.registerCustomCache("firstCache", firstCache());
        return caffeineCacheManager;
    }



    private Caffeine<Object, Object> caffeineConfig() {
        return Caffeine.newBuilder()
                .initialCapacity(10)
                .maximumSize(500L)
                .expireAfterWrite(20, TimeUnit.HOURS);
    }
    
    private Cache<Object, Object> firstCache() {
        return Caffeine.newBuilder()
                .initialCapacity(20)
                .maximumSize(100L)
                .expireAfterAccess(10, TimeUnit.SECONDS)
                .build();
        
    }
}

 

 

 

CaffieneCacheManager가 가지고 있는 메서드 설명

 

● setCaffeine

해당 캐시 매니저가 지니는 캐시에 대한 공통 설정을 세팅한다. 

 

registerCustomCache

공통 설정을 무시하고 각 캐시별 인스턴스로 설정을 관리하여 처리한다.

 

 

 

Caffeine이 가지고 있는 메서드 설명:

 

  newBuilder 

빌더 패턴으로 카페인 클래스를 생성한다.

 

  initialCapacity

캐시 데이터 구조의 최소 크기를 설정, 크게 설정하면 크기 조정 작업이 일어나지 않아 비용이 적어지지만,

너무 크게 잡는 경우 메모리를 많이 차지 할 수 있다. [단위는 용량이 아닌 엔트리 갯수이다.]

maximumSize: 캐시에 포함될 수 있는 최대 항목(엔트리) 수를 지정한다. 제거하는 동안 이 임계점을 초과할 수 있으나 임계점에 가까워지는 

경우 다시 사용할 일이 적은 항목을 제거한다.(LRU 알고리즘)

0으로 설정하게 되면 캐시에 로드된 후 바로 제거된다.

(이는 테스트 유용 혹은 코드 변경없이 캐싱을 비활성화 할 수 있는 방법으로 될 수 있다.)

maximumSize를 사용하는 경우 maximumWeight와 함께 사용할 수 없다.

 

  expireAfterWriter

캐싱에 쓰여지거나, 대체되는 경우 메서드에 고정 된 시간만큼 지나면 항목이 자동으로 만료되게 한다. 

만료된 항목들의 크기는 계산될 수 있지 읽기 또는 쓰기 작업에 표시되지 않는다.

javadoc 클래스에 설명된 routine maintenance로 해당 만료 항목이 정리된다.

혹은 스케줄러를 정의해서 즉시 제거할 수 있도록 할 수 있다.

 

 

  expireAfterAccess

캐싱에 위와 동일한 경우 (쓰기 혹은 수정) 혹은 마지막 읽기 이후 고정된 시간 지나면 자동으로 만료된다.

 

  evictionListener

캐시 항목이 제거될 때마다 이를 알리기 위한 리스너를 지정한다. 

이 메서드를 호출한 후에는 빌더 참조를 더이상 사용하지 말아야하며 해당 return값으로 사용 해주어야한다.

 

 

 

로그 활성화

 

logback 사용시

    <logger name="org.springframework.cache" level="trace">
        <appender-ref ref="STDOUT" />
    </logger>

 

 

yml 사용시

logging:
  level:
    org:
      springframework:
        cache: TRACE

로 활성화 할 수 있다.

 

 

테스트를 위해 엔티티 하나를 만들어 놓고 DB에 저장하도록 해보자

 

Entity

@Entity
@Table(name = "member")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;
    private String name;
    private Integer age;
    @OneToMany(mappedBy = "member")
    private List<Home> homeList;
}


@Getter
@Entity
@Table(name ="home")
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Home {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "home_id")
    private Long id;
    private String address;
    private String addressDetail;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;
}

 

 

간단하게 회원을 찾아오는 API 만들어 단순히 호출만 2번 해보았다.

 

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/members")
public class MemberController {

    private final MemberService memberService;

    @GetMapping("/{seqMember}")
    public ResponseEntity<Member> findMember(@PathVariable long seqMember) {

        return ResponseEntity.ok(memberService.findMember(seqMember));
    }
}

 

예상과 동일하게  쿼리가 2번 나가게 되어있다.

 

회원 조회 요청 2번 호출 결과

 

 

Repository와 Service단에 Cache를 씌워보도록 하겠다.

 

먼저 Repository에 설정 후 동일하게 2번 호출해보았다.

처음 메서드 호출시에만 DB 서버 접근을 허용한 로그를 볼 수 있으며 그 이후로 몇번을 호출하여도 DB접근을 하지 않고

결과를 반환하였다.

 

 

'SPRING 공부' 카테고리의 다른 글

ORM , SQLMapper  (0) 2022.01.27
REST , REST API  (0) 2022.01.25
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함