simDev1234
심플하고 차분하게
simDev1234
전체 방문자
오늘
어제
  • 분류 전체보기
    • Computer Science
      • Basic Math
      • Data Structure
      • Algorithm
      • Database
      • OS
    • Language
      • Java
      • Kotlin
      • SQL
    • Framework
      • Spring
      • Orm&Mapper
      • 프로젝트로 스프링 이해하기
      • 스프링 라이브러리
    • Infra
      • Cloud
      • Docker
      • Redis
      • AWS, Azure
      • Device
    • Etc
      • CleanCoding
    • Git,Github

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 자바프로그래밍
  • 자바메모리구조
  • 스프링
  • 자바프로그램
  • 참조변수
  • 참조타입
  • scanner #next() #nextLine()
  • null
  • JVM메모리구조
  • controllerTest
  • 404
  • 자바
  • 컨트롤러

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
simDev1234

심플하고 차분하게

Language/Java

SOLID 원칙

2022. 8. 29. 15:58

|  OOP의 핵심

[ 지난 번 OOP 관련 포스팅 내용 요약 ]
- OOP는 객체 간 상호 관계에 포커스를 맞춘 방법론으로, 추상화, 상속, 다형성, 캡슐화 4가지의 주요 특징이 있다.
- 추상화는 모델링이고, 상속은 확장과 재사용성이며, 다형성은 사용 편의, 캡슐화는 정보 은닉에 해당된다.
- 추상화 단계에서는 클래스 내 응집도를 높이는 게 필요하고, 상속 시에는 클래스 간 결합도에 주의해야 한다.

- OOP는 풀면 참 복잡했는데, 오늘 배운 수업에서는 분류와 교체라는 단어로 그 핵심을 단순화해 설명했다.

* 현업에 오래 종사했던 강사분은 스파게티 소스에 대해 언급하며,

  소프트웨어를 유연하게 만들려면 객체지향 방식이 유용하다 얘기주셨다.

분류한다 코드를 적절히 잘 분류한다. -> 클래스
교체한다 필요에 따라서 특정 모듈을 통째로 교체하기도 한다. -> DBMS 업체나 라이브러리를 통째로 교체하기도 한다.

※ 라이브러리와 프레임워크와 아키텍처 간의 상관관계 바로가기

 

|  SOLID 원칙

- 로버트 C. 마틴에 의해 만들어진 원칙

SRP 단일 책임 원칙(분류)    Simple Responsibility principle
OCP 개방 폐쇄 원칙(교체) Open/Closed principle
LSP 리스코프 치환 법칙(교체) Liskov subsituation principle
ISP 인터페이스 분리 원칙(분류) Interface segregation principle
DIP 의존성 역전 원칙(교체) Dependency inversion principle

 

※ 참조 : 실제 현장에서 사용하는 코드는 아래와 달라 보입니다. 원리를 이해하기 위한 코드로 봐주시면 감사하겠습니다.

 

1. SRP : 한 클래스는 단일의 책임만 가져야 한다.

 

SRP가 지켜지지 않은 코드 

class PaymentService{
    public void pay(Customer customer, Product product) {

        if (!isValidatedPrice(customer, product)) {
            throw new IllegalArgumentException("charge your money");
        }

        customer.setMoney(customer.getMoney() - product.getPrice());
    }

    public boolean isValidatedPrice(Customer customer, Product product) {
        if (customer.getMoney() < product.getPrice()) {
            return false;
        }

        return true;
    }

    public void sendSMS(Customer customer, Product product) {
        System.out.println("phone number : " + customer.getPhone());
        System.out.println("상품 " + product.getName() + "(" 
                          + product.getProduct_no() + ")이 결제되었습니다.");
    }
}

- 고객이 상품을 주문할 때, PaymentService라는 클래스에서 아래와 같이 세가지의 기능을 제공한다고 할 때에

  (1) 결제하기 - pay()

  (2) 결제가능 여부 확인하기 - isValidatedPrice()

  (3) 결제완료 SMS(메세지) 전송하기 - sendTMS()

- 이 세가지 기능은 엄밀히 말하면 서로 다른 기능이라 볼 수 있다. 

- 전체 scope이 작은 경우에는 세가지 기능이 묶일 수도 있겠지만, 만약 기능이 더 복잡해지고 확장된다면 세가지를 분리할 수 있다.

 

클래스를 분리

class PaymentService{
    public void pay(Customer customer, Product product) {

        if (!PaymentValidation.isValidatedPrice(customer, product)) {
            throw new IllegalArgumentException("charge your money");
        }

        customer.setMoney(customer.getMoney() - product.getPrice());
    }
}
class PaymentValidation{
    public static boolean isValidatedPrice(Customer customer, Product product) {
        if (customer.getMoney() < product.getPrice()) {
            return false;
        }

        return true;
    }
}
class SMSService{
    public void sendSMS(Customer customer, Product product) {
        System.out.println("phone number : " + customer.getPhone());
        System.out.println("상품 " + product.getName() + "(" 
                           + product.getProduct_no() + ")이 결제되었습니다.");
    }
}

 

2. OCP : 확장에는 열려있고, 변경에는 닫혀있다.

* if-else의 반복적인 케이스가 보일 때에는 클래스를 분리하는 것이 효과적일 수 있다.

 

OCP가 지켜지지 않은 코드

class PointService {
    public double getPointRate(String category) {
        // 구매 카테고리별로 포인트 비율을 다르게 지급
        if (category.equals("옷")){
            return 0.05;
        } else if (category.equals("잡화")) {
            return 0.02;
        } else if (category.equals("기타")) {
            return 0.01;
        }

        return 0;
    }

    public double getVIPRate(String category){
        // 구매 카테고리 별로 VIP 여부에 따라서 비율을 다르게 지급
        if (category.equals("잡화")){
            return 0.12;
        } else if (category.equals("도서")) {
            return 0.08;
        } else if (category.equals("기타")) {
            return 0.01;
        }

        return 0;
    }
}

- 만약에 포인트 제도가 추가되어, 구매 카테고리 별로 서로 다르게 포인트를 지정한다고 하자.

- 이럴때 카테고리가 위와 같이 한정적이라면 상관이 없겠지만 카테고리가 계속 늘어난다면 문제가 발생할 수 있다.

- 더불어 getPoint()와 getVIPRate()의 카테고리명과 순서가 다른 걸 볼 수 있다.

- 이런 경우 로직이 통일적이지 않아지기 때문에 결과적으로 문제가 발생할 수 있다.

 

인터페이스를 통한 OCP 확보

interface PointServiceIF{
    public abstract double getPointRate(String category);
    public abstract double getVIPRate(String category);
}

class Clothe implements PointServiceIF{

    @Override
    public double getPointRate(String category) {
        return 0.05;
    }

    @Override
    public double getVIPRate(String category) {
        return 0.00;
    }
}

class Book implements PointServiceIF{

    @Override
    public double getPointRate(String category) {
        return 0.12;
    }

    @Override
    public double getVIPRate(String category) {
        return 0.25;
    }
}

- 인터페이스를 통해서 메소드를 프로토타입으로 만들어 준 후에, 각각의 카테고리를 클래스로 만들어 메소드를 구현하면 OCP를 확보할 수 있다.

 

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

- 이 부분은 상위 클래스(또는 인터페이스)의 기능과 동일한 기능을 하위 클래스가 상속(또는 구현)하는 가를 말한다.

상속 is kind of  하위 클래스는 상위 클래스의 한 분류이다.
구현 is able to 하위 클래스는 상위 인터페이스를 구현할 수 있다.

- 여기서는 예제를 만들지 않고 대신 예제 링크만 가져왔다.

https://blog.itcode.dev/posts/2021/08/15/liskov-subsitution-principle

- 위 예제에서는, 직사각형 > 정사각형으로 LSP를 설명하는데, 정사각형은 직사각형의 메소드를 모두 동일하게 가져가지 않기 때문에, 사각형 클래스를 만들고 직사각형과 정사각형이 그것을 상속받게 했다.

더보기

- 실무에서는 상속을 많이 사용하지 않는다고 한다.

왜냐하면, 오버라이딩을 했는지 안 했는지 매번 확인하기가 어렵고,

오버라이딩 잘못하면 로직 충돌 (Fragile base class문제)이 일어나기도 하며, 

기능을 너무 확장하거나 변경하면 재활용성이 낮아지기 때문이라고 한다.

 

- 그래서 상속의 대안 및 상속을 잘하기 위해서는

(1) 상위 클래스를 한 클래스만 상속하거나

(2) 클래스 상속이 아닌, 인터페이스로 대체하거나

(3) 상위 클래스와 상호 치환(=동일 기능을 갖도록)이 가능하도록 상속을 해야한다. 

 

4. ISP : 인터페이스도 단일 책임을 갖도록 분리해야 한다.

* 인터페이스를 너무 크게 만들면, 사용하지 않는 메소드들도 구현하도록 해야 한다.

* 실무에서 보면 단일 메소드를 가진 인터페이스들이 있다. > 세분화된 인터페이스로 쪼개서 단일 책임을 갖게 한 것

 

- 이 부분은 별도로 내용을 추가하지 않고, 바로 앞의 예시로 이야기하겠다.

interface PointServiceIF{
    public abstract double getPointRate(String category);
    public abstract double getVIPRate(String category);
}

class Clothe implements PointServiceIF{

    @Override
    public double getPointRate(String category) {
        return 0.05;
    }

    @Override
    public double getVIPRate(String category) {
        return 0.00;
    }
}

class Book implements PointServiceIF{

    @Override
    public double getPointRate(String category) {
        return 0.12;
    }

    @Override
    public double getVIPRate(String category) {
        return 0.25;
    }
}

- 바로 위의 코드를 보면 "옷"의 경우 안 쓰는 메소드인 getVIPRate()를 구태어 어거지로 구현한 걸 볼 수 있는데,

  이유는 인터페이스 안에 메소드를 두 개를 넣어 두어서 두 가지를 모두 구현해야 하기 때문이다.

- 이렇듯 상속은 주의해서 사용하는 게 좋다. 필요하지 않은 메소드까지 구현해야할 수 있고,

  너무 많이 확장되고 재사용되다보면 최초에 목적하고 있던 기능에서 많이 동떨어진 메소드가 만들어질 수 있기 때문이다.

- 이러한 문제를 해결하기 위해서는 단일 인터페이스, 곧 단일 메소드를 가진 인터페이스를 만드는 것이 좋다.

interface GeneralPointServiceIF{
	public abstract double getPointRate(String category);
}

interface VIPPointServiceIF{
	public abstract double getVIPRate(String category);
}

class Clothe implements GeneralPointServiceIF{
    @Override
    public double getPointRate(String category) {
        return 0.05;
    }
}

class Book implements GeneralPointServiceIF, VIPPointServiceIF{
    @Override
    public double getPointRate(String category) {
        return 0.12;
    }

    @Override
    public double getVIPRate(String category) {
        return 0.25;
    }
}

 

5. DIP : 하위 모듈의 변경이 상위 모듈의 변경을 요구하는 의존성을 끊어내야 한다.

- 개발을 하다보면 보안상의 이슈가 발생한 라이브러리를 다른 라이브러리로 교체해야할 때가 있다고 한다.

- 또는 외주업체에 맡겨 서비스를 추가하는 경우도 있는데, 업체를 변경할 때에도 기존 서비스를 변경해야할 때가 있다고 한다.

- 만약 호텔에 Deposit 금액으로 20만원을 넣어두고, 호텔의 서비스를 사용할 때마다 deposit 금액을 차감시킨다고 하자.

[1] 호텔 비용 정산 클래스 - HotelPaymentService

[2] 고객 deposit에 연결하는 클래스 - HotelDepositAdapter

class HotelPaymentService{
    String pay(Customer customer, ServiceRequest serviceRequest){
    	HotelDepositAdapter hotelDepositAdapter = new HotelDepositAdapter();
        int payResult = hotelDepositAdapter.useService(customer, serviceRequest);
    	
        if (payResult == -1){
        	return "Fail";
        }
        
        return "Success : left deposit - " + payResult + "(원)"; 
    }
}

class HotelDepositAdapter{  
    int useService(Customer customer, ServiceRequest serviceRequest){
    	int possibleDeposit = customer.getDeposit() - serviceRequest.getServicePrice();
        
        if (possibleDeposit < 0) {
        	return -1;
        }
        
        return possibleDeposit;
    }
}

- 이 상태에서, 만약 호텔측에서 Deposit 금액 외에 카드를 맡기면 추가적으로 결제가 가능하게 해달라는 요청을 한다면

[1] 호텔 비용 정산 클래스 - HotelPaymentService

[2] 고객 deposit에 연결하는 클래스 - HotelDepositAdapter

[3] 고객 card에 연결하는 클래스 - HotelCardAdapter

- 위와 같이 card를 연결하는 어뎁터 클래스를 하나 더 추가하게 될 수 있다.

- 이런 경우 HotelPaymentService이 이미 HotelDepositAdapter에 의존적이라 코드를 전반적으로 수정해주어야 한다.

- 앞으로 비용 정산 방식이 추가적으로 바뀌게 되면 이 방식은 비효율적이다

class HotelPaymentService{
    String pay(Customer customer, ServiceRequest serviceRequest, PaymentType type){
    	
        if (type.getType() < 0 || type.getType() >= 2) {
        	return "PaymentRequest Error : write correct PaymentType";
        }
        
        int payResult = -1;
        
        if (type.getType() == 0){
        	HotelDepositAdapter hotelDepositAdapter = new HotelDepositAdapter();
        	payResult = hotelDepositAdapter.useService(customer, serviceRequest);
        } else if (type.getType() == 1){
        	HotelCardAdapter hotelCardAdapter = new HotelCardAdapter();
        	payResult = hotelCardAdapter.useService(customer, serviceRequest);
        } 
    	
        if (payResult == -1){
        	return "Fail";
        }
        
        return "Success : left deposit - " + payResult + "(원)"; 
    }
}

- 이런 경우 아래와 같이 어뎁터 자체가 인터페이스를 구현하도록 하고,

  HotelPaymentService가 인터페이스를 바라보도록 하면 해결된다.

enum HotelPaymentType{
    DEPOSIT, CARD
}

class HotelPaymentService{

    private final HotelDepositAdapter hotelDepositAdapter = new HotelDepositAdapter();
    private final HotelCardAdapter hotelCardAdapter = new HotelCardAdapter();

    String pay(Customer customer, ServiceRequest serviceRequest){
    
    	HotelPaymentIF hotelPaymentIF;
        
        if (serviceRequest.getPaymentType() == HotelPaymentType.DEPOSIT){
        	hotelPaymentIF = hotelDepositAdapter;
        } else {
        	hotelPaymentIF = hotelCardAdapter;
        }
    
    	int payResult = hotelPaymentIF.useService(customer, serviceRequest);
    	
        if (payResult == -1){
        	return "Fail";
        }
        
        return "Success : left deposit - " + payResult + "(원)"; 
    }
}

interface HotelPaymentIF{
	int useService(Customer customer, ServiceRequest serviceRequest);
}

class HotelDepositAdapter implements HotelPaymentIF{  
    int useService(Customer customer, ServiceRequest serviceRequest){
    	int possibleDeposit = customer.getDeposit() - serviceRequest.getServicePrice();
        
        if (possibleDeposit < 0) {
        	return -1;
        }
        
        return possibleDeposit;
    }
}

class HotelCardAdapter implements HotelPaymentIF{  
    int useService(Customer customer, ServiceRequest serviceRequest){
    	if (CardService.useCard(customer, serviceRequest) < 0) {
        	return -1;
        }
        
        return serviceRequest.getServicePrice();
    }
}

 

 

[ 출처 및 참조 ]

부트캠프 강의를 들은 후 정리한 내용입니다.

[스프링 입문을 위한 자바 객체지향의 원리와 이해]

SOLID https://bottom-to-top.tistory.com/27

위키백과 https://ko.wikipedia.org/wiki/SOLID_(%EA%B0%9D%EC%B2%B4_%EC%A7%80%ED%96%A5_%EC%84%A4%EA%B3%84) 

 

'Language > Java' 카테고리의 다른 글

JAVA 유용한 타입 - Enum  (0) 2022.09.15
JAVA 라이브러리 - Optional<T> 클래스  (0) 2022.09.15
JAVA 라이브러리 - 컬렉션  (0) 2022.08.11
JAVA 라이브러리 - 제네릭 클래스  (0) 2022.08.10
JAVA 라이브러리 - 날짜, 시간 관련 클래스(java.util/java.time..)  (0) 2022.08.10
    'Language/Java' 카테고리의 다른 글
    • JAVA 유용한 타입 - Enum
    • JAVA 라이브러리 - Optional<T> 클래스
    • JAVA 라이브러리 - 컬렉션
    • JAVA 라이브러리 - 제네릭 클래스
    simDev1234
    simDev1234
    TIL용 블로그. * 저작권 이슈가 있는 부분이 있다면 댓글 부탁드립니다.

    티스토리툴바