코로나는 전염병이다!...그렇다.
객체와 자료 구조라길래 자료구조를 만드는 법에 대한 내용일 줄 알았는데 전혀 아니었다. 일단 개인적으로 생각하기에 자료 구조라는 말 자체가 엄청나게 흔히 사용되는 용어인데, 여기서는 아예 일반적인 Java 개발자들이 흔히 만드는 클래스들 자체도 자료 구조라는 말을 썼다. 아마 C 언어의 struct 같은 개념을 의미하는게 아닐까?
여기서 이야기하는 건 "struct 처럼 데이터만 모아두는 단위를 기피할것"이라는게 아니었다. Java 개발자들이 흔히 사용하는 Bean 패턴의 무용성, public을 너무 멀리할 필요 없다는 것, 그리고 최종적으로 class와 그 인스턴스를 활용하는 방법을 생각하면서 객체를 만들라는 이야기이다.
그리고 여태까지 챕터 중 가장 짧다
어째서 수많은 프로그래머가 조회(getter) 함수와 설정(setter) 함수를 당연하게 공개(public)해 비공개 변수를 외부에 노출할까?
Java 개발을 배우면서 흔히 private을 사용하는 이유는 변수를 외부에 노출하지 않아 캡슐화를 달성하기 위해서라고 배운다.
getter와 setter를 만든다.Java의 interface는 구현하는 구상 클래스가 반드시 특정 메서드를 가지고 있을거라 강제하며, interface를 반환 값으로 가지고 있으면 실제 반환되는 구상 클래스 자체를 감출 수 있다. 그럼에도 정확하게 원하는데로 동작할 것을 기대하는 것이 가능하다.
객체는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다.
자료 구조는 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다.
둘은 본질적으로 상반된 개념이다.
Square, Rectangle, Circle 등의 클래스를 구현한 뒤 그들의 너비를 구하는 Geometry 클래스를 구현한다면 너비를 구하는 area(Object shape) 메서드는 절차적인 형태를 보일 수 밖에 없다.
public class Square {
// 정사각형을 표현하는 변수들
}
public class Rectangle {
// 직사각형을 표현하는 변수들
}
public class Circle {
// 원을 표현하는 변수들
}
// ...
public class Geometry {
double area(Object shape) {
// 제어문으로 실행할 코드를 조정한다.
if (shape instanceof Square) {
// ...
}
else if (shape instanceof Rectangle) {
// ...
}
else if (shape instanceof Circle) {
// ...
}
}
}
반대로 Shape이라고 하는 interface를 만들고, area라는 추상 메서드를 만든 다음, 각 클래스가 그들을 구현하게 만든다면, 각 클래스 안에 너비를 구현하는 방식이 정의된다.
public class Square implements Shape {
// 정사각형을 표현하는 변수들
public double area() {
// 정사각형의 너비를 구하는 코드
}
}
public class Rectangle implements Shape {
// 직사각형을 표현하는 변수들
public double area() {
// 직사각형의 너비를 구하는 코드
}
}
public class Circle implements Shape {
// 원을 표현하는 변수들
public double area() {
// 원의 너비를 구하는 코드
}
}
Java를 공부하면 당연히 후자의 형태로 구현을 많이 하게 된다. 다만, 이것이 항상 모든 상황에서 정답인것은 아니다. 대표적으로 각 도형의 지름을 구하는 클래스를 만들고 싶다면,
Geometry 클래스에 메서드를 추가하면 된다.Shape 인터페이스에 메서드를 추가한다.Shape의 구현체들에 전부 그 메서드를 추가해 주어야 한다.즉 객체지향적 방법이 오히려 더 많은 수정을 불러일으키게 된다는 뜻이다.
반대로 객체지향적 방법에서는 새로운 도형을 추가하는 것은 헐씬 쉽다. 새로 만든 Shape의 구현체는 다른 모든 Shape의 구현체와 전혀 상관이 없으니까.
다시말해, 객체 지향 코드에서 어려운 변경은 절차적인 코드에서 쉬우며, 절차적인 코드에서 어려운 변경은 객체 지향 코드에서 쉽다!
모든 것이 객체라는 생각은 미신이다. 때로는 단순한 자료 구조와 절차적인 코드가 가장 적합한 상황도 있다. 맨날 class 만들고 시작하면 피곤하다
모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다.
어떤 메서드 내부에서 다음과 같은 코드가 있다고 가정하면,
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
이는 메서드가 ctxt의 객체가 Options라는 객체를 가지고 있으며, Options는 ScratchDir를, ScratchDir는 absolutePath라는 문자열을 가지고 있다는 것을 사전에 알아야 한다. 만약 ctxt가 단순히 데이터를 저장하기 위한 자료 구조라면 아는것이 문제가 아니지만, 객체라면 디미터 법칙을 위반하는 사항이다.
차라리 public으로 만들면 디미터 법칙이 언급도 되지 않는다.
final String outputDir = ctxt.options.scratchDir.absolutePath;
어차피 ctxt는 그런 데이터를 간편히 담아두기 위한 자료 구조이기 때문에.
하지만 Java 진영에 오랫동안 퍼져있던 Bean 패턴(Spring의 Bean과는 전혀 다르다)은 이런 가능성을 말소시키고 불편한 getter, setter의 사용을 표준화 해버렸다.
만약 각각이 진짜 객체였다면, 내부 구조를 감추지 못한 것이기 때문에 문제이다. 이를 해결하기 위해 목적을 찾아서 적당한 메서드를 만들어 주어야 한다. 책의 예시는 임시 파일을 만들기 위해서기 때문에, 다음처럼 변경해준다.
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
ctxt의 내부는 드러나지 않으며 ctxt를 사용하는 입장에서도 내부 구조를 알 필요 없다.
공개 변수만 있고 함수가 없는 Data Transfer Object(DTO)들을 부른다. 근데 앞서 이야기한 데로 Bean 패턴의 형태로 만들어진 경우도 빈번하다.
특수한 형태의 DTO로서, 자체로 데이터라기 보다는 데이터베이스에 직접적으로 연결되어 자료를 반환해주는 특수한 형태이다. JPA의 Entity 같은 거려나?
활성 레코드도 자료 구조이기 때문에 비즈니스 규칙을 추가하는 것은 바람직하지 않다.
시스템을 구현할 때, 새로운 자료 타입을 추가하는 유연성이 필요하면 객체가 더 적합하다.
새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 더 적합하다.
일전에 어디서 Google은 private을 그냥 아예 쓰지 않는것도 괜찮다고 하는 것을 본적이 있다. Clean Code는 Java 개발자들이 무조건 읽어야 한다고 이야기 하는 필독서에 가깝다고 보며, 여기에도 Google과 비슷한 이야기가 있었다는 점에서 놀랐다. 비슷한 이야기를 하는게 놀랍다는게 아니라 지켜지는 경우를 찾기 힘들다는게 좀 놀라웠다.
Java Bean은 굉장히 흔한 형태이며, 지금도 DTO는 Java Bean으로 만드는 사람이 많을 것이다. 나부터도 그렇다. 처음 학교에서 캡슐화에 대해서 들을 때부터 Getter Setter는 private을 쓰기 귀찮은 public으로 만드는게 아닌가 궁금했는데, 군대가기 전의 나는 그 질문을 하지 못했고, 여전히 관성적으로 이렇게 DTO를 만든다.
미래에도 여전히 사람들은 Java Bean 패턴을 사용하게 될까? 어쩌면 그것은 앞선 장의 팀 규칙의 일환일수도 있다. 하지만 어느날 누군가는 여태까지 해온 행위가 실제로는 매우 비효율적인 관습적 행위라는 이야기를 하게 될지도 모른다. 그날이 오든 오지 않든, 적어도 변화해야 하는 순간에 잘 적응할 수 있도록 대비하는 것이 좋을것 같다.