SOLID 디자인 원칙은 깔끔한 코드를 작성하기 위해 알아야 할 가장 중요한 디자인 원칙입니다. SOLID 원칙에 대한 확실한 명령을 갖는 것은 모든 프로그래머에게 없어서는 안 될 기술입니다. 이는 다른 디자인 패턴이 개발되는 기초입니다. 이 기사에서는 실제 사례를 사용하여 SOLID 설계 원칙을 다루고 그 중요성을 이해합니다. 다형성, 추상화, 상속과 함께 SOLID 원칙은 목표 지향 프로그래밍을 잘하는 데 정말 중요합니다. SOLID 원칙이 중요한 이유는 무엇입니까? SOLID 원칙은 여러 가지 이유로 중요합니다. SOLID 원칙을 사용하면 깔끔하고 유지 관리 가능한 코드를 작성할 수 있습니다. 새 프로젝트를 시작할 때 구현하는 기능 세트가 제한되어 있으므로 처음에는 코드 품질이 좋습니다. 그러나 더 많은 기능을 통합할수록 코드가 복잡해지기 시작합니다. SOLID 원칙은 추상화, 다형성 및 상속의 기초를 기반으로 하며 일반적인 사용 사례에 대한 디자인 패턴으로 이어집니다. 이러한 디자인 패턴을 이해하면 프로그래밍에서 일반적인 사용 사례를 구현하는 데 도움이 됩니다. SOLID 원칙은 코드의 테스트 가능성을 향상시키는 깔끔한 코드를 작성하는 데 도움이 됩니다. 이는 코드가 모듈식이고 느슨하게 결합되어 있기 때문입니다. 각 모듈은 독립적으로 개발되고 독립적으로 테스트될 수 있습니다. 이제 실제 사례를 통해 각 SOLID 원칙을 자세히 살펴보겠습니다. 1. 단일 책임 원칙 SOLID 원칙의 S는 단일 책임 원칙을 나타냅니다. 단일 책임 원칙은 클래스가 변경해야 하는 단 하나의 이유만 가져야 한다고 명시합니다. 이는 프로젝트에 추가 요구 사항을 통합할 때 변경해야 하는 위치의 수를 제한합니다. 각 클래스에는 변경해야 할 이유가 정확히 하나만 있어야 합니다. 예를 들어, 직불, 신용 및 와 같은 기본 작업을 허용하는 클래스가 있는 Java로 뱅킹 애플리케이션을 설계한다고 가정해 보겠습니다. 메소드는 (예: 이메일, SMS 등)이라는 열거형을 사용하여 적절한 매체와 함께 업데이트를 보냅니다. 이에 대한 코드를 아래와 같이 작성하겠습니다. sendUpdates SavingsAccount sendUpdate NotificationMedium public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void sendNotification(NotificationMedium medium) { if (medium == NotificationMedium.SMS) { // Send SMS here } else if (medium == NotificationMedium.EMAIL) { // Send Email here } } } public enum NotificationMedium { SMS, EMAIL } 이제 위의 클래스를 보면 여러 가지 이유로 변경될 수 있습니다. SavingsAccount 클래스의 핵심 로직(예: , 등)에 변경 사항이 있는 경우. SavingsAccount debit credit 은행이 새로운 알림 매체(예: WhatsApp)를 도입하기로 결정한 경우. 이는 SOLID 원칙의 단일 책임 원칙을 위반하는 것입니다. 이 문제를 해결하기 위해 알림을 보내는 별도의 클래스를 만들겠습니다. SOLID 원칙에 따라 위의 코드를 리팩터링해 보겠습니다. public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void printBalance() { System.out.println("Name: " + name+ " Account Balance: " + balance); } public void sendNotification(Medium medium) { Sender.sendNotification(medium, this); } } public enum NotificationMedium { SMS, EMAIL } public class Sender { public static void sendNotification(NotificationMedium medium, SavingsAccount account) { // extract account data from the account object if (medium == NotificationMedium.SMS) { //logic to send SMS here } else if (medium == NotificationMedium.EMAIL) { // logic to send Email here } } } 이제 코드를 리팩터링했으므로 이나 형식이 변경되면 클래스를 변경하겠습니다. 그러나 의 핵심 로직이 변경되면 클래스도 변경됩니다. NotificationMedium Sender SavingsAccount SavingsAccount 이는 첫 번째 예에서 관찰한 위반 사항을 수정합니다. 2. 개방/폐쇄 원칙 Open Close 원칙은 클래스가 확장(추가 기능을 추가하는 경우)에는 열려 있지만 수정에는 닫혀 있도록 클래스를 디자인해야 한다고 명시합니다. 수정을 위해 폐쇄되면 다음 두 가지 면에서 도움이 됩니다. 많은 경우 원래 클래스 소스를 사용하지 못할 수도 있습니다. 프로젝트에서 사용되는 종속성일 수 있습니다. 원래 클래스를 변경하지 않고 유지하면 버그가 발생할 가능성이 줄어듭니다. 수정하려는 클래스에 종속된 다른 클래스가 있을 수 있기 때문입니다. Open Close 원칙의 예를 보려면 전자상거래 웹사이트에서 구현된 것과 같은 장바구니의 예를 살펴보겠습니다. 추가할 수 있는 목록이 포함된 라는 클래스를 만들겠습니다. 항목 유형과 그에 대한 과세에 따라 클래스 내에서 총 장바구니 값을 계산하는 메서드를 만들고 싶습니다. Item Cart Cart 수업은 연장을 위해 열려야 하고 수정을 위해 닫혀야 합니다. import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart() { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { if (item.getItemType() == GIFT) { // 8% tax on gift + 2% gift wrap cost value += (item.getValue()*1.08) + item.getValue()*0.02 } else if (item.getItemType() == ItemType.ELECTRONIC_ITEM) { value += (item.getValue()*1.11); } else { value += item.getValue()*1.10; } } return value; } } @Getter @Setter public abstract class Item { protected double price; private ItemType itemType; public double getValue() { return price; } } public enum ItemType { ELECTRONIC, GIFT } 위의 예에서 메소드는 카트 내부의 모든 항목을 반복하고 항목 유형에 따라 로직을 호출하여 카트 값을 계산합니다. calculateCartValue 이 코드는 정확해 보이지만 SOLID 원칙을 위반합니다. 장바구니 값을 계산하는 동안 다른 유형의 항목(예: 식료품)에 대한 새 규칙을 추가해야 한다고 가정해 보겠습니다. 이 경우 원래 클래스인 수정하고 그 안에 유형의 항목을 확인하는 또 다른 조건을 작성해야 합니다. Cart Grocery else if 그러나 리팩토링을 거의 하지 않으면 코드가 열기/닫기 원칙을 준수하도록 만들 수 있습니다. 방법을 살펴보겠습니다. 먼저, 클래스를 추상화하고 아래와 같이 다양한 유형의 에 대한 구체적인 클래스를 만듭니다. Item Item public abstract class Item { protected double price; public double getValue() { return price; } } public class ElectronicItem extends Item { public ElectronicItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.11; } } public class GiftItem extends Item { public GiftItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.08 + super.getValue()*0.02; } } public class GroceryItem extends Item { public GroceryItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.03; } } , 및 과 같은 각 구체적인 항목 클래스 내에는 과세 및 가치 계산을 위한 비즈니스 논리가 포함된 메서드를 구현합니다. GroceryItem GiftItem ElectronicItem getValue() 이제 아래와 같이 클래스를 추상 클래스 에 종속시키고 각 항목에 대해 메서드를 호출하겠습니다. Cart Item getValue() import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } } 이제 이 리팩터링된 코드에서는 새로운 유형의 도입되더라도 클래스는 변경되지 않습니다. 다형성으로 인해 내부에 있는 의 실제 유형이 무엇이든 해당 클래스의 메서드가 호출됩니다. Item Cart items ArrayList Item getValue() 3. 리스코프의 대체 원리 Liskov의 대체 원칙은 주어진 코드에서 상위 클래스의 객체를 하위 클래스의 객체로 대체하더라도 코드가 중단되어서는 안 된다는 것입니다. 즉, 하위 클래스가 상위 클래스를 상속하고 해당 메서드를 재정의할 때 상위 클래스의 메서드 동작과 일관성을 유지해야 합니다. 예를 들어 다음 클래스와 및 클래스 두 개를 만든다고 가정해 보겠습니다. 이제 Vehicle 클래스 내에 이라는 메서드를 생성한다고 가정해 보겠습니다. 이 메서드는 클래스에서 재정의될 수 있지만 에는 엔진이 없으므로 클래스에서는 지원되지 않습니다(아래 코드 샘플 참조). Vehicle Car Bicycle startEngine() Car Bicycle Bicycle 하위 클래스는 메서드를 재정의할 때 상위 클래스의 동작과 일관성을 유지해야 합니다. class Vehicle { public void startEngine() { // start engine of the vehicle } } class Car extends Vehicle { @Override public void startEngine() { // Start Engine } } class Bicycle extends Vehicle { @Override public void startEngine(){ throw new UnsupportedOperationException("Bicycle doesn't have engine"); } } 이제 vehicle 유형의 객체를 기대하고 메서드에 의존하는 일부 코드가 있다고 가정해 보겠습니다. 유형의 객체를 전달하는 대신 해당 코드 조각을 호출하는 동안 객체를 전달하면 코드에 문제가 발생할 수 있습니다. (s) 클래스의 메소드는 메소드가 호출될 때 예외를 발생시킵니다. 이는 SOLID 원칙(Liskov의 대체 원칙)을 위반하는 것입니다. startEngine() Vehicle Bicycle Bicycle startEngine() 이 문제를 해결하기 위해 과 두 클래스를 만들고 클래스에서 상속되고 에서 상속되도록 할 수 있습니다. MotorizedVehicle NonMotorizedVehicle Car MotorizedVehicle Bicycle NonMotorizedVehicle class Vehicle { } class MotorizedVehicle extends Vehicle { public void startEngine() { // start engine here } } class Car extends MotorizedVehicle { @Override public void startEngine() { // Start Engine } } class NonMotorizedVehicle extends Vehicle { public void startRiding() { // Start without engine } } class Bicycle extends NonMotorizedVehicle { @Override public void startRiding(){ // Start riding without the engine. } } 4. 인터페이스 분리 원칙 SOLID 원칙의 "I"는 인터페이스 분리 원칙을 나타냅니다. 인터페이스 분리 원칙은 구현 클래스가 사용되지 않는 메소드를 구현하도록 강제하는 더 큰 인터페이스를 갖기보다는 더 작은 인터페이스를 갖고 클래스를 구현해야 한다고 명시합니다. 이런 방식으로 클래스는 관련 메서드만 구현하고 깔끔하게 유지됩니다. 하나의 큰 인터페이스가 아닌 여러 개의 작은 인터페이스로 인터페이스를 나눕니다. 예를 들어 Java에 내장된 컬렉션 프레임워크를 살펴보겠습니다. 다른 데이터 구조 중에서 Java는 및 데이터 구조도 제공합니다. LinkedList ArrayList 클래스는 , , , , 및 인터페이스를 구현합니다. ArrayList Serializable Cloneable Iterable Collection List RandomAccess 클래스는 , , , , , 및 구현합니다. LinkedList Serializable Cloneable Iterable Collection Deque List Queue 정말 많은 인터페이스가 있습니다! Java 개발자는 너무 많은 인터페이스를 사용하는 대신 , , , , 및 하나의 인터페이스, 즉 인터페이스로 결합할 수 있었습니다. 이제 및 클래스 모두 이 새로운 인터페이스를 구현할 수 있었습니다. Serializable Cloneable Iterable Collecton List RandomAccess IList ArrayList LinkedList IList 그러나 임의 액세스를 지원하지 않기 때문에 인터페이스에서 메서드를 구현했을 수 있으며 누군가 호출을 시도할 때 발생했을 수 있습니다. LinkedList RandomAccess UnsupportedOperationException 그러나 이는 필요하지 않더라도 LinkedList 클래스가 인터페이스 내부에 메서드를 구현하도록 "강제"하므로 SOLID 원칙의 인터페이스 분리 원칙을 위반하는 것입니다. RandomAccess 따라서 공통 동작에 따라 인터페이스를 분할하고 각 클래스가 하나의 큰 인터페이스보다는 여러 개의 인터페이스를 구현하도록 하는 것이 좋습니다. 종속성 반전 원리 종속성 역전 원칙은 상위 수준의 클래스가 하위 수준의 클래스에 직접 종속되어서는 안 된다는 것을 명시합니다. 이로 인해 두 레벨 간에 긴밀한 결합이 발생합니다. 그 대신 하위 클래스는 상위 클래스가 의존해야 하는 인터페이스를 제공해야 합니다. 클래스보다는 인터페이스에 의존 예를 들어, 위에서 본 예제를 계속해서 몇 가지 결제 옵션을 추가하도록 개선해 보겠습니다. 와 에 두 가지 유형의 결제 옵션이 있다고 가정해 보겠습니다. 이제 클래스에서 장바구니 값을 계산하고 제공된 결제에 따라 결제를 시작하는 메서드를 추가하려고 합니다. 방법. Cart DebitCard Paypal Cart placeOrder 이를 위해 클래스 내부에 두 가지 결제 옵션을 필드로 추가하여 위의 예제에 종속성을 추가할 수 있었습니다. 그러나 이렇게 하면 클래스가 및 클래스와 긴밀하게 결합됩니다. Cart Cart Cart DebitCard Paypal 그 대신 인터페이스를 만들고 및 클래스 모두 인터페이스를 구현하도록 합니다. 이제 클래스는 개별 결제 유형이 아닌 인터페이스에 따라 달라집니다. 이렇게 하면 클래스가 느슨하게 결합됩니다. Payment DebitCard Paypal Payment Cart Payment 아래 코드를 참조하세요. public interface Payment { void doPayment(double amount); } public class PaypalPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate paypal payment } } public class DebitCardPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate payment via debit card } } import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; private Payment paymentOption; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); this.paymentOption = paymentOption; } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } public void placeOrder() { this.paymentOption.doPayment(calculateCartValue()); } } 객체 지향 프로그래밍, GoF 디자인 패턴 및 하위 수준 디자인 인터뷰에 대해 더 자세히 알아보고 싶다면 내 높은 평가를 받는 확인하세요. 객체 지향 프로그래밍 + Java 디자인 패턴 과정을