책/CleanCode

Clean Code - #6 객체와 자료 구조

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