"클래스가 아니라 타입에 집중하라. 중요한 것은 행동이다."
이 장에서는 타입 계층을 구현하는 다양한 방법을 다룹니다. 클래스, 인터페이스, 추상 클래스, 덕 타이핑, 믹스인 등 여러 구현 기법의 장단점과 적용 시나리오를 학습합니다.
- 타입과 클래스의 차이 이해하기
- 다양한 타입 계층 구현 방법 학습하기
- 각 방법의 장단점과 트레이드오프 파악하기
- 인터페이스와 추상 클래스의 조합 이해하기
- 덕 타이핑의 개념과 특성 학습하기
- 믹스인을 통한 코드 재사용 이해하기
┌─────────────────────────────────────────────────────┐
│ │
│ 타입 (Type): │
│ 객체의 퍼블릭 인터페이스 │
│ → 객체가 외부에 제공하는 행동의 집합 │
│ │
│ 클래스 (Class): │
│ 객체의 구현 │
│ → 내부 상태와 메서드 구현을 정의 │
│ │
└─────────────────────────────────────────────────────┘
비교:
| 측면 | 타입 | 클래스 |
|---|---|---|
| 관심사 | 행동 (What) | 구현 (How) |
| 범위 | 퍼블릭 인터페이스 | 내부 상태 + 메서드 |
| 추상화 | 높음 | 낮음 |
| 변경 | 어려움 | 상대적으로 쉬움 |
타입 계층이란:
동일한 메시지에 대한
행동 호환성을 전제로 한 구조
핵심:
- 행동의 일관성
- 치환 가능성
- 다형성
중요한 사실:
┌─────────────────────────────────────────────────────┐
│ │
│ 타입 계층을 구현한다고 해서 │
│ 서브타이핑 관계가 자동으로 보장되는 것은 아니다 │
│ │
│ → 리스코프 치환 원칙(LSP)을 준수해야 함! │
│ │
└─────────────────────────────────────────────────────┘
타입 계층 구현 방법:
1. 클래스 상속
2. 인터페이스 구현
3. 추상 클래스 상속
4. 인터페이스 + 추상 클래스
5. 덕 타이핑
6. 믹스인
→ 각 방법의 장단점과 트레이드오프를 이해해야 함!
클래스는:
- 객체의 타입을 정의
- 동시에 구현도 정의
→ 타입과 구현이 강하게 결합!
예시:
// 클래스로 타입 정의
public class SalariedEmployee {
private String name;
private Money basePay;
public SalariedEmployee(String name, Money basePay) {
this.name = name;
this.basePay = basePay;
}
public Money calculatePay(double taxRate) {
return basePay.minus(basePay.times(taxRate));
}
}
public class HourlyEmployee {
private String name;
private Money basePay;
private int timeCard;
public HourlyEmployee(String name, Money basePay, int timeCard) {
this.name = name;
this.basePay = basePay;
this.timeCard = timeCard;
}
public Money calculatePay(double taxRate) {
return basePay.times(timeCard)
.minus(basePay.times(timeCard).times(taxRate));
}
}문제점:
1. 타입과 구현의 강한 결합
→ 구현 변경 시 타입도 영향받음
2. 다중 상속 불가
→ 하나의 객체가 여러 타입을 가질 수 없음
3. 코드 중복
→ 유사한 구현을 공유하기 어려움
4. 유연성 부족
→ 다양한 구현 방법 제약
// ❌ 나쁜 방법: 구체 클래스 상속
public class Manager extends SalariedEmployee {
private Money bonus;
public Manager(String name, Money basePay, Money bonus) {
super(name, basePay);
this.bonus = bonus;
}
@Override
public Money calculatePay(double taxRate) {
// 부모 구현에 강하게 결합됨!
return super.calculatePay(taxRate).plus(bonus);
}
}문제:
구체 클래스 상속:
→ 부모 클래스의 구현에 강하게 결합
→ 부모 변경 시 자식에 파급 효과
→ 캡슐화 위반
→ 유지보수 어려움
인터페이스:
- 타입만 정의 (구현 X)
- 퍼블릭 인터페이스만 명시
- 구현은 클래스에 위임
→ 타입과 구현 완전 분리!
예시:
// 타입 정의
public interface Employee {
Money calculatePay(double taxRate);
}
// 구현 1
public class SalariedEmployee implements Employee {
private String name;
private Money basePay;
public SalariedEmployee(String name, Money basePay) {
this.name = name;
this.basePay = basePay;
}
@Override
public Money calculatePay(double taxRate) {
return basePay.minus(basePay.times(taxRate));
}
}
// 구현 2
public class HourlyEmployee implements Employee {
private String name;
private Money basePay;
private int timeCard;
public HourlyEmployee(String name, Money basePay, int timeCard) {
this.name = name;
this.basePay = basePay;
this.timeCard = timeCard;
}
@Override
public Money calculatePay(double taxRate) {
return basePay.times(timeCard)
.minus(basePay.times(timeCard).times(taxRate));
}
}사용:
public class TaxOffice {
public Money calculate(Employee employee, double taxRate) {
// 인터페이스에만 의존!
return employee.calculatePay(taxRate);
}
}
// 다형성
TaxOffice office = new TaxOffice();
office.calculate(new SalariedEmployee("Alice", Money.wons(3000000)), 0.1);
office.calculate(new HourlyEmployee("Bob", Money.wons(15000), 160), 0.1);1. 타입과 구현 분리
✅ 구현 변경이 타입에 영향 없음
2. 다중 타입 구현 가능
✅ 하나의 클래스가 여러 인터페이스 구현
3. 낮은 결합도
✅ 클라이언트가 인터페이스에만 의존
4. 높은 유연성
✅ 새로운 구현 추가 용이
다중 인터페이스 구현 예시:
// 게임 객체 타입 계층
public interface GameObject {
String getName();
}
public interface Displayable extends GameObject {
Point getPosition();
void update(Graphics graphics);
}
public interface Collidable extends Displayable {
boolean collideWith(Collidable other);
}
public interface Effect extends GameObject {
void activate();
}
// 하나의 클래스가 여러 타입 구현
public class Explosion implements Displayable, Effect {
@Override
public String getName() { return "Explosion"; }
@Override
public Point getPosition() { return new Point(0, 0); }
@Override
public void update(Graphics graphics) { /* ... */ }
@Override
public void activate() { /* ... */ }
}
// Player는 Collidable
public class Player implements Collidable {
@Override
public String getName() { return "Player"; }
@Override
public Point getPosition() { return new Point(0, 0); }
@Override
public void update(Graphics graphics) { /* ... */ }
@Override
public boolean collideWith(Collidable other) { return false; }
}타입 계층 시각화:
GameObject
↑
│
┌───────┴───────┐
│ │
Displayable Effect
↑ ↑
│ │
Collidable (Explosion은 둘 다 구현)
↑
│
Player, Monster
1. 여러 클래스가 동일한 타입 구현 가능
Employee ← SalariedEmployee
← HourlyEmployee
2. 하나의 클래스가 여러 타입 구현 가능
Explosion → Displayable
→ Effect
❌ 코드 중복 문제
인터페이스는 구현을 제공하지 않으므로
각 클래스가 동일한 로직을 중복 구현해야 함
→ Java 8 이전의 한계
추상 클래스:
- 타입 정의 (추상 메서드)
- 일부 구현 제공 (구체 메서드)
- 인스턴스 생성 불가
→ 구현 공유하면서도 의존성 역전!
예시:
// 추상 클래스로 타입 정의
public abstract class DiscountPolicy {
private List<DiscountCondition> conditions;
public DiscountPolicy(DiscountCondition... conditions) {
this.conditions = Arrays.asList(conditions);
}
// 구체 메서드 (구현 공유)
public Money calculateDiscountAmount(Screening screening) {
for (DiscountCondition each : conditions) {
if (each.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
// 추상 메서드 (타입 정의)
abstract protected Money getDiscountAmount(Screening screening);
}
// 구현 1
public class AmountDiscountPolicy extends DiscountPolicy {
private Money discountAmount;
public AmountDiscountPolicy(Money discountAmount,
DiscountCondition... conditions) {
super(conditions);
this.discountAmount = discountAmount;
}
@Override
protected Money getDiscountAmount(Screening screening) {
return discountAmount;
}
}
// 구현 2
public class PercentDiscountPolicy extends DiscountPolicy {
private double percent;
public PercentDiscountPolicy(double percent,
DiscountCondition... conditions) {
super(conditions);
this.percent = percent;
}
@Override
protected Money getDiscountAmount(Screening screening) {
return screening.getMovieFee().times(percent);
}
}1. 의존성 역전 원칙 (DIP) 준수
┌──────────────────────────┐
│ DiscountPolicy │ ← 추상 클래스
│ (추상) │
│ │
│ calculateDiscountAmount│ ← 구체 메서드
│ getDiscountAmount │ ← 추상 메서드
└────────┬─────────────────┘
│ depends on
▼
(추상 메서드)
▲
│ implements
│
┌────────┴─────────────────┐
│ AmountDiscountPolicy │
│ (구체) │
└──────────────────────────┘
부모와 자식 모두 추상 메서드에 의존!
→ 의존성 역전
2. 상속을 염두에 둔 설계
구체 클래스 상속:
❌ 우연한 상속
❌ 구현 세부사항 노출
❌ 취약한 기반 클래스 문제
추상 클래스 상속:
✅ 의도된 상속
✅ 추상화를 통한 확장 포인트 제공
✅ 안전한 확장
3. 인스턴스 생성 불가
// ❌ 컴파일 에러
DiscountPolicy policy = new DiscountPolicy() { ... };
// ✅ 서브클래스를 통해서만 인스턴스화
DiscountPolicy policy = new AmountDiscountPolicy(...);| 측면 | 구체 클래스 상속 | 추상 클래스 상속 |
|---|---|---|
| 의존 대상 | 구체적 구현 | 추상 메서드 |
| 결합도 | 높음 | 낮음 |
| 확장성 | 낮음 | 높음 |
| 목적 | 코드 재사용 | 타입 계층 정의 |
| DIP | 위반 | 준수 |
┌─────────────────────────────────────────────────────┐
│ │
│ 인터페이스: 타입 정의 │
│ 추상 클래스: 공통 구현 제공 │
│ │
│ → 골격 구현 추상 클래스 │
│ (Skeletal Implementation Abstract Class) │
│ │
└─────────────────────────────────────────────────────┘
패턴:
1. 인터페이스로 타입 정의
2. 추상 클래스로 기본 구현 제공
3. 구체 클래스로 특화된 구현
→ 다중 타입 + 코드 재사용!
Java 8 이전 방식:
// 1. 인터페이스로 타입 정의
public interface DiscountPolicy {
Money calculateDiscountAmount(Screening screening);
}
// 2. 골격 구현 추상 클래스
public abstract class DefaultDiscountPolicy implements DiscountPolicy {
private List<DiscountCondition> conditions;
public DefaultDiscountPolicy(DiscountCondition... conditions) {
this.conditions = Arrays.asList(conditions);
}
// 공통 구현
@Override
public Money calculateDiscountAmount(Screening screening) {
for (DiscountCondition each : conditions) {
if (each.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
// 확장 포인트
abstract protected Money getDiscountAmount(Screening screening);
}
// 3. 구체 클래스
public class AmountDiscountPolicy extends DefaultDiscountPolicy {
private Money discountAmount;
public AmountDiscountPolicy(Money discountAmount,
DiscountCondition... conditions) {
super(conditions);
this.discountAmount = discountAmount;
}
@Override
protected Money getDiscountAmount(Screening screening) {
return discountAmount;
}
}
// 4. 다른 계층에서도 인터페이스 구현 가능
public class CustomDiscountPolicy implements DiscountPolicy {
// 골격 구현 사용 안 함
// 완전히 다른 방식으로 구현
@Override
public Money calculateDiscountAmount(Screening screening) {
// 자체 구현
return Money.ZERO;
}
}구조:
<<interface>>
DiscountPolicy
↑
│ implements
├──────────────────────┐
│ │
DefaultDiscountPolicy CustomDiscountPolicy
(추상 클래스) (독립 구현)
↑
│ extends
├──────────────┐
│ │
AmountDiscount PercentDiscount
Policy Policy
1. 다양한 구현 방법 지원
상황 A: 기본 구현 활용
→ DefaultDiscountPolicy 상속
상황 B: 완전히 다른 방식
→ DiscountPolicy 직접 구현
상황 C: 다른 부모 클래스 존재
→ DiscountPolicy 구현
→ DefaultDiscountPolicy 로직을 위임으로 활용
2. 기존 클래스 확장 용이
// 이미 다른 부모가 있는 경우
public class SpecialMovie extends AbstractMovie {
// 이미 상속 중
}
// 인터페이스 추가로 새로운 타입 확장!
public class SpecialMovie extends AbstractMovie
implements DiscountPolicy {
// DiscountPolicy 타입으로도 사용 가능
@Override
public Money calculateDiscountAmount(Screening screening) {
// 구현
return Money.ZERO;
}
}// 인터페이스에 디폴트 메서드
public interface DiscountPolicy {
// 디폴트 메서드 (구현 제공)
default Money calculateDiscountAmount(Screening screening) {
for (DiscountCondition each : getConditions()) {
if (each.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
// 추상 메서드
List<DiscountCondition> getConditions();
Money getDiscountAmount(Screening screening);
}
// 구현 클래스
public class AmountDiscountPolicy implements DiscountPolicy {
private Money discountAmount;
private List<DiscountCondition> conditions;
public AmountDiscountPolicy(Money discountAmount,
DiscountCondition... conditions) {
this.discountAmount = discountAmount;
this.conditions = Arrays.asList(conditions);
}
@Override
public List<DiscountCondition> getConditions() {
return conditions;
}
@Override
public Money getDiscountAmount(Screening screening) {
return discountAmount;
}
}문제 1: 캡슐화 약화
public interface DiscountPolicy {
default Money calculateDiscountAmount(Screening screening) {
// getConditions()를 사용하려면
// 이 메서드가 public이어야 함!
for (DiscountCondition each : getConditions()) {
if (each.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
// ❌ 외부에 노출될 필요 없는데 public이어야 함
List<DiscountCondition> getConditions();
Money getDiscountAmount(Screening screening);
}추상 클래스 방식:
public abstract class DefaultDiscountPolicy {
// ✅ private 가능
private List<DiscountCondition> conditions;
public Money calculateDiscountAmount(Screening screening) {
// private 필드 직접 접근
for (DiscountCondition each : conditions) {
if (each.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
// ✅ protected 가능
protected abstract Money getDiscountAmount(Screening screening);
}문제 2: 디폴트 메서드의 원래 목적
┌─────────────────────────────────────────────────────┐
│ │
│ 디폴트 메서드의 목적: │
│ 추상 클래스를 대체하는 것이 아니라 │
│ 하위 호환성 문제를 해결하는 것! │
│ │
└─────────────────────────────────────────────────────┘
실제 사용 사례:
// Java 8에서 기존 Collection 인터페이스에
// stream() 메서드 추가
public interface Collection<E> {
// 기존 메서드들...
// 새로 추가된 디폴트 메서드
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
}
// 기존 구현 클래스들은 수정 없이도
// stream() 메서드 사용 가능!
List<String> list = Arrays.asList("a", "b", "c");
list.stream().forEach(System.out::println);┌─────────────────────────────────────────────────────┐
│ │
│ 단순한 경우: │
│ → 인터페이스 또는 추상 클래스 중 하나만 사용 │
│ │
│ 복잡한 경우: │
│ → 인터페이스 + 골격 구현 추상 클래스 │
│ │
│ 단일 상속 계층: │
│ → 추상 클래스 고려 │
│ │
│ 다중 타입 필요: │
│ → 인터페이스 고려 │
│ │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ │
│ "어떤 새가 오리처럼 걷고, │
│ 오리처럼 헤엄치며, │
│ 오리처럼 꽥꽥 소리를 낸다면 │
│ 나는 이 새를 오리라고 부를 것이다" │
│ │
│ - James Whitcomb Riley │
│ │
└─────────────────────────────────────────────────────┘
핵심:
덕 타이핑 (Duck Typing):
어떤 대상의 행동이 오리와 같다면
그것을 오리라는 타입으로 취급해도 무방
→ 행동이 타입을 결정!
전통적 타입 시스템:
- 명시적 타입 선언 필요
- 컴파일 타임에 타입 검사
- Employee 인터페이스 구현 필요
덕 타이핑:
- 타입 선언 불필요
- 런타임에 행동 검사
- calculatePay 메서드만 있으면 OK
Ruby (동적 타입 언어):
# 타입 선언 없음!
class SalariedEmployee
def initialize(name, base_pay)
@name = name
@base_pay = base_pay
end
def calculate_pay(tax_rate)
@base_pay - (@base_pay * tax_rate)
end
end
class HourlyEmployee
def initialize(name, base_pay, time_card)
@name = name
@base_pay = base_pay
@time_card = time_card
end
def calculate_pay(tax_rate)
(@base_pay * @time_card) - (@base_pay * @time_card) * tax_rate
end
end
# 덕 타이핑!
def calculate(employee, tax_rate)
# employee가 calculate_pay 메서드만 있으면 OK
employee.calculate_pay(tax_rate)
end
# 사용
calculate(SalariedEmployee.new("Alice", 3000000), 0.1)
calculate(HourlyEmployee.new("Bob", 15000, 160), 0.1)C# (dynamic 키워드):
public class SalariedEmployee
{
private string name;
private decimal basePay;
public SalariedEmployee(string name, decimal basePay)
{
this.name = name;
this.basePay = basePay;
}
public decimal CalculatePay(decimal taxRate)
{
return basePay - basePay * taxRate;
}
}
public class HourlyEmployee
{
private string name;
private decimal basePay;
private int timeCard;
public HourlyEmployee(string name, decimal basePay, int timeCard)
{
this.name = name;
this.basePay = basePay;
this.timeCard = timeCard;
}
public decimal CalculatePay(decimal taxRate)
{
return (basePay * timeCard) - (basePay * timeCard) * taxRate;
}
}
public class TaxOffice
{
// dynamic 사용 - 런타임 타입 체크
public decimal Calculate(dynamic employee, decimal taxRate)
{
return employee.CalculatePay(taxRate);
}
}C++ (템플릿):
// 타입 선언 없음
class SalariedEmployee {
private:
string name;
long base_pay;
public:
SalariedEmployee(string name, long base_pay)
: name(name), base_pay(base_pay) {}
long calculate_pay(double tax_rate) {
return base_pay - (base_pay * tax_rate);
}
};
class HourlyEmployee {
private:
string name;
long base_pay;
int time_card;
public:
HourlyEmployee(string name, long base_pay, int time_card)
: name(name), base_pay(base_pay), time_card(time_card) {}
long calculate_pay(double tax_rate) {
return (base_pay * time_card) - (base_pay * time_card) * tax_rate;
}
};
// 템플릿 - 컴파일 타임 덕 타이핑
template <typename T>
long calculate(T employee, double tax_rate) {
return employee.calculate_pay(tax_rate);
}
// 사용
calculate(SalariedEmployee("Alice", 3000000), 0.1);
calculate(HourlyEmployee("Bob", 15000, 160), 0.1);1. 유연성
- 명시적 타입 선언 불필요
- 새로운 타입 추가 용이
2. 간결성
- 인터페이스 정의 불필요
- 보일러플레이트 코드 감소
3. 빠른 프로토타이핑
- 타입 계층 설계 없이 개발 가능
- 실험적 코드 작성 용이
예시:
# 새로운 타입 추가 - 인터페이스 수정 불필요!
class ContractEmployee
def initialize(name, hourly_rate, hours)
@name = name
@hourly_rate = hourly_rate
@hours = hours
end
def calculate_pay(tax_rate)
(@hourly_rate * @hours) - (@hourly_rate * @hours) * tax_rate
end
end
# 즉시 사용 가능
calculate(ContractEmployee.new("Charlie", 50000, 100), 0.1)1. 타입 안전성 부족
- 컴파일 타임 오류 검출 불가
- 런타임 오류 발생 가능
2. IDE 지원 약함
- 자동 완성 제한적
- 리팩토링 도구 사용 어려움
3. 가독성 저하
- 명시적 타입 정보 부재
- 코드 이해 어려움
4. 디버깅 어려움
- 오류 발생 시점이 늦음
- 오류 원인 파악 어려움
문제 예시:
# 오타가 있어도 컴파일 타임에 발견 못함
def calculate(employee, tax_rate)
# calculate_pay가 아니라 calcuate_pay (오타!)
employee.calcuate_pay(tax_rate)
end
# 런타임에만 오류 발생!
calculate(SalariedEmployee.new("Alice", 3000000), 0.1)
# NoMethodError: undefined method `calcuate_pay'C++ 템플릿:
- 덕 타이핑의 유연성
- 정적 타입의 안전성
→ 컴파일 타임에 타입 체크!
작동 방식:
template <typename T>
long calculate(T employee, double tax_rate) {
return employee.calculate_pay(tax_rate);
}
// 컴파일러가 각 타입별로 함수 생성
// calculate<SalariedEmployee>(...)
// calculate<HourlyEmployee>(...)
// 컴파일 타임에 calculate_pay 메서드 존재 여부 확인!장점:
✅ 유연성 (덕 타이핑)
✅ 타입 안전성 (정적 타입)
✅ 성능 (인라인 가능)
단점:
❌ 컴파일 시간 증가
❌ 바이너리 크기 증가 (각 타입별로 함수 생성)
❌ 복잡한 오류 메시지
┌─────────────────────────────────────────────────────┐
│ │
│ Java는 덕 타이핑을 지원하지 않는다 │
│ │
│ 이유: │
│ - 정적 타입 언어 │
│ - 명시적 타입 선언 필수 │
│ - 컴파일 타임 타입 체크 │
│ │
│ 대안: │
│ - 인터페이스 사용 │
│ - 리플렉션 (성능 문제) │
│ - dynamic proxy (복잡함) │
│ │
└─────────────────────────────────────────────────────┘
믹스인 (Mixin):
객체를 생성할 때
코드 일부를 섞어 넣을 수 있도록 만들어진
일종의 추상 서브클래스
목적:
다양한 객체 구현 안에서
동일한 행동을 중복 코드 없이 재사용
특징:
믹스인 ≠ 상속
상속:
- is-a 관계
- 단일 계층
- 타입 계층 정의
믹스인:
- has-a 행동
- 다중 조합 가능
- 행동 재사용
// Money 클래스
case class Money(amount: Long) extends Ordered[Money] {
// + 연산자
def +(that: Money): Money = Money(this.amount + that.amount)
// - 연산자
def -(that: Money): Money = Money(this.amount - that.amount)
// compare 메서드만 구현!
def compare(that: Money): Int = (this.amount - that.amount).toInt
}
// Ordered 트레이트가 제공하는 것들:
// < (less than)
// > (greater than)
// <= (less than or equal)
// >= (greater than or equal)
// 사용
println(Money(10) < Money(20)) // true
println(Money(30) > Money(20)) // trueOrdered 트레이트 내부 (간소화):
trait Ordered[A] {
// 추상 메서드 - 구현 필요
def compare(that: A): Int
// 믹스인이 제공하는 메서드들
def <(that: A): Boolean = (this compare that) < 0
def >(that: A): Boolean = (this compare that) > 0
def <=(that: A): Boolean = (this compare that) <= 0
def >=(that: A): Boolean = (this compare that) >= 0
}간결한 인터페이스
↓
믹스인
↓
풍부한 인터페이스
Money 클래스:
- 구현한 것: compare 메서드 (1개)
- 얻은 것: <, >, <=, >= 메서드 (4개)
→ 최소한의 노력으로 풍부한 기능!
// Comparable을 확장한 Ordered 인터페이스
public interface Ordered<T> extends Comparable<T> {
// 추상 메서드
int compareTo(T other);
// 디폴트 메서드 (믹스인)
default boolean lessThan(T other) {
return compareTo(other) < 0;
}
default boolean greaterThan(T other) {
return compareTo(other) > 0;
}
default boolean lessThanOrEqual(T other) {
return compareTo(other) <= 0;
}
default boolean greaterThanOrEqual(T other) {
return compareTo(other) >= 0;
}
}
// Money 클래스
public class Money implements Ordered<Money> {
private long amount;
public Money(long amount) {
this.amount = amount;
}
// compareTo만 구현
@Override
public int compareTo(Money other) {
return Long.compare(this.amount, other.amount);
}
// lessThan, greaterThan 등은 자동으로 사용 가능!
}
// 사용
Money m1 = new Money(10);
Money m2 = new Money(20);
System.out.println(m1.lessThan(m2)); // true
System.out.println(m1.greaterThan(m2)); // false
System.out.println(m1.lessThanOrEqual(m2)); // true문제: 캡슐화 약화
public interface DiscountPolicy {
// 믹스인으로 제공하고 싶은 메서드
default Money calculateDiscountAmount(Screening screening) {
// 문제: getConditions()를 사용하려면 public이어야 함!
for (DiscountCondition each : getConditions()) {
if (each.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
// ❌ 외부에 노출될 필요 없는데 public!
List<DiscountCondition> getConditions();
// ❌ 외부에 노출될 필요 없는데 public!
Money getDiscountAmount(Screening screening);
}이상적인 형태 (불가능):
public interface DiscountPolicy {
default Money calculateDiscountAmount(Screening screening) {
// private 메서드 사용하고 싶지만...
// 인터페이스는 private 메서드 불가 (Java 8)
for (DiscountCondition each : getConditions()) {
// ...
}
return Money.ZERO;
}
// 이렇게 하고 싶지만 불가능
private List<DiscountCondition> getConditions();
private Money getDiscountAmount(Screening screening);
}참고: Java 9부터는 private 메서드 가능
// Java 9+
public interface DiscountPolicy {
default Money calculateDiscountAmount(Screening screening) {
// private 메서드 호출 가능!
return calculateInternal(screening);
}
// Java 9부터 가능
private Money calculateInternal(Screening screening) {
// 구현...
return Money.ZERO;
}
}┌─────────────────────────────────────────────────────┐
│ │
│ 디폴트 메서드의 목적: │
│ │
│ 추상 클래스를 대체하려는 것이 아니라 ✗ │
│ 하위 호환성 문제를 해결하려는 것이다 ✓ │
│ │
└─────────────────────────────────────────────────────┘
배경:
Java 8 이전:
interface Collection<E> {
// 기존 메서드들
boolean add(E e);
boolean remove(Object o);
// ...
}
// 수많은 구현 클래스들
class ArrayList<E> implements Collection<E> { ... }
class LinkedList<E> implements Collection<E> { ... }
class HashSet<E> implements Collection<E> { ... }
// ...
문제:
Java 8에서 stream() 추가하고 싶다면?
interface Collection<E> {
// 새 메서드 추가
Stream<E> stream(); // ← 모든 구현 클래스가 컴파일 에러!
}
→ 하위 호환성 깨짐!
해결책: 디폴트 메서드
interface Collection<E> {
// 기존 메서드들...
// 디폴트 메서드로 추가
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
}
// 기존 구현 클래스들은 수정 없이도 동작!
List<String> list = new ArrayList<>();
list.stream().forEach(System.out::println);✅ 믹스인 사용이 좋은 경우:
- 간결한 인터페이스 → 풍부한 인터페이스
- 여러 클래스에서 동일한 행동 재사용
- 행동 조합이 필요한 경우
❌ 믹스인을 피해야 하는 경우:
- 캡슐화가 중요한 경우
- 복잡한 내부 상태 관리가 필요한 경우
- 추상 클래스로도 충분한 경우
| 방법 | 장점 | 단점 | 사용 시나리오 |
|---|---|---|---|
| 클래스 상속 | 간단함 | 강한 결합, 다중 상속 불가 | 단순한 단일 계층 |
| 인터페이스 | 낮은 결합도, 다중 타입 | 코드 중복 | 다중 타입 필요 |
| 추상 클래스 | 구현 공유, DIP | 단일 상속 제약 | 단일 계층 + 구현 공유 |
| 인터페이스 + 추상 | 최선의 조합 | 복잡성 증가 | 복잡한 타입 계층 |
| 덕 타이핑 | 높은 유연성 | 타입 안전성 부족 | 동적 언어, 빠른 개발 |
| 믹스인 | 행동 재사용 | 캡슐화 약화 | 간결→풍부 인터페이스 |
타입 계층 구현 필요
↓
단일 상속 계층인가?
↓ YES ↓ NO
추상 클래스 사용 인터페이스 사용
↓ ↓
구현 공유 필요? 구현 공유 필요?
↓ YES ↓ YES
그대로 사용 골격 구현 추상 클래스 추가
↓
인터페이스 + 추상 클래스
1. 클래스가 아니라 타입에 집중
→ 행동이 중요
2. 타입과 구현의 분리
→ 인터페이스 활용
3. 구현 공유 시 추상화에 의존
→ 추상 클래스, DIP
4. 리스코프 치환 원칙 준수
→ 타입 계층의 필수 조건
5. 트레이드오프 이해
→ 상황에 맞는 선택
1. 인터페이스로 타입 정의
- 퍼블릭 인터페이스만 명시
- 구현은 클래스에 위임
2. 추상 클래스로 공통 구현 제공
- 코드 중복 제거
- 의존성 역전 원칙 준수
3. 골격 구현 패턴 활용
- 유연성과 재사용성 모두 확보
- 복잡한 타입 계층에 적합
4. 믹스인으로 행동 재사용
- 간결한 → 풍부한 인터페이스
- 여러 클래스에서 동일 행동 공유
5. LSP 항상 준수
- 서브타입은 슈퍼타입 대체 가능해야
- 계약 규칙 준수
1. 구체 클래스 상속 지양
- 강한 결합도
- 캡슐화 위반
2. 타입과 구현 혼동 금지
- 클래스 ≠ 타입
- 행동에 집중
3. 디폴트 메서드 남용 금지
- 추상 클래스 대체 목적 아님
- 캡슐화 약화 주의
4. 덕 타이핑 무분별 사용 금지
- 타입 안전성 고려
- 적절한 상황에만 사용
5. LSP 위반 금지
- 타입 계층의 근본
- 치환 가능성 필수
┌─────────────────────────────────────────────────────┐
│ │
│ 타입 계층 구현은 도구일 뿐이다 │
│ │
│ 중요한 것은: │
│ - 행동의 일관성 │
│ - 치환 가능성 │
│ - 유연한 설계 │
│ │
│ 올바른 도구를 선택하라: │
│ - 상황에 맞는 방법 │
│ - 트레이드오프 이해 │
│ - LSP 항상 준수 │
│ │
│ "클래스가 아니라 타입에 집중하라" │
│ │
└─────────────────────────────────────────────────────┘
1. 타입 = 행동의 집합
→ 퍼블릭 인터페이스가 타입 정의
2. 클래스는 타입 구현 방법 중 하나
→ 타입과 구현 분리 중요
3. 인터페이스 + 추상 클래스 = 최선
→ 유연성과 재사용성 모두 확보
4. 덕 타이핑은 양날의 검
→ 유연성 vs 안전성 트레이드오프
5. 믹스인은 행동 재사용 도구
→ 간결함을 풍부하게
6. LSP는 타입 계층의 기본
→ 항상 준수해야 함
7. 설계는 트레이드오프
→ 상황에 맞는 선택이 중요
8. 클래스가 아니라 타입에 집중
→ 이것이 객체지향의 본질
- Appendix A: 계약 → 타입 계층에서 LSP 준수
- Chapter 13: 서브타이핑 → 다양한 구현 방법
- 전체 책: 역할, 책임, 협력 → 타입으로 구체화
- 다음 단계:
- 언어별 특성 이해 (Java, Scala, Kotlin 등)
- 디자인 패턴 학습 (GoF 패턴)
- 타입 시스템 깊이 학습
- 함수형 프로그래밍의 타입 시스템