본문 바로가기

프로그래밍 기초

객체 지향 프로그래밍의 5가지 설계 원칙 - SOLID

객체 지향 프로그래밍에서 좋은 설계를 위한 SOLID 원칙에 대해 공부해보자.

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

한 클래스는 한 가지 책임만 가져야 한다.

 

클래스가 변경되어야 하는 이유는 오직 하나뿐이어야 한다는 의미

하나의 클래스가 여러 책임을 가지면 변경 사항이 생겼을 때 예상치 못한 부작용이 발생할 수 있다.

 

// 단일 책임 원칙을 위반한 예
public class User {
    private String name;
    private String email;

    // 사용자 데이터 관련 메서드
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    // 데이터베이스 저장 관련 메서드 (다른 책임)
    public void saveToDatabase() {
        // DB 저장 로직
        System.out.println("사용자 정보를 DB에 저장");
    }

    // 이메일 발송 관련 메서드 (다른 책임)
    public void sendEmail(String subject, String body) {
        // 이메일 발송 로직
        System.out.println("이메일 발송: " + subject);
    }
}

// 단일 책임 원칙을 따른 예
public class User {
    private String name;
    private String email;

    // 사용자 데이터 관련 메서드만 포함
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}

// 데이터베이스 관련 책임은 별도 클래스로 분리
public class UserRepository {
    public void save(User user) {
        // DB 저장 로직
        System.out.println("사용자 정보를 DB에 저장");
    }
}

// 이메일 관련 책임은 별도 클래스로 분리
public class EmailService {
    public void sendEmail(String to, String subject, String body) {
        // 이메일 발송 로직
        System.out.println("이메일 발송: " + subject);
    }
}

 

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

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

 

기존 코드를 변경하지 않고도 기능을 확장할 수 있어야 한다는 의미이다. 주로 인터페이스와 추상화를 통해 이 원칙을 지킬 수 있다.

// 개방-폐쇄 원칙을 위반한 예
public class PaymentProcessor {
    public void process(String type, double amount) {
        if (type.equals("CREDIT")) {
            System.out.println("신용카드: " + amount + "원");
        } else if (type.equals("PAYPAL")) {
            System.out.println("페이팔: " + amount + "원");
        }
        // 새로운 결제 방식 추가 시 이 메서드를 수정해야 함
    }
}

// 개방-폐쇄 원칙을 따른 예
public interface Payment {
    void process(double amount);
}

public class CreditCard implements Payment {
    public void process(double amount) {
        System.out.println("신용카드: " + amount + "원");
    }
}

public class PayPal implements Payment {
    public void process(double amount) {
        System.out.println("페이팔: " + amount + "원");
    }
}

// 새 결제 수단 추가 시 기존 코드 수정 없이 확장 가능
public class PaymentProcessor {
    public void process(Payment payment, double amount) {
        payment.process(amount);
    }
}

 

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

하위 타입은 상위 타입을 대체할 수 있어야 한다.

 

부모 클래스의 인스턴스를 사용하는 위치에 자식 클래스의 인스턴스를 넣어도 프로그램이 올바르게 동작해야 한다는 의미이다.

// 자동차 인터페이스
public interface Car {
    // 앞으로 가는 것이 기본 동작
    void accelerate();
}

// 정상적인 구현
public class NormalCar implements Car {
    @Override
    public void accelerate() {
        System.out.println("자동차가 앞으로 갑니다.");
    }
}

// 리스코프 치환 원칙을 위반한 구현
public class MalfunctioningCar implements Car {
    @Override
    public void accelerate() {
        System.out.println("자동차가 뒤로 갑니다.");  // 기대와 다른 동작
    }
}

// 자동차 테스트
public class CarTest {
    public static void testDrive(Car car) {
        // Car 인터페이스를 구현한 어떤 객체든 앞으로 가야 함
        System.out.println("테스트 주행 시작");
        car.accelerate();
        // 정상차는 앞으로 가지만, 고장난 차는 뒤로 감 (LSP 위반)
    }
}

 

위 코드에서 MalfunctioningCar는 Car 인터페이스를 구현하지만, accelerate 메서드의 기본 동작(앞으로 가기)과 다르게 동작한다. 이는 리스코프 치환 원칙을 위반한 것으로, Car 타입을 사용하는 코드는 어떤 구현체든 앞으로 가는 동작을 기대하기 때문이다.

 

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

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

 

큰 인터페이스보다는 클라이언트에 필요한 메서드만 있는 작은 인터페이스가 여러 개인 것이 낫다는 의미

// 인터페이스 분리 전 - 하나의 큰 인터페이스
public interface Car {
    void drive();      // 운전 관련
    void changeOil();  // 정비 관련
}

// 인터페이스 분리 후 - 역할별 인터페이스
public interface Drivable {
    void drive();
}

public interface Maintainable {
    void changeOil();
}

// 자동차 구현체
public class Tesla implements Drivable, Maintainable {
    public void drive() { System.out.println("주행"); }
    public void changeOil() { System.out.println("정비"); }
}

// 운전자는 운전 인터페이스만 필요
public class Driver {
    public void drive(Drivable car) {
        car.drive();  // 운전만 신경씀
    }
}

// 정비사는 정비 인터페이스만 필요
public class Mechanic {
    public void service(Maintainable car) {
        car.changeOil();  // 정비만 신경씀
    }
}

 

이렇게 분리하면 정비 인터페이스가 변경되어도 운전자에게 영향을 주지 않는다.

 

5. 의존관계 역전 원칙 (DIP: Dependency Inversion Principle)

클라이언트가 구현 클래스를 바라보지 말고 인터페이스만 바라봐야 한다.

 

구체적인 구현보다는 인터페이스나 추상클래스와 같은 추상화에 의존해야 한다는 의미

 

// 의존관계 역전 원칙을 위반한 예
public class Light {
    public void turnOn() {
        System.out.println("불 켜짐");
    }
}

public class Switch {
    private Light light;  // 구체 클래스에 직접 의존
    
    public Switch() {
        this.light = new Light();  // 강한 결합
    }
    
    public void press() {
        light.turnOn();
    }
}

// 의존관계 역전 원칙을 따른 예
public interface Device {
    void turnOn();
}

public class Light implements Device {
    public void turnOn() {
        System.out.println("불 켜짐");
    }
}

public class Switch {
    private Device device;  // 추상화에 의존
    
    public Switch(Device device) {  // 의존성 주입
        this.device = device;
    }
    
    public void press() {
        device.turnOn();
    }
}

 

SOLID 원칙은 코드의 유지보수성, 재사용성, 확장성을 높이기 위한 지침이다. 

모든 상황에서 완벽하게 적용하기는 어렵지만, 이 원칙들을 염두에 두고 설계하면 더 나은 코드를 작성할 수 있다.


let textNodes = document.querySelectorAll("div.tt_article_useless_p_margin.contents_style > *:not(figure):not(pre)"); textNodes.forEach(function(a) { a.innerHTML = a.innerHTML.replace(/`(.*?)`/g, '$1'); });