개인 프로젝트로 SNS 피드 서비스를 개발하면서 팬아웃(포스팅 전송) 기능의 성능을 높이고자 Kotlin의 Coroutine 및 Dispatcher.IO 워커 스레드를 사용하다가 성능 테스트를 통해 OutOfMemoryException을 발견하게 되었고, 이를 해결하고 처리 성능을 높인 경험을 소개하려고 한다. 문제의 발단 일단 팬아웃(포스팅 전송) 기능은 내가 SNS 게시물을 올리면, 나를 팔로우하는 사람에게 피드를 전송하는 기능이다. 그래서 팔로워들은 자신의 피드에서 나의 게시물을 볼 수 있다. 그리고 아키텍처는 다음과 같이 설계했다. Server에서 Kafka로 Feed 이벤트를 발행할 때, 아래와 같은 로직으로 구현을 했다. 우선 execute 메서드 안에서는 CoroutineScope(Dispa..
예전에 Spring DispatcherServlet에서 시작해서 Controller까지 요청이 전달되는 글을 간단히 작성한 적이 있었고, 이를 통해서 Spring 서버에 요청이 들어오면 어떻게 처리되는지 이해할 수 있었다. (https://hyos-dev-log.tistory.com/21) 그러던 중 최근에 DispatcherServlet까지 요청이 어떻게 도달하는지에 대해 궁금증이 생겨 Socket부터 Spring DispatcherServlet까지의 요청 과정을 정리해봤다. 가장 먼저 Socket 통신 과정을 살펴보자 클라이언트 - 서버 프로그램이 실행되면, 커널 영역에서는 통신을 위한 Socket을 생성하는데, 아래와 같은 과정을 거치게 된다. 여기서 서버 프로그램의 과정을 확인해보면 Socket ..
문제 Spring Batch에서 Partitoner를 통해 Step들을 병렬 처리하려고 했었다. 그런데 처음에는 정상적으로 수행이 되다가 어느 시점에 아래와 같은 에러가 발생했다. 병렬 처리를 위한 TaskExecutorPool의 Size는 50으로 설정했었고, DB Connection Pool의 Size는 10으로 설정했었다. 일반적인 상황이라면, 충분히 수행되어야 하는 상황인데도 위와 같은 에러가 발생하게 되었다. 원인 하나의 스레드에서 2개의 DB Connection을 획득하려고 하다 보니 Connection이 부족하게 되었고, 이로 인해 Timout이 발생한 상황이었다. Spring Batch에서 Chunk 지향 처리는 Reader, Processor, Writer라는 3가지 구성 요소를 기반으로..
기존에 운영되고 있던 Api 서버에서 동시성 이슈가 발생이 되었다. 포인트에서 마일리지로 전환하는 과정에서 적립과 차감내역이 정상적으로 반영되지 않던 문제였는데, 해결한 과정을 정리하려고 한다. 문제 포인트에서 마일리지로 전환할 때, 포인트는 차감되고 마일리지는 적립이 된다. 이때, 포인트가 1,000원이 있고 마일리지가 0원인 상태에서 포인트 -> 마일리지 전환에 대한 3번의 요청이 동시에 들어오면, 아래와 같은 결과가 발생하게 되었다. 포인트 차감 포인트 차감의 경우, 950원 -> 900원 -> 850원으로 반영되어야 하는데 3개의 내역이 모두 950원의 내역으로 반영되어 있다. 마일리지 적립 마일리지 적립의 경우, 50원 -> 100원 -> 150원으로 반영되어야 하는데 3개의 내역이 모두 50원..
부모 클래스에 알고리즘의 골격인 템플릿을 정의하고, 일부 변경되는 로직은 자식 클래스에서 정의하는 것이다. 이렇게 하면 자식 클래스가 알고리즘의 전체 구조를 변경하지 않고, 특정 부분만 재정의할 수 있다. 상속과 오버 라이딩을 통한 다형성으로 문제를 해결하는 것 Spring Batch의 AbstractJob 추상 클래스 Spring Batch에 Job 인터페이스를 구현한 AbstractJob 추상 클래스와 그 하위 구현체들은 템플릿 메서드 패턴을 기반으로 구현되어 있다. 위의 구조를 설명하자면, AbstractJob 추상 클래스에는 doExecute() 추상 메서드가 있고, execute() 메서드에서 doExecute() 메서드를 실행한다. 그리고 doExecute() 추상 메서드는 하위 구현체인 Si..
지난 몇 개월 동안 인스타그램 API를 활용해서 사내의 콘텐츠 서비스와 연동하는 작업을 진행했었고, 약 한 달간의 QA를 끝으로 실제 서비스에 배포하게 되었다. 워낙 큰 작업을 진행했었기 때문에 대규모 단위의 배포를 진행했었다. 사전에 꼼꼼하게 점검하고, 놓친 부분이 없는지 다시 점검한 후에 실 서비스에 배포했지만, 불과 2시간 만에 ConnectionAcquireTimeoutError가 발생했다. 문제 배포를 하고 나서 직접 기능들을 사용해 봤을 때는 문제가 없었다. 하지만 2시간 후에 원인을 알 수 없는 에러로 인해 전체 서비스에 장애가 발생하게 되었다. 우선 Error로그를 파악해봤는데, 특정 시점부터 아래와 같은 Error 로그가 지속적으로 발생하게 되었다. 굉장히 당황스러웠고, 뭐부터 해야 할지..
사내 프로젝트를 진행하다가 테스트 코드를 작성하던 도중 두 객체의 값은 같았는데, 다르다는 에러가 계속해서 발생하게 되었다. 그래서 equals()를 재정의 하지 않아서 발생했던 문제라고 인식하게 되었고, equals()와 hashcode()를 재정의해서 문제를 해결했다. 여기서 끝이 아니라 equals()와 hashcode()를 왜 같이 재정의해야 하는지 간단히 정리하려고 한다. equals()는 언제 정의해야 할까?객체를 기준으로 예를 들자면, equals()는 두 개의 객체가 있고, 두 객체의 값의 동등성을 비교할 때 사용한다. hashcode()는 언제 정의해야 할까?equals()만 재정의 한다고 해서 객체의 값이 동등하다고 할 수는 없다. 객체의 값이 동등하다고 하더라도 해시 값은 다를 수 있..
문제 Hibernate 사용 시 Query Plan Cache 객체를 너무 많이 생성하면, OutOfMemoryError가 발생할 수 있다. https://meetup.toast.com/posts/211 DBA와 개발자가 모두 행복해지는 Hibernate의 in_clause_parameter_padding 옵션 : NHN Cloud Meetup Java ORM 기술의 표준 명세인 JPA가 소개된 지 참 오래되었지만, 국내 현실상 대규모 시스템에서 적용되어 사용된 운영 경험이 충분히 쌓이지 않고 공유되지도 않는 것 같습니다. meetup.toast.com 따라서 Hibernate Query Plan Cache 옵션을 설정하여, OutOfMemoryError를 예방할 수 있다. https://medium.c..
Kotlin 언어는 nullable 특성을 가지고 있다. 즉, null 값을 허용하며, 타입도 존재한다. 따라서 null이 될 수 있는 타입을 다룰 때 처리하는 방법을 정리하려고 한다. ?. (안전한 호출 연산자) 호출하려는 값이 null이 아니면, 일반 메서드 호출처럼 작동한다. 만약에 null 인 경우, null 값을 반환한다. fun safetyCallOperation(text: String?) { val result: String? = text?.toUpperCase() // 결과는??? } 위와 같은 코드가 있을 때, text 값이 null이 아니면, toUpperCase() 메서드를 호출하고, null인 경우 그대로 null 값을 반환한다. 따라서 result 변수의 타입은 String?이다...
Spring의 MessageSource를 사용하려고, message.properties 파일에 한글을 정의하고 나서, 테스트 코드를 실행하던 도중 한글 인코딩이 제대로 되지 않아서 발생했던 에러 해결방법을 정리하려고 한다. 상황 MessageSource 단위 테스트를 진행하기 위해서 아래와 같은 코드를 작성했고, messages.properties 파일은 다음과 같이 정의했다. // messages.properties test=테스트 @SpringBootTest public class MessageSourceTest { @Autowired private MessageSource messageSource; @Test void helloMessage() { String result = messageSourc..