SOLID

5 분 소요




SOLID

객체지향 프로그래밍에서 소프트웨어 디자인 품질을 향상시키기 위한 다섯 가지 원칙이다.
이 원칙을 따르면 아래와 같은 장점이 있다.

  • 유지보수성 향상
  • 재사용성 항샹
  • 확장성 향상
  • 변경에 대한 유연성 향상
  • 코드의 가독성 향상
  • 결합도 감소
  • 테스트 용이성 향상
  • 오류발생 가능성 감소





SRP - 단일 책임 원칙

하나의 클래스는 하나의 책임을 가져야 한다는 원칙이다.
이것은 클래스가 변경되어야 하는 이유는 단 하나여야 함을 의미한다.
클래스의 응집력은 높이고 결합도는 낮춰서 유지보수, 재사용성, 확장성에 용이하게 하는 원칙으로,
클래스 하나가 여러 책임을 가지게 되면 그 클래스를 변경해야 하는 이유도 여러가지가 생기므로,
코드를 변경할때 다른 책임과 관련된 코드까지 함께 변경해야 한다.

SRP 원칙을 지키기 위해서는 클래스가 자신의 책임을 명확하게 정의하고,
다른 책임을 수행하는 클래스와의 의존성을 최소화 해야한다.

단점으로는 클래스와 인터페이스 수가 늘어나게 되고 클래스간의 상호작용을 복잡하게 만들 수 있다.

자바에서 SRP 를 구현하는 방식에는 클래스를 단일책임으로 분리,
인터페이스를 이용해 책임을 분리, 디자인패턴, AOP 등이 있다.


  • 클래스를 단일 책임으로 분리하는 방식
// Order 클래스는 너무 많은 책임을 가지고 있다.
public class Order {
    private List<Item> items;
    private Customer customer;
    
    public void addItem(Item item) {}
    
    public void removeItem(Item item) {}
    
    public double calculateTotalPrice() {}
    
    public void sendConfirmEmail() {}
}

// 클래스의 책임을 분리함으로써, 클래스간 의존성이 낮아지게 된다.
public class OrderItem {
    private Item item;
    private int quantity;
}

public class OrderCalculator {
    public double calculateTotalPrice(List<OrderItem> orderItems) {}
}

public class EmailSender {
    public void sendConfirmEmail(Customer customer) {}
}


  • 인터페이스로 책임을 분리하는 방식
// 상품 목록을 관리하는 ItemManager 인터페이스
public interface ItemManager {
    public void addItem(Item item);
    public void removeItem(Item item);
    public double calculateTotalPrice();
}

// 이메일을 전송하는 EmailSender 인터페이스
public interface EmailSender {
    public void sendConfirmEmail(Customer customer);
}

// ItemManager 인터페이스를 구현하는 ItemManagerImpl 클래스
public class ItemManagerImpl implements ItemManager {
    private List<Item> items;
    
    public void addItem(Item item) {}
    
    public void removeItem(Item item) {}
    
    public double calculateTotalPrice() {}
}

// EmailSender 인터페이스를 구현하는 EmailSenderImpl 클래스
public class EmailSenderImpl implements EmailSender {
    public void sendConfirmEmail(Customer customer) {}
}





OCP - 개방 / 폐쇄 원칙

소프트웨어 구성요소 (클래스, 모듈, 함수 등) 는 확장에는 열려있어야 하지만, 변경에는 닫혀 있어야 한다.
즉, 기존의 코드를 변경하지 않아도 새로운 기능을 추가할 수 있도록 하는 것이다.
OCP 는 다형성, 추상화, 인터페이스 등을 통해 내부 구현을 외부로 노출시키지 않고도 기능을 확장할 수 있는데
이는 코드의 유지보수, 확장성, 재사용성 등을 향상시킬 수 있다.

단점으로는 초기 설계와 인터페이스 설계등의 리소스가 많이 들어가게 되며 OCP를 적용하기 위해
인터페이스와 추상화를 사용해 내부의 세부 구현사항을 숨기게 되면 코드의 복잡성이 증가하게 된다.

자바에서 OCP 를 구현하는 방식에는
추상클래스 / 인터페이스, 전략패턴, 팩토리 메서드 패턴, 데코레이터 패턴 등이 있다.


  • 추상클래스를 이용한 방식
abstract class Animal {
    public abstract void makeSound();
}

class Dog extends Animal {
    public void makeSound() {
        System.out.println("멍멍");
    }
}

// 새로운 동물인 고양이를 이렇게 기존 코드를 변경하지 않고 확장할 수 있다. Cat 클래스는 Animal 타입으로 선언될 수 있고,
// Animal 타입으로 다루어 질때 makeSound() 메서드가 호출된다.
class Cat extends Animal { 
    public void makeSound() { 
        System.out.println("야옹"); 
    } 
}


  • 인터페이스를 이용한 방식
interface Animal {
    public void makeSound();
}

class Dog implements Animal {
    public void makeSound() {
        System.out.println("멍멍");
    }
}

// 기존의 코드를 변경하지 않고 Cat 클래스를 확장
class Cat implements Animal { 
    public void makeSound() { 
        System.out.println("야옹"); 
    } 
}





LSP - 리스코프 치환 원칙

하위타입은 상위타입으로 대체 가능해야 한다는 원칙이다.
상속관계에서 부모클래스에 선언된 속성과 메서드는 하위클래스에서 동일하게 동작해야 한다.
즉, 하위클래스가 상위클래스의 기능을 정확하게 대체할 수 있어야 한다.
LSP 는 유연한 확장, 재사용성 등에 장점이 있다.

단점으로는 설계에 리소스가 들어가게 되며 LSP 는 인터페이스와 상속관계에서만 적용할 수 있다.

자바에서 LSP 를 구현하는 방식에는 인터페이스, 상속 이 있다.


  • LSP 를 위반한 코드
abstract class Animal {
    public abstract void makeSound(); 
}

class Dog extends Animal {
    public void makeSound() {
        System.out.println("Bark");
    }
    
    public void wagTail() {
        System.out.println("Tail wagging");
    }
}

class BigDog extends Dog {
    public void makeSound() {
        System.out.println("Big bark");
    }
    
    public void wagTail(int speed) {
        System.out.println("Tail wagging at speed " + speed);
    }
}

/*
BigDog 클래스의 wagTail() 메서드는 Dog 클래스의 wagTail() 메서드와
시그니처가 다르기 때문에 에러가 발생한다.
즉, BigDog 객체가 Dog 객체를 완벽하게 대체하지 못한다.
*/
public class Main {
    public static void main(String[] args) {
        Dog bigDog = new BigDog();
        bigDog.makeSound();
        bigDog.wagTail();  // 컴파일 에러 발생
    }
}


  • LSP 를 준수한 코드
abstract class Animal {
    public abstract void makeSound();
}

class Dog extends Animal {
    public void makeSound() {
        System.out.println("Bark");
    }
    public void wagTail() {
        System.out.println("Tail wagging");
    }
}

class BigDog extends Dog {
    public void makeSound() {
        System.out.println("Big bark");
    }
    
    public void wagTail() {
        wagTail(1);
    }
    
    public void wagTail(int speed) {
        System.out.println("Tail wagging at speed " + speed);
    }
}

// Dog 클래스와 BidDog 클래스 모두 같은 wagTail() 메서드를 가지게 된다.
// Dog 객체를 대신하여 BigDog 객체를 사용할 때 문제가 일어나지 않는다.
// 즉, 부모클래스를 자식클래스로 대체해도 동일한 기능을 수행해서 LSP를 준수한다.
public class Main {
    public static void main(String[] args) {
        BigDog bigDog = new BigDog();
        bigDog.makeSound();
        bigDog.wagTail();
        bigDog.wagTail(2);
    }
}





ISP - 인터페이스 분리 원칙

인터페이스 분리 원칙을 의미한다.
즉, 인터페이스가 클라이언트에서 필요한 메서드만 가지도록 권장한다.
이렇게 함으로써 클라이언트 입장에선 필요하지 않은 메서드를 호출하거나
구현할 필요가 없게되고, 이것은 의존성을 줄이고 유지보수, 재사용성을 증가시킨다.

단점으로는 인터페이스클래스 가 많아지는 문제가 있다.


  • ISP 를 위반한 코드
public interface Animal {
    void run();
    void fly();
}

public class Bird implements Animal {
    public void run() {}
    public void fly() {}
}

public class Dog implements Animal {
    public void run() {}
    public void fly() {}
}

/*
Bird 클래스는 run() 메서드를 구현하지 않고, Dog 클래스는 fly() 메서드를 구현하지 않는다.
이것은 필요하지 않은 메서드를 구현체에서 구현하기 때문에 ISP 원칙을 위반한다.

Animal 인터페이스를 run() 메서드와 fly() 메서드를 각각 가지는 2개의 인터페이스로 분리하면
ISP 원칙을 준수할 수 있다.
*/
public class Main {
    public static void main(String[] args) {
        Animal bird = new Bird();
        bird.fly();
        
        Animal dog = new Dog();
        fish.run();
    }
}





DIP - 의존관계 역전 원칙

상위모듈이 하위모듈에게 의존하면 안되며 둘다 추상화에 의존해야 한다는 원칙이다.
즉, 클래스는 다른 클래스에 의존하지말고 추상화를 통해 상호작용 해야 한다는 의미이다.
이것은 결합도를 낮추기 위한 방식으로 객체간의 의존성을 느슨하게 만드는 효과가 있다.
DIP 를 준수하면 하위 모듈이 변경되어도 상위 모듈에 영향이 없으므로 유연성과 확장성이 향상된다.

단점으로는 추상화를 위한 인터페이스추상클래스 가 많아지고 복잡성이 증가한다.


  • DIP 를 위반한 코드
class UserService {
    private final UserRepository userRepository;
    
    public UserService() {
        this.userRepository = new UserRepository();
    }
    
    public User findUserById(int id) {
        return userRepository.findById(id);
    }
}


// UserRepository 에 직접 의존하고 있으므로 UserRepository 클래스의 변경이 UserService 에게 영향을 준다.
class UserRepository {
    public User findById(int id) {
        ...
        return user;
    }
}


  • DIP 를 준수한 코드
interface UserRepository {
    User findById(int id);
}

class UserService {
    private final UserRepository userRepository;
    
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public User findUserById(int id) {
        return userRepository.findById(id);
    }
}

/*
UserService 클래스는 UserRepository 인터페이스에만 의존하도록 하고 생성자를 통해 주입받는다.
이제 UserRepository 클래스의 구현체인 DatabaseUserRepository 클래스가 UserRepository 인터페이스를 구현하도록 해서 DatabaseUserRepository 클래스가 변경되더라도 UserService 클래스에 영향을 미치지 않는다.
*/
class DatabaseUserRepository implements UserRepository {
    public User findById(int id) {
        ...
        return user;
    }
}





책임이란?

객체가 수행하는 역할 또는 기능을 말한다. 책임은 객체의 상태와 행위를 결정한다.
SOLID원칙 에서 책임은 객체나 모듈은 단 한가지의 책임을 가져야 하고
이 객체나 모듈이 변경될 이유는 단 하나여야만 한다.
즉, 변경이 일어날 때 해당 객체만을 수정하면 단일책임원칙을 따르는 것이고 해당 객체 뿐만이 아니라
다른객체 까지 수정이 일어나면 단일책임원칙을 준수하지 못한것이다.






댓글남기기