가능하면 적게 캡슐화
- 객체는 함께 동작하는 객체들의 집합체 이기 때문에, 객체에는 캡슐화된 객체들이 있어야 한다 . 객체는 상태(내부의 객체)를 이용해 식별되어야 하는데, Java 에서는 껍질(==이나 Object 클래스의 기본적인 equals() 정의를 생각해보면 된다 ) 로 비교하고 있어 결함이 존재한다는 얘기를 하고 있다.
- 책에서 “상태로 객체를 식별" 한다는 것은 값객체에 대한 말인가? 엔티티와 값객체를 구분하면서 쓰여진 것인지 그 의도가 분명하지 않다는 생각이 들었다.
- 객체에 너무 많은 상태가 존재하는 것 보다는, 일부 상태를 캡슐화한 이루어져야 한다. 그 이유는 너무 많은 상태를 가진 객체는 인간의 인지적 한계로 이해하기 어렵기 때문이다. 상태를 객체들로 쪼개놓으면, 개별적인 객체들을 이해하기 쉬울 것으로 생각된다.
최소한 뭔가는 캡슐화하세요
어떤 것도 캡슐화하지 않고있으며 ( 상태 가 없는 ) 행동만을 포함하는 클래스는 , 결국 인스턴스 메소드가 정적 메소드 같은 역할을 하게 된다.
앞에서 말했던 것 처럼, 이런 클래스 조차도, 이 클래스로 생성한 인스턴스들은 상태가 없는 객체들임에도 Java의 == 연산자나 ,equals를 통해 각 객체들은 식별이 되어버린다. 이런 클래스의 객체는 세상에 오직 하나만 존재하는게 적절해 보이는데 개별적인 객체들로 존재하게 되어버린다.
개인적으로는, 객체라면 상태 즉 식별할 만한 정보를 가졌거나, 상태를 이용하여 이 객체만이 할 수 있는 행위가 있는 것이어야 한다고 생각한다.
항상 인터페이스를 사용하세요
인터페이스를 사용하면 느슨한 결합을 하게 된다.
사용자는 구현체를 알 필요 없고, 구현방법을 알 필요 없다. 또한 인터페이스를 통해 구현체는 서로 대체될 수 있다는 장점이 있다.
만약 구현체에서 인터페이스에 없는 메소드를 public 하게 제공한다면, 실수로라도 인터페이스 타입이 아닌 구현체에 직접 의존하고 있는 사용자는 후에 해당 객체를 다른 객체로 대체할 수가 없어진다. ( 이미 구현체 의존하는 것만으로도 강한 결합을 하고 있는데, 해당 구현체에만 정의된 메소드를 사용하고 있다면 최악일 듯 하다 )
( 인터페이스에 조차도 결합도를 갖지 않으려는 건 불가능 하다 )
메소드 이름을 신중하게 선택하라
책에서는 아래의 내용에 대해 계속 얘기하고 있는데, 솔직히 와닿지 않았다.
빌더는 “명사" , “어떤 것을 리턴”
조정자는 “동사", void 여야 한다.
이런거를 다 지키면서 네이밍을 하기는 어려울 것 같다.
하지만 적어도 네이밍을 하는데 있어 주의하라는 의미라고 생각된다.
그리고 생각났던 것은 Java 14 에 프리뷰 기능으로 처음으로 도입되었던 record type(클래스) 이다. 해당 클래스로 선언할 경우, 각각의 필드에 대하여우리가 흔히들 게터라고 말하는 메소드의 네임은 자동으로 “그 필드의 이름"이 된다. 이 책에서 말하는 “빌더는 명사" 라는 것에 적합한 예시가 아닌가 생각했다
빌더 패턴 사용을 한다는 것은 생성자의 인자가 너무 많다는 건데, 그러면 상태가 많다는 것 → 애초에 객체를 잘게 쪼개지 않은 것이 문제다.. 이런 얘기를 하고 있었는데, 반대로 객체를 너무 잘게 쪼개는 것도 너무 의미없는 객체를 만들게 되지 않을까? 라는 생각을 했다. 물론 책에서는 A(a1,a2,a3),B(b1,b2,b3) 이런식으로 쪼개라는 것 같긴한데.. 너무 쪼개다 보면 겉에 있는 객체를 이해하기가 점차 어려워질 것 같다.
또한 우리가 평소 사용하던 isEmpty() 와 같은 메소드는 empty() 이런 형용사로 쓰면서 is 를 붙이지 말라고 하는데, 오히려 형용사로만 사용할 경우 혼란이 가중된다고 생각되었다. 코드에서도 is를 prefix 로 쓰는게 합당하다고 생각된다.
Public constant 를 사용하지 마세요
Class Records {
public static final String EOL = "\\r\\n";
- 불변인 상수를 공유한다고 뭐가 문제냐??? 라는 생각도 했었던 과거의 나가 있다.. (하지만 앞으로도 그런 실수를 할 것 같다.습관의 무서움 )
어떤 문제가 발생하는가?
- 결합도가 높아지고 응집도가 낮아진다.
- 결합도 : 다른 클래스의 public 상수를 직접 사용 → 상수 클래스에 대한 결합도가 높아진다. 상수클래스의 변경에 영향을 받게 된다.
- 응집도 : 퍼블릭 상수를 사용하는 측에서는 상수를 사용하여 수행하고자 하는 어느정도 비슷한 기능들(ex_ 줄 또는 레코드의 끝에 EOL 을 어펜드하는 것 ) 까지 “추가로 수행"하고 있는 것이 된다. 본질적인 역할 외의 기능을 갖는 것 → 응집도가 떨어진다.
- 의미 있는 클래스로 분리&& 묶어서 사용하라 → 그러면 상수를 가져와 사용하던 객체들은 이제 “어떤 역할을 하는 객체(EOL객체)" 를 가져와서 사용하는 것이 된다. 훨씬 객체지향적이다. 이제는 EOL 과 관련된 모든 것은 EOL 객체에서 해줄 것이다. OS 가 달라서 다른 EOL 을 붙여줘야 한다면 해당 객체에서 또 알아서 해 줄 것이다. 더는 클라이언트들에서 public 상수에 대해 알 필요가 없다
- 즉, 데이터가 아닌! “기능을 공유" 하도록 하는 것 이다.
책에서는 코드 중복 문제 ( 각 클래스마다 상수를 선언) 를 해결하지 말고, 클래스를 분리하라고 하고 있으면서 “Java 의 열거형" 에 대해 부정적인 입장을 보이고 있다.
아무래도 Java 의 열거 타입 상수 역시 public 하게 사용할 수도 있다보니, 결합도가 높아진다는 단점을 갖고 있기 때문인듯 하다.
하지만 위의 public 상수와는 달리 , Java 의 열거타입은 클래스로서의 특징 또한 모두 갖고 있기 때문에 “기능을 공유" 하는 역할을 할 수 있다.
아무튼 책에서 말하는 “코드의 중복을 피하기 위해 단순히 public 상수를 선언" 하는 것보다는 “상수와 관련된 기능을 가진 클래스를 선언" 하는 것이 좋다는 것에 동의하며, 이러한 관점에서 Java 를 사용하면서는 “상수" 같은 것이 필요한 경우 “열거형"을 많이 사용해왔다.
2.6 불변 객체로 만드세요
하지만 Java 에서 불변 이면서 지연로딩을 구현할 수는 없다
지연 로딩은, 생성시점에는 null 또는 NONE 객체 로 초기화 되어있다가 , 이후에 해당 프로퍼티에 대한 “변경”, 즉 “로딩"을 해 오는 것을 말한다.
현 시점 Java 의 불변객체에서는 불가능한 일이다 .
언어 차원에서 지연로딩을 지원하고 있지 않기에, 불변 클래스이면서 지연로딩 구현은 불가능 하다
식별자 가변성
Map 의 key 가 가변객체라면? 아래와 같은 일이 생긴다
실패 원자성
마치 “롤백"을 생각하면된다.
가변 객체는, 실패 상황 ( 예외가 터진다거나..) 이 일어날 때, try - catch 문과 같은 곳에서 “복구" 과정이 필요하다.
왜 복구가 필요하냐..? 예외가 터지는 구문 이전에 객체의 프로퍼티에 대한 값을 변경하였기 때문이다. 변경하는 게 허용되어있으니 이런 일이 일어날 수 밖에 없다.
이런식으로 “직접 롤백 하는 코드” 들이 들어가다보니 “복잡"하고 “실수 가능성이 높아(복구 코드를 누락)” 지며 “유지보수성이 떨어”지게 된다.
불변 객체를 사용하면 변경 자체가 허용되지 않다보니 이런 일은 전혀 신경 쓰지 않아도 된다.
시간적 결합
가변 객체는 세터와 같은 메소드가 열려있다.
어디선가 세터를 통해 값을 변경할 것이다.
그런 값의 변경이 “어디서?? 언제??” 일어나는지에 대해 알고 있어야, 해당 객체와 관련한 문제를 해결하거나, “예방" 할 수 있을 것이다.
그러다보니 “절차적인 코드" 를 작성하게 된다. 구문들의 시간적인 순서들이 중요해진다. 우리는 앞서 어떤 구문이 수행되는지 파악하고 코드를 짜야한다. 심지어는 코드간의 순서를 바꾸려고 할 경우에는 이에 대한 파악이 더욱 중요해진다.
반면 불변객체를 사용한다면? 항상 하나의 문장(생성자 호출) 만으로도 객체를 인스턴스화하고, 이후 변경은 일어나지 않는다. 이를 통해 구문 사이의 시간적 결합을 제거할 수 있다.
멀티스레드 환경에서의 “효율"관점 : 가변 객체를 사용할 때의 문제점
가변 객체의 부수효과? ( 불변 good)
어떤 객체의 참조를 여러 곳에서 사용하고 있을 경우 흔히들 부수효과가 생긴다고 한다.
멘토님 : 이 때 “여러 곳" 은 무엇을 의미할까요?
생각이 안났었다.😢 스프링 빈은 Stateless 해야한다 는 말이 있는것도 , 스프링은 자바 기반 엔터프라이즈 환경을 위한 프레임워크이기 때문이다. 즉, “멀티스레드 환경에서 동시성 문제" 가 존재한다. 스프링 컨테이너 내부에서 싱글톤으로 존재하는 빈이 여러 스레드에서 공유되고 있는 경우, 가변 필드가 존재한다면 A 스레드에서 필드를 변경한 경우 B 스레드에서는 자신이 변경한적도 없는 값을 바라보게 되는 것이다.
메모리 관점 (불변 tradeOff)
불변 객체는 매 번 새로운 객체를 만드는 것이 되기 때문에, 메모리 누수 위험과, GC 가 더 자주 일어나게 되어 트레이드 오프가 존재한다 .
OOP 관점 : 가변 객체는 나쁘기만 한가용?
멘토님 : OOP 관점에서 객체는 살아있는 존재같죠. 각자의 생명주기가 있을거에요. 객체지향 프로그래밍의 객체가 갖는 생명 관점에서
불변객체는, 기존의 애가 가진 프로퍼티를 변경하기 위해, 기존 애를 그냥 죽이고 새로 만드는게 되네요?
사실 불변 객체를 처음 접했을 때 가장 어색했던게 이 점이었다. 프로퍼티를 변경하고 싶은데 새로운 객체를 생성하라니…….. 이게 객체가 맞나? 싶었다. 그냥 단순히 데이터 전달체가 아닌가?? 그런 생각이 여전히 든다.
하지만 프로퍼티 변경이 필요하지 않은 경우, 그리고 위의 멀티스레드 환경에서와 같이, 예상하지 못한 변경이 발생할 확률이 높다면, 그런 시도 조차 막아놓는 것이 훨씬 좋을 것 같다. ( 불변 객체까지는 안되더라도 최대한으로 막아야 한다고 생각한다. 예를들어 JPA 의 경우 , 필드에 대하여 리플렉션을 통한 세팅이 일어날 거라면, 우리는 세터를 열어두지는 않는게 좋다고 생각한다. )예를들어 Spring 에서 흔히들 “서비스"라고 부르는 객체 들이 여기에 해당된다고 생각했다.
( 아직 객체의 생명주기 동안 값을 변경하는 상황에 놓여보지 않아서 이렇게 느끼는 건 아닐까.. 모르겠다 그런 상황이 있어도 되는건가 )
생각..
어떤 도메인 객체 또는 엔티티의 필드를 리턴할 때, 그것이 "객체" 고 가변 객체라면, 외부에서 이를 변경하는 일이 일어날 것입니다.
이게 바료.. 예상치 못한 결과를 낼 수 있는 상황이 아닐까요??
만약 이 객체가 불변이라면 우리는 이를 리턴하는 게터 메소드를 작성하면서도 망설임 없을 것 같습니당
적용하기
네이밍
Cashier 는 Customer 로부터 주문을 받고, 내부적으로는 “커피를 만들줄 아는 이" 에게 커피를 만들어달라고 요청하는 애다.
아래 나의 코드를 보자
뭔가 굉장히 장황하다
- 빌더는 생성하는 객체에대한 명사이름, 조정자는 동사 라는 이 책의 권고를 한 번 따라보자
- 실질적으로 둘 다 Coffee 를 반환하기 때문에 둘 다 “coffee” 라는 이름이 되어버린다
캐셔는 내부적으로는 바리스타에게 커피 생성을 요청한다. 만약 이 메소드가 외부에 공개되어있었다면, 마치 캐셔 자체가 커피를 제공해주는 존재 같이 보일텐데, 지금은 그것을 신경 쓰지 않아도 된다.
캐셔는 내부적으로, 바리스타 에게 커피를 요청하는 것이다.
그런 관점에서 아래와 같이 변경하였다
'책 > 엘레강트 오브젝트' 카테고리의 다른 글
[elegant]ch04 ! 끝! (0) | 2022.06.23 |
---|---|
[elegant] ch03 (정리중) (0) | 2022.06.23 |
[elegant] ch02 - 2.8 (0) | 2022.06.15 |
[elegant] 1. Birth 와 관련 자료 발표 영상 (0) | 2022.06.05 |