-
0. 들어가며이번 아이템은 상속을 활용해야하는 이유에 대한 내용을 담고있습니다. 아래에서 천천히 알아보겠습니다. 1. 태그 달린 클래스아이템의 제목에서도 그렇고, 책에서도 그 의미가 궁금해졌습니다. 1.1 태그란?저는 태그란 클래스를 더 세분화 시키기 위한 필드/상태라고 이해했습니다. 특정 클래스에 이런 상태를 붙여서 더 세분화된 것이 제가 친구들에게 “얼음이 녹으면?” 이라는 질문을 합니다. 그러면 친구들은 다음과 같이 대답합니다.
그러면 저는 친구라는 클래스에 질문을 했지만, 문과인지 이과인지에 따라 다른 결과가 나올 것 입니다. 즉, 친구는 세분화된 특징에 따라 다르게 동작한 것이죠. 이 내용은 아래 예제 코드를 보면 더 쉽게 이해할 수 있습니다. (추가 설명을 위해 책의 예제와는 조금 다르게 작성했습니다.) class Shape {
private enum ShapeType { TRIANGLE, RECTANGLE }
private ShapeType shapeType;
private double width;
private double height;
public static Shape createTriangle(double width, double height) {
Shape triangle = new Shape();
triangle.shapeType = ShapeType.TRIANGLE;
triangle.width = width;
triangle.height = height;
return triangle;
}
public static Shape createRectangle(double width, double height) {
Shape rectangle = new Shape();
rectangle.shapeType = ShapeType.RECTANGLE;
rectangle.width = width;
rectangle.height = height;
return rectangle;
}
// 넓이 계산
public double area() {
switch (this.shapeType) {
case TRIANGLE: return (width * height) / 2;
case RECTANGLE: return width * height;
default:
throw new IllegalStateException("지원하지 않는 모양입니다.");
}
}
} Shape 이라는 클래스는 도형 타입, 정보, 계산기능을 갖고있습니다. 하지만 이 코드가 좋은 코드가 아닐 확률이 아주 높습니다. 그 이유는 객체지향 설계 원칙의 SRP, OCP 를 위반했기 때문입니다.
그러면 이 둘을 만족하는 구조로 가기 위해서는 어떻게 해야할까요. 책에서 제시하는 답은 1.2 상속을 해야하는 이유위의 기존 소스코드는 정리하면 다음과 같은 문제가 있었습니다.
책에서는 이를 해결하기 위한 방법인 그런데 상속을 하기 위해서는 계층구조를 설계해야합니다. 그래서 우리는 위에서 도출해 낼 수 있는 3가지 개념(Shape, Triangle, Rectangle)의 관계를 정의해야 합니다. 그리고 이 관계를 바탕으로 계층 구조를 설계하고, 이를 코드로 옮기면 되는것이죠. 각 개념들은 다음과 같이 관계를 정리할 수 있습니다. 그리고 이를 소스 코드로 옮기게 되면 다음과 같습니다. abstract class AbstractShape {
protected final double width;
protected final double height;
public AbstractShape(double width, double height) {
this.width = width;
this.height = height;
}
public abstract double area();
}
class Triangle extends AbstractShape {
public Triangle(double width, double height) {
super(width, height);
}
@Override
public double area() {
return (width * height) / 2;
}
}
class Rectangle extends AbstractShape {
public Rectangle(double width, double height) {
super(width, height);
}
@Override
public double area() {
return width * height;
}
} 공통된 부분을 추상화한 상위 개념(AbstractShape)에 선언하고, 하위 개념에서 구현하는 형태로 되었습니다. 이러한 구조는 다음과 같은 장점이 있습니다.
그리고 이전 코드는 SRP, OCP 를 위반했지만 이 코드는 이 두 원칙을 지키고 있습니다.
1.3 상속을 하지 말아야 하는 이유책에서는 1.2 까지의 내용으로 끝나지만, 상속에 대해 조금 더 짚어봐야할 얘기들이 있습니다. 그것은 바로 상속이 만능은 아니라는 것 입니다. 관련된 내용은 [아이템 18번](https://github.com/Study-2-Effective-Java/Effective-Java/discussions/38)에서 상속이 야기하는 문제상황들을 통해 확인할 수 있었는데요. 그러면 어떤 경우에 상속을 하지 말아야 할까요? 1) 코드 중복 제거이번 예제처럼 코드 중복을 제거하기 위한 상속 구조는 지양하는것이 좋습니다. 위 예제에서의 클래스들을 간단히 정리해보면 다음과 같습니다.
Triangle, Rectangle 에서 선언되어 사용되던 중복된 두 필드 선언을 AbstractShape 으로 옮겼었습니다. 이 덕분에 Triangle, Rectangle 은 ‘중복된 코드가 제거되어’ 더 간결하고 깔끔한 코드가 되었는데요. 하지만 중복 제거를 위해 상속을 이용한 경우에는 더 큰 문제를 야기할 수 있습니다. 2) 새 클래스 추가Circle 이라는 새로운 클래스가 필요해졌습니다. 이 클래스는 width, height 는 사용하지 않고, 대신 radius 라는 필드를 필요로 합니다. 이렇게 변하게 된 AbstractShape, Circle 의 코드는 다음과 같습니다. abstract class AbstractShape {
// 더 이상 불변 객체가 아님
protected double width;
protected double height;
protected double radius;
// 상속을 사용하기 전과 똑같은 구조의 생성자
public AbstractShape(double radius) {
this.radius = radius;
}
// 상속을 사용하기 전과 똑같은 구조의 생성자
public AbstractShape(double width, double height) {
this.width = width;
this.height = height;
}
public abstract double area();
}
class Circle extends AbstractShape {
// 여기서는 radius 만 필요하지만 불필요한 필드인 width, height 모두 접근 가능함
public Circle(double radius) {
super(radius);
}
public double area() {
return Math.PI * (radius * radius);
}
} 단지 Circle 클래스만 추가하고 싶었을 뿐인데, 이전의 동일한 문제들이 다시 생겨났습니다. 그래서 이를 해결하기 위해서 다시 코드를 수정합니다. abstract class AbstractShape {
public abstract double area();
}
class Triangle extends AbstractShape {
private final double width;
private final double height;
public Triangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return (width * height) / 2;
}
}
class Rectangle extends AbstractShape {
private final double width;
private final double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
class Circle extends AbstractShape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * (radius * radius);
}
} 이렇게 각자 필요한 필드를 관리하고 공통된 기능(area)을 구현하는데 신경을 쓰면 됩니다. 모든 클래스들은 불변이며 SRP, OCP 를 지키는 좋은 구조가 되었습니다. 그리고 여기서 한단계 더 나아간다면, 다음과 같이 변경할 수 있습니다. interface Area {
double area();
}
class Triangle implements Area {
...
}
class Rectangle implements Area {
...
}
class Circle implements Area {
...
} Shape 를 Area 라고 변경하면, 다중상속을 통해 다른 기능도 추가할 수 있는 유연한 구조가 가능해 집니다.
|
Beta Was this translation helpful? Give feedback.
Replies: 2 comments
-
오... 1.3 보고 좀 놀랬습니다. 해당 내용이 없었으면 댓글로 적었을 것 같아요. 단순히 책 내용의 활자를 읽고 정리하는 건 누구나 할 수 있습니다(물론 이펙티브 자바는 누구나 할 수 있는 건 아닙니다만). 저도 해당 아이템을 읽으면서 자꾸만 전 아이템들이 오버랩되었고(e.g. #40), 나아가 전후 흐름과 같이 이해할 수 있으면 좋겠다는 생각이 들었습니다. P.S. 예시 코드에서 |
Beta Was this translation helpful? Give feedback.
-
책에서는 태그 등 추상적인 설명들이 있어 이해가 쉽지 않았는데, |
Beta Was this translation helpful? Give feedback.
오... 1.3 보고 좀 놀랬습니다. 해당 내용이 없었으면 댓글로 적었을 것 같아요.
단순히 책 내용의 활자를 읽고 정리하는 건 누구나 할 수 있습니다(물론 이펙티브 자바는 누구나 할 수 있는 건 아닙니다만).
하지만, 책에서 언급하지 않은 내용을 확장하거나 숨은 의미를 찾아내는 과정은 고되지만, 나의 사고를 더욱 유연하게 만들 수 있는 계기가 되지요.
저도 해당 아이템을 읽으면서 자꾸만 전 아이템들이 오버랩되었고(e.g. #40), 나아가 전후 흐름과 같이 이해할 수 있으면 좋겠다는 생각이 들었습니다.
내용 잘 봤습니다~!
P.S. 예시 코드에서
Triangle
과Rectangle
의 필드 값도 어떻게 보면 중복이라고 보고,여기에 추상 골격 구현을 적절하게 활용하면 더 좋은 개선점이 있을 거라고 생각만 했습니다 ㅎㅎ