
🚨오늘 실제로 있었던 일!🚨
면접관 : 다형성이 뭔지 아시나요?
나 : (그.. 클래스가.. 다른데.. 유연하게.. 음.. 상속...) 후.. 모르겠습니다.
시원하게 K.O 당했다.
근데 진짜 이것도 모른다고 말한 내 자신이 너무 부끄러워서 견딜 수 없는거 실화냐?
당장 다시 책을 꺼내들었다.
객체지향 프로그래밍이란 인간 중심적 프로그래밍 패러다임이다. 즉, 현실 세계를 프로그래밍으로 옮겨와 프로그래밍하는 것을 말한다. 현실 세계의 사물들을 객체라고 보고, 그 객체로부터 개발하고자 하는 애플리케이션에 필요한 특징들을 추출해 프로그래밍하는 것이다. 이것을 추상화라고 한다.
객체 지향의 가장 기본은 객체이며, 객체의 핵심은 기능을 제공하는 것이다. 객체를 정의할 때 사용하는 것은 객체가 제공해야 할 기능이며, 객체가 내부적으로 어떤 데이터를 갖고 있는 지로는 정의되지 않는다. 이러한 기능들을 오퍼레이션이라고 부른다.
객체들이 공통적으로 필요로 하는 속성이나 동작을 하나로 추출해내는 작업
추상적인 개념에 의존하여 설계해야 유연함을 갖출 수 있다. 즉, 세부적인 사물들의 공통적인 특징을 파악한 후, 하나의 묶음으로 만드는 것이 추상화다.
Q. 추상화가 뭔데요?
A. 사물에서 공통적인 특징을 파악해서 뭐 속성이나 동작 같은 것들을 하나로 추출해내는 작업이죠!
예를 들면.. 시그니엘이나 펜트하우스-청담은 집에서 창문으로 보이는 전경이 이쁘지만 제 쥐꼬리만한 월급으론 살 수 없는 집이에요
이처럼 "쥐꼬리만한 월급으로 살 수 없는 집" 과 같은 추상화 집합을 만들어두고, 이들이 가진 공통적인 특징들을 만들어 활용하면 된다.(비쌈, 전경이 좋음, 위치가 좋음)
이 같은 경우, '한남더힐'이 '쥐월급은못사는집'에 추가될 수 있는데, 추상화로 구현해두면 다른 코드는 건드리지 않고 추가로 만들 부분만 새로 만들어주면 된다.
type House struct{ name string view string price string } func NewHouse(name, view, price string) *House{ return &House{name} } func (h *House) View() error{ return h.view } func (h *House) Price() error{ return h.price func main(){ h := NewHouse("한남더힐", "야경미쳤음", "ㅈㄴ비쌈") fmt.Print(h.View(), h.Price()) }
객체지향은 기본적으로 캡슐화를 통해서 한 곳의 변화가 다른 곳에 미치는 영향을 최소화한다. 캡슐화란 객체가 내부적으로 기능을 어떻게 구현하는지를 감추는 것이다. 내부 기능 구현이 변경되더라도, 그 기능을 사용하는 코드는 영향을 받지 않도록 해준다. 즉, 내부 구현 변경의 유연함을 주는 기법이 캡슐화이다.
캡슐화에는 두 가지 규칙이 있다.
Tell, Don't Ask
데이터를 물어보지 않고 기능을 실행하라는 규칙이다. 데이터를 읽는 것은 데이터를 중심으로 코드를 작성하게 만드는 원인이 되기 때문이다. 데이터는 private으로 클래스 내부에 숨기고, 메서드를 통해 데이터에 접근한다.
go에서는 변수의 대소문자를 바탕으로 Public/Private을 결정할 수 있다. 짱편함.
(대문자 = public, 소문자 = private)
type House struct{ Name string // 외부에서 접근 가능 price string // 외부에서 접근 불가. 메서드로 접근 가능.
a.getA().getB().getValues()
이렇게 점 여러개 쓰지말라는거임!
type House struct{ name string view string tx *sql.Tx } func (h *House) Query(query string, args ...interface{}) (*sql.Rows, error){ return h.tx.Query(query, args...) } h.tx.Query(@#$) // 이렇게 쓰면 안되고 h.Query(@#$) // 인터페이스를 구현하면 demeter 법칙 지키기 쌉가능 야호
우리가 흔히 알고 있는 "자식 클래스가 부모 클래스의 필드나 메서드를 그대로 물려받아 사용하거나 좀 다듬어서 사용할 수 있게 해주는" 그거다. 관점과 워딩을 조금 바꿔보면, 여러 개체들이 지닌 공통된 특성을 부각시켜 하나의 개념이나 법칙으로 성립하는 과정이다 라고 표현할 수 있다. 이를 일반화 라고도 한다.
근데 상속을 하다보면 문제가 생긴다.
상위 클래스 변경이 어려움
어떤 클래스를 상속받는다는건 그 클래스에 의존한다는 뜻이다. 따라서 의존하는 클래스의 코드가 변경되면 영향을 받게 되는 것이다. 변경의 여파가 상속 계층을 따라 전파되게 된다.
클래스의 불필요한 증가
유사한 기능을 확장하는 과정에서 클래스의 개수가 불필요하게 증가할 수 있다.
상속의 오용
같은 종류가 아닌 클래스의 구현을 재사용하기 위해 상속을 받게 되면 잘못된 사용으로 인한 문제가 발생한다. 상속을 받는 클래스가 상위 클래스와 IS-A관계가 아닐 때 발생한다.
이를 해결하려면 Composition(called 객체 조립, 구성) 방법을 사용한다.
객체지향언어에서 객체 조립은 필드에서 다른 객체를 참조하는 방식으로 구현된다.
상속에 비해 조립을 통한 재사용의 단점은 런타임 구조가 복잡해진다는 것이고, 상속보다 구현도 어렵다. 그러나 구현/구조의 복잡함보다 변경의 유연함을 확보하는데서 오는 장점이 크기 때문에 상속보다 조립하는 방법을 먼저 고려해야 한다.
상속은 재사용 이라는 관점보다, 기능의 확장 이라는 관점을 통해 사용해야 한다. 또한 명확한 IS-A 관계가 성립되어야 한다.
어 근데 go에는 상속이 없다.. (출처) 하지만 임베딩이라는 것이 있어서 상속과 유사한 효과를 볼 수 있다. 구조체를 임베딩하면 외부 타입(House)와 내부 타입(shape)로 구분할 수 있다. 내부 타입의 메서드는 외부 타입의 메서드로 호출할 수 있게 되는데 이를 내부 타입 승격이라고 부른다. 내부 타입 승격 시 메서드의 수신자는 외부 타입이 아닌 내부 타입이 된다.
type Area interface { Area() int } type shape struct{ name string } func (s *shape) Area() int{ return 0 }
type House struct{ room int shape // 이처럼 구조체 안에 변수명 없이 타입명만 정의하는 것으로 그 타입을 임베딩할 수 있다. } func NewHouse(name string room int){ return &House{shape{name},5) } func (h *House) Area() int{ return h.room }
type Empty struct{ shape } func NewEmpty() * Empty{ return &Empty{shape{"집이음슴"}}
func main(){ someHouses := []Area{ NewHouse("한남더힐", 5), NewEmpty(), } for _, h := range someHouses { fmt.Println(h, h.Area()) } } /* 출력 Type=[*House], Name=[한남더힐], Area=[5] 5 Type=[*shape], Name=[집이음슴], Area=[0] 0 */
마지막 Empty 구조체는 Area 인터페이스를 구현하지 않았으므로 Empty 객체에 대한 Area() 메서드 호출은 내부 타입 승격이 이루어져 shape 구조체의 메서드가 호출된다.
여러 가지 형태를 가질 수 있는 능력을 의미한다. 서로 다른 클래스의 객체가 같은 동작 수행 명령을 받았을 때, 각자의 특성에 맞는 방식으로 동작한다는 말이기도 하다.
예를 들어, 쥐꼬리는못사요집들은 창문 밖 전경이 각기 다르고 고유의 뷰가 있다.
이전 예시에서 '쥐꼬리는못사요집' 이라는 개념을 일반화해서 한남더힐, 시그니엘 등의 객체를 만들었다. 그런데 쥐꼬리못사집이라는 클래스의 '창문 밖 전경을 봐라!' 라는 메서드를 실행했을 때, 자식 클래스들이 각기 다른 전경을 보여준다는 것이 다형성이 부각된 부분이다.
type House interface{ viewPanorama() string }
type Apartment struct{ name string rooms int } func NewApartment(name string) House{ return &Apartment{ name: name, rooms: 5, } } func (a *Apartment) viewPanorama() string { return a.name + ", 야경 지리네!" }
type Villa struct{ name string rooms int } func NewVilla(name string) House{ return &Villa{ name: name, rooms: 2, } } func (v *Villa) viewPanorama() string { return v.name + ", 벽뷰 지리네.." }
func main(){ var villa, apart House villa = NewVilla("빌라촌A") apart = NewApartment("한남더힐") fmt.Println(villa.viewPanorama()) fmt.Println(apart.viewPanorama()) } /* 출력 빌라촌A, 벽뷰 지리네.. 한남더힐, 야경 지리네! */(쓰고보니 이상한데 빌라 폄하 의도는 전혀 없습니다)
Go의 interface를 사용해서 쉽게 구현이 가능했다.
이것으로 오늘 대답하지 못했던 객체지향에 대해 조금 알아보았다.
이정도만 설명했어도 쪽팔리진 않았을거 아냐 ㅠㅠ
내일은 SOLID 가즈아..