12장 - 상속 다루기

600g (Kim Dong Geun)·2022년 2월 5일
0

상속다루기

상속 다루기는 다음과 같은 내용을 담고 있다.

목차

  1. 메소드 올리기
  2. 필드 올리기
  3. 생성자 올리기
  4. 메소드 내리기
  5. 필드 내리기
  6. 타입코드를 서브클래스로 바꾸기
  7. 서브클래스 제거하기
  8. 슈퍼클래스 추출하기
  9. 계층 합치기
  10. 서브클래스를 위임으로 바꾸기
  11. 슈퍼클래스를 위임으로 바꾸기

많네...

메소드 올리기

동기 : 동일한 기능을 하는 함수를 여러개 둘 경우 관리하기 힘들다. 하나가 변경되면 나머지 함수들도 변경을 고려해야하기 때문이다.

그래서 메소드 올리기의 핵심은 동일한 기능의 코드라면 부모클래스에서 관리하자 이다.

절차

  1. 똑같이 동작하는 메소드인지 확인
    • 실질적으로 하는 로직은 같고, 단지 코드내용만 다를 뿐이라면 같은 메소드이다.
  2. 메소드 안에서 호출하는 다른 메소드와 참조하는 필드들을 슈퍼클래스에서도 호출하고 참조 할 수 있는지 확인
  3. 메소드 시그니처가 다르다면 함수 선언 바꾸기로 슈퍼클래스에서 사용하고 싶은 형태로 통일
  4. 슈퍼클래스에 새로운 메소드를 생성하고, 대상 메소드의 코드를 복사
  5. 정적검사 수행
  6. 서브클래스 중 하나의 메소드 제거
  7. 테스트
  8. 6~7번 과정 반복

위의 절차를 요약하자면 메소드 올리기는 크게 3가지로 분류된다
1. 그냥 같은 기능을 하는 함수 -> 상위 클래스로 바로 올린다
2. 리턴값은 다르지만 거의 같은 기능을 하는 함수 -> 함수 매개변수화
3. 큰 로직은 같은데, 함수마다 개성이 넘칠때 -> 템플릿 메소드

예시

  1. 그냥 같은 기능 -> 생략
  2. 리턴값은 다르지만 거의 같은 기능을 하는 함수
//변경전
void goBusan();
void goSeoul();

//변경후
void goTo(String region);
  1. 큰로직은 같은데 함수마다 개성이 넘치는 경우

abstract class Book{
	
	abstract public void login();
	
	void book(){
		login();
		anything();
	}
	
}

class KakaoBook extends Book {
	@Override
	public void login(){
		kakaoLoginCheck();
	}
}

class NaverBook extends Book {
	@Override
	public void login(){
		naverLoginCheck();
	}
}

abstract class BookFactory {
	public static KakaoBook createKakaoBookInstance(){
		return new KakaoBook();
	}
	
	public static NaverBook createNaverBookInstance(){
		return new NaverBook();
	}
	
}

template method pattern + abstract Factory pattern 을 이용한 결과만 보여줬는데, book 에 해당하는 함수 로직들은 login을 제외하고는 동일한 로직을 가졌다. 즉 공통로직을 전부 상위 클래스로 올리고, 다른 로직만 하위클래스로 책임을 넘겼다고 보면 될 것 같다.

서브클래스 책임오류
서브클래스에서 부모클래스의 메소드가 필요하지 않은 겨웅가 있다. 이럴때는 구현되지 않음에 대한 표시를 해줘야하는데, 이를 보통 오류로 한다. 이런 오류를 Subclass Reponsibility Error 라고한다.
근데 이렇게 구현하면 리스코프 치환 법칙을 만족했다고 해야하나? 만족하지 않는다고 해야하나? -> 설계적 관점에서 보면 어긋난다고 생각되는게 개인 의견.

필드 올리기

동기 : 나중에 독립적으로 만들어진 서브클래스거나, 계층적으로 클래스가 구성된 경우에는 공통의 필드를 하위클래스에서 가진 경우가 있다. 이런 공통적인 필드를 상위 클래스에서 편하게 관리하자

절차

  1. 후보 필드들을 사용하는 곳 모두가 그 필드들을 똑같은 방식으로 사용하는지 면밀히 살핀다.
  2. 필드들의 이름이 각기 다르다면 똑같은 이름으로 바꾼다.
  3. 슈퍼클래스에 새로운 필드를 생성한다.
  4. 서브클래스의 필드들을 제거한다.
  5. 테스트한다.

예시


class Employee {
	
}

class Daeri extends Employee {
	private List<Grade> grades;
}

class Bujang extends Employee {
	private List<Grade> grades;
}

class Employee {
	private List<Grade> grades;
}

class Daeri extends Employee {
}

class Bujang extends Employee {
}

이렇게 둠으로써 데이터의 중복선언해당필드에 관련된 메소드들을 슈퍼클래스에서 공통적으로 관리하게 둘 수 있다.

무작정 올리기 보다는 클래스를 잘 나타내는 도메인인지 확인하고, 그것을 잘나타내는 도메인일 경우에만 모는 것이 적당할 것 같다.

생성자 본문 올리기

동기 : (책에는 나와있지 않지만 메소드 올리기와 상이하다 생각든다. 하지만 추가로 생성자는 다루기 까다롭기 때문에 따로 장으로 분류 해놓은 것 같다. ) 생성자는 할수 있는 일과 호출순서에 제약이 있기 때문에 조심스럽게 접근해야함.

생성자 관련해서 힘들었던 경험이 있으셨음? 물어보자
있었던것 같은데 잘 기억나지 않아서 공감되는 사례랑 들으면 더 와닿을 것 같음

절차

  1. 슈퍼클래스에 생성자가 없다면 하나 정의한다. 서브클래스의 생성자들에서 이 생성자가 호출되는지 확인한다.
  2. 문장 슬라이드하기로 공통 문장 모두룰 super() 호출 직후로 옮긴다.
  3. 공통 코드를 슈퍼클래스에 추가하고 서브클래스들에서는 제거한다. 생성자 매개변수 중 공통 코드에서 참조하는 값들을 모두 super()로 건넨다.
  4. 테스트
  5. 생성자 시작 부분으로 옮길 수 없는 공통 코드에는 함수 추출하기와 메소드 올리기를 차례로 적용한다.

예시

class A {
	public A(String name, int age){
		this.name = name;
		this.age = age;
	}
}

class B extends A{
	
	//변경전
	public B(String name, int age, String address){
		this.name = name;
		this.age = age;
		this.address = address;
	}
	
	//변경후
	public B(String name, int age, String address){
		super(name, age);
		this.address = address;
	}
}

메소드 내리기

동기 : 상위 클래스의 메소드가 모든 자식 클래스에 적용되는게 아닌 특정 클래스에 적용이 된다고 한다면, 상위클래스의 메소드를 아래로 내려 관리한다. -> 응집도 up, 결합도 down

절차

  1. 대상 메소드를 모든 서브클래스에 복사한다.
  2. 슈퍼클래스에서 그 메소드를 제거한다.
  3. 테스트한다.
  4. 이 메소드를 사용하지 않는 모든 서브 클래스에서 제거한다.
  5. 테스트한다.

예시 생략
책임을 구분할 수 있는 판단력을 기르고 생각을 많이하자

필드내리기

메소드 내리기와 동일하다. 특정 서브클래스에 해당하는 필드라면 해당 필드로 내린다.

타입 코드를 서브클래스로 바꾸기

동기 : 보통 타입코드를 처리하는 곳은 로직을 한대 모으는 경우가 많기때문에, 일반적으로 타입코드가 포함된 로직을 처리하는 함수는 굉장히 비대해진다. (타입코드를 처리하는 부분이 파편화되면,, 뜯어 고치자..) 굉장히 비대해진 함수는 이해하기도, 고치기도 굉장히 난해한 경우가 많다. 이를 분리해보자

절차

  1. 타입 코드 필드를 자가 캡슐화한다.
  2. 타입 코드 값 하나를 선택하여 그 값에 해당하는 서브클래스를 만든다. 타입 코드 게터 메소드를 오버라이드 하여 해당 타입 코드의 리터럴 값을 반환하게 만든다.
  3. 매개변수로 받은 타입 코드와 방금 만든 서브클래스를 매핑하는 선택 로직을 만든다.
  4. 테스트한다.
  5. 타입 코드 값 각각에 대해 서브클래스 생성과 선택 로직 추가를 반복
  6. 타입 코드 필드 제거
  7. 테스트
  8. 타입 코드 접근자를 이용하는 메소드 모두에 메소드 내리기와 조건부 로직을 다형성으로 바꾸기를 적용한다.

예시

메소드 올리기랑 예시가 얼추 같다. 그대로 들고와도 될듯?
대신 저자가 원하는 방법대로 변경하면 다음과 같을 것 같다.


enum LoginType{
	NAVER, KAKAO
}

class Book{
		
	private LoginType loginType;
	
	public void book(){
		login();
		anything();
	}
	
	private void login(){
		LoginService loginService = getLoginServiceInstance();
		loginservice.login();
	}
	
	private LoginService getLoginServiceInstance(){
		switch(loginType){
			case LoginType.NAVER:
				return new NaverLoginService();
			case LoginType.KAKAO:
				return new KakaoLoginService();
			default:
				throw new IllegalArgumentException("invalid login type error");
		}
	}
	
}

class KakaoLoginService implements LoginService {
	@Override
	public void login(){
		kakaoLoginCheck();
	}
}

class NaverLoginService implements LoginService {
	@Override
	public void login(){
		naverLoginCheck();
	}
}

// 나머지 Book을 상속한 KakaoBook. NaverBook 만드는건 생략

login에 관한 부분이 확실히 빼서 관리할 수 있다. Book 은 로그인에 대한 부분은 신경쓰지 않아도 된다.
-> 응집도 Up

서브 클래스제거하기

동기 : 프로그래밍을 하다보면, 특정 제품이나 도메인이 페이드 아웃되면서 쓰이지 않을때가 있다. (위의 예를들면 더는 예약에 있어 login check가 필요하지 않는다던가) 이런경우 서브클래스를 다시 하나의 클래스로 관리하면 유지보수하기 편해진다.

누가 그러더라 조립은 분해의 역순이라고

근데 분해해놓고 다시 조립하면 못하는게 현실

암튼 절차만 적고 생략하겠다.

절차

  1. 서브 클래스의 생성자를 팩토리 함수로 바꾼다.
  2. 서브 클래스의 타입을 검사하는 코드가 있다면 그 검사 코드에 함수 추출하기와 함수 옮기기를 차례로 적용하여 슈퍼클래스로 옮긴다.
  3. 서브클래스의 타입을 나타내는 필드를 슈퍼클래스에 만든다.
  4. 서브클래스를 참조하는 메소드가 방금 만든 타입 필드를 이용하도록 수정한다.
  5. 서브클래스를 지운다.
  6. 테스트한다.

슈퍼 클래스 추출

동기 : 코드를 작성하다보면 두클래스가 어떤 연관관계를 가지고 있진 않지만, 하는 행동이 비슷해보이는 친구가 있을수도 있다. 두친구들의 공통 특성을 모으고 슈퍼클래스로 추출하도록하여 관리 편하게 한다.

절차

  1. 빈 슈퍼클래스를 만든다. 원래의 클래스들이 새클래스를 상속하도록 한다.
  2. 테스트
  3. 생성자 본문올리기, 메소드 올리기, 필드 올리기를 차례로 적용하여 슈퍼클래스에 올린다.
  4. 서브클래스에 남은 메메소드들을 검토한다. 공통되는 부분이 있다면 함수로 추출한 다음 메소드 올리기 사용.
  5. 원래 클래스들을 사용하는 코드를 검토하여 슈퍼클래스의 인터페이스를 사용하게 할지 고민한다.

예시

써야할 코드가 많을 것 같아서 예시를 들어보자. 웹브라우저의 Cookie, Storage를 관리하는 각각의 CookieUtil, StroageUtil 클래스를 구현했다고하자.

생각해보면 둘다 공통적인 기능이 많다.
<key, value> 저장소에 expire time 설정까지, 암튼 이런것을 공통적으로 풀어나갈수 있을 것 같다


class KeyValueUtil {
	
	void setValue(String key){
	
	}
	
	Object getValue(String key){
	
	}
}

class CookieUtil extends KeyValueUtil {

}

class StorageUtil extends keyValueUtil {

}

근데 생각해보니, 해당 기능가지고는 단순 class 보다는 interface가 더적합하다고 본다.
keyValueUtil에서 Caching 기능을 지원한다던지, 어플리케이션 단에서 공통적인 특정 기능이 있어야 하는게 오히려 다 잘 어울릴 듯.

계층 합치기

동기 : A extends B extends C extends D, 알기 어렵다, 쓰임새도 명확하지 않다면, 합쳐라!

서브클래스를 위임으로 바꾸기

동기 : 큰 로직은 비슷한데, 동작이 달라지는 객체들은 상속으로 표현하는게 자연스럽다. 하지만 상속은 너무 비용이크다. 상속을 한다는 것 자체가 부모클래스와의 거의 완전한 결합을 의미하며 하위 클래스의 동작을 알기 위해서는 부모클래스의 동작까지도 빠짐없이 잘알아야한다. 또한 서브클래스를 만든다는 것 자체가 큰 비용을 유발한다. 특정 동작만 변경하고 싶은데, 부모클래스의 모든 클래스를 구현해야할 수 도있다. 이럴때 생성 비용을 최소화할 수 있는 방법이 Delegate이다.

절차

  1. 생성자를 호출하는 곳이 많다면 생성자를 팩토리 함수로 바꾼다.
  2. 위임으로 활용할 빈 클래스를 만든다. 이 클래스의 생성자는 서브클래스에 특화된 데이터를 전부 받아야 하며, 보통은 슈퍼클래스를 가리키는 역참조도 필요하다.
  3. 위임을 저장할 필드를 슈퍼클래스에 추가한다.
  4. 서브클래스 생성 코드를 수정하여 위임 인스턴스를 생성하고 위임 필드에 대입해 초기화한다.
  5. 서브 클래스의 메소드 중 위임 클래스로 이동할 것을 고른다.
  6. 함수 옮기기를 저용해 위임 클래스로 옮긴다. 원래메소드에서 위임하는 코드를 지우지 않는다.
  7. 서브 클래스 외부에도 원래 메소드를 호출하는 코드가 있다면 서브클래스의 위임 코드를 슈퍼클래스로 옮긴다. 이때 위임이 존재하는지를 검사하는 보호 코드로 감싸야한다. 호출하는 외부코드가 없다면 원래 메소드는 죽은 코드가 되므로 제거한다.
  8. 테스트한다.
  9. 서브클래스의 모든 메소드가 옮겨질대까지 과정을 반복한다.
  10. 서브클래스들의 생성자를 호출하는 코드를 찾아서 슈퍼클래스의 생성자를 사용하도록 수정한다.
  11. 테스트 한다.
  12. 서브클래스를 삭제한다.

넘 양이 많아...

예시


class Booking {

	BookingDelegate bookingDelegate;
	
	public getPrice(){
		return bookingDelegate != null ? bookingDelegate.getPrice() :this.price;
	}
}


class BookingDeletegate {
	
	Booking booking;
	
	public BookingDelegate(Booking booking){ //역참조가 일어날 수도 있다.
		this.booking = booking;
	}
	
	public getPrice(){
		return booking.getPrice() + getPremiumprice();
		
	}
	
	private getPremiumPrice(){
		return 1000;
	}

}

Delegate 클래스를 둠으로써 변경은 최소화하고, getPrice부분만 변경하도록 할 수 있다.

슈퍼 클래스를 위임으로 관리하기

동기 : 아까 부킹이라 쳐보자, 예약에 관한 delegate, 조회에 관한 Delegate, 가격에 대한 Delegate, 각기능 마다 Delegate 할꺼면 위임해라 ㅅㄱ ㅂㅇ

귀찮다. 생략

후기

처음에는 분리하고 나중에는 다시 합치라하고 이 책의 공통적인 내용이다.
컴포지션을써라, 아니다 상속을써라, 떼라, 붙여라..등등
일관적이지 못한 저자의 태도를 비판해볼만하다.
감히 내가 뭐라고 비판하겠냐, 그냥 하라면 해야지.

그럼에도 불구하고 코드를 붙였다 뗏다하는 내용이 반복적으로 나온 이유는
독자에게 분리하고 분리안하고의 책임을 넘김(현실 Abstract Method Pattern good..)과 동시에 많은 생각과 고민을 해서 좋은 코드를 짜라는게 목적이 아닌가 싶다.

아무튼 읽으면서 분리 하고 분리안하고의 내 주관적인 기준은 내가 아니더라도 읽기 쉬운 코드인가가 분리의 기준이 될 것 같다.

여러분들도 어떤 기준으로 좋은 코드를 짜고 있는지 잘 생각하고 고민하셔서 코드 똥내 나는 깃허브 민폐남이 되지 않도록 주의하자.

profile
수동적인 과신과 행운이 아닌, 능동적인 노력과 치열함

0개의 댓글