728x90
반응형
우리는 남들이 변수에 의존하지 않기 위해 변수를 비공개로 정의한다. (private)
그런데 왜 getter와 setter 를 당연히 public 으로 외부에 노출할까?
먼저 객체와 자료구조의 특징을 살펴보자
- 객체
- 동작을 공개하고 자료를 숨긴다.
- 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기 쉬운 반면, 기존 객체의 새 동작을 추가하기는 어렵다.
- 자료 구조
- 별다른 동작 없이 자료를 노출
- 기존 자료구조에 새 동작을 추가하기는 쉬우나, 기존 함수에 새로운 자료 구조를 추가하기는 어렵다.
우리는 사용자가 내부 구현 구조를 모른 채, 필요한 자료의 핵심만 조작할 수 있도록 클래스를 설계해야한다.
자료 추상화
- 객체의 변수 사이에 메서드를 넣는다고 구현을 외부로 숨길 수는 없다.
- 구현을 감추기 위해서는 추상화가 필요하다
- getter와 setter가 있다면 외부로 노출하는 셈이다 (어떻게 구현됬는지 알기 때문에)
Public class Point{ // 구현이 외부로 노출
public double X;
public double Y;
}
Public interface Point { // 구현을 완전히 숨김
double getX();
double getY();
void setCartesian(double x, double y);
double getR();
double getTheta();
void setPolar(double r, double theta);
}
- 자료를 세세히 공개하기보단, 추상적인 개념을 표현하도록 구상해야한다.
- 해당 예시처럼 interface , getter , setter 를 통해 추상화가 이뤄지진 않는다.
- 개발자는 객체가 포함하는 자료를 표현할 수 있는 가장 좋은 방법을 계속해서 구상할 것
자료 & 객체 비대칭
객체 - 추상화 뒤로 자료를 숨겨, 자료를 다루는 함수만 공개
자료구조 - 자료를 그대로 공개하며 별다른 함수를 제공하지 않음
서로 상반대의 개념으로 정의된다.
절차적 도형 클래스
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 double width;
}
public class Geometry { // 세가지 도형 클래스를 다루는 클래스
public final double PI = 3.141592653585793;
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
}
}
}
- 각 도형 클래스는 간단한 자료 구조 ( 메서드 X )
- 동작 방식을 외부 Geometry 클래스에서 구현
- 따라서 Geometry 클래스에서 메서드(동작) 추가시 -> 각 도형 클래스에서는 영향 X (결합도 ↓)
- 하지만 Geometry 클래스 새 도형 (자료) 가 추가된다면 -> 클래스 내 메서드를 모두 고쳐야 함
객체 지향적 도형 클래스
public class Square implements Shape {
public Point topLeft;
public double side;
public double area() {
return side*side;
}
}
public class Rectangle implements Shape {
public Point topLeft;
public double height;
public double width;
public double area() {
return height*width;
}
}
public class Circle implements Shape {
public Point center;
public double radius;
public double width;
public double area() {
return PI*radius*radius;
}
}
Shape Interface 를 각 도형마다 구현해야할 다형 메서드 : area() 를 통해 절차지향 외부에서 쓰이던 Geometry를 쓰지 않는 방식
- Shape을 구현하는 새로운 클래스가 생성되어도 기존 함수에는 아무런 변화 X
- 하지만 새로운 함수를 추가하고 싶다면 결국 Shape 내에 메서드를 만들고 이를 구현하는 도형 클래스 전부를 고쳐줘야한다.
결론
- 절차적 코드
자료구조를 사용하기 때문에 기존 자료 구조를 변경하지 않고, 해당 자료구조를 이용하는 새 함수를 추가하기 쉽다.
하지만 새로운 자료구조를 추가하기 위해서는 자료구조를 사용하는 모든 함수를 다 고쳐야 한다.
- 객체 지향 코드
기존 함수를 변경하지 않으면서 새로운 클래스를 추가하기 쉽다.
하지만 새로운 함수를 추가하기 위해서는 해당 함수를 사용해야할 클래스 모두를 고쳐야 한다.
이처럼 서로 정 반대의 특징과 장단점을 가지고 있다. 따라서 개발자는 상황에 따라 적절한 방식을 고려할 줄 알아야 한다.
디미터 법칙
모듈은 자신이 조작하는 객체 내부를 몰라야 한다.
객체
- 추상화를 통해 자료를 숨기고 함수를 공개한다.
따라서 객체는 getter 를 통해 섣불리 내부 구조를 공개하면 안된다.
디미터 법칙
클래스 C의 메서드 f1은 다음과 같은 객체의 메서드만 호출해야한다.
- Class C
- f1이 생성한 객체
- f 인수로 넘어온 객체
- C 인스턴스 변수에 저장된 객체
주의사항은 위에서 언급한 4가지의 허용된 메서드의 return 의 메서드를 호출해선 안된다!
예시를 통해 확인해보자
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
getOptions()가 반환하는 객체의 메서드, getScratchDir()가 반환하는 객체의 메서드, getAbsolutePath() ...
이처럼 반환되는 객체의 연속된 메서드를 호출하고 있다.
위와 같은 코드를 기차 충돌 이라고 부른다.
이 때 ctxt가 객체였기 때문에 디미터 법칙을 위반한 것이다.
만약 ctxt가 자료구조였다면 자료구조는 내부 구조를 당연시 노출해야하기 때문에 디미터 법칙에는 해당되지 않는다.
if Type(ctxt) == DataStructure
final String outputDir = ctxt.options.scratchDir.absolutePath;
이러한 기차충돌 문제를 어떻게 해결하는지 살펴보자
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
이처럼 각각의 반환되는 객체를 명시적으로 나눠주는 것이 좋다.
잡종 구조
- 절반은 객체, 절반은 자료 구조를 사용하는 구조
- 잡종 구조는 새로운 함수, 새로운 자료 구조도 추가하기 어려운 혼돈의 도가니탕
- 따라서 개발자는 되도록 잡종 구조를 피하는 것이 좋다.
자료 전달 객체 [ DTO ]
자료 구조체의 전형적인 형태 : 공개 변수만 있고 함수는 없는 클래스
CF) DAO / DTO / VO
DAO(Data Access Object)
- 데이터베이스의 data에 접근하기 위한 객체
- DataBase에 접근 하기 위한 로직 & 비지니스 로직을 분리하기 위해 사용
DTO(Data Transfer Object)
- 계층 간 데이터 교환을 하기 위해 사용하는 객체
- DTO는 로직을 가지지 않는 순수한 데이터 객체(getter & setter 만 가진 클래스)
- 유저가 입력한 데이터를 DB에 넣는 과정
유저가 자신의 브라우저에서 데이터를 입력하여 form에 있는 데이터를 DTO에 넣어서 전송
해당 DTO를 받은 서버가 DAO를 이용하여 데이터베이스로 데이터를 삽입.
VO
- VO(Value Object) 값 오브젝트로써 값을 위해 쓰임
- read-Only 특징(사용하는 도중에 변경 불가능하며 오직 읽기만 가능)
- DTO와 유사하지만 DTO는 setter를 가지고 있어 값이 변할 수 있습니다.
@Getter
public class AddressDTO {
private String street;
private String city;
private String state;
public Address(String street, String city, String state) {
this.street = street;
this.city = city;
this.state = state;
}
}
- Bean 구조 : 일반적인 DTO 형태
- private 변수를 조회/ 설정 함수를 통해 접근
- 전달 인자가 없는 생성자를 가지는 형태의 클래스
- public의 no-argument 생성자 (@NoArgsConstructor)
- POJO ( Plain Old Java Object)와 동일한 개념이라고 이해해도 됨
- 활성 레코드 : 특수한 DTO 형태
- 공개 변수가 있거나, 비공개 변수에 getter/setter 존재하는 자료구조
- save 나 find 메서드 존재
- 활성 레코드에 비즈니스 규칙 메서드를 추가하여 객체로 취급하면 잡종 구조 생성
- 따라서 비즈니스 규칙을 담는 객체는 따로 생성할 것
- 내부 자료는 활성 레코드의 인스턴스일 가능성이 높다.
class Person { //잡종 구조
private String name;
private String email;
public Person(String name, String email) {
this.name = name;
this.email = email;
}
...
public void sendEmail(){
...
}
}
class EmailSender{ // 비즈니스 규칙 분리
private Person receiver;
...
public void sendEmail() {
...
}
...
}
결론
시스템 구현시,
새로운 자료 타입을 추가하는 유연성이 필요하다 → 객체 선정
새로운 동작을 추가하는 유연성이 필요하다 → 자료구조 & 절차적 코드
각 상황에 따라 최적의 해결책을 선택할 줄 알아야한다.
728x90
반응형
'책 > CleanCode' 카테고리의 다른 글
CleanCode - 17.냄새와 휴리스틱 (0) | 2022.05.07 |
---|---|
CleanCode - Chapter 11. System (0) | 2022.03.14 |