class MySQLDatabase {
void connect() { System.out.println("MySQL 연결"); }
}
class DataManager {
MySQLDatabase database = new MySQLDatabase(); // 특정 구현체에 직접 의존
}
➡︎ DataManger 가 MySQLDatabase 와 강하게 결합되어 다른 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가지 방식
생성자 주입 (Constructor Injection): 생성자를 통해 의존 객체를 주입 ex) new DataManager(new MySQLDatabase())
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 위반)
}
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;
}
}
// 부모 클래스 (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 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(); // 동작 수행
}
}
➡︎ myCar 는 Car 클래스로부터 만들어진 객체!
➡︎ 개별 객체에 속성을 설정할 수 있음 (myCar.brand = "Tesla")
객체를 사용하는 이유
코드의 재사용성 증가
클래스를 한 번 정의하면 여러 객체를 생성할 수 있음
같은 구조를 가진 객체를 만들 때 중복 코드를 줄일 수 있음
유지보수성 향상
코드가 모듈화되므로, 특정 기능을 수정할 때 다른 부분에 영향을 줄 가능성이 낮아짐
관련 기능을 묶어 가독성이 향상됨
캡슐화 및 정보 은닉 가능
객체 내부의 데이터를 보호하고, 불필요한 접근을 제한할 수 있음
외부에서는 제한된 방식으로만 데이터를 변경할 수 있도록 제어 가능
캡슐화
캡슐화(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() 메서드를 통해서만 잔액 변경 가능! ⭐️ 데이터 무결성 유지 ⭐️
주요 포인트
클래스와 객체의 차이를 명확히 알아야 함!
클래스: 객체를 만들기 위한 설계도
객체: 클래스를 기반으로 생성된 실체
객체를 사용하는 이유를 코드 재사용성, 유지보수성, 캡슐화 관점에서 논리적으로 설명할 줄 알아야 함!