https://github.com/Seungkyu-Han/Toge-do-backend
GitHub - Seungkyu-Han/Toge-do-backend: Toge-do 앱의 백엔드 리포지토리입니다
Toge-do 앱의 백엔드 리포지토리입니다. Contribute to Seungkyu-Han/Toge-do-backend development by creating an account on GitHub.
github.com
얼마 전에 게임회사의 면접을 보게 되었다.
해당 면접에서 Heap 영역에서의 메모리 단편화를 막기 위해서는 어떤 방법이 좋을까요?라는 질문이 있었다.
그 당시에는 답변을 하지 못했지만, 지금와서 생각해보면 이 ObjectPool을 적용하는 것이 답일 것 같다.
잊지 않기 위해 현재 진행하고 있는 프로젝트에 ObjectPool을 적용해보려 한다.
직접 만들어야 하나... 생각하고 있었는데
다행히 스프링에서 해당 라이브러리가 존재한다.
일단 Object Pool은 객체가 필요할 때, 매번 생성하는 것이 아니라 관리하고 있는 pool에서 가져오는 것을 말한다.
이렇게 Pool에서 객체를 가져오게 되면, 우선 객체를 생성하는 데 필요한 비용이 감소한다.
객체를 생성할 때, 메모리를 할당하고 객체를 생성하는 과정은 시간이 굉장히 오래 걸리기 때문에 이 과정을 아낄 수 있다.
또한, 불연속적인 Heap 메모리에 계속 할당하지 않기 때문에 메모리의 단편화가 줄어들게 된다.
찾아보니 게임업계에서 굉장히 많이 사용하는 방법인 것 같다.
바로 적용을 해보도록 하자.
//OBJECT POOL
implementation("org.apache.commons:commons-pool2:2.12.1")
해당 라이브러리를 추가해준다.
java에서 Object Pool을 관리해주는 라이브러리이다.
그 다음에는 Pool에서 관리할 객체를 넣어주도록 하자.
나는 UserDocument를 Pool에서 관리하도록 할 것이다.
@Component
class UserPoolComponent: BasePooledObjectFactory<UserDocument>() {
private var a = 0
override fun create(): UserDocument {
a += 1
return UserDocument(
name = a.toString(),
oauth = Oauth()
)
}
override fun wrap(userDocument: UserDocument): PooledObject<UserDocument> {
return DefaultPooledObject(userDocument)
}
}
우선 create 메서드를 오버라이딩해서 미리 저장해둘 객체들을 만들어준다.
이 create 메서드를 통해 객체를 생성해서 pool에 저장해두게 된다.
wrap 메서드를 통해 해당 객체를 관리하도록 하는데, 이것도 DefaultPooledObject를 통해 라이브러리를 사용해 객체를 관리할 수 있다.
wrap을 통해 해당 객체의 대여상태, 유휴상태등을 저장하고 관리하게 된다.
그리고는
val userPool = GenericObjectPool(userPoolComponent, GenericObjectPoolConfig<UserDocument>().apply{
maxTotal = 10
minIdle = 5
})
해당 component로 pool을 만들어서 사용한다.
해당 pool에서 borrowObject를 통해 객체를 대여하고, returnObject를 통해 객체를 반납한다.
당연하게도 대여한 후에 반납을 꼭 해줘야, 다음 사용자가 사용이 가능하다.
그리고 중요한 부분은, 반납하는 순간은 해당 함수의 종료 시점이 아니라 해당 함수를 호출한 함수가 해당 객체의 사용이 끝날 때이다.
호출당한 함수에서 반납을 해버리면, 동시성 문제가 발생 할 수 있다.
일단 테스트를 진행해보도록 하자.
@Test
@DisplayName("Object Pool 테스트")
fun createUserObjectPoolTest(){
//given
for (i in 1..10){
val a = userService.userPool.borrowObject()
println(a)
}
//when
//then
}
이렇게 10개를 반납하지 않고, 호출해보았다.
객체들의 이름은 알기 쉽도록 1부터 10까지 차례로 생성해보았다.
이렇게 반납을 하지 않으니, 계속 객체풀에서 다음 객체가 대여되어 다른 이름을 가지는 것을 볼 수 있다.
@Test
@DisplayName("Object Pool 테스트")
fun createUserObjectPoolTest(){
//given
for (i in 1..10){
val a = userService.userPool.borrowObject()
println(a)
userService.userPool.returnObject(a)
}
//when
//then
}
이렇게 반납을 하면서 출력을 해보니,
하나의 객체만을 사용해서 출력이 되는 것을 볼 수 있다.
Object Pool이 객체 생성에 대한 비용을 아낄 수는 있지만, 해당 객체의 사용 종료 시점을 추적해야 하는 등 생각보다 사용이 어렵다.
동시성 문제를 피하기 위해서는 정확하게 추적이 가능할 때만 사용하도록 하자.
'토이 프로젝트' 카테고리의 다른 글
스프링의 AOP @Transactional 알아보기 (0) | 2025.04.11 |
---|---|
트랜잭션 격리 수준(Transaction Isolation Level) (0) | 2025.03.26 |
Stomp에서 Jwt Filter 구현하기 (1) | 2025.01.23 |
Hexagonal Architecture 적용하기 (1) | 2025.01.22 |
직렬화 과정에서 함수 조건 검사 (0) | 2025.01.13 |