안녕하세요! 오늘은 자바 프로그래밍의 핵심이자, 프로그래밍 패러다임의 거장, 객체 지향 프로그래밍 (OOP, Object-Oriented Programming) 에 대해 심층적으로 탐구하는 시간을 갖겠습니다. 마치 건축가가 건물을 설계하듯, 객체 지향 프로그래밍은 우리가 더욱 체계적이고 효율적으로 코드를 설계하고 개발할 수 있도록 강력한 도구를 제공합니다.
이번 포스팅에서는 객체 지향 프로그래밍의 4가지 핵심 개념인 캡슐화, 상속, 다형성, 추상화를 낱낱이 파헤치고, 객체 지향 설계의 기본 원칙 (SOLID) 까지 꼼꼼하게 짚어보면서, 자바 프로그래밍의 본질을 이해하는 여정을 함께 떠나볼 거예요. 준비되셨나요? 😃
💡 객체 지향 프로그래밍 (OOP) 이란 무엇일까요?
객체 지향 프로그래밍 (OOP) 은 프로그램을 객체 (Object) 라는 단위로 구성하고, 객체 간의 상호작용을 통해 프로그램을 개발하는 프로그래밍 패러다임입니다. 현실 세계를 모델링하여 코드를 작성하기 때문에, 직관적이고 이해하기 쉬운 코드를 만들 수 있으며, 코드의 재사용성, 유지보수성, 확장성을 높이는 데 매우 효과적입니다.
객체 지향 프로그래밍은 단순히 코딩 스타일을 넘어서, 사고방식과 문제 해결 방식을 변화시키는 패러다임입니다. 객체 지향적인 사고 방식을 익히면, 더욱 창의적이고 혁신적인 소프트웨어 개발이 가능해질 거예요.
🧱 클래스와 객체 심층 분석: OOP의 기본 틀 다지기
객체 지향 프로그래밍의 핵심은 클래스 (Class) 와 객체 (Object) 입니다. 클래스는 설계도, 객체는 실제 제품이라고 비유할 수 있습니다.
🔨 클래스 (Class): 객체를 찍어내는 틀
클래스 (Class) 는 객체를 정의하는 설계도 또는 템플릿 입니다. 클래스에는 객체의 속성 (Attribute, 멤버 변수) 과 기능 (Method, 멤버 함수) 이 정의되어 있습니다. 클래스는 객체를 생성하기 위한 청사진 역할을 하며, 실제 데이터나 코드를 저장하는 것은 아닙니다.
public class Car { // Car 클래스 정의
// 속성 (멤버 변수)
String modelName; // 모델 이름
String color; // 색상
int speed; // 속도
// 기능 (메소드)
void accelerate() { // 가속 기능
speed += 10;
}
void brake() { // 감속 기능
speed -= 10;
if (speed < 0) {
speed = 0;
}
}
}
위 예시는 Car 라는 클래스를 정의한 코드입니다. Car 클래스는 modelName, color, speed 라는 속성과 accelerate(), brake() 라는 기능을 가지고 있습니다.
🔩 객체 (Object): 클래스로 만들어진 실체
객체 (Object) 는 클래스를 기반으로 실제로 생성된 실체 (Instance) 입니다. 객체는 클래스에 정의된 속성과 기능을 메모리에 할당받아, 실제로 데이터를 저장하고 기능을 수행할 수 있습니다. 하나의 클래스로부터 여러 개의 객체를 생성할 수 있습니다.
public class Main {
public static void main(String[] args) {
Car myCar = new Car(); // Car 클래스의 객체 myCar 생성
myCar.modelName = "Sonata"; // 객체의 속성 값 설정
myCar.color = "Black";
myCar.speed = 0;
System.out.println("내 차 모델: " + myCar.modelName); // 객체의 속성 값 사용
System.out.println("내 차 색상: " + myCar.color);
System.out.println("현재 속도: " + myCar.speed);
myCar.accelerate(); // 객체의 기능 호출
System.out.println("가속 후 속도: " + myCar.speed);
myCar.brake(); // 객체의 기능 호출
System.out.println("감속 후 속도: " + myCar.speed);
}
}
위 코드는 Car 클래스의 객체 myCar 를 생성하고, 속성 값을 설정하고, 기능을 호출하는 예시입니다. new Car() 를 통해 Car 클래스의 객체를 생성하고, myCar.modelName = "Sonata" 와 같이 점 (.) 연산자를 사용하여 객체의 속성과 기능에 접근할 수 있습니다.
🔑 this 키워드: 객체 자신을 가리키는 특별한 존재
this 는 현재 객체 자신을 가리키는 참조 변수 입니다. 클래스 내부에서 멤버 변수와 매개변수 이름이 같을 경우, this 키워드를 사용하여 멤버 변수를 명확하게 지칭할 수 있습니다. 또한, 생성자 내부에서 다른 생성자를 호출할 때도 this 키워드를 사용합니다.
public class Person {
String name;
int age;
// 생성자
public Person(String name, int age) {
this.name = name; // this.name: 멤버 변수 name, name: 매개변수 name
this.age = age; // this.age: 멤버 변수 age, age: 매개변수 age
}
// 또 다른 생성자 (this() 사용하여 다른 생성자 호출)
public Person(String name) {
this(name, 20); // Person(String name, int age) 생성자 호출 (나이는 기본값 20으로 설정)
}
void introduce() {
System.out.println("안녕하세요, 제 이름은 " + this.name + "이고, " + this.age + "살입니다."); // this.name, this.age: 현재 객체의 속성
}
}
this 키워드를 사용하면 코드의 가독성을 높이고, 객체 내부에서 멤버 변수와 매개변수를 명확하게 구분할 수 있습니다.
📌 static 키워드: 클래스 레벨 멤버를 위한 특별한 지시자
static 키워드는 멤버 변수 또는 메소드를 클래스 레벨 멤버로 선언할 때 사용합니다. static 멤버는 객체가 아닌 클래스 자체에 속하며, 모든 객체가 공유하는 멤버입니다. 객체를 생성하지 않고도 클래스 이름으로 직접 접근할 수 있습니다.
public class Counter {
static int count = 0; // static 멤버 변수 (클래스 변수)
public Counter() {
count++; // 객체 생성 시 static 변수 count 증가
}
static void printCount() { // static 메소드 (클래스 메소드)
System.out.println("현재 Counter 객체 수: " + count); // static 멤버 변수에 접근
}
}
public class Main {
public static void main(String[] args) {
Counter.printCount(); // 객체 생성 없이 클래스 이름으로 static 메소드 호출 (결과: 0)
Counter c1 = new Counter(); // 객체 c1 생성
Counter.printCount(); // 클래스 이름으로 static 메소드 호출 (결과: 1)
Counter c2 = new Counter(); // 객체 c2 생성
Counter.printCount(); // 클래스 이름으로 static 메소드 호출 (결과: 2)
System.out.println("Counter 객체 수 (static 변수): " + Counter.count); // 클래스 이름으로 static 변수에 접근 (결과: 2)
}
}
static 멤버는 프로그램 전역에서 공유하는 데이터나 기능을 구현할 때 유용하게 사용됩니다. 대표적인 예시로 싱글톤 패턴, 유틸리티 클래스, 상수 등이 있습니다. main 메소드 또한 static 메소드로 선언되어 프로그램 시작 시 객체 생성 없이 바로 실행될 수 있습니다.
🔒 캡슐화 (Encapsulation): 정보 은닉으로 안전하게 감싸기
캡슐화 (Encapsulation) 는 데이터 (속성) 와 기능 (메소드) 을 하나의 캡슐로 묶는 것을 의미합니다. 클래스 내부의 구현 details 를 숨기고, 외부로부터의 직접적인 접근을 제한하여 데이터의 무결성과 보안성을 높이는 객체 지향 프로그래밍의 핵심 원리입니다. 마치 약 캡슐처럼, 중요한 내용물을 안전하게 감싸서 보호하는 것과 같습니다.
정보 은닉 (Information Hiding) 은 캡슐화의 중요한 측면으로, 클래스 내부 데이터와 구현을 외부에 숨기고, 필요한 기능만 메소드를 통해 외부에 제공하는 것을 의미합니다. 정보 은닉을 통해 클래스 내부 구현이 변경되더라도 외부 코드에 미치는 영향을 최소화하고, 코드의 유연성과 유지보수성을 향상시킬 수 있습니다.
자바에서는 접근 제어 지시자 (Access Modifier) 를 사용하여 캡슐화 및 정보 은닉을 구현합니다.
- public: 모든 클래스에서 접근 가능합니다.
- protected: 같은 패키지 또는 상속 관계의 클래스에서 접근 가능합니다.
- default (package-private): 같은 패키지 내에서만 접근 가능합니다. (접근 제어 지시자를 명시하지 않을 경우 기본적으로 default 접근 제어자가 적용됩니다.)
- private: 자신이 선언된 클래스 내부에서만 접근 가능합니다.
일반적으로 클래스의 속성 (멤버 변수) 은 private 으로 선언하여 외부 클래스에서의 직접적인 접근을 막고, 속성에 접근하거나 수정하는 기능은 public 메소드 (getter/setter 메소드) 를 통해 제공하여 캡슐화를 구현합니다.
public class Account {
private int balance; // private 멤버 변수 (잔액), 외부에서 직접 접근 불가
public int getBalance() { // public getter 메소드, 잔액 조회 기능 제공
return balance;
}
public void deposit(int amount) { // public setter 메소드 (deposit), 입금 기능 제공
if (amount > 0) {
balance += amount;
}
}
public void withdraw(int amount) { // public setter 메소드 (withdraw), 출금 기능 제공
if (amount > 0 && balance >= amount) {
balance -= amount;
} else {
System.out.println("잔액 부족 또는 잘못된 출금 요청입니다.");
}
}
}
public class Main {
public static void main(String[] args) {
Account account = new Account();
// account.balance = 10000; // 에러! private 멤버 변수에 직접 접근 불가
account.deposit(50000); // public 메소드를 통해 입금 기능 사용 (캡슐화)
System.out.println("현재 잔액: " + account.getBalance()); // public 메소드를 통해 잔액 조회 (캡슐화)
account.withdraw(100000); // 잘못된 출금 요청 (잔액 부족)
System.out.println("현재 잔액: " + account.getBalance()); // 잔액 변동 없음
account.withdraw(20000); // 정상 출금
System.out.println("현재 잔액: " + account.getBalance()); // 잔액 감소
}
}
위 예시에서 Account 클래스의 balance 속성은 private 으로 선언되어 외부에서 직접 접근할 수 없지만, public 메소드인 getBalance(), deposit(), withdraw() 를 통해 잔액 조회, 입금, 출금 기능을 안전하게 사용할 수 있도록 캡슐화가 구현되었습니다.
🤝 상속 (Inheritance): 코드 재사용과 확장, 뼈대 위에 살 붙이기
상속 (Inheritance) 은 상위 클래스 (부모 클래스, Super Class) 의 속성과 기능을 하위 클래스 (자식 클래스, Sub Class) 가 물려받는 객체 지향 프로그래밍의 핵심 개념입니다. 상속을 통해 코드 재사용성을 높이고, 클래스 설계를 확장하기 용이하게 만들 수 있습니다. 마치 건물의 뼈대 위에 새로운 층을 쌓아 올리는 것처럼, 기존 코드를 기반으로 새로운 기능을 추가하고 확장하는 데 효과적입니다.
자바에서는 extends 키워드를 사용하여 클래스 상속을 구현합니다.
// 상위 클래스 (부모 클래스)
class Animal {
String name;
void eat() {
System.out.println(name + "이(가) 먹이를 먹습니다.");
}
void sleep() {
System.out.println(name + "이(가) 잠을 잡니다.");
}
}
// 하위 클래스 (자식 클래스), Animal 클래스를 상속
class Dog extends Animal {
void bark() {
System.out.println(name + "이(가) 멍멍 짖습니다.");
}
}
// 하위 클래스 (자식 클래스), Animal 클래스를 상속
class Cat extends Animal {
void meow() {
System.out.println(name + "이(가) 야옹 웁니다.");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.name = "Happy";
dog.eat(); // Animal 클래스로부터 상속받은 메소드
dog.sleep(); // Animal 클래스로부터 상속받은 메소드
dog.bark(); // Dog 클래스에 정의된 메소드
Cat cat = new Cat();
cat.name = "Navi";
cat.eat(); // Animal 클래스로부터 상속받은 메소드
cat.sleep(); // Animal 클래스로부터 상속받은 메소드
cat.meow(); // Cat 클래스에 정의된 메소드
}
}
위 예시에서 Dog 클래스와 Cat 클래스는 Animal 클래스를 상속받았습니다. Dog 와 Cat 클래스는 Animal 클래스의 name, eat(), sleep() 속성과 기능을 상속받아 사용할 수 있으며, Dog 클래스는 bark(), Cat 클래스는 meow() 와 같이 자신만의 고유한 기능을 추가할 수 있습니다.
메소드 오버라이딩 (Method Overriding) 은 하위 클래스에서 상위 클래스로부터 상속받은 메소드를 재정의하는 것을 의미합니다. 하위 클래스에서 상속받은 메소드를 필요에 맞게 수정하거나 확장할 수 있습니다.
class Animal {
void makeSound() {
System.out.println("동물이 소리를 냅니다.");
}
}
class Dog extends Animal {
@Override // 어노테이션, 오버라이딩 명시
void makeSound() { // 메소드 오버라이딩 (재정의)
System.out.println("멍멍!"); // Dog 클래스에 맞게 makeSound() 메소드 재정의
}
}
class Cat extends Animal {
@Override // 어노테이션, 오버라이딩 명시
void makeSound() { // 메소드 오버라이딩 (재정의)
System.out.println("야옹!"); // Cat 클래스에 맞게 makeSound() 메소드 재정의
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Animal();
Dog dog = new Dog();
Cat cat = new Cat();
animal.makeSound(); // Animal 클래스의 makeSound() 호출 (결과: "동물이 소리를 냅니다.")
dog.makeSound(); // Dog 클래스의 makeSound() 호출 (결과: "멍멍!")
cat.makeSound(); // Cat 클래스의 makeSound() 호출 (결과: "야옹!")
}
}
위 예시에서 Dog 와 Cat 클래스는 Animal 클래스의 makeSound() 메소드를 오버라이딩하여, 각 클래스에 맞는 소리를 내도록 재정의했습니다. @Override 어노테이션은 메소드 오버라이딩을 명시적으로 나타내며, 컴파일러에게 오버라이딩 검사를 요청하는 역할을 합니다.
🎭 다형성 (Polymorphism): 유연하고 확장 가능한 설계, 여러 모습으로 변신
다형성 (Polymorphism) 은 "하나의 인터페이스, 다양한 구현" 을 의미합니다. 같은 타입의 변수 또는 메소드 호출이 실제 객체의 타입에 따라 다르게 동작하는 것을 말합니다. 다형성을 통해 코드의 유연성과 확장성을 높이고, 복잡한 시스템을 단순하게 만들 수 있습니다. 마치 배우가 다양한 역할을 소화하듯, 객체가 상황에 따라 여러 모습으로 변신하는 것과 같습니다.
자바에서 다형성은 오버로딩 (Overloading) 과 오버라이딩 (Overriding), 그리고 인터페이스 (Interface) 와 추상 클래스 (Abstract Class) 를 통해 구현됩니다.
- 메소드 오버로딩 (Method Overloading): 같은 클래스 내에서 이름은 같지만 매개변수 타입 또는 개수가 다른 메소드를 여러 개 정의하는 것을 의미합니다. 메소드 오버로딩을 통해 같은 기능을 수행하지만 다양한 방식으로 호출할 수 있는 메소드를 제공할 수 있습니다.
public class Calculator {
// 메소드 오버로딩 예시
int add(int a, int b) { // 정수 2개 덧셈
return a + b;
}
double add(double a, double b) { // 실수 2개 덧셈
return a + b;
}
int add(int a, int b, int c) { // 정수 3개 덧셈
return a + b + c;
}
}
public class Main {
public static void main(String[] args) {
Calculator calculator = new Calculator();
System.out.println(calculator.add(3, 5)); // int add(int a, int b) 호출 (결과: 8)
System.out.println(calculator.add(3.5, 2.7)); // double add(double a, double b) 호출 (결과: 6.2)
System.out.println(calculator.add(1, 2, 3)); // int add(int a, int b, int c) 호출 (결과: 6)
}
}
위 예시에서 Calculator 클래스는 add() 라는 이름의 메소드를 매개변수 타입과 개수를 달리하여 3개 오버로딩했습니다. 호출 시점에 전달되는 매개변수 타입과 개수에 따라 컴파일러가 적절한 메소드를 자동으로 선택하여 실행합니다.
- 메소드 오버라이딩 (Method Overriding): 상속 챕터에서 이미 설명했듯이, 하위 클래스에서 상위 클래스로부터 상속받은 메소드를 재정의하는 것을 의미합니다. 오버라이딩은 다형성의 중요한 구현 방식 중 하나이며, 상위 타입의 변수로 하위 타입의 객체를 참조하고 메소드를 호출했을 때, 실제 객체의 타입에 맞는 오버라이딩된 메소드가 실행되는 다형성을 구현할 수 있습니다.
public class Main {
public static void main(String[] args) {
Animal animal = new Animal(); // Animal 객체
Dog dog = new Dog(); // Dog 객체
Cat cat = new Cat(); // Cat 객체
Animal animalDog = new Dog(); // 다형성! Animal 타입 변수로 Dog 객체 참조
Animal animalCat = new Cat(); // 다형성! Animal 타입 변수로 Cat 객체 참조
animal.makeSound(); // Animal 객체의 makeSound() 호출 (결과: "동물이 소리를 냅니다.")
dog.makeSound(); // Dog 객체의 makeSound() 호출 (결과: "멍멍!")
cat.makeSound(); // Cat 객체의 makeSound() 호출 (결과: "야옹!")
animalDog.makeSound(); // Animal 타입 변수이지만, 실제 Dog 객체의 makeSound() 호출 (다형성, 결과: "멍멍!")
animalCat.makeSound(); // Animal 타입 변수이지만, 실제 Cat 객체의 makeSound() 호출 (다형성, 결과: "야옹!")
Animal[] animals = {animal, dog, cat, animalDog, animalCat}; // Animal 타입 배열에 다양한 Animal 하위 타입 객체 저장 (다형성)
for (Animal ani : animals) {
ani.makeSound(); // 배열 요소 타입은 Animal 이지만, 실제 객체 타입에 맞는 makeSound() 호출 (다형성)
}
}
}
위 예시에서 Animal 타입 변수 animalDog 와 animalCat 은 각각 Dog 객체와 Cat 객체를 참조하지만, animalDog.makeSound() 와 animalCat.makeSound() 를 호출하면 실제 객체 타입인 Dog 와 Cat 클래스에서 오버라이딩된 makeSound() 메소드가 실행됩니다. 이것이 다형성의 핵심적인 특징입니다. 또한, Animal 타입 배열 animals 에 다양한 Animal 하위 타입 객체를 저장하고, 반복문 안에서 ani.makeSound() 를 호출하면 각 객체의 실제 타입에 맞는 makeSound() 메소드가 실행되는 다형성을 확인할 수 있습니다.
- 인터페이스 (Interface) 와 추상 클래스 (Abstract Class): 인터페이스와 추상 클래스는 다형성을 구현하는 데 중요한 역할을 합니다. 인터페이스는 메소드 시그니처 (이름, 매개변수 타입, 반환 타입) 만 정의하고, 구현은 인터페이스를 구현하는 클래스에 맡깁니다. 추상 클래스는 일부 메소드는 구현하고, 나머지 메소드는 추상 메소드로 선 1 언하여 하위 클래스에서 반드시 구현하도록 강제합니다. 인터페이스와 추상 클래스를 사용하면 느슨한 결합 (Loose Coupling) 을 통해 유연하고 확장 가능한 설계를 구현할 수 있습니다.
🌁 추상화 (Abstraction): 복잡함은 숨기고, 핵심만 드러내기
추상화 (Abstraction) 는 복잡한 시스템을 단순화하여 핵심적인 부분만 드러내고, 불필요한 details 는 숨기는 객체 지향 프로그래밍의 중요한 원리입니다. 추상화를 통해 시스템의 복잡성을 관리하고, 사용자가 시스템의 본질을 쉽게 이해하고 사용할 수 있도록 돕습니다. 마치 자동차 운전자가 복잡한 엔진 내부 구조를 몰라도 운전할 수 있는 것처럼, 추상화는 시스템 사용 방법을 단순화하고 편리하게 만들어 줍니다.
자바에서 추상화는 추상 클래스 (Abstract Class) 와 인터페이스 (Interface) 를 통해 구현됩니다.
- 추상 클래스 (Abstract Class): abstract 키워드를 사용하여 선언하는 클래스입니다. 추상 클래스는 객체를 생성할 수 없고, 상속을 통해서만 사용할 수 있습니다. 추상 클래스는 추상 메소드 (Abstract Method) 를 포함할 수 있으며, 추상 메소드는 구현부가 없고 선언만 되어 있는 메소드입니다. 추상 클래스를 상속받는 하위 클래스는 반드시 추상 메소드를 오버라이딩 (구현) 해야 합니다. 추상 클래스는 일부 기능은 구현하고, 나머지 기능은 하위 클래스에 위임하는 템플릿 역할을 합니다.
// 추상 클래스
abstract class Shape {
String color;
public Shape(String color) {
this.color = color;
}
// 추상 메소드 (구현부 없음)
abstract double calculateArea(); // 넓이 계산 추상 메소드
void displayColor() { // 일반 메소드 (구현부 있음)
System.out.println("색상: " + color);
}
}
// 추상 클래스 Shape 상속받는 하위 클래스
class Circle extends Shape {
double radius;
public Circle(String color, double radius) {
super(color); // 부모 클래스 생성자 호출
this.radius = radius;
}
@Override
double calculateArea() { // 추상 메소드 오버라이딩 (구현)
return Math.PI * radius * radius; // 원의 넓이 계산
}
}
// 추상 클래스 Shape 상속받는 하위 클래스
class Rectangle extends Shape {
double width;
double height;
public Rectangle(String color, double width, double height) {
super(color); // 부모 클래스 생성자 호출
this.width = width;
this.height = height;
}
@Override
double calculateArea() { // 추상 메소드 오버라이딩 (구현)
return width * height; // 사각형 넓이 계산
}
}
public class Main {
public static void main(String[] args) {
// Shape shape = new Shape("Red"); // 에러! 추상 클래스는 객체 생성 불가
Circle circle = new Circle("Blue", 5.0);
Rectangle rectangle = new Rectangle("Yellow", 4.0, 6.0);
circle.displayColor(); // Shape 클래스로부터 상속받은 일반 메소드 호출
System.out.println("원 넓이: " + circle.calculateArea()); // Circle 클래스에서 구현한 메소드 호출
rectangle.displayColor(); // Shape 클래스로부터 상속받은 일반 메소드 호출
System.out.println("사각형 넓이: " + rectangle.calculateArea()); // Rectangle 클래스에서 구현한 메소드 호출
Shape[] shapes = {circle, rectangle}; // Shape 타입 배열에 하위 타입 객체 저장 (다형성)
for (Shape shape : shapes) {
System.out.println("도형 넓이: " + shape.calculateArea()); // 배열 요소 타입은 Shape 이지만, 실제 객체 타입에 맞는 calculateArea() 호출 (다형성)
}
}
}
위 예시에서 Shape 클래스는 추상 클래스로 선언되었으며, calculateArea() 라는 추상 메소드를 가지고 있습니다. Circle 과 Rectangle 클래스는 Shape 추상 클래스를 상속받아 calculateArea() 추상 메소드를 반드시 오버라이딩 (구현) 해야 합니다. 추상 클래스는 공통 속성과 기능을 상위 클래스에서 정의하고, 구체적인 구현은 하위 클래스에 위임하는 추상화를 구현하는 데 유용합니다.
- 인터페이스 (Interface): interface 키워드를 사용하여 선언하는 특별한 형태의 추상 자료형입니다. 인터페이스는 추상 메소드 와 상수 (final static 변수) 만을 멤버로 가질 수 있습니다. 인터페이스는 구현부가 전혀 없는 순수 추상 클래스라고 할 수 있습니다. 클래스는 다중 인터페이스 구현이 가능하며 (다중 상속 효과), 인터페이스를 통해 클래스 간의 결합도를 낮추고, 유연하고 확장 가능한 설계를 구현할 수 있습니다.
// 인터페이스
interface Drawable {
void draw(); // 추상 메소드 (구현부 없음)
}
// 인터페이스 Drawable 구현하는 클래스
class Circle implements Drawable {
@Override
public void draw() { // 인터페이스의 추상 메소드 구현
System.out.println("원을 그립니다.");
}
}
// 인터페이스 Drawable 구현하는 클래스
class Rectangle implements Drawable {
@Override
public void draw() { // 인터페이스의 추상 메소드 구현
System.out.println("사각형을 그립니다.");
}
}
public class Main {
public static void main(String[] args) {
Circle circle = new Circle();
Rectangle rectangle = new Rectangle();
circle.draw(); // Circle 클래스에서 구현한 draw() 호출 (결과: "원을 그립니다.")
rectangle.draw(); // Rectangle 클래스에서 구현한 draw() 호출 (결과: "사각형을 그립니다.")
Drawable[] drawables = {circle, rectangle}; // 인터페이스 타입 배열에 구현 클래스 객체 저장 (다형성)
for (Drawable drawable : drawables) {
drawable.draw(); // 배열 요소 타입은 Drawable 인터페이스 이지만, 실제 객체 타입에 맞는 draw() 호출 (다형성)
}
}
}
위 예시에서 Drawable 인터페이스는 draw() 라는 추상 메소드를 정의하고 있습니다. Circle 과 Rectangle 클래스는 Drawable 인터페이스를 구현 (implements) 하여 draw() 메소드를 반드시 구현해야 합니다. 인터페이스는 구현 클래스에게 특정 기능 (메소드) 을 구현하도록 강제하고, 다형성을 활용하여 유연한 코드를 작성하는 데 유용합니다.
📐 객체 지향 설계 원칙 (SOLID) 소개: 견고한 설계를 위한 5가지 지침
SOLID 는 객체 지향 설계의 5가지 기본 원칙을 묶어서 부르는 용어입니다. SOLID 원칙을 따르면 코드의 유지보수성, 확장성, 재사용성을 높이고, 견고하고 안정적인 소프트웨어를 개발하는 데 도움이 됩니다.
- SRP (Single Responsibility Principle): 단일 책임 원칙
- 클래스는 단 하나의 책임만 가져야 합니다. 클래스 변경 이유는 단 하나여야 합니다.
- 장점: 클래스 응집도 (Cohesion) 를 높이고, 변경에 유연한 설계를 가능하게 합니다.
- OCP (Open/Closed Principle): 개방-폐쇄 원칙
- 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 합니다. 기존 코드를 수정하지 않고, 새로운 기능을 추가할 수 있도록 설계해야 합니다.
- 장점: 코드 변경으로 인한 영향 범위를 최소화하고, 유지보수성을 높입니다. 상속, 다형성, 추상화 등을 통해 구현할 수 있습니다.
- LSP (Liskov Substitution Principle): 리스코프 치환 원칙
- 하위 타입은 상위 타입을 대체할 수 있어야 합니다. 상위 타입 객체를 사용하는 곳에 하위 타입 객체를 대신 사용해도 프로그램이 정상적으로 동작해야 합니다.
- 장점: 다형성을 제대로 활용하고, 코드의 유연성과 확장성을 높입니다. 상속 관계를 설계할 때 LSP를 준수해야 합니다.
- ISP (Interface Segregation Principle): 인터페이스 분리 원칙
- 클라이언트는 자신이 사용하지 않는 메소드에 의존하도록 강요받아서는 안 됩니다. 인터페이스는 최대한 작게, 응집도 높게 분리해야 합니다.
- 장점: 불필요한 의존성을 줄이고, 인터페이스 변경 시 영향 범위를 최소화합니다. 여러 개의 작은 인터페이스를 만들어 클라이언트의 요구사항에 맞는 인터페이스를 선택적으로 구현하도록 합니다.
- DIP (Dependency Inversion Principle): 의존 역전 원칙
- 고수준 모듈은 저수준 모듈에 의존해서는 안 됩니다. 둘 다 추상화에 의존해야 합니다. 추상화는 details 에 의존해서는 안 됩니다. details 가 추상화에 의존해야 합니다. (말이 좀 어렵죠? 😅)
- 쉽게 말해, 구체적인 구현 클래스보다는 인터페이스 또는 추상 클래스에 의존해야 합니다.
- 장점: 모듈 간 결합도를 낮추고, 코드 재사용성 및 유연성을 높입니다. 의존성 주입 (Dependency Injection, DI) 등을 통해 DIP를 구현할 수 있습니다.
SOLID 원칙은 객체 지향 설계의 모범 사례이며, 반드시 지켜야 하는 엄격한 규칙은 아닙니다. 하지만 SOLID 원칙을 이해하고 적용하려고 노력하면, 더욱 품질 높은 소프트웨어를 개발할 수 있을 거예요.
✅ 결론:
이번 포스팅에서는 객체 지향 프로그래밍 (OOP) 의 핵심 개념과 객체 지향 설계 원칙 (SOLID) 에 대해 자세히 알아보았습니다. 캡슐화, 상속, 다형성, 추상화는 객체 지향 프로그래밍의 4 pillars 이자, 자바 프로그래밍의 핵심 패러다임입니다. SOLID 원칙은 견고한 객체 지향 설계를 위한 지침 역할을 합니다.
객체 지향 프로그래밍은 처음에는 어렵게 느껴질 수 있지만, 꾸준히 학습하고 실습하다 보면, 여러분도 객체 지향적인 사고방식을 자연스럽게 체득하고, 더욱 우아하고 효율적인 코드를 작성할 수 있게 될 것입니다. 객체 지향 프로그래밍은 자바뿐만 아니라, 현대 소프트웨어 개발에서 필수적인 역량입니다. 객체 지향 프로그래밍을 완벽하게 정복하여, 소프트웨어 개발 전문가로 발돋움해 보세요! 💪
'프로그래밍 > JAVA' 카테고리의 다른 글
[JAVA]자바 예외 처리와 입출력 (IO): 견고한 프로그램을 만드는 핵심!🛡️ (10) | 2025.02.28 |
---|---|
[JAVA]자바 컬렉션 프레임워크: 데이터 관리, 이제 효율적으로! 🧰 (8) | 2025.02.28 |
[JAVA]자바 프로그래밍의 튼튼한 뼈대 세우기: 기본 문법 완벽 마스터 🚀 (11) | 2025.02.27 |
[JAVA]자바의 세계로 첫 발을 내딛다! 자바 시작하기 🚀 (7) | 2025.02.27 |
[JAVA]자바 추상 클래스와 인터페이스 완벽 분석: 선택 가이드와 활용법 🚀 (8) | 2025.02.26 |