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

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

클린코드

목록 보기
2/12
post-thumbnail

들어가기


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


단일 책임 원칙 SRP


SRP 란?

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

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

예시

srp

설명

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


개방-폐쇄 원칙 OCP


OCP 란?

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

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

예시

ocp

설명

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

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

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


리스코프 치환 원칙 LSP


리스코프 치환 원칙 이란?

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

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

예시

lsp

설명

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

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

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

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


인터페이스 분리 원칙 ISP


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

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

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

예시

isp

설명

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

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

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


의존성 역전 원칙 DIP


의존성 역전 원칙 이란?

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

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

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

예시

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개의 댓글

관련 채용 정보