[Clean Code] Functions

도모·2023년 3월 8일
0

Clean Code

목록 보기
2/5
post-thumbnail

정리한 내용 중 잘못된 내용이 있을 수 있습니다. 잘못된 내용이 있다면 꼭 알려주세요.

1.SOLID

SOLID 원칙

객체 지향 설계의 5가지 원칙

  • SRP: 단일 책임 원칙
  • OCP: 개방-폐쇄 원칙
  • LSP: 리스코프 치환 원칙
  • ISP: 인터페이스 분리 원칙
  • DIP: 의존성 역전 원칙

SRP(단일 책임 원칙)

한 클래스는 하나의 책임만 가져야 한다.

  • 클래스는 하나의 기능만 가지며, 어떤 변화에 의해 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.
  • SRP 책임이 분명해지기 때문에, 변경에 의한 연쇄작용에서 자유로워질 수 있다.
  • 가독성 향상과 유지보수가 용이해진다.
  • 실전에서는 쉽지 않지만 늘 상기해야 한다!

OCP(개방-폐쇄 원칙)

소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야한다.

  • 변경을 위한 비용은 가능한 줄이고, 확장을 위한 비용은 가능한 극대화 해야 한다.
  • 요구사항의 변경이나 추가사항이 발생하더라도, 기존 구성요소에는 수정이 일어나지 않고, 기존 구성 요소를 쉽게 확장해서 재사용한다.
  • 객체지향의 추상화와 다형성을 활용한다.

LSP(리스코프 치환 원칙)

서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다.

  • 서브 타입은 기반 타입이 약속한 규약(접근제한자, 예외 포함)을 지켜야 한다.
  • 클래스 상속, 인터페이스 상속을 이용해 확장성을 획득한다.
  • 다형성과 확장성을 극대화하기 위해 인터페이스를 사용하는 것이 더 좋다.
  • 합성(composition)을 이용할 수도 있다.

ISP(인터페이스 분리 원칙)

자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다

  • 가능한 최소한의 인터페이스만 구현한다.
  • 만약 어떤 클래스를 이용하는 클라이언트가 여러 개고, 이들이 클래스의 특정 부분만 이용한다면, 여러 인터페이스로 분류하여 클라이언트가 필요한 기능만 전달한다.
  • SRP가 클래스의 단일 책임이라면, ISP는 인터페이스의 단일 책임

DIP(의존성 역전 법칙)

상위 모델은 하위 모델에 의존하면 안된다. 둘 다 추상화에 의존해야 한다.
추상화는 세부 사항에 의존해서는 안된다. 세부 사항은 추상화에 따라 달라진다.

  • 하위 모델의 변경이 상위 모듈의 변경을 요구하는 위계관계를 끊는다.
  • 실제 사용관계는 그대로이지만, 추상화를 매개로 메세지를 주고 받으면서 관계를 느슨하게 만든다.

조금 더 자세하게 코드로 살펴보자.

class PaymentController {
	@PostMapping("/api/payment")
    public void pay(@RequestBody ShinhanCardDto.PaymentRequest req) {
    	shinhanCardPaymentService.pay(req);
    }
}

class ShinhanCardPaymentService {
	public void pay(hinhanCardDto.PaymentRequest req) {
    	shinhanCardApi.apy(req);
    }
}

위 코드에서 새로운 카드사가 추가된다면 어떻게 될까?

class PaymentController {
	@PostMapping("/api/payment")
    public void pay(@RequestBody ShinhanCardDto.PaymentRequest req) {
    	if(req.getType() == CardType.SHINHAN) {
        	shinhanCardPaymentService.pay(req);
        } else if (req.getType() == CardType.WOORI) {
        	wooriCardPaymenyService.pay(req);
        }
    }
}

위와 같이 작성한다면 PaymentController에서는 ShinhanCardPaymentServiceWooriCardPaymenyService의 의존성을 갖고있고,
카드사가 더 많아진다면 if else문이 계속 생기는 등 확장성에 좋지 않다.

그럼 어떻게 해야할까?

위 그림과 같이 추상화된 인터페이스에 의존하도록 하면 된다.

그렇다면 아래와 같이 코드를 깔끔하게 작성할 수 있다.

class PaymentController {
	@PostMapping("/api/payment")
    public void pay(@RequestBody CardPaymentDto.PaymentRequest req) {
    	final CardPaymentService cardPaymentService = cardPaymentFactory.getType(req.getType());
        cardPaymentService.pay(req);
    }
}

public interface CardPaymentService {
	void pay(CardPaymentDto.PaymentRequest req);
}

class ShinhanCardPaymentService implements CardPaymentService {
	@Override
	public void pay(CardPaymentDto.PaymentRequest req) {
    	shinhanCardApi.apy(req);
    }
}

2.간결한 함수 작성하기

간결한 함수 작성하기

아래 코드를 보면 함수가 길고, 여러가지 기능이 섞여있는걸 볼 수 있다.

public static String renderPageWithSetupsAndTeardowns(PageData pagedata, boolean isSuite) throws Exception {
	boolean isTestPage = pageData.hasAttribute("Test");
    if (isTestPage) {
    	WikiPage testPage = pageData.getWikiPage();
        StringBuffer newPageContent = new StringBuffer();
        includeSetupPages(testPage, newPageContent, isSuite);
        newPageContent.append(pageData.getContent());
        includeTeardownPages(testPage, newPageContent, isSuite);
        pageData.setContent(newPageContent.toString());
    }
    return pageData.getHtml();
}

위 함수를 아래처럼 작게 쪼개고, 함수 내 추상화 수준을 동일하게 맞춘다.

public static String renderPageWithSetupsAndTeardowns(PageData pagedata, boolean isSuite) throws Exception {
	if (isTestPage(pageData))
    	includeSetupAndTeardownPages(pageData, isSuite);
    return pageData.getHtml();
}

한 가지만 하기(SRP), 변경에 닫게 만들기(OCP)

public Money calculatePay(Employee e) throws InvalidEmployeeType {
	switch (e.type) {
    	case COMMISSIONED:
        	return calculateCommissionedPay(e);
        case HOURLY:
        	return calculateHourlyPay(e);
        case SALARIED:
        	return calculateSalariedPay(e);
        default:
        	throw new InvalidEmployeeType(e.type);
    }
}

위 코드에서는 두 가지 문제점이 보인다.
1.계산도 하고, Money도 생성한다. 라는 두가지의 기능이 보인다.
2.새로운 직원 타입이 추가된다면?

public abstract class Employee {
	public abstract boolean isPayday();
    public abstract Money calculatePay();
    public abstract void deliverPay(Money pay);
}

public interface EmployeeFactory {
	public Employee makeEmployee(EmplyeeRecord r) throws InvalidEmployeeType;
}

public class EmployeeFactoryImpl implements EmployeeFactory {
		public Employee makeEmployee(EmplyeeRecord r) throws InvalidEmployeeType {
        	switch (r.type) {
                case COMMISSIONED:
                    return CommissionEmployee(r);
                case HOURLY:
                    return new HourlyEmployee(r);
                case SALARIED:
                    return new SalariedEmployee(r);
                default:
                    throw new InvalidEmployeeType(r.type);
        	}
    	}
}

계산과 타입관리를 분리하여 계산은 Employee class 내부에 calculatePay메서드를 통해 계산하고
타입에 대한 처리는 최대한 Factory에서만 하라고 권고한다.

함수 인수

인수의 갯수는 0~2개가 적당하다고 한다.
3개 이상인 경우에는

// 1.객체를 인자로 넘기는 방법👍🏻
Circle makeCircle(double x, double y, double redius); 	// 👎🏻 
Circle makeCircle(Point center, double redius); 		// 👍🏻

// 2.가변 인자를 넘기기 -> 특별한 경우가 아니라면 잘 사용하지는 않는다. 🤔
String.format(String format, Object... args);

3.안전한 함수 작성하기

부수 효과(Side Effect)없는 함수

부수효과? 값을 반환하는 함수가 외부 상태를 변경하는 경우

public class UserValidater {
	private Cryptographer cryptographer;
    public boolean checkPassword(String userName, String password) {
    	User user = UserGateway.findByName(userName);
        if (user != User.NULL) {
        	String codedPhrase = user.getPhraseEncodedByPassword();
            String phrase = cryptographer.decrypt(codedPhrase, password);
            if ("Valid Password".equals(phrase)) {
            	Session.initialize(); //함수와 관계 없는 외부 상태를 변경시킨다.
                return true;
            }
        }
        return false;
    }
}

위 코드에서 Session을 초기화 하는 함수가 들어가있는데, 패스워드 체크하는 함수에서
외부의 상태를 변경시키기 때문에, 좋지 않은 코드라고 말할 수 있다.

04.함수 리팩터링

함수 리팩터링을 하는 3단계

  1. 기능을 구현하는 서투른 함수를 작성한다.
    • 길고, 복잡하고, 중복도 있다.
  2. 테스트 코드를 작성한다.
    • 함수 내부의 분기와 엣지값마다 빠짐없이 테스트하는 코드를 짠다.
  3. 리팩터링 한다.
    • 코드를 다듬고, 함수를 쪼개고, 이름을 바꾸고, 중복을 제거한다.
profile
안녕하세요. 백엔드 엔지니어 도모입니다.

0개의 댓글