본문 바로가기

java

SOLID(객체지향설계) - 코드 예제, 리팩토링

SOLID 원칙에 대한 개념을 그림과 코드 예제를 통해 알아보고 SOLID 원칙을 적용 할 수 있는 리팩터링을 다시 확인 합니다.

모든 내용에는 출처가 있습니다

 

 

목차 

1. SOLID 원칙이란? 

     1) SRP 단일 책임 원칙

     2) OCP 개방-폐쇄 원칙 

     3) LSP 리스코프 치환 원칙

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

    5) DIP 의존성 역전 원칙 

 

2. 코드로 보는 SOLID 원칙 예제

 

3. 함수 리팩터링

 

 

1. SOLID 원칙이란?

 러버트 마틴이 2000년대 초반 명명한 객체 지향 프로그래밍 및 설계의 다섯가지 기본 원칙을 마이클 페더스가 두(머리)문자어 기억술로 소개 한것이다. 프로그래머가 시간이 지나도 유지보수와 확장이 쉬운 시스템을 만들고자 할 때 이 원칙들을 함께 적용 할 수 있다. SOLID 원칙들은 소프트웨어 작업에서 프로그래머가 소스코드가 읽기 쉽고 확장하기 쉽게 될 때까지 소프트웨어 소스 코드를 리팩터링 하여 코드 냄새를 제거하기 위해 적용 할 수 있는 지침이다. 이 원칙들은 애자일 소프트웨어 개발과 적응적 소프트웨어 개발의 전반적인 전략의 일부이다.  [위키백과]

 

 

 

주문자 약어 개념
S SRP (Single Responsibility Principle)
단일책임 원칙
 "한 클래스는 하나의 책임만 가져야 한다."
O OCP (Open/Closed Principle)
개방-폐쇄 원칙
  "소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다."
L LSP (Liskov Substitution Principle)
리스코프 치환 원칙
  "프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으며
하위타입의 인스턴스로 바꿀 수 있어야 한다. "
I ISP (Interface Segregation Principle)
인터페이스 분리 원칙
 "특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다."
D DIP (Dependency Inversion Principle)
의존관계 역전 원칙
  프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다"
- 의존성 주입은 이 원칙을 따르는 방법 중 하나다.

 

 

 

 

 

1 .  S - Single Responsibility

[이미지 출처] -https://medium.com/backticks-tildes/the-s-o-l-i-d-principles-in-pictures-b34ce2f1e898

Goal

This principle aims to separate behaviours so that if bugs arise as a result of your change, it won’t affect other unrelated behaviours.

 

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

 

 - 클래스는 하나의 기능만 가지며, 어떤 변화에 의해 클래스를 변경해야 하는 이유는 오직 하나 뿐이어야 한다. 

 - SRP 책임이 분명해 지기 때문에, 변경에 의한 연쇄작용에서 자유로워 질 수 있다.

 - 가독성 향상과 유지보수가 용이해 진다. 

 

 

2 . O - open closed

Goal

 

This principle aims to extend a Class’s behaviour without changing the existing behaviour of that Class. This is to avoid causing bugs wherever the Class is being used.

 

 

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

- 변경을 위한 비용은 가능한 줄이고, 확장을 위한 비용은 가능한 극대화 해야한다

- 요구사항의 변경이나 추가사항이 발생하더라도, 기존 구성요소에는 수정이 일어나지 않고, 기존 구성 요소를 쉽게 확장해서 재사용한다.

- 객체지향의 추상화와 다향성을 활용한다.

 

 

3 . L - liskov Substitution 

 

Goal

This principle aims to enforce consistency so that the parent Class or its child Class can be used in the same way without any errors.

 

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

 - 서브 타입은 기반타입이 약속한 규약(접근제한자, 예외 포함)을 지켜야한다. 

 - 클래스 상속, 인터페이스 상속을 활용해 확장성을 획득한다. 

 - 다향성과 확장성을 극대화 하기 위해 인터페이스를 사용하는 것이 더 좋다. 

 - 합성(composition) 을 이용할 수도 있다. 

 

 

4 . I - Interface Segregation 

 

Goal

This principle aims at splitting a set of actions into smaller sets so that a Class executes ONLY the set of actions it requires.

 

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

  - 가능한 최소한의 인터페이스만 구현 

  -  만약 어떤 클래스를 이용하는 클라이언트가 여러개고, 이들이 클래스의 특정 부분만 이용한다면, 여러 인터페이스로 분류하여 필요한 클라이언트가 필요한 기능만 전달한다. 

 - SRP 가 클래스의 단일 책임이라면, ISP 는 인터페이스의 단일 책임 

   "묶을 수 있는 기능 한가지에 대한 인터페이스 식으로 설계를 하는 것이고 다른 기능인데 한가지로 묶으면 안된다" 

 

5 . D - Dependency Inversion

 

 

Goal

This principle aims at reducing the dependency of a high-level Class on the low-level Class by introducing an interface.

 

I Cut pizza with my pizza cutter arm (x) , I cut pizza with my Tool

추상화를 통해서 tool 로 변경하자.

 - 하위 모델의 변경이 상위 모듈의 변경을 요구하는 위계관계를 끊는다. 

 - 실제 사용관계는 그대로이지만, 추상화를 매개로 메시지를 주고 받으면서 관계를 느슨하게 만든다.

 

 

2.  코드로 보는 SOLID 원칙 예제

 

" 결제 API 예제를 통해서 SOLID 원칙 방안을 살펴보자. "

 

Example  1. 확장에 유연하지 않은 소스 코드

[문제 1]

여러 카드사가 나열되어 있다. 

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

 [문제 1 - 해결방안]

   여러 카드사를 하나의 인터페이스에 의존 하도록 한다. [DIP]

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

 

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);
   }
   
   class ShinhanCardPaymentService {
      puvlic void pay(ShinhanCardDto.PaymentRequest req) {
        shinhanCardApi.pay(req);
      }
   }
}

 

Example 2.  간결한 함수 작성하기

 [문제 2] 여러 기능이 섞여 있다. 가독성과 유지보수에 문제가 생긴다.

public static String renderPageWithSetupsAndTeardown(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(); 
}

[문제 2 - 해결 방안]

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

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

 

Example 3.

[문제 3]

: switch 문을 확인하면 계산도 하고, Money 도 생성한다. 두가지 기능이 보이고 새로운 직원 , 타입의 추가 되면 복잡해 진다.

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

 

 

[문제 3 - 해결 방안] 한가지만 하기 (SRP), 변경에 닫게 만들기(OCP)  (ISP)

  기능을 분리한다. 계산과 타입관리를 분리하고 타입에 대한 처리는 최대한 Factory 에서만 진행한다.

  실제로 여러 타입을 관리하다 보면 언젠가 한 순간에는 분기 처리를 해줘야 할때가 있다. 객체를 생성할 때는 팩토리 내부에서 분기를 태워주는 과정에서 객체를 생성하고 다른 부분은 다형성을 이용해서 계속 사용 할수 있도록 한다. 

public abstract class Employee {
  // employee 에 대한 필요한 메서드를 추상화로 선언해두어 계산하는 과정을 담았다. 
 public abstract boolean isPayday();
 public abstract Money CalculatePay(); 
 public abstract void deliverPay(Money pay); 
}

// employee 를 생성해 주는 과정을 따로 분류
public interface EmployeeFactory {
 public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}

// 생성 과정 구현, Money 는 Employee 자체의 caluatePay를 통해서.. type에대한 처리는 최대한 factory 안에서
public interface EmployeeFactoryImple implements EmployeeFactory {
 public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
 switch (e.type) {
      case COMMISSIONED:
          return new Commitionedemployee(r);
      case HOURLY:
          return new HourlyEmployee(r);
      case SALARIED:
          return new SalariedEmployee(r);
      default:
          return new InvalidEmployeeType(r.type);
     }
 }
}

 

Example 4 .  부수 효과 (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(codePhrase, password); 
             if("Valid Password".equals(phrase)){
               Session.initialize(); // <----
               return true;
             }
        return false
       }
}

 

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

 

 " 패스워드를 체크하는 checkPassword 함수에서 전역에 해당하는 Session.initialize() 롤 호출해서 세션함수를 수행하는 함수 자신의 범위를 벗어나는 행위. 예를 들어 checkPasswordAndInitializeSession() 이라는 식으로 외부의 상태를 변환시킨다는 말이라도 제목에 써주면 될테지만 없으니 이거도 문제다. " 

 

3 . 함수 리팩토링

 

- 서투른 함수 작성  :  어떠한 기능에 해당하는 코드를 작성하고 처음부터 클린코드를 짜려는 것보다 기능에 충실해서 함수를 작성한다. 

- 테스트 코드와 리팩터링  :  리팩토링은 현재 output 은 동일하다는 것을 전제하에 코드의 가독성과 성능을 개선하는 것이기 때문에 내가 짯던 코드가 깔끔하다는 전제하에서 리팩토링을 해야한다. 그렇기 때문에 테스트 코드를 중간에 꼭 짜줘야 한다. 

 

 

출처 : 

1.  클린 코드 책, 위키 백과 SOLID 

2. [예제 출처] - 제로베이스 클린코드 인강

3. [그림 출처] - https://medium.com/backticks-tildes/the-s-o-l-i-d-principles-in-pictures-b34ce2f1e898

 

반응형