본문 바로가기
언어 공부/JAVA

JAVA D6 [ 객체 지향 ] - 주니어 개발자를 위한 객체 지향 프로그래밍 총정리

by TMrare 2025. 3. 9.
주니어 개발자를 위한 객체 지향 프로그래밍 총정리

주니어 개발자를 위한 객체 지향 프로그래밍 총정리

실무에서 꼭 알아야 할 OOP 개념과 실전 활용법

1. 객체 지향 프로그래밍 개요

객체 지향 프로그래밍(Object-Oriented Programming, OOP)은 현실 세계의 개념이나 사물을 객체로 추상화해 프로그램을 구조화하는 패러다임. 객체를 중심으로 데이터와 기능을 함께 관리해 코드의 재사용성과 유지보수성을 높임.

📌 객체 지향 프로그래밍의 핵심 장점

  • 높은 재사용성 - 클래스와 상속을 통해 코드 재사용 극대화
  • 쉬운 유지보수 - 모듈화로 인한 변경 영향 범위 최소화
  • 유연한 확장성 - 기존 코드 수정 없이 새 기능 추가 가능
  • 직관적 모델링 - 실세계 개념을 자연스럽게 코드로 표현
  • 안정적 구조화 - 캡슐화를 통한 안전한 코드 관리

실무에서 OOP는 코드를 이해하기 쉽게 만들고 팀 협업을 원활하게 하는 핵심 요소. 특히 대규모 프로젝트에서 유지보수성과 확장성에 큰 영향을 미침.

2. 절차 지향 vs 객체 지향

🔄 절차 지향 프로그래밍

  • 순차적 실행 흐름 중심의 설계
  • 데이터와 함수가 별도로 존재
  • "어떻게(How)" 처리할지 초점
  • Top-Down 방식의 문제 해결
  • 간단한 프로그램에 적합
C FORTRAN COBOL

🧩 객체 지향 프로그래밍

  • 객체 간 상호작용 중심의 설계
  • 데이터와 메서드가 객체 내에 통합
  • "무엇이(What)" 필요한지 초점
  • Bottom-Up 방식의 문제 해결
  • 복잡하고 큰 프로젝트에 적합
Java C# Python JavaScript
절차 지향 vs 객체 지향 예시: 사각형 넓이 계산
// 절차 지향 방식
function calculateRectangleArea(width, height) {
    return width * height;
}

function calculateRectanglePerimeter(width, height) {
    return 2 * (width + height);
}

// 사용
const area = calculateRectangleArea(5, 10);
const perimeter = calculateRectanglePerimeter(5, 10);

// 객체 지향 방식
class Rectangle {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }
    
    calculateArea() {
        return this.width * this.height;
    }
    
    calculatePerimeter() {
        return 2 * (this.width + this.height);
    }
}

// 사용
const rectangle = new Rectangle(5, 10);
const area = rectangle.calculateArea();
const perimeter = rectangle.calculatePerimeter();

실무 포인트: 절차 지향과 객체 지향은 상호 배타적이지 않음. 최신 애플리케이션은 두 패러다임을 적절히 혼합해 사용. 상황에 맞는 접근법 선택이 중요!

3. 객체 지향의 4대 핵심 원칙

🔒 캡슐화 (Encapsulation)

객체의 데이터와 메서드를 하나로 묶고, 실제 구현 내용 일부를 외부에 감추는 것.

class BankAccount {
    // private 필드
    private double balance;
    private String accountNumber;
    
    // 생성자
    public BankAccount(String accountNumber) {
        this.accountNumber = accountNumber;
        this.balance = 0;
    }
    
    // 공개 메서드
    public void deposit(double amount) {
        if (amount > 0) {
            this.balance += amount;
        } else {
            throw new IllegalArgumentException("입금액은 0보다 커야 합니다.");
        }
    }
    
    public boolean withdraw(double amount) {
        if (amount > 0 && balance >= amount) {
            balance -= amount;
            return true;
        }
        return false;
    }
    
    // getter
    public double getBalance() {
        return balance;
    }
}

실무 핵심:

  • 클래스 내부 데이터는 private으로 보호
  • public 메서드로만 접근 허용
  • 데이터 검증 로직을 메서드 내부에 구현

🧬 상속 (Inheritance)

기존 클래스의 특성을 그대로 물려받으면서 필요한 기능을 확장하거나 재정의하는 것.

// 부모 클래스
class Vehicle {
    protected String brand;
    protected String model;
    
    public Vehicle(String brand, String model) {
        this.brand = brand;
        this.model = model;
    }
    
    public void start() {
        System.out.println("차량 시동 걸림");
    }
    
    public void stop() {
        System.out.println("차량 정지");
    }
}

// 자식 클래스
class Car extends Vehicle {
    private int numDoors;
    
    public Car(String brand, String model, int numDoors) {
        super(brand, model);  // 부모 생성자 호출
        this.numDoors = numDoors;
    }
    
    // 메서드 오버라이딩(재정의)
    @Override
    public void start() {
        System.out.println(brand + " " + model + " 자동차 시동 걸림");
    }
    
    // 새로운 메서드 추가
    public void honk() {
        System.out.println("빵빵!");
    }
}

실무 핵심:

  • 코드 재사용성 증가, 반복 제거
  • IS-A 관계일 때만 상속 사용
  • 메서드 오버라이딩 시 @Override 명시

👥 다형성 (Polymorphism)

같은 인터페이스나 부모 타입을 통해 여러 객체를 동일한 방식으로 다루는 것.

// 인터페이스 정의
interface Shape {
    double calculateArea();
    double calculatePerimeter();
}

// 구현 클래스들
class Circle implements Shape {
    private double radius;
    
    public Circle(double radius) {
        this.radius = radius;
    }
    
    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
    
    @Override
    public double calculatePerimeter() {
        return 2 * Math.PI * radius;
    }
}

class Rectangle implements Shape {
    private double height;    
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    
    @Override
    public double calculateArea() {
        return width * height;
    }
    
    @Override
    public double calculatePerimeter() {
        return 2 * (width + height);
    }
}

// 다형성 활용
public void printShapeInfo(Shape shape) {
    System.out.println("면적: " + shape.calculateArea());
    System.out.println("둘레: " + shape.calculatePerimeter());
}

// 사용 예시
Shape circle = new Circle(5);
Shape rectangle = new Rectangle(4, 6);

printShapeInfo(circle);      // Circle의 정보 출력
printShapeInfo(rectangle);   // Rectangle의 정보 출력

실무 핵심:

  • 인터페이스/추상클래스를 통한 유연한 코드 설계
  • 동일한 메서드명으로 다양한 구현체 동작
  • 확장에 유리한 구조 설계 가능

🔍 추상화 (Abstraction)

복잡한 현실에서 핵심적인 개념이나 기능만을 추출하여 단순화하는 과정.

// 추상 클래스 정의
abstract class Database {
    // 공통 속성
    protected String connectionString;
    protected boolean isConnected;
    
    // 생성자
    public Database(String connectionString) {
        this.connectionString = connectionString;
        this.isConnected = false;
    }
    
    // 공통 구현 메서드
    public final void disconnect() {
        if (isConnected) {
            closeConnection();
            isConnected = false;
            System.out.println("데이터베이스 연결 해제됨");
        }
    }
    
    // 추상 메서드 - 자식에서 반드시 구현해야 함
    public abstract boolean connect();
    public abstract void executeQuery(String query);
    protected abstract void closeConnection();
}

// 구현 클래스
class MySQLDatabase extends Database {
    public MySQLDatabase(String connectionString) {
        super(connectionString);
    }
    
    @Override
    public boolean connect() {
        // MySQL 연결 구현
        System.out.println("MySQL 데이터베이스에 연결중...");
        isConnected = true;
        return true;
    }
    
    @Override
    public void executeQuery(String query) {
        if (isConnected) {
            System.out.println("MySQL에서 쿼리 실행: " + query);
        } else {
            System.out.println("데이터베이스 연결 필요");
        }
    }
    
    @Override
    protected void closeConnection() {
        // MySQL 연결 해제 구현
        System.out.println("MySQL 연결 해제 중...");
    }
}

실무 핵심:

  • 핵심 비즈니스 로직에 집중할 수 있음
  • 추상화 단계를 통한 코드 복잡성 관리
  • 추상 클래스와 인터페이스를 적절히 활용

4. SOLID 원칙

객체 지향 설계의 5가지 핵심 원칙으로, 유지보수가 용이하고 확장 가능한 시스템을 만들기 위한 가이드라인.

📌 S - 단일 책임 원칙 (Single Responsibility Principle)

"한 클래스는 하나의 책임만 가져야 한다."

❌ 위반 예시

class UserManager {
    public void register(User user) {
        // 사용자 등록 로직
    }
    
    public void sendEmail(String to, String subject) {
        // 이메일 발송 로직
    }
    
    public void saveLogsToFile(String logData) {
        // 로그 파일 저장 로직
    }
}

✅ 개선 예시

class UserManager {
    public void register(User user) {
        // 사용자 등록 로직
    }
}

class EmailService {
    public void sendEmail(String to, String subject) {
        // 이메일 발송 로직
    }
}

class LogManager {
    public void saveLogsToFile(String logData) {
        // 로그 파일 저장 로직
    }
}

핵심 이점: 클래스가 단일 책임을 갖게 되면 변경 이유가 하나로 제한되어 유지보수가 용이해지고, 다른 기능에 영향을 미치지 않음.

📌 O - 개방-폐쇄 원칙 (Open-Closed Principle)

"소프트웨어 엔티티는 확장에는 열려 있으나, 변경에는 닫혀 있어야 한다."

❌ 위반 예시

class PaymentProcessor {
    public void processPayment(String paymentType, double amount) {
        if (paymentType.equals("credit")) {
            // 신용카드 결제 처리
        } else if (paymentType.equals("paypal")) {
            // 페이팔 결제 처리
        }
        // 새 결제 방식을 추가하려면 이 클래스를 수정해야 함
    }
}

✅ 개선 예시

// 인터페이스
interface PaymentMethod {
    void processPayment(double amount);
}

// 구현체들
class CreditCardPayment implements PaymentMethod {
    @Override
    public void processPayment(double amount) {
        // 신용카드 결제 처리
    }
}

class PayPalPayment implements PaymentMethod {
    @Override
    public void processPayment(double amount) {
        // 페이팔 결제 처리
    }
}

// 새로운 결제 방식 추가 시 기존 코드 수정 없이 확장
class BitcoinPayment implements PaymentMethod {
    @Override
    public void processPayment(double amount) {
        // 비트코인 결제 처리
    }
}

핵심 이점: 기능 확장 시 기존 코드를 수정하지 않아 버그 발생 위험을 줄이고, 유연한 설계를 통해 새로운 요구사항 적용이 용이해짐.

📌 L - 리스코프 치환 원칙 (Liskov Substitution Principle)

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

❌ 위반 예시

class Rectangle {
    protected int width;
    protected int height;
    
    public void setWidth(int width) {
        this.width = width;
    }
    
    public void setHeight(int height) {
        this.height = height;
    }
    
    public int getArea() {
        return width * height;
    }
}

class Square extends Rectangle {
    // 정사각형은 가로세로가 같아야 함
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width;  // 문제 발생!
    }
    
    @Override
    public void setHeight(int height) {
        this.width = height;  // 문제 발생!
        this.height = height;
    }
}

✅ 개선 예시

// 공통 인터페이스
interface Shape {
    int getArea();
}

// 별도 클래스로 분리
class Rectangle implements Shape {
    private int width;
    private int height;
    
    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }
    
    public void setWidth(int width) {
        this.width = width;
    }
    
    public void setHeight(int height) {
        this.height = height;
    }
    
    @Override
    public int getArea() {
        return width * height;
    }
}

class Square implements Shape {
    private int side;
    
    public Square(int side) {
        this.side = side;
    }
    
    public void setSide(int side) {
        this.side = side;
    }
    
    @Override
    public int getArea() {
        return side * side;
    }
}

핵심 이점: 상속 관계에서 예상치 못한 동작을 방지하고, 클라이언트 코드에서 일관된 동작을 보장해 안정적인 프로그램 구현이 가능.

📌 I - 인터페이스 분리 원칙 (Interface Segregation Principle)

"클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다."

❌ 위반 예시

// 너무 큰 인터페이스
interface Worker {
    void work();
    void eat();
    void sleep();
}

// 로봇은 잠이나 식사가 필요 없지만 구현해야 함
class Robot implements Worker {
    @Override
    public void work() {
        // 작업 수행
    }
    
    @Override
    public void eat() {
        // 필요 없는 기능
        throw new UnsupportedOperationException();
    }
    
    @Override
    public void sleep() {
        // 필요 없는 기능
        throw new UnsupportedOperationException();
    }
}

✅ 개선 예시

// 작은 인터페이스로 분리
interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

interface Sleepable {
    void sleep();
}

// 필요한 인터페이스만 구현
class Human implements Workable, Eatable, Sleepable {
    @Override
    public void work() { /* 구현 */ }
    @Override
    public void eat() { /* 구현 */ }
    @Override
    public void sleep() { /* 구현 */ }
}

class Robot implements Workable {
    @Override
    public void work() { /* 구현 */ }
    // 불필요한 메서드 없음
}

핵심 이점: 클라이언트는 필요한 메서드만 구현하면 되므로 불필요한 의존성이 제거되고, 인터페이스 변경 시 영향 범위가 최소화됨.

📌 D - 의존성 역전 원칙 (Dependency Inversion Principle)

"고수준 모듈은 저수준 모듈에 의존해서는 안 된다. 두 모듈 모두 추상화에 의존해야 한다."

❌ 위반 예시

// 저수준 모듈
class MySQLDatabase {
    public void save(User user) {
        // MySQL에 사용자 저장
    }
}

// 고수준 모듈이 저수준 모듈에 직접 의존
class UserService {
    private MySQLDatabase database = new MySQLDatabase();
    
    public void registerUser(User user) {
        database.save(user);
    }
}

✅ 개선 예시

// 추상화 (인터페이스)
interface UserRepository {
    void save(User user);
}

// 구현체 (저수준 모듈)
class MySQLUserRepository implements UserRepository {
    @Override
    public void save(User user) {
        // MySQL에 사용자 저장
    }
}

class MongoDBUserRepository implements UserRepository {
    @Override
    public void save(User user) {
        // MongoDB에 사용자 저장
    }
}

// 고수준 모듈은 추상화에 의존 (의존성 주입)
class UserService {
    private UserRepository userRepository;
    
    // 생성자 주입
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public void registerUser(User user) {
        userRepository.save(user);
    }
}

핵심 이점: 고수준 모듈이 저수준 모듈에 종속되지 않아 변경에 용이하고, 쉽게 테스트할 수 있으며, 다양한 구현체로 대체 가능해 유연성이 크게 향상됨.

실무 SOLID 적용 팁: 모든 원칙을 한번에 완벽히 적용하려 하기보다 점진적으로 개선하는 접근이 효과적. 경험이 쌓일수록 자연스럽게 원칙을 적용하는 코드를 작성할 수 있게 됨.

5. 클래스와 객체

🧰 클래스 (Class)

객체를 생성하기 위한 템플릿으로, 객체의 상태(속성)와 행동(메서드)을 정의하는 청사진.

  • 객체의 구조와 기능을 정의
  • 메모리에 직접 존재하지 않음
  • 데이터(필드)와 기능(메서드) 포함
  • 접근 제어자로 캡슐화 구현

🎭 객체 (Object)

클래스의 인스턴스로, 실제 메모리에 할당된 실체.

  • 클래스의 실제 구현체
  • 고유한 상태(데이터)와 행동(메서드) 소유
  • 실행 시간(런타임)에 생성 및 조작
  • 다른 객체와 메시지를 주고받음
클래스와 객체 예시
// 클래스 정의
public class Car {
    // 필드(속성): 객체의 상태
    private String model;
    private String color;
    private int year;
    private double speed;
    
    // 생성자: 객체 초기화
    public Car(String model, String color, int year) {
        this.model = model;
        this.color = color;
        this.year = year;
        this.speed = 0;
    }
    
    // 메서드(행동): 객체의 기능
    public void accelerate(double amount) {
        if (amount > 0) {
            this.speed += amount;
            System.out.println(model + "의 속도가 " + amount + "만큼 증가했습니다. 현재 속도: " + speed);
        }
    }
    
    public void brake(double amount) {
        if (amount > 0 && this.speed >= amount) {
            this.speed -= amount;
        } else {
            this.speed = 0;
        }
        System.out.println(model + "가 감속했습니다. 현재 속도: " + speed);
    }
    
    // Getter/Setter 메서드
    public String getModel() {
        return model;
    }
    
    public String getColor() {
        return color;
    }
    
    public void setColor(String color) {
        this.color = color;
    }
    
    public double getSpeed() {
        return speed;
    }
}

// 객체(인스턴스) 생성 및 사용
public class Main {
    public static void main(String[] args) {
        // 첫 번째 Car 객체 생성
        Car myCar = new Car("소나타", "흰색", 2020);
        
        // 두 번째 Car 객체 생성
        Car friendsCar = new Car("아반떼", "검정색", 2019);
        
        // 객체의 메서드 호출
        myCar.accelerate(30);
        friendsCar.accelerate(20);
        
        // 같은 클래스에서 생성된 다른 객체들
        System.out.println("내 차 모델: " + myCar.getModel());
        System.out.println("친구 차 모델: " + friendsCar.getModel());
        
        // 객체의 상태 변경
        myCar.setColor("파란색");
        System.out.println("내 차 색상 변경: " + myCar.getColor());
    }
}

📊 클래스 구성 요소

🔹 필드 (Fields)

  • 클래스 내부에 선언된 변수
  • 객체의 상태/속성을 나타냄
  • 인스턴스 변수, 클래스 변수(static)
  • 접근 제어자로 가시성 제어

🔹 생성자 (Constructors)

  • 객체 초기화를 담당하는 특수 메서드
  • 클래스 이름과 동일한 이름
  • 반환 타입 없음
  • 오버로딩 가능

🔹 메서드 (Methods)

  • 객체가 수행하는 동작 정의
  • 인스턴스 메서드, 정적 메서드
  • 오버로딩, 오버라이딩 가능
  • 매개변수와 반환 타입을 가질 수 있음

🔹 접근 제어자 (Access Modifiers)

  • private: 같은 클래스 내에서만 접근
  • default(package-private): 같은 패키지 내에서만 접근
  • protected: 같은 패키지 + 상속 관계에서 접근
  • public: 어디서든 접근 가능

실무 설계 팁: 클래스는 단일 책임 원칙을 따라 하나의 목적에 집중하도록 설계. 속성은 private으로 선언하고 필요한 경우에만 getter/setter 제공. 클래스 이름은 명사, 메서드 이름은 동사로 시작하게 작성.

6. 상속과 인터페이스

🧬 상속 (Inheritance)

기존 클래스(부모)의 속성과 메서드를 새로운 클래스(자식)가 물려받는 메커니즘.

// 부모 클래스
class Animal {
    protected String name;
    protected int age;
    
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public void eat() {
        System.out.println(name + "이(가) 먹고 있습니다.");
    }
    
    public void sleep() {
        System.out.println(name + "이(가) 자고 있습니다.");
    }
}

// 자식 클래스 (상속)
class Dog extends Animal {
    private String breed;
    
    public Dog(String name, int age, String breed) {
        super(name, age);  // 부모 클래스 생성자 호출
        this.breed = breed;
    }
    
    // 메서드 오버라이딩
    @Override
    public void eat() {
        System.out.println(breed + " " + name + "이(가) 사료를 먹고 있습니다.");
    }
    
    // 새로운 메서드 추가
    public void bark() {
        System.out.println(name + "이(가) 짖고 있습니다: 멍멍!");
    }
}

// 사용 예시
Animal animal = new Animal("동물", 5);
animal.eat();  // "동물이(가) 먹고 있습니다."

Dog dog = new Dog("바둑이", 3, "진돗개");
dog.eat();     // "진돗개 바둑이이(가) 사료를 먹고 있습니다."
dog.sleep();   // "바둑이이(가) 자고 있습니다." (상속받은 메서드)
dog.bark();    // "바둑이이(가) 짖고 있습니다: 멍멍!" (추가된 메서드)

장점

  • 코드 재사용성 증가
  • 계층적 관계 표현에 적합
  • 다형성 구현의 기반

주의사항

  • 상속은 IS-A 관계일 때만 사용
  • 깊은 상속 계층은 복잡성 증가
  • 부모 클래스 변경 시 자식에 영향
  • 다중 상속 지원 안 함 (Java 기준)

🔌 인터페이스 (Interface)

클래스가 구현해야 할 메서드의 명세를 제공하는 추상화 메커니즘.

// 인터페이스 정의
interface Swimmable {
    void swim();
    boolean canDiveDeep();
}

interface Flyable {
    void fly();
    int getMaxAltitude();
}

// 여러 인터페이스 구현
class Duck extends Animal implements Swimmable, Flyable {
    public Duck(String name, int age) {
        super(name, age);
    }
    
    // Swimmable 인터페이스 구현
    @Override
    public void swim() {
        System.out.println(name + "이(가) 수영하고 있습니다.");
    }
    
    @Override
    public boolean canDiveDeep() {
        return false;
    }
    
    // Flyable 인터페이스 구현
    @Override
    public void fly() {
        System.out.println(name + "이(가) 날고 있습니다.");
    }
    
    @Override
    public int getMaxAltitude() {
        return 1000;
    }
}

// 인터페이스를 활용한 다형성
Swimmable swimmer = new Duck("오리", 2);
swimmer.swim();  // "오리이(가) 수영하고 있습니다."
// swimmer.fly();  // 컴파일 에러! Swimmable 타입에는 fly() 메서드가 없음

Flyable flyer = new Duck("날쌘오리", 3);
flyer.fly();     // "날쌘오리이(가) 날고 있습니다."

장점

  • 다중 구현 가능 (다중 상속 효과)
  • 객체 간 계약 명확화
  • 느슨한 결합으로 유연성 증가
  • 모듈화 및 테스트 용이성

특징

  • 모든 메서드는 기본적으로 public abstract
  • Java 8부터 default, static 메서드 지원
  • 상수만 포함 가능 (변수 X)
  • 구현 클래스는 모든 메서드 구현 필수

🧩 추상 클래스 vs 인터페이스

특성 추상 클래스 인터페이스
정의 일부 구현이 있는 불완전한 클래스 메서드 명세만 제공하는 완전한 추상체
선언 방법 abstract class ClassName interface InterfaceName
확장/구현 extends (단일 상속) implements (다중 구현)
메서드 추상 메서드와 구현 메서드 모두 가능 Java 8 이전: 추상 메서드만
Java 8 이후: default, static 메서드 가능
필드 인스턴스 변수, 상수 모두 가능 상수만 가능 (public static final)
생성자 가질 수 있음 가질 수 없음
접근 제어자 모든 접근 제어자 사용 가능 메서드는 기본적으로 public
사용 상황 IS-A 관계, 공통 기능 구현이 필요할 때 CAN-DO 관계, 서로 다른 클래스에 공통 동작 정의할 때

실무 중요 포인트: 상속보다 합성(Composition)을 우선 고려할 것. 상속은 강한 결합을 만들어 유연성을 제한하는 반면, 합성은 객체를 필드로 포함해 사용함으로써 느슨한 결합을 유지할 수 있음.

7. 객체 지향 디자인 패턴

디자인 패턴은 소프트웨어 설계에서 자주 발생하는 문제에 대한 검증된 솔루션. 코드 재사용성, 유지보수성, 확장성을 높이는 데 도움이 됨.

🔒 싱글톤 패턴 (Singleton)

클래스의 인스턴스가 오직 하나만 생성되도록 보장하는 패턴.

public class DatabaseConnection {
    // 1. private static 인스턴스 변수
    private static DatabaseConnection instance;
    
    // 2. private 생성자 (외부에서 new 키워드로 생성 방지)
    private DatabaseConnection() {
        // 데이터베이스 연결 설정
    }
    
    // 3. public static 접근 메서드
    public static DatabaseConnection getInstance() {
        if (instance == null) {
            // 최초 호출 시에만 객체 생성
            instance = new DatabaseConnection();
        }
        return instance;
    }
    
    public void query(String sql) {
        // 쿼리 실행 로직
        System.out.println("SQL 실행: " + sql);
    }
}

// 사용 예시
DatabaseConnection db1 = DatabaseConnection.getInstance();
DatabaseConnection db2 = DatabaseConnection.getInstance();

// db1과 db2는 같은 인스턴스를 참조
System.out.println(db1 == db2);  // true

db1.query("SELECT * FROM users");

사용 사례: 데이터베이스 연결, 로깅, 설정 관리, 캐시 등 애플리케이션 전체에서 하나의 인스턴스만 필요한 경우.

쓰레드 안전성을 위해 Double-Checked Locking 패턴이나 정적 내부 클래스 방식을 고려할 것.

🏭 팩토리 패턴 (Factory)

객체 생성 로직을 캡슐화하여 클라이언트 코드와 분리하는 패턴.

// 제품 인터페이스
interface Vehicle {
    void drive();
}

// 구체적인 제품 클래스들
class Car implements Vehicle {
    @Override
    public void drive() {
        System.out.println("자동차를 운전합니다.");
    }
}

class Motorcycle implements Vehicle {
    @Override
    public void drive() {
        System.out.println("오토바이를 운전합니다.");
    }
}

class Truck implements Vehicle {
    @Override
    public void drive() {
        System.out.println("트럭을 운전합니다.");
    }
}

// 팩토리 클래스
class VehicleFactory {
    public static Vehicle createVehicle(String type) {
        switch (type.toLowerCase()) {
            case "car":
                return new Car();
            case "motorcycle":
                return new Motorcycle();
            case "truck":
                return new Truck();
            default:
                throw new IllegalArgumentException("지원하지 않는 차량 유형: " + type);
        }
    }
}

// 클라이언트 코드
Vehicle car = VehicleFactory.createVehicle("car");
car.drive();  // "자동차를 운전합니다."

Vehicle motorcycle = VehicleFactory.createVehicle("motorcycle");
motorcycle.drive();  // "오토바이를 운전합니다."

사용 사례: 객체 생성 프로세스가 복잡한 경우, 객체 생성 로직을 클라이언트 코드와 분리해야 할 때, 객체 생성 시점을 제어하고 싶을 때.

📝 전략 패턴 (Strategy)

알고리즘 군을 정의하고 캡슐화하여 교체 가능하게 만드는 패턴.

// 전략 인터페이스
interface PaymentStrategy {
    void pay(int amount);
}

// 구체적인 전략 클래스들
class CreditCardStrategy implements PaymentStrategy {
    private String cardNumber;
    private String cvv;
    
    public CreditCardStrategy(String cardNumber, String cvv) {
        this.cardNumber = cardNumber;
        this.cvv = cvv;
    }
    
    @Override
    public void pay(int amount) {
        System.out.println(amount + "원을 신용카드로 결제했습니다.");
    }
}

class PaypalStrategy implements PaymentStrategy {
    private String email;
    private String password;
    
    public PaypalStrategy(String email, String password) {
        this.email = email;
        this.password = password;
    }
    
    @Override
    public void pay(int amount) {
        System.out.println(amount + "원을 페이팔로 결제했습니다.");
    }
}

// 컨텍스트 클래스
class ShoppingCart {
    private PaymentStrategy paymentStrategy;
    
    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }
    
    public void checkout(int amount) {
        paymentStrategy.pay(amount);
    }
}

// 사용 예시
ShoppingCart cart = new ShoppingCart();

// 신용카드로 결제
cart.setPaymentStrategy(new CreditCardStrategy("1234-5678-9012-3456", "123"));
cart.checkout(50000);

// 페이팔로 결제 방식 변경
cart.setPaymentStrategy(new PaypalStrategy("user@example.com", "password"));
cart.checkout(30000);

사용 사례: 런타임에 알고리즘을 선택해야 할 때, 다양한 알고리즘 변형이 필요할 때, 조건문으로 알고리즘을 선택하는 코드를 리팩토링할 때.

👀 옵저버 패턴 (Observer)

객체 간 일대다 의존성을 정의하고, 한 객체의 상태 변화가 다른 객체에 자동으로 통지되도록 하는 패턴.

// 옵저버 인터페이스
interface Observer {
    void update(String message);
}

// 주제(Subject) 인터페이스
interface Subject {
    void registerObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
}

// 구체적인 주제 클래스
class NewsAgency implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private String news;
    
    @Override
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }
    
    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }
    
    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(news);
        }
    }
    
    public void setNews(String news) {
        this.news = news;
        notifyObservers();  // 뉴스가 변경되면 모든 옵저버에게 알림
    }
}

// 구체적인 옵저버 클래스
class NewsChannel implements Observer {
    private String name;
    
    public NewsChannel(String name) {
        this.name = name;
    }
    
    @Override
    public void update(String news) {
        System.out.println(name + ": 속보 - " + news);
    }
}

// 사용 예시
NewsAgency agency = new NewsAgency();

NewsChannel channel1 = new NewsChannel("채널 A");
NewsChannel channel2 = new NewsChannel("채널 B");

// 옵저버 등록
agency.registerObserver(channel1);
agency.registerObserver(channel2);

// 뉴스 발행 (모든 채널에 전파됨)
agency.setNews("중요한 뉴스가 발생했습니다!");
// 출력:
// 채널 A: 속보 - 중요한 뉴스가 발생했습니다!
// 채널 B: 속보 - 중요한 뉴스가 발생했습니다!

// 옵저버 제거
agency.removeObserver(channel1);

// 새 뉴스 발행 (channel2만 수신)
agency.setNews("또 다른 뉴스가 있습니다.");
// 출력:
// 채널 B: 속보 - 또 다른 뉴스가 있습니다.

사용 사례: 이벤트 처리 시스템, GUI 프레임워크, 데이터 모델 변경 시 뷰 업데이트(MVC), 메시징 시스템, 구독 시스템.

🔍 그 외 중요한 디자인 패턴

데코레이터 패턴

기존 객체에 동적으로 기능을 추가하는 패턴.

사용 사례: I/O 스트림, UI 컴포넌트 확장, 기능 레이어링.

어댑터 패턴

호환되지 않는 인터페이스를 함께 작동하도록 변환하는 패턴.

사용 사례: 라이브러리 통합, 레거시 시스템 연동, API 어댑터.

템플릿 메서드 패턴

알고리즘의 구조는 정의하고 일부 단계를 하위 클래스에서 구현하도록 하는 패턴.

사용 사례: 프레임워크 설계, 반복적인 프로세스, 알고리즘 틀 정의.

프록시 패턴

다른 객체에 대한 접근을 제어하는 대리자 객체를 제공하는 패턴.

사용 사례: 지연 로딩, 접근 제어, 원격 객체 접근, 로깅, 캐싱.

컴포지트 패턴

객체를 트리 구조로 구성하여 단일 객체처럼 다룰 수 있게 하는 패턴.

사용 사례: UI 컴포넌트, 파일 시스템, 메뉴 시스템, 조직도.

커맨드 패턴

요청을 객체로 캡슐화하여 매개변수화, 큐에 저장, 실행 취소 등을 가능하게 하는 패턴.

사용 사례: GUI 버튼, 매크로 기능, 트랜잭션, 멀티 단계 작업.

실무 디자인 패턴 팁: 패턴을 위한 패턴 적용은 오히려 코드를 복잡하게 만들 수 있음. 특정 문제 해결에 필요할 때 적절한 패턴을 선택하고, 팀의 이해도를 고려해 적용할 것. 과도한 엔지니어링(over-engineering)을 피하고 YAGNI(You Aren't Gonna Need It) 원칙을 염두에 둘 것.

주니어 개발자를 위한 객체 지향 프로그래밍 총정리 © 2025