객체지향 프로그래밍의 단점

상속의 남용

  • 코드 복잡도 증가
  • 깊은 상속 계층 구조 ➔ 유지보수 어려움
  • 부모 클래스 수정 시 모든 자식 클래스에 영향 ➔ 강한 결합(Tight Coupling) 발생

객체 간 강한 의존성

  • 재사용성 저하
  • 하나의 클래스를 수정하면 여러 곳에 영향을 받을 수 있음
  • 객체 간 관계가 복잡해지면 확장성과 유연성이 떨어짐

불필요한 메모리 낭비

  • 상속을 사용할 경우, 부모 클래스의 불필요한 필드/메서드도 함께 사용됨
  • 필요하지 않은 기능이 포함될 수 있음

 

상속이 과도하게 사용된 경우

class Animal {
    void eat() { System.out.println("먹는 중..."); }
}

class Bird extends Animal {
    void fly() { System.out.println("나는 중..."); }
}

class Penguin extends Bird {
    void swim() { System.out.println("헤엄치는 중..."); }
}

 

➡︎ Penguin 은 Bird 를 상속받지만, 펭귄은 fly() 메서드 불필요

➡︎ 잘못된 상속 구조로 인해 불필요한 메서드가 포함됨

➡︎ 강한 결합도가 발생하여 유지보수가 어려워짐

 

‼️ 상속을 사용하기 보다 구성(Composition) 으로 변경


구성

구성(Composition) 이란?

하나의 객체가 다른 객체를 멤버 변수로 포함하는 방식

객체 간 결합도를 줄이고, 유연성을 높일 수 있음

 

구성 적용 코드

class Swimmer {
    void swim() { System.out.println("헤엄치는 중..."); }
}

class Penguin {
    private Swimmer swimmer; // 조합(Composition) 사용

    Penguin() {
        this.swimmer = new Swimmer();
    }

    void swim() {
        swimmer.swim();
    }
}

 

➡︎ 필요한 기능만 포함하여 유연한 설계 가능


상속과의 비교

비교 항목 상속(Inheritance) 구성(Composition)
코드 재사용성 높음 높음
유지보수성 낮음 (부모 클래스 변경 시 영향) 녹음 (독립적인 기능 구성 가능)
객체 간 결합도 높음 (Tight Coupling) 낮음 (Loose Coupling)
유연성  낮음 (상속 관계 변경 어려움) 높음 (필요한 기능만 구성 가능)

 

⭐️ 유지보수성과 확장성을 고려할 때, 가능하면 구성(Composition) 을 우선적으로 고려하고, 

     부모-자식 관계가 명확한 경우에만 상속(Inheritance) 을 사용


객체지향 대안

SOLID 원칙 준수

  • SRP(단일 책임 원칙): 하나의 클래스가 너무 많은 역할을 하지 않도록 정리
  • DIP(의존성 역전 원칙): 직접적인 의존성을 줄이고, 인터페이스 활용

함수형 프로그래밍 개념 활용

  • 불변성: 객체 변경이 아닌 새로운 객체 생성
  • 순수 함수: 부작용 최소화

디자인 패턴 활용

  • 전략 패턴: 특정 동작을 외부에서 주입하여 유연성 확보
  • 데코레이터 패턴: 객체 기능을 동적으로 확장

의존성

의존성(Dependency) 이란?

한 클래스가 다른 클래스의 객체를 직접 생성하거나 사용하는 관계

의존성이 높으면 코드 변경이 어렵고, 테스트가 어려움

 

의존성이 높은 코드

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

class DataManager {
    MySQLDatabase database = new MySQLDatabase(); // 특정 구현체에 직접 의존
}

 

➡︎ DataMangerMySQLDatabase 와 강하게 결합되어 다른 DB 로 변경하려면 기존 코드 수정 필요

➡︎ 의존성 주입(DI) 을 사용하여 DataManager 가 특정 DB 구현체에 직접 의존하지 않도록 변경


의존성 주입(Dependency Injection) 이란?

객체가 직접 다른 객체를 생성하는 것이 아니라, 외부에서 주입받는 방식

객체 간 결합도를 낮추고 유연한 코드를 만들 수 있음

 

의존성 주입 적용 코드

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 {
    private Database database;

    // 생성자를 통한 의존성 주입 (Constructor Injection)
    DataManager(Database database) {
        this.database = database;
    }

    void connectDatabase() {
        database.connect();
    }
}

// DI 적용 후 사용
public class Main {
    public static void main(String[] args) {
        Database db = new MySQLDatabase(); // MySQL 사용
        DataManager manager = new DataManager(db);
        manager.connectDatabase(); // 출력: MySQL 연결
    }
}

 

➡︎ DataManager 가 특정 DB 에 직접 의존하지 않음

➡︎ 인터페이스를 사용하여 유연한 확장 가능

➡︎ Mock 객체를 주입하여 테스트 가능 ➔ 테스트 용이성 향상

 

의존성 주입의 3가지 방식

  1. 생성자 주입 (Constructor Injection): 생성자를 통해 의존 객체를 주입 ex) new DataManager(new MySQLDatabase())
  2. 세터 주입 (Setter Injection): 세터 메서드를 통해 의존 객체 주입 ex) manager.setDatabase(new MySQLDatabase())
  3. 필드 주입 (Field Injection): 필드에 직접 주입 ex) @Autowired private Database db; (Spring 의 경우)

⭐️ 실무에서는 주로 생성자 주입을 사용 - 테스트 용이성 & 불변성 유지 가능


싱글톤 패턴

싱글톤 패턴(Singleton Pattern) 이란?

애플리케이션 전체에서 하나의 객체만 유지하는 디자인 패턴

객체가 불필요하게 여러 개 생성되는 것을 방지하여 메모리 낭비를 줄일 수 있음

 

싱글톤 패턴 적용 코드

class Singleton {
    private static Singleton instance;

    private Singleton() { } // private 생성자 (외부에서 객체 생성 차단)

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    public void showMessage() {
        System.out.println("싱글톤 인스턴스 실행");
    }
}

// 싱글톤 객체 사용
public class Main {
    public static void main(String[] args) {
        Singleton obj1 = Singleton.getInstance();
        Singleton obj2 = Singleton.getInstance();

        obj1.showMessage();  // 출력: 싱글톤 인스턴스 실행

        System.out.println(obj1 == obj2); // true (같은 객체)
    }
}

 

➡︎ getInstance() 를 호출하면 항상 같은 인스턴스 반환

➡︎ new Singleton() 을 직접 호출할 수 없음 (private 생성자)

➡︎ 전역적으로 단 하나의 객체만 유지


싱글톤 패턴의 장단점

장점

  • 객체의 공유: 모든 클래스가 같은 인스턴스를 공유하여 메모리 사용 절약
  • 객체 생성 제한: new 키워드로 객체를 여러 번 생성하는 것을 방지

단점

  • 멀티스레드 환경에서 동기화 문제 발생 가능: 여러 스레드가 동시에 getInstance() 를 호출하면 동시에 객체가 여러 개 생성될 위험
    ➡︎ 이중 체크 락킹(Double-Checked Locking) 기법 사용

멀티스레드 안전한 싱글톤

class ThreadSafeSingleton {
    private static volatile ThreadSafeSingleton instance;
    
    private ThreadSafeSingleton() { }

    public static ThreadSafeSingleton getInstance() {
        if (instance == null) {
            synchronized (ThreadSafeSingleton.class) {
                if (instance == null) {
                    instance = new ThreadSafeSingleton();
                }
            }
        }
        return instance;
    }
}

 

➡︎ synchronized 블록을 사용하여 멀티스레드 환경에서도 안전한 싱글톤 유지

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 원칙과 추상화를 연계하여 유연한 설계 방법을 설명할 줄 알아야 함

상속

상속(Inheritance) 이란?

기존 클래스를 확장하여 새로운 클래스를 만드는 개념

코드 재사용성 증가 및 객체 간 계층 구조 형성

 

상속의 기본 구조

// 부모 클래스 (Super Class)
class Animal {
    String name;
    void makeSound() {
        System.out.println("동물이 소리를 냅니다.");
    }
}

// 자식 클래스 (Sub Class)
class Dog extends Animal {
    void makeSound() { // 메서드 오버라이딩 (Overriding)
        System.out.println("멍멍!");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog myDog = new Dog();
        myDog.name = "바둑이";
        myDog.makeSound();  // 출력: 멍멍!
    }
}

 

➡︎ Dog 클래스는 Animal 클래스를 상속

➡︎ makeSound() 메서드를 오버라이딩(Overriding) 하여 고유 기능 구현


상속의 장단점

장점

  • 코드 재사용성 증가: 기본 기능을 부모 클래스에 정의
  • 유지보수 용이: 공통 로직을 부모 클래스에서 관리

단점

  • 상속 구조가 깊어질수록 의존성 증가 ➔ 유지보수 어려움
  • 부모 클래스가 변경되면 자식 클래스에 영향을 미칠 위험

➡︎ 상속보다 "조합(Composition)" 을 사용하는 것이 더 유리한 경우도 많음!


다형성

다형성(Polymorphism) 이란?

하나의 인터페이스로 여러 구현을 처리할 수 있는 개념

오버라이딩(Overriding)오버로딩(Overloading) 이 대표적인 방식

 

다형성 예제

class Animal {
    void makeSound() {
        System.out.println("동물이 소리를 냅니다.");
    }
}

class Cat extends Animal {
    void makeSound() { // 오버라이딩
        System.out.println("야옹~");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Cat(); // 부모 타입으로 자식 객체 참조
        myAnimal.makeSound(); // 출력: 야옹~
    }
}

 

➡︎ Animal myAnimal = new Cat(); ➔ 부모 타입을 사용하여 다양한 객체를 다룰 수 있음


오버로딩과 오버라이딩 차이

  • 오버로딩(Overloading): 같은 클래스에서 메서드 이름이 같고 매개변수가 다름 ex) void add(int a), void add(double a)
  • 오버라이딩(Overriding): 부모 클래스의 메서드를 재정의 ex) @Override void makeSound()

인터페이스 vs 추상 클래스

인터페이스(Interface) 란?

메서드의 동작만 정의하고, 구현은 하지 않음

다중 구현 가능 (implements)

 

인터페이스 예제

interface Animal {
    void makeSound(); // 구현 X (추상 메서드)
}

class Dog implements Animal {
    public void makeSound() {
        System.out.println("멍멍!");
    }
}

 

➡︎ 다양한 객체가 동일한 동작을 할 때 사용!


추상 클래스(Abstract Class) 란?

일부 구현이 포함된 클래스: 추상 메서드 + 일반 메서드 포함 가능

단일 상속만 가능 (extends)

 

추상 클래스 예제

abstract class Animal {
    abstract void makeSound(); // 추상 메서드 (구현 X)

    void sleep() { // 일반 메서드 (구현 O)
        System.out.println("동물이 잠을 잡니다.");
    }
}

 

➡︎ 공통 기능을 제공하면서 일부만 구현이 필요할 때 사용!


주요 포인트

  • 상속과 다형성의 차이를 명확히 알아야 함!
    • 상속: 부모 클래스를 기반으로 새로운 클래스를 확장하는 방식, 코드 재사용성 증가, 깊은 상속 구조는 유지보수 어려움
    • 다형성: 부모 타입으로 여러 자식 객체를 다룰 수 있는 개념, 메서드 오버라이딩을 활용하여 객체마다 다른 동작 수행 가능
  • 인터페이스와 추상 클래스의 차이
    • 인터페이스: 행동만 정의하고 구현은 하지 않음, 다중 구현 가능
    • 추상 클래스: 일부 구현이 포함될 수 있음, 단일 상속만 가능

클래스와 객체의 개념

클래스(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