본문 바로가기

개발새발 개발자/기타

휴면 처리를 하시겠읍니까, 휴먼? - 1

어느 서비스든 유령 회원은 존재한다. 특히 내가 일하고 있는 교육 서비스라면 더더욱 그럴 것이다. 뭐라도 하나 배워볼까? 하고 들어왔던 유저들이 작심삼일 후 우리의 존재를 잊어버리기 일쑤이기 때문이다.

 

'고갱님, 그러지 말고 다시 한번 들어와서 뭐라도 배워보세요' 하고 마케팅 메일을 보내는 순간, '써본 적도 없는(?,,,잘 생각해보세요,,,우리 좋았잖아요,,,ㅠ) 서비스에서 내 개인정보를 가져다 쓰냐' 며 항의 CS가 빗발친다.

 

사실 장기 미접속자는 법적으로도 꽤나 민감한 이슈다. 장기 미접속자 정보를 분리 보관, 파기하지 않을 경우 과태료를 물게 되며, 개인정보가 유출되면 회사 이미지 훼손은 물론 과징금도 물게 된다. 

중요한 건 꺾일 거지만 하는 마음

문제는...관련 법이 올해 9월에 폐지 예정이라는 것이다. 프로젝트를 넘겨받은 시점은 4월. 빨리 개발하지 않으면 얼마 써보지도 못하고 사라질 위기에 처했다!

 

개발적으로 풀어야 할 요구 사항은 크게 3가지다.

  • 유저는 휴면 처리되기 60일 전 이메일, 7일 전 카카오톡 알림을 받는다.
  • 1년(365일) 장기 미접속한 경우 휴면 계정으로 전환한다.
  • 휴면 처리 된 유저는 개인 정보를 분리하여 보관한다.

요구 사항을 만족시키기 위해 배치 애플리케이션으로 개발하기로 결정했다.

  • 대용량의 데이터를 정해진 스케줄대로 자동으로 처리할 수 있고
  • 같은 데이터를 중복으로 처리하는 실수를 방지할 수 있으며
  • 만약 실패한 경우 실패한 부분부터 다시 실행할 수 있기 때문이다.

기존 유저 서비스는 Spring Boot로 되어있었고, 최대한 빠르게 개발하기 위해 Spring의 장점을 그대로 활용할 수 있는 Spring Batch를 도입했다.

유저 서비스 구조

user-service
ㄴ user-batch          (batch module)
ㄴ user-service        (http module)
ㄴ user-common         (domain module)
ㄴ user-infrastructure (infrastructure module)

유저 서비스는 멀티 모듈로 구성되어 있다. 

user-batch

배치 애플리케이션을 위해 사용하는 특수한 용도의 모듈이다.

user-service

http 요청, 응답을 처리하는 모듈이다. user-common으로 비즈니스 로직을 처리하도록 작업을 위임한다.

user-common

비즈니스 로직을 처리하는 모듈이다.

user-infrastructure

database, 서드파티 등 인프라스트럭처 의존성을 모아둔 모듈이다.

Spring Batch 용어

Batch Stereotypes

Spring Batch 애플리케이션이 실행되면 JobLauncher를 실행하면서 Job이 실행된다. 배치와 관련된 모든 메타데이터는 JobRepository라는 곳에서 알아서 관리해 준다.

Job

내가 실행하려는 대용량 처리 묶음 하나라고 생각하면 된다. Job은 1개 이상의 Step으로 구성되어 있다.

Step

Step은 말 그대로 Job의 각 실행 단계를 의미한다. ItemReader, ItemProcessor, ItemWriter로 구성된다.

ItemReader

배치 대상을 조회한다.

ItemProcessor (Optional)

조회한 값을 writer로 넘기기 전에 필요한 비즈니스 로직을 처리한다.

ItemWriter

조회한 값을 처리하고 결과를 반환한다.


기본적인 배경 지식은 알아봤으니 이제 본격적으로 개발에 들어가 보자!

휴면 처리 N일 전 알림 전송

알림 전송 프로세스는 아래와 같다.

  1. 매일 10시 알림 전송 Job 실행
  2. user-common 모듈에서 휴면 유저 조회
  3. user-infrastructre 모듈에서 DB에서 조회한 휴면 유저 반환
  4. user-batch 모듈에 휴면 유저 리스트 반환
  5. user-infrastrcture 모듈에서 Notification 서비스에 알림 전송 요청

1. Spring Batch 설정

먼저, 실행할 Job을 정의해 보자. (전체적인 설정 및 코드는 이미 구글링 해보면 잘 나와있기 때문에 주요 코드만 설명한다.)

 

    @Bean
    public Job Job() {
    	// 혹시 모를 장애를 대비해 피처 플래그를 설정했다.
        if (!featureFlag.isOn(FEATURE_FLAG_KEY)) {
            logger.info("The feature flag is off.");
            return null;
        }

        return jobBuilderFactory.get(JOB_NAME)
                .preventRestart()
                .listener(executionLoggingListener)
                .incrementer(incrementer())
                .start(step())
                .build();
    }

    @Bean
    @JobScope
    public Step Step() {
        return stepBuilderFactory.get(JOB_STEP_NAME)
                .<InputObject, OutputObject>chunk(100)
                .reader(reader())
                .processor(processor())
                .writer(writer())
                .listener(executionLoggingListener)
                .listener(chunkLoggingListener)
                .build();
    }

BuilderFactory

return jobBuilderFactory.get(JOB_NAME)
                ...
                .build();

Spring Batch는 BuilderFactory를 이용해 간단하게 Job, Step을 정의할 수 있다.

Chunk

.<InputObject, OutputObject>chunk(100)

한꺼번에 많은 유저를 처리하면 서비스에 장애가 발생할 수 있기 때문에 chunk로 나눠서 관리하기로 했다. 

출처: https://jojoldu.tistory.com/331

chunk를 설정하면 기본적으로 chunk 단위로 트랜잭션을 관리한다. 즉, 한 chunk 한 만큼 read 한게 쌓이면 write(commit) 한다.

 

chunk 값은 사내 인프라 분석 툴에서 유저 서비스가 1분당 안정적으로 write-read 하는 평균값을 구한 뒤, 배치가 돌 때 기존에 있는 서버들의 latency 등에 영향을 끼치지 않도록 최대한 보수적으로 결정했다.

Listener

.listener(executionLoggingListener)

필요하다면 listener를 구현해 Job이나 Step이 실행될 때마다 특정 기능을 같이 실행할 수 있다. 내 경우는 로깅 리스너를 따로 만들어 배치 각 단계의 정보를 슬랙에 전송하도록 했다.

 

@Component
public class ExecutionLoggingListener implements JobExecutionListener, StepExecutionListener {
	...
}

 

배치 각 단계의 메타데이터를 슬랙에 기록했다.

Spring Batch에서는 다양한 리스너를 제공하고 있으니 자유롭게 구현하면 된다. 이때 주의할 점은 Job에 관련된 리스너는 Job 정의 FactoryBuilder에, Step에 관련된 리스너는 Step 정의 FactoryBuilder에 각각 넣어줘야 동작한다는 것이다.

 

    @Bean
    public Job Job() {
   		...
        return jobBuilderFactory.get(JOB_NAME)
                .listener(executionLoggingListener)
                .build();
    }

    @Bean
    @JobScope
    public Step Step() {
    	...
        return stepBuilderFactory.get(JOB_STEP_NAME)
                .listener(executionLoggingListener)
                .build();
    }

맨 처음 보여준 정의 코드에서 똑같은 executionLoggingListener가 Job과 Step에 중복으로 들어간 이유가 바로 이 때문이다.

Incrementer

.incrementer(incrementer())

사내에 Spring Batch가 도입되지 않았던 시절, 같은 배치가 하루에 여러 번 돌아 알림 폭탄으로 회사가 뒤집힌 적이 있었다. 이런 끔찍한 사고를 막기 위해 Spring Batch는 기본적으로 같은 파라미터가 input으로 들어오면 동작하지 않도록 되어있다.

 

이 파라미터를 정의해 주는 게 incrementer다. 기본적으로 제공하는 RunIdIncrementer를 사용하면 한 번씩 실행할 때마다 버전 값을 자동으로 1만큼 증가시킨다. 하지만 우리는 하루에 한 번씩만 돌아야 하니까 날짜값을 받는 게 좋겠다.

 

public class DailyIncrementer implements JobParametersIncrementer {

    @Override
    public JobParameters getNext(JobParameters parameters) {
        Map<String, JobParameter> beforeParam = parameters == null ? new HashMap<>() : parameters.getParameters();
        Map<String, JobParameter> params = new HashMap<>(beforeParam);

        LocalDateTime now = LocalDateTime.now();
        String startTime = now.format(DateTimeFormatter.ISO_DATE);

        params.put("createDate", new JobParameter(startTime));
        return new JobParameters(params);
    }
}

 

이렇게 오늘 하루의 date 값으로 파라미터를 넘기면 동일한 날짜에는 2번 이상 실행할 수 없게 된다.

2. N일 전 알림 대상 유저 선별

요구 사항에서는 60일 전 이메일, 7일 전 카카오톡 알림을 받는다고 했지만 이 일자는 언제든 변경될 수 있다. 또한, N값을 제외하고는 대상 유저를 선별하고 알림을 보내는 과정까지 모든 로직이 같기 때문에 중복 로직이 생길 수 있다. 모든 비즈니스 로직에 이런 코드가 들어간다면 유지 보수를 하는 미래의 나에게 큰 고통을 주게 될 것임이 분명했다.

 

그래서 적용한 것이 팩토리 매서드 패턴이다.

기준 일자 계산 로직

일단, 알림을 보내거나 휴면 처리를 하는 과정에서 유저를 선별하는 구체적인 내용은 기준 일자 factory로 위임했다. 

 

기준 일자 factory는 현재 처리할 기준이 이메일 알림인지, 카카오톡 알림인지에 따라 각자 다른 생성기를 생성한다. 해당 생성기 구현체기준 일자 클래스를 생성한다. 기준 일자 클래스는 해당 기준에 맞는 일자를 계산해서 반환한다.

 

예를 들어, 지금 휴면 처리 60일 전 이메일 알림을 처리하고 있고 오늘이 2023년 5월 1일이라면, 2022년 6월 30일을 반환한다. 휴면 처리 60일 전이라는 건 현재로부터 접속한 지 305일이 지났다는 뜻이기 때문이다. 참고로, 올해 9월에 폐지될 법이기 때문에 윤년 등의 고려는 하지 않았다.

 

기준 일자 생성기는 인터페이스를 상속받도록 했기 때문에, 실제 사용하는 코드와 생성하는 코드를 분리할 수 있다. 그러면 생성하는 코드를 다른 코드와 독립적으로 확장할 수 있게 된다. 이제, 비즈니스 로직은 뭐가 됐든 신경 쓰지 않고 본인의 임무인 알림 전송이나 휴면 처리를 하면 된다.

 

 

다음 장에서는 실제 휴면 전환을 처리하면서 Spring Batch를 어떻게 활용했는지 설명하겠다.

https://makemethink.tistory.com/195