클린코드 핵심 정리 (SOLID, 3장 함수)

김상운(개발둥이)·2022년 3월 20일
0

클린코드

목록 보기
2/12
post-thumbnail

들어가기


SOLID 란?
클린코드로 유명한 로버트 마틴이 좋은 객체 지향 설계의 5가지 원칙을 제시한 내용을 포스팅하도록 하겠다.


단일 책임 원칙 SRP


SRP 란?

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

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

예시

설명

왼쪽의 로봇을 보면 4개의 기능을 수행한다. 로봇을 클래스로 보면 하나의 클래스에서 여러 책임을 가지고 있다는 의미이다. 이를 해결하기 위해 오른쪽의 그림처럼 4개의 책임 하나당 하나의 클래스로 분리를 한다.


개방-폐쇄 원칙 OCP


OCP 란?

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

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

예시

설명

왼쪽 그림처럼 OCP 가 적용되지 않을 경우, Paint 기능을 추가하였는데 기존 Cut 기능이 동작하지 않는 예시이다.

즉, 클래스의 현재 동작을 변경하면 해당 클래스를 사용하는 모든 시스템에 영향을 미친다는 뜻이다.

OCP 의 목표는 해당 클래스의 기존 동작을 변경하지 않고 클래스의 확장하는 것을 목표로 한다. 클래스가 더 많은 기능을 수행하기를 원한다면 이상적인 접근 방식은 이미 존재하는 기능을 변경하지 않고 추가하는 것이다.


리스코프 치환 원칙 LSP


리스코프 치환 원칙 이란?

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

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

예시

설명

위 그림은, 부모 로봇이 Coffee 종류를 배달하는 그림이다. 카푸치노, 에스프레소는 커피의 종류이기 때문에 자식 로봇이 배달하는 것을 허용하지만 물을 배달하는 것을 허용하지 않는다는 예시이다.

즉, 자식 클래스가 부모 클래스와 동일한 작업을 수행할 수 없는 경우 버그가 발생할 수 있다라는 의미

자식 클래스는 부모 클래스가 할 수 있는 모든 것을 할 수 있어야 하며 이 프로세스를 상속이라고 한다.

목표
이 원칙은 부모 클래스 또는 자식 클래스가 오류 없이 동일한 방식으로 사용될 수 있도록 일관성을 유지하는 것이다.


인터페이스 분리 원칙 ISP


인터페이스 분리 원칙이란?

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

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

예시

설명

왼쪽의 그림은 로봇의 용도가 다르지만 필요하지 않은 기능까지 모두 구현을 한 예시 이며, 오른쪽 그림은 로봇의 용도에 맞게 기능이 분리되어있는 것을 의미한다.

클래스는 필요하지 않은 기능을 수행해야 하는 경우 버그가 발생할 수 있다. 따라서 인터페이스를 분리하여 클래스가 필요한 기능만 상속 받을 수 있게 하는 것이 이 규칙이다.

이 원칙은 작업 집합을 더 작은 집합으로 분할하여 클래스가 필요한 작업 집합만 실행하도록 하는 것이 목표이다.


의존성 역전 원칙 DIP


의존성 역전 원칙 이란?

상위 모델은 하위 모델에 의존하면 안된다. 둘 다 추상화에 의존해야 한다.

추상화는 세부 사항에 의존해서는 안된다. 세부 사항은 추상화에 따라 달라진다.

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

예시

설명

왼쪽 그림은 로봇의 팔에 피자를 자르는 기능만 할 수 있는 것을 의미하며, 오른쪽의 로봇은 언제든 팔을 갈아 끼워 새로운 도구를 사용할 수 있다는 것을 의미한다.

즉, 클래스가 작업을 실행하는 데 구현체 클래스에 의존하는 것이 아닌 추상화에 의존하여 다양한 구체 클래스를 사용할 수 있다는 의미이다.

새로운 카드사가 추가된다면?

@RequestMapping(value = "/api/payment", method = RequestMethod.POST)
public void pay(@RequestBody CardPaymentDto.PaymentRequest req){
	if(req.getType() == CardType.SHINHAN) {
		shinhanCardPaymentService.pay(req);
	} else if(req.getType() == CardType.WOORI){
		wooriCardPaymentService.pay(req);
	}
}

확장에 유연하지 않다..

-> 둘다 추상화된 인터페이스에 의존하도록 한다.

class PaymentController {
	@RequestMapping(value = "/payment", method = RequestMethod.POST)
	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);
}*

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


간결한 함수 작성하기

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)

한 가지만 해라!

함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다.

  • 그럼 '한 가지'가 무엇인지 어떻게 알지?

    • 지정된 함수 이름 아래에서, '추상화 수준'이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 하는 것이다.(사소한 요소를 제거하고 핵심만 뽑아내는 것)
  • 하지만, 의미 있는 이름으로 다른 함수를 추출할 수 있다면, 그 함수는 여러 작업을 하는 것이다.

'추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 하는 것이다.'
라는 말이 어려울 수 있다. 하지만 예시를 보면 그렇게 어려운 것은 아니다.
1. 쓰레기를 판단한다.
2. 쓰레기를 줍는다.
3. 쓰레기를 휴지통에 넣는다.	


쓰레기를 휴지통에 버리는 함수 아래에서 3가지 함수를 호출해야 한다는 것을 알 수 있다. 따라서 함수 이름 아래에서 추상화 수준은 하나라고 알 수 있다. 즉 한 가지 작업만 하는 함수는 함수 안에서 모든 문장의 추상화 수준이(판단한다, 줍는다, 넣는다 등..) 동일해야 한다는 것을 알 수 있다.

NOT
판단, 줍기, 넣기 등 세가지 함수로 구분할 수 있는데. 판단 함수에서 줍기까지 더해 판단하여 줍기 등과 같은 추상화를 하면 안된다는 의미이다.

추상화 수준이란?

말 그대로 구체적으로 풀어 쓰기보다는 추상적으로 표현되어 있다면 추상화 수준이 높은 것이고, 추상화 되어 있지 않고 직접적인 코드는 추상화 수준이 낮다고 한다.


예시

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);
	}
}

'계산도 하고, Money도 생성한다.. 두 가지 기능이 보인다.'
'새로운 직원 타입이 추가된다면?'

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

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

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

계산과 타입관리를 분리

타입에 대한 처리는 최대한 Factory에서만

함수 인수

인수의 갯수는 0~2개가 적당하다.

3개 이상인 경우에는?

// 객체를 인자로 넘기기
Circle makeCircle(double x, double y, double radius); // X
Circle makeCircle(Point center, double radius); // O

// 가변 인자를 넘기기 ==> 특별한 경우가 아니면 잘 안쓴다.
String.format(String format, Object... args);


안전한 함수 작성하기

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

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

public class UserValidator {
	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;
	}
}



참고

해당 포스팅은 제로 베이스 클린코드 한달한권을 수강 후 정리한 내용입니다.

profile
공부한 것을 잊지 않기 위해, 고민했던 흔적을 남겨 성장하기 위해 글을 씁니다.

0개의 댓글