이 책은 객체가 살아있는 생명체인 것 처럼 표현한다.
객체가 태어나고 학습하고 취업하고 은퇴한다는.. 일종의 객체의 생명주기? 로 컨텐츠를 설명하고 있다.
그 중 첫 번째 챕터인 Birth 내용을 정리한다
+++(추가) 교육을 수강하며 ch01 과 관련하여 발표했던 자료가 있어 첨부한다
https://present.do/documents/62dd647c85df05656ca15627?page=0
객체의 가시성
- 객체의 가시성?
- 숫자 5 는 extra 라는 Cash 타입 객체 의 내부에 캡슐화 되어있다.반면 외부세계는, extra 객체가 어떻게 캡슐화 되어 있는가에 따라 extra 내부에 존재하는 5 를 직접적으로 볼 수도, 간접적으로 볼 수도 있을 것이다.
if(price < 100){ Cash extra = new Cash(5); price.add(extra); }
- 이것이 객체의 가시성이라고 생각한다 . 결국은 객체의 캡슐화에 따라 객체의 가시성은 달라지지 않을까?
- extra 객체 내부에서 가시적 이기 때문에 , extra 라는 인스턴스 내부의 인스턴스 필드들에 가시적(visible)이다.
public class Cash {
private int cash; // 외부에서는 extra 내부의 5를 직접적으로 볼 수 없다
public int cash; // 외부에서는 extra 내부의 5를 직접적으로 볼 수 있다.
유지 보수성
- 유지 보수성이 중요한 이유 는 “코드를 읽는 사람이 쉽게 코드를 이해할 수 있기 위함" 이다.
- 객체지향 프로그래밍에선 각 객체의 역할이 명확하게 이해할 수 있도록 코드를 작성한다면 유지 보수성을 향상 시킬 수 있을 것이다
- 코드의 길이가 더 짧아지고, 소화하기 쉽고, 모듈성이 향상되고, 응집도가 높아진다.
-er 로 끝나는 이름을 사용하지 말라
( 예외 : Computer 같은 애들 )
클래스란 무엇인가?
Java 와 같은 객체지향언어라고 불리는 것을 배울 때면 .. 다른 곳은 어떤지 모르겠지만 나는 항상
클래스는 “붕어빵 틀"이다
라고 배웠다. 생성되는 객체들의 설계도라는 의미에서 하는 말인데 이는 거의
클래스는 “템플릿” 이다
라고 말하는 것과 유사하다고 생각한다. 항시 똑같은 객체를 복사하여 만들어낸다는 느낌을 주고 있다.
하지만 이제는 이 생각에서 벗어나야 할 필요가 있다고 느꼈다
이제는 이렇게 생각해야 한다
클래스는 “ 객체의 팩토리 “ 이고 “ 객체의 warehouse(창고)” 다
클래스는 “타입"에 비해서 조금 더 “런타임에 가까운 존재"다. 타입 기반 시스템들에서는 컴파일 타임에 “타입을 체크" 하지만, 어떤 클래스인지 체크하지는 못한다는 것을 생각하면 좋을 듯 하다.
사실 아직까지도 명확하게 어떤식으로 다른지는 와 닿지 않지만 이제는 아래의 특징에 초점을 맞춰야 한다
- 객체를 생성하고, 생성된 객체를 추적하고, 적절한 시점에 파괴한다.
- 필요할 때 객체를 꺼낼 수 있고, 더이상 필요하지 않은 객체를 반환 할 수 있다.
이러한 특징으로 인해 “클래스는 객체들의 능동적인 관리자로 → 객체의 warehouse” 다.
class Shapes {
// 정적 팩토리메소드의 장점 -> 하위타입으로 리턴할 수 있다
// new 생성자와 달리, 타입명을 사용하여 Shapes 클래스 타입의 객체가 아닌, 다른 클래스 타입 객체 생성 가능
public static Shape make(String name){
if ( name.equals("circle")) {
return new Circle();
}
if ( name.equals("rectangle")) {
return new Rectangle();
}
throw new IllegalArgumentException("not found");
}
}
- Java 에서 new 연산자는 “객체의 팩토리를 제어하는 원시적인 수단" 이다. ( 조금은 부족한 수단이다 )
- 조금 부족하다고 하는 이유는, Java에서 new 연산자는 무조건 해당 클래스 타입의 새로운 인스턴스를 생성 해야만 하기 때문이다.
- 이미 존재하는 객체를 확인하여 재사용하는 캐싱과 같은 것을 할 수 없다. 동장 방식을 변경할 수가 없다.
- 이러한 것들을 위해서는 “정적 팩토리 메소드" 나 “디자인 패턴의 팩토리 패턴" 의 도움을 받아야 한다고 생각한다.
- 여기서도 , “새로운 객체를 생성하는 경우에는 항상 new 연산자를 사용" 한다.
- 또한 아쉽게도 “적절한 시점에 파괴"하는 것은 Java 뿐만 아닌 다른 언어들에서도 거의 제공하고 있지 않다.
- 조금 부족하다고 하는 이유는, Java에서 new 연산자는 무조건 해당 클래스 타입의 새로운 인스턴스를 생성 해야만 하기 때문이다.
클래스의 이름 짓기
클래스의 이름을 지을 때면 주로 “이 클래스가 무엇을 하고 있는지" 즉, “기능" 에 초점을 맞추고는 한다.
예를들어 “검증을 하는 애" 는 Validator 고, 인코딩, 디코딩을 하는 애는 Encoder, Decoder 이런식으로 생각하게 된다.
하지만 책에서는
- 클래스의 이름은 “해당 객체가 public 하게 제공하고 있는 기능에 기반해선 안된다”
- 클래스의 이름은 “ 무엇을 하는지" 가 아닌 “그 클래스는 무엇인지" 에 기반해야 한다
이런 얘기를 하고 있다.
클래스가
- “하는 것" 과
- “할 수 있는 것”
을 다르게 보고 “ 할 수 있는 것" 으로 설명해야 한다는 것이다
왜 이렇게 하지 말라는 걸까?
- 데이터 중심의 사고 , 즉 객체는 “객체에 캡슐화된 데이터” 를 다루는 연산들의 집합이다
라는 생각 으로 이어질 수 있기 때문이다.
“객체는 자율적인 존재" 라는 생각을 해야 한다. 객체는 스스로 결정하고 행동한다
객체는 그 자체 여야 하지, 💥 객체의 이름으로서 “ 그 객체가 하는 것을 나타내는 이름” 을 가질 경우, 그 객체는 일련의 절차들 의 집합이 될 수도 💥 있다.
- 책에서는 “-er” 로 끝나는 클래스는 “데이터를 다루는 절차의 집합"이 될 뿐이라고 경고하고 있다.
예를들어 PrimeNumbers 는 “숫자의 리스트 그 자체" 인 것이지, “리스트를 처리하기 위한 메소드의 집합이 아니 “ 다.
개인적으로는, 관습적으로 사용하는 Controller, handler의 경우는 오히려 해당 이름을 사용할 경우, 이를 읽는 사람들이 이해하기 쉬워진다고 생각한다.
책에서는 나쁜 이름으로 얘기하고 있지만 관습이라는 것이 있고, 코드는 작성자가 아닌 여러 사람이 보고 이해할 수 있어야 유지보수성이 향상되는 것이기 때문에 어느정도 -er 이 붙은 이름들을 사용해야하지 않을까 생각한다.
물론 객체지향에 대한 연습을 할 때는 최대한 -er 이라는 이름을 피하는 것이 좋을 것 같다.
주 생성자와 생성자 오버로딩
생성자 : 전달받은 인자를 사용해 “캡슐화하고 있는 속성들을 초기화" 한다.
응집도가 높고, 견고한 클래스에는 “적은 수의 메소드"와 “많은 수의 생성자가 존재" 한다.
- 메소드가 많아지면, 클래스가 가진 책임에 대한 초점이 흐려진다 → SRP 위반
- 반면, 생성자가 많아지면 → 해당 클래스를 사용함에 있어 유연성이 향상된다.
이 때, 생성자는 반드시 주 생성자( Primary Constructor) 가 존재하고, 내부적으로 주 생성자를 호출하는 부 생성자들이 있는 것이 좋다.
→ 즉, 내부 프로퍼티는 초기화 로직은 오직 한 곳에 둔다
→ 다른 곳들에서는, “인자를 준비" 하고, 포맷팅하고, 파싱, 변환만 해야 한다!
🔆 효과
예를들어
public class Cash{
private int dollars;
Cash(int dlr) { // 주 생성자
this.dollars = dlr;
}
Cash(String dlr) { // 부 생성자
this(Integer.parseInt(dlr); // 예외 생각안하고 일단 짜본다면
}
..
}
// main
Cash myWallet = new Cash("45"); //클라이언트 측에서 String -> int 로 변환하지 않아도 된다.
- 클라이언트에서 별도의 변환이나 파싱 작업을 할 필요 없기에 “유연성이 향상"된다.
- 주 생성자가 존재한다면 → 유지보수성이 향상된다
- 특히, 이 주 생성자를 정의해 놓은 위치도 유지보수성 향상에 도움이 되는데
- 항상!! 주 생성자는, 생성자들의 “마지막에 위치" 시키도록 하자
- 중복 코드를 방지하고, 설계를 간결하게 만든다.
- 예를들어, 검증로직을 주 생성자에만 위치시킬 수 있다.
- 특히, 이 주 생성자를 정의해 놓은 위치도 유지보수성 향상에 도움이 되는데
이 때 주의해야 할 사항들이 존재한다
- 부 생성자에서 “주 생성자 호출" 전에는 인스턴스 메소드를 호출할 수 없다
- 따라서, 변환로직이 필요한 경우에는 static method 를 사용해야만 한다.
메소드 오버로딩
주 생성자, 부 생성자의 핵심은 “메소드 오버로딩" 이다
불행하게도 모든 객체지향 언어들에서 “메소드 오버로딩" 을 지원하고 있지는 않다. ( 그럼에도 객체지향 언어라고 불리고 있는 아이러니가 존재하져 )
- 메소드 오버로딩을 사용하면 “코드의 가독성을 극적으로 향상" 시킬 수 있다.
메소드 오버로딩이 불가능한 경우에는 , 인자들을 map 에 담아 전달하는 방법을 사용할 수도 있다.
이 경우에는 장황한 생성자 코드가 만들어질 수 밖에 없다.
생성자에 코드를 넣지 말라
주 생성자를 code-free 하게 만들자
주 생성자와 같이, 객체를 초기화 시키는 곳에서는 코드가 없어야(code-free) 한다.
—> 따라서 전달받는 인자들은 그 자체로 속성을 초기화시키기에 완전한 값이어야 한다!
즉, 주 생성자에서는 전달 받은 인자를 파싱하거나 변환하는 코드가 있어서는 안 된다.
- 변환, 파싱하는 코드가 존재할 경우, 객체 초기화 코드가 명확하지 않게 된다.
예를들어 아래와 같은 클래스는 잘못된 경우로 볼 수 있습니다.
여기서 생성자가 하나만 존재하는 상황이기에 주 생성자는 Cash (String dlr) 인데 , 생성자 내부에 파싱하는 코드가 존재하기 때문입니다.
// 1번 예시
class Cash {
private int dollars;
Cash (String dlr) {
this.dollars = Integer.parseInt(dlr);
}
}
책에서는 이런 경우에 대해,
- 객체의 변환 시점을, 객체를 사용하는 시점으로 연기하라
고 하고 있다
즉, Composition
어떻게 이런것이 가능할까?
- 전달 받은 인자를 wrapping 하는 다른 객체로 감싸는 방식을 사용하면 가능하다.
// 2번 예시
class Cash {
private Number dollars;
Cash (String dlr) {
this.dollars = new StringAsInteger(dlr);
}
Cash (Number dlr) {
this.dollars = dlr;
}
}
class StringAsInteger extends Number {
private String source;
StringAsInteger(String src) {
this.source = src;
}
int intValues() {
return Integer.parseInt(this.source);
}
}
위의 1번 예시 Cash 클래스를 사용할 경우, five 객체는, 이 객체를 초기화하는 시점에 String → Integer(→int) 로 변환하는 로직이 바로 수행된다.
Cash five = new Cash("5");
반면 2번 예시 Cash 클래스를 사용할 경우, five 객체는, 이 객체를 초기화하는 시점에는 String → Number 타입 객체로 인스턴스화되어 → Cash 클래스에 초기화 될 뿐이다. 즉 int 형으로 변환되는 시점은 후에 intValues() 메소드를 호출할 때인 것이다.
- 우리의 StringAsInteger 객체는 String 타입의 src 를 int 로 변환해주는 기능을 제공해 주고 있다.
이런식으로, code - free 생성자를 만들기 위해, 필요한 기능을 제공하는 더 작은 객체를 Composition 으로 갖는 큰 객체를 만드는 것 즉, 객체들의 조합(Composition) 을 할 것 을 제시하고 있다
- 이를 통해, 확장성을 기대할 수 있다 ( 예시에서도 Number 타입의 인스턴스라면 어떤 것도 dollars 필드에 들어올 수 있다 )
생성자에는 오직 할당문만 - 부 생성자 에도 오직 할당문만 두자
결론적으로는 다음과 같다
생성자에 코드가 없을 경우, 성능 최적화가 더 쉽다.
→ 제어하기가 더 쉬워진다 🤔 → 어떤 동작이 Lazy 하게 일어나도록 하는 제어를 할 수가 있다!
생성자에는 오직 할당문만 두자
→ 리팩토링은 끊임없이 일어난다. 생성자 내부에서 어떤 일을 처리하고 있는 경우라면, 생성자 내부 로직을 다른 메소드로 옮기는 과정이 필요해진다
1번 예시와 2번 예시를 통해 왜 이런 얘기를 하는 것인지 알아보자
// 1번 예시
public class StringAsInteger extends Number{
private String text;
public StringAsInteger(String text) { // 생성자에서는 할당만 하는 경우
this.text = text;
}
public int intValue() {
return Integer.parseInt(text); // 사용시점에 파싱
}
// 2번 예시
public class StringAsInteger2 extends Number{
private int num;
public StringAsInteger2(String text) { // 생성자 내부에 코드가 존재
this.num = Integer.parseInt(text); // 초기화 시점에 파싱
}
public int intValue() {
return num;
}
- 2번 예시 처럼 객체 생성자 내부에서 파싱을 수행할 경우 → 그 실행 여부를 제어할 수가 없다.
- —> 평생 intValue() 를 호출 할 일이 없는 경우에도 무조건 파싱이 일어난다.
반대로 우리가 ⭐️ 원하는 것은 “실제로 요청을 받을 때 파싱" 하는 것 ( 실행을 제어하는 것 )⭐️ 이다.
- 따라서 1번 예시처럼 인자를 전달된 상태 그대로 캡슐화 → “ 실제로 요청을 받을 때, 파싱” 하는 방법을 선택 할 수 있다
물론 1번 예시에서도 문제점이 존재한다 → intValue() 를 호출 할 때 마다!! 파싱이 일어난다는 것이다.
이는 비효율적으로 보인다.
이 경우에는 “캐싱"을 통해 해결할 수 있다.
캐싱을 구현하기 위해, 이 Number 타입을 감싸고는 추가적으로 캐싱 기능을 제공하는 데코레이터 패턴을 사용할 수도 있다
public class CachedNumber extends Number {
private Number origin;
private Collection<Integer> cached = new ArrayList<>(1);
public CachedNumber(Number origin) {
this.origin = origin;
}
public int intValue() {
if (this.cached.isEmpty()) {
this.cached.add(this.origin.intValue());
}
return ((List<Integer>)this.cached).get(0);
}
하지만 이런 데코레이터 패턴을 구현하지 않는다면 결국은 intValue()를 호출 할 때 마다 매 번 파싱이 일어나게 된다.
따라서 데코레이터 패턴을 구현하지 않을 거라면 나는 아래와 같은 코드를 계속 사용할 것 같다 ( 적어도, 주생성자에서만큼은 할당만을 하게 되는 코드 )
public class StringAsInteger extends Number{
private int number;
public StringAsInteger(String text) { // 부 생성자에서는 파싱
this(Integer.parseInt(text));
}
public StringAsInteger(int number) { // 주 생성자에서는 할당만 하는 경우
this.number = number;
}
public int intValue() {
return this.number;
}
생성시점에는 생성만하고, → 이후에 해당 객체와 관련된 제어는 모두 , 해당 객체가 갖게 된다. ex) 해당 객체에서는 내부적으로 lazy 하게 어떤 연산을 수행할 수도 있다.
이런식으로 생성자에 할당문만 둘 경우, 우리는 다른 메소드들, 또는 합성객체를 통한 여러가지 제어(ex. 수행시점 제어 )를 하는 것이 가능하다! 이러한 제어는 추가 변경이 가능하기 때문에 리팩토링에서 장점이 있다고 생각한다
결론
초기화 생성자를 하나만 두고 생성자 오버로딩을 통해 코드의 중복을 줄이고 유지 보수성을 높일 수 있다.
생성자에는 검증 로직 외에는 프로퍼티에 할당하는 로직만을 둘 경우, 객체를 생성하는 시점에는 “인스턴스의 생성"만을 수행하고, 이후에 모든 제어권은 해당 객체가 가져가도록 할 수 있다. ( Lazy 하게 어떤 연산이 수행되도록 할 수 가 있다_합성 객체에서 알아서 할 거임 ) ( 객체가 좀 더 자율적이게 된다고 생각함 ) (자율적인 객체들의 합성을 통해 확장성을 얻게 된다고도 생각한다) 또한 후에 리팩토링 할 경우, 생성자에 다른 로직이 존재 && 해당 로직에 대한 변경이 필요하다면 생성자 부터 코드를 변경해야 할 것이다. 반면 생성자에 다른 로직이 존재하지 않고 프로퍼티에 대한 할당문만이 존재한다면, 제어와 관련된 로직에 변경이 생기더라도, 각각의 메소드에 대한 리팩토링만을 수행하면 될 것이다.
- 그런데 무조건 생성자에는 할당로직만 두는게 가능한가?
아직 보지 못한 내용 -> 엘레강트 오브젝트 토론장(?)
https://www.yegor256.com/2015/03/09/objects-end-with-er.html
https://www.yegor256.com/2015/05/07/ctors-must-be-code-free.html
'책 > 엘레강트 오브젝트' 카테고리의 다른 글
[elegant]ch04 ! 끝! (0) | 2022.06.23 |
---|---|
[elegant] ch03 (정리중) (0) | 2022.06.23 |
[elegant] ch02 - 2.8 (0) | 2022.06.15 |
[elegant]ch02_ ~ 2.6.4 (0) | 2022.06.09 |