본문 바로가기

Programming/Java & Spring 관련 내용 정리

Push Server "개선" 작업을 해보다.

 

얼마 전 회사에서 기존 푸시서버 개선을 진행했다.

 

기존 푸시 서버의 문제점은

인증정보 기반의 타겟 푸시 발송에 국한되어 있는 포맷이라

서비스성 및 이벤트성 메세지 발송을 하기에는 비효율적이었다.

 

(ex) 보내려는 서비스성 푸시메세지가 기존 시스템의 포맷과 맞지 않아

새로운 DB 테이블을 생성하거나

새로운 객체에 맞게 발송 프로그램을 따로 구현하고 테스트 해야 했음

 

한 마디로 확장성과 유연성이 떨어지는 구조였다.

 

 

이를 해결하기 위해 인증 기반 푸시 메세지 뿐 아니라

다양한 서비스 유형 푸시 메세지를 발송할 수 있도록 DB와 코드를 재설계했다.

 

개선 전에는 새로운 유형의 푸시를 보낼 때

기존 포맷에 맞춰 추가 개발하고 테스트하는데 하루 정도 시간이 소요되었으나

개선된 시스템에서는 스펙만 정해지면 대상 쿼리, 발송 시점, 메세지 내용 등을 간편하게 설정할 수 있어

작업시간이 약 30분 이내로 단축되었다.

 

 


[ 다형성 활용 ]

 

< 변경 전 > 

 

 

 

 

변경전 코드의 문제점을 요약해서 설명하자면

- 나열식 코드가 많아서 메소드 하나에 코드 내용이 너무 길었다. 

- 통신 3사마다 요청 포맷이 다 다른데, 구조화된 느낌이 거의 없었다. (통신사 별 포맷이 다른 것도 case 분기 처리)

- 한 클래스 내에 코드가 죄 나열되어 있는 느낌이 강했다.

 

 

 

< 변경 후 >   

 

 

 

 

변경 후 코드는 Abstract class를 사용해서 구조화 시켰다.

 

 위 그림에서 Dog에 해당하는 것이

아래 AbstractNotificationSender 클래스이다.

 

그리고 하위 클래스 3개가 아래와 같다.

- SktNotificationSender

- KtNotificationSender

- LguNotificationSender

 

 

 

개선된 이유

- 다형성 지원 : sendNotification과 getMediaSeq와 같은 추상 메서드를 통해

서브클래스에서 구체적인 구현을 정의하도록 하여,

다양한 매체에 대해 유연하게 대응할 수 있도록 한다.

 

- 공통 기능 제공 : NotificationDao, StringRedisTemplate, Environment 등

공통적으로 사용되는 의존성들을 한 곳에서 관리하고,

getNeedAgreeCdList()와 같은 공통 메서드를 제공하여 코드 중복을 줄인다.

 

 

이렇게 추상 클래스를 사용하면

공통 기능과 상태를 중앙에서 관리하고, 다양한 매체에 대해 유연하게 확장할 수 있다는 장점이 있다.

 

즉 사용자 정상 여부 체킹, 사용자 동의 여부 체킹, 푸시 수량 제한 체킹은 모두 공통적으로 이루어지기 때문에

이 AbstractNotificationSender 에 모두 코드를 작성하고,

실제로 통신사 포맷에 맞춰 API를 호출하고 응답을 받는 과정과 같이 다르게 작성되어야 하는 부분만

extends 하고 있는 서브클래스에서 구현해주면 되는 것이다.

 

그리고 이렇게 구조화가 되어 나뉘어져 있기 때문에

각 통신사마다 다른 부분을 구현하는 코드가 한 클래스 내에 너무 방대하게 있지 않다는 장점이 있고,

통신사 별로 수정이 필요할 때도 빠르게 찾을 수 있어서 좋다.

 

 

 


[ Redis 를 활용한 푸시 수량 체크 ]

 

 

푸시가 하루에 너무 여러 번 발송되면 사용자는 피로감을 많이 느낀다.

그렇기 때문에 푸시 수량 제한 기능이 필요한데, 이 때 Redis를 적극 활용했다.

 

일단 통신사별로 푸시 수량 개수가 다를 수 있기 때문에

제한 숫자는 DB에 저장해 두었다.

 

Redis를 활용한 부분은 사용자별로 오늘 푸시 보낸 수량을 체킹하는 부분이다.

 

 

< 변경 후 > 

 

 

 

 

 

사용자의 custSeq, 푸시 카테고리, 날짜 등을 조합하여 Redis 키를 생성한다.

이 키를 사용해 Redis에서 값을 조회하고, 오늘 보낼 수 있는 푸시 수량을 초과했는지 확인하는 것이다.

또한 오늘 자정부터 다음 날 자정까지의 기간 동안 dailyLimitKey가 유효하도록 만료시간을 설정할 수 있다.

 

개선된 이유

- 효율적인 푸시 수량 관리:

사용자의 custSeq, 푸시 카테고리, 날짜 등을 조합한 Redis 키를 사용함으로써

개별 사용자와 푸시 유형에 따른 세밀한 관리를 할 수 있다.

Redis의 메모리 기반 저장소 덕분에 높은 속도와 성능을 보장한다.

 

- 자동 만료 지원: Redis의 TTL(Time-to-Live) 기능을 활용하여,

푸시 수량 체크와 관련된 데이터를 자동으로 관리하고 만료시킬 수 있다.

이로 인해 매일 초기화가 필요하지 않으며, 관리 작업을 줄일 수 있다.

 

데이터 일관성 및 정확성: Redis를 통해 푸시 수량을 관리함으로써

데이터베이스의 복잡한 트랜잭션과 동시성 문제를 피하고, 푸시 수량의 일관성과 정확성을 유지할 수 있다.

 

 

 


[ 스케쥴링 방식 변경 ]

 

< 변경 전 > 

 

 

 

< 변경 후 > 

 

 

개선된 이유

- 변경 전: 예를 들어 DB를 읽고 푸시를 보내는 작업이 2분 12초가 걸렸다면,

다음 호출까지 48초동안 대기를 하고 있다. cron은 1분단위로만 설정이 가능하기 때문이다.

 

- 변경 후: 작업이 완료된 후 1초 대기하고 다음 작업을 시작한다.

이렇게 하면 대기 시간을 줄이고 작업이 더 지속적으로 이루어진다. 

 

대량의 푸시를 보내는 경우에는

위와 같은 예시 상황에서 48초 대기시간이 상당히 비효율적이다. 

변경된 코드 내용으로는 대기시간을 최소화 할 수 있다. (1초 정도는 서버의 안정성을 위해 쉬어주는 느낌!)

 

 

 

현재의 스케줄링 방식은 서버 1대일 때 최적의 성능을 발휘하도록 설계되었다.

그러나 향후 트래픽 증가나 비즈니스 요구사항의 변화에 대비하여

다음과 같은 확장 방안을 고려해 볼 수 있을 것 같다.

 

- 비동기 처리 및 메시지 큐 도입

여러 서버가 병렬로 작업을 처리할 수 있도록 비동기 처리 방식을 도입하고,

메시지 큐 시스템(예: RabbitMQ, Kafka 등)을 활용하여

각 서버가 독립적으로 작업을 분담할 수 있도록 설계하는 방법을 생각해보고 있다.

 


[ DB 구조 변경 ]

 

< 기존 DB 구조 > 

 

보내야할 푸시와 보낸 결과를 모두 하나의 테이블에서 관리했다.

보낸 결과는 기존에 등록된 푸시 status를 업데이트 하는 방식이었다.

 

 

< 변경 후 DB 구조 > 

 

보내야 할 푸시와 푸시 보낸 결과 테이블을 2개로 분리하였다.

 

 

 

 

장점

- 유지보수 용이성:

(1) 푸시 발송 대상 데이터 테이블 (2) 푸시 발송 결과 테이블

이렇게 각 테이블의 역할이 명확해져 데이터 관리와 유지보수가 쉬워진다.

 

- 성능 개선:

두 테이블로 분리하면 각 테이블의 데이터 양이 줄어들어 인덱스 성능과 쿼리 성능이 향상된다.

예를 들어 푸시 발송 결과를 자주 조회해야 하는 경우

불필요한 데이터를 포함하지 않는 테이블에서 조회 속도가 빨라진다.

 

- 스케일링:

데이터가 분리되어 있으면, 향후 데이터베이스 스케일링(예: 파티셔닝)이나 데이터베이스 성능 조정이 더 용이하다.

푸시 요청과 결과를 별도로 관리하여 스케일 아웃이나 분산 처리가 수월해진다.

 

 

 


[ @PostConstruct 와 @Value 활용 ]

 

 

< 변경 전 > 

 

3사 통신사마다 호출해야할 도메인 서버 주소가 달랐다.

기존 방식은 코드가 쭉 진행되다가 통신사로 API를 호출하는 시점에 세팅해주는 방식이었다.

 

 

< 변경 후 > 

 

개선된 이유

각 통신사 서브클래스 init() 메서드에서

@PostConstruct를 활용하여 RestClient를 설정하는 방식은

통신 서비스마다 서로 다른 URL 및 설정을 동적으로 관리할 때 유리하다.

 

1. 동적 설정 적용

- 문제: 외부 서비스마다 통신 URL과 설정이 다를 경우, 각각의 설정을 하드코딩하거나 별도로 관리해야 한다.

 

- 개선: @PostConstruct를 사용하면 클래스가 생성된 후

외부 설정값(예: URL, 타임아웃)을 주입받아 동적으로 RestClient를 설정할 수 있다.

각 서비스의 URL과 설정값을 환경 설정 파일에서 관리할 수 있다.

 

 

2. 환경 기반 설정

- 문제: 각 서비스의 설정이 다를 경우, 다양한 환경(개발, 테스트, 프로덕션)에 맞게 각각 다른 설정을 적용하기 어렵다.

 

- 개선: @Value를 사용하여 환경 설정 파일(application.properties 또는 application.yml)에서

서비스별 URL과 타임아웃을 읽어와 RestClient를 구성한다.

이를 통해 환경에 따라 동적으로 설정을 조정할 수 있어, 코드 변경 없이 설정만으로 다양한 환경을 지원할 수 있다.

 

 

3. 코드 유지보수 용이

- 개선: 초기화 로직을 @PostConstruct 메서드에 집중시킴으로써

설정값이 변경될 경우 환경 설정 파일만 수정하면 되어 코드의 수정 없이 설정을 쉽게 업데이트할 수 있다.

 

 


[ 곳곳에  ENUM 활용하기 ]

 

 

 


< 변경 후 > 

 

푸시 발송 실패는 여러가지 원인이 있을 수 있다.

그렇기 때문에 그 원인이 발생한 시점에 진행을 멈추고, 그 원인을 DB에 저장해 주어야 한다.

 

SendResultCode enum을 활용하여 실패 원인을 한 곳에서 관리할 수 있도록 했다.

 

 

 


[ @DiscriminatorColumn 과 @SQLRestriction 활용 ]

 

cust-info 라는 테이블에서 사용자 정보를 조회해 와야 하는데,

각 통신사마다 추가로 더 필요한 사용자 정보는

cust_dtl_info 라는 테이블에 추가로 저장되어 있는 상황이다.

 

이 때 @DiscriminatorColumn 어노테이션,

@SQLRestriction 어노테이션,

@Inheritance 어노테이션을 활용했다.

 

 

 

< 변경 후 >

 

 

 

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)을 사용하면

하나의 테이블이 여러 자식 엔티티의 데이터를 관리하게 된다.

 

구분자 컬럼(DiscriminatorColumn)은 각 행이 어떤 자식 엔티티와 연결되는지 식별하는 데 사용된다.

예를 들어, MEDIA_SEQ 컬럼을 사용해

어떤 행이 KtCustInfoEntity, LguCustInfoEntity 등과 연결되는지 구분할 수 있다.

 

 

 

 

 

 

@DiscriminatorColumn은 싱글 테이블 전략에서 구분자 역할을 하는 컬럼을 지정한다.

이 컬럼의 값에 따라 각 행이 어떤 엔티티 타입에 해당하는지 구분한다.

 

예를 들어 @DiscriminatorColumn(name = "MEDIA_SEQ")를 사용하면,

MEDIA_SEQ 컬럼의 값에 따라 데이터베이스 레코드가 어느 자식 엔티티의 데이터인지 식별할 수 있다.

 

 

 

 

 

 

각 엔티티에 @SQLRestriction을 사용하여

특정 조건에 맞는 데이터만 해당 엔티티에서 처리되도록 제한할 수 있다.

 

예를 들어 KtCustDtlInfoEntity 클래스에 @SQLRestriction("CUST_DTL_TYPE_CD = 'OO'")를 적용하면,

이 엔티티는 CUST_DTL_TYPE_CD 값이 'OO'인 데이터만을 처리한다.

 

이렇게 하면 단일 테이블 전략을 사용하면서도

각 엔티티가 자신에게 맞는 데이터만 다루도록 할 수 있어

데이터 무결성을 유지하고 관리가 용이해진다.

 

 

 

 

 

 

CustDtlInfoEntity :

여러 엔티티에서 공통으로 사용할 수 있는 필드와 매핑 정보를 정의하는 추상적인 클래스이다.

이 클래스는 자체로는 테이블에 매핑되지 않는다.

 

KtCustDtlInfoEntity :

CustDtlInfoEntity의 필드들을 상속받아 실제로 테이블에 매핑되는 엔티티이다.

이 클래스는 cust_dtl_info 테이블에 매핑되며,

특정 조건(CUST_DTL_TYPE_CD = 'OO')에 해당하는 데이터만 처리한다.

 

 

 


[ 개선 후기 ]

 

오늘은 이렇게 푸시 개선 작업을 위해

새롭게 서버를 구축하고 기존 푸시 발송 코드 부분을 개선했던 내용 중

중요하다고 생각한 부분을 정리해 보았다.

 

백지 상태에서 서버를 처음부터 새롭게 구성해 나가는 과정은 정말 값진 경험이었다.

 

"개선"의 목표는

  1. 기존 코드의 구조화
    • 기존 코드보다 더 구조화된 형태로 재구성하여 유지보수와 확장성을 높이는 것이 목표
  2. DB 테이블 분리
    • 기존에 하나의 테이블에서 관리되던 데이터를 (1) 푸시 발송 대상 테이블과 (2) 푸시 발송 결과 테이블로 분리하여 데이터 관리를 용이하게 하기 위함

 

통신사마다 방식이 다르기 때문에, 구조화를 적용하여 더 효과적인 결과를 얻을 수 있었고

테이블 하나에 발송 대상과 결과를 같이 관리했을 때 생겼던 단점들이 보완되었다.

(ex) 쿼리 성능저하, 부하, 확장성이 떨어짐