객체지향과 SOLID

JJinu·2026년 1월 30일

JAVA

목록 보기
3/3
post-thumbnail

면접에서 받은 질문 2편입니다! (저 CS 질문 답변 잘 못했는데 1차합격했어요! 😆 이래서 면까몰인가.. 암튼! 2차면접도 화이팅 해보겠슴돠) 이번에는 객체지향과 SOLID에 대해서 한번 알아보겠습니다.

객체지향?

Oracle 공식문서에서는 아래와 같이 정의했습니다.

"Object-oriented programming is a method of implementation in which programs are organized as cooperative collections of objects, each of which represents an instance of some class, and whose classes are all members of a hierarchy of classes united via inheritance relationships."

"객체지향 프로그래밍은 여러 객체들이 서로 협력해서 프로그램을 만드는 방식인데, 각 객체는 클래스라는 설계도로 만들어지고, 이 클래스들은 부모-자식 상속 관계로 계층적으로 연결되어 있다."


객체지향하면 SOLID 아니야?

객체지향과 SOLID의 관계는 밀접하게 연결되어 있지만 객체지향 = SOLID 라는건 아닙니다.

객체지향 (패러다임)

좋은 객체지향 설계가 필요

SOLID 원칙 (가이드라인)

위 계층처럼 객체지향프로그래밍 패러다임으로 "어떻게 프로그램을 구성할 것인가"에 대한 근본적인 방식이며, SOLID 원칙좋은 객체지향적인 설계를 위한 가이드라인입니다.
(저는 객체지향 = SOLID라고 머릿속에 박혀있어서 면접답변에서도 SOLID를 이야기를 해버린..)


빠질수 없는 SOLID 원칙

SOLID는 로버트 마틴(Uncle Bob)이 정리한 객체지향 설계 5원칙입니다.

👀 한눈에 보기

원칙한 줄 요약해결하는 문제핵심 키워드
SRP한 클래스는 한 가지 책임만변경 영향 최소화책임 분리
OCP확장에 열려있고 수정에 닫혀있게기존 코드 수정 최소화확장 가능
LSP부모를 자식으로 치환 가능잘못된 상속 방지올바른 상속
ISP사용 안 하는 메서드에 의존X불필요한 구현 방지인터페이스 분리
DIP구체가 아닌 추상에 의존강한 결합 방지추상화 의존

각 원칙별 예시를 한번 보면 이해가 쉽겠죠? 가봅시다!

1️⃣ SRP (Single Responsibility Principle)

단일 책임 원칙

🔴 나쁜 예시

public class User {
    private String name;
    private String email;
    
    public void sendEmail(String message) { ... }      // 책임 1: 이메일 발송
    public void saveToDatabase() { ... }               // 책임 2: DB 저장
    public void generateReport() { ... }               // 책임 3: 보고서 생성
}

문제: 이메일, DB, 보고서 중 하나만 바뀌어도 User 클래스를 수정해야 함!

🟢 좋은 예시

public class User {
    private String name;
    private String email;
    // 사용자 정보만 관리
}

public class EmailService {
    public void send(String email, String message) { ... }
}

public class UserRepository {
    public void save(User user) { ... }
}

public class ReportGenerator {
    public void generate(User user) { ... }
}

장점: 각 클래스가 하나의 책임만! 변경 영향 최소화


2️⃣ OCP (Open-Closed Principle)

개방-폐쇄 원칙

🔴 나쁜 예시

public class DiscountCalculator {
    public int calculate(String type, int price) {
        if (type.equals("Regular")) return price;
        else if (type.equals("VIP")) return price * 90 / 100;    // 10% 할인
        else if (type.equals("VVIP")) return price * 80 / 100;   // 20% 할인
        return price;
    }
}

문제: 새 등급(SVIP) 추가 시 → DiscountCalculator 수정 필요!

🟢 좋은 예시

public interface DiscountPolicy {
    int discount(int price);
}

public class RegularDiscount implements DiscountPolicy {
    public int discount(int price) { return price; }
}

public class VIPDiscount implements DiscountPolicy {
    public int discount(int price) { return price * 90 / 100; }
}

// 새 등급 추가 - 기존 코드 수정 없이!
public class SVIPDiscount implements DiscountPolicy {
    public int discount(int price) { return price * 70 / 100; }
}

장점: 새 등급 추가해도 기존 코드 수정 불필요!


3️⃣ LSP (Liskov Substitution Principle)

리스코프 치환 원칙

🔴 나쁜 예시

public class Rectangle {
    protected int width, height;
    public void setWidth(int w) { width = w; }
    public void setHeight(int h) { height = h; }
    public int getArea() { return width * height; }
}

public class Square extends Rectangle {
    @Override
    public void setWidth(int w) { width = height = w; }  // 정사각형은 가로=세로
    @Override
    public void setHeight(int h) { width = height = h; }
}

// 테스트
Rectangle rect = new Rectangle();
rect.setWidth(5);
rect.setHeight(4);
System.out.println(rect.getArea());  // 20 ✅

Rectangle square = new Square();
square.setWidth(5);
square.setHeight(4);
System.out.println(square.getArea());  // 16 ❌ (기대: 20)

문제: 부모(Rectangle)를 자식(Square)으로 치환하니 예상과 다르게 동작!

🟢 좋은 예시

public interface Shape {
    int getArea();
}

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

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

// 테스트
Shape rect = new Rectangle(5, 4);
Shape square = new Square(4);
System.out.println(rect.getArea());    // 20 ✅
System.out.println(square.getArea());  // 16 ✅

장점: 상속 대신 인터페이스! 치환해도 문제없음


4️⃣ ISP (Interface Segregation Principle)

인터페이스 분리 원칙

🔴 나쁜 예시

public interface SmartDevice {
    void print();
    void scan();
    void fax();
}

public class MultiFunctionPrinter implements SmartDevice {
    public void print() { ... }
    public void scan() { ... }
    public void fax() { ... }
}

public class SimplePrinter implements SmartDevice {
    public void print() { ... }
    public void scan() { throw new UnsupportedOperationException(); }  // 억지 구현!
    public void fax() { throw new UnsupportedOperationException(); }   // 억지 구현!
}

문제: SimplePrinter는 print만 필요한데 scan, fax도 구현해야 함!

🟢 좋은 예시

public interface Printer {
    void print();
}

public interface Scanner {
    void scan();
}

public interface Fax {
    void fax();
}

public class SimplePrinter implements Printer {
    public void print() { ... }  // print만 구현!
}

public class MultiFunctionPrinter implements Printer, Scanner, Fax {
    public void print() { ... }
    public void scan() { ... }
    public void fax() { ... }
}

장점: 필요한 인터페이스만 구현! 불필요한 메서드 구현 강제 안 됨


5️⃣ DIP (Dependency Inversion Principle)

의존성 역전 원칙

🔴 나쁜 예시

public class MySQLDatabase {
    public void save(String data) { ... }
}

public class UserService {
    private MySQLDatabase database;  // 구체 클래스에 직접 의존!
    
    public UserService() {
        this.database = new MySQLDatabase();
    }
    
    public void saveUser(String userName) {
        database.save(userName);
    }
}

문제: MySQL → PostgreSQL 변경 시 UserService 수정 필요!

🟢 좋은 예시

public interface Database {
    void save(String data);
}

public class MySQLDatabase implements Database {
    public void save(String data) { ... }
}

public class PostgreSQLDatabase implements Database {
    public void save(String data) { ... }
}

public class UserService {
    private Database database;  // 인터페이스에 의존!
    
    public UserService(Database database) {  // 의존성 주입
        this.database = database;
    }
    
    public void saveUser(String userName) {
        database.save(userName);
    }
}

// 사용
Database mysql = new MySQLDatabase();
UserService service = new UserService(mysql);  // DB 교체 쉬움!

장점: DB 교체 시 UserService 수정 불필요! 테스트용 Mock도 쉽게 생성


이해하면 좋겠지만 까먹잖아요..? 쉽게 외워볼까요

다들 코딩하다보면 아래와 같은 상황들을 아주 수 많이 느낄텐데요 (특히 내가 짠 코드..)
SOLID를 어긴 코드는 요런 느낌을 풀풀 풍기니 확인 한번 해보면 좋겠어요!

S 위반 ➜ "이 클래스 왜 이렇게 길어?"
O 위반 ➜ "if-else가 왜 이렇게 많아?"
L 위반 ➜ "상속받았는데 이상하게 동작하네?"
I 위반 ➜ "왜 안 쓰는 메서드까지 구현해야 해?"
D 위반 ➜ "DB 바꾸려면 코드 다 뜯어고쳐야 하네?"

이렇게 외우면 쉬울까요..? 단(일책임)개(방폐쇄)이(리스코프)분(리)역(전)
이번역은 단개이분역 입니다. (죄송합니다.. 혹시라도 쉽게 외우는방법이 생각나면 수정해볼게요)


마무리

일단 마지막에 분위기 싸하게 만들어서 죄송하다는 말씀 먼저 드리겠습니다. 그냥 해보고 싶었어요! (어차피 아무도 안보잖아요..)

무튼! 객체지향과 SOLID에 대해 잘 알아가고 다음엔 당당하게 말할 수 있었으면 좋겠습니다!

다음 포스팅은 클래스, 추상클래스, 인터페이스에 대해 공부해볼거에요!

끝까지 읽어주신 분들께 감사드리며 오늘도 행복한 하루 되셨으면 좋겠습니다!

profile
하루하루 의미있고 행복하게! (Yesterday is History, Tomorrow is a mystery, But today is a gift)

0개의 댓글