S - Single Responsibility Principle (SRP) : 정의: 클래스는 단 하나의 책임만 가져야 한다.
O - Open/Closed Principle (OCP) : 소프트웨어 엔티티는 확장에는 열려 있어야 하지만 수정에는 닫혀 있어야 한다.
L - Liskov Substitution Principle (LSP) : 자식 클래스는 부모 클래스에서 호출하는 동작에 대체가 가능해야 한다.
I - Interface Segregation Principle (ISP) : 하나의 일반적인 인터페이스보다는 여러 개의 구체적인 인터페이스가 더 좋다.
D - Dependency Inversion Principle (DIP) : 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다.
Single Responsibility Principle (SRP)
•
정의: 클래스는 단 하나의 책임만 가져야 한다.
public boolean validateOrder(Order order) {
if (order.getItems().size() == 0) {
log.info("주문 항목이 없습니다.");
return false;
} else {
if (order.getTotalPrice() > 0) {
if (!order.hasCustomerInfo()) {
log.info("사용자 정보가 없습니다.");
return false;
} else {
return true;
}
} else if (!(order.getTotalPrice() > 0)) {
log.info("올바르지 않은 총 가격입니다.");
return false;
}
}
return true;
}
Java
복사
클래스에는 단 하나의 책임만 가져야 한다.
현재 클래스를 전체가 작성 되어 있지 않지만, 각각의 유호성체크를 Order에서 만 진행하는 것이 과연 단 하나의 책임인가? 라는 생각이 듭니다.
Order, Item, User 각각의 객체의 역할과 책임을 부여하고 유효한 값인가? 물어보면 응답값을 확인하면 된다고 생각하여 아래와 같이 설계하였습니다.
Item
Item.java
Items
Items.java
User
user.java
@Slf4j
@Getter
public class Order {
private Items items;
private User user;
@Builder
public Order(Items items, User user) {
this.items = items;
this.user = user;
}
public boolean validateOrder() {
if (items.isItemNotAvailable()) {
log.info("주문 항목이 없습니다.");
return false;
}
if (items.isInvalidTotalPrice()) {
log.info("올바르지 않은 총 가격입니다.");
return false;
}
if (user.isNullCustomerInfo()) {
log.info("사용자 정보가 없습니다.");
return false;
}
return true;
}
}
Java
복사
O - Open/Closed Principle (OCP)
소프트웨어 엔티티는 확장에는 열려 있어야 하지만 수정에는 닫혀 있어야 한다.
•
기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 한다.
abstract class Shape {
public abstract double area();
}
class Rectangle extends 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 extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
class AreaCalculator {
public static double calculateArea(List<Shape> shapes) {
double totalArea = 0;
for (Shape shape : shapes) {
totalArea += shape.area();
}
return totalArea;
}
}
Java
복사
•
Shape 클래스:
◦
추상 클래스 Shape는 모든 도형의 기본 형태를 정의합니다. 여기서 area() 메서드는 각 도형의 면적을 계산하는 추상 메서드로 선언되어 있습니다. 이는 다양한 도형 클래스들이 공통으로 구현해야 하는 메서드를 정의합니다.
•
Rectangle 및 Circle 클래스:
◦
Rectangle과 Circle 클래스는 Shape 클래스를 상속받아 각각의 도형에 맞는 면적 계산 로직을 구현합니다. 이 두 클래스는 Shape의 추상 메서드인 area()를 오버라이드하여 자신들의 면적을 계산합니다.
◦
새 도형을 추가하고 싶다면, 예를 들어 Triangle 클래스를 추가할 수 있습니다. 이 경우 Triangle 클래스는 Shape를 상속받고 area() 메서드를 구현하면 됩니다. 기존의 Rectangle이나 Circle 클래스는 수정할 필요가 없습니다.
•
AreaCalculator 클래스:
◦
AreaCalculator 클래스는 List<Shape>를 인자로 받아 각 도형의 면적을 계산하여 총합을 반환합니다. 이 클래스는 Shape 타입의 객체들만을 처리하기 때문에 새로운 도형을 추가하더라도 기존의 AreaCalculator 클래스는 그대로 사용할 수 있습니다.
L - Liskov Substitution Principle (LSP)
자식 클래스는 부모 클래스에서 호출하는 동작에 대체가 가능해야 한다.
class Bird {
public void fly() {
System.out.println("Flying");
}
}
class Parrot extends Bird {
@Override
public void fly() {
// 추가적인 메서드나 속성을 구현할 수 있습니다.
throw new UnsupportedOperationException("Ostrich can't fly!");
}
}
class Ostrich extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Ostrich can't fly!");
}
}
class BirdHandler {
public void letBirdFly(Bird bird) {
bird.fly();
}
}
Java
복사
I - Interface Segregation Principle (ISP)
하나의 일반적인 인터페이스보다는 여러 개의 구체적인 인터페이스가 더 좋다.
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Sparrow implements Flyable {
@Override
public void fly() {
System.out.println("Sparrow is flying");
}
}
class Penguin implements Swimmable {
@Override
public void swim() {
System.out.println("Penguin is swimming");
}
}
Java
복사
•
ISP는 인터페이스를 클라이언트의 필요에 맞게 분리하는 원칙입니다.
•
새의 경우, Flyable과 Swimmable로 나누어 각 새가 필요하지 않은 메서드를 구현하지 않도록 했습니다.
•
이를 통해 각 클래스는 자신의 특성에 맞는 기능만을 구현하게 되어 코드의 유연성, 가독성, 유지보수성을 높일 수 있습니다. ISP를 준수하면 각 인터페이스가 명확한 책임을 가지게 되어 소프트웨어의 구조가 더 나아집니다.
Dependency Inversion Principle (DIP)
고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다.
interface Flyable {
void fly();
}
class Sparrow implements Flyable {
@Override
public void fly() {
System.out.println("Sparrow is flying");
}
}
class Eagle implements Flyable {
@Override
public void fly() {
System.out.println("Eagle is soaring high");
}
}
class BirdHandler {
private Flyable bird;
public BirdHandler(Flyable bird) {
this.bird = bird;
}
public void letBirdFly() {
bird.fly();
}
}
// 사용 예
public class Main {
public static void main(String[] args) {
Flyable sparrow = new Sparrow();
BirdHandler sparrowHandler = new BirdHandler(sparrow);
sparrowHandler.letBirdFly();
Flyable eagle = new Eagle();
BirdHandler eagleHandler = new BirdHandler(eagle);
eagleHandler.letBirdFly();
}
}
Java
복사
•
유연성: 새로운 비행 가능한 새(예: Penguin, 만약 비행 기능을 추가한다고 가정)를 추가할 때, 기존의 BirdHandler 클래스를 수정할 필요 없이 새로운 클래스를 구현하고 Flyable 인터페이스를 사용하면 됩니다.
•
테스트 용이성: Flyable 인터페이스를 구현한 Mock 클래스를 만들어 유닛 테스트를 수행할 수 있습니다. 예를 들어, 비행하는 대신 특정 로직을 테스트하기 위해 Mock 비행 클래스를 만들 수 있습니다.
•
코드의 응집도: BirdHandler는 구체적인 비행 가능한 새 클래스에 의존하지 않기 때문에, 변경 사항이 있을 때 그 영향을 최소화합니다.