김영한의 실전 자바 - 기본편
목차
객체 지향 프로그래밍(OOP)에서 다형성(Polymorphism) 은 유지보수성과 확장성을 높이는 핵심 개념이다.
다형성을 활용하면 역할과 구현을 분리하여 클라이언트 코드의 변경 없이도 새로운 기능을 추가할 수 있다.
이 원칙을 가장 잘 실천하는 대표적인 설계 원칙이 OCP (Open-Closed Principle, 개방-폐쇄 원칙) 이다.
1. 좋은 객체 지향 프로그래밍이란?
객체 지향 프로그래밍(OOP)은 프로그램을 객체 단위로 설계하고, 객체들 간의 관계를 통해 시스템을 구축하는 방법론이다.
주요한 특징은 다음과 같다.
객체 지향 프로그래밍
- 객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나
여러 개의 독립된 단위, 즉 "객체"들의 모임으로 파악하고자 하는 것이다.
각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있다. (협력) - 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다.
객체 지향 프로그래밍의 핵심 원칙
- 캡슐화(Encapsulation)
- 객체의 데이터를 외부에서 직접 접근하지 못하도록 하고, 메서드를 통해서만 접근할 수 있도록 제한한다.
- 불필요한 정보를 감추고 필요한 정보만 노출하여 데이터의 무결성을 유지할 수 있다.
- 상속(Inheritance)
- 기존 클래스(부모 클래스)의 속성과 동작을 새로운 클래스(자식 클래스)에서 재사용할 수 있도록 하는 개념이다.
- 코드 재사용성을 높이고, 공통 기능을 모아 관리할 수 있다.
- 다형성(Polymorphism)
- 같은 인터페이스나 부모 클래스를 상속받은 객체가 서로 다른 방식으로 동작할 수 있는 특성을 의미한다.
- 다형성을 활용하면 유연하고 확장 가능한 프로그램 설계가 가능하다.
- 추상화(Abstraction)
- 객체의 핵심적인 속성과 동작만 정의하고, 불필요한 부분은 감추는 기법이다.
- 인터페이스나 추상 클래스를 통해 객체의 역할만 정의하고, 구현은 하위 클래스에서 담당하도록 설계할 수 있다.
→ 특히 다형성을 잘 활용하면 객체 지향의 장점을 극대화할 수 있으며, 역할과 구현을 분리하여 OCP 원칙을 준수할 수 있다.
역할과 구현을 분리
- 자바 언어의 다형성을 활용
- 역할 = 인터페이스
- 구현 = 인터페이스를 구현한 클래스, 구현 객체
- 인터페이스를 사용하면 클라이언트는 역할(인터페이스)만 알면 된다.
- 구현 대상의 내부 구조를 몰라도 되고, 변경되어도 영향 X
- 구현 대상 자체를 변경해도 영향 X
- 객체 설계 시 역할과 구현을 명확히 분리
- 역할(인터페이스)를 먼저 부여하고, 그 역할을 수행하는 구현 객체 만들기 (구현보다는 역할이 먼저)
- 꼭 인터페이스가 아니라 일반적인 상속 관계에서도 가능
객체의 협력
- 클라이언트: 요청, 서버: 응답
(요청하는 것은 모두 클라이언트, 요청에 응답하는 것은 모두 서버라고 함) - 수 많은 객체 클라이언트와 객체 서버는 서로 협력 관계를 가진다.
다형성
- 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있다.
- 다형성의 본질을 이해하려면 협력이라는 객체 사이의 관계에서 시작해야 한다.
- 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다.
- 디자인 패턴의 대부분은 다형성을 활용하는 것이다.
- 스프링의 핵심인 제어의 역전(IoC), 의존관계 주입(DI)도 다형성을 활용하는 것이다.
2. 다형성 - 역할과 구현
역할와 구현을 분리 - 정리
- 실세계의 역할과 구현이라는 편리한 컨셉을 다형성을 통해 객체 세상으로 가져올 수 있음
- 유연하고, 변경이 용이
- 확장 가능한 설계 → 수많은 종류의 새로운 자동사를 만들 수 있음
- 클라이언트에 영향을 주지 않는 변경 가능
- 역할(인터페이스) 자체가 변하면, 클라이언트, 서버 모두 큰 변경이 발생 (한계)
- 인터페이스를 안정적으로 잘 설계하는 것이 중요
역할과 구현을 분리하지 않은 경우 (문제점)
public class Driver {
private K3Car k3Car;
public void setK3Car(K3Car k3Car) {
this.k3Car = k3Car;
}
public void drive() {
System.out.println("자동차를 운전합니다.");
k3Car.startEngine();
k3Car.pressAccelerator();
k3Car.offEngine();
}
}
- Driver 클래스가 K3Car에 의존하고 있다. (클래스 의존 관계)
- 새로운 자동차(Model3Car)가 추가되면 Driver 코드도 수정해야 한다.
- 즉, 확장하기 어려운 구조(OCP 위반) 이다.
역할과 구현을 분리한 경우 (좋은 설계)
public interface Car {
void startEngine();
void offEngine();
void pressAccelerator();
}
public class K3Car implements Car {
@Override
public void startEngine() { System.out.println("K3Car.startEngine"); }
@Override
public void offEngine() { System.out.println("K3Car.offEngine"); }
@Override
public void pressAccelerator() { System.out.println("K3Car.pressAccelerator"); }
}
public class Model3Car implements Car {
@Override
public void startEngine() { System.out.println("Model3Car.startEngine"); }
@Override
public void offEngine() { System.out.println("Model3Car.offEngine"); }
@Override
public void pressAccelerator() { System.out.println("Model3Car.pressAccelerator"); }
}
public class Driver {
private Car car;
public void setCar(Car car) {
System.out.println("자동차를 설정합니다: " + car);
this.car = car;
}
public void drive() {
System.out.println("자동차를 운전합니다.");
car.startEngine();
car.pressAccelerator();
car.offEngine();
}
}
package poly.car1;
/**
* 다형성을 활용한 런타임 변경
* 런타임: 애플리케이션 실행 도중에 변경 가능
*/
public class CarMain1 {
public static void main(String[] args) {
Driver driver = new Driver();
// 차량 선택(k3)
K3Car k3Car = new K3Car();
driver.setCar(k3Car);
driver.drive();
// 차량 변경 (k3 -> model3)
Model3Car model3Car = new Model3Car();
driver.setCar(model3Car);
driver.drive();
}
}
자동차를 설정합니다: poly.car1.K3Car@214c265e
자동차를 운전합니다
K3Car.startEngine
K3Car.pressAccelerator
K3Car.offEngine
자동차를 설정합니다: poly.car1.Model3Car@7cca494b
자동차를 운전합니다
Model3Car.startEngine
Model3Car.pressAccelerator
Model3Car.offEngine
- `Driver` : 운전자는 자동차( `Car` )의 역할에만 의존한다. 구현인 K3, Model3 자동차에 의존하지 않는다.
- `Driver` 클래스는 `Car car` 멤버 변수를 가진다. 따라서 `Car` 인터페이스를 참조한다.
- 인터페이스를 구현한 `K3Car` , `Model3Car` 에 의존하지 않고, `Car` 인터페이스에만 의존한다.
- 여기서 설명하는 의존은 클래스 의존 관계를 뜻한다. 클래스 상에서 어떤 클래스를 알고 있는가를 뜻한다.
- `Driver` 클래스 코드를 보면 `Car` 인터페이스만 사용하는 것을 확인할 수 있다.
- 새로운 자동차가 추가되더라도 `Driver` 클래스의 코드는 변경할 필요가 없다.
- `Car` : 자동차의 역할이고 인터페이스이다. `K3Car` , `Model3Car` 클래스가 인터페이스를 구현한다.
3. OCP (Open-Closed Principle) 원칙
OCP(Open-Closed Principle, 개방-폐쇄 원칙)는 "확장에는 열려 있고, 수정에는 닫혀 있어야 한다."는 객체 지향 설계 원칙 중 하나이다.
- Open for extension: 새로운 기능의 추가나 변경 사항이 생겼을 때, 기존 코드는 확장할 수 있어야 한다.
- Closed for modification: 기존의 코드는 수정되지 않아야 한다.
즉, 기존의 코드 수정 없이 새로운 기능을 추가할 수 있다는 의미이다.
OCP를 준수하는 코드 설계
1. 인터페이스 또는 추상 클래스를 활용해 역할과 구현을 분리한다.
2. 클라이언트(사용 객체)는 구체적인 구현이 아니라 인터페이스(역할)에 의존해야 한다.
3. 새로운 기능 추가 시 기존 코드를 변경하는 것이 아니라 새로운 클래스를 추가해야 한다.
전략 패턴 (Strategy Pattern)
전략 패턴은 OCP 원칙을 준수하면서, 런타임에서 알고리즘을 변경할 수 있도록 하는 대표적인 디자인 패턴이다.
- 알고리즘(전략)을 독립적인 클래스로 분리하여 런타임에 동적으로 변경 가능하도록 한다.
- 클라이언트 코드(사용하는 객체)는 전략(알고리즘) 자체를 변경하지 않고도 다양한 기능을 수행할 수 있다.
전략 패턴의 구조
1. Strategy 인터페이스: 다양한 알고리즘을 정의하는 역할.
2. ConcreteStrategy (구현 클래스): 실제 알고리즘을 구현하는 클래스.
3. Context (클라이언트 코드): Strategy 인터페이스에 의존하며, 실제 전략을 변경할 수 있음.
결제 시스템 예제
1. `Pay` - Strategy 인터페이스 정의
public interface Pay {
boolean pay(int amount);
}
2. 각 결제 방식이 `Pay` 를 구현
public class KakaoPay implements Pay {
@Override
public boolean pay(int amount) {
System.out.println("카카오페이 시스템과 연결합니다.");
System.out.println(amount + "원 결제를 시도합니다.");
return true;
}
}
public class NaverPay implements Pay {
@Override
public boolean pay(int amount) {
System.out.println("네이버페이 시스템과 연결합니다.");
System.out.println(amount + "원 결제를 시도합니다.");
return true;
}
}
public class NewPay implements Pay {
@Override
public boolean pay(int amount) {
System.out.println("NewPay 시스템과 연결합니다.");
System.out.println(amount + "원 결제를 시도합니다.");
return true;
}
}
public class DefaultPay implements Pay {
@Override
public boolean pay(int amount) {
System.out.println("결제 수단이 없습니다.");
return false;
}
}
3. PayStore - `Map` 을 활용한 결제 수단 저장소
// 결제 수단을 보관하고 관리
public abstract class PayStore {
private final static Map<String, Pay> payMethods = new HashMap<>();
// 결제 수단 등록 (새로운 결제 수단 생길 시 수정됨)
static {
payMethods.put("kakao", new KakaoPay());
payMethods.put("naver", new NaverPay());
payMethods.put("new", new NewPay());
}
// 결제 수단 찾기
public static Pay findPay(String option) {
return payMethods.getOrDefault(option, new DefaultPay());
}
}
새로운 결제 수단이 생기면 static 에 추가하면 된다.
4. `PayService` - OCP 를 준수하는 결제 서비스
public class PayService {
public void processPay(String option, int amount) {
boolean result = false;
System.out.println("결제를 시작합니다: option=" + option + ", amount=" + amount);
Pay pay = PayStore.findPay(option); // 결제 수단 찾기
result = pay.pay(amount); // 결제 수행
if (result) {
System.out.println("결제가 성공했습니다.");
} else {
System.out.println("결제가 실패했습니다.");
}
}
}
결제 요청을 처리하는 역할을 하는 서비스 클래스이다.
새로운 결제 수단이 추가되더라도 PayService 코드에는 변경 사항이 발생하지 않는다.
5. 실행 코드
public class PayMain {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
PayService payService = new PayService();
while(true) {
System.out.print("결제 수단을 입력하세요:");
String payOption = scanner.nextLine();
if(payOption.equals("exit")) {
System.out.println("프로그램을 종료합니다.");
return;
}
System.out.print("결제 금액을 입력하세요:");
int amount = scanner.nextInt();
scanner.nextLine();
payService.processPay(payOption, amount);
}
}
}
실행 결과
결제 수단을 입력하세요:kakao
결제 금액을 입력하세요:10000
결제를 시작합니다: option=kakao, amount=10000
카카오페이 시스템과 연결합니다.
10000원 결제를 시도합니다.
결제가 성공했습니다.
결제 수단을 입력하세요:naver
결제 금액을 입력하세요:5000
결제를 시작합니다: option=naver, amount=5000
네이버페이 시스템과 연결합니다.
5000원 결제를 시도합니다.
결제가 성공했습니다.
결제 수단을 입력하세요:unknown
결제 금액을 입력하세요:2000
결제를 시작합니다: option=unknown, amount=2000
결제 수단이 없습니다.
결제가 실패했습니다.
결제 수단을 입력하세요:exit
프로그램을 종료합니다.
4. 정리
- 객체 지향 프로그래밍(OOP) 은 프로그램을 독립적인 객체들의 협력으로 구성되며, 다형성을 활용하여 유연하고 확장 가능한 설계를 가능하게 한다.
- 역할과 구현을 분리하면 클라이언트 코드(사용하는 객체)가 특정 구현체(구현 방식)에 의존하지 않으므로, 새로운 기능 추가 시 기존 코드를 변경할 필요가 없다.
- OCP (Open-Closed Principle, 개방-폐쇄 원칙) 을 준수하려면, 인터페이스(역할)와 구현을 분리하고, 새로운 기능 추가 시 기존 코드를 수정하는 것이 아니라 새로운 클래스를 추가하는 방식으로 설계해야 한다.
- 전략 패턴(Strategy Pattern) 을 활용하면, 실행 중(런타임)에 전략(알고리즘 또는 기능)을 변경할 수 있으며, 클라이언트 코드의 수정 없이 확장 가능한 구조를 만들 수 있다.
'Course > Java' 카테고리의 다른 글
[java-mid1] 2. 불변 객체 (0) | 2025.03.04 |
---|---|
[java-mid1] 1. Object 클래스 (0) | 2025.02.25 |
[java-basic] 11. 다형성2 (0) | 2025.01.30 |
[java-basic] 10. 다형성1 (0) | 2025.01.20 |
[java-basic] 9. 상속 (0) | 2024.12.27 |
댓글