-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/#543 로컬 캐시에 좋아요 데이터가 실시간으로 반영되도록 수정 #549
Changes from all commits
0ed68c1
64e0a88
44f67c5
21f7976
1fa1b45
d0d8fa2
0f1c9e3
fd5f35e
2c0a4ee
12ae35c
4266541
2373790
1d3fdcc
43c5651
e700811
2e8398a
03654ec
dc73563
358254e
1e18d62
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package shook.shook.auth.config; | ||
|
||
import java.util.List; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.context.annotation.Profile; | ||
import org.springframework.web.method.support.HandlerMethodArgumentResolver; | ||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; | ||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||
import shook.shook.auth.ui.argumentresolver.AuthArgumentResolver; | ||
import shook.shook.auth.ui.interceptor.LocalInterceptor; | ||
|
||
@Profile("local") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 전반적인 궁금증인데 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bearer 형식으로 지정하지 않았던 이유는 Local 에서 테스트할 때 필요한 오버헤드를 최대한 줄이고 싶어서입니다! 지금까지는 로컬에서 테스트 할 때도 항상 토큰을 만들어야 해서 너무 불편했는데, 이런 구조의 |
||
@Configuration | ||
public class LocalAuthConfig implements WebMvcConfigurer { | ||
|
||
private final AuthArgumentResolver authArgumentResolver; | ||
private final LocalInterceptor localInterceptor; | ||
|
||
public LocalAuthConfig(final AuthArgumentResolver authArgumentResolver, final LocalInterceptor localInterceptor) { | ||
this.authArgumentResolver = authArgumentResolver; | ||
this.localInterceptor = localInterceptor; | ||
} | ||
|
||
@Override | ||
public void addInterceptors(final InterceptorRegistry registry) { | ||
registry.addInterceptor(localInterceptor); | ||
} | ||
|
||
@Override | ||
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) { | ||
resolvers.add(authArgumentResolver); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package shook.shook.auth.ui.interceptor; | ||
|
||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import org.springframework.context.annotation.Profile; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.servlet.HandlerInterceptor; | ||
import shook.shook.auth.ui.AuthContext; | ||
|
||
@Profile("local") | ||
@Component | ||
public class LocalInterceptor implements HandlerInterceptor { | ||
|
||
private final AuthContext authContext; | ||
|
||
public LocalInterceptor(final AuthContext authContext) { | ||
this.authContext = authContext; | ||
} | ||
|
||
@Override | ||
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) | ||
throws Exception { | ||
final long memberId = Long.parseLong(request.getHeader("Authorization")); | ||
authContext.setAuthenticatedMember(memberId); | ||
|
||
return true; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,16 @@ | ||
package shook.shook.song.application; | ||
|
||
import jakarta.annotation.PostConstruct; | ||
import jakarta.persistence.EntityManager; | ||
import java.util.List; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.scheduling.annotation.Scheduled; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.transaction.annotation.Transactional; | ||
import shook.shook.song.domain.InMemorySongs; | ||
import shook.shook.song.domain.Song; | ||
import shook.shook.song.domain.killingpart.KillingPart; | ||
import shook.shook.song.domain.repository.SongRepository; | ||
|
||
@RequiredArgsConstructor | ||
|
@@ -17,6 +21,7 @@ public class InMemorySongsScheduler { | |
|
||
private final SongRepository songRepository; | ||
private final InMemorySongs inMemorySongs; | ||
private final EntityManager entityManager; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 따봉쓰 👍 |
||
|
||
@PostConstruct | ||
public void initialize() { | ||
|
@@ -26,6 +31,25 @@ public void initialize() { | |
@Scheduled(cron = "${schedules.in-memory-song.cron}") | ||
public void recreateCachedSong() { | ||
log.info("InMemorySongsScheduler worked"); | ||
inMemorySongs.recreate(songRepository.findAllWithKillingParts()); | ||
final List<Song> songs = songRepository.findAllWithKillingPartsAndLikes(); | ||
detachSongs(songs); | ||
inMemorySongs.refreshSongs(songs); | ||
} | ||
|
||
private void detachSongs(final List<Song> songs) { | ||
songs.stream() | ||
.peek(entityManager::detach) | ||
.flatMap(song -> song.getKillingParts().stream()) | ||
.forEach(entityManager::detach); | ||
} | ||
|
||
@Transactional | ||
@Scheduled(cron = "${schedules.in-memory-song.update-cron}") | ||
public void updateCachedSong() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 일정 주기마다 캐싱된 노래 데이터와 DB 싱크를 맞춰주는 코드 같아요!
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
log.info("InMemorySongsScheduler LikeCount update progressed"); | ||
final List<KillingPart> killingParts = inMemorySongs.getSongs().stream() | ||
.flatMap(song -> song.getKillingParts().stream()) | ||
.toList(); | ||
killingParts.forEach(entityManager::merge); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그리구 한 번 merge 한 다음에 다시 KillingPart를 detach 해주는 작업도 필요할 것 같은데 이건 따로 작성하지 않아도 되는건가용?.? (몰라서 하는 질문임니닷) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저도 이 부분이 궁금하네요! 따로 다시 detach를 안해도 괜찮은지 궁금합니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저 궁금한 것이 있는데 updateCachedSong이 동작하는 중간에 killingPart에 좋아요가 발생하는 상황은 따로 고려하지 않아도 되는 것인지도 궁금합니다. (updateCachedSong의 경우 영속성 컨텍스트로 정보를 merge하기에 이 때 좋아요가 발생해도 문제가 없는 것인지 궁금했습니다.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 동시성 관련된 부분은 당시 코드에서 고려되지 않았기 때문에 아코가 말씀하셨던 것처럼 updateCacheSong 이 동작하는 중간에 killingPart 에 좋아요가 발생하면 좋아요가 유실될 가능성이 있습니다. synchronized 키워드로 동시성을 보장하도록 코드를 작성했습니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그리고 원래라면 detach 를 해야 하지만, 위에서 말씀드린 것처럼 좋아요 데이터의 deleted 여부는 실시간으로 DB 에 반영되어야 할 것 같아서 하지 않았습니다...! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. detach를 하지 않는 이유를 혹시 조금만 더 자세하게 설명해주실 수 있을까요? 제가 잘 이해를 하지 못해서 그렇습니다!🥲 |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
import shook.shook.member.domain.repository.MemberRepository; | ||
import shook.shook.member.exception.MemberException; | ||
import shook.shook.song.application.killingpart.dto.KillingPartLikeRequest; | ||
import shook.shook.song.domain.InMemorySongs; | ||
import shook.shook.song.domain.killingpart.KillingPart; | ||
import shook.shook.song.domain.killingpart.KillingPartLike; | ||
import shook.shook.song.domain.killingpart.repository.KillingPartLikeRepository; | ||
|
@@ -22,6 +23,7 @@ public class KillingPartLikeService { | |
private final KillingPartRepository killingPartRepository; | ||
private final MemberRepository memberRepository; | ||
private final KillingPartLikeRepository likeRepository; | ||
private final InMemorySongs inMemorySongs; | ||
|
||
@Transactional | ||
public void updateLikeStatus( | ||
|
@@ -54,8 +56,8 @@ private void create(final KillingPart killingPart, final Member member) { | |
final KillingPartLike likeOnKillingPart = likeRepository.findByKillingPartAndMember(killingPart, member) | ||
.orElseGet(() -> createNewLike(killingPart, member)); | ||
if (likeOnKillingPart.isDeleted()) { | ||
inMemorySongs.like(killingPart, likeOnKillingPart); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 결과적으로 DB에 좋아요가 반영되는 코드는 없는 것 같은데, 캐시 데이터에 저장된 좋아요 개수가 언제 DB에 반영되는 건가요? 아직까지는 동시성 테스트를 해봤을 때, DB의 좋아요 개수는 증가/감소하지 않는 것 같아서요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DB 좋아요 개수 증감 로직을 안 짰네요... Scheduler 로 좋아요를 batch update 하는 로직도 만들어 보겠습니다! 좋은 지적 감사합니다 역시 바론이네요 ^^ |
||
likeRepository.pressLike(likeOnKillingPart.getId()); | ||
killingPartRepository.increaseLikeCount(killingPart.getId()); | ||
} | ||
} | ||
|
||
|
@@ -68,8 +70,8 @@ private KillingPartLike createNewLike(final KillingPart killingPart, final Membe | |
private void delete(final KillingPart killingPart, final Member member) { | ||
killingPart.findLikeByMember(member) | ||
.ifPresent(likeOnKillingPart -> { | ||
inMemorySongs.unlike(killingPart, likeOnKillingPart); | ||
likeRepository.cancelLike(likeOnKillingPart.getId()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 좋네용~ 그런데 좋아요 취소할 때는 // create 메서드 일부
if (likeOnKillingPart.isDeleted()) {
inMemorySongs.like(killingPart, likeOnKillingPart);
likeRepository.pressLike(likeOnKillingPart.getId());
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분은 제 실수인 것 같아요 호출하는 코드로 바꿔두었습니다 👍🏻 |
||
killingPartRepository.decreaseLikeCount(killingPart.getId()); | ||
}); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LocalAuthConfig가 생기면서 product와 local를 분리할 때 "!"연산자를 이용해 분리한 것 처음 배워갑니다!