brunch

You can make anything
by writing

C.S.Lewis

by 에디의 기술블로그 Nov 20. 2022

주니어를 위한
소소한 백엔드 개발 이야기 97

5. Object Pool Pattern

Overview 


주니어를 위한 소소한 백엔드 개발 이야기의 5번째 주제는 Object Pool Pattern 입니다. 너무 오랫만에 작성하는 글인데, 빠르게 글을 쓰고 다시 읽어보니 매우 잡다한 글이 되었습니다. 가벼운 마음으로 편하게 읽어주세요. 이 글에서는 부득이하게 Java & Spring 에 대한 예시를 포함하였습니다. "소소한 백엔드" 시리즈에서는 가능하면 언어에 종속적이지 않은 글을 쓰고 싶었으나, 필자의 지식이 미천하여 다양한 언어를 샘플로 제공하기 어렵습니다. 비록 자바&스프링을 샘플 예시로 작성된 글이지만, 다른 언어 (Go, C# 닷넷 등) 에서도 충분히 참고할 수 있으리라 생각됩니다. 


참고로 필자가 블로그는 주말에만 작성하는데, 

최근 육아로 인해서 글을 작성할 시간이 없어서 다음 글도 많이 늦어질 예정입니당



목차 


"주니어를 위한 소소한 백엔드 개발 이야기 97" 이라는 주제로 잡다하고 가벼운 글을 작성해서 공유합니다. 이 글은 시니어를 위한 글은 아닙니다. 취준생 또는 주니어 개발자들에게 조금이라도 도움이 되었으면 좋겠습니다. 다음 주제에 대해서 추천 받습니다. 주니어 개발자분들은 어떤 이야기를 나누고 싶으신지요? 


001. Java equals(), hashCode()

002. 의존성 주입

003. 백엔드 개발자에 대해서

004. 캐싱

005. [이번글] Object Pool Pattern


다음 주제는 미정, 언제 쓸지도 미정

- JVM 언어에 대해서 (자바, 코틀린 등)

- 리프레시 토큰이 왜 필요한가?

- 테스트 코드에 대해서

- 함수형 프로그래밍 vs 절차적 프로그래밍 

- 비동기 논블록킹

.......


등등 주제 고민 중 (추천 받음)



Object Pool Pattern(오브젝트풀 패턴)


오브젝트 풀 패턴에 대해서 간략하게 설명한다.


오브젝트 풀 패턴이란?

오브젝트풀 패턴은, 객체가 필요할때 객체풀에 요청하고 반환하는 일련의 작업을 수행하는 패턴이다. 풀에 이미 객체가 존재하는 경우 풀에서 객체를 요청하고 이를 사용하여 작업을 수행한 후 나중에 요청할 때 재사용할 수 있도록 풀로 객체를 반환한다. 객체를 재사용하는 이유는 객체를 초기화하는 데 비용이 많이 들고 이들을 재사용하면 늘어난 GC 시간과 트레이드 오프 요소를 감안하더라도 더 효율적이기 때문이다.[1] 객체가 여러개 있는 경우 이 패턴이 의미가 있다. 하나의 객체만 존재할 수 있거나 무제한 객체를 생성할 수 있는 경우라면 풀패턴을 사용할 필요가 없다. 


어쨋든, 오브젝트 풀 패턴은 성능 향상을 위해 필요하다. 


오브젝트풀 패턴 사용 예시

- 데이터베이스 커넥션 풀

- Thread Pool


데이터베이스에 접속하는 여러 객체를 만들 때 항상 새로 생성하는 것보다는, 미리 생성된 풀에서 꺼내서 사용하는 것이 효율적일 것이다. 즉, 데이터베이스 연결은 재사용해서 사용하는 것과 비교해서, 신규 생성 또는 파기하는 비용이 훨씬 더 많이 드는 경우이다. 


스레드풀은 동시성을 달성하기 위한 패턴으로, 스레드풀은 한정된 리소스에서 자원을 재사용하는 방식으로 애플리케이션의 성능을 향상시키는 결정적인 역할을 한다.


풀 제한을 무조건 높게 설정해도 될까? 

소프트웨어의 환경에 맞게, 관리할 객체 풀의 최대값을 적절하게 설정해야 한다. 불필요하게 너무 높은 설정은 (성능 개선이 아닌) 메모리만 차지하고, 불필요한 리소스 소모가 될 수 있다. 즉, 오브젝트 풀 패턴이 오히려 성능을 저하시킬 수도 있다. 



DBCP


흔히 말하는 DBCP 는 데이터베이스 커넥션 풀을 뜻한다. 오브젝트풀의 대표적인 예가 바로 커넥션풀 이다. 일반적으로 DB 커넥션풀은 미리 커넥션 객체를 생성하고 해당 커넥션 객체를 관리한다. 커넥션풀은 풀 속에 커넥션이 생성되어 있기 대문에 커넥션을 생성하는 비용을 줄일 수 있고, 커넥션을 재사용하기 때문에 생성되는 커넥션 수가 많지 않다. 데이터베이스 연결 시 드는 비용보다, 재사용하는 비용이 저렴하기 때문에 이미 만들어놨던 커넥션을 풀에서 관리하는 객체를 사용하는 것이다. 


만약 서비스가 커지면서 데이터베이스 커넥션이 끊기는 문제가 발생한다면, 커넥션 풀 설정이 잘 되어있는지 확인을 해보자. 로그에서도 커넥션이 끊겼다는 로그를 확인할 수 있을 것이다. 최고의 성능을 유지할 수 있도록 DB 연동 로직은 지속적인 관심이 필요하다. 다른 업무로 신경을 쓰지 못하는 사이에, 갑자기 DB 커넥션 문제가 발생할 수도 있고, 해당 문제는 시스템에 심각한 장애를 유발시킬 수 있다. 참고로, 스프링부트에서는 디폴트로 HikariCP 를 사용한다. 


참고할만한 글

https://github.com/brettwooldridge/HikariCP

https://techblog.woowahan.com/2664/

스프링부트의 AutoConfiguration 에 의해서 커넥션풀이 기본으로 설정될 것이다. 시스템 성능에 맞게 최대 커넥션 풀 설정을 조정해야 한다. 


Tomcat Thread Pool


JVM 환경에서 가장 많이 사용하는 WAS 서버는 톰캣일 것이다. 스프링 부트 환경에서는 임베디드 톰캣을 주로 사용한다. 이때, 다수의 개발자는 톰캣 설정을 하지 않는 경우를 종종 볼 수 있다. 물론, 따로 설정하지 않아도 디폴트 값으로 동작을 하겠지만, 비즈니스 요구사항 및 예상 트래픽을 검토 후 반드시 Pool 설정을 해야 한다. 

디폴트가 200 이라서, 별도로 설정하지 않는다면 기본값인 200 으로 셋팅이 될 것이다. 더 높은 또는 낮은 값을 원한다면 해당 값을 변경하면 된다. 만약 톰캣 스레드풀 설정을 200으로 한 상황에서, 동시에 사용하는 톰캣 쓰레드 200 개가 전부 사용중이라면 어떻게 될까? 신규 요청은 정상적으로 처리가 되지 않을 것이다. 톰캣 스레드풀을 늘리거나, 서버 인스턴스를 늘리거나, 하나의 Thread 가 처리되는 속도를 개선하거나, WebFlux 등으로 개선하거나 등등 여러가지 방법으로 개선할 수 있다. 사실, 스프링부트의 장점이면서 단점인 AutoConfiguration 에 대해서 반드시 이해를 해야 한다. 스프링부트는 애플리케이션 실행 시 자동으로 컨피그 설정을 해준다. DBCP 에서도 언급했지만, AutoConfiguration 에 대해서는 이 글의 주제와 거리가 있지만, 매우 중요한 내용이므로 꼭 알고 넘어가길 바란다. 

https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.server.server.tomcat.threads.max

톰캣 스레드풀이 어떻게 사용되고 있는지 알고 싶다면 어떻게 하면 될까? 각자 사용하고 있는 APM 툴을 모니터링하면 된다. 대부분 APM 툴에서 현재 톰캣 쓰레드 상태를 모니터링 할 수 있다. 또는 쓰레드 덤프를 확인하면 현재 쓰레드가 어떻게 사용되고 있는지 확인할 수 있다. 오류로 인해서 Dead Lock 이 걸려 있는 것을 확인하고 싶다면, 쓰레드 덤프를 확인해서 어디서에 막혀있는지 찾을 수 있다. 


참고로 추가로, 같이 고려해야할 설정은 톰캣 커넥션 최대 카운트이다. 

https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.server.server.tomcat.max-connections

자세한 내용은 생략한다. 


CompletableFuture Common Pool


Java 의 CompletableFuture 는 별도의 셋팅을 하지 않는다면,  ForkJoinPool 의 CommonPool 을 사용하게 된다. 사실, 일반적으로는 commonPool 을 사용하는 방법은 바람직하지 않다. Common Pool 를 사용하지 않고, 별도의 쓰레드풀을 만들어서 동작하도록 작업해야 한다. 자세한 내용은 필자의 예전 글을 참고하길 바란다.

https://brunch.co.kr/@springboot/267


참고로, Java 의 Stream API에서 사용하는 parallelStream 메서드 역시 ForkJoinPool 의 commonpool 를 사용하게 된다. 이 역시, 별도의 커스텀 풀을 사용하도록 변경하는게 좋다. 


다수의 비즈니스 로직에서 CompletableFuture 와 parallelStream 를 별도의 스레드풀 설정없이 무분별하게 사용한다면, CommonPool 의 제한으로 인해서 성능 이슈가 반드시 발생할 것이다.


RestTemplate 


사실 최근에는 RestTemplate 보다는 WebClient 를 많이 사용하고 있어서, RestTemplate 에 대해서는 언급을 안하려고 했지만, 아직 사용하는 곳이 있어서 짧게 소개하겠다. RestTemplate 이란, Spring 3.0 부터 지원하는 Http 통신에 사용하는 템플릿이다. 반본적인 코드를 깔끔하게 정리해주고, Restful 원칙을 지키며 Json, Xml 등 쉽게 요청/응답을 받을 수 있다. RestTemplate 은 Http를 사용하는 범용 라이브러리인, HttpClient 를 추상화해서 제공한다. RestTemplate 의 커넥션 풀 설정이 제대로 되어 있지 않다면, 트래픽이 몰리는 순간에는 기본으로 설정된 RestTempalte Pool 사이즈로 인해서 최대 성능을 끌어내지 못할수 있다. 즉, RestTemplate Pool 로 인해서 애플리케이션 사이에 통신에서 병목현상이 발생할수 있다. RestTemplate 를 기능별로 분리하고, 각각의 역할에 맞는 RestTemplate @Bean을 생성하도록 구성해야 한다. 그리고, 각각의 역할에 맞도록 RestTemplate 커넥션 풀을 설정하여 병목현상을 최소화할 수 있는 최대,최소 수치를 찾아야 한다. 단, 풀 사이즈를 너무 높게 잡으면 오히려 장애를 유발할수 있기 때문에 주의가 필요하다. maxConnTotal, maxConnPerRoute 등의 설정을 유의해서 설정해야 한다. 최상의 Pool 사이즈를 찾기 위해서 커넥션풀 로그를 지속적으로 확인하자. leased  및 pending 수치를 확인하면서 부하가 심한 시간대에 최대로 수용할 수 있는 풀 사이즈가 어느정도인지 지속적인 모니터링을 하면서 수치를 조정해 나가야 한다. 


Spring @Async


스프링의 @Async 를 선언한 메서드가 비동기로 실행될 때 쓰레드는 새로 생성되는데, 별도의 설정을 하지 않는다면 스프링부트에서 만들어서 제공되는 ThreadPoolTaskExecutor 에 의해서 동작한다. 만약, 쓰레드풀을 별도로 관리해서 적용할 수 있을까?  ThreadPoolTaskExecutor @Bean 을 정의하고, 명시적인 이름을 설정해주면 된다. 자세한 내용은 필자의 아래 글을 읽어보길 바란다.

https://brunch.co.kr/@springboot/401


그 외 수많은 Pool 설정


이 글에서 언급하지 않았지만, 수많은 Pool 설정들이 있다. 예를 들어서, Hystrix는 Netflix에서 만든 라이브러리로 마이크로 서비스 아키텍처에서 분산된 서비스 간 통신이 원활하지 않은 경우에 각 서비스가 장애 내성과 지연 내성을 갖게 하도록 도와주는 라이브러리인데, 시스템의 Hystrix 에서도 thread-pool 설정이 매우 중요하다. 


또한... 너무 많은 샘플 사례가 있지만, 더이상 언급은 생략하겠다. 


Thread Pool MaxSize


애플리케이션의 성능에서 가장 중요한 요소는 Thread Pool Size 이고, 최대 성능을 내기 위해서는 Thread Pool Size 를 적절하게 조정해야 한다. Thread Pool 을 너무 작게 설정해도 안되고, 반대로 너무 높게 설정해도 성능에 악영향을 끼칠 수 있다. Pool 사이즈를 설정하는 것은 필자에게는 가장 어려운 기술 중 하나라고 생각한다.  idle Thread 가 너무 많이 발생하면, 프로세스가 제대로 역할을 못하는 경우가 발생하는데, [자바 성능 튜닝 - 스캇오크스 지음] 에서는 쓰레드 최대 개수 설정하는 방법에 대해서 아래와 같이 나와 있다. 


특정 하드웨어에 주어진 작업 부하에 대한 적절한 스레드의 최대 개수는 몇 개인가? 이에 대한 간단한 답은 없다. 작업 부하와 실행되는 하드웨어의 특성에 달려 있다. 특히 최적의 쓰레드 개수는 각 개별 태스크가 얼마나 자주 대기 상태가 되는 가에 달려 있다.  [자바 성능 튜닝 335page - 스캇오크스 지음]



기술부채 개선 사례


DBCP, 쓰레드풀 로 인해서 애플리케이션 성능을 개선했던 필자의 실제 사례를 공유한다. 

https://brunch.co.kr/@springboot/173



대충 글을 마무리하면서


필자가 시간이 없어서 글을 짧게 대충 작성하고 마무리하겠다. 조금이라도 도움이 되었길 바라며.. 소프트웨어 개발을 하면서 각자 커넥션풀, 쓰레드풀 에 대해서 깊은 고민을 해보길 바란다. 훌륭한 백엔드 개발자가 되기 위한 가장 중요한 스킬이 될 것이다. 끝!@

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari