리팩토링으로 기술 부채를 해결하자 (전략 패턴 사용하기)

dasd412·2024년 12월 24일
0

실무 문제 해결

목록 보기
13/18

문제 상황

회사에 일정 주기마다 작동하는 대량 작업 스케쥴러 코드가 있다.해당 코드들은 내가 작성했지만, 시간이 지나면서 유지보수하기 번거로워졌다.

상황은 아래 그림과 같다.
서비스 패키지에는 실제 스케쥴러가 작동하는 로직의 코드를 배치했다. 그리고 컨트롤러 패키지엔 스케쥴러의 강제 작동을 위한 API를 공개하였고, 스케쥴러 패키지엔 주기마다 작동시키는 코드를 배치했다.

그에 따라 스케쥴러 로직이 필요해지면서 서비스에 배치하는 스케쥴러 코드가 난립하게 되었다. 심지어 service라는 이름으로 명명되어 있었다. 이러한 코드는 응집성이 떨어지고 가독성이 떨어진다. 서비스를 보고 있던 다른 개발자가 스케쥴러의 코드까지 읽어봐야 하는 번거로움이 생긴다.


해결

그래서 다음 그림처럼 해결하였다.

서비스레이어에 난립되었던 스케쥴러 작동 코드를 전부 스케쥴러 패키지 내의 scheduler_job_runner.go라는 파일에 배치하였다. 그리고 server.go에서 스케쥴러 잡을 실행시키는 runner 의존성을 주입할 수 있게 하였다.

이에 따라 유지보수하는 사람이 서비스에서 스케쥴러 작동 코드가 있는 것을 보고 헷갈리지 않을 수 있고, 스케쥴러 패키지 안에서 스케쥴러 작동 코드를 찾을 수 있다. 또한 의존성을 주입하기 때문에 객체 자신이 직접 생성에 관여하지 않아 코드가 더 유연해진다. (의존성 주입으로 테스트 코드를 작성하기 쉬워진다.)


코드

scheduler_job_runner.go

type JobRunner interface {
	Execute(ctx context.Context, entClient *ent.Client) (interface{}, error)
}

// 구현체는 생략

모든 스케쥴러 작동 코드는 JobRunner 인터페이스의 Execute(ctx context.Context, entClient *ent.Client) (interface{}, error)를 구현하게 강제했다.

scheduler.go

package scheduler

import (
	"context"
	"github.com/go-faster/errors"
	"github.com/punchylab/server/libs/croncore"
	"time"
)

// 기타 코드 생략

type BaseScheduler struct {
	cron *croncore.CronCore
}

func NewBaseScheduler() *BaseScheduler {
	return &BaseScheduler{
		cron: croncore.NewCronCore(),
	}
}

func (s *BaseScheduler) Start(ctx context.Context) error {
	go func() {
		<-ctx.Done()
		s.Stop()
	}()
	s.cron.Start()
	return nil
}

type Spec int

const (
	Hourly Spec = iota
	Daily
	MidNight
)

type (
	timeSpecParser struct {
		spec Spec
	}

	TimeSpecParser interface {
		ParseSpec() (string, error)
	}
)

func NewTimeSpecParser(spec Spec) TimeSpecParser {
	return &timeSpecParser{
		spec: spec,
	}
}

func (s timeSpecParser) ParseSpec() (string, error) {
	if s.spec == Hourly {
		return "@every 1h", nil
	} else if s.spec == Daily {
		return "@every 24h", nil
	} else if s.spec == MidNight {
		localTimeZone, _ := time.Now().Zone()
		if localTimeZone == "KST" { // KST 자정에 실행
			return "0 0 0 * * *", nil
		} else { // UTC 기준 KST 자정에서 9시간 전인 (즉, UTC 15:00)에 실행
			return "0 0 15 * * *", nil
		}
	} else if s.spec == Minutely {
		return "@every 1m", nil
	} else {
		return "", errors.New("not implemented spec")
	}
}

BaseSchedulercron 라이브러리를 사용하여 스케쥴러를 실제로 작동시킨다.
Spec은 스케쥴러의 작동 주기를 나타내는 Enum이다.
TimeSpecParser는 생성자에 주입된 Speccron라이브러리에서 사용되는 표현식으로 파싱한다.
KSTUTC는 9시간 차이가 남에 유의한다.

ent_scheduler.go

type EntScheduler struct {
	*BaseScheduler
	schedulerJobRunner JobRunner
	entClient          *ent.Client
	parsedTimeSpec     string
}

func NewEntScheduler(
	entClient *ent.Client,
	schedulerJobRunner JobRunner,
	parser TimeSpecParser,
) *EntScheduler {
	parsedSpec, err := parser.ParseSpec()

	if err != nil {
		//예외 처리
		return nil
	}

	return &EntScheduler{
		BaseScheduler:      NewBaseScheduler(),
		schedulerJobRunner: schedulerJobRunner,
		entClient:          entClient,
		parsedTimeSpec:     parsedSpec,
	}
}

func (s *EntScheduler) Start(ctx context.Context) error {
	_, err := s.cron.AddJob(
		s.parsedTimeSpec, func() {
			_, err := s.schedulerJobRunner.Execute(
						ctx,
						client,
					)

			if err != nil {
				//예외처리 
			}
		},
	)

	if err != nil {
		//예외처리 
	}

	return s.BaseScheduler.Start(ctx)
}

EntScheduler의 생성자에 JobRunner 구현체와 TimeSpecParser를 주입할 수 있게 했다. Start 에는 스케쥴러를 작동시키는 공통 코드를 작성하였다.

server.go

func setupSchedulers(
	ctx context.Context,
	entClient *ent.Client,
) (*scheduler.ScheduleManager, error) {
	scheduleManager := scheduler.NewSchedulerManager()

	schedulers := []scheduler.Scheduler{
		scheduler.NewEntScheduler(
			entClient,
			scheduler.NewUpdateGrammarGradeJobRunner(),
			scheduler.NewTimeSpecParser(scheduler.MidNight),
		),
		scheduler.NewEntScheduler(
			entClient,
			scheduler.NewUpdateWordGradeJobRunner(),
			scheduler.NewTimeSpecParser(scheduler.MidNight),
		),
        //이하 생략
	}

	for _, s := range schedulers {
		scheduleManager.AddScheduler(s)
	}

	if err := scheduleManager.StartAll(ctx); err != nil {
		return nil, fmt.Errorf("failed to start schedulers: %v", err)
	}

	return scheduleManager, nil
}

위 코드는 server.go에서 실제 스케쥴러를 생성하는 코드다.
생성자만 봐도, 어떤 일을 하는 스케쥴러이고 언제 작동하는 지 알 수 있다.
그리고 NewEntScheduler()라는 코드 하나에 여러가지 조합 (언제 어떤 일을 할지)을 넣을 수 있게 되서 유연성이 높아졌다.


profile
시스템 아키텍쳐 설계에 관심이 많은 백엔드 개발자입니다. (Go/Python/MSA/graphql/Spring)

0개의 댓글

Powered by GraphCDN, the GraphQL CDN