Java/클린코드

[클린코드] 함수

chjs93 2022. 2. 7. 23:57
  • 클린코드 3장(함수) 을 읽어보며.
    • 작게 만들어라!
    • 한 가지만 해라
    • 함수당 추상화 수준은 하나
    • 이야기 처럼 읽여야 한다 (내려가기 규칙)
    • 서술적인 이름을 사용해라 (하지만 동사)
    • 함수 인수는 가능한 적게!
    • 명령과 조회를 분리해라
    • 오류 코드 보다는 예외를 사용하기 (오류처리도 한가지 작업)
    • 반복하지 말자
    • 단위 테스트를 작성해가면서 첫 장황한 함수를 개선해나가자

 

  • 함수를 작성할때는.. SOLID 방식을 사용하자
    • SRP (단일 책임 원칙) : 한 클래스는 하나의 책임만 가져야 한다.
    • OCP (개방-폐쇄 원칙) : 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야한다.
    • LSP (리스코프 치환 원칙) : 서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다.
    • ISP (인터페이스 분리 원칙) : 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다.
    • DIP (의존성 역전 원칙) : 상위 모델은 하위 모델에 의존하면 안된다. 둘 다 추상화에 의존해야 한다. 추상화는 세부 사항에 의존해서는 안된다. 세부 사항은 추상화에 따라 달라진다.

 

  • 간결한 함수 작성하기
    • 작게 쪼갠다
    • 타입에 대한 처리는 Factory 에서만
    • 인수의 갯수는 0~2개, 3개 이상은 객체를 인자로 넘기기

 

  • 안전한 함수
    • 부수 효과(Side Effect) 없는 함수

 

  • 함수 리팩터링
    1. 기능을 구현하는 서투른 함수를 작성한다.
    2. 테스트 코드를 작성한다.
    3. 리팩터링을 한다.

 

예시

SRP 단일책임 원칙

각 매장마다 할인 계산을 호출하는 로직이 Controller 에 있습니다.

@RestController
public class MarketQueryController {

    @GetMapping("/api/calc/sail")
    public Long calcSail(@requestParam MarketType marketType,
                         @RequestParam Long price) {

        if (marketType == MarketType.COSTCO) {
            CostcoPolicy policy = new CostcoPolicy();
            return policy.calculate(price);
        }
        if (marketType == MarketType.TRADERS) {
            TradersPolicy policy = new TradersPolicy();
            return policy.calculate(price);
        }
        return null;
    }
}
public class CostcoPolicy {
    public Long calculate(Long price) {

        MarketRule rule;
        if (price < 50_000) {
            rule = new MarketRule(0.6);
        } else if (price < 200_000) {
            rule = new MarketRule(0.5);
        } else if (price < 600_000) {
            rule = new MarketRule(0.4);
        } else if (price < 900_000) {
            rule = new MarketRule(0.5);
        } else {
            rule = new MarketRule(0.9);
        }
        return rule.calcSail(price);
    }
}

 

위 코드는 코스트코 가격 정책에 의해서 할인율이 다르게 계산되는 함수입니다.

위 함수에서는 가격 조건에 따라 할인율이 다르게 적용되고, 할인이 계산이 되는 두가지 책임이 존재하므로 분리하면 다음과 같습니다.

 

public class CostcoPolicy {
    public Long calculate(Long price) {
        MarketRule rule = createMarketRule(price);
        return rule.calcSail(price);
    }

    private MarketRule createMarketRule(Long price) {
        MarketRule rule;
        if (price < 50_000) {
            rule = new MarketRule(0.6);
        } else if (price < 200_000) {
            rule = new MarketRule(0.5);
        } else if (price < 600_000) {
            rule = new MarketRule(0.4);
        } else if (price < 900_000) {
            rule = new MarketRule(0.5);
        } else {
            rule = new MarketRule(0.9);
        }
        return rule;
    }
}

가격에 따라서 세일적용하는 부분과 세일을 계산하는 로직을 분리할 수 있습니다.

 

 

OCP (개방-폐쇄 원칙)

코스트코 정책처럼 트레이더스 가격 정책이 따로 있습니다.

public class CostcoPolicy {
    public Long calculate(Long price) {
        MarketRule rule = createMarketRule(price);
        return rule.calcSail(price);
    }

    private MarketRule createMarketRule(Long price) {
        MarketRule rule;
        if (price < 50_000) {
            rule = new MarketRule(0.6);
        } else if (price < 200_000) {
            rule = new MarketRule(0.5);
        } else if (price < 600_000) {
            rule = new MarketRule(0.4);
        } else if (price < 900_000) {
            rule = new MarketRule(0.5);
        } else {
            rule = new MarketRule(0.9);
        }
        return rule;
    }
}
public class TradersPolicy {
    public Long calculate(Long price) {
        MarketRule rule = createMarketRule(price);
        return rule.calcSail(price);
    }

    private Long calculate(Long price) {

        MarketRule rule;
        if (price < 50_000) {
            rule = new MarketRule(0.6);
        } else if (price < 200_000) {
            rule = new MarketRule(0.5);
        } else if (price < 600_000) {
            rule = new MarketRule(0.4);
        } else if (price < 900_000) {
            rule = new MarketRule(0.5);
        } else {
            rule = new MarketRule(0.9);
        }
        return rule.calcMaxMarket(price);
    }
}

 

위 두가지 클래스의 공통 되는 부분을 인터페이스로 묶어서 계산에 대한 기능을 공통화를 할 수 있습니다.

 

public interface MarketPolicy {

    MarketRule createMarketRule(Long price);

    default Long calculate(Long price) {
        MarketRule rule = createMarketRule(price);
        return rule.calcSail(price);
    }
}
public class CostcoPolicy implements MarketPolicy {

    public MarketRule createMarketRule(Long price) {
        MarketRule rule;
        if (price < 50_000) {
            rule = new MarketRule(0.6);
        } else if (price < 200_000) {
            rule = new MarketRule(0.5);
        } else if (price < 600_000) {
            rule = new MarketRule(0.4);
        } else if (price < 900_000) {
            rule = new MarketRule(0.5);
        } else {
            rule = new MarketRule(0.9);
        }
        return rule;
    }
}
public class TradersPolicy implements MarketPolicy {

    public Long calculate(Long price) {

        MarketRule rule;
        if (price < 50_000) {
            rule = new MarketRule(0.6);
        } else if (price < 200_000) {
            rule = new MarketRule(0.5);
        } else if (price < 600_000) {
            rule = new MarketRule(0.4);
        } else if (price < 900_000) {
            rule = new MarketRule(0.5);
        } else {
            rule = new MarketRule(0.9);
        }
        return rule.calcMaxMarket(price);
    }
}

계산 로직을 인터페이스로 공통화 함으로서 각 매장마다의 정책에 대해서는 확장이 가능하며 계산에 대해서는 변경 영향이 없도록 바꿀 수 있습니다.

 

 

DIP (의존성 역전 원칙)

처음 Controller 에서 타입과 정책에 의존적인 로직을 해결할 수 있습니다.

@RestController
public class MarketQueryController {

    @GetMapping("/api/calc/sail")
    public Long calcSail(@requestParam MarketType marketType,
                         @RequestParam Long price) {

        if (marketType == MarketType.COSTCO) {
            CostcoPolicy policy = new CostcoPolicy();
            return policy.calculate(price);
        }
        if (marketType == MarketType.TRADERS) {
            TradersPolicy policy = new TradersPolicy();
            return policy.calculate(price);
        }
        return null;
    }
}
public class MarketPolicyFactory {

    public static MarketPolicy of(MarketType marketType) {
        switch (marketType) {
            case RENT:
                return new CostcoPolicy();
            case PURCHASE:
                return new TradersPolicy();
            default:
                throw new IllegalArgumentException("해당 marketType에 대한 정책이 존재하지 않습니다.");
        }
    }
}

 

MarketPlicy 인터페이스를 구현하는 Factory 클래스를 생성하면 다음과 같이 리팩토링할 수 있습니다.

 

@RestController
public class MarketQueryController {

    @GetMapping("/api/calc/sail")
    public Long calcSail(@requestParam MarketType marketType,
                         @RequestParam Long price) {

        MarketPolicy policy = MarketPolicyFactory.of(marketType);
        return policy.calculate(price);
    }
}