Caching: 동일한 요청에 대한 응답을 미리 저장해두고, 사용한다. 캐시에서 가져올 경우 속도가 비약적으로 상승( 탐색 필요 x disk에서 정보를 가져오지 않고, 메모리에서 가져오기에 Disk IO 감소) 다만 캐싱은 "일관성" 유지가 필수 적이다.
일관성: 캐시 내의 정보와 disk 내의 정보가 다르다면, 해당 캐싱 값은 "INVALID" 한 값이다.
캐싱 방법
1) 스프링 내장 캐싱 사용하기
implementation 'org.springframework.boot:spring-boot-starter-cache'
@SpringBootApplication
@EnableCaching
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
[@Cacheable(): 캐시 적용]
public class MemberService {
private final MemberRepository menuListRepository;
@Transactional
@Cacheable("member") // 캐시 데이터 공간의 명칭
public List<Member> getMemberList() {
return this.memberRepository.findAll();
}
@Transactional
@Cacheable(value = "memeber", key = "#id") // id를 캐시의 키값으로 설정
public Member getMember(int id) {
return this.memberRepository.findById(id).orElseThrow(()->RuntimeExceptio("error"));
}
}
@Cacheable는 메소드의 결과를 캐싱하며, value는 캐시할 데이터의 이름을 나타내며, key는 캐시에서 데이터를 찾을 때 사용할 키를 나타낸다. value 같은 경우, 이후에 해당 캐시 데이터의 업데이트, 삭제를 수행하는 @CachePut, @CacheEvict 등에서 value로 지정해서 원하는 캐시를 수정, 삭제 가능하다.
**주의 캐시 키(key)를 설정할 때는 해당 캐시의 유니크한 값을 선택해야 합니다
cache 테스트 코드
@Test
public void findMemberMultipleTime (){
for(int i=0;i<10;i++) {
ClubMember entityById = clubMemberService.findEntityById(9983L);
log.info("user find id: " + entityById.getId());
}
}
cache가 없을 때
@Override
public ClubMember findEntityById(Long id) {
if (id == null) {
throw new IllegalArgumentException("id cannot be null");
}
return memberRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("ClubMember not found"));
}
Result
ibernate: select clubmember0_.club_member_id as club_mem1_2_0_, clubmember0_.created_date as created_2_2_0_, clubmember0_.moddate as moddate3_2_0_, clubmember0_.email as email4_2_0_, clubmember0_.from_social as from_soc5_2_0_, clubmember0_.name as name6_2_0_, clubmember0_.password as password7_2_0_, clubmember0_.profile_file_name as profile_8_2_0_, clubmember0_.step_id as step_id9_2_0_, clubmember0_.token_id as token_i10_2_0_ from club_member clubmember0_ where clubmember0_.club_member_id=?
2023-04-26 11:54:31.500 INFO 99192 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
Hibernate: select clubmember0_.club_member_id as club_mem1_2_0_, clubmember0_.created_date as created_2_2_0_, clubmember0_.moddate as moddate3_2_0_, clubmember0_.email as email4_2_0_, clubmember0_.from_social as from_soc5_2_0_, clubmember0_.name as name6_2_0_, clubmember0_.password as password7_2_0_, clubmember0_.profile_file_name as profile_8_2_0_, clubmember0_.step_id as step_id9_2_0_, clubmember0_.token_id as token_i10_2_0_ from club_member clubmember0_ where clubmember0_.club_member_id=?
2023-04-26 11:54:31.503 INFO 99192 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
Hibernate: select clubmember0_.club_member_id as club_mem1_2_0_, clubmember0_.created_date as created_2_2_0_, clubmember0_.moddate as moddate3_2_0_, clubmember0_.email as email4_2_0_, clubmember0_.from_social as from_soc5_2_0_, clubmember0_.name as name6_2_0_, clubmember0_.password as password7_2_0_, clubmember0_.profile_file_name as profile_8_2_0_, clubmember0_.step_id as step_id9_2_0_, clubmember0_.token_id as token_i10_2_0_ from club_member clubmember0_ where clubmember0_.club_member_id=?
2023-04-26 11:54:31.505 INFO 99192 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
Hibernate: select clubmember0_.club_member_id as club_mem1_2_0_, clubmember0_.created_date as created_2_2_0_, clubmember0_.moddate as moddate3_2_0_, clubmember0_.email as email4_2_0_, clubmember0_.from_social as from_soc5_2_0_, clubmember0_.name as name6_2_0_, clubmember0_.password as password7_2_0_, clubmember0_.profile_file_name as profile_8_2_0_, clubmember0_.step_id as step_id9_2_0_, clubmember0_.token_id as token_i10_2_0_ from club_member clubmember0_ where clubmember0_.club_member_id=?
2023-04-26 11:54:31.508 INFO 99192 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
Hibernate: select clubmember0_.club_member_id as club_mem1_2_0_, clubmember0_.created_date as created_2_2_0_, clubmember0_.moddate as moddate3_2_0_, clubmember0_.email as email4_2_0_, clubmember0_.from_social as from_soc5_2_0_, clubmember0_.name as name6_2_0_, clubmember0_.password as password7_2_0_, clubmember0_.profile_file_name as profile_8_2_0_, clubmember0_.step_id as step_id9_2_0_, clubmember0_.token_id as token_i10_2_0_ from club_member clubmember0_ where clubmember0_.club_member_id=?
2023-04-26 11:54:31.510 INFO 99192 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
Hibernate: select clubmember0_.club_member_id as club_mem1_2_0_, clubmember0_.created_date as created_2_2_0_, clubmember0_.moddate as moddate3_2_0_, clubmember0_.email as email4_2_0_, clubmember0_.from_social as from_soc5_2_0_, clubmember0_.name as name6_2_0_, clubmember0_.password as password7_2_0_, clubmember0_.profile_file_name as profile_8_2_0_, clubmember0_.step_id as step_id9_2_0_, clubmember0_.token_id as token_i10_2_0_ from club_member clubmember0_ where clubmember0_.club_member_id=?
2023-04-26 11:54:31.511 INFO 99192 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
Hibernate: select clubmember0_.club_member_id as club_mem1_2_0_, clubmember0_.created_date as created_2_2_0_, clubmember0_.moddate as moddate3_2_0_, clubmember0_.email as email4_2_0_, clubmember0_.from_social as from_soc5_2_0_, clubmember0_.name as name6_2_0_, clubmember0_.password as password7_2_0_, clubmember0_.profile_file_name as profile_8_2_0_, clubmember0_.step_id as step_id9_2_0_, clubmember0_.token_id as token_i10_2_0_ from club_member clubmember0_ where clubmember0_.club_member_id=?
2023-04-26 11:54:31.512 INFO 99192 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
Hibernate: select clubmember0_.club_member_id as club_mem1_2_0_, clubmember0_.created_date as created_2_2_0_, clubmember0_.moddate as moddate3_2_0_, clubmember0_.email as email4_2_0_, clubmember0_.from_social as from_soc5_2_0_, clubmember0_.name as name6_2_0_, clubmember0_.password as password7_2_0_, clubmember0_.profile_file_name as profile_8_2_0_, clubmember0_.step_id as step_id9_2_0_, clubmember0_.token_id as token_i10_2_0_ from club_member clubmember0_ where clubmember0_.club_member_id=?
2023-04-26 11:54:31.514 INFO 99192 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
Hibernate: select clubmember0_.club_member_id as club_mem1_2_0_, clubmember0_.created_date as created_2_2_0_, clubmember0_.moddate as moddate3_2_0_, clubmember0_.email as email4_2_0_, clubmember0_.from_social as from_soc5_2_0_, clubmember0_.name as name6_2_0_, clubmember0_.password as password7_2_0_, clubmember0_.profile_file_name as profile_8_2_0_, clubmember0_.step_id as step_id9_2_0_, clubmember0_.token_id as token_i10_2_0_ from club_member clubmember0_ where clubmember0_.club_member_id=?
2023-04-26 11:54:31.515 INFO 99192 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
Hibernate: select clubmember0_.club_member_id as club_mem1_2_0_, clubmember0_.created_date as created_2_2_0_, clubmember0_.moddate as moddate3_2_0_, clubmember0_.email as email4_2_0_, clubmember0_.from_social as from_soc5_2_0_, clubmember0_.name as name6_2_0_, clubmember0_.password as password7_2_0_, clubmember0_.profile_file_name as profile_8_2_0_, clubmember0_.step_id as step_id9_2_0_, clubmember0_.token_id as token_i10_2_0_ from club_member clubmember0_ where clubmember0_.club_member_id=?
2023-04-26 11:54:31.516 INFO 99192 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
cache가 있을 때
@Override
@Cacheable( value = "member" ,key = "#id")
public ClubMember findEntityById(Long id) {
if (id == null) {
throw new IllegalArgumentException("id cannot be null");
}
return memberRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("ClubMember not found"));
}
Result
Hibernate: select clubmember0_.club_member_id as club_mem1_2_0_, clubmember0_.created_date as created_2_2_0_, clubmember0_.moddate as moddate3_2_0_, clubmember0_.email as email4_2_0_, clubmember0_.from_social as from_soc5_2_0_, clubmember0_.name as name6_2_0_, clubmember0_.password as password7_2_0_, clubmember0_.profile_file_name as profile_8_2_0_, clubmember0_.step_id as step_id9_2_0_, clubmember0_.token_id as token_i10_2_0_ from club_member clubmember0_ where clubmember0_.club_member_id=?
2023-04-26 10:30:49.857 INFO 98473 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
2023-04-26 10:30:49.858 INFO 98473 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
2023-04-26 10:30:49.858 INFO 98473 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
2023-04-26 10:30:49.858 INFO 98473 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
2023-04-26 10:30:49.858 INFO 98473 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
2023-04-26 10:30:49.858 INFO 98473 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
2023-04-26 10:30:49.858 INFO 98473 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
2023-04-26 10:30:49.858 INFO 98473 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
2023-04-26 10:30:49.858 INFO 98473 --- [ Test worker] com.example.clubsite.testMember : user find id: 9983
cache가 있다면, 동일한 쿼리 수행 시 sql이 1번만 수행된다.
[적용 예시]
이제 캐시 적용에 의한 실제 적용 방법에 대해 알아보자.
채팅 방내에서, 자신을 제외한 모든 유저를 찾는 기능의 수행 횟수가 높다고 가정해보자.( 자신을 제외한 모든 유저에게 채팅 알림 메세지 전송 등등...)
이 경우, 두가지 선택지로 나누어볼 수 있다.
1) 쿼리를 날려서 채팅 방 내의 모든 유저를 가져온 뒤, 자신은 제외하기
2) 쿼리를 날려서 채팅방 내의 모든 유저중, 자신을 제외한 모든 유저 가져오기
이 경우, 2번이 추가적인 작업을 수행해주지 않아도 되어 편해보일 수 있다.
하지만 해당 기능의 수행횟수가 매우높기에 해당 기능에 캐시를 적용한다는 가정하에 비교해보자.
@Override
@Cacheable(value = "request_list", key = "#chatRoomId + '-' + #clubMemberId")
public List<ClubMember> findOtherClubMembersInChatRoom(Long chatRoomId, Long clubMemberId) { // 캐시를 적용하면, 너무 많은 캐시데이터 생성. 채팅룸 - 유저 를 key 로 생성시 너무 많아진다.
List<ClubMember> allMemberIdsByChatRoomId = this.findAllMemberIdsByChatRoomId(chatRoomId);// chatRoom의 모든 유저 가져오는 것만 캐시로 가져오고, 여기서 1명을 제외시키자!
return memberChatRoomRepository.findOtherClubMembersInChatRoom(chatRoomId, clubMemberId).orElseThrow(()-> new RuntimeException("no user without you"));
}
캐시의 경우, 만약 key 값이 캐시 데이터 안의 특정 값의 key 값과 일치한다면, 해당하는 값을 그대로 돌려주는 것이다.
따라서 key에 각 캐시 데이터를 "고유하게 식별 가능한" key 를 설정해야한다. (아니면 원치 않는 결과가 들어오게 될 것)
하지만 위와 같은 메소드의 경우
1) 특정 채팅방
2 제외될 유저
2가지 식별자를 조합하기에, 많은 양의 캐시가 생성될 것이다.
ex) 10명이 있는 채팅방 "ChatRoom1" 에서 A,B,C,D 가 채팅 전송 시, A 를 제외한 9명, B를 제외한 9명, C를 제외한 9명, D를 제외한 9명에게 알림이 가는 기능이 수행되야하고, 만약 이를 위의 메소드를 사용한다고 가정해보자.
그럼 "ChatRoom1"-A,"ChatRoom1"-,"ChatRoom1"-C,"ChatRoom1"-D 즉 채팅방 내 모든 전송자에 대해 캐시가 생성될 것이다.
아래는 또 다른 방법이다.
@Cacheable( value = "request_list" ,key = "#chatRoomId")
@Override
public List<ClubMember> findAllMemberIdsByChatRoomId(Long chatRoomId) {
return memberChatRoomRepository.findAllMemberIdsByChatRoomId(chatRoomId).orElseThrow(()->new RuntimeException("findAllMemberIdsByChatRoomId error!"));
}
.........
List<ClubMember> allMemberInChatRoom = memberChatRoomService.findAllMemberIdsByChatRoomId(chatRoomId);
for (ClubMember chatRoomMemberExceptMe : allMemberInChatRoom) {
if(clubMemberId==chatRoomMemberExceptMe.getId())continue; //자신은 제외.
}
위와같이 "모든 채팅방 멤버" 를 구한 뒤, 추가 로직에서 1명만 선별한다고 생각해보자.
그러면 캐시데이터가 "각 채팅방 당 1개" 생성이 될 것이고, "자기자신을 제외" 하는 아래의 라인을 추가하는 것만으로 캐시 공간을 매우 절약할 수 있다.
if(clubMemberId==chatRoomMemberExceptMe.getId())continue;
[캐시 일관성 업데이트: @CachePut]
스프링 내장 캐시도 그렇고 대부분의 캐시는 ,캐시의 일관성을 유지하지 않는다.
일관성이란?
데이터 베이스내의 데이터와 캐시의 데이터가 일치하지 않는 것이다.
만약 ChatRoom1 에 A,B 가 있는데, C 가 해당 채팅 방에 추가적으로 참여한다고 가정해보자.
캐시에는 ChatRoom1에 A,B가 있다고 나오지만, DB에는 A,B,C 가 존재하다고 나오고, 이를 캐시의 일관성이 유지되지 않았다고 한다.
이런 경우 해당 캐시의 데이터는 "잘못된 데이터" 이기에 사용되어선 안된다.
따라서 이러한 캐시를 업데이트하거나(유지 비용이 많다), 버리고 새로 가져오는 방법이 있다(update는 유지 비용이 많이 드니 그냥 새로 하자!).
@Cacheable( value = "request_list" ,key = "#chatRoomId")
@Override
public List<ClubMember> inviteMemberToChatRoom(Long memberId,Long chatRoomId){
ClubMember findMember = clubMemberService.findEntityById(memberId);
ChatRoom findChatRoom = chatRoomService.findEntityById(chatRoomId);
MemberChatRoom memberChatRoom = MemberChatRoom.builder()
.clubMember(findMember)
.chatRoom(findChatRoom)
.build();
this.save(memberChatRoom);//새로운 채팅방 멤버 추가.
//이후에 원본 리스트+ 새롭게 추가된 멤버의 List를 반환해야 캐시가 올바르게 대체된다.
}
@CachePut(value = "chatRoom", key = "#id")
@Override
public List<ClubMember> findAllMemberIdsByChatRoomId(Long chatRoomId) {
return memberChatRoomRepository.findAllMemberIdsByChatRoomId(chatRoomId).orElseThrow(()->new RuntimeException("findAllMemberIdsByChatRoomId error!"));
}
@CachePut(value="CacheName",key=~~)
을 적용한 메소드의 return 값으로 "CacheName"의 key에 위치한 데이터를 대채한다.
즉, @Cahceable(value="CacheName",key=~~) 와 리턴 타입이 일치해야한다. ( 같은 형태의 데이터로 대체해야하기에)
만약 리스트에서 특정 1개의 원소만 변경하더라도, 전체 리스트를 갈아 껴서 업데이트해야하는 등의 문제가 있기에, List를 반환하는 메소드의 경우, 그냥 해당 캐시를 버리고 새로 가져오는 것이 여러모로 더 나은 경우가 많다.
[캐시 제거: @CacheEvict]
메소드가 리스트를 반환할 경우, 변경된 부분만 캐시를 업데이트하는건 쉽지 않다. 따라서 캐시를 유지하는 것을 포기하고, 이제 데이터 업데이트로 인해 invalid해진 캐시데이터를 제거하여, 부정확한 데이터가 들어있는 캐시 를 제거하여 캐시 일관성을 유지하는 방법이 있다.
@Override
@CacheEvict(value = "chatRoom", key = "#chatRoomId") //채팅방에 유저 추가. 기존의 채팅방 멤버에 대한 캐시는 불필요해졌기에, 제거한다 (업데이트보다 이편이 전체적으로 나은 성능 예상)
public void inviteMemberToChatRoom(Long memberId,Long chatRoomId){
ClubMember findMember = clubMemberService.findEntityById(memberId);
ChatRoom findChatRoom = chatRoomService.findEntityById(chatRoomId);
MemberChatRoom memberChatRoom = MemberChatRoom.builder()
.clubMember(findMember)
.chatRoom(findChatRoom)
.build();
this.save(memberChatRoom);//새로운 채팅방 멤버 추가.
}
@Cacheable( value = "chatRoom" ,key = "#chatRoomId")
@Override
public List<ClubMember> findAllMemberIdsByChatRoomId(Long chatRoomId) {
re
최종적으로, 위와 같이 채팅방에 새로운 멤버가 들어오는 로직 수행 시, 채팅방의 멤버들을 저장해두는 캐시에서, 해당 채팅방의 캐시데이터를 삭제하는 것으로, 일관성을 유지시키는 것이 가장 나은 방안인 것 같았다.
+@
아래는 하나의 메소드에서 friend_list 캐시 데이터의 2개의 데이터를 key값을 기준으로 삭제하는 케이스이다.
@Transactional
@Caching(evict = {
@CacheEvict(value = "friend_list", key = "#fromMemberId"),
@CacheEvict(value = "friend_list", key = "#toMemberId")
})
이런식으로 다수의 캐시 데이터에 대해 evict, put, Cacheable 작업을 수행가능하다.
[스프링 내장 캐시의 한계]
이 캐시 데이터는 애플리케이션이 죽게되면 사라진다 ( 내장 되어있는 캐시 이기에 ). 따라서 애플리케이션이 죽더라도, 기존 캐시를 계속해서 이용하고 싶다면, 별도의 캐싱 서버를 두고 분산 캐싱을 수행해야한다.
위와 같은 단점을 위해 따로 캐싱 서버를 두고 분산 캐싱을 수행하는 방법에는 여러가지가 있는데, 이후에 Redis를 사용하는 방법에 대해 정리하겠다.
'Spring boot' 카테고리의 다른 글
[spring 데이터 접근 핵심 원리 1] OverView (0) | 2023.05.25 |
---|---|
[Spring boot] 트랜잭션 설정 (0) | 2023.04.28 |
[query Optimization] rdb와 객체의 차이에 대해 (0) | 2023.04.26 |
[query Optimization] LAZY 로딩과 N+1 문제 (0) | 2023.04.26 |
[walk-talk] 걸음 수 기반 채팅 어플 데모 영상 (0) | 2023.04.11 |