객체지향 프로그래밍이란?
객체지향 프로그래밍(Object-Oriented Programming, OOP)은 현실 세계를 프로그래밍으로 모델링하기 위해 객체(Object)와 클래스(Class)를 중심으로 설계된 프로그래밍 패러다임입니다. 여기서 객체(Object)와 클래스(Class)는 아래와 같이 정의할 수 있습니다.
- 객체(Object): 상태(속성)와 행동(메서드)을 가진 독립적인 단위
- 클래스(Class): 객체를 생성하기 위한 설계도 혹은 틀, 클래스 모양 그대로 생성되는 것이 객체
예를 들어, 하트 모양의 쿠키 틀이 있으면 이 쿠키 틀로 찍어낸 쿠키의 모양은 모두 같지만 쿠키의 반죽에 따라 맛은 초코맛, 민트맛, 바닐라맛 등 모두 다를 수 있습니다. 하지만 이 모든 쿠키들은 하트 쿠키입니다. 여기서 하트 모양의 쿠키 틀은 클래스(Class)를 나타내고, 하트 모양 쿠키는 객체(Object)로 볼 수 있습니다.
이러한 객체지향 프로그래밍은 절차지향 프로그래밍의 단점을 보완하고 소프트웨어야의 생산성 향상 및 실세계에 대한 쉬운 모델링을 위해 탄생하였습니다. 여기서 절차 지향과 객체 지향의 차이점은 다음과 같습니다.
절차 지향 vs 객체 지향
절차 지향
- 절차 지향 언어: C 언어
- 목적을 달성하기 위한 일의 흐름이 중점
- 설계한 흐름도에 따라 동작을 함수로 작성하여 동작들이 순차적으로 실행
- 단점: 복잡한 시스템에서는 함수 간 의존성으로 인해 유지보수가 어려움
객체 지향
- 객체 지향 언어: Java, Python
- 프로그램을 실제 세상에 가깝게 모델링하여 실제 세상의 물체를 객체로 표현
- 이렇게 표현한 객체들의 관계 및 상호 작용을 설계 후 각 객체를 클래스로 작성
- 유지보수성: 모듈화된 코드로 쉽게 수정 가능
- 확장성: 새로운 요구사항에 쉽게 대처 가능
- 코드 재사용: 반복적인 코드 최소화, 간결한 코드 표현 가능
객체지향 프로그래밍의 특성
1. 추상화(Abstraction)
- 객체의 공통적인 속성과 기능을 추출하여 정의
- Java에서 추상 클래스(abstract class)와 인터페이스(interface)로 추상화 구현
- 각각의 클래스에 공통적인 기능을 추출하여 그 기능에 대한 인터페이스 정의
- 따라서 인터페이스는 공통적인 핵심 역할만 규정하고 실제적으로 각 객체에서 구현
2. 캡슐화(Encapsulation)
- 객체를 캡슐로 싸서 외부로부터 내부를 보호하고 내부 구현을 볼 수 없게 하는 것
- Java에서 객체는 클래스(class)를 사용하여 캡슐화, 필드(멤버 변수)와 메소드(멤버 함수)로 구성
3. 상속(Inheritance)
- 실세계에서는 상위 개체의 속성이 하위 객체에 물려져서 하위 객체가 상위 객체의 모든 속성을 가지는 관계
- Java에서는 부모 클래스(슈퍼 클래스)의 특성과 동작을 자식 클래스(서브 클래스)에 물려받고 기능을 추가하여 확장함
- 코드를 재사용하고 계층적인 설계를 통해 확장성 제공
- 예시: extends, implements
4. 다형성(Polymorphism) (중요!)
- 실세계에서는 역할과 구현으로 구분
- 예를 들어, 뮤지컬에서 로미오 역할이 있으면 그 역할을 할 배우는 배우 A, 배우 B 등 다양한 사람이 이를 구현할 수 있다.
- Java에서 다형성은 다음과 같이 활용
- 역할: 인터페이스
- 구현: 인터페이스를 구현한 클래스, 역할을 수행하는 구현 객체
- 즉, 클라이언트를 변경하지 않고 서버의 구현 기능을 유연하게 변경할 수 있다.
// 역할 정의 (인터페이스) interface RomeoRole { void act(); // 연기 void sing(); // 노래 }// 배우 A (구현) class ActorA implements RomeoRole { @Override public void act() { System.out.println("Actor A is acting as Romeo."); } @Override public void sing() { System.out.println("Actor A is singing as Romeo."); } } // 배우 B (구현) class ActorB implements RomeoRole { @Override public void act() { System.out.println("Actor B is acting as Romeo."); } @Override public void sing() { System.out.println("Actor B is singing as Romeo."); } }// 실제 역할 수행 public class Musical { public static void main(String[] args) { RomeoRole romeo = new ActorA(); // ActorA로 역할 할당 romeo.act(); romeo.sing(); romeo = new ActorB(); // ActorB로 역할 전환 romeo.act(); romeo.sing(); } } - Java에서는 같은 이름의 메소드가 클래스나 객체에 따라 다르게 동작하도록 하는 것
- 메소드 오버라이딩(overriding): 슈퍼 클래스에 구현된 메소드를 서브 클래스에서 동일한 이름으로 자신의 특징에 맞게 다시 구현하는 것
- 메소드 오버로딩(overloading): 클래스 내에서 이름이 같지만 서로 다르게 동작하는 메소드를 여러 개 만드는 메소드
좋은 객체지향 설계 5가지 원칙
SOLID 원칙
1. 단일 책임 원칙(SRP)
- 하나의 클래스는 하나의 책임만 가져야 한다.
- 변경이 중요한 기준점 - 변경이 있을 때 파급 효과가 적어야 한다.
2. 개방-폐쇄 원칙(OCP)
- 기존 코드를 수정하지 않고 확장 가능해야 한다.
- 다형성을 활용 - 인터페이스를 구현한 새로운 클래스로 기능 추가
- 문제점: 다형성만으로 OCP를 지킬 수 없다. -> 스프링의 DI(Dependency Injection) 및 컨테이너 제공 기능으로 지원
3. 리스코프 치환 원칙(LSP)
- 하위 클래스는 상위 클래스를 대체할 수 있어야 한다.
- 다형성에서 하위 클래스는 인터페이스 규약을 모두 지켜야 한다.
4.인터페이스 분리 원칙(ISP)
- 특정 클라이언트를 위한 인터페이스는 여러 개로 분리한다.
- 여러 개로 분리할 경우 인터페이스가 더 명확해지고 대체 가능성이 높아진다.
5. 의존성 역전 원칙(DIP)
- 상위 모듈은 하위 모듈(구현 클래스)에 의존하지 않고, 추상화(인터페이스)에 의존해야 한다.
- 따라서 역할에 의존해야 유연하게 구현체 변경 가능
- 문제점: 인터페이스와 구현 클래스 동시에 의존하는 경우 발생 -> 스프링의 DI(Dependency Injection) 및 컨테이너 제공 기능으로 지원
[출처 및 참고자료]
서적: JAVA Programming, 생능출판
강의 : 스프링의 핵심 원리 - 기본편