카테고리 없음

3.4 충성스러우면서 불변이거나, 아니면 상수이거나 ~ 3.6 부 ctor 밖에서는 new 를 사용하지마세요

1son 2024. 5. 17. 09:33

3.4 충성스러우면서 불변이거나, 아니면 상수이거나

3.4 충성스러우면서 불변이거나, 아니면 상수이거나

 

세상은 가변적이지만, 그렇다고 불변 객체로 세상을 모델링할 수 없는 것은 아님

- 혼란스러운 이유는 '상태(state)와 데이터(data)'에 관해 오해하고 있기 때문

 

예제 

class WebPage {
     private final URI uri;
     
     WebPage(URI path) { 
    	this.uri = path;
 	}
 
     public String content(){
         // HTTP GET 요청을 전송해서,
         // 웹 페이지의 컨텐츠를 읽은 후,
         // 읽혀진 컨텐츠를 UTF-8 문자열로 변환한다
        }
 }

 

WebPage는 가변일까 불변일까?

- content()메서드를 호출할 때마다 서로 다른 값이 반환되더라도 이 객체는 불변임

=> 핵심은 메서드의 반환값이 아닌, 객체가 살아있는 동안 상태가 변하지 않는다는 사실 

( 이 사실로 불변 객체의 개념을 혼동하게 함)

 

 


 

 

사람들은 불변 객체의 메서드를 호출할 때마다 상수(constant)처럼 매번 동일한 데이터가 반환되것이라 기대

- 실제로도 Java를 포함 언어들에서 String, URI, Double 등 대부분의 불변 클래스는 상수처럼 동작

- 이 클래스들로부터 인스턴스를 생성 하고나면, 생성된 객체의 모든 메서드는 항상 동일한 값을 반환

=> 객체의 행동을 100% 예측 가능 

 

 

하지만 이런 사고 방식은 불완전함

상수처럼 동작하는 객체는, 불변성의 특별한 경우(corner case)일 뿐이기 때문임 

 

사실 불변 객체는 그 이상임 

앞서 봤던 WebPage는 불변 객체에 속하는데 

우리는 content() 메서드가 무엇을 수신할 지 예상 불가 => 이런 측면에서는 WebPage가 String 클래스와 다름 

하지만 WebPage 역시 String 처럼 불변임! ! 

 

왜? => 객체가 대표하는 엔티티에 '충성하기(loyal)'때문에 불변 객체로 분류됨 

 

 

지금까지 설명이 어려웠을 수 있음 

객체상태의 정의부터 보자 

 

객체란...?

: 디스크에 있는 파일, 웹 페이지, 바이트 배열, 해시맵, 달력의 월과 같은 실제 엔티티 (real-life entity)의 대표자(representative)

 

예를 들어 다음 코드에서 객체 f는 디스크에 저장되어 있는 파일을 대표함 

public void echo() {
     File f = new File("tmp/test.txt");
     System.out.println("File size: %d", file.length());
 }

 

여기에서 f의 가시성 범위는 echo() 메서드의 ‘경계’에 대응합니다.

예제에서는 디스크에 저장된 파일의 사이즈를 알아내기 위해

객체 f의 length() 메서드와 의사소통하고 있습니다.

 

코드에서 객체 f는 /tmp/test.txt’ 파일의 대표자입니다.

객체 f는 실제 파일의 입장을 대변합니다.

우리의 관점에서 객체 f는 echo() 메서드 안에서만큼은 파일임

 

디스크에 저장된 파일을 다루기 위해 객체는 파일의 좌표(coordinates)를 알아야 함

이 좌표 를 다른 말로 객체의 상태(state)라고 부름

=> 예를 들어, WebPage 객체의 상태는 페이지 URI

 

 

File 클래스의 상태는 파일 시스템 상에 위치한 파일 의 전체 경로

=> 따라서 앞의 예제에서 객체 f의 상태는 /tmp/test.txt

 

.....

 

불변 객체는 실제 객체가 어디에 존재하고 어떤 방식으로 사용해야 하는지 알고 있음 

 

 

....

앗 ! 겁나어렵당! 

 

 

 

 

 

3.5 절대 getter와 setter를 사용하지 마세요 

 

getter와 setter의 형태는 다음과 같음 

 class Cash {
     private int dollars;
     public int getDollarsO { 
    	return this.dollars;
 }
     public void setDollars(int value) { 
    	this.dollars = value;
 	} 
}

 

Cash는 가변(mutable) 클래스

-  getter인 getDollars()를 이용해 노출

- setter인 setDollars()를 이용해 변경 가능

 -- 변경 가능한 하나의 private 프로퍼티 dollars를 포함

 

이 클래스는 이미 앞에서 말했던 원칙들, 

- 모든 클래스는 불변 

- 메서드 이름 짓는 법

- 생성자 필수 

이런 원칙들에 위배중

 

사실 가장 큰 일은

=> Cash가 진짜 클래스가 아니라 단순한 자료구조(data structure) 라는 사실

 

 

 

3.5.1 객체 대 자료구조 

 

객체와 자료구조의 차이점 ? 

자료구조가 OOP에 해로운 이유?

 

//c로 구현한 자료구조 
struct Cash {
	int dollars;
}


//(자료구조와 비슷하지만) C++의 객체
 #include <string> 
class Cash { 
public:
     Cash(int v): dollars(v) {};
     std::string print() const;
 private:
 	int dollars;
 };

 

이 둘의 차이점은?

 

 // C에서 Cash 자료구조를 사용하는 방법
printf("Cash value is %d", cash.dollars);

// C++에서 Cash 클래스의 객체를 이용해 동일한 작업 수행
printf("Cash value is %s’\ cash.print());

 

struct의 경우 

- 멤버인 dollars에 직접 접근 후 해당 값을 정수로 취급

- struct를 가지고는 어떤 일도 하지 않음 

- struct와는 아무런 의사소통도 하지 않고 직접적으로 멤버에 접근 -> struct는 단순한 데이터 가방(data bag) 일 뿐

 

클래스의 경우

- 멤버에게 접근하는 것을 허용하지 않음

- 멤버 노출도 안함

=> 캡슐화, OOP가 지향하는 원칙 

 

 

자료구조 

- 투명_글래스 박스 

- 수동적

- 죽어있음 

 

객체 

- 불투명_ 블랙 박스 

- 능동적 

- 살아있음 

 

이제 객체가 자료구조보다 좋다고 가정한 채 getter와 setter에 관해 얘기해봄 

그래, 이제 객체가 좋은 건 알겠어 근데 

단순히 몇 개의 멤버만을 포함하는 단순한 자료구조만 필요한 경우에도

행동, 식별자, 상태를 모두 포함하는 객체를 만들어야 하는 이유?

 

 

모든 프로그래밍 스타일의 핵심 목표는? 

: 가시성의 범위를 축소해서 사물을 단순화시키는 것

- 특정한 시점에 이해해야 하는 범위가 작을수록,

- 소프트 웨어의 유지보수성 향상/ 이해, 수정 쉬워

 

 

(코드가 데이터를 조작하는) 절차적인 프로그래밍과 명령형 프로그래밍 세계에서

사물을 단순화 시키는 가장 좋은 방법은?

- 서브루틴과 데이터 집합을 사용하는 것 

- 수천 개의 문장을 한꺼번에 다루는 대신, 문장들을 따로 묶어 이름을 붙인 서브루틴 사용

- 수백 바이트를 한꺼번에 다루는 대신 배열이나 자료구조로 묶어 하나의 포인터로 참조 가능

 

 

3.5.2 좋은 의도, 나쁜 결과

 

근본적으로 getter와 setter는 캡슐화 원칙을 위반하기 위해 설계됨 

 

Java에서 getter와 setter는 클래스자료구조로 바꾸기 위해 도입된 것임 

- Java에서는 수동적인 자료구조를 만들기 위해 getter와 setter가 필요했음 

- 물론 Java에서도 객체 프로퍼티를 public으로 만들면 클래스를 자료구조로 변환 가능

 

 

But, 이 방식은 OOP를 제대로 이해하지 못한 것

그래서 public을 추가해야 하는 상황을 피하기 위해,

 

private으로 변경하고 프로퍼티에 getter와 setter를 추가함 

=> getter와 setter를 이용해 private 프로퍼티를 감싸는 방식으로 

 

 

요점 : getter와 setter를 사용하면 OOP의 캡슐화 원칙을 손쉽게 위반 가능하며,

이때 데이터는 무방비로 노출되어 있는 것과 같음 

 

 

 

3.5.3 접두사에 관한 모든 것 

 

getter/setter 패턴에서 유해한 부분은 두 접두사인 get과 set 임 

 

getDollars() 

dollars() 

이 둘의 차이가 있음 

dollars()는 객체를 데이터의 저장소로 취급하지 않고, 객체를 존중함 

dollar()는 데이터를 노출하지 않지만, getDollars()는 데이터를 노출함

 

 

암튼 결론은 getter와 setter가 OOP에서 끔찍한 안티 패턴임 

 

 

 

3.6 부 ctor 밖에서는 new를 사용하지 마세요 

 

예제를 봅시다. 

 class Cash {
     private final int dollars;
     
     public int euro() {
         return new Exchange().rate("USD", "EUR") * this.dollars;
    }
 }

 

의존성에 문제가 있는 코드의 전형적인 모습을 잘 보여주고 있음 

- 메서드 안에서 new 연산자 이용해 Exchange 인스턴스 생성

 

문제 : 하드코딩된 의존성 

- Cash 클래스는 Exchange 클래스에 직접 연결되어 있음 

- 의존성을 끊기 위해선 Cash 클래스 의 내부 코드 변경 필요 

 

 

사용 코드 

 Cash five = new Cash("5.00");
 print("$5 equals to %d", five.euro());

 

 

문제의 근본 원인 : new 연산자

- 예제에서 Cash가 Exchange의 인스턴스를 직접 생성

 

 

수정 후의 Cash 클래스 

 class Cash {
     private final int dollars; private final Exchange exchange;
     
     Cash(int value, Exchange exch) {
         this.dollars = value; 
         this.exchange = exch;
     }
     public int euro() {
     	return this.exchange.rate("USD", "EUR") * this.dollars;
     }	
 }
 
 
/*print 테스트 코드 */
Cash five = new Cash(5, new FakeExchange()); 
print("$5 equals to %d", five.euro());

 

Cash 클래스: 더이상 Exchange 인스턴스를 직접 생성 못 함

- ctor을 통해 제공된 Exchange와만 협력가능

- Cash 클래스는 더 이상 Exchange 클래스에 의존하지 않음

 

객체가 필요한 의존성을 직접 생성하는 대신, 우리가 ctor을 통해 의존성을 주입 (inject)함 

필요한 의존성 전체를 ctor을 통해 전 달받는다는 점에서 Cash의 설계는 매우 훌륭함 

 

어떤 객체라도 훌륭하게 설계할 간단한 규칙 하나 추천

- 부 ctor을 제외한 어떤 곳에서도 new를 사용하지 마세요.

- 객체들은 상호간 분리되고 테스트 용이성, 유지보수성 향상 가능