일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- JavaScript
- exception
- Collections
- design-pattern
- Python
- 큐
- 자바
- Java
- DesignPattern
- 람다 칼큘러스
- Spring
- Eclipse
- 백준
- Network
- lambda calculus
- Pattern
- JDBC
- solid
- tcp
- 프로그래머스
- functional programming
- 겨울카카오인턴
- 디자인패턴
- 파이썬
- Collection
- Rails
- 로버트마틴
- 함수형 프로그래밍
- 스택
- javscript
Archives
- Today
- Total
개발자 노트
코틀린에서 자기 참조를 우회하는 방법 (feat. 확장함수) 본문
스프링 AOP 문제
Spring에서 self-invocation은 클래스의 메서드가 같은 클래스 내의 다른 메서드를 호출하는 방식으로, 특히 @Transactional과 같은 AOP(Aspect-Oriented Programming) 관련 애노테이션을 사용할 때 문제를 일으킬 수 있습니다. Spring에서는 프록시 기반 AOP를 사용하기 때문에, 프록시 객체가 아닌 클래스의 실제 메서드가 호출될 경우 AOP 애노테이션이 적용되지 않습니다.
예를 들어, Spring에서 @Transactional 애노테이션을 적용한 메서드 A가 같은 클래스의 다른 메서드 B에서 호출될 때, B가 프록시를 통해 호출되지 않으면 A의 트랜잭션이 제대로 관리되지 않을 수 있습니다. 이는 프록시가 생성한 객체 외부에서 메서드를 호출해야 AOP 애노테이션이 제대로 적용되기 때문입니다.
@Service
public class MyService {
// 문제 발생 코드
public void methodB() {
// 같은 클래스의 메서드를 직접 호출 (self-invocation)
methodA();
}
@Transactional
public void methodA() {
// 트랜잭션이 제대로 작동하지 않을 수 있음
System.out.println("methodA 트랜잭션 시작");
// 데이터베이스 작업 등...
System.out.println("methodA 트랜잭션 종료");
}
}
코틀린스러운 해결 방법
확장함수를 이용하면 됩니다.
확장함수를 사용할 경우
* 클래스명Kt.class 로 확장함수가 컴파일되며, static method로 스프링이 주입해준 객체를 호출합니다.
확장함수 디컴파일 결과
확장 함수를 활용한 예제
목표
유저 목록을 반환하는 캐시는 사용하되, 편의 메서드로 Map을 반환하는 함수를 만들자.
import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Service
data class User(val id: Long)
@Service
class UserService {
@Cacheable("users")
fun getUsers(): List<User> {
return listOf(
User(1L),
User(2L),
User(3L),
User(4L),
)
}
fun userMapV1(): Map<Long, User> {
// self-invocation
return getUsers().associateBy { it.id }
}
}
fun UserService.userMapV2(): Map<Long, User> {
return getUsers().associateBy { it.id }
}
- getUsers()
- 유저를 조회하는 메서드이며, 결과인 유저 목록을 캐싱합니다.
- userMapV1()
- self-invocation 했기 때문에 캐시를 사용하지 않고 매번 실제 값으로 Map을 반환합니다.
- userMapV2()
- 확장 함수로 선언했기 때문에 정상적으로 캐시된 유저 목록으로 Map으로 반환합니다.
간단한 테스트
import com.github.benmanes.caffeine.cache.stats.CacheStats
import jurogrammer.self.user.UserService
import jurogrammer.self.user.userMapV2
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cache.caffeine.CaffeineCache
import org.springframework.cache.caffeine.CaffeineCacheManager
@SpringBootTest
class CacheTest {
@Autowired
private lateinit var userService: UserService
@Autowired
private lateinit var cacheManager: CaffeineCacheManager
@Test
fun `getUsers 호출시 캐시가 사용된다`() {
// given
val rep = 5
// when
repeat(rep) { userService.getUsers() }
// then
val stats = cacheManager.getCacheStats("users")
assertThat(stats.missCount()).isEqualTo(1)
assertThat(stats.hitCount()).isEqualTo(4)
}
@Test
fun `객체 내 함수에서 getUsers를 호출한 경우 캐시가 사용되지 않는다`() {
// given
val rep = 5
// when
repeat(rep) { userService.userMapV1() }
// then
val stats = cacheManager.getCacheStats("users")
assertThat(stats.missCount()).isEqualTo(0)
assertThat(stats.hitCount()).isEqualTo(0)
}
@Test
fun `확장 함수를 사용하여 getUsers를 호출한 경우 캐시가 사용된다`() {
// given
val rep = 5
// when
repeat(rep) { userService.userMapV2() }
// then
val stats = cacheManager.getCacheStats("users")
assertThat(stats.missCount()).isEqualTo(1)
assertThat(stats.hitCount()).isEqualTo(4)
}
private fun CaffeineCacheManager.getCacheStats(cacheName: String): CacheStats {
val cache = this.getCache(cacheName) as? CaffeineCache
return cache?.nativeCache?.stats() ?: throw RuntimeException("stats cannot be null")
}
}
반응형
Comments