"SOLID"는 소프트웨어 개발에서 사용되는 객체 지향 프로그래밍과 설계의 원칙들을 가리키는 약어로 아래와 같다.
- SRP(Single Responsibility Principle): 단일 책임 원칙
- OCP(Open Closed Priciple): 개방 폐쇄 원칙
- LSP(Listov Substitution Priciple): 리스코프 치환 원칙
- ISP(Interface Segregation Principle): 인터페이스 분리 원칙
- DIP(Dependency Inversion Principle): 의존 역전 원칙
Single Responsibility Principle (SRP): 각 클래스는 하나의 책임만을 가져야 합니다. 이 원칙은 클래스가 변경되어야 하는 이유가 하나만 있어야 함을 의미합니다.
SRP를 위반하는 클래스를 생각해봅시다. 'Employee'라는 클래스는 직원의 상세 정보를 관리하고, 그 정보를 데이터베이스에 저장하며, 그 정보를 콘솔에 출력하는 기능을 가지고 있다고 가정해 봅시다.
public class Employee {
private String name;
private String address;
private int salary;
// constructor, getters and setters...
public void saveEmployee() {
// code to save employee details to database
}
public void displayEmployee() {
// code to print employee details to console
}
}
이 클래스는 SRP를 위반합니다. 왜냐하면 이 클래스는 두 가지 이유로 변경될 수 있기 때문입니다: 하나는 직원 데이터의 관리 방식이 변경될 때, 다른 하나는 직원 데이터를 표시하는 방식이 변경될 때입니다.
따라서 SRP에 따르면, 이 두 가지 책임은 각각 별도의 클래스로 분리되어야 합니다:
public class Employee {
private String name;
private String address;
private int salary;
// constructor, getters and setters...
}
public class EmployeeDB {
public void saveEmployee(Employee employee) {
// code to save employee details to database
}
}
public class EmployeeConsoleReporter {
public void displayEmployee(Employee employee) {
// code to print employee details to console
}
}
각 클래스는 각자의 책임에만 집중하며, 변경의 이유가 하나뿐인 클래스를 유지할 수 있습니다. 이것이 바로 Single Responsibility Principle입니다.
Open-Closed Principle (OCP): 소프트웨어의 개체(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하지만, 코드의 변경에는 닫혀 있어야 합니다. 즉, 기존의 코드를 변경하지 않고도 새로운 기능을 추가할 수 있어야 합니다.
public class Rectangle {
public double height;
public double width;
}
public class AreaCalculator {
public double calculateRectangleArea(Rectangle rectangle) {
return rectangle.height * rectangle.width;
}
}
위 코드에서는 AreaCalculator가 Rectangle의 면적을 계산합니다. 이제 새로운 도형, 예를 들어 원의 면적을 계산해야 한다고 가정해 보겠습니다. 이 경우, AreaCalculator를 수정해야 하는 문제가 생깁니다. 이러한 접근 방식은 OCP를 위반합니다.
OCP를 준수하기 위해, 우리는 도형들이 공통적으로 가질 수 있는 인터페이스를 만들고, 각 도형이 그 인터페이스를 구현하게 만들 수 있습니다. 그리고 AreaCalculator는 이 인터페이스에 의존하도록 만들 수 있습니다.
public interface Shape {
double calculateArea();
}
public class Rectangle implements Shape {
public double height;
public double width;
@Override
public double calculateArea() {
return height * width;
}
}
public class Circle implements Shape {
public double radius;
@Override
public double calculateArea() {
return Math.PI * Math.pow(radius, 2);
}
}
public class AreaCalculator {
public double calculateShapeArea(Shape shape) {
return shape.calculateArea();
}
}
이제 새로운 도형이 추가되어도 AreaCalculator 클래스를 변경할 필요가 없습니다. 각 도형 클래스는 Shape 인터페이스를 구현하므로 AreaCalculator는 이 인터페이스에 의존하게 됩니다. 이렇게 하면 코드는 확장에는 열려 있고 수정에는 닫혀 있게 됩니다, 즉 OCP를 만족하게 됩니다.
Liskov Substitution Principle (LSP): 프로그램에서 부모 클래스를 자식 클래스로 바꿔도 프로그램이 정상적으로 작동해야 합니다. 즉, 하위 유형은 그들의 기반(또는 상위) 유형을 대체할 수 있어야 합니다.
먼저, LSP를 위반하는 예를 들어보겠습니다.
public class Bird {
public void fly() {
System.out.println("Flying...");
}
}
public class Ostrich extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Ostrich can't fly.");
}
}
Ostrich : 타조는 날 수 없다
여기서 Ostrich는 Bird를 상속받았지만, fly 메소드를 호출하면 예외가 발생합니다. 이는 LSP를 위반하는 예입니다. 왜냐하면 Bird 타입의 객체를 Ostrich 타입의 객체로 바꿔도 프로그램이 정상적으로 작동해야 하는데, 그렇지 않기 때문입니다.
이 문제를 해결하기 위해, 우리는 Bird 클래스를 두 개의 서브 클래스로 분리할 수 있습니다: FlyingBird와 NonFlyingBird. Ostrich는 NonFlyingBird를 상속받고, 나머지 새들은 FlyingBird를 상속받을 수 있습니다.
public class Bird {
// common attributes and methods of all birds
}
public class FlyingBird extends Bird {
public void fly() {
System.out.println("Flying...");
}
}
public class NonFlyingBird extends Bird {
// other methods specific to non-flying birds
}
public class Ostrich extends NonFlyingBird {
// methods specific to Ostrich
}
public class Sparrow extends FlyingBird {
// methods specific to Sparrow
}
이렇게 하면, Ostrich 객체를 NonFlyingBird 타입의 객체로 바꿔도 프로그램이 여전히 정상적으로 작동합니다. 이것이 바로 Liskov Substitution Principle입니다.
Interface Segregation Principle (ISP): 많은 클라이언트 전용 인터페이스가 하나의 일반적인 인터페이스보다 낫다는 원칙입니다. 이는 사용자가 필요로 하지 않는 메소드에 의존하지 않도록 하는 것을 목표로 합니다.
먼저, ISP를 위반하는 예를 살펴봅시다.
public interface Worker {
void work();
void eat();
}
public class HumanWorker implements Worker {
public void work() {
// working...
}
public void eat() {
// eating during lunch break...
}
}
public class RobotWorker implements Worker {
public void work() {
// working...
}
public void eat() {
throw new UnsupportedOperationException("Robots can't eat.");
}
}
여기서 RobotWorker는 eat 메서드를 구현해야 하지만, 실제로는 로봇이 먹는 기능을 수행할 수 없습니다. 이는 ISP를 위반하는 예입니다. 이 문제를 해결하기 위해, 우리는 인터페이스를 더 잘 분리할 수 있습니다:
public interface Worker {
void work();
}
public interface Eater {
void eat();
}
public class HumanWorker implements Worker, Eater {
public void work() {
// working...
}
public void eat() {
// eating during lunch break...
}
}
public class RobotWorker implements Worker {
public void work() {
// working...
}
}
Dependency Inversion Principle (DIP): 상위 수준의 모듈은 하위 수준의 모듈에 의존하면 안 됩니다. 모두가 추상화에 의존해야 합니다. 이는 구체적인 클래스보다 인터페이스나 추상 클래스에 의존하도록 함으로써 달성됩니다.
DIP를 위반하는 예를 살펴봅시다:
public class EmailService {
public void sendEmail(String message, String receiver){
// logic to send email
}
}
public class Notification {
private EmailService emailService;
public Notification(){
this.emailService = new EmailService();
}
public void promote(String message, String receiver){
this.emailService.sendEmail(message, receiver);
}
}
위의 코드에서 Notification 클래스는 EmailService 클래스에 직접 의존하고 있습니다. 이는 DIP를 위반하는 것으로, 이 경우 Notification 클래스는 EmailService 클래스의 구현에 강하게 결합되어 있습니다.
이 문제를 해결하기 위해, 우리는 메시지 전송 기능에 대한 인터페이스를 정의하고, Notification 클래스가 이 인터페이스에 의존하게 만들 수 있습니다.
public interface MessageService {
void sendMessage(String message, String receiver);
}
public class EmailService implements MessageService {
public void sendMessage(String message, String receiver) {
// logic to send email
}
}
public class Notification {
private MessageService messageService;
public Notification(MessageService svc){
this.messageService = svc;
}
public void promote(String message, String receiver) {
this.messageService.sendMessage(message, receiver);
}
}
이제 Notification 클래스는 EmailService가 아닌 MessageService 인터페이스에 의존하므로, 이메일 뿐만 아니라 다른 메시지 서비스를 사용하려면 Notification 클래스를 변경할 필요가 없습니다. 이것이 바로 Dependency Inversion Principle입니다.
'Backend' 카테고리의 다른 글
openApi 활용(4) : servlet redirection, GeoLocation, 버튼 구현 (0) | 2023.04.17 |
---|---|
openApi 활용(3) DB 연동하기 (0) | 2023.04.16 |
openApi 활용(2) : gson (0) | 2023.04.11 |
openApi 활용(1) : okhttp3 사용해보기 (0) | 2023.04.11 |