들어가며
개인적인 생각으로 디자인 패턴은 뭔가 코드를 공통화하고 추상화가 필요할 때 필요한 것 같다는 생각이 든다.
현재 내가 겪고 실천해본 입장에서는 일단 공통 코드를 추상화하기 위해서 위 패턴을 적용해 보았다.
실무에서 비즈니스 로직을 만들다보면 'OOP' 랑은 뭔가 거리가 먼? 코드를 작성하고 있다는 생각이 든다.
과연 OOP 는 무엇일까? 개념적인 부분은 알고있다.
하지만 비즈니스에 내가 어떻게 적용을 하고 있고 어떻게 더 개선할 수 있을지에 대한 고민을 하고는 한다. 그리고 그 OOP 의 첫 걸음은 디자인 패턴을 조금씩 알면서 눈에 객체지향이 조금씩 눈에 들어오고 보이기 시작했다.
내가 겪은 상황은 이렇다.
거래 데이터를 집계 해야하는 상황이다. 그리고 비즈니스 로직을 짜기전에 나는 항상 텍스트 및 그림으로 플로우를 그려본 다음에
좀 간단하다 싶으면 'TDD' 를 적용해보면서 코드를 짜고 좀 복잡하다 싶으면 코드를 짜면서 단위 테스트를 진행하는 편이다.
그냥 1번 거래 데이터를 뽑아 오는거면 금방 로직을 짤 수 있지만 아쉽게도 그건 아니였다
- 1시간에 1번 데이터를 집계한다.
- 하루에 1번 전날 데이터를 집계한다.
- 일주일에 1번 일주일 데이터를 집계한다.
- 한달에 1번 한달 데이터를 집계한다
- 1년에 한번 1년 데이터를 집계한다.
조건은 위 5가지 정도였다.
여러분이라면 위 요구사항을 받았을 때 처음 머리에 떠오르는게 무엇인가요?
저는 처음 요구사항을 보았을 때 어떻게 '추상화' 를 할지에 대한 고민을 했습니다.
뭔가 로직을 5개 메소드 다 만들지 않고 뭔가 분리해서 어떻게 어떻게 하면 유지보수 하고 가독성 좋은 코드가 나올 것 같다는 생각이 들었습니다.
그래서 구글링 및 인생의 동반자(Chat gpt) 와 고민을 한 결과
'전략 패턴' 을 도입하여 코드를 추상화 해 보기로 결정을 하였습니다.
간단하게 전략 패턴 개념 정도만 설명하고 바로 본론으로 들어가 보겠습니다.
전략 패턴은 행동들의 객체들을 객체들로 변환하며 이들이 원래 콘텍스트 객체 내에서 상호 교환이 가능하게 만드는 행동 디자인 패턴입니다
위 내용이 전략 패턴을 대략적인 개념 입니다.
디자인 패턴을 아는 사람들은 '전략 패턴' 은 알고리즘과 관련된 뭔가를 할 때 사용한다고 알고 있을 것이고
왜? 전략 패턴을 도입했는지 의문을 가질 수 있다고 생각합니다.
제가 실무에서 전략 패턴을 도입하게된 history 는 전략 패턴 적용시 아래의 장점 때문입니다.
- 전략 패턴은 일부 행동을 실행하는 방식에서만 차이가 있는 유사한 클래스들이 많은 경우에 사용
- 객체 내에서 한 알고리즘의 다양한 변형들을 사용하고 싶을 때, 런타임 중에 한 알고리즘에서 다른 알고리즘으로 전환하고 싶을 때 사용한다.
위 2가지 이유가 저한테 가장 와 닿았습니다.
저는 요구사항을 구현하기 위해 Spring Scheduler 를 사용했기에 집계 데이터를 배치를 통해서 DB 에 Insert 하고
집계된 데이터를Select 하여 화면에 보여주는게 메인 비즈니스 였습니다.
그리고 5개의 요구사항이지만 사소한 차이만 존재하고 나머지는 공통된 기능을 구현하기에 때문에 '전략 패턴' 에 적합하다고 생각했습니다.
본론
아래에 나오는 코드들은 전략 패턴을 적용했던 간단한 코드들과 비슷한 예시 입니다.
먼저 공통 로직을 추상화할 인터페이스를 선언했다.
public interface DashBoardRepositoryInterface {
void save();
}
추가적인 이야기로는 스프링부트에서 인터페이스는 인스턴스를 만들수 없기에 인터페이스를 스프링 컨텍스트에 등록을 하는 행위는 의미가 없다. 추가적으로 @Transactional 또한 의미가 없다.
그리고 인터페이스를 구현할 클래스를 3가지 구현 했다.
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DailyDashBoardRepositoryImpl implements DashBoardRepositoryInterface {
private final JdbcTemplate jdbcTemplate;
@Override
public void save() {
// save 시킬 비즈니스 로직 대충
jdbcTemplate.update()
}
}
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MonthlyDashBoardRepositoryImpl implements DashBoardRepositoryInterface {
@Override
public void save() {
// save 시킬 비즈니스 로직 대충
}
}
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class YearlyDashBoardRepositoryImpl implements DashBoardRepositoryInterface {
private final JdbcTemplate jdbcTemplate;
@Override
public void save() {
// save 시킬 비즈니스 로직 대충
}
}
위와 같은 형식이 나왔고 위 구현체를 스케쥴링을 돌려야 했다
그리고 스케쥴링을 돌리기 위해선 Bean 에 등록된 위 객체를 직접 선언하여 다 주입 을 해줘야 하는 상황이였다.
뭔가 위 방식은 적절하지 않다는 생각에, 위 주입을 대신 해줄 객체를 선언했다.
위부터가 이제 전략 패턴을 시작이다.
@RequiredArgsConstructor
@Service
public class DashBoardTransactionStrategyService {
private final List<DashBoardRepositoryInterface> repositoryInterfaces;
public void executeSaveStrategy(String batchType) {
for(DashBoardRepositoryInterface repository : repositoryInterfaces) {
if(isDailySchedulingStrategy(batchType, repository)) {
repository.save();
}
if(isMonthlySchedulingStrategy(batchType, repository)) {
repository.save();
}
if(isYearlySchedulingStrategy(batchType, repository)) {
repository.save();
}
}
}
private boolean isYearlySchedulingStrategy(String batchType, DashBoardRepositoryInterface repository) {
return repository instanceof YearlyDashBoardRepositoryImpl && batchType.equals(
SchedulerStatus.YEARLY.getType());
}
private boolean isMonthlySchedulingStrategy(String batchType, DashBoardRepositoryInterface repository) {
return repository instanceof MonthlyDashBoardRepositoryImpl && batchType.equals(
SchedulerStatus.MONTHLY.getType());
}
private boolean isDailySchedulingStrategy(String batchType, DashBoardRepositoryInterface repository) {
return repository instanceof DailyDashBoardRepositoryImpl && batchType.equals(SchedulerStatus.DAILY.getType());
}
}
일단은 List 로 객체를 받아서 주입시키는 방식을 선택했다.
instanceOf 를 통해 같은 객체인지 체크를 시켰다.
위와 같은 방식으로 StrategyService 를 만들고 빈을 등록한 후에
위 Service 를 실제 스케쥴링 서비스에 도입할 수 있었다.
@Slf4j
@RequiredArgsConstructor
@Service
public class DashBoardScheduledService {
private final DashBoardTransactionStrategyService dashBoardTransactionStrategyService;
@Scheduled(cron = "40 0 0 * * *")
public void doScheduled() {
log.info("[Daily] 거래집계 스케쥴러 시작");
dashBoardTransactionStrategyService.executeSaveStrategy(SchedulerStatus.DAILY.getType());
log.info("[Daily] 거래집계 스케쥴러 완료");
}
@Scheduled(cron = "40 0 1 * * *")
public void doScheduledMonthly() {
log.info("[Monthly] 거래집계 스케쥴러 시작");
dashBoardTransactionStrategyService.executeSaveStrategy(SchedulerStatus.MONTHLY.getType());
log.info("[Monthly] 거래집계 스케쥴러 완료");
}
@Scheduled(cron = "0 0 1 3 * *")
public void doScheduledYearly() {
log.info("[Yearly] 거래집계 스케쥴러 시작");
dashBoardTransactionStrategyService.executeSaveStrategy(SchedulerStatus.YEARLY.getType());
log.info("[Yearly] 거래집계 스케쥴러 시작");
}
}
위 코드가 최종적인 코드이다.
아 뭔가 조금 더 수정하고 싶은대 아직 생각이 잘 안난다.
위 스케쥴링 또한 뭔가 한개의 로직으로 분기처리를 할 수 있을 것 같긴한다.
ex) switch ~ case ~ yield
위 처럼 코드를 짜면 한개의 메소드에서 분기처리를 할 수 있지만 좀 더 객체지향적인 코드를 짜보고 싶다.
어떻게 해야할지 고민을 해보다가,, 아직 고민 중입니다.. 좀 더 좋은 방법이 생각나신분은 코멘트 한번 주세요
+ 제가 먼저 생각을 해낸다면 나중에 내용을 더 추가하겠습니다.
결론
요즘 따라 개발이 재미가 없었는대 허상속에 있는 OOP 에 1% 정도를 깨달은 것 같아 약간 다시 흥미가 생겼다.
디자인 패턴이 괜히 옛날 30년 전부터 내려다 온게 아니라는 걸 리마인드 하는 시간이였다.
참조
https://refactoring.guru/ko/design-patterns/strategy