SOLID 원칙은 객체 지향 프로그래밍과 설계에서의 다섯 가지 기본 원칙을 나타내는 약어이다.
해당 원칙들은 왜 쓰는 건지, 무엇이 있는지 알아보자
SRP: 수정을 가하는 이유(대상)이 하나의 역할만 있어야한다.
SOLID 원칙들은 소프트웨어 개발에서 코드의 유연성과 유지보수성을 높이기 위해 사용된다.
SRP (Single Responsibility Principle): 단일 책임 원칙
클래스는 하나의 책임만 가져야 하며, 클래스는 하나의 변경 이유만 가져야 한다.
말이 어렵지만 다시 말하면
”클래스가 변경되어야 할 이유가 하나뿐이어야 한다” 이다.
→ 클래스는 하나의 변경 요인(요구사항의 변경, 버그 수정 등)으로만 수정되어야 한다는 뜻이다.
즉, 최대한 기능에 따라 분리하라는 소리다.
class 직원{
void 회계팀_로직(){};
void 인사팀_로직(){};
void 기술팀_로직(){};
}
이렇게 다 몰아 넣지 말고 책임을 분리해야한다.
class 직원{...}
class 회계팀{
void 회계팀_로직(){};
...
}
class 인사팀{...}
class 기술팀{...}
이런식으로 분리 해야한다.
⇒ 유지보수가 쉬워지고, 코드의 변경이 다른 부분에 미치는 영향을 최소화할 수 있다.
OCP (Open/Closed Principle): 개방-폐쇄 원칙
소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.
즉, 코드,기능의 확장(추가)은 기존 코드의 변경이 있어서는 안된다.
interface Shape {
double calculateArea();
}
class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
}
class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.calculateArea();
}
}
위의 코드를 보면 영역계산을 하는 “AreaCalculator” 클래스는 인터페이스로 구현이 되어 기존 코드의 변경 없이 확장이 가능하다.
만약 인터페이스가 아닌 구현으로 되어있었다면
class AreaCalculator{
if (원 이면){}
else if (네모면){}
else if (세모면){}
...
}
처럼 하나하나 추가할 때 마다 기존 코드의 변경이 일어날 것이다.
⇒ 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있어, 코드의 안정성과 재사용성이 높아진다.
LSP (Liskov Substitution Principle): 리스코프 치환 원칙
프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으며, 하위타입의 인스턴스로 바꿀 수 있어야한다.
즉, 자식 클래스는 언제나 자신의 부모 클래스를 대체할 수 있어야 한다.
→ Animal클래스를 Cat이 완벽하게 대체 할 수 있어야 한다.
⇒ 상속 구조를 올바르게 설계할 수 있으며, 다형성(polymorphism)을 통해 유연한 코드를 작성할 수 있다.
ISP (Interface Segregation Principle): 인터페이스 분리 원칙
특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫습니다. 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 않도록 한다.
이렇게 인터페이스를 생성해 놓고
public interface Powerable {
void powerOn();
void powerOff();
}
public interface MusicPlayable {
void playMusic();
}
public interface InternetBrowsable {
void browseInternet();
}
필요한 것만 가져다 쓰면 된다.
public class 스마트폰 implements Powerable, MusicPlayable, InternetBrowsable {
@Override
public void powerOn() {
System.out.println("켜지는중");
}
@Override
public void powerOff() {
System.out.println("꺼지는중");
}
@Override
public void playMusic() {
System.out.println("음악 재생 기능");
}
@Override
public void browseInternet() {
System.out.println("인터넷 검색 기능");
}
}
⇒ 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 분리하여, 더 작은 인터페이스를 제공함으로써 유연성을 높일 수 있다.
DIP (Dependency Inversion Principle): 의존성 역전 원칙
고수준 모듈은 저수준 모듈에 의존해서는 안 됩니다. 이 둘 모두 추상화에 의존해야 합니다. 추상화는 세부 사항에 의존해서는 안 됩니다. 세부 사항은 추상화에 의존해야 한다.
아래 예시는 추상화가 아닌 구현에 의존된 코드이다
// PizzaOrder 클래스
class PizzaOrder {
public void order() {
System.out.println("Ordering a pizza.");
}
}
class BurgerOrder {
public void order() {
System.out.println("Ordering a Burger.");
}
}
class ChickenOrder {
public void order() {
System.out.println("Ordering a Chicken.");
}
}
// OrderService 클래스
class OrderService {
private PizzaOrder pizzaOrder;
private ChickenOrder chickenOrder;
private BurgerOrder burgerOrder;
public OrderService() {
this.pizzaOrder = new PizzaOrder(); // 직접 참조
}
public void placeOrder() {
pizzaOrder.order();
}
public static void main(String[] args) {
OrderService orderService = new OrderService();
orderService.placeOrder();
}
}
해당 코드를 추상화에 의존하게 작성해본다면
// Order 인터페이스
interface Order {
void order();
}
// PizzaOrder 클래스 (Order 인터페이스 구현)
class PizzaOrder implements Order {
@Override
public void order() {
System.out.println("Ordering a pizza.");
}
}
// SushiOrder 클래스 (Order 인터페이스 구현)
class SushiOrder implements Order {
@Override
public void order() {
System.out.println("Ordering sushi.");
}
}
// OrderService 클래스
class OrderService {
private Order order;
public OrderService(Order order) {
this.order = order; // 인터페이스에 의존
}
public void placeOrder() {
order.order();
}
public static void main(String[] args) {
Order pizzaOrder = new PizzaOrder();
Order sushiOrder = new SushiOrder();
OrderService pizzaOrderService = new OrderService(pizzaOrder);
pizzaOrderService.placeOrder();
OrderService sushiOrderService = new OrderService(sushiOrder);
sushiOrderService.placeOrder();
}
}
처럼 짜인 것을 볼 수 있다.
이렇게 코드를 작성한다면 새로운 Order가 만들어지더라도 OrderService를 수정하지 않아도 된다.
⇒ 모듈 간의 결합도를 낮추고, 시스템을 더 유연하고 확장 가능하게 만들어, 코드의 재사용성과 테스트 용이성을 높일 수 있다.