개요
- 이번 장에서는 객체와 자료구조의 차이를 설명한다.
- 또한 변수를 비공개(private) 으로 정의하는 이유, 많은 프로그래머가 getter 와 setter 를 공개(public)해 비공개 변수를 외부로 노출하는 이유에 대해서 설명한다.
추상화
- 추상화란 무엇일까?
- 컴퓨터 과학에서 추상화는 복잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것을 말한다.
- 즉, 추상화란 자료에 대한 핵심적인 부분을 간추려 내는 것을 말한다.
- 추상화를 통해서 개발자(사용자)는 세세한 구현 부분을 몰라도 기능을 사용할 수 있게 하는 것이다.
자료 추상화
public class Point {
public double x;
public double y;
}
- 위의 코드를 보면 변수들이 public으로 되어있다. -> 그렇기 때문에 사용자는 해당 클래스에 대한 변수들을 전부 알 수 있다.
- 이것은 추상화에 위배되는 구현이다.
- 반면에 아래의 코드를 보자
public interface Point {
double getX();
double getY();
void setCartesian(double x, double y);
double getR();
double getTheta();
void setPolar(double r, double theta);
}
- 위의 코드는 Point 클래스를 추상화 했다고 볼 수 있다.
- 클래스 메서드가 접근을 강제해서, 공개나 변경을 원하는 자료 구조와 기능만을 간추려서 공개했다.
- 그렇다면 위의 코드는 좋은 추상화인가? -> 아니다 -> 각 값마다 getter 와 setter 를 제공한다면 이전과 마찬가지로 구현을 외부로 노출시키는 것과 같다.
- 다시말해 변수 사이에 함수라는 계층을 넣는다고 해서 구현이 저절로 감춰지진 않는다.
- 그렇기 때문에 그저 형식과 논리에 치우쳐 Getter 와 Setter 함수로 변수를 다룬다고 클래스가 되진 않는다.
- 핵심은, 추상 인터페이스를 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 클래스이다.
- 따라서 아래와 같이 위의 코드를 개선할 수 있다.
public interface Point {
void getCartesian(double x, double y);
void setCartesian(double x, double y);
}
- 위의 코드는 정보가 어디서 오는지 전혀 드러나지 않는다.
- 정리하면, 자료를 세세하게 공개하기보다는 추상적인 개념으로 표현하는 편이 좋다.
- 또한, 인터페이스나 조회/설정 함수만으로는 추상화가 이뤄지지 않는다.
자료/객체 비대칭
- 객체와 자료구조의 차이를 아는가?
- 객체는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만을 제공한다.
- 자료구조는 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다.
- 비교를 위해서 먼저 아래의 코드를 보자.
// 절차지향적인 코드
public class Square {
public Point topLeft;
public double side;
}
public class Rectangle {
public Point topLeft;
public double height;
public double width;
}
public class Circle {
public Point center;
public double radius;
}
public class Geometry {
public final double PI = 3.141592653589793;
public double area(Object shape) throws NoSuchShapeException {
if (shape instanceof Square) {
Square s = (Square)shape;
return s.side * s.side;
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle)shape;
return r.height * r.width;
} else if (shape instanceof Circle) {
Circle c = (Circle)shape;
return PI * c.radius * c.radius;
}
throw new NoSuchShapeException();
}
}
- 위의 코드는 3가지 도형을 절차 지향적인 코드이다.
- 도형의 동작은 Geometry 클래스 에서 구현한다.
- 객체 지향으로 개발하는 나와 같은 프로그래머는 이런 코드를 보고 비웃을 지도 모른다.
- 하지만 위와 같은 코드가 무조건 비웃음의 대상이 된다고 할 순 없다.
- 그 이유는 만약 도형의 동작이 추가된다고 했을때 도형클래스에는 수정할 필요가 없기 때문이다. -> 장점이다.
- 하지만, 도형을 추가할 때는 모든 함수를 수정해야 된다는 단점이 있다.
- 이번에는 위와 상반되는 객체 지향적인 코드를 보자.
public class Square implements Shape {
private Point topLeft;
private double side;
public double area() {
return side * side;
}
}
public class Rectangle implements Shape {
private Point topLeft;
private double height;
private double width;
public double area() {
return height * width;
}
}
public class Circle implements Shape {
private Point center;
private double radius;
public final double PI = 3.141592653589793;
public double area() {
return PI * radius * radius;
}
}
- 위의 코드는 Geometry 클래스는 존재하지 않는다. -> 다형성(다형 메소드)이 적용되었기 때문이다.
- 그렇기 때문에 새 도형을 추가해도 기존 함수에 영향을 미치지 않는다.
- 하지만, 새 함수를 추가할 때에는 도형 클래스를 전부 고쳐야 한다.
- 그렇다면, 절차적인 코드와 객체 지향적인 코드중 어떤것이 좋은가?
- 정리하자면 아래와 같다.
- 새로운 자료 타입은 잘 추가되지 않고, 새로운 함수가 자주 추가되는 경우엔 절차적인 코드가 유라하다.
- 새로운 함수보다는 새로운 자료 타입이 필요한 경우에는 객체지향적인 코드가 유리하다.
- 개발을 하다보면 객체지향적인 코드가 유리한 경우가 많으나, 모든 문제를 객체로 해결하려고 하는것은 바람직 하지 못하다.
- 적어도 차이를 알고 객체지향적으로 코딩하는 것과 무작정 객체지향적으로 코딩하는 것은 큰 차이가 있다.
디미터 법칙
- 디미터 법칙이란 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다.
- 최소 지식의 원칙이라고도 불린다.
- 결합도가 낮은 설계를 위한 법칙이다.
- 클래스 C의 메서드의 f라는 메서드는 다음과 같은 객체의 메서드만 호출할 수 있다.
- 클래스 C
- f가 생성한 객체
- f 인수로 넘어온 객체
- C 인스턴스 변수에 저장된 객체
- 다시 말해서, 낯선 사람은 경계하고 친구랑만 놀라는 의미이다.
- 즉, A가 B를 사용하고, B가 C를 사용한면, A는 C의 메소드는 호출할 수 있지만 C를 알 필요는 없다.
- 따라서 아래와 같은 코드는 좋지 않다.
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
- 하지만, 위의 코드도 객체의 입장에서는 디미터의 법칙을 지켰다고 할 수 없다. -> 내부의 정보를 너무 많이 노출하기 때문이다.
- 그렇지만, 자료구조의 측면에서는 디미터의 법칙을 지켰다고 할 수 있다.
- 그렇다면 위의 코드를 객체지향적인 측면에서 디미터의 법칙을 지키려면 어떻게 해야할까? -> 구조체를 감추면 된다.
- 객체라면 내부 구조를 감춰야 하니 때문에 아래와 같이 구현할 수 있다.
ctxt.getAbsolutePathOfScratchDirectoryOption();
- 하지만 ctxt 객체에 공개해야 하는 메서드가 너무 많아지기 때문에 좋은 방법은 아니다.
- 절대 경로를 얻으려는 이유는 임시 파일을 생성하기 위함이다.
- 그렇기 때문에 적절하게 classFileName 만들고, 아래와 같이 ctxt 객체에 임시 파일을 생성하라고 시키는 것이 낫다.
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
- 이렇게 되면, ctxt는 내부 구조를 드러내지 않는다.
- 또한, 모듈에서 해당 함수는 자신이 몰라야 하는 여러 객체를 탐색할 필요가 없다.
- 그렇기 때문에 디미터 법칙을 위반하지 않는다.
활성 레코드
- DTO의 특수한 형태이다.
- 공개 변수가 있거나 비공개 변수에 조회/설정 함수가 있는 자료구조이다.
- save나 find와 같은 탐색 함수도 제공한다.
- 하지만 save 나 find 와 같은 메소드가 포함되어 있다고 해서 다른 비즈니스 메서드를 추가해서 사용하는 것은 바람직하지 않다.
- 이런실수로 인해서 아래와 같이 잡종코드가 생겨난다.
class PersonDto {
private String name;
private String email;
public PersonDto(String name, String email) {
this.name = name;
this.email = email;
}
// save, find 메서드 생략
// 비즈니스 메서드
public void sendEmail(){
...
}
}
- 해결책은 활성 레코드를 자료 구조로 취급하는 것이다.
- 그 다음 비즈니스 규칙을 담으면서 내부 자료를 숨기는 객체는 따로 생성하면 된다.
class EmailSender{
private PersonDto personInfo;
public void sendEmail() {
//...
}
}
REFERENCES