최근에 했던 개발 중, 특정 상품에 대해 필터링을 해야 하는 내용이 있었다.
내용을 간단히 말하면,
현재 이 상품을 사용하려는 사용자가 가진 조건이
이 상품이 요구하는 조건과 일치하지 않는다면 필터링 아웃 시켜야하는 것이었다.
제일 처음 작성했던 코드 스타일은 다음과 같다.
@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로의 리팩토링은
코드의 구조 개선과 유지보수성 강화, 확장 가능성 제고 등 다양한 이점을 가져왔습니다.
이 변경으로 인해 코드는 더욱 모듈화 되었고, 새로운 기능 추가와 유지보수가 훨씬 쉬워졌으며,
코드의 가독성과 유지보수성이 크게 향상되었습니다.
'Programming > Java & Spring 관련 내용 정리' 카테고리의 다른 글
[Java] 인터페이스를 통해 if문의 향연을 고쳐보았다. (0) | 2023.09.11 |
---|---|
[Java] 역직렬화로 JSON 포맷을 바꿔보았다. (0) | 2023.08.21 |
[Spring] 멀티 쓰레드 환경에서 자원이 공유돼 버렸다. (0) | 2023.05.31 |
[Spring] @Async는 왜 제대로 동작하지 못했는가악! (0) | 2023.03.23 |
[Spring] Thread-safe 에 대해서 고찰해 보다. (0) | 2023.03.20 |