본문 바로가기

Programming/Java & Spring 관련 내용 정리

[Java] 인터페이스 사용하여 필터링 기능을 구현해 보았다.

 

최근에 했던 개발 중,  특정 상품에 대해 필터링을 해야 하는 내용이 있었다.

 

내용을 간단히 말하면,

현재 이 상품을 사용하려는 사용자가 가진 조건이

이 상품이 요구하는 조건과 일치하지 않는다면 필터링 아웃 시켜야하는 것이었다.

 

 

 

제일 처음 작성했던 코드 스타일은 다음과 같다.

 

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class CalculatorService {

private boolean filterConditions(CalculatorContext context, long productSeq) {

        return Stream.of(
                isPurposeValid(putData.getPurposeCd(), product.getPurposeCdList()),
                isAssetTypeValid(),
                isMarketValueValid(),
                isGradeValid()
        ).allMatch(Boolean::booleanValue);
    }
    
    
// [목적] 필터링
    private boolean isPurposeValid(String userPurposeCd, List<String> purposeCdList) {
        if (purposeCdList != null && userPurposeCd != null) {
            return purposeCdList.contains(userPurposeCd);
        }

        return true;
    }
    
    
 //   .... 다른 필터링 메소드 생략
 
 }

 

 

 

필터링에서 모두 true를 리턴 받으면, 필터링을 전부 통과한다는 의미로 작성하였다.

 

 

뭔가 마음에 들지 않았다. (굉장히 별로인 코드이시다.....ㅋ)

 

필터링을 해야 할 항목이 많아질 수록 코드가 너무 길어질 것 같았고,

추후에는 정확히 어떤 필터링을 돌다가 실패했는지도 DB에 저장하고 싶었기 때문에 

이 부분까지 추가된다면 필터링 관련 코드는 더 길어질 것 같았다.

 

그럼 어떻게 리팩토링 하는게 좋을까?

인터페이스를 활용하면 조금 더 깔끔할 것 같다는 생각이 들었다.

 

그래서 아래와 같이 인터페이스 기능을 사용하여 리팩토링 해보았다. 

 

 

 


[ 인터페이스 - ProductFilter ]

 

public interface ProductFilter {

    boolean filter(CalculatorContext context, Product product);

    String getMessage();

    default int getPriority() {
        return 100;
    }
    
}

 

 

먼저 ProductFilter 라는 인터페이스를 만들었다.

 

  •  filter() 메소드는 실질적으로 필터링할 내용을 검증하는 코드를 작성하기 위한 메소드이다.

 

  • getMessage()필터링이 실패했을 경우, DB에 메세지 내용을 저장하는데 이때 리턴할 메세지를 구현하고자 했다.

 

  • getPriority() 는 필터링에도 우선순위를 주기 위해 필요한 내용이다.
    • 예를 들어 10과 50이 있을 때, 10이 우선순위가 더 높은 필터링 내용임을 의미한다.

 


 

[ 인터페이스 구현체 (실제 필터링 관련 코드) - FilterByPurposeType ]

 

@Component
public class FilterByPurposeType implements ProductFilter  {

    @Override
    public boolean filter(CalculatorContext context, Prodcut product) {

        List<String> purposeCdList = product.getPurposeCdList();
        String userPurposeCd = context.getUserInputData().getPurposeCd();

        if (purposeCdList != null && userPurposeCd != null) {
            return purposeCdList.contains(userPurposeCd);
        }

        return true;
    }

    @Override
    public String getMessage() {
        return "목적 필터링 실패";
    }

    @Override
    public int getPriority() {
        return 60;
    }
}

 

 

위에서 뼈대가 되는 ProductFilter  인터페이스를 만든 후,

여러 필터링 내용을 구현하기 위해

ProductFilter 를 implements 하는 각각의 필터링 클래스를 만들었다.

 

 

  • 여러 필터링 클래스가 존재하며, 그중에 하나인 위 클래스는 목적을 필터링하는 클래스이며
    • filter() 메소드를 오버라이드 한 후 그 안에 실제 필터링 내용을 작성했다.

 

  • 그다음 실패했을 경우에는 "목적 필터링 실패"라고 메세지를 리턴하기 위해 getMessage() 코드를 작성했다.

 

  • 우선순위 (getPriority())는 60이라고 지정하였다.

 

인터페이스이기 때문에 3개를 모두 오버라이드 해서 구현해 주었다.

 

이런 식으로 필터링 내용을 담은 클래스를 여러 개 생성하면 된다. 


 

[ 필터링을 활용하는 서비스 클래스 - CalculatorService ]

 

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class CalculatorService {

private List<ProductFilter> filterList;

    @PostConstruct
    public void initFilterComponents() {
    
        Map<String, ProductFilter> beans = applicationContext.getBeansOfType(ProductFilter.class);
        
        filterList = beans.values().stream().sorted(Comparator.comparing(ProductFilter::getPriority)).toList();
    }
    
    
    private boolean filterConditions(CalculatorContext context, long productSeq) {

        // 모든 필터를 돌면서, 필터링 결과 확인 및 메시지 저장
        for (ProductFilter filter : filterList) {
            if (!filter.filter(context, product)) {
                context.addFailResultList(productSeq, filter.getMessage());
                return false; // 필터 중 한 개라도 통과하지 못하면 바로 종료
            }
        }

        return true;
    }
}

 

그렇다면, 위에서 만든 터링 인터페이스를 어떻게 활용했을까?

 

 

위 코드를 보면 initFilterComponents() 메소드 안에서

 

1. getBeansOfType(ProductFilter.class)를 통해서

bean 으로 등록했던 클래스를 찾아오는 코드를 작성하였다.

 

 

 

 

어떻게 bean 으로 등록했던 클래스를 찾아올 수 있을까? 

 

 

  • implements 키워드를 사용하여 인터페이스를 구현한 클래스는 해당 인터페이스의 타입으로 간주된다.
  • 그래서 ProductFilter 인터페이스를 구현한 클래스들은 ProductFilter 타입으로 간주된다.

이러한 클래스들이 @Component 어노테이션을 통해 Spring의 bean으로 등록되어 있다면, applicationContext.getBeansOfType(ProductFilter .class) 호출 시 해당 클래스들을 찾아올 수 있다.

 

 

 

 

2. 그다음 아래 코드에서는, 우선순위대로 필터링 순서를 다시 sorting 하고 있다.

 

 

 

 

 

3. 이렇게 해서 찾아온 filterList 를 for 문으로 하나씩 돌면서 필터링을 진행하는 방식이다.

 

 

 


 

이 전 코드(A)와 비교해서, 인터페이스를 사용해서 리팩토링한 코드(B)는 무엇이 개선되었을까?

 

챗지피티에게 물어보았는데,

리팩토링 의도를 정확하고 자세하게 파악해 주었다.

 

 

💻 챗 지피티 왈 


당신이 한 리팩토링은 이전에 있던 코드에서 새로운 구조로 변경하였습니다. 

이 변경으로 인해 여러 가지 이점들이 있습니다.

 


(1) 모듈화와 유연성 증가

B의 코드에서는 필터링 로직이 ProductFilter 인터페이스와 구현 클래스로 분리되어 있습니다.

이로 인해 각각의 필터링 조건을 별도의 클래스로 만들어 관리할 수 있습니다.

이는 코드의 모듈화와 유연성을 증가시켜 새로운 필터를 추가하거나 기존 필터를 수정할 때 더욱 효과적입니다.


(2) 확장 가능성

이 변경을 통해 새로운 필터를 추가하거나 기존 필터를 수정하는 작업이 이전보다 더욱 간편해집니다.

ProductFilter 인터페이스를 구현하여 새로운 필터를 만들고,

필요에 따라 filterList에 추가함으로써 시스템의 확장 가능성이 크게 증가하였습니다.

 

👉  (1)과 (2) 부분이 큰 장점이라고 생각한다.

만약에 필터링을 추가하고 싶으면?

@Component 어노테이션을 붙이고, ProductFilter을 implements 하는 클래스를 하나 만들면

 applicationContext.getBeansOfType(ProductFilter.class); 이렇게 bean 으로 찾아오기 때문에

알아서 필터링 리스트에 추가가 된다.

 

 


(3) 우선순위 제어

각 필터링 조건은 우선순위를 가지며, getPriority() 메서드를 통해 지정됩니다.

이로 인해 필터를 적용하는 순서를 조절할 수 있습니다.

예를 들어, 가장 중요한 필터부터 적용하여 필터링 로직의 실행 흐름을 더욱 정교하게 제어할 수 있습니다.

 

👉  이 부분도 정교한 필터링 기능을 구현함에 있어서 중요하다고 생각한다.

기존 코드에서는 만약 순서를 제어하고 싶으면 직접적으로 코드 순서를 바꿔야 했는데,

인터페이스를 활용해서 좀 더 쉽게 우선순위를 정할 수 있다. (getPriority() 메소드 활용)

 

 


(4) 클래스 분리와 단일 책임 원칙

리팩토링으로 인해 클래스들이 단일 책임 원칙을 준수하도록 분리되었습니다. 

각 필터는 자신의 역할에 집중하며, 코드를 더욱 읽기 쉽고 관리하기 쉽게 만듭니다.

 

(5) 가독성 향상

B의 코드에서 필터링 로직은 반복문을 통해 명확하게 표현되어 있습니다.

각 필터의 역할이 분명하게 드러나며, 개별 필터의 메시지를 저장하여 어떤 필터에서 걸렸는지 빠르게 파악할 수 있습니다.

(6) 유지보수성 강화

B의 코드 구조는 새로운 필터를 추가하거나 기존 필터를 수정하는 경우

관련된 코드만 수정하면 되므로 유지보수성이 향상되었습니다.

또한, 필터 간의 의존성이 낮아져서 변경 시 다른 부분에 영향을 덜 주게 되었습니다.

(7) 개방/폐쇄 원칙 준수

B의 코드에서는 새로운 필터를 추가할 때 기존 코드를 수정하지 않아도 되므로 개방/폐쇄 원칙을 준수합니다. 

이는 기존 코드의 안정성을 유지하면서 새로운 기능을 추가할 수 있음을 의미합니다.

(8) 테스트 용이성

각 필터링 조건을 개별적으로 테스트하기 쉽습니다. 

각 필터링 조건에 대한 테스트 케이스를 작성하여 코드의 정확성을 검증하기 용이합니다.


요약하면, A에서 B로의 리팩토링은 

코드의 구조 개선과 유지보수성 강화, 확장 가능성 제고 등 다양한 이점을 가져왔습니다. 

이 변경으로 인해 코드는 더욱 모듈화 되었고, 새로운 기능 추가와 유지보수가 훨씬 쉬워졌으며, 

코드의 가독성과 유지보수성이 크게 향상되었습니다.