CodeGym /Java Blog /무작위의 /SOLID: 자바 클래스 설계의 다섯 가지 기본 원칙
John Squirrels
레벨 41
San Francisco

SOLID: 자바 클래스 설계의 다섯 가지 기본 원칙

무작위의 그룹에 게시되었습니다
클래스는 응용 프로그램의 빌딩 블록입니다. 건물의 벽돌처럼. 잘못 작성된 수업은 결국 문제를 일으킬 수 있습니다. SOLID: 자바 클래스 설계의 다섯 가지 기본 원칙 - 1클래스가 제대로 작성되었는지 이해하려면 클래스가 "품질 표준"에 얼마나 부합하는지 확인할 수 있습니다. Java에서는 이것이 소위 SOLID 원칙이며 이에 대해 이야기할 것입니다.

Java의 SOLID 원칙

SOLID는 OOP와 클래스 디자인의 처음 5가지 원칙의 대문자로 구성된 약어입니다. 이 원칙은 2000년대 초반에 Robert Martin에 의해 표현되었으며, 이후 Michael Feathers에 의해 약어가 도입되었습니다. SOLID 원칙은 다음과 같습니다.
  1. 단일 책임 원칙
  2. 개방 폐쇄 원칙
  3. Liskov 대체 원리
  4. 인터페이스 분리 원리
  5. 종속성 역전 원칙

단일 책임 원칙(SRP)

이 원칙은 클래스를 변경해야 하는 이유가 한 가지 이상이어서는 안 된다는 것입니다. 각 개체에는 클래스에 완전히 캡슐화되는 하나의 책임이 있습니다. 클래스의 모든 서비스는 이러한 책임을 지원하는 데 목적이 있습니다. 이러한 클래스는 클래스가 무엇인지, 책임지지 않는지가 명확하기 때문에 필요한 경우 항상 쉽게 수정할 수 있습니다. 즉, 우리는 변경을 할 수 있고 결과, 즉 다른 개체에 대한 영향을 두려워하지 않을 것입니다. 또한 이러한 코드는 테스트하기가 훨씬 쉽습니다. 테스트가 다른 모든 기능과 격리된 한 가지 기능을 다루기 때문입니다. 주문을 처리하는 모듈을 상상해 보십시오. 주문이 올바르게 형성되면 이 모듈은 주문을 데이터베이스에 저장하고 주문을 확인하기 위해 이메일을 보냅니다.

public class OrderProcessor {

    public void process(Order order){
        if (order.isValid() && save(order)) {
            sendConfirmationEmail(order);
        }
    }

    private boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }

    private void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}
이 모듈은 세 가지 이유로 변경될 수 있습니다. 첫째, 주문 처리 논리가 변경될 수 있습니다. 둘째, 주문이 저장되는 방식(데이터베이스 유형)이 변경될 수 있습니다. 셋째, 확인을 보내는 방법이 변경될 수 있습니다(예: 이메일이 아닌 문자 메시지를 보내야 한다고 가정). 단일 책임 원칙은 이 문제의 세 가지 측면이 실제로 세 가지 다른 책임임을 의미합니다. 즉, 서로 다른 클래스 또는 모듈에 있어야 합니다. 다른 시간에 다른 이유로 변경될 수 있는 여러 엔터티를 결합하는 것은 잘못된 설계 결정으로 간주됩니다. 모듈을 각각 단일 기능을 수행하는 세 개의 개별 모듈로 분할하는 것이 훨씬 좋습니다.

public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}

public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}

개방 폐쇄 원칙(OCP)

이 원칙은 다음과 같이 설명됩니다. 소프트웨어 엔터티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하지만 수정에는 닫혀 있어야 합니다 . 즉, 클래스의 기존 코드를 변경하지 않고도 클래스의 외부 동작을 변경할 수 있어야 합니다. 이 원칙에 따라 클래스는 특정 조건에 맞게 클래스를 조정하려면 클래스를 확장하고 일부 기능을 재정의하면 됩니다. 이는 시스템이 유연해야 하고 소스 코드를 변경하지 않고도 변화하는 조건에서 작업할 수 있어야 함을 의미합니다. 주문 처리와 관련된 예제를 계속 진행하면서 주문이 처리되기 전과 확인 이메일이 전송된 후에 몇 가지 작업을 수행해야 한다고 가정합니다. 변경하는 대신OrderProcessor개방형 폐쇄 원칙을 위반하지 않고 목표를 달성하기 위해 클래스 자체를 확장할 것입니다.

public class OrderProcessorWithPreAndPostProcessing extends OrderProcessor {

    @Override
    public void process(Order order) {
        beforeProcessing();
        super.process(order);
        afterProcessing();
    }
    
    private void beforeProcessing() {
        // Take some action before processing the order
    }
    
    private void afterProcessing() {
        // Take some action after processing the order
    }
}

Liskov 치환 원리(LSP)

이것은 앞에서 언급한 개방 폐쇄 원칙의 변형입니다. 다음과 같이 정의할 수 있습니다. 개체는 프로그램의 속성을 변경하지 않고 하위 클래스의 개체로 대체될 수 있습니다. 즉, 기본 클래스를 확장하여 만든 클래스는 클라이언트의 관점에서 기능이 손상되지 않도록 해당 메서드를 재정의해야 합니다. 즉, 개발자가 클래스를 확장하여 응용 프로그램에서 사용하는 경우 재정의된 메서드의 예상 동작을 변경해서는 안 됩니다. 하위 클래스는 클라이언트의 관점에서 기능이 손상되지 않도록 기본 클래스의 메서드를 재정의해야 합니다. 다음 예에서 자세히 살펴볼 수 있습니다. 주문의 유효성을 검사하고 주문에 포함된 모든 상품의 재고가 있는지 확인하는 클래스가 있다고 가정합니다.isValid()true 또는 false를 반환하는 메서드 :

public class OrderStockValidator {

    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if (!item.isInStock()) {
                return false;
            }
        }

        return true;
    }
}
예를 들어 일부 주문의 경우 주문에 포함된 모든 상품의 재고가 있는지, 모든 상품이 포장되어 있는지 확인해야 하는 경우와 같이 일부 주문을 다른 주문과 다르게 검증해야 한다고 가정합니다. 이를 위해 OrderStockValidator클래스를 생성하여 클래스를 확장합니다 OrderStockAndPackValidator.

public class OrderStockAndPackValidator extends OrderStockValidator {

    @Override
    public boolean isValid(Order order) {
        for (Item item : order.getItems()) {
            if ( !item.isInStock() || !item.isPacked() ){
                throw new IllegalStateException(
                     String.format("Order %d is not valid!", order.getId())
                );
            }
        }

        return true;
    }
}
그러나 여기서 우리는 Liskov 대체 원칙을 위반했습니다. 왜냐하면 주문이 유효성 검사에 실패하면 false를 반환하는 대신 우리 메서드가 IllegalStateException. 이 코드를 사용하는 클라이언트는 이것을 기대하지 않습니다. true 또는 false 반환 값을 기대합니다 . 이로 인해 런타임 오류가 발생할 수 있습니다.

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

이 원칙의 특징은 다음과 같습니다. 클라이언트는 사용하지 않을 메서드를 강제로 구현해서는 안 됩니다 . 인터페이스 분리 원칙은 너무 "두꺼운" 인터페이스는 더 작고 더 구체적인 인터페이스로 나누어야 하므로 작은 인터페이스를 사용하는 클라이언트는 작업에 필요한 메서드만 알 수 있습니다. 결과적으로 인터페이스 메서드가 변경되면 해당 메서드를 사용하지 않는 클라이언트는 변경되지 않아야 합니다. 다음 예를 고려하십시오. 개발자인 Alex는 "보고서" 인터페이스를 만들고 두 가지 메서드를 추가했습니다 generateExcel().generatedPdf(). 이제 클라이언트는 이 인터페이스를 사용하려고 하지만 Excel이 아닌 PDF 형식의 보고서만 사용하려고 합니다. 이 기능이 이 고객을 만족시킬까요? 아니요. 클라이언트는 두 가지 방법을 구현해야 합니다. 그 중 하나는 크게 필요하지 않으며 소프트웨어를 설계한 Alex 덕분에 존재합니다. 클라이언트는 다른 인터페이스를 사용하거나 Excel 보고서에 대한 방법으로 아무 작업도 수행하지 않습니다. 그래서 해결책은 무엇입니까? 기존 인터페이스를 두 개의 작은 인터페이스로 분할하는 것입니다. 하나는 PDF 보고서용이고 다른 하나는 Excel 보고서용입니다. 이를 통해 클라이언트는 필요한 기능만 사용할 수 있습니다.

종속성 역전 원칙(DIP)

Java에서 이 SOLID 원칙은 다음과 같이 설명됩니다. 시스템 내의 종속성은 추상화를 기반으로 구축됩니다.. 상위 수준 모듈은 하위 수준 모듈에 의존하지 않습니다. 추상화는 세부 사항에 의존해서는 안 됩니다. 세부 사항은 추상화에 따라 달라집니다. 다양한 모듈이 독립적이고 추상화를 통해 서로 연결되도록 소프트웨어를 설계해야 합니다. 이 원칙의 고전적인 적용은 Spring Framework입니다. Spring Framework에서 모든 모듈은 함께 작동할 수 있는 별도의 구성 요소로 구현됩니다. 그들은 매우 자율적이어서 Spring Framework 이외의 프로그램 모듈에서도 쉽게 사용할 수 있습니다. 이것은 닫힌 원칙과 열린 원칙의 의존성 덕분에 달성됩니다. 모든 모듈은 다른 모듈에서 사용할 수 있는 추상화에 대한 액세스만 제공합니다. 예를 사용하여 이를 설명하려고 합니다. 단일 책임 원칙에 대해 말하면서 우리는 다음을 고려했습니다.OrderProcessor수업. 이 클래스의 코드를 다시 살펴보겠습니다.

public class OrderProcessor {
    public void process(Order order){

        MySQLOrderRepository repository = new MySQLOrderRepository();
        ConfirmationEmailSender mailSender = new ConfirmationEmailSender();

        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }

}
이 예에서 우리 OrderProcessor클래스는 두 개의 특정 클래스인 MySQLOrderRepository및 에 의존합니다 ConfirmationEmailSender. 또한 다음 클래스의 코드도 제공합니다.

public class MySQLOrderRepository {
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}

public class ConfirmationEmailSender {
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }
}
이러한 클래스는 우리가 추상화라고 부르는 것과는 거리가 멉니다. 그리고 종속성 역전 원칙의 관점에서 보면 구체적인 구현보다는 앞으로 작업할 수 있는 몇 가지 추상화를 만드는 것부터 시작하는 것이 좋습니다. 두 개의 인터페이스를 만들어 보겠습니다. MailSenderOrderRepository). 다음은 우리의 추상화입니다.

public interface MailSender {
    void sendConfirmationEmail(Order order);
}

public interface OrderRepository {
    boolean save(Order order);
}
이제 이를 위해 이미 준비된 클래스에서 이러한 인터페이스를 구현합니다.

public class ConfirmationEmailSender implements MailSender {

    @Override
    public void sendConfirmationEmail(Order order) {
        String name = order.getCustomerName();
        String email = order.getCustomerEmail();

        // Send an email to the customer
    }

}

public class MySQLOrderRepository implements OrderRepository {

    @Override
    public boolean save(Order order) {
        MySqlConnection connection = new MySqlConnection("database.url");
        // Save the order in the database

        return true;
    }
}
OrderProcessor우리는 수업이 구체적인 세부 사항이 아니라 추상화에 의존하도록 준비 작업을 했습니다 . 클래스 생성자에 종속성을 추가하여 변경합니다.

public class OrderProcessor {

    private MailSender mailSender;
    private OrderRepository repository;

    public OrderProcessor(MailSender mailSender, OrderRepository repository) {
        this.mailSender = mailSender;
        this.repository = repository;
    }

    public void process(Order order){
        if (order.isValid() && repository.save(order)) {
            mailSender.sendConfirmationEmail(order);
        }
    }
}
이제 우리 클래스는 특정 구현이 아닌 추상화에 의존합니다. 개체가 생성 될 때 원하는 종속성을 추가하여 동작을 쉽게 변경할 수 있습니다 OrderProcessor. Java에서 SOLID 설계 원칙을 살펴보았습니다. CodeGym 과정에서 일반적인 OOP와 Java 프로그래밍의 기본 사항(지루하지 않고 수백 시간의 연습)에 대해 자세히 배우게 됩니다. 몇 가지 작업을 해결할 시간입니다 :)
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION