🧩 객체 지향 프로그래밍(OOP)이란?
객체 지향 프로그래밍은 프로그램을 명령어의 나열로 바라보는 전통적인 방식에서 벗어나, 여러 개의 독립된 단위인 "객체"들의 협력 관계로 프로그램을 구성하는 방식입니다.
각 객체는 메시지를 주고받으며 데이터를 처리하고, 상호작용을 통해 전체 프로그램이 동작합니다.
이러한 구조는 프로그램을 유연하고 변경에 강한 구조로 만들어, 대규모 시스템 개발에 특히 적합합니다.
🔄 유연하고 변경이 용이하다?
- 레고 블록을 조립하듯이: 객체를 자유롭게 추가하거나 교체할 수 있습니다.
- 키보드나 마우스를 바꾸듯이: 특정 기능을 담당하는 객체를 손쉽게 대체할 수 있습니다.
- 컴퓨터 부품을 교체하듯이: 시스템의 일부가 바뀌어도 전체에 큰 영향을 주지 않습니다.
즉, 객체 간 결합도를 낮춰 변경에 유연한 구조를 만드는 것이 핵심입니다.
🧠 다형성(Polymorphism)
다형성은 하나의 인터페이스를 통해 서로 다른 구현을 동적으로 사용할 수 있는 객체 지향 프로그래밍의 핵심 개념입니다. 동일한 메시지를 보냈을 때, 객체에 따라 서로 다른 동작을 하도록 만드는 기능이라고도 할 수 있습니다.
🎭 예시 1: 운전자와 자동차
- 운전자는 자동차가 K3든 테슬라든 운전 방식은 동일합니다.
- 자동차가 바뀌어도 "운전"이라는 인터페이스는 동일하게 유지됩니다.
🎭 예시 2: 뮤지컬
- 같은 역할을 여러 배우가 연기할 수 있습니다.
- 대본(역할)은 같지만, 배우(구현)는 다양할 수 있습니다.
🧩 역할과 구현의 분리
역할(인터페이스)과 구현(구체 클래스)을 분리하면 시스템 구조가 더 단순해지고, 변경과 확장에 훨씬 유연해집니다.
✅ 역할과 구현을 분리할 때의 장점
- 클라이언트는 객체가 어떤 역할(인터페이스)을 하는지만 알면 됩니다.
- 내부 구조나 구현 방식은 몰라도 사용할 수 있습니다.
- 구현체가 바뀌어도 클라이언트는 영향을 받지 않습니다.
- 새로운 구현체로 교체해도 시스템 전체에 미치는 영향이 최소화됩니다.
☕ 자바 언어에서의 적용
- 역할: interface
- 구현: interface를 구현한 class
객체를 설계할 때는 역할을 먼저 정의하고, 그 역할을 수행할 수 있는 다양한 구현 객체를 만들 수 있도록 구조를 설계하는 것이 객체 지향적인 접근입니다.
🔁 자바 언어에서의 다형성
자바에서 다형성은 주로 오버라이딩(Overriding)을 통해 실현됩니다.
- 부모 타입으로 선언된 변수에 자식 객체를 할당하고,
- 메서드를 호출하면 자식 클래스에서 오버라이딩한 메서드가 실행됩니다.
- 실행 시점에 실제 객체의 타입에 따라 호출되는 메서드가 결정됩니다.
이러한 메커니즘을 런타임 다형성(Run-time Polymorphism)이라고 합니다.
📌 예시 코드
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("야옹~");
}
}
public class PolymorphismExample {
public static void main(String[] args) {
Animal animal1 = new Dog(); // 실제 객체는 Dog
Animal animal2 = new Cat(); // 실제 객체는 Cat
animal1.sound(); // 멍멍!
animal2.sound(); // 야옹~
}
}
💡 설명
- animal1과 animal2는 모두 Animal 타입으로 선언되었지만,
- 각각 실제로는 Dog, Cat 객체입니다.
- sound() 메서드 호출 시점에, JVM은 실제 객체의 타입을 확인하여
- Dog, Cat 클래스의 sound() 메서드를 실행합니다.
👉 컴파일 시점에는 부모 타입의 메서드만 확인하지만,
👉 실행 시점에는 실제 객체 타입에 따라 오버라이딩된 메서드가 호출됩니다.
이처럼 런타임에 결정되는 메서드 호출이 바로 자바 다형성의 핵심입니다.
🛠️ 스프링 부트에서의 다형성 적용 예시
// MemberRepository.java
// 역할 (인터페이스)
public interface MemberRepository {
Optional<Member> findByEmail(String email);
}
// MemoryMemberRepository.java
// 구현체 1: 메모리 저장소
@Repository
public class MemoryMemberRepository implements MemberRepository {
private Map<String, Member> store = new HashMap<>();
@Override
public Optional<Member> findByEmail(String email) {
return Optional.ofNullable(store.get(email));
}
}
// JpaMemberRepository.java
// 구현체 2: JPA 저장소
@Repository
public class JpaMemberRepository implements MemberRepository {
@PersistenceContext
private EntityManager em;
@Override
public Optional<Member> findByEmail(String email) {
return em.createQuery("SELECT m FROM Member m WHERE m.email = :email", Member.class)
.setParameter("email", email)
.getResultList()
.stream()
.findFirst();
}
}
// MemberService.java
// Service는 오직 역할(인터페이스)만 알면 됨
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
public Member findMemberByEmail(String email) {
return memberRepository.findByEmail(email)
.orElseThrow(() -> new EntityNotFoundException("Member not found"));
}
}
☝️ 어떤 구현체가 주입되느냐에 따라 동작이 달라짐
- MemoryMemberRepository를 주입하면 메모리 기반 저장소가 사용되고,
- JpaMemberRepository를 주입하면 데이터베이스 기반 저장소가 사용됩니다.
- 하지만 MemberService의 코드는 전혀 변경되지 않습니다.
이것이 바로 런타임 다형성이며, 스프링이 DI(의존성 주입)를 통해 인터페이스로 구현체를 바꿔 끼울 수 있도록 지원하기 때문에 가능한 구조입니다.
✅ 마무리
좋은 객체 지향 프로그래밍이란 변경에 유연하고 확장 가능한 구조를 설계하는 것입니다. 이를 위해 우리는 역할과 구현을 분리하고, 다형성을 적극적으로 활용해야 합니다. 그리고 자바에서의 오버라이딩은 이러한 다형성을 실현하는 핵심 도구로, 객체 지향 설계의 유연함을 실제 코드에서 구현 가능하게 만들어줍니다.
'Back end > Java' 카테고리의 다른 글
| [Java] Java란 무엇인가? (0) | 2025.05.20 |
|---|---|
| [Java] 객체 지향 설계의 핵심! SOLID 원칙 (0) | 2025.05.14 |
| [Java] List<Map<String, String>> 형식을 String[] 형식으로 변경하는 방법 (0) | 2024.08.02 |
| [Java] JUnit과 AssertJ를 활용한 효과적인 단위 테스트 작성 방법 (0) | 2024.06.05 |
| [Java] 자바 레코드(Record)란? (예제 포함) (0) | 2024.06.04 |