🎉 berenickt 블로그에 온 걸 환영합니다. 🎉
CS
클린코드
3장 함수

1. SOLID

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

  1. SRP(Single Responsibility) : 단일 책임 원칙
  2. OCP(Open/Closed Principle) : 개방-폐쇄 원칙
  3. LSP(Liskov's Substitution Principle) : 리스코프 치환 원칙
  4. ISP(Interface Segregation) : 인터페이스 분리 원칙
  5. DIP(Dependency Inversion) : 의존성 역적 원칙

1.1 SRP(단일 책임 원칙)

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

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

1.2 OCP(개방-폐쇄 원칙)

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

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

1.3 LSP(리스코프 치환 원칙)

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

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

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

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

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

1.5 DIP(의존성 역전 원칙)

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

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

만약 다음 코드에서 새로운 카드사가 추가된다면?

1
class PaymentController {
2
@RequestMapping(value = "/api/payment", method = RequestMethod.POST)
3
public void pay(@RequestBody ShinhanCardDto.PaymentRequest req) {
4
shinhancarPaymentService.pay(req);
5
}
6
}
7
8
class ShinhanCardPaymentService {
9
public void pay(ShinhanCardDto.PaymentRequest req) {
10
shinhanCardApi.pay(req);
11
}
12
}

확장에 유연하지 않다.

1
@RequestMapping(value = "/api/payment", method = RequestMethod.POST)
2
public void pay(@RequestBody CardPaymentDto.PaymentRequest req) {
3
if(req.getType() === CardType.SHINHAN) {
4
shinhancarPaymentService.pay(req);
5
} else if(req.getType() == CardType.WOORI) {
6
wooriCardPaymentService.pay(req);
7
}
8
}

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

1
class PaymentController {
2
@RequestMapping(value = "/payment", method = RequestMethod.POST)
3
public void pay(@RequestBody CardPaymentDto.PaymentRequest req) {
4
final CardPaymentService cardPaymentService = cardPaymentFactory.getType(req.getType());
5
cardPaymentService.pay(req);
6
}
7
}
8
9
public interface CardPaymentService {
10
void pay(CardPaymentDto.PaymentRequest req);
11
}
12
13
public class ShinhanCardPaymentService implements CardPaymentService {
14
@Override
15
public void pay(CardPaymentDto.PaymentRequest req) {
16
shinhanCardApi.pay(req);
17
}
18
}

2. 간결한 함수 작성하기

다음 코드는 함수가 길고, 여러가지 기능이 섞여있습니다.

1
public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
2
boolean isTestPage = pageData.hasAttribute("Tset");
3
if (isTestPage) {
4
WikiPage testPage = pageData.getWikiPage();
5
StringBuffer newPageContent = new StringBuffer();
6
includeSetupPages(testPage, newPageContent, isSuite);
7
newPageContent.append(pageData.getContent());
8
includeTeardownPages(testPage, newPageContent, isSuite);
9
pageData.setContent(newPageContent.toString());
10
}
11
return pageData.getHtml();
12
}

작게 쪼갠다. 함수 내 추상화 수준을 동일하게 맞춘다.

1
public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
2
if (isTestPage(pageData))
3
includeSetupAndTeardownPages(pageData, isSuite);
4
return pageData.getHtml();
5
}

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

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

1
public Money calculatePay(Employee e) throws InvalidEmployeeType {
2
switch (e.type) {
3
case COMMISSIONED:
4
return calculateCommissionedPay(e);
5
case HOURLY:
6
return calculateHourlyPay(e);
7
case SALARIED:
8
return calculateSalariedPay(e);
9
default:
10
throw new InvalidEmployeeType(e.type);
11
}
12
}

계산과 타입관리를 분리, 타입에 대한 처리는 최대한 Factory에서만

1
public abstract class Employee {
2
public abstract boolean isPayday();
3
public abstract Money calculatePay();
4
public abstract void deliverPay(Money pay);
5
}
6
7
public interface EmplyeeFactory {
8
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeTytpe;
9
}
10
11
public class EmployeeFactoryImpl implements EmployeeFactory {
12
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
13
switch (r.type) {
14
case COMMISSIONED:
15
return calculateCommissionedPay(e);
16
case HOURLY:
17
return calculateHourlyPay(e);
18
case SALARIED:
19
return calculateSalariedPay(e);
20
default:
21
throw new InvalidEmployeeType(e.type);
22
}
23
}
24
}

2.2 함수 인수

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

1
// 객체를 인자로 넘기기 👍
2
Circle makeCircle(double x, double y, double radius); // ❌
3
Circle makeCircle(Point center, double radius); // ⭕
4
5
// 가변 인자를 넘기기 -> 특별한 경우가 아니면 잘..🖐
6
String.format(String format, Object... args);

3. 안전한 함수 작성하기

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

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

함수와 관계없는 외부 상태를 변경시킨다.

1
public class UserValidator {
2
private Cryptographer crytographer;
3
public boolean checkPassword(String userName, String password) {
4
User user = UserGateway.findByName(userName);
5
if (user != User.NULL) {
6
String codedPhrase = user.getPhraseEncodedByPassword();
7
String phrase = cryptographer.decrypt(codedPhrase, password);
8
if ("Valid Password".equals(phrase)) {
9
Seesion.initialize();
10
return true;
11
}
12
}
13
return false;
14
}
15
}

4. 함수 리팩터링

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

5. 프로젝트