≈아마 개발 하면서 이 코드는 정말 고치고 싶다는 생각들을 많이 하게 될거다.

 

회사 코드만 봐도 2000줄 3000줄이 넘어가는 코드들이 즐비하다.

이외에도 객체지향보다는 절차지향, get set메소드의 무분별한 사용, 오타는 기본, 심지어 변수명조차 논리적으로 이해되지 않게 작성하는 등 별의 별일이 프로젝트에 다 섞여들어가 있다.

 

그래서 어떤 식으로 코드를 개선해나가야할 지 조금 분류를 하고 서비스를 개선해나가고자 한다.

 

1. 객체 지향 원칙

2. 메서드 추출

3. 함수형 인터페이스를 활용한 Switch-Case문 제거

4. 디자인 패턴 (템플릿 메서드. 팩토리 패턴)

객체지향 원칙

어떻게 도메인을 정의하는지가 중요할듯 하다. 내가 리팩토링 하는 코드 같은 경우에는 비즈니스 로직과 히스토리 로직이 너무 혼재되어 발생하는 복잡성이 가장 큰 문제였다. 계속 객체의 값을 바꿔가며 사용하기 때문에 구현부에서 정확한 흐름을 파악하는데 상당한 시간이 걸리고, 혹여나 본 로직에 수정 사항이 생기면 처음부터 끝까지 관련 클래스들을 모두 확인해야 하는 문제점이 있다.

 

그래서 이 행동이 어디서 나오게 된건지, 역할과 책임이 뭔지를 정의하는 과정이 무엇보다 중요하게 느껴진다.

이런 객체지향 얘기는 많이 들어볼 수 있으니 생략하자. 앞으로 뒤에서 얘기할 내용들이 객체지향 관련 설명이tㅈ 될 거 같아서 이부분에 대해서는 뒤에서 겸사겸사 설명해보록 하겠다!

 

메서드 추출

가장 기본적인 방식. 비즈니스 로직에서 불필요한 부분이나 복잡하게 구현된 로직을 분리하는 목적이 될 수 있다. 

여기서 조금만 더 생각해보자. 대부분의 로직이 비슷한 상황에서 약간의 구현부만 다르다면 어떻게 처리할 것인가?

우선 그 수준의 차이를 한번 분류해보자

제네릭 > 옵셔널 콜백 > 전략 패턴

한번 기준을 잡고 메서드 추출을 하려고 생각한다.

1. 로직의 유사성도 필요에 따라서 조금씩 달라진다고 생각했다. 대부분의 로직이 동일하지만, 파라미터의 타입이 조금씩 다른 경우는 제네릭을 활용한 해결

2. 대부분의 로직은 동일하나 일부 로직이 조금 추가되거나 변경되는 내용이 있다면, 함수형 인터페이스를 활용한 옵셔널 콜백

3. 로직은 다르지만, 어떤 행위를 하는 목적만 같은 경우에는 전략 패턴을 활용한 개선 등

유사도의 정도에 따라서 세가지 방법을 활용해보려고 한다.

 

제네릭

먼저 제네릭은 모든 로직이 공통되고, 사용하는 인자의 타입만 다른 경우 사용한다.

	public static <T extends CursorIdParser> CursorSummary<T> getCursorSummaries(final List<T> summaries) {
		if (summaries.isEmpty()) {
			return new CursorSummary<>(null, 0, summaries);
		}

		T lastSummary = summaries.get(summaries.size() - 1);
		String nextCursorId = lastSummary.cursorId();

		int summaryCount = summaries.size();

		return new CursorSummary<>(nextCursorId, summaryCount, summaries);
	}


이 경우 List타입의 아무 형식의 데이터가 들어오더라도 같은 행동을 하고 같은 형태의 반환값을 내보내기 때문에, 어디서든 공통으로 사용할 수 있는 로직으로 적절하다. 목적과 행동이 동일한 경우 사용하면 좋을듯 하다.

 

옵셔널 콜백

public class ProductServiceWithCallback {

    public void processProduct(Product product, Consumer<Product> priceProcessor, Optional<Consumer<Product>> additionalLogic) {
        System.out.println("Processing product: " + product.getName());
        
        // 가격 처리 로직 실행
        priceProcessor.accept(product);
        
        // 추가 로직이 존재하면 실행
        additionalLogic.ifPresent(logic -> logic.accept(product));

        System.out.println("Product processed: " + product.getName());
        System.out.println("---------------------------------");
    }
}
public class Main {
    public static void main(String[] args) {
        ProductServiceWithCallback service = new ProductServiceWithCallback();

        Product laptop = new Product("Laptop", 1000.0);
        Product book = new Product("Book", 20.0);

        // 1. 할인 적용 + 포인트 적립
        service.processProduct(
            laptop,
            product -> System.out.println("Discounted Price: " + (product.getPrice() * 0.9)),
            Optional.of(product -> System.out.println("Reward Points Earned: " + (product.getPrice() * 0.1)))
        );

        // 2. 일반 상품 처리 (추가 로직 없음)
        service.processProduct(
            book,
            product -> System.out.println("Regular Price: " + product.getPrice()),
            Optional.empty()
        );
    }
}

목적이 같고 대부분의 행위는 같지만, 추가적인 행동이 있는지, 없는지에 따라서  다음과 같은 로직을 활용해볼 수도 있다.

함수형 인터페이스에 Optional로 감싸서 행동을 선택적으로 할 수 있도록 만든다.

만약 전략패턴처럼 좀 큰 확장성을 가지지 않는다면, 다음과 같은 처리로 조금 가볍게 문제를 해결하는 것도 하나의 방법일듯 하다.

 

전략 패턴

public interface PriceStrategy {
    double calculatePrice(double basePrice);
}
public class ProductService {

    public void processProduct(Product product, PriceStrategy strategy) {
        System.out.println("Processing product: " + product.getName());

        // 전략(로직)을 사용하여 가격 계산
        double finalPrice = strategy.calculatePrice(product.getPrice());
        System.out.println("Final Price: " + finalPrice);
    }
}
public class Main {
    public static void main(String[] args) {
        ProductService service = new ProductService();

        Product laptop = new Product("Laptop", 1000.0);
        Product book = new Product("Book", 20.0);

        // 할인 전략
        PriceStrategy discountStrategy = basePrice -> basePrice * 0.9;

        // 프리미엄 가격 전략
        PriceStrategy premiumStrategy = basePrice -> basePrice * 1.2;

        // 기본 가격 전략
        PriceStrategy normalStrategy = basePrice -> basePrice;

        // 전략 실행
        service.processProduct(laptop, discountStrategy); // 할인 적용
        service.processProduct(laptop, premiumStrategy);  // 프리미엄 적용
        service.processProduct(book, normalStrategy);     // 기본 가격
    }
}

목적은 같지만 구체적인 행동이 다른 경우 다음과같이 전략을 위한 인터페이스를 만들고 이를 실제 구현하는 방식으로 문제를 해결할 수도 있다. 이전의 Optional 콜백 보다는 확장성을 갖춘 패턴으로 조금 더 다양한 행동을 정의할 수 있다.

 

이런 방법들은 일종의 하나의 공식처럼 여길 게 아니라, 공통된 내용을 하나로 묶는 과정에서도 유연함을 갖출 수 있는 다양한 설계 방식을 생각나는대로 한번 정리해 본 내용이다. 입맛에 맞게 충분한 생각을 가지고 코드를 정리해보자

 

함수형 인터페이스를 활용한 Switch-Case문 제거

대부분의 분기 처리들이 Switch-Case로 처리되는 내용들이 많을거다. 그래서 각 행동마다 메소드 중복이 일어날 수도 있고, break처리를 해줘야하고, 불필요한 defualt 처리를 할 수도 있고, Switch-Case문 자체가 반복될 수도 있다. 

매번 이런 로직을 보게 되면 코드를 읽느라 피곤해질 수 있다.

 

보통 이제 Case문을 보면 쉽게 변하지 않을, 상수에 가까운 키워드가 많을 거다. 그렇다면 Enum을 활용하고, 인자로 함수형 인터페이스를 조합해서 활용하면 코드가 훨씬 많이 개선될 수 있다.

public enum WebSite {

	NAVER("naver", NaverCrawler::new),
	DANAWA("danawa", DanawaCrawler::new),
	COUPANG("coupang", CoupangCrawler::new);

	private final String market;
	private final Function<String, WebCrawler> marketFactory;

	WebSite(
		final String market,
		final Function<String, WebCrawler> marketFactory
	) {
		this.market = market;
		this.marketFactory = marketFactory;
	}

	public static WebCrawler selectCrawler(final String url) {
		for (WebSite site : values()) {
			if (url.contains(site.market)) {

				return site.marketFactory.apply(url);
			}
		}
		throw new RuntimeException("지원하지 않는 아이템 URL 입니다.");
	}
}
WebCrawler webCrawler = WebSite.selectCrawler(request.itemUrl());

이렇게 보면 itemUrl에 해당하는 내용듣을 Enum내에서 순환하면서 찾고, 찾게 되면 바로 Function<> 을 통해서 그 행동을 하게 해주면 되기 때문에 Enum 클래스 내에서 충분한 역할과 책임을 가질 수 있다.

그리고 본 로직에서 충분히 분리되기 때문에 메인 메서드에서도 단 한줄로 메세지만 전달하면 돼서 훨씬 더 쉽게 비즈니스 로직을 이해할 수 있다.

이 로직 같은 경우에는 생성의 책임도 Enum내에서 다하고 있기 때문에 팩토리 디자인 패턴이라고도 할 수 있겠다~

 

이건 개인적으로 많이 애정하는 개선 방법 중 하나다 ㅎㅎ(홍석쿤 감사)

 

디자인 패턴

디자인 패턴은 알아두면 생각을 확장하기에 좋은 설계가 떠오르는 방법들이다. 항상 개발을 할 때, '아 이런 디자인 패턴으로 문제를 해결해야겠다' 를 떠올리기 보다 하다보니 '이런 디자인 패턴을 쓰면 조금 더 정리나 설계가 깔끔하겠다'라는 생각으로 접근하는 게 좋다고 생각한다. 

우선 지금 레거시 코드 같은경우에는 굉장히 절차지향적으로 짜여져있다. 그러다보니 로직이 실행되는 순서가 굉장히 길고 구현은 각각 다르지만 흐름은 비슷하다는 특징이 있었다. 그래서 공통된 행동 틀을 정해주고, 그 틀에 맞게끔 행동하도록 템플릿 메소드 패턴을활용해 문제를 해결하려고 시도했다.

 

public abstract class OrderProcess {

    public final void processOrder() {
        selectItem();
        makePayment();
        prepareForDelivery();
        deliver();
    }

    // 공통 로직: 모든 주문에서 동일하게 수행
    private void selectItem() {
        System.out.println("Item selected.");
    }

    // 변해야 하는 로직: 자식 클래스에서 구현
    protected abstract void makePayment();

    // 공통 로직: 배송 준비
    private void prepareForDelivery() {
        System.out.println("Preparing item for delivery.");
    }

    // 변할 수 있는 로직: 기본 구현 제공 (필요 시 오버라이드)
    protected void deliver() {
        System.out.println("Item delivered by default shipping.");
    }
}

템플릿 메소드 패턴은 추상클래스와 접근제어자의 특징을 잘 살려서 문제를 해결해볼 수 있다.

먼저 processOrder 메소드로 공통 진행 흐름을 정의한다.

그리고 protected 접근제어자를 활용해서 공통된 로직을 정의하고 상속을 받아서 이를 활용하되, 필요 시 오버라이드를 통해서 재정의도 가능하다!

만약 공통 로직이 없고 구현 내용이 다른 경우 protected abstract를 사용해서 자식 클래스에서 구현하면 된다

 

이런식으로 상속을 활용해 다양하게 의도를 전달하게 되면 핵심 비즈니스 로직만 남기고 목적에 맞게 내용을 정리할 수 있는 형태로 코드 개선이 가능하다.

 

이렇게 리팩토링 혹은 코드를 개선하기 위한 다양한 방법들을 정리해보았다. 이런 방법들도 중요하지만 무엇보다 중요한 건 도메인을 어떻게 정의하고 이들의 상호작용 관계를 파악해서 설계 방향을 잡는게 중요하다. 아직은 많은 연구로 머리가 터질 것 같지만, 문제 발생 범위를 최소화하면서 좋은 설계를 한번 만들어가보자.. 화이팅

 

 

 

 

복사했습니다!