1. 계층 구성: 추상에서 구체로
추상화와 계층 구조를 구현한다는 것은 코드를 일반 규칙에서 구체적인 세부 사항으로 구조화하는 방법입니다. 먼저 모든 객체가 무엇을 할 수 있어야 하는지(추상화)를 정의하고, 그다음 각 구체 클래스가 그것을 어떻게 수행하는지 구체화합니다.
프로그래밍에서도 삶과 마찬가지로 모든 것은 질문에서 시작됩니다. 예를 들어: “원과 직사각형의 공통점은 무엇일까요?” 답: 둘 다 — 도형입니다. 그럼 도형의 공통점은 무엇일까요? 보통 넓이가 있고, 그릴 수 있습니다.
Java에서는 이것을 abstract-클래스로 표현합니다:
public abstract class Shape {
public abstract double area();
public abstract void draw();
}
여기서 말하는 것은:
- 모든 도형은 자신의 면적을 계산할 수 있어야 합니다(area()).
- 모든 도형은 스스로를 그릴 수 있어야 합니다(draw()).
- 구체적으로 어떻게 하는지는 아직 중요하지 않습니다(지금은).
이제 구체적인 도형을 만들어 봅시다:
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public void draw() {
System.out.println("반지름이 " + radius + "인 원을 그립니다");
}
}
public class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public void draw() {
System.out.println("직사각형 " + width + "x" + height + "을 그립니다");
}
}
무엇을 했나요?
- 공통점을 추상 클래스에 올렸습니다.
- 세부 동작은 하위 클래스에서 구체화했습니다.
도식:
. Shape
/ \
Circle Rectangle
표: 무엇이 어디에 구현되어 있는가
| 클래스 | area() | draw() | 고유 필드 |
|---|---|---|---|
| Shape | |
|
- |
| Circle | 구현됨 | 구현됨 | |
| Rectangle | 구현됨 | 구현됨 | |
2. 왜 편리할까? (그리고 왜 이렇게 동작할까)
서로 다른 객체를 위한 통일된 인터페이스
예를 들어, 도형 컬렉션이 있다고 합시다:
Shape[] shapes = {
new Circle(5),
new Rectangle(3, 4),
new Circle(2.5)
};
형에 상관없이 동일하게 순회할 수 있습니다:
for (Shape shape : shapes) {
shape.draw();
System.out.println("면적: " + shape.area());
}
누가 원이고 누가 직사각형인지는 JVM이 알아서 처리하게 두세요! 이것이 바로 다형성입니다(이미 한 번 언급했고, 다음 블록에서 더 자세히 다룹니다).
확장 용이성
삼각형을 추가하고 싶나요? 이렇게 작성하면 됩니다(새 타입 — 기존 코드는 수정하지 않음): Triangle extends Shape.
public class Triangle extends Shape {
private double base, height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double area() {
return 0.5 * base * height;
}
@Override
public void draw() {
System.out.println("삼각형을 그립니다: 밑변 " + base + ", 높이 " + height);
}
}
나머지 코드는(예: 도형 목록 순회) 변경할 필요가 없습니다.
코드 중복 방지
모든 도형에 공통 속성(예: 색상)이 생겼다면, 추상 클래스에 올리는 것이 편합니다:
public abstract class Shape {
private String color = "검정";
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
public abstract double area();
public abstract void draw();
}
이제 어떤 하위 클래스든 — 원이든 삼각형이든 — 색상을 상속으로 받습니다.
3. 실습: 미니 그래픽 편집기 구현
모든 것을 합쳐 봅시다. 간단한 그래픽 편집기를 만든다고 가정해 보세요.
추상 클래스 Figure
public abstract class Figure {
private String color = "black";
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
public abstract void draw();
public abstract void resize(double factor);
}
구체 도형
public class Line extends Figure {
private double length;
public Line(double length) {
this.length = length;
}
@Override
public void draw() {
System.out.println("길이가 " + length + "인 선을 색상 " + getColor() + "로 그립니다");
}
@Override
public void resize(double factor) {
length *= factor;
System.out.println("새 선 길이: " + length);
}
}
public class Ellipse extends Figure {
private double a, b;
public Ellipse(double a, double b) {
this.a = a;
this.b = b;
}
@Override
public void draw() {
System.out.println("축이 " + a + "과 " + b + "인 타원을 색상 " + getColor() + "로 그립니다");
}
@Override
public void resize(double factor) {
a *= factor;
b *= factor;
System.out.println("타원의 새로운 축: " + a + ", " + b);
}
}
새 도구(예: Polygon)를 추가하고 싶나요? 새 클래스를 만들기만 하면 됩니다 — 편집기 전체 코드는 추상 Figure를 통해 동작합니다.
코드에서의 사용
Figure[] figures = {
new Line(10),
new Ellipse(5, 3)
};
for (Figure figure : figures) {
figure.setColor("red");
figure.draw();
figure.resize(1.5);
}
출력:
길이가 10.0인 선을 색상 red로 그립니다
새 선 길이: 15.0
축이 5.0과 3.0인 타원을 색상 red로 그립니다
타원의 새로운 축: 7.5, 4.5
계층 시각화
. Figure
/ \
Line Ellipse
4. 중복을 피하는 방법: 공통 필드와 메서드
때로는 모든 하위 클래스에 공통 메서드뿐 아니라 공통 필드(예: 중심 좌표)도 있습니다. 추상 클래스가 이를 넣기에 가장 좋은 곳입니다:
public abstract class Figure {
private double x, y; // 중심 좌표
public Figure(double x, double y) {
this.x = x;
this.y = y;
}
public void moveTo(double newX, double newY) {
x = newX;
y = newY;
System.out.println("도형이 위치 (" + x + ", " + y + ")로 이동되었습니다");
}
public abstract void draw();
}
이제 Line이나 Ellipse는 이 메서드를 다시 구현하지 않고도 이동할 수 있습니다.
5. 또 다른 예: 결제 시스템
추상화는 도형에만 국한되지 않습니다! 결제 처리 시스템을 만든다고 가정해 봅시다.
추상 클래스 Payment
public abstract class Payment {
public abstract void process();
}
구체 구현
public class CreditCardPayment extends Payment {
@Override
public void process() {
System.out.println("신용카드 결제 처리");
}
}
public class PaypalPayment extends Payment {
@Override
public void process() {
System.out.println("PayPal 결제 처리");
}
}
사용
Payment[] payments = {
new CreditCardPayment(),
new PaypalPayment()
};
for (Payment payment : payments) {
payment.process();
}
출력:
신용카드 결제 처리
PayPal 결제 처리
6. 이 접근 방식의 장점
- 통일된 인터페이스: 서로 다른 객체를 동일한 방식으로 다룰 수 있습니다.
- 확장성: 새로운 종류의 객체를 추가해도 기존 코드를 다시 작성할 필요가 없습니다.
- 최소한의 중복: 공통 사항을 기본 추상 클래스에 올립니다.
- 유연성: 세부를 신경 쓰지 않고 추상 타입의 컬렉션을 사용할 수 있습니다.
7. 현실 예시: 교통
추상화는 교과서 밖에서도 등장합니다. 예를 들어, 교통 시스템을 설계한다고 합시다:
public abstract class Transport {
public abstract void move();
public abstract void fuelUp();
}
구체적인 교통수단은 세부를 구현합니다:
public class Car extends Transport {
@Override
public void move() {
System.out.println("자동차가 도로를 달립니다");
}
@Override
public void fuelUp() {
System.out.println("휘발유를 주유합니다");
}
}
public class Bicycle extends Transport {
@Override
public void move() {
System.out.println("자전거는 페달을 밟습니다");
}
@Override
public void fuelUp() {
System.out.println("자전거는 연료가 필요 없습니다. 라이더의 샌드위치만 있으면 됩니다!");
}
}
8. 유용한 도식: 추상화 계층을 어떻게 구성할까
[추상 클래스]
|
[구체 하위 클래스]
|
[더 구체적인 하위 클래스] (필요하다면)
- 공통된 것은 위로!
- 고유한 것은 아래로!
9. 추상화와 계층 구현 시 흔한 실수
실수 № 1: 하위 클래스에서 코드 중복.
각 하위 클래스에서 동일한 필드나 메서드를 반복해서 작성하고 있다면, 그것들을 추상 클래스로 올려야 한다는 신호입니다. 중복이 줄어든다면 추상 범위를 “더 넓게” 가져가는 것을 두려워하지 마세요.
실수 № 2: ‘일반에서 구체로’ 원칙을 어김.
초보자는 종종 공통점을 잊고 세부부터 계층을 만들기 시작합니다. 그 결과 RedCircleWithShadow 같은, 전체 구조에 잘 맞지 않는 이상한 클래스가 생깁니다. 항상 먼저 추상을 도출하고, 그다음에 세부를 정하세요.
실수 № 3: 지나치게 깊은 계층 구조.
상속 체인이 3–4단계를 넘는다면, 상속 대신 합성(composition)이나 인터페이스를 사용하는 편이 나을 수 있습니다.
실수 № 4: 의미 없는 메서드의 강제 구현.
추상 클래스에 일부 하위 클래스에는 맞지 않는 추상 메서드가 너무 많다면 구조를 재고할 필요가 있습니다. 예를 들어, 모든 교통수단이 fuelUp() 메서드를 필요로 하는 것은 아닙니다(자전거에는 불필요합니다).
실수 № 5: 추상 클래스와 인터페이스를 혼동.
추상 클래스는 공통 상태 및/또는 부분 구현이 있을 때 사용합니다. 인터페이스는 데이터를 보관하거나 동작을 구현하지 않고 메서드의 존재만 “약속”할 때 사용합니다. 필요 없으면 이 둘을 섞지 마세요.
GO TO FULL VERSION