객체지향 프로그래밍의 4가지 특징
초반에 개발을 배우기 시작했을 땐 객체지향 프로그래밍은 곧 SOLID 원칙을 따르는 프로그래밍인줄로만 알고 있었는데 Java 공부를 하면서 SOLID 이전에 기본적으로 4가지의 특징이 따로 있다는 걸 알게 됐다.
캡슐화 (Encapsulation)
캡슐화는 객체의 속성과 메소드를 하나로 묶고 외부에서의 직접적인 접근을 제한하는 것이다. 따라서 데이터를 보호하고 객체의 상태를 관리하는 방법을 제공한다. 이를 통해 데이터의 무결성을 유지하고 원치 않는 외부 영향을 막을 수 있다.
자바에서는 아래와 같은 접근 제어자(access modifier)를 사용해 캡슐화를 구현할 수 있다:
- private: 클래스 내부에서만 접근 가능
- public: 모든 클래스에서 접근 가능
- protected: 같은 패키지나 상속받은 클래스에서 접근 가능
class Person {
private String name; // 외부에서 접근 불가
// 외부가 객체의 name에 접근할 수 있는 메소드를 따로 제공하면 name값을 읽을 수 있다
public String getName() {
return name;
}
// 외부에서 객체의 name값을 직접 바꿀 수 있는 메소드를 제공하면 값 변경도 가능하다
public void setName(String name) {
this.name = name;
}
}
위에서 private 값인 name은 외부에서 직접 접근할 수 없으니 person.name 값을 확인하거나 바꿀 수 없다. 그 대신 getName()과 setName() 메소드를 따로 만들어 외부에서도 name을 간접적으로 조작할 수 있도록 한다. 이처럼 캡슐화를 통해 클래스의 내부 데이터를 보호하고 필요한 경우에만 공개된 메소드를 통해 조작할 수 있도록 한다.
추상화 (Abstraction)
추상화는 복잡한 시스템에서 불필요한 세부 사항을 숨기고 핵심적인 부분만 드러내는 것을 의미한다. 객체의 중요한 특징만을 노출하고 세부 구현은 명시하지 않는 것이다. 이를 통해 코드의 복잡도를 낮추고, 더 직관적이고 명확한 코드 구조를 만들 수 있게 된다.
자바에서는 추상 클래스(abstract class)나 인터페이스(interface)를 이용해 추상화를 구현할 수 있다. 추상 클래스는 일부 구현을 포함할 수 있고, 인터페이스는 모든 메소드가 구현되지 않은 상태로 선언된다.
// 추상 클래스
abstract class Animal {
abstract void sound(); // 추상 메소드
}
// 구체적 클래스
class Dog extends Animal {
@Override
void sound() {
System.out.println("멍멍");
}
}
위에서 Animal 클래스는 동물의 공통적인 특성만 정의하고, sound() 메소드는 구체적으로 어떤 소리를 낼지 구현하지 않는다. 이처럼 추상화를 통해 특정 객체의 기능만 노출하고, 세부적인 구현은 하위 클래스에서 처리한다.
상속 (Inheritance)
상속은 자식 클래스가 부모 클래스의 속성과 메소드를 물려받는 기능이다. 코드를 재사용하면서 중복코드를 줄여 유지보수성을 높일 수 있다. 상속을 통해 자식 클래스는 부모 클래스의 기본 기능을 그대로 사용하면서 필요한 부분만 확장하거나 변경할 수 있다.
// 부모
class Animal {
void eat() {
System.out.println("챱챱");
}
}
// 자식
class Cat extends Animal {
void meow() {
System.out.println("냥냥");
}
}
// 사용 가능:
Cat cat = new Cat();
cat.eat();
cat.meow();
위 코드에서 Cat 클래스는 Animal 클래스를 상속받아 eat() 메소드를 사용할 수 있으면서, 추가로 meow() 메소드를 정의해 Cat만의 고유기능도 쓸 수 있다. 상속을 활용하면 새 클래스를 만들할 때 기존 클래스의 기능을 재사용하고 확장할 수 있다.
다형성 (Polymorphism)
다형성은 하나의 객체가 여러가지 형태를 가질 수 있는 것을 의미한다. 같은 인터페이스나 부모 클래스를 공유하는 객체들이 서로 다른 방식으로 동작할 수 있는 것을 말한다. 다형성은 메소드 오버라이딩과 오버로딩을 통해 구현된다.
- 오버라이딩(Overriding): 부모 클래스의 메소드를 자식 클래스에서 재정의하여 사용하는 방식
- 오버로딩(Overloading): 같은 이름의 메소드를 여러 개 정의하고, 매개변수의 타입이나 개수에 따라 다르게 동작하는 방식
// 부모 클래스
class Animal {
void sound() {
System.out.println("동물이 내는 소리");
}
}
// 자식 클래스
class Dog extends Animal {
@Override
void sound() {
System.out.println("멍멍");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("냥냥");
}
}
위 코드에서 Dog와 Cat은 각각 Animal을 상속받고, sound() 메소드를 오버라이딩해서 서로 다른 방식으로 구현했다. Animal 타입으로 선언된 객체는 런타임에 따라 각각 다른 형태로 동작할 수 있는데, 이걸 런타임 다형성이라 한다. 다형성을 통해 동일한 메소드 호출이 상황에 따라 다르게 동작할 수 있다.