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

    상속의 남용

    • 코드 복잡도 증가
    • 깊은 상속 계층 구조 ➔ 유지보수 어려움
    • 부모 클래스 수정 시 모든 자식 클래스에 영향 ➔ 강한 결합(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