SOLID 원칙

SOLID 원칙이란?

객체지향 설계에서 유지보수성과 확장성을 높이기 위한 5가지 방법

이 원칙을 준수하면, 유연하고 변경에 강한 코드 작성 가능!


단일 책임 원칙 (SRP)

Single Responsibility Principle

 

하나의 클래스는 단 하나의 책임(기능) 만 가져야 함

즉, 하나의 변경 이유(Reason to Change) 만 가져야 함

 

⚠️ 여러 기능이 한 클래스에 섞이면 유지보수가 어려움!

 

SRP 위반

class Report {
    void generateReport() {
        // 리포트 생성 로직
    }

    void saveToFile() {
        // 파일 저장 로직
    }
}

 

➡︎ 리포트 생성파일 저장이라는 두 가지 책임을 가짐

 

SRP 적용

class ReportGenerator {
    void generateReport() {
        // 리포트 생성 로직
    }
}

class FileSaver {
    void saveToFile() {
        // 파일 저장 로직
    }
}

개방-폐쇄 원칙 (OCP)

Open/Closed Principle

 

코드는 확장에는 열려(Open) 있고, 변경에는 닫혀(Closed) 있어야 함

즉, 새로운 기능을 추가할 때 기존 코드를 수정하지 않아야 함

 

OCP 위반

class PaymentService {
    void pay(String paymentType) {
        if (paymentType.equals("CreditCard")) {
            // 신용카드 결제 로직
        } else if (paymentType.equals("PayPal")) {
            // PayPal 결제 로직
        }
    }
}

 

➡︎ 새로운 결제 방식이 추가될 때마다 기존 코드를 수정해야 함

 

OCP 적용

interface Payment {
    void pay();
}

class CreditCardPayment implements Payment {
    public void pay() {
        // 신용카드 결제 로직
    }
}

class PayPalPayment implements Payment {
    public void pay() {
        // PayPal 결제 로직
    }
}

class PaymentService {
    void processPayment(Payment payment) {
        payment.pay();
    }
}

 

➡︎ 다형성을 활용해서 기존 코드를 수정하지 않고, 새로운 결제 방식 추가 가능!


리스코프 치환 원칙(LSP)

Liskov Substitution Principle

 

자식 클래스는 부모 클래스를 대체할 수 있어야 함

즉, 부모 클래스를 상속받은 모든 클래스는  부모의 역할을 온전히 수행할 수 있어야 함

 

LSP 위반

class Rectangle {
    int width, height;

    void setWidth(int width) { this.width = width; }
    void setHeight(int height) { this.height = height; }
}

class Square extends Rectangle {
    void setWidth(int width) { 
        this.width = width;
        this.height = width; // 가로와 세로를 동일하게 설정 (LSP 위반)
    }
}

 

➡︎ Square 클래스는 Rectangle 을 대체할 수 없음

 

LSP 적용

interface Shape {
    int getArea();
}

class Rectangle implements Shape {
    int width, height;
    public int getArea() { return width * height; }
}

class Square implements Shape {
    int side;
    public int getArea() { return side * side; }
}

 

➡︎ 공통 인터페이스를 활용

 


인터페이스 분리 원칙(ISP)

Interface Segregation Principle

 

하나의 큰 인터페이스보다, 여러 개의 작은 인터페이스로 분리하는 게 좋음

즉, 사용하지 않는 메서드에 의존하면 안됨

 

ISP 위반

interface Worker {
    void work();
    void eat();
}

class Robot implements Worker {
    public void work() { System.out.println("일을 합니다."); }
    public void eat() { throw new UnsupportedOperationException(); } // 로봇은 먹을 수 없음 (ISP 위반)
}

 

➡︎ Roboteat() 메서드를 가질 필요가 없음

 

ISP 적용

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class Robot implements Workable {
    public void work() { System.out.println("일을 합니다."); }
}

 

➡︎ 인터페이스를 분리하여 불필요한 의존성 제거


의존성 역전 원칙(DIP)

Dependency Inversion Principle

 

상위 모듈이 하위 모듈에 의존하면 안 됨

즉, 세부 구현이 아니라 추상화(Interface)에 의존해야 함

 

DIP 위반

class MySQLDatabase {
    void connect() { System.out.println("MySQL 연결"); }
}

class DataManager {
    MySQLDatabase database = new MySQLDatabase(); // 특정 DB에 강하게 결합 (DIP 위반)
}

 

➡︎ DB 를 변경하려면 DataManager 코드를 수정해야 함

 

DIP 적용

interface Database {  // 추상화 적용
    void connect();
}

class MySQLDatabase implements Database {
    public void connect() { System.out.println("MySQL 연결"); }
}

class PostgreSQLDatabase implements Database {
    public void connect() { System.out.println("PostgreSQL 연결"); }
}

class DataManager {
    Database database;

    DataManager(Database database) { // 의존성 주입 (DIP 적용)
        this.database = database;
    }
}

 

➡︎ 추상화 적용 ➔ 인터페이스를 활용하여 유연한 설계 가능


추상화

추상화(Abstraction) 란?

객체의 핵심점인 특징만 노출하고, 불필요한 세부 사항은 숨기는 개념

인터페이스추상 클래스로 구현 가능

코드의 복잡도/결합도를 낮추고, 확장성을 높임

 

비교 항목 인터페이스 추상 클래스
목적 행동 정의 기본 기능 제공
메서드 구현 전부 구현 X (default 메서드 제외) 일부 구현 가능
다중 상속 가능 - implements 불가능 - extends
사용 예시 다양한 객체가 동일한 동작을 할 때 공통 기능을 제공하면서 일부만 구현이 필요할 때

 

* 자세한 내용은 아래 글의 인터페이스 vs 추상 클래스 내용 참고

 

상속(Inheritance)과 다형성(Polymorphism)

상속상속(Inheritance) 이란?기존 클래스를 확장하여 새로운 클래스를 만드는 개념코드 재사용성 증가 및 객체 간 계층 구조 형성 상속의 기본 구조// 부모 클래스 (Super Class)class Animal { String name; voi

jelliclesu.tistory.com


주요 포인트

  • SOLID 원칙은 단순 암기가 아니라 어떻게 적용되는 지 알아야 함
    • SRP: 하나의 클래스는 하나의 책임만 가져야 함
    • OCP: 코드는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 함
    • LSP: 자식 클래스는 부모 클래스를 대체할 수 있어야 함
    • ISP: 하나의 큰 인터페이스보다, 여러 개의 작은 인터페이스로 분리하는 게 좋음
    • DIP: 상위 모듈이 하위 모듈에 의존하면 안 됨
  • 추상화가 왜 필요한지, 인터페이스와 추상 클래스의 차이는 무엇인지 명확하게 알아야 함
    • 추상화: 객체의 핵심적인 특징만 노출하고, 불필요한 세부 사항은 숨김
    • 장점: 코드의 결합도는 낮아지고, 유지보수성 향상
  • SOLID 원칙과 추상화를 연계하여 유연한 설계 방법을 설명할 줄 알아야 함

클래스와 객체의 개념

클래스(Class)란?

객체를 생성하기 위한 설계도(템플릿)

즉, 속성(필드)와 동작(메서드)을 정의하는 툴

 

클래스 정의

class Car {
    String brand; // 속성 (필드)
    int speed;

    void accelerate() { // 동작 (메서드)
        speed += 10;
        System.out.println(brand + "가 속도를 올립니다. 현재 속도: " + speed);
    }
}

객체(Object)란?

클래스를 기반으로 생성된 실체(인스턴스)

즉, 클래스에서 정의한 속성과 동작을 실제로 가지고 있는 개별 데이터

 

객체 생성

public class Main {
    public static void main(String[] args) {
        Car myCar = new Car();  // 객체 생성
        myCar.brand = "Tesla";  // 속성 값 설정
        myCar.speed = 0;
        
        myCar.accelerate(); // 동작 수행
    }
}

 

➡︎ myCarCar 클래스로부터 만들어진 객체!

➡︎ 개별 객체에 속성을 설정할 수 있음 (myCar.brand = "Tesla")


객체를 사용하는 이유

  1. 코드의 재사용성 증가
    • 클래스를 한 번 정의하면 여러 객체를 생성할 수 있음
    • 같은 구조를 가진 객체를 만들 때 중복 코드를 줄일 수 있음
  2. 유지보수성 향상
    • 코드가 모듈화되므로, 특정 기능을 수정할 때 다른 부분에 영향을 줄 가능성이 낮아짐
    • 관련 기능을 묶어 가독성이 향상됨
  3. 캡슐화 및 정보 은닉 가능
    • 객체 내부의 데이터를 보호하고, 불필요한 접근을 제한할 수 있음
    • 외부에서는 제한된 방식으로만 데이터를 변경할 수 있도록 제어 가능

캡슐화

캡슐화(Encapsulation)란?

객체의 내부 데이터를 외부에서 직접 접근하지 못하도록 보호하는 개념

➡︎ 데이터 보호 + 객체 내부 구현 숨기기 가능


접근 제어자(Access Modifier)

Java 에서는 접근 제어자를 사용해 데이터 접근 범위를 제한할 수 있음

  • private: 클래스 내부에서만 접근 가능, 가장 제한적
  • protected: 같은 패키지 또는 상속 관계에서 접근 가능
  • public: 어디서든 접근 가능, 가장 개방적

 

캡슐화 적용

class BankAccount {
    private int balance = 0; // private으로 직접 접근 불가

    public void deposit(int amount) { // 외부에서는 public 메서드로 접근
        if (amount > 0) {
            balance += amount;
            System.out.println("입금 완료! 현재 잔액: " + balance);
        }
    }

    public int getBalance() { // 잔액 조회 메서드
        return balance;
    }
}

 

 

캡슐화된 객체 사용

public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        account.deposit(1000);
        
        System.out.println("현재 잔액: " + account.getBalance());
    }
}

 

➡︎ balance 필드는 private 이므로 외부에서 직접 접근 불가

➡︎ deposit() 메서드를 통해서만 잔액 변경 가능! ⭐️ 데이터 무결성 유지 ⭐️


주요 포인트

  • 클래스와 객체의 차이를 명확히 알아야 함!
    • 클래스: 객체를 만들기 위한 설계도
    • 객체: 클래스를 기반으로 생성된 실체
  • 객체를 사용하는 이유를 코드 재사용성, 유지보수성, 캡슐화 관점에서 논리적으로 설명할 줄 알아야 함!
    • 코드 재사용성 증가
    • 유지보수성 향상
    • 캡슐화를 통한 정보 보호
  • 캡슐화접근 제어자와 실제 활용 예시를 알고 있어야 함!
    • 객체 내부 데이터를 외부에서 직접 접근하지 못하게 보호
    • private, public 등의 접근 제어자를 사용

 

+ Recent posts