Java
S.O.L.I.D 원칙
yoooon1212
2024. 9. 26. 10:57
로버트 C. 마틴(Robert C. Martin), 흔히 "아저씨 보브(Uncle Bob)"로 알려진 소프트웨어 엔지니어가 발표한 객체 지향 프로그래밍 설계 원칙입니다.
즉, SOLID 원칙이란 객체지향 설계의 5가지 중요한 원칙을 뜻하며, 유지보수성과 확장성을 높이기 위해 설계 과정에서 따르는 지침입니다.
S 단일 책임 원칙 (Single Responsibility Principle, SRP)
- 클래스는 하나의 책임만 가져야 한다(일을 해야 한다).
- 하나의 책임이란 클래스가 변경되어야 하는 이유가 하나뿐이어야 한다는 의미입니다.
- 예를 들어, 주문을 처리하는 클래스가 있다고 할 때, 이 클래스는 주문과 관련된 기능만 해야하고 사용자 알림 같은 다른 책임도 동시에 맡지 않아야 합니다.
- 이 원칙이 중요한 이유는 여러 기능을 담당하는 클래스는 수정이 어려워집니다. 기능 중 하나를 수정하면 다른 기능에 문제가 생길 가능성이 크기 때문입니다.
class User {
private String name;
public void setName(String name) {
this.name = name;
}
}
// 이 클래스는 사용자 관리만 책임져야 함. 예를 들어,
// 데이터베이스 저장 로직이 포함되면 단일 책임을 위반.
class UserService {
public void save(User user) {
// save logic
}
}
잘못된 예 (SRP 위반)
class UserService {
private DBConnection dbConnection;
public void save(User user) {
dbConnection = new DBConnection(); // DB 연결을 직접 처리
dbConnection.save(user); // 데이터 저장 로직도 직접 처리
}
}
SRP를 준수하기 위해서는 데이터베이스와의 상호작용을 다른 클래스로 분리하고, UserService는 그 클래스를 통해 상호작용해야 합니다. 이렇게 하면 사용자 관리와 데이터 저장의 책임이 분리됩니다.
올바른 예 (SRP 준수)
// DB 관련 로직은 별도의 클래스에서 담당
class UserRepository {
private DBConnection dbConnection;
public UserRepository(DBConnection dbConnection) {
this.dbConnection = dbConnection;
}
public void save(User user) {
dbConnection.save(user);
}
}
// 사용자 관리만 담당하는 UserService 클래스
class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void saveUser(User user) {
userRepository.save(user); // UserService는 저장을 위임만 함
}
}
O 개방-폐쇄 원칙 (Open-Closed Principle, OCP)
- 소프트웨어 엔티티(클래스, 모듈, 함수 등)(코드)는 확장에 열려 있고, 변경(수정)에는 닫혀 있어야 한다.
- 새로운 기능을 추가할 때 기존 코드를 수정하지 않고 확장할 수 있어야 한다는 뜻입니다.
- 예를 들어, 할인 정책을 관리하는 시스템에서 처음에는 고정 할인만 적용하다가 후에 퍼센트 할인 정책을 추가해야 한다고 가정해 봅시다. 새로운 할인 정책을 추가할 때 기존의 할인 코드 부분을 수정하지 않고, 새로운 클래스만 추가하여 확장할 수 있어야 OCP를 지킨 설계입니다.
interface Shape {
double area();
}
class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
// 오버라이드
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle implements Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
// 오버라이드
public double area() {
return width * height;
}
}
class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.area();
}
}
L 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
- 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다.
- 자식 클래스가 부모 클래스의 행동을 변경해서는 안 되며, 부모 클래스 대신 자식 클래스를 사용해도 프로그램이 정상적으로 작동해야 한다는 뜻입니다.
- 예를 들어, 동물이라는 부모 클래스가 있고 그 하위에 개와 고양이 클래스가 있다면 동물로 선언된 변수에 개나 고양이 객체를 넣어도 모든 메서드가 문제없이 동작해야 합니다. 만약 자식 클래스가 부모의 기능을 다르게 하거나, 아예 사용할 수 없게 한다면 LSP를 어긴 것입니다.
class Bird {
public void fly() {
System.out.println("새늘 날아다릴 수 있어요");
}
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("펭귄은 못 날아요");
}
}
Penguin은 Bird 클래스를 상속했지만 fly() 메서드를 정상적으로 구현하지 못하므로 LSP를 위반합니다.
올바른 예시 (LSP 준수)
해결책은 Bird 클래스에 공통 동작으로 날 수 있는 기능을 넣는 대신, 새의 특성에 따라 상속 구조를 분리하거나, 날 수 있는지 여부를 결정할 수 있는 더 적절한 설계를 도입하는 것입니다.
interface Flyable {
void fly();
}
class Bird {
public void eat() {
System.out.println("Bird is eating");
}
}
class Sparrow extends Bird implements Flyable {
@Override
public void fly() {
System.out.println("Sparrow is flying");
}
}
class Penguin extends Bird {
// Penguin 클래스는 Flyable을 구현하지 않음, 즉 fly() 메서드가 없음
}
I 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
- 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 않아야 한다.
- 큰 인터페이스보다 구체적이고 작은 인터페이스로 나누는 것이 좋습니다.
- 예를 들어, TV와 라디오가 있다고 가정해 봅시다. 둘 다 전원 켜기/끄기 기능이 있지만, 라디오에는 채널 조정 기능도 있습니다. 전원 인터페이스와 채널 조정 인터페이스를 분리해 사용하는 것이 더 나은 설계입니다. TV는 전원 기능만 필요하고, 라디오는 두 가지 인터페이스를 모두 구현하면 됩니다.
interface Worker {
void work();
void eat();
}
// 잘못된 예: 한 인터페이스에 너무 많은 역할을 포함
class Robot implements Worker {
public void work() {
// robot working
}
public void eat() {
// robot can't eat, 위반된 예
}
}
// 더 나은 설계
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class Human implements Workable, Eatable {
public void work() {
// human working
}
public void eat() {
// human eating
}
}
class Robot implements Workable {
public void work() {
// robot working
}
}
D 의존성 역전 원칙 (Dependency Inversion Principle, DIP)
- 고수준 모듈은 저수준 모듈에 의존해서는 안 되고, 둘 다 추상화에 의존해야 한다.
- 클래스는 구체적인 것이 아닌, 추상적인 것(인터페이스나 추상 클래스)에 의존해야 한다는 뜻입니다. 상위 레벨의 모듈(전체 로직을 관리하는 부분)이 하위 레벨 모듈(세부 기능)과 직접 의존하지 않도록 설계해야 합니다.
- 예를 들어, 결제 시스템을 설계할 때, 신용카드 결제 기능에 직접 의존하면, 나중에 페이팔 같은 다른 결제 방식을 추가할 때 기존 코드를 많이 수정해야 합니다. 대신 결제라는 인터페이스를 만들고, 신용카드와 페이팔 결제 클래스는 이 인터페이스를 구현하게 하면, 새로운 결제 방식 추가가 훨씬 쉬워집니다.
interface MessageSender {
void send(String message);
}
class EmailSender implements MessageSender {
public void send(String message) {
System.out.println("Sending email: " + message);
}
}
class NotificationService {
private MessageSender sender;
public NotificationService(MessageSender sender) {
this.sender = sender;
}
public void sendNotification(String message) {
sender.send(message);
}
}