메인 페이지 코드 복잡도 개선하기

이재민·2024년 6월 17일
0

트러블슈팅&개선

목록 보기
5/5

스타트업 특성상 소비자 반응을 확인하고자 서비스 앱 메인 페이지의 섹션 및 구성 요소를 자주 변경해야 하는 요구사항이 발생했습니다.
하나의 엔드포인트에서 여러 비즈니스 로직을 조합하여 응답하는 방식으로 구현되어 있었습니다.
이는 코드의 복잡도를 증가시키고 유지보수를 어렵게 만드는 원인이 되었습니다.


문제

아래와 같은 문제들로 인해 앱 메인 코드 로직의 복잡도는 높으며 객체 지향 원칙을 위배하고 있어 저 포함 동료 개발자들은 로직을 수정할 때 불안한 마음을 갖고 있었습니다.

1. 코드 복잡도

700라인에 육박하는 절차지향적 코드가 존재합니다.

대략적인 코드 품질을 확인할 수 있는 CodeMetrics 플러그인 기준으로 저희 앱 메인 페이지의 코드 복잡도는 65 Bloody hell..입니다. 😭

다양한 비즈니스 로직이 하나의 객체에 집중되어 있어 코드가 복잡하고 가독성이 떨어졌습니다.

2. 유지보수 어려움

신규 섹션 추가나 변경 시 기존 코드의 많은 부분을 수정하거나 코드 탐색에 오랜 시간이 걸렸습니다.

또한, 사이드 이펙트는 없는지 불안함이 동반되었습니다.

public class AppMainService {

  for(MainPageItem item : mainPageIteams()) {
      swich(type) {
          case ASection:
              if() {
                  ...
              } else {
                  ...
              }
              break;

           case BSection:
              if() {
                  ...
              } else if() {
                  ... 
              } else if() {
                  ...
              } else {
                  ...
              }
              break;

           ...

      }
   }
}

위 코드를 확인해보시면 AppMainService 객체는 너무 많은 책임을 갖고 있어 특정 섹션 수정, 추가 요구사항이 발생하면 어떠한 파급 효과가 발생할지 예상되지 않을 뿐더라 유지보수, 확장성은 떨어지고 코드의 복잡도는 높아지기 마련입니다.

3. 확장성 제한

새로운 섹션을 추가할 때마다 기존 로직을 수정해야 하므로 확장성이 제한되었습니다.

4. 테스트 어려움

비즈니스 로직이 한 곳에 집중되어 있어 단위 테스트 작성에 어려움을 겪었습니다.


어떻게 구조를 개선할까?

우선 하나의 엔드포인트에서 데이터를 조합하여 클라이언트에게 응답을 제공하는 설계는 변경하지 않았습니다.(성능 개선이 아닌 구조 개선이었기에)

구조를 개선하기 위해서 전략 패턴을 적극적으로 이용하였습니다.

1. 디자인 패턴 적용

특정 타입에 맞는 전략을 선택 후 실행하는전략 패턴을 적용하였습니다.

1-1 디자인 패턴 적용

코드 구조를 개선하면서 유지보수성과 코드 가독성이 높아진 것을 확인할 수 있습니다.
또한, 새로운 요구사항이 발생하여도 SRP 원칙과 OCP 원칙을 준수할 수 있게 되었습니다.

새로운 SectionStrategy 구현체를 추가할 때 기존 코드를 수정할 필요 없이, 단순히 새로운 구현체를 추가하고 DI 컨테이너에 등록하기만 하면 됩니다.
즉, 시스템의 확장성을 크게 향상시킬 수 있게 되었습니다.

public class AppMainService {
	private final List<SectionStrategy<SectionType>> sectionStrategies;

	public SectionResponse retrieveSection() {
	    List<SectionData<?>> sectionResponseList = new ArrayList<>();
    
    	SectionComponent sections = sectionCacheService.getSectionComponents();
        
		for(SectionComponent component : sections) {
        	// 요청 객체 생성
        	SectionRequest request = SectionReqeust.of();
            
            // type에 해당하는 strategy 조회
        	SectionStrategy<SectionType> sectionStrategy = findStrategy(SectionType.valueOfType(request.getType()))
            if(sectionStrategy == UNDEFINED) {
	            return;
            }
            
            // 섹션 조회 및 조합
            sectionStrategy.execute(request, sectionResponseList)
        }
	}
    	
    private SectionStrategy<SectionType> findMyHomeStrategy(MyHomeSectionType type) {
	    return sectionStrategies.stream()
              .filter(myHomeStrategy -> myHomeStrategy.isMatchingRole(type))
              .findFirst()
              .orElse(UNDEFINED);      
    }
}

1-2 개별 전략 알고리즘 객체 생성

개별 전략 알고리즘에서는 공용 인터페이스에 정의된 메소드를 구현하고 있습니다.

  1. isMatchingRole(): 자신의 전략에 해당하는지 확인합니다.
  2. execute(): 비즈니스 로직을 수행합니다.
public class ASectionStrategy implements SectionStrategy<SectionType> {

    private final AService aService;

    @Override
    public boolean isMatchingRole(SectionType type) {
        return ASectionType == type;
    }

    @Override
    public void execute(SectionRequest request, List<SectionData<?>> sectionResponseList) {
        SectionData<AResponse> sectionData = createSectionData(new SectionData(), request);

		// execute business logic
		...

        sectionResponseList.add(sectionData);
    }
    
     @Override
    public <T> MenuSectionData<T> createSectionData(T t, MenuSectionRequest request) {
        return new MenuSectionData<>(request.getMenuSection());
    }
}

1-3 공통 전략 알고리즘 인터페이스 개발

전략 알고리즘 인터페이스에는 함수들이 정의되어있습니다.

  1. 전략 알고리즘 인터페이스에는 모든 알고리즘 객체에서 구현해야 될 함수를 정의하였습니다.
  2. 응답에 필요한 기본 데이터를 생성하는 메소드들이 존재합니다.
    상황에 따라 기본 데이터를 세팅할 수도 있고, supplier를 제공받아 데이터를 세팅할 수도 있기에 createSectionData() 함수를 오버로딩을 통해 구현하였습니다.
  3. 선택적으로 재정의할 수 있는 메소드들이 필요하여 default 메소드를 제공합니다.
public interface MenuSectionStrategy<E1 extends Enum<E1>> {

    boolean isMatchingRole(E1 type);

    void execute(SectionRequest request, List<SectionData<?>> sectionList);

    /**
     * 섹션의 기본 데이터를 생성합니다.
     *
     * @param t
     * @param request
     * @param <T>
     * @return
     */
    <T> SectionData<T> createSectionData(T t, SectionRequest request) {
        return new SectionData<>(request.getSection());
    }

    /**
     * 기본 데이터 생성과, Supplier 로 제공 받은 함수를 수행하여 데이터까지 세팅합니다.
     *
     * @param request
     * @param dataFactory
     * @param <T>
     * @return
     */
    <T> SectionData<T> createSectionData(SectionRequest request, Supplier<T> dataFactory) {
        SectionData<T> sectionData = new SectionData<>(request.getSection());

        T data = dataFactory.get();

        if (data == null || (data instanceof Collection && ((Collection<?>) data).isEmpty())) {
            return sectionData;
        }

        sectionData.setData(data);

        return sectionData;
    }
    
	deafult AMethod() {
    	...
    }
    
    deafult BMethod() {
    	...
    }
}

결론

이번 구조 개선을 통해 절자지향적으로 작성된 하나의 거대한 객체를 전략 패턴을 이용해 OOP 원칙을 준수할 수 있게 되었고, 동료 개발자들은 조금 더 유지보수하기 용이해졌습니다.

profile
문제 해결과 개선 과제를 수행하며 성장을 추구하는 것을 좋아합니다.

0개의 댓글