본문 바로가기

TIL

SOLID 원칙

SOLID의 다섯가지 원칙은 시간이 지나도 유지보수와 확장이 용이하고, 이해하기 쉽고 유연한 소프트웨어를 만들기 위한 객체 지향 설계 원익의 모음입니다

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

클래스는 단 하나의 책임만 가져야 한다.

 

말 그대로 클래스에는 단 하나만의 책임을 가지고 있도록 설계해야 합니다.

 

아래와 같이 직원이 정보관리, 급여 계산, 저장, 생성 등 많은 책임을 가지게 설계해서는 안됩니다

오케이 이런 규칙이 있다는것은 알겠는데 이런 규칙이 왜 필요할까요?

1. 높은 유지보수, 코드의 이해 및 가독성 향상

2. 쉬운 테스트와 디버깅

3. 연쇄 효과 최소화 (Reduced Coupling):  클래스의 변경이 다른 연관된 클래스들의 수정을 유발하는 '폭포 효과(Ripple Effects)' 줄여줍니다.

4. 결합도 감소 응집도 증가: 클래스 내의 모든 메서드가 하나의 책임에 집중하여 클래스 내부의 응집도는 높아지고, 다른 클래스와의 결합도는 낮아집니다.

 

저는 이중에 3,4번이 가장 중요하다고 생각합니다.

이전에 이런 단일 책임 원칙을 고려하지 않고 코딩을했다가 수정 사항이 있을때 이파일 하나 수정하니까 저파일 컴파일에러나고,,
그거 수정하니 또 다음 파일 에러나고...그냥 타입하나 추가했을뿐인데 난리난 경험이있어서 진짜 고생한 기억이있습니다.

이 책임 분리를 얼마나 잘 하느냐가 추후 유지보수를하는데 아주 중요한 키가 될 수 있다고 생각합니다.

// 너무 많은 책임을 가진 클래스
public class Employee {
    private String name;
    private double salary;
    
    // 책임 1: 직원 정보 관리
    public void setName(String name) {
        this.name = name;
    }
    
    // 책임 2: 급여 계산
    public double calculatePay() {
        return salary * 1.1;
    }
    
    // 책임 3: 데이터베이스 저장
    public void saveToDatabase() {
        // DB 저장 로직
        System.out.println("Saving to database...");
    }
    
    // 책임 4: 리포트 생성
    public void generateReport() {
        System.out.println("Generating report...");
    }
}

 

올바르게 설계된 코드

// 직원 정보만 관리
public class Employee {
    private String name;
    private double salary;
    
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public double getSalary() { return salary; }
}

// 급여 계산 담당
public class PayrollCalculator {
    public double calculatePay(Employee employee) {
        return employee.getSalary() * 1.1;
    }
}

// 데이터베이스 관리 담당
public class EmployeeRepository {
    public void save(Employee employee) {
        System.out.println("Saving " + employee.getName() + " to database");
    }
}

// 리포트 생성 담당
public class ReportGenerator {
    public void generateReport(Employee employee) {
        System.out.println("Generating report for " + employee.getName());
    }
}

 

2. Open/Closed Principle(개방/폐쇄 원칙)

"클래스는 확장에는 열려있고, 수정에는 닫혀있어야 한다”

확장은 가능하도록 열려 있어야 하고, 변경이 필요할 경우 기존 코드를 직접 수정하기보단 확장을 통해 기능을 추가할 수 있도록 설계해야 한다고 생각합니다.

 

마찬가지로 이는 가독성을 높이고 유지보수에 용이하다는 장점이있습니다.

또한 유연한 확장이 가능하며 변경에 닫혀 있는 코드는 안정적인 컴포넌트로 기능하여 다른 프로젝트나 모듈에서 안심하고 재사용할 있습니다.

또한 확장시 기존 코드를 수정할 필요가 없으므로 여러 개발자가 작업 시 충돌을 줄이고 독립적으로 개발할 수 있습니다.

public class DiscountCalculator {
    public double calculateDiscount(String customerType, double amount) {
        if (customerType.equals("REGULAR")) {
            return amount * 0.05;
        } else if (customerType.equals("VIP")) {
            return amount * 0.15;
        } else if (customerType.equals("PREMIUM")) {  // 새 타입 추가시 코드 수정 필요
            return amount * 0.20;
        }
        return 0;
    }
}

 

// 추상화를 통한 확장 가능한 설계
public interface DiscountStrategy {
    double calculateDiscount(double amount);
}

public class RegularCustomerDiscount implements DiscountStrategy {
    @Override
    public double calculateDiscount(double amount) {
        return amount * 0.05;
    }
}

public class VIPCustomerDiscount implements DiscountStrategy {
    @Override
    public double calculateDiscount(double amount) {
        return amount * 0.15;
    }
}

// 새로운 할인 정책 추가시 기존 코드 수정 없이 확장 가능
public class PremiumCustomerDiscount implements DiscountStrategy {
    @Override
    public double calculateDiscount(double amount) {
        return amount * 0.20;
    }
}

public class DiscountCalculator {
    public double calculateDiscount(DiscountStrategy strategy, double amount) {
        return strategy.calculateDiscount(amount);
    }
}

 

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

"자식 클래스는 부모 클래스를 대체할 수 있어야 한다”

해당 원칙은 다형성을 보장해줄 수 있고, 시스템 안정성을 확보합니다.

 

 시스템 안정성 확보 (Fragile Base Class 방지): 부모 클래스를 변경하거나 자식 클래스를 추가할 , 하위 클래스의 예기치 않은 동작으로 인한 시스템 오류를 예방합니다.

이는 코드로 보면 더 이해하기 쉬울 거같습니다.

public class Rectangle {
    protected int width;
    protected int height;
    
    public void setWidth(int width) {
        this.width = width;
    }
    
    public void setHeight(int height) {
        this.height = height;
    }
    
    public int getArea() {
        return width * height;
    }
}

// 정사각형은 가로와 세로가 같아야 하는데...
public class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width;  // 높이도 같이 변경
    }
    
    @Override
    public void setHeight(int height) {
        this.width = height;  // 너비도 같이 변경
        this.height = height;
    }
}

// 문제 상황
public class TestLSP {
    public static void testRectangle(Rectangle rect) {
        rect.setWidth(5);
        rect.setHeight(10);
        // Rectangle이라면 50이어야 하지만, Square라면 100이 됨
        System.out.println("Area: " + rect.getArea()); 
    }
}
// 공통 인터페이스 사용
public interface Shape {
    int getArea();
}

public class Rectangle implements Shape {
    private int width;
    private int height;
    
    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }
    
    @Override
    public int getArea() {
        return width * height;
    }
}

public class Square implements Shape {
    private int side;
    
    public Square(int side) {
        this.side = side;
    }
    
    @Override
    public int getArea() {
        return side * side;
    }
}

 

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

"클라이언트는 자신이 사용하지 않는 메서드에 의존하면 안 된다”

불필요한 의존성을 제거하며, 변경의 영향에 최소화 할 수 있습니다.

// 너무 많은 기능을 가진 인터페이스
public interface Worker {
    void work();
    void eat();
    void sleep();
    void receiveSalary();
}

// 로봇은 먹거나 자지 않는데도 구현해야 함
public class Robot implements Worker {
    @Override
    public void work() {
        System.out.println("Robot working");
    }
    
    @Override
    public void eat() {
        // 로봇은 먹지 않음 - 불필요한 구현
        throw new UnsupportedOperationException();
    }
    
    @Override
    public void sleep() {
        // 로봇은 자지 않음 - 불필요한 구현
        throw new UnsupportedOperationException();
    }
    
    @Override
    public void receiveSalary() {
        // 로봇은 급여를 받지 않음
        throw new UnsupportedOperationException();
    }
}
// 인터페이스를 작은 단위로 분리
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public interface Sleepable {
    void sleep();
}

public interface Payable {
    void receiveSalary();
}

// 필요한 인터페이스만 구현
public class Human implements Workable, Eatable, Sleepable, Payable {
    @Override
    public void work() {
        System.out.println("Human working");
    }
    
    @Override
    public void eat() {
        System.out.println("Human eating");
    }
    
    @Override
    public void sleep() {
        System.out.println("Human sleeping");
    }
    
    @Override
    public void receiveSalary() {
        System.out.println("Human receiving salary");
    }
}

public class Robot implements Workable {
    @Override
    public void work() {
        System.out.println("Robot working");
    }
    // 불필요한 메서드 구현 없음!
}

 

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

"고수준 모듈은 저수준 모듈에 의존하면 안 된다. 둘 다 추상화에 의존해야 한다”

추상화(인터페이스) 에 의존하여 코드의 결합도를 낮추고 유지보수성을 극대화 할 수있습니다.

 

// 저수준 클래스 (구체적인 구현)
public class EmailSender {
    public void sendEmail(String message) {
        System.out.println("Sending email: " + message);
    }
}

// 고수준 클래스가 저수준 클래스에 직접 의존
public class NotificationService {
    private EmailSender emailSender = new EmailSender();
    
    public void notify(String message) {
        emailSender.sendEmail(message);
        // SMS로 바꾸려면? 코드를 수정해야 함!
    }
}
// 추상화 (인터페이스)
public interface MessageSender {
    void sendMessage(String message);
}

// 저수준 클래스들이 인터페이스 구현
public class EmailSender implements MessageSender {
    @Override
    public void sendMessage(String message) {
        System.out.println("Sending email: " + message);
    }
}

public class SMSSender implements MessageSender {
    @Override
    public void sendMessage(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

// 고수준 클래스는 추상화에 의존
public class NotificationService {
    private MessageSender messageSender;
    
    // 의존성 주입을 통해 유연성 확보
    public NotificationService(MessageSender messageSender) {
        this.messageSender = messageSender;
    }
    
    public void notify(String message) {
        messageSender.sendMessage(message);
    }
}

// 사용 예제
public class Main {
    public static void main(String[] args) {
        // 이메일로 알림
        NotificationService emailNotification = 
            new NotificationService(new EmailSender());
        emailNotification.notify("Hello via Email");
        
        // SMS로 알림 - 코드 변경 없이 교체 가능!
        NotificationService smsNotification = 
            new NotificationService(new SMSSender());
        smsNotification.notify("Hello via SMS");
    }
}

'TIL' 카테고리의 다른 글

AOP  (0) 2026.03.03
github - 협업준비하기.  (0) 2026.02.15
ORM 과 JPA  (1) 2026.02.03
개발자 대화법  (0) 2026.02.03
데이터베이스  (0) 2026.01.30