SOLID
SOLID는 객체 지향 프로그래밍과 설계에 대한 다섯 가지 핵심 원칙을 나타낸다. 이 원칙들은 코드의 유지 보수성, 가독성, 확장성을 향상시키는 데 큰 도움이 된다.
S : 단일 책임 원칙 (Single Responsibility Principle, SRP)
- 클래스는 단 하나의 책임만 가져야 한다는 원칙이다. 다시 말해, 클래스가 변경되어야 하는 이유는 하나뿐이어야 한다. 이 원칙을 따르면 코드의 결합도가 낮아지고 각 클래스가 자신의 역할에 집중할 수 있어 코드가 더욱 명확하고 이해하기 쉬워진다.
1. 잘못된 예시
public class User {
private String name;
private String email;
// 생성자, getter, setter 등 생략
public void saveUser() {
// 사용자 정보 데이터베이스 저장 로직
}
public String renderUser() {
// 사용자 정보 HTML 렌더링 로직
return "<div>" + name + " : " + email + "</div>";
}
}
위의 예에서 'User' 클래스는 사용자 정보를 저장하는 책임, 데이터베이스에 저장하는 책임, HTML로 렌더링하는 책임을 모두 가지고 있다. 이를 SRP에 따라 분리하면 다음과 같이 될 수 있다.
2. SRP 적용 예시
먼저, 'User' 클래스는 사용자 정보를 저장하는 역할만 수행하도록 한다.
public class User {
private String name;
private String email;
// 생성자, getter, setter 등은 생략
}
'UserRepository' 클래스는 사용자 정보를 데이터베이스에 저장하는 역할을 수행하도록 한다.
public class UserRepository {
public void saveUser(User user) {
// 사용자 정보 데이터베이스 저장 로직
}
}
'UserRenderer' 클래스는 사용자 정보를 HTML로 렌더링하는 역할을 수행하도록 한다.
public class UserRenderer {
public String renderUser(User user) {
// 사용자 정보를 HTML로 렌더링하는 로직
return "<div>" + user.getName() + " : " + user.getEmail() + "</div>";
}
}
이제 'User' 클래스가 변경되어도 사용자 정보의 저장 방식이나 렌더링 방식에는 영향을 미치지 않으며, 반대의 경우도 마찬가지이다다. 이러한 방식은 시스템의 각 부분을 독립적으로 개발하고 테스트하는 데도 유용하다.
O : 개방-폐쇄 원칙 (Open-Closed Principle, OCP)
- 클래스는 확장에 대해 열려 있어야 하고, 변경에 대해서는 닫혀 있어야 한다는 원칙이다. 즉, 새로운 기능을 추가할 때 기존 코드를 변경하지 않고 새로운 코드를 추가함으로써 기능을 확장할 수 있어야 한다.
1. 잘못된 예시
- 도형의 면적을 계산하는 클래스가 있다고 가정
public class AreaCalculator {
public double calculateRectangleArea(Rectangle rectangle) {
return rectangle.width * rectangle.height;
}
public double calculateCircleArea(Circle circle) {
return Math.PI * Math.pow(circle.radius, 2);
}
}
이 코드는 새로운 도형이 추가될 때마다 AreaCalculator 클래스에 새로운 메소드를 추가해야 한다. 따라서 이 코드는 OCP를 위반하고 있다.
이 문제를 해결하기 위해, 각 도형 클래스가 자신의 면적을 계산하는 메소드를 가지도록 하고, AreaCalculator 클래스는 이 메소드를 호출하도록 변경할 수 있다.
2. OCP 적용 예시
public interface Shape {
double calculateArea();
}
public class Rectangle implements Shape {
public double width;
public double height;
public double calculateArea() {
return width * height;
}
}
public class Circle implements Shape {
public double radius;
public double calculateArea() {
return Math.PI * Math.pow(radius, 2);
}
}
public class AreaCalculator {
public double calculateShapeArea(Shape shape) {
return shape.calculateArea();
}
}
이제 AreaCalculator 클래스는 어떤 도형의 면적도 계산할 수 있으며, 새로운 도형이 추가되더라도 AreaCalculator 클래스를 수정할 필요가 없다. 새로운 도형은 단순히 Shape 인터페이스를 구현하면 됩니다. 이로써 OCP를 준수하게 된다.
L : 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
부모 클래스를 자식 클래스로 대체할 수 있어야 한다는 원칙이다. 즉, 상속받는 클래스는 상속하는 클래스를 완전히 대체할 수 있어야 한다. 이 원칙을 준수하면 부모 클래스를 사용하는 코드를 변경하지 않고도 자식 클래스를 사용할 수 있다.
1. 잘못된 예시
Bird 클래스와 이를 상속받은 Penguin 클래스가 있다고 가정
public class Bird {
public void fly() {
System.out.println("Bird is flying.");
}
}
public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly.");
}
}
이 코드는 Penguin은 Bird의 하위 타입이지만, Bird가 할 수 있는 행동인 fly() 를 Penguin이 수행할 수 없기 때문이다. (펭귄은 날 수 없기 때문) 이 경우 Bird 타입의 참조를 사용하여 fly() 메소드를 호출하면, 이 참조가 실제로 Penguin 인스턴스를 가리키고 있다면 예외가 발생하게 된다.
이 문제를 해결하려면, 공통적인 행동만을 가진 상위 클래스를 정의하고, 특정 행동은 하위 클래스에서 정의하는 것이 좋다.
2. LSP 적용 예시
public class Bird {
public void eat() {
System.out.println("새가 밥을 먹는다.");
}
}
public class FlyingBird extends Bird {
public void fly() {
System.out.println("새가 난다.");
}
}
public class Penguin extends Bird {
// 펭귄은 날 수 없기 때문에, fly() 를 오버라이딩하지 않는다.
}
이제 Bird 타입의 참조는 항상 eat() 메소드를 호출할 수 있으며, FlyingBird 타입의 참조는 fly() 메소드를 호출할 수 있다. Penguin은 이제 공통 기능인 eat()은 정의하지만, fly()를 정의하지 않는다.
I : 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안 된다는 원칙이다. 즉, 한 클래스는 자신이 필요로 하는 메소드만을 가진 인터페이스에만 의존해야 한다. 이렇게 하면 클래스간의 의존성이 낮아져 코드의 유연성이 증가하고, 결합도가 낮아진다.
1. 잘못된 예시
Animal인터페이스가 fly(), swim(), run() 메소드를 모두 가지고 있다고 가정
public interface Animal {
void fly();
void swim();
void run();
}
public class Dog implements Animal {
@Override
public void fly() {
throw new UnsupportedOperationException();
}
@Override
public void swim() {
// Swim 로직
}
@Override
public void run() {
// Run 로직
}
}
위 코드에서 Dog 클래스는 Animal 인터페이스를 구현하지만, Dog는 fly() 메소드를 사용할 수 없다. 이 문제를 해결하기 위해, 각 행동에 대해 별도의 인터페이스를 만드는 것이 좋다.
2. ISP 적용 예시
public interface Flyable {
void fly();
}
public interface Swimmable {
void swim();
}
public interface Runnable {
void run();
}
public class Dog implements Swimmable, Runnable {
@Override
public void swim() {
// Swim 로직
}
@Override
public void run() {
// Run 로직
}
}
이렇게 인터페이스를 분리하면, 각 클래스는 자신이 필요로 하는 메소드만을 가진 인터페이스를 구현하게 된다.
D : 의존성 역전 원칙 (Dependency Inversion Principle, DIP)
상위 모듈은 하위 모듈에 의존해서는 안 된다는 원칙이다. 둘 다 추상화에 의존해야 한다. 이 원칙은 구체적인 구현이 아닌 인터페이스에 의존하도록 코드를 작성하면, 코드의 재사용성과 테스트 용이성이 향상된다.
1. 잘못된 예시
ZooKeeper 클래스가 Lion 클래스에 의존하는 경우 가정
class Lion {
public void feed() {
System.out.println("사자한테 밥 주기");
}
}
class ZooKeeper {
private Lion lion;
public ZooKeeper() {
this.lion = new Lion();
}
public void feedLion() {
lion.feed();
}
}
이 코드는 DIP를 위반하고 있다. 왜냐하면 ZooKeeper 클래스가 구체적인 Lion 클래스에 의존하고 있기 때문이다. 만약 ZooKeeper가 Tiger나 다른 동물들에게 밥을 먹이려면 ZooKeeper 클래스도 수정해야 한다.이 문제를 해결하기 위해, Animal이라는 인터페이스를 만들고 ZooKeeper 클래스가 이 인터페이스에 의존하도록 변경할 수 있다.
2. DIP 적용 예시
interface Animal {
void feed();
}
class Lion implements Animal {
public void feed() {
System.out.println("사자한테 밥 주기");
}
}
class Tiger implements Animal {
public void feed() {
System.out.println("호랑이한테 밥 주기");
}
}
class ZooKeeper {
private Animal animal;
public ZooKeeper(Animal animal) {
this.animal = animal;
}
public void feedAnimal() {
animal.feed();
}
}
이제 ZooKeeper는 Animal 인터페이스에 의존하므로, 어떤 동물이든 밥을 먹일 수 있다.
정 리
이 원칙들은 모두 서로 연결되어 있으며, 하나의 원칙이 깨질 경우 다른 원칙들도 깨질 가능성이 높아진다. 이 원칙들을 잘 이해하고 적용하면 소프트웨어 설계의 품질을 크게 향상시킬 수 있다.
예를 들어, 만약 한 클래스가 여러 가지 역할을 맡고 있다면 (SRP 위반), 그 클래스를 수정하는 것이 매우 어려워질 수 있다. 만약 기능을 추가하거나 변경하려면, 그 클래스의 모든 역할에 대해 이해해야 하며, 한 역할에 대한 변경이 다른 역할에 부작용을 일으킬 수 있다.
또한, 만약 클래스가 자신이 사용하지 않는 메서드를 가진 인터페이스에 의존하고 있다면 (ISP 위반), 그 클래스는 필요 이상으로 많은 의존성을 가지게 되어 복잡성이 증가하고, 코드 변경이 어려워질 수 있다.
SOLID 원칙은 디자인 패턴과 밀접한 관련이 있다. 예를 들어, '전략 패턴'은 OCP를 준수하는 데 도움이 된다. '템플릿 메소드 패턴'은 LSP를 준수하는 데 도움이 된다. '팩토리 메소드 패턴'과 '의존성 주입'은 DIP를 준수하는 데 도움이 된다.
이 원칙들은 모든 상황에 적용되는 법칙은 아니지만, 대체로 코드 품질을 향상시키는 데 도움이 되므로 소프트웨어 개발자들은 이 원칙들을 잘 이해하고 있어야 한다.