Skip to content
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/#34. 유저 활동 데이터 누적 AOP로 처리 #38

Merged
merged 39 commits into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1ccbc45
🔥 의미없는 로그 제거
RokwonK Apr 15, 2024
c83cbe2
🎨 Question관련 Entity 내 non-nullable 필드들 타입 수정
RokwonK Apr 15, 2024
66aedb2
✨ Question Open 검색 쿼리 추가
RokwonK Apr 15, 2024
472591c
✨ Question Get 및 발급 시 발생가능한 예외 추가
RokwonK Apr 15, 2024
49cc146
✨ Question Get & 발급에 필요한 쿼리추가
RokwonK Apr 15, 2024
b783a6c
✨ Question Domain 내 응답 Dto 추가
RokwonK Apr 15, 2024
d41137c
✨ Question 발급 도메인 로직 구현
RokwonK Apr 15, 2024
058d64a
✨ Question 가져오기 도메인 로직 구현
RokwonK Apr 15, 2024
a794d36
✨ Question Application Service 로직 구현
RokwonK Apr 15, 2024
aa47f73
✨ Question GET API 구현
RokwonK Apr 15, 2024
673aac5
✨ aop 의존성 추가
RokwonK Apr 16, 2024
3334df3
✨ Domain 내 Lock 관련 예외 추가
RokwonK Apr 16, 2024
5f7657e
✨ Lock에 사용될 Key interface 정의
RokwonK Apr 16, 2024
895dca1
✨ Lock을 사용할 메서드에 적용될 어노테이션 추가
RokwonK Apr 16, 2024
e47162d
✨ Lock 저장소 인터페이스 및 SameOrigin 구현체 추가
RokwonK Apr 16, 2024
9ca37fd
✨ 분산락 AOP 구현
RokwonK Apr 16, 2024
2fd1e19
✨ 오늘 Question 검색&발급 메서드에 분산락 적용
RokwonK Apr 16, 2024
84a5cde
✨ DistributedLockKey -> LockKey로 이름 변경
RokwonK Apr 17, 2024
85006e5
🎨 코드의 구조 / 형태 개선
RokwonK Apr 17, 2024
4fd4145
🎨 update submodule
RokwonK Apr 17, 2024
870b801
🎨 DistributedLockRepository 메서드 추가
RokwonK Apr 17, 2024
6d42c0f
✨ 새로운 Error 추가
RokwonK Apr 17, 2024
06f73a8
🔥 TODO 삭제
RokwonK Apr 17, 2024
b786013
✨ LockDataSource 어노테이션 추가
RokwonK Apr 17, 2024
7c667fe
✨ Lock 용 데이터소스 나누기
RokwonK Apr 17, 2024
548b529
✨ DistributedLockAdvisor 구현
RokwonK Apr 17, 2024
7056c8e
✨ SameOriginLockRepository 구현 완료
RokwonK Apr 17, 2024
5b53fb5
✨ 질문 발급 시 user의 question Cnt 올리기
RokwonK Apr 17, 2024
44773c0
✨ Domain 모듈 Test Fixture 추가(Member, Question)
RokwonK Apr 18, 2024
e332c7b
✅ issueQuestion 테스트 코드 추가
RokwonK Apr 18, 2024
8a2be3c
✅ 오늘의 문제, 문제 조회 테스트 코드 추가
RokwonK Apr 18, 2024
fe161e0
✅ 오늘의 문제 발급&조회 동시성 테스트 추가
RokwonK Apr 18, 2024
e12b735
Merge branch 'develop' of https://github.com/kids-ground/adevspoon-ba…
RokwonK Apr 19, 2024
0c68769
✨ AOP 우선순위 조절을 위한 트랜잭션 AOP 우선순위 조절
RokwonK Apr 19, 2024
3384684
✨ Activity 이벤트 정의
RokwonK Apr 19, 2024
dac7cce
✨ Activity Event Handler 추가
RokwonK Apr 19, 2024
570fc4a
✨ Activity Repository 내 쿼리 추가
RokwonK Apr 19, 2024
e5f5004
✨ Activity Event AOP 추가
RokwonK Apr 19, 2024
79b9f82
✨ BoardPost 생성 시 Activity Event 추가
RokwonK Apr 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import com.adevspoon.domain.board.exception.BoardTageNotFoundException
import com.adevspoon.domain.board.repository.BoardPostRepository
import com.adevspoon.domain.board.repository.BoardTagRepository
import com.adevspoon.domain.common.annotation.DomainService
import com.adevspoon.domain.common.annotation.ActivityEvent
import com.adevspoon.domain.common.annotation.ActivityEventType
import com.adevspoon.domain.common.service.LikeDomainService
import com.adevspoon.domain.common.utils.CursorPageable
import com.adevspoon.domain.common.utils.PageWithCursor
Expand All @@ -23,6 +25,7 @@ class BoardPostDomainService(
val memberDomainService: MemberDomainService,
val likeDomainService: LikeDomainService
) {
@ActivityEvent(ActivityEventType.BOARD_POST)
@Transactional
fun registerBoardPost(userId: Long, tagId: Int, title: String, content: String): BoardPost {
val user = memberDomainService.getUserEntity(userId)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.adevspoon.domain.common.annotation

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ActivityEvent(
val type: ActivityEventType
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.adevspoon.domain.common.annotation

enum class ActivityEventType {
ATTENDANCE,
ANSWER,
BOARD_POST,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.adevspoon.domain.common.aop

import com.adevspoon.domain.board.dto.response.BoardPost
import com.adevspoon.domain.common.annotation.ActivityEvent
import com.adevspoon.domain.common.annotation.ActivityEventType
import com.adevspoon.domain.common.event.BoardPostActivityEvent
import org.aspectj.lang.JoinPoint
import org.aspectj.lang.annotation.AfterReturning
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.reflect.MethodSignature
import org.springframework.context.ApplicationEventPublisher
import org.springframework.core.Ordered
import org.springframework.core.annotation.Order
import org.springframework.stereotype.Component


@Aspect
@Component
@Order(Ordered.LOWEST_PRECEDENCE)
class ActivityEventAdvisor(
private val eventPublisher: ApplicationEventPublisher
) {
@AfterReturning(pointcut = "@annotation(com.adevspoon.domain.common.annotation.ActivityEvent)", returning = "result")
fun afterReturningAdvice(joinPoint: JoinPoint, result: Any?) {
val activityAnnotation = getAnnotation(joinPoint)

when(activityAnnotation.type) {
ActivityEventType.ATTENDANCE -> attendanceEventPublish(result)
ActivityEventType.ANSWER -> answerEventPublish(result)
ActivityEventType.BOARD_POST -> boardPostEventPublish(result)
}
}

private fun getAnnotation(joinPoint: JoinPoint) =
(joinPoint.signature as MethodSignature).method
.getAnnotation(ActivityEvent::class.java)

private fun boardPostEventPublish(result: Any?) {
(result as? BoardPost)?.let {
eventPublisher.publishEvent(BoardPostActivityEvent(it.user.memberId))
}
}

private fun attendanceEventPublish(result: Any?) {
TODO("""
Implement the logic to publish the ATTENDANCE event
""".trimIndent())
}

private fun answerEventPublish(result: Any?) {
TODO("""
Implement the logic to publish the ANSWER event
""".trimIndent())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.adevspoon.domain.common.event

data class AnswerActivityEvent(
val memberId: Long,
val answerId: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.adevspoon.domain.common.event

data class AttendanceActivityEvent(
val memberId: Long
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.adevspoon.domain.common.event

data class BoardPostActivityEvent(
val memberId: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.adevspoon.domain.config

import org.springframework.context.annotation.Configuration
import org.springframework.scheduling.annotation.EnableAsync

@Configuration
@EnableAsync
class AsyncConfig
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
import org.springframework.core.Ordered
import org.springframework.data.jpa.repository.config.EnableJpaAuditing
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import org.springframework.transaction.annotation.EnableTransactionManagement
import javax.sql.DataSource


@Configuration
@EnableJpaAuditing
@EntityScan(basePackages = ["com.adevspoon.domain"])
@EnableJpaRepositories(basePackages = ["com.adevspoon.domain"])
// Transaction 전 후로 실행되는 AOP를 위해 직접 설정
@EnableTransactionManagement(order = Ordered.LOWEST_PRECEDENCE - 10)
class JpaConfig {

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
package com.adevspoon.domain.member.repository

import com.adevspoon.domain.member.domain.UserActivityEntity
import jakarta.persistence.LockModeType
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Lock
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query

interface UserActivityRepository : JpaRepository<UserActivityEntity, Long> {
@Modifying(clearAutomatically = true)
@Query("UPDATE UserActivityEntity u SET u.boardPostCount = u.boardPostCount + 1 WHERE u.id = :userId")
fun increaseBoardPostCount(userId: Long): Int

@Lock(LockModeType.PESSIMISTIC_WRITE)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserActivityEntity는 유저가 어떤 활동을 했을 때에만 데이터가 변경되는 것 같아요. 그렇다면 충돌 가능성이 매우 낮을 거라고 생각되고, 이럴 때는 낙관적 락이 적절하다고 생각해요. 비관적 락을 고민한 이유도 같이 적어주시면 이해해 도움이 될 것 같아요.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

낙관적 락을 사용하면 버전을 명시하는 필드가 필요하다는 점, 무엇보다도 재시도 처리 로직도 들어가야한다는 점이 걸렸습니다.
또한 요청이 많지 않을 것이기 때문에 락으로 인한 성능 이슈가 크지는 않을 듯 했습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요청의 빈도가 낮으면 비관적 락을 사용해도 성능에 미치는 영향이 미미하니 그대로 사용해도 좋을 것 같아요 :)

@Query("SELECT u FROM UserActivityEntity u WHERE u.id = :userid")
fun findByIdWithLock(userid: Long): UserActivityEntity?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.adevspoon.domain.member.service

import com.adevspoon.domain.common.annotation.DomainService
import com.adevspoon.domain.common.event.AnswerActivityEvent
import com.adevspoon.domain.common.event.AttendanceActivityEvent
import com.adevspoon.domain.common.event.BoardPostActivityEvent
import com.adevspoon.domain.member.repository.UserActivityRepository
import jakarta.transaction.Transactional
import org.springframework.scheduling.annotation.Async
import org.springframework.transaction.event.TransactionPhase
import org.springframework.transaction.event.TransactionalEventListener

@DomainService
class MemberActivityEventHandler(
private val userActivityRepository: UserActivityRepository,
) {

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional
fun handleAttendanceEvent(event: AttendanceActivityEvent) {
TODO("""
Implement the logic to handle the ATTENDANCE event
""".trimIndent())
}

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional
fun handleAnswerEvent(event: AnswerActivityEvent) {
TODO("""
Implement the logic to handle the ANSWER event
""".trimIndent())
}

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional
fun handleBoardPostEvent(event: BoardPostActivityEvent) {
userActivityRepository.increaseBoardPostCount(event.memberId)
}
}