Search
📁

⭐ SOLID

SOLID 원칙

SOLID 원칙은 객체지향 프로그래밍(OOP)에서 소프트웨어의 설계 품질을 향상시키기 위한 다섯 가지 원칙.
SOLID 원칙을 준수함으로써 코드의 유지보수성, 확장성, 재사용성 등을 향상.
원칙을 준수하여 설계된 코드는 변경에 유연하고 의존성이 낮으며, 확장이 용이하여 좀 더 품질 높은 소프트웨어를 개발할 수 있다.

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

하나의 클래스는 하나의 책임만 가져야 한다. 클래스가 변경되어야 하는 이유는 오직 하나.
하나의 클래스는 하나의 목적에 집중. 여러가지 목적을 가지고 있으면 유지보수가 어려워진다.

예시

이메일을 보내는 클래스는 이메일을 보내는 책임만 가져야 하며, 데이터베이스에 저장하는 책임은 다른 클래스에게 위임해야 한다..
class EmailSender { public void sendEmail(String message) { // 이메일을 보내는 로직 } }
Java
복사

장단점

장점
코드의 응집성이 높아져 가독성
유지보수성이 향상.
단점
클래스의 수가 늘어나고, 더 많은 인터페이스와 추상화가 필요할 수 있다.

만약 원칙을 지키지 않으면?

한 클래스에 여러 책임이 모여 있어 코드가 복잡해지고 유지보수가 어려워진다.
유저정보등록 클래스가 있는데 여기에 메일 전송기능까지 있으면
메일 전송기능 변경할때 유저정보등록까지 책임질수있음
그래서 클래스를 별도로 파서 하나의 클래스에 하나의 책임만 지게하는게 좋음

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

소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하고, 변경에 대해서는 닫혀 있어야 한다.
기존의 코드를 변경하지 않고 새로운 기능을 추가할 수 있도록 설계해야 합니다.
즉, 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 한다.

예시

interface Shape { // 도형의 넓이를 반환하는 메서드 double area(); } //사각형 넓이를 계산하는 메서드 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 area() { // 사각형의 넓이를 계산하여 반환 return width * height; } } //원의 넓이를 계산하는 매서드 class Circle implements Shape { private double radius; public Circle(double radius) { this.radius = radius; } @Override public double area() { // 원의 넓이를 계산하여 반환 return Math.PI * radius * radius; } } class AreaCalculator { // 도형의 넓이를 계산하는 메서드 public double calculateArea(Shape shape) { // Shape 인터페이스를 구현한 클래스의 area() 메서드를 호출하여 넓이를 반환 return shape.area(); } } public class Main { public static void main(String[] args) { // 사각형과 원 객체 생성 Rectangle rectangle = new Rectangle(5, 4); Circle circle = new Circle(3); // 넓이 계산기 객체 생성 AreaCalculator calculator = new AreaCalculator(); // 각 도형의 넓이 계산 및 출력 System.out.println("Rectangle Area: " + calculator.calculateArea(rectangle)); // 출력: Rectangle Area: 20.0 System.out.println("Circle Area: " + calculator.calculateArea(circle)); // 출력: Circle Area: 28.274333882308138 } }
Java
복사
기존의 코드를 수정하지않고 Shape 인터페이스를 구현하는 새로운 클래스를 추가함으로써 새로운 기능으로 확장.

장단점

장점
새로운 기능 추가 시 기존 코드를 변경할 필요가 없어져 확장성이 높아진다.
단점
추상화와 인터페이스의 추가로 인해 초기 개발 속도가 느려질 수 있다.

만약 원칙을 지키지 않으면?

새로운 요구사항이나 변경사항이 있을 때 기존 코드를 수정해야 하므로 코드의 유연성이 떨어지고 유지보수가 어려워진다.
상품할인 클래스가 있는데 여기에 계산메소드가 있음.
근데 상품이 추가될때마다 그럼 계산메소드를 직접수정해야함.
이러면 OCP위반.. 그래서 차라리 상품할인 인터페이스를 생성하고 상품마다 구현클래스로 만들어서
직접 구현체에 계산법을 추가하는 방식이 좋음

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

상속받은 클래스는 상위 클래스의 기능을 변경하지 않고 확장만 해야 한다.
하위 클래스는 상위 클래스의 역할을 대체할 수 있어야 한다.
하위 클래스는 상위 클래스와의 관계를 파괴하지 않고 동작해야 한다.

예시

class Bird { void fly() { System.out.println("I'm flying"); } } class Duck extends Bird { // 오리의 날개짓은 다르게 구현할 수 있지만 여전히 날 수 있는 기능을 가지고 있음 @Override void fly() { System.out.println("I'm flying like a duck"); } } class Ostrich extends Bird { // 타조는 날지 못함. @Override void fly() { System.out.println("I can't fly"); // 날지 못하는 메시지 출력 } } class BirdWatcher { void watchBird(Bird bird) { bird.fly(); } } public class Main { public static void main(String[] args) { // 새 객체 생성 Bird duck = new Duck(); Bird ostrich = new Ostrich(); // 새 감시자 객체 생성 BirdWatcher watcher = new BirdWatcher(); // 새 감시 watcher.watchBird(duck); // 출력: I'm flying like a duck watcher.watchBird(ostrich); // 출력: I can't fly }
Java
복사
모든 새 클래스가 Bird 클래스를 상속받는 것만으로도 BirdWatcher 클래스에서 사용될 수 있음을 의미.

특징

서브 타입은 기반 타입의 기능을 모두 제공.
서브 타입은 기반 타입의 기능을 변경하거나 제거해서는 안됨.

장단점

장점
코드의 재사용성과 유연성이 높아지며, 확장성이 향상.
다형성을 통해 코드의 재사용성이 증가.
단점
상속 구조를 변경하지 않고 기능을 확장하기 때문에 잘못된 상속 관계가 설정되어 있는 경우 수정이 어려울 수 있다.

만약 원칙을 지키지 않으면?

상속 관계가 잘못 설정되어 있거나, 서브 타입이 기반 타입의 기능을 충족시키지 못할 경우 예기치 않은 동작이 발생할 수 있다.

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

클라이언트는 자신이 사용하지 않는 메서드에 의존하도록 강요받아서는 안됨.
사용자 인터페이스가 여러 개의 구체적인 인터페이스보다는 하나의 범용 인터페이스에 의존해야 한다.
인터페이스는 클라이언트가 필요로 하는 기능만 제공해야 하며, 불필요한 메서드를 포함해서는 안 됨.

예시

// 인터페이스 분리 원칙을 준수하는 인터페이스들 interface Driveable { void drive(); } interface Flyable { void fly(); } // 자동차 클래스 class Car implements Driveable { @Override public void drive() { System.out.println("Driving a car"); } } // 비행기 클래스 class Plane implements Flyable { @Override public void fly() { System.out.println("Flying a plane"); } } public class Main { public static void main(String[] args) { // 자동차 객체 생성 Car car = new Car(); // 비행기 객체 생성 Plane plane = new Plane(); // 자동차 운전 car.drive(); // 출력: Driving a car // 비행기 비행 plane.fly(); // 출력: Flying a plane } }
Java
복사
Driveable 인터페이스와 Flyable 인터페이스를 따로 정의하여 ISP를 준수.
Car 클래스와 Plane 클래스는 각각 필요한 인터페이스를 구현하여 필요한 기능만을 가지고 있다.

장단점

장점
인터페이스가 작고 일관적이면 코드의 유지보수성이 향상.
구현 클래스의 의존성이 감소.
단점
인터페이스가 너무 작게 분리되면 구현 클래스가 많아져야 할 수 있다.

만약 원칙을 지키지 않으면?

클라이언트가 사용하지 않는 메서드에 의존할 수 있어서 코드의 유지보수가 어려워질 수 있다.
인쇄기능만 필요한 간단한 프린터를 사용하고싶음.
스캔 , 팩스 기능까지 구현도 제공햇을때 이 기능은 클라이언트가 사용하지 않는 기능인데 의존하게 만들어서
불필요한 구현에 부담을 주게된다.
지킬려면 프린터기능을 분리된 여러 인터페이스로 나눠서 필요한 기능에 해당하는 인터페이스만 구현하도록 함

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

상위 수준의 모듈은 하위 수준의 구체적인 구현에 의존해서는 안 되며, 둘 모두 추상화에 의존해야 한다.
이는 상위 수준 모듈이 하위 수준 모듈의 변경 사항에 민감하지 않도록 하는 것을 의미.
대신에, 두 모듈 모두 추상화된 인터페이스나 추상 클래스에 의존하여야 한다.
자동차는 바퀴와 엔진에 의존.
자동차 클래스가 직접 바퀴와 엔진의 구체적인 구현에 의존하는 것이 아니라
바퀴와 엔진의 '기능'을 정의하는 인터페이스에 의존.
더 쉽게 말하자면 운전자가 있고 자동차가 있다.
운전자는 면허증이 있으니 아무 자동차를 타도된다. 내가 sm7타고 다니다가 bmw로 바껴도 문제가없다.
bmw의 무슨 신기능까지 구체적으로 알 필요가 없는거다.

예시

// 인터페이스 정의 interface MessageService { String getMessage(); } // 구체적인 구현 클래스 class EmailService implements MessageService { @Override public String getMessage() { return "Email message"; } } class SMSService implements MessageService { @Override public String getMessage() { return "SMS message"; } } // 고수준 모듈 class Messenger { private final MessageService service; // 의존성 주입(Dependency Injection)을 통해 MessageService 객체를 주입받음 public Messenger(MessageService service) { this.service = service; } public void sendMessage() { String message = service.getMessage(); System.out.println("Sending message: " + message); } } public class Main { public static void main(String[] args) { // 의존성 주입을 통해 EmailService 객체를 생성하여 Messenger 객체에 주입 Messenger emailMessenger = new Messenger(new EmailService()); emailMessenger.sendMessage(); // 출력: Sending message: Email message // 의존성 주입을 통해 SMSService 객체를 생성하여 Messenger 객체에 주입 Messenger smsMessenger = new Messenger(new SMSService()); smsMessenger.sendMessage(); // 출력: Sending message: SMS message } }
Java
복사

장단점

장점
의존성이 추상화에 의존하므로 시스템의 유연성이 향상.
구현의 변경이나 교체가 쉽다.
단점
추상화와 인터페이스의 추가로 인해 초기 개발 속도가 느려질 수 있다.

만약 원칙을 지키지 않으면?

구현 클래스에 대한 의존성이 높아져 변경이 어려워진다.
단위 테스트가 어려워진다.