[Clean Architecture #0] SOLID 5원칙

SOLID 원칙: 깨끗한 아키텍처를 위한 기초 개념

소프트웨어 개발에서 유지보수성과 확장성을 높이기 위해 여러가지 설계 원칙이 있다. 그중에서도 특히 중요한 것이 바로 SOLID 원칙이다. SOLID 원칙은 객체지향 설계에서 지켜야 할 다섯가지 기본 원칙을 의미하며, 코드의 품질을 높이고 버그를 줄이며, 변경에 유연하게 대응할 수 있게 해준다. 이번 포스팅에서는 Clean Architecture를 이해하기 위한 기초로서 SOLID 원칙의 다섯가지 요소에 대해 알아본다.

1. SRP(Single Responsibility Principle) - 단일책임원칙

  • 정의: 클래스는 하나의 책임만 가져야한다. 즉, 클래스는 하나의 기능만을 가져야 하며, 그 기능을 변경해야 하는 이유도 하나뿐이어야 한다.
  • 단일책임원칙은 클래스나 모듈이 하나의 기능만을 책임지도록 하는것이다. 이를 통해 클래스가 변경되는 이유를 하나로 제한하여 코드의 변경이 다른 부분에 미치는 영향을 최소화할 수 있다.
class User {
    private String name;
    private String emial;

    // 사용자 정보를 저장하는 메소드
    public void saveUser() {
        // 데이터베이스 로직
    }

    // 사용자 정보를 출력하는 메소드
    public void printUser() {
        // 출력 로직
    }
}

위 예시에서는 User 클래스가 데이터 저장과 출력 두 가지 책임을 가지고 있다. 이를 단일 책임 원칙에 따라 분리하면 다음과 같다.

class User {
    private String name;
    private String email;

}

class UserRepository {
    public void saveUser(User user) {
        // 데이터베이스 로직
    }
}

class UserPrinter {
    public void printUser() {
        // 출력 로직
    }
}

2. OCP(Open/Closed Principle) - 개방폐쇄원칙

  • 정의: 소프트웨어 요소는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.
  • 개방폐쇄원칙은 시스템을 변경하지 않고도 확장할 수 있도록 설계해야 한다는 원칙이다. 이를 위하 인터페이스와 추상클래스를 활용하여 기능을 확장할 수 있다.
abstract class Shape {
    abstract void draw();
}

class Circle extends Shape {
    void draw() {
        // 원을 그리는 로직
    }
}

class Rectangle extends Shape {
    void draw() {
        // 사각형을 그리는 로직
    }
}

class Drawing {
    private List<Shape> shapes;

    void drawAllShapes() {
        for (Shape shape : shapes) {
            shape.draw();
        }
    }
}

위 예시에서는 Shape 클래스가 확장 가능하도록 만들어져 있으며, 새로운 도형을 추가할 때 기존 코드를 변경할 필요가 없다.

3. LSP(Liskov Substitution Principle) - 리스코프치환원칙

  • 정의: 서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다.
  • 리스코프치환원칙은 상속관계에 있는 클래스들이 상위 클래스의 기능을 온전히 수행해야 한다는 원칙이다. 이를 통해 코드의 유연성과 재사용성을 높일 수 있다
class Bird {
    void fly() {
        // 새가 날아오르는 로직
    }
}

class Ostrich extends Bird {
    void fly() {
        // 타조는 날 수 없음
        throw new UnsupportedOperationException("Ostrich can't fly");
    }
}

위 예시에서는 타조가 새의 특성을 상속받았지만, 날 수 없다는 점에서 문제가 발생한다. 이를 해결하기 위해 BirdNonFlyingBird로 분리할 수 있다.

class Bird {
    void fly() {
        // 새가 날아오르는 로직
    }
}

class NonFlyingBird extends Bird {
    void fly() {
        // 아무 동작도 하지 않음
    }
}

class Ostrich extends NonFlyingBird {
    // 타조는 날 수 없음
}

4. ISP (Interface Segregation Principle) - 인터페이스분리원칙

  • 정의: 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
  • 인터페이스 분리원칙은 하나의 일반적인 인터페이스보다 여러 개의 구체적인 인터페이스를 사용하는 것이 낫다는 원칙이다. 이를 통해 클라이언트가 불필요한 메서드에 의존하지 않도록 한다.
interface Worker {
    void work();
    void eat();
}

class HumanWorker implements Worker {
    public void work() {
        // 일하는 로직
    }
    public void eat() {
        // 식사하는 로직
    }
}

class RobotWorker implements Worker {
    public void work() {
        // 일하는 로직
    }
    public void eat() {
        // 로봇은 먹을 수 없음
        throw new UnsupportedOperationException("Robots don't eat");
    }
}

위 예시에서는 Worker 인터페이스가 구체적이지 않아 로봇에게는 불필요한 메서드를 포함하고 있다. 이를 인터페이스 분리원칙에 따라 분리하면 다음과 같다.

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class HumanWorker implements Workable, Eatable {
    public void work() {
        // 일하는 로직
    }
    public void eat() {
        // 식사하는 로직
    }
}

class RobotWorker implements Workable {
    public void work() {
        // 일하는 로직
    }
}

5. DIP (Dependency Inversion Principle) - 의존성역전원칙

  • 정의: 고수준 모듈은 저수준 모듈에 의존해서는 안되며, 둘 다 추상화에 의존해야 한다. 추상화는 구체적인 사항에 의존해서는 안된다.
  • 의존성 역전원칙은 고수준 모듈이 저수준 모듈의 구현에 의존하지 않고, 인터페이스나 추상 클래스와 같은 추상황에 의존하도록 만드는 원칙이다. 이를 통해 의존성을 줄이고 유연성을 높일 수 있다.
class Light {
    void turnOn() {
        // 불을 켜는 로직
    }
}

class Switch {
    private Light light;

    public Switch(Light light) {
        this.light = light;
    }

    void operate() {
        light.turnOn();
    }
}

위 예시에서는 Switch클래스가 Light클래스에 의존하고 있다. 이를 의존성 역전 원칙에 따라 개선하면 다음과 같다.

interface Switchable {
    void turnOn();
}

class Light implements Switchable {
    public void turnOn() {
        // 불을 켜는 로직
    }
}

class Switch {
    private Switchable device;

    public Switch(Switchable device) {
        this.device = device;
    }

    void operate() {
        device.turnOn();
    }
}

이제 Switch클래스는 Switchable인터페이스에 의존하게 되어, Light클래스 외에도 Switchable인터페이스를 구현한 다른 클래스들과도 함께 사용할 수 있다.

결론

SOLID 원칙은 객체지향 설계에서 코드의 품질을 높이고 유지보수성을 향항시키는 데 중요한 역할을 한다. 이 원칙들을 잘 이해하고 적용하면, 코드의 가독성, 재사용성, 유연성을 크게 높일 수 있다. 다음 포스팅에서는 이러한 원칙들이 실제로 적용된 Clean Architecture에 대해 알아볼것이다.

댓글남기기