1. SOLID
1. SRP, 단일 책임 원칙
한 클래스는 하나의 책임만 가져야 한다.
- 클래스는 하나의 기능만 가지며, 어떤 변화에 의해 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.
- SRP 책임이 분명해지기 때문에, 변경에 의한 연쇄작용에서 자유로워질 수 있다.
- 가독성 향상과 유지 보수가 용이해진다.
- 실전에서는 쉽지 않지만 늘 상기해야 한다.
2. OCP, 개방-폐쇄 원칙
소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
- 변경을 위한 비용은 가능한 줄이고, 확장을 위한 비용은 가능한 극대화 해야 한다.
- 요구사항의 변경이나 추가사항이 발생하더라도, 기존 구성요소에는 수정이 일어나지 않고, 기존 구성 요소를 쉽게 확장해서 재사용한다.
- 객체지향의 추상화와 다형성을 활용한다.
3. LSP, 리스코프 치환 원칙
서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다.
- 서브 타입은 기반 타입이 약속한 규약(접근 제한자, 예외 포함)을 지켜야 한다.
- 클래스 상속, 인터페이스 상속을 이용해 확장성을 획득한다.
- 다형성과 확장성을 극대화하기 위해 인터페이스를 사용하는 것이 더 좋다.
- 합성(compsition)을 이용할 수도 있다.
4. ISP, 인터페이스 분리 원칙
자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다.
- 가능한 최소한의 인터페이스만 구현한다.
- 만약 어떤 클래스를 이용하는 클라이언트가 여러 개고, 이들이 클래스의 특정 부분만 이용한다면, 여러 인터페이스로 분류하여 클라이언트가 필요한 기능만 전달한다.
- SRP가 클래스의 단일 책임이라면, ISP는 인터페이스의 단일 책임이다.
5. DIP, 의존성 역전 원칙
상위 모델은 하위 모델에 의존하면 안된다. 둘 다 추상화에 의존해야 한다. 추상화는 세부 사항에 의존해서는 안된다. 세부 사항은 추상화에 따라 달라진다.
- 하위 모델의 변경이 상위 모듈의 변경을 요구하는 위계관계를 끊는다.
- 실제 사용관계는 그대로이지만, 추상화를 매개로 메시지를 주고 받으면서 관계를 느슨하게 한다.
예제 1. 나쁜 예
class PaymentController {
@RequestMapping(value = "/api/payment", method = RequestMethod.POST)
public void pay(@RequestBody ShinhanCardDto.PaymentRequest req) {
shinhanCardPaymentService.pay(req);
}
}
class ShinhanCardPaymentService {
public void payShinhanCardDto.PaymentRquest req) {
shinhanCardApi.pay(req);
}
}
예제 2. 예제 1에 카드사가 추가되는 경우
class PaymentController {
@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);
}
}
예제 3. 좋은 예
- 추상화된 인터페이스를 활용한다. 확장성이 좋아졌다.
class PaymentController {
@RequestMapping(value = "/api/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);
}
class ShinhanCardPaymentService {
@Override
public void payShinhanCardDto.PaymentRquest req) {
shinhanCardApi.pay(req);
}
}
2. 간결한 함수 작성하기
예제 1. 나쁜 예
public static 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 pageDate.getHtml();
}
- 함수가 길고, 여러가지 기능이 섞여있다.
- append도 있고 함수도 있고 초상화 수준이 뒤죽박죽이다.
예제 2. 좋은 예
public static renderPageWithSetupsAndTeardowns(pageData pagedata, boolean isSuite) throws Exception {
if (isTestpage(pageData)
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
- 작게 쪼갠다.
- 함수 내 초상화 수준을 동일하게 맞춘다.
한 가지만 하기(SRP), 변경에 닫게 만들기(OCP)
예제 3. 나쁜 예
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);
}
}
예제 4. 좋은 예
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 HourlyEmployee(r);
case SALARIED:
return new SalariedEmployee(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
- 계산과 타입 관리를 분리
- 타입에 대한 처리는 최대한 Factory에서만 한다.
함수 인수
- 인수의 갯수는 0 ~ 2개가 적당하다.
- 3개 이상에서는?
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
String.format(String format, Object... args);
3. 안전한 함수 작성하기
부수 효과(side Effect)없는 함수
- 부수효과란 값을 반환하는 함수가 외부 상태를 변경하는 경우이다.
- 예) checkPassword 함수 내에 session.initialize();가 있는 경우
4. 함수 리팩터링
- 기능을 구현하는 서투른 함수를 작성한다.
- 테스트 코드를 작성한다.
- 함수 내부의 분기와 엣지값마다 빠짐없이 테스트하는 코드를 짠다.
- 정상적으로 동작하는 경우에 다음단계로 넘어가야한다.
- 리팩터링 한다.
- 코드를 다듬고, 함수를 쪼개고, 이름을 바꾸고, 중복을 제거한다.
6. 실습 강의에서 배운 것
enum 타입 필드에 추가 설명 붙이기
- description을 선언하고 해당 필드가 무엇인지 설명하는 용도로 사용했다.
- enum에 대해 더 알아봐야겠다.
@AllArgsConstructor
- 롬북 코드에 속하며, 해당 클래스의 모든 멤버필드를 파라미터로 갖는 생성자를 자동으로 생성한다.
@AllArgsConstructor
pubic enum ActionType {
PURCHASE(description: "매매");
RENT(description: "임대차");
private String description;
}
// TODO
- 코드를 짜기 전에 어떤 코드를 짜야할지 해당 주석과 함께 써놓는다.
폴더 구조
숫자 사이에 _
123_456_789
와 같이 숫자 사이에 언더바를 넣어서 가독성을 향상시킬 수 있다.
인터페이스 디폴트 메소드
- 인터페이스에서 선언한 메소드는 구현 클래스에서 오버라이드 해야한다.
- 하지만, 인터페이스의 메소드에 default를 붙여주면, 구현클래스에서 오바라이드 할 필요없이 바로 사용 가능하다.
리팩터링
나쁜 예 : 계산이 목적인 함수에 객체를 생성 기능도 포함됨.
calcBrokerage.java
@GetMapping("/api/calc/brokerage")
public Long calcBrokerage(@RequestParam ActionType actionType, @RequestParam Long price) {
if (actionType == ActionType.PURCHASE) {
PurchaseBrokeragePolicy policy = new PurchaseBrokeragePoicy();
return policy.calcuate(price);
}
if (actionType == ActionType.Rent) {
PurchaseBrokeragePolicy policy = new RentBrokeragePoicy();
return policy.calcuate(price);
}
}
좋은 예 : 객체 생성은 Factory 클래스에서 구현
BrokeragePolicy.java
public interface BrokeragePolicy {
brokeraeRule createBrokerageRule(Long price);
default Long calculate(Long price) {
BrokerageRule rule = createBrokerageRule(price);
return rule.calcuMaxBrokerage(price);
}
}
BrokeragePolicyFactory.java
- factory 클래스 구현 : 타입 처리 부분은 해당 클래스에서 분기되도록 한다.
- default를 사용하여 case에 없는 작업이 들어온 경우 예외를 던짐
public class BrokeragePolicyFactory {
public static BrokeragePolicy of(ActionType actionType) {
switch (actionType) {
case RENT:
return new RentBrokeragePolicy();
case PURCHASE:
return new PurchaseBrokeragePolicy();
default:
throw new IllegalArgumentException("해당 actionType에 대한 정책이 존재하지 않습니다.");
}
}
}
calcBrokerage.java
@GetMapping("/api/calc/brokerage")
public Long calcBrokerage(@RequestParam ActionType actionType, @RequestParam Long price) {
BrokeragePolicy policy = BrokeragePolicyFactory.of(actionType);
return policy.calculate(price);
}