SOLID 원칙
객체 지향 설계의 5가지 기본 원칙, 소프트웨어 설계에서 유지보수성, 확장성, 유연성을 높이기 위한 지침을 제공한다.
SOLID 원칙의 종류
1. 단일 책임 원칙 SRP(Single Responsibility Principle)
- 하나의 클래스는 하나의 책임만 가져야 한다.
- 클래스는 한 가지 기능에 집중해야 하며, 그 외의 기능을 담당하지 않아야한다.
- 예시
- User 클래스는 사용자 정보 관리, 로그인 및 데이터베이스 저장 책임을 동시에 가지고 있다.
public class User {
private String name; // 사용자 정보
public void login() { /* 로그인 기능 */ }
public void saveUser() { /* 데이터베이스 저장 기능 */ }
}
public class User { /* 사용자 정보 관리 */ }
public class AuthService {
public void login(User user) { /* 로그인 기능 */ }
}
public class UserRepository {
public void saveUser(User user) { /* 데이터베이스 저장 */ }
}
- 실제로는 상황에 따라 책임의 크기가 달라진다.
- 클래스가 변경될 때 파급 효과가 작으면 된다.
2. 개방 패쇄 원칙 OCP(Open Closed Principle)
- 소프트웨어 요소는 확장에는 열려있어야 하고, 수정에는 닫혀있어야 한다.
- 새로운 기능을 추가할 때 기존 코드를 수정하지 않고, 확장 할 수 있도록 설계해야 한다.
- 새로운 도형이 추가될 때 마다 AreaCalculator 클래스를 수정해야 한다.
public class Shape {
public String type;
}
public class AreaCalculator {
public double calculate(Shape shape) {
if (shape.type.equals("circle")) {
return /* 원의 넓이 계산 */;
} else if (shape.type.equals("square")) {
return /* 사각형의 넓이 계산 */;
}
}
}
public interface Shape {
double calculateArea();
}
public class Circle implements Shape {
public double calculateArea() { return /* 원의 넓이 계산 */; }
}
public class Square implements Shape {
public double calculateArea() { return /* 사각형의 넓이 계산 */; }
}
public class AreaCalculator {
public double calculate(Shape shape) {
return shape.calculateArea();
}
}
- 새로운 도형이 추가되더라도 Shape 인터페이스만 구현하면 된다.
- AreaCalculator는 수정할 필요가 없다.
- 다형성을 활용하여 해결한다.
- 인터페이스를 implements하여 구현한 새로운 클래스를 만들어서 새로운 기능을 구현한다.
- 역할(도형)과 구현(원, 사각형, 삼각형 등)을 분리하면 된다.
- 문제점
// Circle을 계산하는 경우
public class Main {
public static void main(String[]) {
AreaCalculator areaCalculator = new AreaCalculator();
Circle circle = new Circle();
areaCalculator.calculate(circle);
}
}
// Square를 계산하는 경우
public class Main {
public static void main(String[]) {
AreaCalculator areaCalculator = new AreaCalculator();
// Circle circle = new Circle();
Square square = new Square();
areaCalculator.calculate(square);
}
}
- 구현객체를 변경하기 위해서는 해당 코드를 사용하는 클라이언트 측의 코드를 변경해야 한다.
- 객체의 생성, 사용 등을 자동으로 설정해주는 무엇인가가 필요하다
3. 리스크프 치환 원칙 LSP (Liskov Substitution Principle)
- 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다.
- 부모 클래스를 사용하는 곳에서 자식 클래스를 사용해도 프로그램의 동작에 문제가 없어야 한다.
- 예시
- ElectricCar는 Car 클래스를 상속 받았지만, accelerate()를 사용할 수 없다. (LSP위반)
class Car {
public void accelerate() {
System.out.println("자동차가 휘발유로 가속합니다.");
}
}
class ElectricCar extends Car {
@Override
public void accelerate() {
throw new UnsupportedOperationException("전기차는 이 방식으로 가속하지 않습니다.");
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.accelerate(); // "자동차가 가속합니다."
Car electricCar = new ElectricCar();
electricCar.accelerate(); // UnsupportedOperationException 발생
}
}
// 가속 기능(역할)을 인터페이스로 분리
interface Acceleratable {
void accelerate();
}
class Car implements Acceleratable {
@Override
public void accelerate() {
System.out.println("내연기관 자동차가 가속합니다.");
}
}
class ElectricCar implements Acceleratable {
@Override
public void accelerate() {
System.out.println("전기차가 배터리로 가속합니다.");
}
}
public class Main {
public static void main(String[] args) {
Acceleratable car = new Car();
car.accelerate(); // "내연기관 자동차가 가속합니다."
Acceleratable electricCar = new ElectricCar();
electricCar.accelerate(); // "전기차가 배터리로 가속합니다."
}
}
- 인터페이스를 구현한 구현체를 믿고 사용할 수 있도록 만들어준다.
- 엑셀은 앞으로 가는 기능이다. 만약 뒤로 간다면 LSP를 위반한다.
4. 인터페이스 분리 원칙 ISP(Interface Segregation Principle)
- 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
- 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
- 즉, 하나의 큰 인터페이스보다는 여러 개의 작은 인터페이스로 분리해야 한다.
- 예시
- Dog 클래스는 fly()메서드를 사용하지 않지만 구현해야 한다.
public interface Animal {
void fly();
void run();
void swim();
}
public class Dog implements Animal {
public void fly() { /* 사용하지 않음 */ }
public void run() { /* 달리기 */ }
public void swim() { /* 수영 */ }
}
public interface Runnable {
void run();
}
public interface Swimmable {
void swim();
}
public class Dog implements Runnable, Swimmable {
public void run() { /* 달리기 */ }
public void swim() { /* 수영 */ }
}
- 인터페이스가 명확해진다.
- Spring의 기능들은 대부분 인터페이스로 분리되어 있다.
5. 의존관계 역전 원칙 DIP(Dependency Inversion Principle)
- 구체적인 클래스에 의존하지 말고, 인터페이스나 추상 클래스에 의존하도록 설계해야 한다.
- 예시
- NotificationService는 EmailNotifier 클래스를 의존한다.
// Email 알림 클래스
class EmailNotifier {
public void sendEmail(String message) {
System.out.println("Email 알림: " + message);
}
}
// 알림 시스템
class NotificationService {
private EmailNotifier emailNotifier;
public NotificationService() {
// 구체적인 클래스인 EmailNotifier에 의존
this.emailNotifier = new EmailNotifier();
}
public void sendNotification(String message) {
emailNotifier.sendEmail(message);
}
}
public class Main {
public static void main(String[] args) {
NotificationService service = new NotificationService();
service.sendNotification("안녕하세요! 이메일 알림입니다.");
}
}
- 이메일 알림이 아닌 SMS 알림과 같은 기능이 추가되면 NotificationService 는 수정되어야 한다. DIP 위반
- 의존관계 역전 원칙 적용
// 알림 인터페이스(추상화)
interface Notifier {
void send(String message);
}
// Email 알림 클래스
class EmailNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("Email 알림: " + message);
}
}
// SMS 알림 클래스
class SMSNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("SMS 알림: " + message);
}
}
// 알림 서비스 (높은 수준 모듈)
class NotificationService {
// 추상화된 인터페이스에 의존
private Notifier notifier;
// 의존성 주입 (생성자를 통해 주입)
public NotificationService(Notifier notifier) {
this.notifier = notifier;
}
public void sendNotification(String message) {
// notifier가 어떤 구현체인지 상관하지 않음
notifier.send(message);
}
}
public class Main {
public static void main(String[] args) {
// Email 알림을 사용
Notifier emailNotifier = new EmailNotifier();
NotificationService emailService = new NotificationService(emailNotifier);
emailService.sendNotification("안녕하세요! 이메일 알림입니다.");
// SMS 알림을 사용
Notifier smsNotifier = new SMSNotifier();
NotificationService smsService = new NotificationService(smsNotifier);
smsService.sendNotification("안녕하세요! SMS 알림입니다.");
}
}
- 추상화된
Notifier
인터페이스에만 의존한다.
- 새로운 알림 방식이 추가되어도
NotificationService
는 변경되지 않아도 된다.
- 필요한
Notifier
객체를 외부에서 주입받는다.
NotificationService
는 어떤 알림 방식을 사용할지에 대한 세부 사항을 몰라도 되므로, 의존성이 약해진다.
- 모듈간의 결합도를 낮추고 유연성과 확장성을 높일 수 있다.
- 서로의 변경 사항에 독립적이어서 변경에 유연하다.