contents

OOP의 SOLID 원칙: 상세 설명과 예제 코드

SOLID 원칙은 유지 보수 가능하고 확장하기 쉬우며, 견고한 객체지향 코드를 작성하기 위한 5가지 핵심 설계 원칙입니다. 실제 프로젝트와 면접에서 매우 중요하며, 테스트와 변경이 쉬운 구조를 만드는 데 효과적입니다.

1. S – 단일 책임 원칙 (Single Responsibility Principle, SRP)

정의:
클래스는 하나의 책임만 가져야 하며, 변경해야 할 이유도 하나여야 합니다.

이유:

❌ 잘못된 예:

class UserManager {
    public void addUser(User user) { /* ... */ }
    public void saveToDatabase(User user) { /* ... */ }
    public void sendEmail(User user, String msg) { /* ... */ }
}
// 사용자 로직, 데이터베이스 처리, 이메일 전송이라는 여러 책임이 하나의 클래스에 있음

✅ 올바른 예:

class UserManager {
    public void addUser(User user) { /* ... */ }
}

class UserRepository {
    public void saveToDatabase(User user) { /* ... */ }
}

class EmailService {
    public void sendEmail(User user, String msg) { /* ... */ }
}
// 각 클래스가 하나의 명확한 책임만 가짐

2. O – 개방/폐쇄 원칙 (Open/Closed Principle, OCP)

정의:
소프트웨어 엔티티는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.

이유:

❌ 잘못된 예:

class SalaryCalculator {
    public double calculateSalary(Employee emp) {
        if (emp.getType().equals("Manager"))
            return emp.getBase() * 2;
        else if (emp.getType().equals("Engineer"))
            return emp.getBase() * 1.5;
        // 새로운 타입이 생기면 코드 수정 필요
    }
}

✅ 올바른 예 (다형성 활용):

abstract class Employee {
    abstract double calculateSalary();
}

class Manager extends Employee {
    double calculateSalary() { return getBase() * 2; }
}

class Engineer extends Employee {
    double calculateSalary() { return getBase() * 1.5; }
}
// 새로운 역할이 추가되어도 기존 클래스 수정 없이 확장 가능

3. L – 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

정의:
자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다.

이유:

❌ 잘못된 예:

class Bird {
    void fly() { /* ... */ }
}

class Ostrich extends Bird {
    void fly() { throw new UnsupportedOperationException(); }
}
// 타조는 날 수 없는데 fly()를 가지고 있음 → LSP 위반

✅ 올바른 예:

abstract class Bird {}

class FlyingBird extends Bird {
    void fly() { /* ... */ }
}

class Sparrow extends FlyingBird {}
class Ostrich extends Bird {} // Ostrich는 fly() 메서드 없음
// 기반 클래스 설계를 변경하여 위반 방지

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

정의:
클라이언트는 사용하지 않는 함수에 의존하도록 강요받아서는 안 된다.

이유:

❌ 잘못된 예:

interface MultiFunctionDevice {
    void print();
    void scan();
    void fax();
}

class SimplePrinter implements MultiFunctionDevice {
    public void print() { /* ... */ }
    public void scan() { /* 미사용 → 빈 구현 */ }
    public void fax() { /* 미사용 → 빈 구현 */ }
}

✅ 올바른 예:

interface Printer { void print(); }
interface Scanner { void scan(); }
interface Fax { void fax(); }

class SimplePrinter implements Printer {
    public void print() { /* ... */ }
}
// 필요한 기능만 갖는 인터페이스 구현

5. D – 의존 역전 원칙 (Dependency Inversion Principle, DIP)

정의:
고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다.

이유:

❌ 잘못된 예:

class UserService {
    private UserRepository repo = new UserRepository();
    public void register(User user) {
        repo.save(user);
    }
}
// 구체 구현에 직접적으로 의존 → 테스트 어려움

✅ 올바른 예:

interface UserRepository {
    void save(User user);
}

class DatabaseUserRepository implements UserRepository {
    public void save(User user) { /* DB 저장 */ }
}

class UserService {
    private final UserRepository repo;
    public UserService(UserRepository repo) {
        this.repo = repo;
    }
    public void register(User user) {
        repo.save(user);
    }
}
// 인터페이스를 통해 의존성 역전, DI 적용 가능

요약 비교표

원칙 초점 지키지 않았을 때 지켰을 때
SRP 하나의 책임 변경 사유가 서로 섞여 복잡해짐 유지보수 쉬운 작고 명확한 클래스 구성
OCP 확장 가능 + 안정성 유지 코드 수정 시 오류 가능, 로직 변경 필요 기능 추가가 수월하며, 테스트 안정성 보장
LSP 자식이 부모로 대체 가능 Unexpected behavior 발생 안전한 상속 구조, 다형성 활용 가능
ISP 인터페이스는 작게 강제 구현 메서드 다수 → 복잡해짐 필요한 기능만 명확하게 구분하여 구현 가능
DIP 구현이 아닌 추상에 의존 테스트 어려움, 강한 결합 구조 유연한 설계, DI 및 Mock 주입 가능

실전 팁

Spring 기반의 SOLID 원칙 심화 예제

아래는 각 SOLID 원칙이 실제 Spring(Spring Boot) 애플리케이션에서 어떻게 적용되는지를 깊이 있게 설명한 예제입니다. 단순한 구조를 넘어서, 유지보수성, 확장성, 테스트 용이성이 어떻게 향상되는지 보여줍니다.

1. SRP (단일 책임 원칙) — Service, Validation, Persistence 분리

❌ 나쁜 예 — 하나의 클래스가 모든 역할 처리

@Service
public class OrderService {
    @Autowired private OrderRepository repo;

    public void createOrder(Order order) {
        // 비즈니스 로직
        if (order.getPrice() < 0) throw new IllegalArgumentException("...");
        // 검증 & 이메일
        if (!EmailValidator.isValid(order.getEmail())) throw new ...;
        EmailUtils.sendEmail(order.getEmail(), "Thanks!");

        // 데이터 저장
        repo.save(order);
    }
}

✅ 좋은 예 — 역할별 클래스 분리

@Service
public class OrderService {
    @Autowired OrderValidator validator;
    @Autowired NotificationService notification;
    @Autowired private OrderRepository repo;

    public void createOrder(Order order) {
        validator.validate(order);
        repo.save(order);
        notification.sendOrderConfirmation(order);
    }
}

@Component
public class OrderValidator {
    public void validate(Order order) { /* 유효성 검사 */ }
}

@Service
public class NotificationService {
    public void sendOrderConfirmation(Order order) { /* 이메일 전송 */ }
}

2. OCP (개방-폐쇄 원칙) — 결제 시스템 확장

❌ 나쁜 설계 — if문으로 모든 타입 처리

@Service
public class PaymentService {
    public void pay(Order order) {
        if (order.getType().equals("CARD")) processCard(order);
        else if (order.getType().equals("PAYPAL")) processPayPal(order);
    }
}

✅ 좋은 설계 — 전략 인터페이스 + @Component 조합

public interface PaymentStrategy {
    boolean supports(Order order);
    void pay(Order order);
}

@Component
public class CardPayment implements PaymentStrategy { /* 카드 결제 */ }

@Component
public class PaypalPayment implements PaymentStrategy { /* 페이팔 결제 */ }

@Service
public class PaymentService {
    @Autowired List<PaymentStrategy> strategies;

    public void pay(Order order) {
        strategies.stream()
            .filter(s -> s.supports(order))
            .findFirst()
            .orElseThrow()
            .pay(order);
    }
}

3. DIP (의존 역전 원칙) — 구체 구현에 직접 의존하지 않기

❌ 나쁜 예

@Service
public class UserService {
    private final JdbcUserRepository repo = new JdbcUserRepository();
}

✅ 좋은 예 — 인터페이스 기반 DI

public interface UserRepository {
    void save(User user);
}

@Repository
public class JdbcUserRepository implements UserRepository { ... }

@Repository
public class MongoUserRepository implements UserRepository { ... }

@Service
public class UserService {
    private final UserRepository repo;

    @Autowired
    public UserService(UserRepository repo) {
        this.repo = repo;
    }
}

4. ISP (인터페이스 분리 원칙) — 필요한 기능만 제공하는 인터페이스

❌ 안 좋은 예 — 모든 기능을 가진 인터페이스

public interface MultiFunctionService {
    void create();
    void update();
    void delete();
    void archive(); // 어떤 클래스는 사용 안 함
}

@Service
public class CommentService implements MultiFunctionService {
    public void create() {...}
    public void update() {...}
    public void delete() {...}
    public void archive() {
        throw new UnsupportedOperationException();
    }
}

✅ 좋은 예 — 역할별 인터페이스 분리

public interface Creatable { void create(); }
public interface Updatable { void update(); }
public interface Deletable { void delete(); }

@Service
public class CommentService implements Creatable, Updatable, Deletable {
    // 필요한 기능만 구현
}

5. LSP (리스코프 치환 원칙) — 인터페이스의 구현체는 대체 가능해야 함

예제: Notification 인터페이스의 대체 가능성

public interface NotificationService {
    void notify(String msg);
}

@Service
public class EmailNotification implements NotificationService { ... }

@Service
public class SmsNotification implements NotificationService { ... }

Spring에서 SOLID를 실현하는 베스트 프랙티스

references