키보드워리어

[스프링] SOLID에 대해서 본문

Spring framework

[스프링] SOLID에 대해서

꽉 쥔 주먹속에 안경닦이 2023. 5. 7. 22:42
728x90

안녕하세요 오늘은 코드 유연성을 책임지는 SOLID 개념에 대해 구체적인 예시와 함께 살펴보겠습니다.

solid
SOLID


1. SRP (Single Responsibility Principle)

단일 책임 원칙으로 하나의 클래스는 하나의 책임을 가져야 한다는 원칙
클래스의 변경은 단일 책임을 가지는 메서드와 클래스에만 영향을 미치도록 설계해야 한다는 의미

    public User login(String username, String password) {
        // 로그인 처리 로직
    }

    public void updateProfile(User user) {
        // 프로필 수정 처리 로직
    }
    
    public void createReport(User user) {
        // 보고서 생성 처리 로직
    }
}
public class LoginService {
    
    public User login(String username, String password) {
        // 로그인 처리 로직
    }

}

 

위 코드는 단일 책임 원칙에 어긋납니다.

 

왜냐하면 UserService 클래스는 로그인 처리, 프로필 수정, 보고서 생성의

세 가지 책임을 가지고 있기 때문입니다.

 

이를 개선하려면 다음과 같이 세 개의 클래스로 분리할 수 있습니다.

public class ProfileService {
    
    public void updateProfile(User user) {
        // 프로필 수정 처리 로직
    }

}

public class ReportService {
    
    public void createReport(User user) {
        // 보고서 생성 처리 로직
    }

}

 
 

2. OCP (Open/Closed Principle)

개방-폐쇄 원칙,
확장에는 열려 있고, 변경에는 닫혀 있어야 한다는 원칙.
기존의 코드를 변경하지 않고 새로운 기능을 추가할 수 있도록 설계

public class Product {
    
    private String name;
    private int price;

    public Product(String name, int price) {
        this.name = name;
        this.price = price;
    }

    public int getPrice() {
        return price;
    }

    // 상품 할인 기능
    public int getDiscountedPrice() {
        return price * 90 / 100;
    }
}

위 코드는 개방-폐쇄 원칙을 어긋납니다. 상품 할인 기능이 추가되면 Product 클래스의 코드를 수정해야 합니다.

public interface Discount {
    int getDiscountedPrice(int price);
}

public class Product {
    
    private String name;
    private int price;
    private Discount discount;

    public Product(String name, int price, Discount discount) {
        this.name = name;
        this.price = price;
        this.discount = discount

}

 이를 개선하려면 위와 같이 Discount 인터페이스를 추가하고, 할인 기능을 구현한 클래스를 만들어 확장합니다.
 
 

3. LSP (Liskov Substitution Principle)

리스코프 치환 원칙
자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다는 원칙
자식 클래스는 부모 클래스에서 정의한 규칙을 지켜야 하며, 기능을 확장해도 기존 규약은 유지되어야 합니다.
 
다음은 LSP 원칙을 지키지 않은 예시 코드입니다. 자동차를 제어하는 Car클래스와, 전기차를 제어하
는 ElectricCar클래스가 있습니다.

class Car{
    public void accelerate() {
        // 자동차를 가속시키는 코드
    }
}

class ElectricCar extends CarController {
    public void recharge() {
        // 전기를 충전하는 코드
    }
}

위 코드에서 ElectricCar 클래스는 Car클래스를 상속받았습니다. 그러나 ElectricCar클래스에는
Car클래스에서 정의된 accelerate() 메서드 외에도 recharge() 메서드가 추가되었습니다. 이는
ElectricCar클래스가 Car클래스와 서로 다른 기능을 수행하므로, LSP 원칙을 위반합니다.
 
위 코드를 보면, ElectricCar객체는 Car 타입으로 업캐스팅하여 사용할 수 있습니다. 그나 ElectricCar 클래스에서 정의한 recharge() 메서드는 Car클래스에서는 정의되지 않았기 때문에, Car 타입으로 업캐스팅하면 recharge() 메서드를 사용할 수 없습니다. 이렇게 되면, LSP 원칙을 지키지 않은 객체지향 설계가 되어버립니다.

class Car {
    public void accelerate() {
        // 자동차를 가속시키는 코드
    }
}

class ElectricCar extends Car {
    public void accelerate() {
        // 자동차를 가속시키는 코드 (전기차 버전)
    }

    public void recharge() {
        // 전기를 충전하는 코드
    }

}

위 코드에서 ElectricCar클래스는 Car클래스를 상속받았습니다. 그러나 ElectricCar클래스에서
는 Car 클래스의 accelerate() 메서드를 오버라이딩하여, 전기차 버전의 가속 기능을 추가하였습니다.

 

그러면 ElectricCar객체도 Car타입으로 업캐스팅하여 사용할 수 있으며, 

Car클래스에서 정의한 accelerate() 메서드와 ElectricCar클래스에서

오버라이딩한 accelerate() 메서드 모두 사용할 수 있습니다.

이렇게 되면, Car와 ElectricCar 모두 accelerate() 메서드를 가지고 있으므로, 어떤 객체를 사용하더라도 같은 결과를 보장합니다. 

이는 LSP 원칙을 잘 지켰음을 보여줍니다.
 
 

4. ISP (Interface Segregation Principle)

인터페이스 분리 원칙
클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙
인터페이스는 클라이언트에 필요한 최소한의 메서드만 포함해야 함

interface Workable {
    void work();
    void eat();
}

class Robot implements Workable {
    public void work() {
        // 로봇이 일하는 코드
    }

    public void eat() {
        // 로봇이 먹는 코드
    }
}

class Worker implements Workable {
    public void work() {
        // 노동자가 일하는 코드
    }

    public void eat() {
        // 노동자가 먹는 코드
    }
}

위 코드에서 Workable 인터페이스에는 work() 메서드와 eat() 메서드가 모두 정의되어 있습니다.

그러나 Robot 클래스는 work() 메서드만 필요로 하고 eat() 메서드는 필요하지 않습니다.

 

반면에 Worker 클래스는 work() 메서드와 eat() 메서드 모두 필요합니다.

이렇게 인터페이스에 필요하지 않은 메서드까지 정의하면, 클라이언트가

사용하지 않는 메서드에 의존할 수 있기 때문에, ISP 원칙을 위반하게 됩니다.

ISP 원칙을 잘 지킨 예시 코드는 다음과 같습니다. 

아래 코드는 Workable 인터페이스를 Work 인터페이스와 Eat 인터페이스로 분리하여,

Robot 클래스와 Worker 클래스에서는 필요한 인터페이스만 구현하도록 했습니다.

interface Work {
    void work();
}

interface Eat {
    void eat();
}

class Robot implements Work {
    public void work() {
        // 로봇이 일하는 코드
    }
}

class Worker implements Work, Eat {
    public void work() {
        // 노동자가 일하는 코드
    }

    public void eat() {
        // 노동자가 먹는 코드
    }
}

 
 

5. DIP (Dependency Inversion Principle)

의존 역전 원칙
고수준 모듈은 저수준 모듈에 의존해서는 안되며, 추상화에 의존하면서, 구체화된 클래스에는 의존하지 않아야 한다는 원칙입니다.
추상화에 의존하므로써 유연한 코드를 작성할 수 있고, 변경에 대한 영향을 최소화할 수 있습니다.

class PrintDocument {
    public String getContent() {
        // 문서 내용을 반환하는 코드
    }
}

class Printer {
    public void print(PrintDocument document) {
        // 문서를 출력하는 코드
        String content = document.getContent();
        // ...
    }
}


위 코드에서 Printer 클래스는 PrintDocument 클래스에 직접 의존합니다. 이렇게 되면 Printer 클래스는 PrintDocument 클래스와 강하게 결합되어 있기 때문에, 만약 PrintDocument 클래스가 변경되면 Printer 클래스도 변경되어야 하므로 유지 보수성이 떨어지게 됩니다.
 
DIP 원칙을 잘 지킨 예시 코드는 다음과 같습니다. 아래 코드는 Printer 인터페이스와 Document 인터페이스를 도입하고, PrinterImpl 

클래스와 DocumentImpl 클래스에서 각각 구현하도록 했습니다.

interface Document {
    String getContent();
}

class DocumentImpl implements Document {
    public String getContent() {
        // 문서 내용을 반환하는 코드
    }
}

interface Printer {
    void print(Document document);
}

class PrinterImpl implements Printer {
    public void print(Document document) {
        // 문서를 출력하는 코드
        String content = document.getContent();
        // ...
    }
}

 
이러한 SOLID 원리를 적용하면 객체 지향 프로그래밍에서 유연하고 확장성 있는 코드를 작성할 수 있습니다.

728x90