개요
웹 서버를 개발하다보면 비슷한 형태를 가지는 코드가 반복되는 경우가 더러 있다.
이번 프로젝트에서는 JPA를 통해 낙관적 락을 간편하게 구현했는데,
업데이트 쿼리 실행을 실패하면 예외(OptiimisticLockingFailureException
)가 발생한다.
즉, 낙관적 락을 사용하는 모든 기능은 동시성 문제로 인해 쿼리 실행 실패 시,
예외 트래킹, 디버깅, 로깅을 위해 위의 예외를 조금 더 상세하게 핸들링하는 코드가 필요하다.
예외를 핸들링하는 중복코드를 템플릿 메서드 패턴을 사용하여 가독성을 높이고, 관리 포인트를 줄여보려 한다.
원인
앞서 언급했듯 낙관적 락 관련 예외를 핸들링하는 코드가 다수의 메소드에 중복 선언되어 있다.
public Product update(Product targetProduct, Long updatedBy) {
try {
ProductEntity productEntity = findById(targetProduct.getId());
Product updatedProduct = productEntity.updateFrom(targetProduct, updatedBy).toDomain();
return productCacheAdapter.save(updatedProduct);
} catch (OptimisticLockingFailureException exception) {
log.error();
throw new ProductUpdateFailureException("다른 사용자가 상품을 수정하고 있음");
}
}
public Product changeProductQuantity() {
try {
// 상품 개수 변경 코드
} catch (OptimisticLockingFailureException exception) {
log.error();
throw new ProductUpdateFailureException("다른 사용자가 상품을 수정하고 있음");
}
}
중복 선언 된 코드는 가독성을 해칠 뿐 아니라 관리 포인트의 증가 또한 도모한다.
따라서 위의 코드에서 중복을 제거해야겠다고 판단했다.
중복되는 코드는 Try-Catch
문의 형식, 변경되는 코드는 Try
문 내부의 실행되는 코드이다.
Try-Catch
문의 형식은 타 기능에서도 사용될 가능성이 많기에 템플릿 메소드로 정의하여 중복을 줄이고 재사용성을 높이려 한다.
템플릿 메소드 패턴
템플릿 메소드 패턴(Template Method Pattern)은 디자인 패턴 중 행위 패턴의 한 종류로 처리의 흐름(알고리즘)을 하나의 탬플릿으로 정의하고, 구체적인 정의는 다른 함수로 위임하는 패턴이다.
Java에서 템플릿 메소드 패턴은 보통 추상 클래스와 상속 혹은 Functional Interface, Lambda로 구현한다.
아래는 추상 클래스와 상속으로 템플릿 메서드 패턴을 구현하는 예시이다.
public abstract class DataProcessor {
// 템플릿 메서드: 전체 프로세스의 흐름 정의
public final void process() {
readData();
processData();
writeData();
}
// 고정된 로직 (변경되지 않는 부분)
protected void readData() {
System.out.println("Reading data...");
}
// 추상 메서드 (변경될 부분)
protected abstract void processData();
// 고정된 로직 (변경되지 않는 부분)
protected void writeData() {
System.out.println("Writing data...");
}
}
// 구체적인 구현 클래스
public class JsonDataProcessor extends DataProcessor {
@Override
protected void processData() {
System.out.println("Processing JSON data...");
}
}
템플릿 메소드 패턴의 장점
- 중복 코드 제거: 공통적인 흐름을 템플릿 메서드에 두고, 변하는 부분만 분리하여 재사용성을 높인다.
- 유지보수 용이: 로직이 한 곳에 집중되어 있어 수정이 쉬움.
- 코드 가독성 향상: 알고리즘의 구조가 명확하게 드러나고, 변하는 부분이 눈에 잘 보인다.
리팩토링 진행
이번 코드에서는 템플릿을 사용하는 범위가 하나의 클래스로 제한되며,
템플릿으로 분리되는 알고리즘이 복잡하지 않기 때문에 Lambda 방식으로 리팩토링을 진행한다.
기존의 코드를 다시 불러와보자.
public Product update(Product targetProduct, Long updatedBy) {
try {
ProductEntity productEntity = findById(targetProduct.getId());
Product updatedProduct = productEntity.updateFrom(targetProduct, updatedBy).toDomain();
return productCacheAdapter.save(updatedProduct);
} catch (OptimisticLockingFailureException exception) {
log.error();
throw new ProductUpdateFailureException("다른 사용자가 상품을 수정하고 있음");
}
}
public Product changeProductQuantity() {
try {
// 상품 개수 변경 코드
} catch (OptimisticLockingFailureException exception) {
log.error();
throw new ProductUpdateFailureException("다른 사용자가 상품을 수정하고 있음");
}
}
우선 중복되는 코드(Try-catch문)를 템플릿 메소드로 분리한다.
구체적인 기능 구현 코드는 Functional Interface를 통해 템플릿 메소드를 사용하는 메소드로 위임한다.
private <T> T handleOptimisticException(Supplier<T> action) {
try {
return action.get();
} catch (OptimisticLockingFailureException e) {
log.warn("Throw OptimisticLockingFailureException =", e);
throw new ProductUpdateFailureException("다른 사용자가 상품을 수정하고 있음");
}
}
기존의 기능을 수행하던 메소드가 템플릿 메소드를 사용할 수 있도록 변경한다.
람다를 통해 기존의 기능을 템플릿 메소드로 넘겨준다.
public Product update(Product targetProduct, Long updatedBy) {
return handleOptimisticException(() -> {
ProductEntity productEntity = findById(targetProduct.getId());
Product updatedProduct = productEntity.updateFrom(targetProduct, updatedBy).toDomain();
return productCacheAdapter.save(updatedProduct);
});
}
public Product changeProductQuantity() {
return handleOptimisticException(() -> {
// 상품 개수 변경 코드
});
}
유의할 점: 람다식 내부에서 사용되는 변수는 final이어야 한다.
Variable used in lambda expression should be final or effectively final
결론
템플릿 메소드 패턴을 활용해 낙관적 락 관련 예외 처리 코드를 리팩토링함으로써 코드의 중복을 제거하고 재사용성을 향상시켰다.
템플릿 적용 범위와 알고리즘의 복잡도를 통해 현재 코드의 상황을 분석하고 상속 방식과 람다 방식중 람다 방식을 선택하여 리팩토링을 진행했다.
Reference
- 헤드 퍼스트 디자인 패턴
- https://refactoring.guru/ko/design-patterns/template-method
'트러블슈팅' 카테고리의 다른 글
MSA에서 선착순 쿠폰 발급 서비스 설계하기 (0) | 2025.03.11 |
---|---|
커서 기반 페이지네이션으로 조회 성능 개선 (0) | 2025.02.12 |
DB의 외부식별자와 내부식별자 분리 (Auto_Increment vs UUID) (0) | 2025.02.10 |
조건부 속성 문제 해결기 (DDD + Factory Method Pattern) (0) | 2025.01.19 |
Spring Event - Listener가 이벤트를 인식하지 못하는 문제 (Type Erasure) (0) | 2025.01.16 |