Design Pattern

[Design Pattern] 과도한 if 절 타입 분기 코드 개선하기

헝그리개발자 2023. 2. 16. 03:23

이번 포스팅에선 if 절이 과도하게 사용되어 오염된 비즈니스 레이어 결합도를 객체지향 패턴을 통해 낮추는 방법에 대해서 다뤄보려고 한다.

문제 분석

아래 코드는 이전 포스팅에서 다루었던 코드이다.

https://steadycode.tistory.com/89

 

카카오 챗봇 응답 템플릿 작성하기

이번 포스팅에선 ‘채용공고 크롤러’ 사이드 프로젝트를 진행하면서 카카오 챗봇을 구현한 내용을 상세히 기록하고자 한다. 카카오 챗봇 카카오 봇 시스템은 유저로부터 스킬 요청을 받고 스

steadycode.tistory.com

@RestController
public class RecruitController {

    private final RecruitService recruitService;
    private final Map<String, String> companies = Map.of("kakao", "카카오", "naver", "네이버");

    public RecruitController(RecruitService recruitService, ObjectMapper objectMapper) {
        this.recruitService = recruitService;
        this.objectMapper = objectMapper;
    }

    @PostMapping("/api/new-recruits/{company}")  // company : {kakao, naver, all}
    public CommonResponse getNewRecruitsAfterYesterdayByListCard(@PathVariable(name = "company") String company) {
        List<Recruit> findRecruits;
        if(company.equals("all")) {
            findRecruits = recruitService.getAllRecruitsAfterYesterday();
        } else {
            findRecruits = recruitService.getRecruitsByCompanyAfterYesterday(companies.get(company));
        }

        CommonResponse result;
        if(findRecruits.size() != 0) {
            result = ListCardResponse.of(findRecruits);
        } else {
            result = SimpleTextResponse.empty();
        }

        return result;
    }
}

해당 코드의 문제점은 다음과 같다.

  1. 과도한 타입 분기로 인한 높은 결합도
  2. 낮은 유지보수성
  3. 가독성 감소

현재는 company 타입이 “kakao”, “naver”, “all” 밖에 없지만 시스템이 고도화되어서 타입 변수가 1000개로 늘어났다고 가정해본다면 else if 절이 1000줄 추가로 작성되게 될 것이다.

기능이 추가되거나 변경될 때마다 business layer 코드를 건드려야하고 그 변경의 여파가 어디까지 퍼질지 예측할 수 없는 위험이 있다.

해결 방안

결합도를 낮춰서 Controller 의 과도한 책임을 분산시킬 필요가 있어 보인다.

결합도는 캡슐화를 통해 낮출 수 있다.

캡슐화는 객체지향 디자인 패턴의 핵심 원리중 하나로서 내부 구현을 감추는 것을 의미한다.

business layer에선 추상화된 인터페이스에만 의존하도록 하고 해당 인터페이스를 구현하는 구현체들을 생성하여 런타임에 주입되도록 설계한다.

개선된 코드

company 타입을 추상화하는 인터페이스 CompanyCondition을 두고 이를 구현하는 EntireCompany, Kakao, Naver 객체를 생성한다.

그리고 business layer 에선 CompanyCondition에만 의존하고 구현부를 캡슐화한다.

CompanyCondition

public interface CompanyCondition {
    public Boolean isSatisfiedBy(CompanyType companyType);

    List<Recruit> getRecruitsByCondition();
}

EntireCompany

@Component
public class EntireCompany implements CompanyCondition {

    private final RecruitMapper recruitMapper;

    public EntireCompany(RecruitMapper recruitMapper) {
        this.recruitMapper = recruitMapper;
    }

    @Override
    public Boolean isSatisfiedBy(CompanyType companyType) {
        return companyType == CompanyType.ALL;
    }

    @Override
    public List<Recruit> getRecruitsByCondition() {
        LocalDateTime yesterday = LocalDateTime.now().minusDays(1);

        return recruitMapper.findAll().stream()
                .filter(d -> d.getAddDateTime().isAfter(yesterday))
                .collect(Collectors.toList());
    }
}

Kakao

@Component
public class Kakao implements CompanyCondition {

    private final RecruitMapper recruitMapper;

    public Kakao(RecruitMapper recruitMapper) {
        this.recruitMapper = recruitMapper;
    }

    @Override
    public Boolean isSatisfiedBy(CompanyType companyType) {
        return companyType == CompanyType.KAKAO;
    }

    @Override
    public List<Recruit> getRecruitsByCondition() {
        LocalDateTime yesterday = LocalDateTime.now().minusDays(1000);

        List<Recruit> find = recruitMapper.findByCompany(CompanyType.KAKAO.getValue());
        return find.stream()
                .filter(d -> d.getAddDateTime().isAfter(yesterday))
                .collect(Collectors.toList());
    }
}

Naver

@Component
public class Naver implements CompanyCondition {

    private final RecruitMapper recruitMapper;

    public Naver(RecruitMapper recruitMapper) {
        this.recruitMapper = recruitMapper;
    }

    @Override
    public Boolean isSatisfiedBy(CompanyType companyType) {
        return companyType == CompanyType.NAVER;
    }

    @Override
    public List<Recruit> getRecruitsByCondition() {
        LocalDateTime yesterday = LocalDateTime.now().minusDays(1);

        return recruitMapper.findByCompany(CompanyType.NAVER.getValue()).stream()
                .filter(d -> d.getAddDateTime().isAfter(yesterday))
                .collect(Collectors.toList());
    }
}

RecruitController

복잡한 if 문을 Service 내부로 캡슐화했다.

기존에 Controller에 부여된 과한 책임을 분리했다.

@RestController
@Slf4j
public class RecruitController {

    private final RecruitService recruitService;

    public RecruitController(RecruitService recruitService) {
        this.recruitService = recruitService;
    }

    @PostMapping("/api/new-recruits/{company}")
    public CommonResponse getNewRecruitsAfterYesterdayByListCard(@PathVariable(name = "company") String company) {
        CompanyType companyType = CompanyTypeConverter.from(company);  // string to enum
        List<Recruit> findRecruits = recruitService.getRecruitsByCompany(companyType);
        return parseToChatBotTemplate(findRecruits);
    }

    private CommonResponse parseToChatBotTemplate(List<Recruit> findRecruits) {
        if(findRecruits.size() != 0) {
            return ListCardResponse.of(findRecruits);
        } else {
            return SimpleTextResponse.empty();
        }
    }
}

RecruitService

Collection Injection 을 통해 구현체 리스트를 멤버로 가진다.

Service 객체는 단지 인터페이스의 추상 메서드를 실행할 뿐이고 런타임에 조건에 만족하는 구현체가 동작한다.

@Service
public class RecruitService {

    private final List<CompanyCondition> companyConditions;

    public RecruitService(List<CompanyCondition> companyConditions) {
        this.companyConditions = companyConditions;
    }

    public List<Recruit> getRecruitsByCompany(CompanyType companyType) {
        return companyConditions.stream()
                .filter(companyCondition -> companyCondition.isSatisfiedBy(companyType))
                .findFirst().get().getRecruitsByCondition();
    }
}

이제 기능을 확장하고자 할 때 더이상 service layer를 건드는 위험을 감수하지 않아도 된다.

company 타입을 추가하려면 단순히 CompanyCondition 을 implements하는 구현체만 생성하면 된다.

기능을 추가할 때 인터페이스의 하위 구현체를 생성하는 방식으로 기존 코드에 영향을 주지 않으면서 개발할 수 있는 원칙을 OCP 원칙이라고 한다.

코드 참조

https://github.com/KIM-KYOUNG-OH/ChatBot

정리

객체 지향 디자인 패턴을 이용해서 기존의 코드를 개선해보았다.

대규모 시스템으로 갈수록 코드가 복잡해지고 유지보수가 어려워지기 때문에 이를 위한 해결책으로 객체지향 패턴을 고려해볼 수 있을 것 같다.