F-LAB JAVA · 3주차 · Phase 4 · 추상화의 두 도구
🚀 Phase 4 시작 — 추상클래스 vs 인터페이스 정복
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
abstract 키워드 의 정확한 의미는?AbstractList, AbstractMap 의 역할은?추상클래스는 "공통 동작은 구현하고, 변하는 부분은 자식에게 위임" 하는 추상화 도구다.
abstract키워드로 표시되고, 직접 인스턴스화는 불가능 하며 자식 클래스가 추상 메서드를 모두 구현해야 사용 가능.
공통 코드 재사용 + 강제된 확장 지점 의 균형을 제공.
Template Method 패턴의 토대이며,AbstractList,HttpServlet등 자바 라이브러리의 핵심 설계.
일반 클래스 (Concrete Class):
완성된 건물 → 바로 입주 가능
new House() → 즉시 사용
추상클래스 (Abstract Class):
부분 설계도 → 일부는 완성, 일부는 비워둠
- 거실 구조: 완성 (구현 메서드)
- 외벽 자재: 비워둠 (추상 메서드, 자식이 결정)
직접 입주 X → 자식이 빈 곳 채워야 입주
예: AbstractHouse (거실 구현) → KoreanHouse (한옥 자재 선택) → 입주
→ 추상클래스 = 부분 구현 + 강제 확장.
1. abstract 키워드의 정의와 역할
2. 추상클래스의 본질 — 부분 구현
3. 추상 메서드의 문법과 의미
4. 추상클래스의 생성자
5. 단일 상속 제약과 그 이유
6. 7가지 제약과 컴파일 에러
7. Template Method 패턴
8. 자바 표준 라이브러리의 활용
9. 면접 + 자기 점검
abstract 키워드는 두 곳에 사용:
1. 클래스 앞에:
abstract class AbstractShape { ... }
의미: "이 클래스는 추상이라 직접 인스턴스화 못 함"
2. 메서드 앞에:
abstract void draw();
의미: "이 메서드는 구현 없음, 자식이 반드시 구현"
abstract = "불완전함"
불완전한 클래스 = 일부 메서드가 구현 안 됨
→ 인스턴스 생성 불가
→ 자식이 완성해야 함
불완전한 메서드 = 시그니처만 있음
→ 본체 없음
→ 자식이 정의해야 함
// 추상클래스 정의
public abstract class Shape {
// 인스턴스 필드 (가능)
protected String name;
// 생성자 (가능, 자식에서 호출)
protected Shape(String name) {
this.name = name;
}
// 구현 메서드 (가능)
public String getName() {
return name;
}
// 추상 메서드 (자식 강제 구현)
public abstract double area();
public abstract double perimeter();
// 구현 메서드에서 추상 메서드 사용 (Template Method)
public String describe() {
return name + ": area=" + area() + ", perimeter=" + perimeter();
}
}
// 자식 클래스
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
super("Circle");
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
}
// 사용
// Shape s = new Shape("X"); // ❌ 컴파일 에러
Shape circle = new Circle(5); // ✓ 자식으로 생성
circle.describe(); // ✓ 상속받은 메서드 사용
public abstract class Animal {
// 1. 필드 (인스턴스 + static)
protected String name;
private static int count = 0;
// 2. 생성자
protected Animal(String name) {
this.name = name;
count++;
}
// 3. 구현 메서드 (concrete method)
public String getName() {
return name;
}
// 4. 추상 메서드 (abstract method)
public abstract String sound();
// 5. static 메서드
public static int getCount() {
return count;
}
}
핵심:
public abstract class EmptyAbstract {
private int value;
public EmptyAbstract(int value) {
this.value = value;
}
public int getValue() {
return value;
}
// 추상 메서드 0개
}
// EmptyAbstract a = new EmptyAbstract(10); // ❌ 컴파일 에러
abstract 키워드만으로도 인스턴스화 불가.
사용처:
| 항목 | 추상클래스 | 일반 클래스 |
|---|---|---|
| 키워드 | abstract class | class |
| 인스턴스화 | ❌ 불가 | ✓ 가능 |
| 추상 메서드 | 가능 | 불가 |
| 생성자 | 가능 (자식 호출용) | 가능 |
| 필드 | 가능 | 가능 |
| 다중 상속 | ❌ (단일 상속) | ❌ (단일 상속) |
final 조합 | ❌ 에러 | 가능 |
abstract 키워드가 클래스와 메서드에서 의미가 다른가?
답:
→ 함께 사용되지만 별도 개념.
일반 클래스만 있다면:
- 모든 기능을 직접 구현
- 비슷한 클래스에 중복 코드
- 변경 시 모든 곳 수정
인터페이스만 있다면:
- 구현 X (Java 7 까지)
- 메서드 강제만 가능
- 공통 코드 재사용 X
추상클래스의 해결:
- 공통 코드는 부모에서 구현
- 변하는 부분만 자식에서 구현
- 코드 재사용 + 유연성
일반 부모 클래스:
┌─────────────────────────┐
│ class Animal │
│ - name (구현) │
│ - eat() (구현) │
│ - sound() (구현) │ ← 모든 동물 같은 소리?
└─────────────────────────┘
문제: sound() 가 동물마다 달라야 함
추상클래스:
┌─────────────────────────┐
│ abstract class Animal │
│ - name (구현) │
│ - eat() (구현) │
│ + sound() (추상) │ ← 자식이 결정!
└─────────────────────────┘
│
├──→ Dog: sound() → "Bark!"
├──→ Cat: sound() → "Meow!"
└──→ Cow: sound() → "Moo!"
// 운송수단별 요금 계산
public abstract class ShipmentMethod {
protected String name;
protected BigDecimal baseRate;
protected ShipmentMethod(String name, BigDecimal baseRate) {
this.name = name;
this.baseRate = baseRate;
}
// 공통 구현: 모든 운송수단이 같은 로직
public final BigDecimal calculateTotalFare(int weight, int distance) {
BigDecimal base = baseRate.multiply(BigDecimal.valueOf(weight));
BigDecimal distanceFactor = calculateDistanceFactor(distance);
BigDecimal fuelSurcharge = calculateFuelSurcharge(weight);
return base.multiply(distanceFactor).add(fuelSurcharge);
}
// 추상 메서드: 운송수단마다 다른 로직
protected abstract BigDecimal calculateDistanceFactor(int distance);
protected abstract BigDecimal calculateFuelSurcharge(int weight);
}
// 해상 운송
public class SeaShipment extends ShipmentMethod {
public SeaShipment() {
super("Sea", new BigDecimal("0.50"));
}
@Override
protected BigDecimal calculateDistanceFactor(int distance) {
// 거리 1000km 마다 1.1배
return BigDecimal.valueOf(1.0 + (distance / 1000.0) * 0.1);
}
@Override
protected BigDecimal calculateFuelSurcharge(int weight) {
// 해상은 무게당 고정 비율
return BigDecimal.valueOf(weight).multiply(new BigDecimal("0.05"));
}
}
// 항공 운송
public class AirShipment extends ShipmentMethod {
public AirShipment() {
super("Air", new BigDecimal("3.00"));
}
@Override
protected BigDecimal calculateDistanceFactor(int distance) {
// 항공은 거리 100km 마다 1.05배
return BigDecimal.valueOf(1.0 + (distance / 100.0) * 0.05);
}
@Override
protected BigDecimal calculateFuelSurcharge(int weight) {
// 항공은 더 비싼 유가
return BigDecimal.valueOf(weight).multiply(new BigDecimal("0.20"));
}
}
// 사용
ShipmentMethod sea = new SeaShipment();
ShipmentMethod air = new AirShipment();
BigDecimal seaCost = sea.calculateTotalFare(1000, 5000);
BigDecimal airCost = air.calculateTotalFare(1000, 5000);
// 공통 calculateTotalFare 는 한 번만 작성
// 운송수단별 차이는 자식 클래스에서만
일반 클래스로 구현 시:
SeaShipment, AirShipment, GroundShipment 각각:
- calculateTotalFare 모두 구현
- 같은 로직 (base + distance + fuel) 반복
- 변경 시 3곳 모두 수정
추상클래스로 구현 시:
ShipmentMethod 의 calculateTotalFare 한 번
- 모든 자식이 같은 로직 사용
- 변경 시 한 곳만 수정
- 각 자식은 변하는 부분만 작성
설계 의도:
1. "is-a" 강한 관계
Circle is-a Shape
SeaShipment is-a ShipmentMethod
→ 상속 관계 자연스러움
2. 공통 상태 + 공통 동작
필드, 구현 메서드 공유
인터페이스 (Java 8 이전) 와 다른 점
3. 확장 지점 강제
추상 메서드 = "여기 채워야 함"
자식이 구현 안 하면 컴파일 에러
→ 안전한 확장
4. 부분 캡슐화
protected 멤버로 자식만 접근
외부엔 비공개
적합한 경우:
✓ 자식 클래스들이 많은 공통 코드 가짐
✓ "is-a" 관계가 자연스러움
✓ protected 상태/메서드 공유 필요
✓ Template Method 패턴 적용
부적합한 경우:
✗ "can-do" 능력만 표현 (인터페이스가 적합)
✗ 다중 분류 필요 (단일 상속 제약)
✗ 공통 코드 거의 없음
✗ 자식 클래스 수가 적음
추상클래스가 일반 클래스 + 추상 메서드와 다른 의도는?
답:
1. "부분 구현" 의 의도:
강제된 확장 지점:
공통 코드 재사용 + 유연성:
public abstract class Shape {
// 추상 메서드:
// - abstract 키워드
// - 본체 없음 (세미콜론으로 끝)
// - 접근 제어자 가능 (public, protected, package-private)
// - private 불가 (자식이 override 못 함)
// - final 불가 (override 강제와 충돌)
// - static 불가 (인스턴스 메서드만)
public abstract double area();
protected abstract void render(Canvas canvas);
abstract boolean isValid(); // package-private
// private abstract void hidden(); // ❌ 에러
// final abstract void cannot(); // ❌ 에러
// static abstract void invalid(); // ❌ 에러
}
1. 본체 없음
- 세미콜론으로 끝
- { } 또는 코드 작성 시 에러
2. private 불가
- private 은 자식에서 접근 불가
- 추상 메서드는 자식이 구현해야
- 모순 → 에러
3. final 불가
- final 은 override 금지
- 추상 메서드는 override 강제
- 모순 → 에러
4. static 불가
- static 은 인스턴스 무관
- 추상은 polymorphism 위한 것
- 의미적 모순
public abstract class Shape {
public abstract double area();
}
// 자식 1 — 구현
public class Circle extends Shape {
private double radius;
@Override
public double area() {
return Math.PI * radius * radius;
}
}
// 자식 2 — 구현 안 함
public class IncompleteShape extends Shape {
// ❌ 컴파일 에러:
// The type IncompleteShape must implement the inherited
// abstract method Shape.area()
}
// 자식 3 — 자기도 추상
public abstract class AbstractCircle extends Shape {
// 구현 안 해도 됨 (자기도 추상이라)
// 다음 자식이 구현하면 됨
}
public class ConcreteCircle extends AbstractCircle {
@Override
public double area() {
return 3.14;
}
}
public class Circle extends Shape {
@Override // ★ 권장
public double area() {
return Math.PI * radius * radius;
}
}
@Override 의 효과:
aera() 등) 즉시 발견public abstract class Shape {
public abstract double area();
}
public class Circle extends Shape {
// ✓ 올바른 override
@Override
public double area() { return ...; }
// ❌ 시그니처 불일치 (이름 다름)
@Override
public double areaa() { return ...; }
// ❌ 반환 타입 다름
@Override
public int area() { return 0; }
// △ 매개변수 다름 → override 아닌 overload
public double area(double scale) { return ...; }
// 추상 메서드 area() 는 여전히 구현 안 됨 → 컴파일 에러
}
핵심:
자바 5+ 부터 자식이 반환 타입을 좁힐 수 있음:
public abstract class Shape {
public abstract Shape clone();
}
public class Circle extends Shape {
@Override
public Circle clone() { // ★ Shape 아닌 Circle 반환
return new Circle(this.radius);
}
}
// 사용 시 더 정확한 타입
Circle c = new Circle(5);
Circle cloned = c.clone(); // Shape 아닌 Circle 직접 반환
이를 공변 반환 타입 (Covariant Return Type) 이라 함.
public abstract class ShipmentMethod {
// 외부에서 호출 가능
public final BigDecimal calculateTotalFare(int weight, int distance) {
// ...
BigDecimal factor = calculateDistanceFactor(distance); // 자기 호출
// ...
}
// 자식만 구현, 외부 호출 X
protected abstract BigDecimal calculateDistanceFactor(int distance);
}
protected abstract:
public abstract class FileProcessor {
// 추상 메서드도 throws 선언 가능
public abstract void process(File file) throws IOException;
}
public class TextFileProcessor extends FileProcessor {
@Override
public void process(File file) throws IOException {
// ...
}
// 자식은 같은 또는 좁은 예외 (또는 unchecked)
// ✓ 같은 예외
// ✓ IOException 의 자식 (FileNotFoundException 등)
// ✓ 예외 안 던짐도 가능
// ❌ 새 checked 예외 추가는 불가
}
추상 메서드의 4가지 제약과 그 이유는?
답:
1. 본체 없음 — 자식이 구현하라는 의미
2. private 불가 — 자식이 접근/override 못 함, 모순
3. final 불가 — override 금지와 강제의 모순
4. static 불가 — polymorphism 무의미
추가:
@Override 권장추상클래스의 특징:
- 직접 인스턴스화 불가
- new AbstractShape() → 컴파일 에러
그런데 생성자는?
- 보통 객체 생성 시 호출
- 추상클래스 인스턴스화 불가 → 호출 안 됨?
해답:
- 자식 클래스가 생성될 때 호출
- super() 로 부모 생성자 호출
- 부모의 필드 초기화에 활용
public abstract class Shape {
protected String name;
protected Shape(String name) { // 생성자
this.name = name;
System.out.println("Shape created: " + name);
}
}
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
super("Circle"); // ★ 부모 생성자 호출
this.radius = radius;
System.out.println("Circle created with radius " + radius);
}
}
// 사용
Circle c = new Circle(5);
// 출력:
// Shape created: Circle
// Circle created with radius 5
new Circle(5) 호출 시:
Step 1: 메모리 할당 (Circle 크기)
Step 2: Object 의 생성자 시작
Step 3: Shape 의 생성자 시작
- super("Circle") 명시되었음
- name = "Circle"
Step 4: Shape 의 생성자 종료
Step 5: Circle 의 생성자 본체 시작
- radius = 5
Step 6: Circle 의 생성자 종료
Step 7: new 반환 → Circle 객체
→ 위에서 아래로 (Object → Shape → Circle)
public abstract class Shape {
// 기본 생성자
protected Shape() {
System.out.println("Shape()");
}
// 매개변수 생성자
protected Shape(String name) {
this.name = name;
System.out.println("Shape(name)");
}
}
// 자식 1 — super() 명시
public class Circle extends Shape {
public Circle() {
super(); // 명시적
}
}
// 자식 2 — super() 생략 → 컴파일러가 super() 자동 추가
public class Square extends Shape {
public Square() {
// super() 가 자동 추가됨 (기본 생성자가 있다면)
}
}
// 자식 3 — 부모에 기본 생성자 없으면
public class Rectangle extends Shape {
public Rectangle() {
super("Rectangle"); // ★ 명시 필수
// 또는
// super(); // ❌ Shape 의 기본 생성자 없으면 에러
}
}
핵심:
super() 자동 추가public abstract class Shape {
// ✓ public 가능
public Shape() { ... }
// ✓ protected 가능 (자식만 호출)
protected Shape(String name) { ... }
// ✓ package-private 가능
Shape(int n) { ... }
// ✓ private 가능 (드물게)
private Shape(boolean special) { ... }
// → 같은 클래스 내에서만 호출 (다른 생성자가 this() 로)
}
권장:
protectedpublic 도 가능하지만 자식만 사용 → protected 가 의도 명확public abstract class Shape {
public Shape() {
init(); // ❌ 위험!
}
public abstract void init();
}
public class Circle extends Shape {
private double radius = 5;
public Circle() {
super(); // → Shape() → init() 호출
radius = 10;
}
@Override
public void init() {
System.out.println("Radius: " + radius);
// 어떤 값?
}
}
// 출력:
// Radius: 0.0 ← 0!
이유:
해결:
public abstract class Animal {
protected String name;
protected int age;
// 생성자 1: 기본
protected Animal() {
this("Unknown", 0);
}
// 생성자 2: 이름만
protected Animal(String name) {
this(name, 0);
}
// 생성자 3: 전체
protected Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
public class Dog extends Animal {
public Dog() {
super(); // 또는 super("Dog", 0);
}
public Dog(String name, int age) {
super(name, age);
}
}
추상클래스가 인스턴스화 안 되는데 생성자가 있는 이유는?
답:
1. 자식 클래스가 super() 로 호출
2. 부모 필드 초기화에 활용
3. 부모의 invariant 설정 (제약 조건)
4. 공통 초기화 로직 재사용
호출 흐름:
주의:
// ❌ 다중 상속 불가
public class Circle extends Shape, Drawable { // 에러!
}
// ✓ 단일 상속
public class Circle extends Shape {
}
// ✓ 인터페이스는 다중 가능
public class Circle extends Shape implements Drawable, Comparable {
}
다중 상속을 허용한다면:
Animal
/ \
Mammal Bird
\ /
[Bat - 둘 다 상속]
문제:
Mammal.move() 가 있고 Bird.move() 도 있다면
Bat.move() 는 어느 것?
→ "Diamond Problem" — 모호함
자바의 결정 (1995):
- 클래스 다중 상속 ❌
- 인터페이스 다중 구현 ✓ (구현이 없으니 모호함 없음)
이유:
1. 단순성 — Diamond Problem 회피
2. 명확한 메서드 해결 (method resolution)
3. C++ 의 복잡한 다중 상속 회피
public abstract class Animal {
public abstract void move();
}
public abstract class Swimmer {
public abstract void swim();
}
// 문제: 물고기는 Animal 이자 Swimmer
public class Fish extends Animal { // OK
// extends Swimmer 도 못 함
@Override
public void move() { ... }
// Swimmer 의 swim() 못 받음
}
해결 방안:
1. 인터페이스로: interface Swimmer + implements
2. 컴포지션: private Swimmer swimmer;
3. 계층 재구성: Animal → AquaticAnimal → Fish
// 1번째 부모 클래스
public abstract class Animal {
public abstract void move();
}
// 능력을 인터페이스로
public interface Swimmer {
void swim();
}
public interface Flyer {
void fly();
}
// 다중 구현 가능
public class Bat extends Animal implements Flyer {
@Override
public void move() { ... }
@Override
public void fly() { ... }
}
public class Fish extends Animal implements Swimmer {
@Override
public void move() { ... }
@Override
public void swim() { ... }
}
public class Duck extends Animal implements Swimmer, Flyer {
@Override
public void move() { ... }
@Override
public void swim() { ... }
@Override
public void fly() { ... }
}
→ Phase 4 의 핵심 메시지: 클래스는 단일 상속, 인터페이스는 다중 구현.
public interface Swimmer {
default void swim() { // 구현 가능
System.out.println("Swimming");
}
}
public interface Flyer {
default void fly() {
System.out.println("Flying");
}
}
// 다중 default 의 충돌?
public interface SuperHero extends Swimmer, Flyer {
@Override
default void swim() {
System.out.println("Super swim");
}
}
Java 8+ 에선 인터페이스에도 구현 가능 → diamond problem 부분 부활.
다음 Unit 4.3 에서 자세히.
public class Duck extends Animal {
private Swimmer swimmer = new BasicSwimmer();
private Flyer flyer = new BasicFlyer();
public void swim() { swimmer.swim(); }
public void fly() { flyer.fly(); }
}
public class Duck extends Animal implements Swimmer, Flyer {
private Swimmer swimmer;
private Flyer flyer;
public Duck(Swimmer s, Flyer f) {
this.swimmer = s;
this.flyer = f;
}
@Override
public void swim() { swimmer.swim(); }
@Override
public void fly() { flyer.fly(); }
}
Scala, Ruby 등의 mixin:
- 여러 trait/module 을 합성
- 자바는 인터페이스 + default 로 부분적 흉내
자바가 단일 상속을 채택한 이유와 우회 방법은?
답:
이유:
우회 방법:
→ "is-a 는 단일 상속, can-do 는 다중 구현".
public abstract class Shape { ... }
// ❌ 에러
Shape s = new Shape();
// ✓ 자식으로 가능
Shape s = new Circle();
// ✓ 익명 자식 클래스 (사실 자식)
Shape s = new Shape() {
@Override
public double area() { return 10; }
};
// ❌ 에러
public abstract final class Shape { ... }
// 이유:
// - abstract: 자식이 완성해야 함
// - final: 자식 못 만듦
// - 모순!
public abstract class Shape {
// ❌ 에러
private abstract void hidden();
// 이유:
// - abstract: 자식이 override 해야 함
// - private: 자식에서 접근 불가
// - 모순!
}
public abstract class Shape {
// ❌ 에러
public static abstract void doSomething();
// 이유:
// - static: 클래스 메서드 (인스턴스 무관)
// - abstract: polymorphism 통한 자식 동작
// - 모순!
}
// 일반 클래스
public class Circle {
// ❌ 에러
public abstract void draw();
// 이유:
// - 추상 메서드 있는 클래스는 추상클래스여야
// - 일반 클래스로 만들 수 없음
}
// ✓ 추상클래스에서만 가능
public abstract class Shape {
public abstract void draw();
}
public abstract class Shape {
// ❌ 에러
public abstract void draw() {
// 본체 있음
}
// ✓ 본체 없이 세미콜론
public abstract void draw();
// 본체 있으면 abstract 가 아님 (일반 구현 메서드)
}
public abstract class Shape {
public abstract double area();
public abstract double perimeter();
}
// ❌ 에러: area() 구현 안 함
public class Circle extends Shape {
@Override
public double perimeter() { return 0; }
}
// ✓ 모두 구현
public class Circle extends Shape {
@Override
public double area() { return 0; }
@Override
public double perimeter() { return 0; }
}
// ✓ 자식도 추상
public abstract class AbstractCircle extends Shape {
@Override
public double area() { return 0; }
// perimeter() 는 다음 자식이 구현
}
public abstract class Shape {
// ✓ 인터페이스 구현 가능
// implements Drawable
// ✓ 다른 추상클래스 상속 가능
// extends AbstractShape (다른 추상)
// ✓ 일반 클래스 상속 가능 (드물게)
// extends Object (당연)
// ✓ 추상 메서드 0개 가능
// (그래도 인스턴스화 불가)
// ✓ 모든 메서드 구현해도 가능
// (사용자가 인스턴스화 막고 싶을 때)
}
| 조합 | 가능? | 이유 |
|---|---|---|
abstract class | ✓ | 표준 |
abstract final class | ❌ | 모순 (확장 vs 닫음) |
abstract + 메서드 본체 | ❌ | 추상 정의 위반 |
private abstract 메서드 | ❌ | 접근 불가 모순 |
static abstract 메서드 | ❌ | polymorphism 무의미 |
final abstract 메서드 | ❌ | override 강제 vs 금지 모순 |
| 추상 메서드 in 일반 클래스 | ❌ | 클래스도 추상이어야 |
new AbstractClass() | ❌ | 인스턴스화 금지 |
추상 메서드 0개의 abstract class | ✓ | 가능 |
abstract class 의 생성자 | ✓ | 자식이 호출 |
abstract + final, abstract + private, abstract + static 이 모두 에러인 공통 이유는?
답:
→ abstract 는 본질적으로 "확장" 의도, 다른 키워드들과 충돌.
Template Method Pattern:
알고리즘의 골격을 정의하되,
일부 단계는 자식 클래스가 구현하게 하는 패턴.
GoF 디자인 패턴의 핵심 중 하나.
핵심 아이디어:
- 추상클래스 = 골격 (template)
- 자식 클래스 = 채우기 (concrete)
AbstractClass:
templateMethod() { ← final, 외부 진입점
step1(); ← concrete (공통)
step2(); ← abstract (자식 구현)
step3(); ← concrete (공통)
hookMethod(); ← 기본 구현 + 선택적 override
}
step1() { ... } ← 공통 구현
abstract step2(); ← 자식 강제
step3() { ... } ← 공통 구현
hookMethod() { ... } ← 기본 구현 (선택)
public abstract class Beverage {
// Template Method (final 로 알고리즘 보호)
public final void prepare() {
boilWater();
brew(); // 추상 — 자식 구현
pourInCup();
if (customerWantsCondiments()) { // 훅 메서드
addCondiments(); // 추상 — 자식 구현
}
}
// 공통 구현
private void boilWater() {
System.out.println("Boiling water");
}
private void pourInCup() {
System.out.println("Pouring into cup");
}
// 추상 메서드 — 자식이 구현
protected abstract void brew();
protected abstract void addCondiments();
// 훅 메서드 — 기본 true, 자식이 override 가능
protected boolean customerWantsCondiments() {
return true;
}
}
public class Tea extends Beverage {
@Override
protected void brew() {
System.out.println("Steeping tea bag");
}
@Override
protected void addCondiments() {
System.out.println("Adding lemon");
}
}
public class Coffee extends Beverage {
@Override
protected void brew() {
System.out.println("Dripping coffee");
}
@Override
protected void addCondiments() {
System.out.println("Adding sugar and milk");
}
@Override
protected boolean customerWantsCondiments() {
// 사용자 입력으로 결정
return getUserAnswer();
}
private boolean getUserAnswer() {
// ...
return true;
}
}
// 사용
Beverage tea = new Tea();
tea.prepare();
// 출력:
// Boiling water
// Steeping tea bag
// Pouring into cup
// Adding lemon
Beverage coffee = new Coffee();
coffee.prepare();
// 출력:
// Boiling water
// Dripping coffee
// Pouring into cup
// Adding sugar and milk
1. 알고리즘 골격 보호
- templateMethod() 는 final
- 자식이 알고리즘 자체를 못 바꿈
- 변하는 부분만 자식이 결정
2. 코드 재사용
- 공통 단계 (boilWater, pourInCup) 는 한 번만
- 자식마다 반복 안 함
3. 확장 지점 명확
- abstract = 반드시 구현
- hook = 선택적 override
4. Open/Closed Principle
- 알고리즘 닫힘 (변경 X)
- 단계 열림 (확장 가능)
// 운송 처리 알고리즘
public abstract class ShipmentProcessor {
// Template Method
public final void process(Shipment shipment) {
validate(shipment);
enrichData(shipment);
BigDecimal fare = calculateFare(shipment); // 추상
applyDiscount(fare, shipment); // 훅
saveResult(shipment, fare);
notifyCustomer(shipment); // 훅
}
// 공통 구현
private void validate(Shipment s) {
Objects.requireNonNull(s);
if (s.getWeight() <= 0) {
throw new IllegalArgumentException("Invalid weight");
}
}
private void enrichData(Shipment s) {
// DB 에서 추가 정보 조회
}
private void saveResult(Shipment s, BigDecimal fare) {
// DB 저장
}
// 추상 메서드
protected abstract BigDecimal calculateFare(Shipment s);
// 훅 메서드 (기본 동작 + override 가능)
protected BigDecimal applyDiscount(BigDecimal fare, Shipment s) {
return fare; // 기본: 할인 없음
}
protected void notifyCustomer(Shipment s) {
// 기본: 이메일
emailService.send(s.getCustomerEmail());
}
}
// 해상 운송
public class SeaShipmentProcessor extends ShipmentProcessor {
@Override
protected BigDecimal calculateFare(Shipment s) {
return s.getWeight() * 0.5;
}
@Override
protected BigDecimal applyDiscount(BigDecimal fare, Shipment s) {
// 대량 운송 할인
if (s.getWeight() > 1000) {
return fare.multiply(new BigDecimal("0.9"));
}
return fare;
}
}
// 항공 운송
public class AirShipmentProcessor extends ShipmentProcessor {
@Override
protected BigDecimal calculateFare(Shipment s) {
return s.getWeight() * 3.0;
}
@Override
protected void notifyCustomer(Shipment s) {
// 항공은 SMS + 이메일
emailService.send(s.getCustomerEmail());
smsService.send(s.getCustomerPhone());
}
}
Template Method:
- 상속 기반
- 알고리즘 골격은 부모
- 변하는 부분은 자식
Strategy:
- 컴포지션 기반
- 알고리즘 전체를 외부 객체로
- 런타임에 교체 가능
public abstract class Document {
// Template Method
public final void open() {
loadFile();
Parser parser = createParser(); // ← Factory Method
parser.parse();
render();
}
// Factory Method
protected abstract Parser createParser();
}
public class PdfDocument extends Document {
@Override
protected Parser createParser() {
return new PdfParser();
}
}
Factory Method 도 Template Method 의 일종.
1. 깊은 상속 계층
- 자식 → 손자 → 증손자 ...
- 유지보수 어려움
2. final 의 강제
- 알고리즘 자체는 변경 불가
- 비슷하지만 약간 다른 흐름은 못 만듦
3. 자식 클래스 폭발
- 변형마다 새 자식
- Strategy 가 더 유연
4. 부모-자식 강결합
- 자식이 부모 내부 의존
- 부모 변경 시 자식 영향
Template Method 패턴의 핵심 메커니즘과 추상클래스와의 관계는?
답:
Template Method:
추상클래스와의 관계:
→ 추상클래스의 진정한 가치는 Template Method.
public abstract class AbstractList<E> extends AbstractCollection<E>
implements List<E> {
// 추상 메서드 — 자식이 반드시 구현
public abstract E get(int index);
public abstract int size();
// 구현 메서드 — 자식 메서드 활용
public boolean add(E e) {
add(size(), e); // 다른 메서드 호출
return true;
}
public Iterator<E> iterator() {
return new Itr(); // 내부 Iterator 클래스 제공
}
public int indexOf(Object o) {
ListIterator<E> it = listIterator();
while (it.hasNext()) {
if (Objects.equals(o, it.next())) {
return it.previousIndex();
}
}
return -1;
}
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
public boolean equals(Object o) {
// List 의 표준 equals 구현
if (o == this) return true;
if (!(o instanceof List)) return false;
// ...
}
public int hashCode() {
// List 의 표준 hashCode 구현
int hashCode = 1;
for (E e : this) {
hashCode = 31 * hashCode + (e == null ? 0 : e.hashCode());
}
return hashCode;
}
// ... 더 많은 메서드
}
핵심:
get(int) + size() 만 구현하면 List 완성public class ArrayList<E> extends AbstractList<E>
implements List<E>, ... {
transient Object[] elementData;
private int size;
@Override
public E get(int index) {
Objects.checkIndex(index, size);
return (E) elementData[index];
}
@Override
public int size() {
return size;
}
// ArrayList 만의 최적화
@Override
public boolean add(E e) {
// 배열 직접 조작 (AbstractList 의 기본보다 빠름)
elementData[size++] = e;
return true;
}
// ...
}
→ AbstractList 가 90% 구현, ArrayList 는 효율 최적화.
public abstract class AbstractMap<K, V> implements Map<K, V> {
// 추상 메서드 — 자식 강제 구현
public abstract Set<Map.Entry<K, V>> entrySet();
// 구현 메서드 — entrySet 활용
public int size() {
return entrySet().size();
}
public boolean isEmpty() {
return size() == 0;
}
public V get(Object key) {
for (Entry<K, V> entry : entrySet()) {
if (Objects.equals(entry.getKey(), key)) {
return entry.getValue();
}
}
return null;
}
public boolean containsKey(Object key) {
return get(key) != null;
}
// ... 더 많은 메서드
}
핵심:
entrySet() 만 구현하면 Map 완성public abstract class HttpServlet extends GenericServlet {
// Template Method
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String method = req.getMethod();
if ("GET".equals(method)) {
doGet(req, resp);
} else if ("POST".equals(method)) {
doPost(req, resp);
} else if ("PUT".equals(method)) {
doPut(req, resp);
} else if ("DELETE".equals(method)) {
doDelete(req, resp);
}
// ...
}
// 훅 메서드들 — 자식이 필요한 것만 override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 기본: 405 Method Not Allowed
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
protected void doPost(...) { ... } // 기본 405
protected void doPut(...) { ... } // 기본 405
protected void doDelete(...) { ... } // 기본 405
}
// 사용자가 구현
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
resp.getWriter().println("Hello");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
// POST 처리
}
// doPut, doDelete 안 만들어도 됨 (기본 405)
}
public abstract class InputStream implements Closeable {
// 추상 메서드 — 자식 강제
public abstract int read() throws IOException;
// 구현 메서드 — read() 활용
public int read(byte[] b, int off, int len) throws IOException {
// 기본 구현: read() 반복 호출
for (int i = 0; i < len; i++) {
int c = read();
if (c == -1) return i;
b[off + i] = (byte) c;
}
return len;
}
public long skip(long n) throws IOException {
// ...
}
public int available() throws IOException {
return 0;
}
}
// 자식 클래스
public class FileInputStream extends InputStream {
@Override
public int read() throws IOException {
// 파일에서 한 바이트 읽기
return readNative();
}
// 더 효율적인 read(byte[], int, int) 구현
@Override
public int read(byte[] b, int off, int len) throws IOException {
// native 호출로 빠른 읽기
return readBytes(b, off, len);
}
}
컬렉션:
- AbstractCollection
- AbstractList
- AbstractSet
- AbstractMap
- AbstractQueue
- AbstractSequentialList
I/O:
- InputStream / OutputStream
- Reader / Writer
- FilterInputStream / FilterOutputStream
웹:
- HttpServlet
- GenericServlet
- AbstractHandler (Spring)
기타:
- Number (Integer, Long 등의 부모)
- Calendar (GregorianCalendar 의 부모)
- ClassLoader
- Charset
// Spring MVC 의 핵심
public abstract class AbstractController extends WebContentGenerator
implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response)
throws Exception {
// Template Method
checkRequest(request);
prepareResponse(response);
return handleRequestInternal(request, response);
}
// 자식이 구현
protected abstract ModelAndView handleRequestInternal(
HttpServletRequest request, HttpServletResponse response)
throws Exception;
}
// JPA Repository
public abstract class SimpleJpaRepository<T, ID>
implements JpaRepositoryImplementation<T, ID> {
@Override
public Optional<T> findById(ID id) {
// 공통 구현
}
@Override
public List<T> findAll() {
// 공통 구현
}
// ... 모든 표준 메서드 구현
}
AbstractList 가 어떻게 List 인터페이스의 90% 를 처리하나?
답:
1. 추상 메서드 강제: get(int) + size()
2. 다른 메서드는 이 둘로 구현:
contains → indexOf 활용iterator → 내부 Itr 클래스equals, hashCode → 표준 구현add, remove → 인덱스 활용→ 추상클래스가 표준 라이브러리의 코드 재사용 핵심.
| Q | 핵심 답변 |
|---|---|
| abstract 키워드의 의미? | 불완전, 직접 인스턴스화 불가 |
| 추상클래스 vs 일반 클래스? | 인스턴스화 가부 + 추상 메서드 가능 여부 |
| 추상 메서드의 4가지 제약? | 본체 X, private X, final X, static X |
| 추상클래스의 생성자? | 자식이 super() 로 호출 |
| 추상클래스에 생성자 있는 이유? | 부모 필드 초기화 |
| 단일 상속 채택 이유? | Diamond Problem 회피 |
| abstract + final 에러? | 확장 vs 닫음의 모순 |
| Template Method 패턴? | 골격은 부모, 단계는 자식 |
| AbstractList 의 역할? | get + size 만 구현하면 List 완성 |
| HttpServlet 의 service 메서드? | doGet/doPost 등 디스패치 |
| @Override 의 효과? | 컴파일러 검증, 오타 발견 |
| 공변 반환 타입? | 자식이 더 좁은 타입 반환 가능 |
답:
답:
abstract class A {
abstract void m1();
}
abstract class B extends A {
abstract void m2();
// m1() 도 여전히 추상
}
class C extends B {
@Override void m1() { ... }
@Override void m2() { ... }
}
답:
답:
public abstract class AbstractRunner {
public abstract void run();
public static void main(String[] args) {
// 자식 클래스 사용
AbstractRunner runner = new ConcreteRunner();
runner.run();
}
}
1. abstract = "불완전함"
2. 추상클래스의 가치
3. 자바 라이브러리의 핵심
이번 Unit에서 추상클래스를 봤다면, 다음은 인터페이스의 본질.
🚀 Phase 4 — 추상화의 두 도구
✅ Unit 4.1 추상클래스의 특징 ← 여기
⏭ Unit 4.2 인터페이스의 특징
⏭ Unit 4.3 Java 8 default & static 메서드
⏭ Unit 4.4 추상클래스 vs 인터페이스 선택 기준
✅ Phase 1 — Pass by Value (1.1 ~ 1.3 완주)
✅ Phase 2 — 컬렉션 프레임워크 (2.1 ~ 2.6 완주)
✅ Phase 3 — 해시의 원리 (3.1 ~ 3.4 완주)
🚀 Phase 4 — 추상화의 두 도구 (1/4 진행)
총: 14/43 Unit 작성 (약 33%)