Clean Code 읽기(10)
6장 객체와 자료구조
- 변수를 private으로 정의하는 이유는 남들이 변수에 의존하지 않게 만들고 싶어서 이다. 그렇다면 왜 많은 프로그래머들은 get함수와 set함수를 당연하게 public으로 선언하여 변수를 외부에 노출시킬까?
자료 추상화
-
구체적인 클래스와 추상적인 클래스를 보자
//목록 6-1 구체적인 Point 클래스 public class Point { public double x; public double y; } //목록 6-2 추상적인 Point 클래스 public interface Point { double getX(); double getY(); void setCartesian(double x, double y); double getR(); double getTheta(); double setPolar(double r, souble theta); }
- 목록 6-1은 구현을 노출한다. 변수를 private으로 선언했지만 get/set 메서드를 제공한다면 구현을 외부로 노출하는 셈이다.
- 목록 6-2는 클래스가 접근 정책을 강제한다. 좌표를 읽을 때(get)는 각 x,y값을 개별적으로 읽지만, 좌표를 설정할 때(set)는 두 값을 한꺼번에 설정한다.
-
변수 사이에 함수를 넣는다고 구현이 저절로 감춰지지 않는다. 구현을 감추려면 추상화가 필요하다.
-
추상 인터페이스를 제공하여 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 클래스이다.
-
구체적인 Vehicle 클래스와 추상적인 Vehicle 클래스를 보자
//목록 6-3 구체적인 Vehicle 클래스 public interface Vehicle { double getFuelTankCapacityInGallons(); double getGallonsOfGasoline(); } //목록 6-4 추상적인 Vehicle 클래스 public interface Vehicle { double getPercentFuelRemaining(); }
- 목록 6-3은 자동차 연료 상태를 구체적인 숫자 값으로 알려준다.
- 목록 6-4는 자동차 연료 상태를 백분율이라는 추상적인 개념으로 알려준다.
-
자료를 세세하게 공개하기보다는 추상적인 개념으로 표현하는 편이 좋다. 인터페이스나 get/set 메서드만으로는 추상화가 이뤄지지 않는다. 아무 생각 없이 get/set 함수를 추가하는 방법이 가장 나쁘다.
-
🙋♂️ 이해가 잘 안된다. 목록 6-1이랑 목록 6-2 차이가 크게 없는것 같은데, 그냥 get/set 메서드가 있고 없고 차이아닌가..
-
🙋♂️ 목록 6-3과 6-4는 그래도 이해가 되긴한다. 근데 충격적인것은 단순히 get/set 메서드를 추가하는게 나쁘다는것이다. 여태 어느 책이나 어느 블로그나 어느 강의를 봐도 get/set 메서드를 추가하곤 했는데, 내가 알고 있던 것이 전면 부정당하는 느낌이라 받아들이기 어렵다…
자료/객체 비대칭
-
객체는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다.
-
자료구조는 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다.
-
자료구조와 객체는 서로 상반된다
-
절차적인 도형 클래스를 보자
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 instance of Square) { Sqare s = (Square) shape; return s.side * s.side; } else if (shape instance of Rectangle){ Rectangle r = (Rectangle) shape; return r.height * r.width; } else if (shape instance of Circle){ Circle c = (Circle) shape; return PI * c.radius * c.radius; } throws new NoSuchShapeException(); } }
- 각 도형 클래스는 아무 메서드도 제공하지 않는 간단한 자료구조이다. 도형이 동작하는 방식은 Geometry에서 구현한다.
- Geometry에 둘레 길이를 구하는 메서드를 추가하더라도 도형 클래스들은 아무 영향도 받지 않는다. 새 도형 클래스를 추가한다면 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; } } publi class Circle implements Shape { private Point center; private double radius; private final double PI = 3.141592653589793; public double area() { return PI * radius * radius; } }
- area()는 다형 메서드이다. 절차적인 도형 클래스와 다르게 Geometry 클래스가 필요없다. 새 도형을 추가해도 기존 함수에 아무 영향을 미치지 않는다. 새 함수를 추가하고 싶다면 도형 클래스 전부 수정해야한다.
-
다시 말하면
새 함수 추가 새 클래스 추가 절차적인 코드(자료구조) 기존 자료구조 변경없이 가능 추가하려면 모든 함수 수정 객체지향 코드 추가하려면 모든 클래스를 수정 기존 함수 변경없이 가능 - 객체 지향 코드에서 변경이 어려운 코드는 절차적인 코드에서 쉬우며, 절차적인 코드에서 변경이 어려운 코드는 객체 지향 코드에서 쉽다.
- 모든 것이 객체라는 생각은 미신이다. 때로는 단순한 자료 구조와 절차적인 코드가 가장 적합한 상황도 있다.
- 🙋♂️ 다형성에 대해 객체지향 수업에서 배운적이 있다. 그래서 단순히 개념만 알고있었다. 그런데 과제 등을 하며 코드를 짤때 왜 이렇게까지 하는지 이해가 안됐다. 지금은 여러 코드를 보니까 확실히 다형성이 가지는 강점이 보이긴 하는데.. 이걸 어떻게 적용해볼 수 있을까?? 느낌은 있는데 많이 써보질 않아서 어색한점이 있다.
- 🙋♂️ 자바 웹 프로그래밍 Next Step 공부를 하며 MVC 패턴중 Controller를 직접 구현하는 실습이 있었는데 확실히 구현할때 하나의 클래스를 수정하려고 모든 코드를 다 볼 필요가 없다는 강점을 느낄 수 있었다.
디미터 법칙
- 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이 디미터 법칙이다.
- 객체는 자료를 숨기고 함수를 공개한다. 즉, get함수로 내부 구조를 공개하면 안된다는 의미이다.
-
🙋♂️ 이거 보면서 의아함을 느낀게, 보통 프로젝트할때 DTO클래스(User, Point 등..)마다 get/set 메서드를 만들지않나? 심지어 편하게 구현하려고 lombok이라는 라이브러리도있다.
- “클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야한다”
- 클래스 C
- f가 새성한 객체
- f의 인수로 넘어온 객체
- C 인스턴스 변수에 저장된 객체
-
기차 충돌
-
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath()
같이 객차가 한줄로 이어진 기차처럼 보이는 조잡한 코드를 기차 충돌이라고 한다.Option opts = ctxt.getOptions(); File scratchDir = opts.getScratchDir(); final String outputDir = scratchDir.getAbsolutePath();
- 조잡한 코드를 나누는것이 좋다. 그런데 ctxt객체가 Options을 포함하며 Options가 ScratchFile을 포함하고, ScratchFile은 또 Absolutepath를 포함한다. 함수 하나가 아는 지식이 매우 많다.
- 위 클래스가 객체라면 내부 구조를 숨겨야하는데 get함수로 다 노출시키고 있다. 이는 디미터 법칙을 위반한다.
- 위 클래스가 자료구조라면 당연히 내부 구조를 노출하기 때문에 괜찮다.
- 자료구조라면 get메서드를 사용하지않고 public 변수로 바꿔 바로 표현하는것이 좋다.
-
-
잡종 구조
- 위 같은 문제로 인해 때때로 절반은 객체, 절반은 자료구조인 잡종구조가 발생한다.
- 잡종 구조는 중요한 기능을 수행하는 함수도 있고, public 변수나 public get/set 함수도 있다.
- 이런 잡종 구조는 새로운 함수, 새로운 자료구조도 추가하기 어렵다. 양쪽 단점만 모아놓은 구조이다.
- 🙋♂️ 가장 잘 모르겠는 구조이다. 실제로 대부분 자료 구조의 필드를 private으로 선언하고 public get/set 메서드를 구현하는 형태인데….?
-
구조체 감추기
- 위의 ctxt, options, scratchDir이 객체라면 기차 충돌 형태의 코드를 작성하면 안된다. 내부 구조를 감춰야하기 때문이다.
-
ctxt가 객체라면 객체에게 뭔가를 하라고 해야지, 가지고 있는 필드를 드러내라고 하면 안된다.
-
코드 아랫부분을 보면 scratchDirectory의 절대경로를 얻으려는 이유가 임시 파일을 생성하기 때문이다.
String outFile = outputDir = "/" + className.replace('.', '/') + ".class"; FileOutputStream fout = new FileOutputStream(outFile); BufferedOutputStream bos = new BufferedOutputStream(fout);
-
그렇다면 get메서드를 통해 scratchDirectory의 경로를 얻지 말고 객체에게 뭔가를 하라고 해야하므로 ctxt 객체에게 임시 파일을 만들라고 시키는 것이 더 좋다.
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
-
이렇게 하면 ctxt는 내부 구조를 노출시키지 않고 모듈에서는 여러 객체를 탐색할 필요가 없다.
- 🙋♂️ 확실히 추상화 시키는 방법이 이해가 된다. 전에는 추상화하는게 뭔지 잘 몰랐는데 책을 계속 읽다보니 이해가 된다. 여기서 적용하는것만 실습을 통해 익히면 내것으로 만들 수 있을것 같다.
자료 전달 객체 (DTO)
- 자료 구조체의 전형적인 형태는 public 변수만 있고 함수가 없는 클래스이며 이런 자료 구조체를 때로는 DTO(Data Transfer Object)라고 한다.
- DTO는 데이터베이스와 통신하거나 소켓에서 받은 메세지의 구문을 분석할 때 유용하다. 흔히 데이터베이스에 저장된 가공되지 않은 정보를 애플리케이션 코드에서 사용할 객체로 변환하는 일련의 단계에서 가장 처음 사용하는 구조체이다.
-
활성 레코드
public class Address { private String street; private String streetExtra; private String city; private String state; private String zip; public Address(String street, String streetExtra, String city, String state, String zip) { this.street = street; this.streetExtra = streetExtra; this.city = city; this.state = state; this.zip = zip; } public String getStreet() {return street;} public String getStreetExtra() {return streetExtra;} public String getCity() {return city;} public String getState() {return state;} public String getZip() {return zip;} }
- 활성 레코드는 DTO의 특수한 형태이다. public 변수가 있거나 private 변수에 get/set 메서드가 있는 자료 구조이다.
- 활성 레코드는 데이터베이스 테이블이나 다른 소스에서 자료를 직접 변환한 결과이다.
- 활성 레코드에 비즈니스 로직 메서드를 추가하여 자료 구조를 객체로 취급하는 개발자가 많다. 활성 레코드는 자료구조로 취급한다. 비즈니스 로직을 담으면서 내부 자료를 숨기는 객체는 따로 생성한다.
- 🙋♂️ 위에서도 말했지만 이런 구조를 흔히 본다. 객체로 취급하는 개발자가 많다가 아니라 거의 다 이러지않나? 뭐.. 내가 현업 프로젝트 코드를 보지못해서 잘 모르겠는데, 아마 대학생 수준에서는 이런식으로 구현 안하는 사람이 없을텐데.. 그래서 더 어렵다.
댓글남기기