2021 03 06
2021-03-06¶
클래스 vs 객체 vs 인스턴스¶
- 클래스(Class) - 객체가 태어나는 곳 - 필요에 따라 새로운 객체를 생성하고, 더는 사용하지 않을 때 객체를 파괴 - 해당 클래스의 자식들이 어떤 모습 이여야하고, 어찌 행동해야 하는지 알고있다 - 객체를 만들어 내기 위한 "설계도" or "틀" - 연관되어 있는 변수와 메서드의 집합 - "설계도"
- 객체(Object) - 소프트웨어 세계에 구현할 대상 - 클래스에 선언된 모양 그대로 생성된 실체 - "클래스의 인스턴스"라고 불림 - 객체는 모든 인스턴스를 대표하는 포괄적인 의미를 갖는다 - OOP 관점에서 "클래스의 타입으로 선언"되었을 때, 이를 "객체"라고 부른다 - "설계도로 구현한 모든 대상"
- 인스턴스(Instance) - 설계도를 바탕으로 SW에 구현된 "구체적인 실제" - 실체화된 인스턴스는 메모리에 할당됨 - 인스턴스는 객체에 포함된다 - 의미적으로 인스턴스는 어떤 원본(추상적 개념)으로부터 "생성된 복제본"을 의미
- OOP 관점으로 정리 - 소스코드에 class로 작성된 코드는 "설계도"이며, 이는 <클래스>이다. - "클래스의 타입으로 새로운 변수가 선언"이 되었을때, 이는 <객체>가 된다. - 객체가 "메모리에 할당"되어 실제 사용될 때, 이를 <인스턴스>라고 한다.
좋은 객체의 7가지 덕목¶
-
객체가 현실 세계에 존재한다 - 객체는 생명체로, 자신만의 생명주기/행위/습관을 지닌 독립적인 개체다 - 그저 자료구조와 함수의 집합이 아니란 것이다 - 현실 세계의 피조물을 대표하는 것으로, 현실 세계의 대리자이다. - 따라서, 현실세계에 존재하지 않는 다양한 객체는 리팩토링의 대상이 될 수 있다
-
객체가 계약에 따라 동작한다 - 자신의 특성이 아닌, 자신이 준수하는 계약에 의해 사용되어야 한다 - 그 어떤 객체라도 "계약"에 명시된 일을 하도록 예상한다 - Java에서는 "계약"을 interface로 표현한다 - 좋은 객체 안에 담긴 모든 공용 메서드는 interface 내에 선언된, 상응하는 메서드들을 구현할 것
-
객체가 고유하다 - 좋은 객체는 언제나 고유하기 위해 무엇인가를 "캡슐화"한다 - 캡슐화: 객체의 속성과 행위를 하나로 묶고, 실제 구현 내용 일부를 외부에 감추어 은닉한다 - "캡슐화"할게 없다면, 즉 객체의 속성과 행위를 외부에 감출게 없다면, 다 똑같은 복제본을 가질 수 있다 - 따라서, 정적(static) 메서드만이 담긴 util 클래스는 좋은 객체를 인스턴스화 할 수 없다 - 또한, "클래스"라고 부를 수 없다 - OOP 패러다임을 엉터리로 남용하는 것에 불과
-
객체가 불변적이다 - 좋은 객체는 자신이 캡슐화한 상태를 절대 변경하지 않음 - 객체는 개체의 대표자로, 그것이 대표하는 것들을 절대 배신하지 않는다 - 불변성을 보장하여 얻는 이점은... - 생성/테스트/사용에 용이 - 언제나 thread-safe - temporal coupling을 피하게 해준다(?) - 캐싱에 용이 - 방어적 복사를 안해도 된다 - NULL 참조 방지 - 실패 원자성을 보장
-
객체의 클래스에 정적 멤버가 없다 - 정적(static) 멤버는 "object-oriented"를 "class -oriented"로 변경 - 클래스의 행위를 노출한다 - 문제를 여러 부분으로 더 이상 분해할 수 없게 된다 - Mocking이 불가능하다 - thread-safe가 보장이 안된다
-
객체의 이름이 직명을 나타내지 않는다 - 이름은 그것이 무엇인지를 말해야 하지, 무슨일을 하는지 말해서는 안된다 - 그것이 무엇인지를 나타내는 네이밍 (O) - ex) CardDeck - 그것이 무엇을 하는지 나타내는 네이밍 (X) - ex) CardDistributor - "-er"로 끝나는 이름은 최대한 피하자
-
객체의 클래스가 final이나 abstract다 - final: 상속을 통해 확장할 수 없는 클래스 - "나를 분해할 수 없어. 난 블랙박스야" - abstract: 인스턴스를 가질 수 없는 클래스 - "난 이미 망가졌어. 날 먼저 고친 후에 사용해" - 나머지는 왜 안 되지? - 상속... 부모 클래스의 메서드 중 하나를 오버라이딩 하면서 로직이 전부 망가질 수 있음 - 부모 클래스의 모든 메서드가, 자식 클래스의 새로운 버전을 사용함을 기억하자!
클래스와 인스턴스(심화) 수업 복습¶
- 클래스 - 클래스 = "객체의 능동적인 관리자" - 객체의 factory로써, 객체를 만들고, 추적하고, 적절한 시점에 파괴하는 역할 - 클래스가 객체 만드는 것을 "인스턴스화" 라고 한다 - 객체를 보관하고, 필요할 때 객체를 꺼낼 수 있고, 더 이상 필요하지 않을 때는 객체를 반환하는 저장소/웨어하우스로 봐야한다
- 생성자 - 제공된 인자를 사용해 캡슐화된 프로퍼티를 초기화하는 과정 - 생성자에 비즈니스 로직은 안 들어 가거나, 정말 간단하게 들어가거나 - 3~4줄의 생성자 로직이 필요하다... "팩토리 메서드"를 고려해볼 것 - 하나의 생성자에서만 초기화 로직을 수행할 것 - 주 생성자로 만들고, 나머지는 부 생성자에서 호출하는 형식으로! (생성자 체이닝) - 재연링의 피드백 - 생성자를 많이 두어 생성할 수 있는 많은 방법을 제공하면 클라이언트가 편해진다 - 무의미한 생성자가 아닌, 유의미한 생성자라면 좋은 코드 - 생성할 수 있는 방법이 많다 == 사용성이 높다 - 고려할 순서 - 일반적으로 객체의 생성은 생성자에서 한다 - 생성자가 많아져 가독성이 떨어지거나, 이름을 부여하고 싶다면 정적 팩토리 메서드를 사용한다 - 매개변수가 많아지면 빌더 패턴을 사용한다 - 참고: https://johngrib.github.io/wiki/builder-pattern/ - 생성자의 로직이 커져 가독성을 해친다면, 생성만을 담당하는 팩토리를 분리한다.
- 상속과 조합 - 상속 - 코드를 재사용하는 강력한 수단이지만, 객체 관계를 너무 복잡하게 만든다 - 따라서 상속은 "올바르게" 써야한다 - 문제점 - 상위 클래스의 내부 구현이 달라지면, 코드 한 줄 건들지 않은 하위 클래스가 오작동할 수 있다 - 부모 클래스의 모든 메서드가, 자식 클래스에서 오버라이딩한 새로운 버전을 사용함을 기억하자! - 해결 방법 - 모든 클래스/메서드를 final/abstract로 제한한다 - 완벽히 "IS-A" 관계일 때 상속을 사용하자 - 결과적으로 상속을 쓰기 위해서는 부모 클래스의 "내부 구현"을 상세히 알아야 한다 - 이는 곧 "캡슐화"를 깨뜨린다 - 상속은 클래스의 행동을 확장(extend)가 아닌, 정제(refine)할 때 사용하자 - 확장: "새로운 행동"을 덧붙여 기존의 행동을 부분적으로 보완 - 정제: 부분적으로 "불완전한 행동"을 완전하게 만드는 것 (abstract) - 불완전한 요소를 완성시키기 위해 상속을 사용하자! - 조합 - private 필드로 기존 클래스의 인스턴스를 참조하게 한다 - 현재는 reusability 보다 flexibility가 더 강조되는 시대!
- 가변 객체와 불변 객체 - 불변 객체의 장점 - 모든 클래스를 불변 클래스로 만들면 유지 보수성이 올라간다 - 객체가 완전/견고 하거나 아예 실패하는 실패 원자성을 가진다 - 시간적 결합 (메서드 A는 반드시 메서드 B보다 먼저 호출해야 한다) 을 없앤다 - 스레드 안정성 보장 - 객체가 단순해질수록 응집도는 높아지고, 유지보수는 쉬워진다 - Layer간의 왕래가 자유롭다