주니어 개발자를 위한 객체 지향 프로그래밍 총정리
실무에서 꼭 알아야 할 OOP 개념과 실전 활용법
1. 객체 지향 프로그래밍 개요
객체 지향 프로그래밍(Object-Oriented Programming, OOP)은 현실 세계의 개념이나 사물을 객체로 추상화해 프로그램을 구조화하는 패러다임. 객체를 중심으로 데이터와 기능을 함께 관리해 코드의 재사용성과 유지보수성을 높임.
📌 객체 지향 프로그래밍의 핵심 장점
- 높은 재사용성 - 클래스와 상속을 통해 코드 재사용 극대화
- 쉬운 유지보수 - 모듈화로 인한 변경 영향 범위 최소화
- 유연한 확장성 - 기존 코드 수정 없이 새 기능 추가 가능
- 직관적 모델링 - 실세계 개념을 자연스럽게 코드로 표현
- 안정적 구조화 - 캡슐화를 통한 안전한 코드 관리
실무에서 OOP는 코드를 이해하기 쉽게 만들고 팀 협업을 원활하게 하는 핵심 요소. 특히 대규모 프로젝트에서 유지보수성과 확장성에 큰 영향을 미침.
2. 절차 지향 vs 객체 지향
🔄 절차 지향 프로그래밍
- 순차적 실행 흐름 중심의 설계
- 데이터와 함수가 별도로 존재
- "어떻게(How)" 처리할지 초점
- Top-Down 방식의 문제 해결
- 간단한 프로그램에 적합
🧩 객체 지향 프로그래밍
- 객체 간 상호작용 중심의 설계
- 데이터와 메서드가 객체 내에 통합
- "무엇이(What)" 필요한지 초점
- Bottom-Up 방식의 문제 해결
- 복잡하고 큰 프로젝트에 적합
// 절차 지향 방식 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) 원칙을 염두에 둘 것.
'언어 공부 > JAVA' 카테고리의 다른 글
JAVA D8 [ 상속 ] - (1) | 2025.03.12 |
---|---|
JAVA D7 [ 접근 제어자 ] - 실무에서 꼭 알아야할 접근 제어자 (1) | 2025.03.10 |
JAVA D5 [ 기본형&참조형 ] - 실무에서 꼭 알아야 할 자바의 참조형 (2) | 2025.03.08 |
JAVA D5 [ 클래스 ] - 실무에서 바로 사용 가능한 핵심 포인트 (1) | 2025.03.08 |
JAVA D4 [ 메서드 ] - 메서드 실무에서 꼭 알아야할 내용 정리 (0) | 2025.03.07 |