상속

haribo·2021년 2월 27일
0

Java

목록 보기
5/17

Why?

은행일 할때도 상속 온다하면 머리가 아팠는데, 단어부터 어려운 이걸 대체 왜 써야할까.

클래스를 만들다보면 겹치는 기능이 있기 마련이다. 스마트한 집단인 개발자들은 중복되는 값을 무지 싫어한다. 중복을 보면 묶어버리고 싶다는 본능이 되살아난다. 자, 공통된 기능을 묶었는데 그럼 이걸 뭐라 칭해야하지? 그리고 묶은 값을 받아서 쓰는 애들을 뭐라하지?

공통된 값을 묶은 것을 부모클래스라고 한다. 이 클래스의 기능을 받아서 쓰는 클래스를 자식클래스라고 한다. 그리고 자식클래스에 기능을 넘겨주는 것을 상속이라고 정의한다. 짠. 간단하다.

클래스 간의 상속이란?

  • 클래스간에는 부모-자식의 상속 관계를 설정할 수 있다.
  • 부모 클래스 A를 자식 클래스 B가 상속받을 때, B는 A의 모든 멤버변수와 메서드를 자신의 것으로 상속받게 된다. (단, private으로 설정된 기능은 상속되지 않는다. 이유는 단순하다! 은닉해버린걸 꺼내올 순 없으니까. 햄버거집에가서 카운터(프라이빗)를 뛰어넘어가 주방에서 햄버거를 훔쳐오는 것이나 다름없다!)
  • 자식클래스는 부모클래스의 public, protected 기능들을 직접적으로 코딩하지 않더라도 자신의 것으로 사용할 수 있게 된다. 중복적으로 적을 필요가 없어진 것이다.
  • 상속 정의방법
public class 자식클래스 extends 부모클래스 { . . . }

클래스 다이어그램을 통한 상속의 표현

  • 상속은 자식클래스가 부모클래스를 가리키는(바라보는) 화살표로 표현한다.
  • 상속관계가 이루어 질 때 부모 클래스를 super 클래스라 한다.
  • 상단 그림에 있는 plus와 minus 기능 메서드를 받아온 자식클래스는 times와 divide라는 기능을 덧붙여 확장한 것이다.

<실습 01> - 기능의 확장

// 덧셈과 뺄셈 기능이 있는 부모 클래스 생성
/* 생성자를 특별히 정의하지 않는 경우 자바 컴파일러가 기본 생성자가 있다고 간주하기
 * 때문에 클래스 다이어그램에서는 기본생성자를 명시한다. */
public class CalcParent {
	public int plus(int x, int y) {
		return x + y;
	}
	
	public int minus(int x, int y) {
		return x - y;
	}
}
// 자식클래스의 작성 (클래스 다이어그램 구현)
// 부모의 모든 기능을 상속받고 있으며, 곱셈과 나눗셈을 추가하여 기능을 확장시켰다.

public class CalcChild extends CalcParent {
	public int times(int x, int y) {
		return x * y;
	}
	
	public int divide(int x, int y) {
		int result = 0;
		
		if (y != 0) { // 0으로 나누면 에러니까
			result = x / y;
		}
		
		return result;
	}

}
// 열심히 만든 클래스를 이제 Main에서 써보자!

// 부모클래스 기능 확인
public class Main01 {
	public static void main(String[] args) {
		CalcParent parent = new CalcParent();
		System.out.println(parent.plus(100, 50));
		System.out.println(parent.minus(100, 50));
		
		System.out.println("-----------");

// 자식클래스 기능 확인
// 부모클래스 기능을 다 받아오고 곱하기 나누기도 더해 사칙연산으로 기능이 확장되었다
		CalcChild child = new CalcChild();
		System.out.println(child.plus(200, 100));
		System.out.println(child.minus(200, 100));
		System.out.println(child.times(200, 100));
		System.out.println(child.divide(200, 100));
	}
}

<여러개의 클래스에서 공통되는 기능을 추출하여 공유하기>

  • 개발자 A와 B는 게시판을 만들라는 지시를 받아서, 질의응답 게시판과 자료실 게시판을 만들었다. 근데 만들고 보니 게시판이라는 특성상 겹치는 값이 보인다. 글 번호, 제목. 어 이거 우리 겹치지 않아? 너도 만들었어? 개발자 둘은 스마트한 사람답게 효율적으로 두개의 공통된 기능을 게시판이라는 클래스로 묶기로 한다. 이렇게 부모클래스로 묶어버리면 좋은점은 나중에 추가로 후기 게시판 등을 만들때 상속처리를 해 보다 간편하게 만들 수 있다는 것이다. 또 글번호, 제목 형식을 바꿀때 부모클래스만 수정해버리면 되므로 수정도 용이해진다.
  • thinking point : 묶어서 분리해낸다는건 재사용성과 유지보수성을 올려준다는 개념으로 이해하면 될까? 현재까지 배운 수준에서는 그런데...

<실습 02>

// 공통기능을 구현하는 클래스 
// 게시판의 일반적인 기능을 구현한다. 두 자식들의 공통점을 추출한 것이다.

public class Article {
	// 글 번호
	private int num;
	// 제목
	private String title;
	public int getNum() {
		return num;
	}
	public void setNum(int num) {
		this.num = num;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	
	

}
// 질의응답 게시판 클래스
// 공통기능인 글번호와 제목을 받아오기에 답변에 대한 개념만 추가한다.

public class QNAArticle extends Article {
	// 부모 클래스의 title을 질문으로 보고, 답변을 추가한다.
	private String answer;
	
	public String getAnswer() { return answer; }
	
	public void setAnswer(String answer) { this.answer = answer; }
	
	public String toString() {
		return "질문/답변 [글번호=" + getNum() + ", 제목="
				+ getTitle() + ", 답변=" + answer + "]";
	}
}
// 자료실 게시판 클래스
// 첨부파일이라는 개념만 추가한다.

public class FileArticle extends Article {
	// 자료의 확장 --> 파일이름
	private String fileName;

	public String getFileName() {
		return fileName;
	}

	public void setFileName(String fileName) {
		this.fileName = fileName;
	}
	
	public String toString() {
		return "자료실 [번호=" + getNum() + ", 제목="
				+ getTitle() + ", 첨부파일=" + fileName + "]";
	}
}
// 잘 만든 클래스를 이제 써보자

public class Main {
	public static void main(String[] args) {
		FileArticle fileArticle = new FileArticle();
		fileArticle.setNum(1);
		fileArticle.setTitle("첫 번째 자료 입니다.");
		fileArticle.setFileName("java.ppt");
		System.out.println(fileArticle.toString());
		
		System.out.println("----------------");
		
		QNAArticle qna = new QNAArticle();
		qna.setNum(1);
		qna.setTitle("첫 번째 질문입니다.");
		qna.setAnswer("첫 번째 답변입니다.");
		System.out.println(qna.toString());

	}

}

다형성

다형성이란 무엇일까? 소프트웨어가 하드웨어랑 구별되는 특징은 쉽게 모양을 바꿀수도 있고, 확장할 수도 있다는 점이다. 확장이 용이하게 하기 위해선 유지보수가 쉬워야할거고, 좋은 코드를 짜야하는 이유를 여기서도 찾을 수 있겠다.

오버라이드

  • 전투 게임을 만든다고 가정해보자. 스타크래프트에 저그, 프로토스, 테란이 있듯이 우린 k식으로 육군, 공군, 해군을 만들거다. 얘네들은 모두 한가지 공통된 기능을 수행한다. 바로 공격이라는 것이다. 근데 공격을 하긴하는데 형태가 다 다를것이다. 육군은 육지에서 공격하고, 해군은 바다에서 공격하고, 공군은 하늘에서 공격할테니까. 마치 쿠키런 게임을 했을때 쿠키별로 점프, 슬라이드 모션이 다르고 기능도 다소 차이가 있는것처럼 말이다.
  • 아니, 상속은 같은 기능을 묶기 위해 쓰는건데 이렇게 "같았는데요, 달랐습니다." 하면서 조금씩 다르면 도대체 어쩌라고? 라는 생각이 든다. 여기서 쓸 수 있는 것이 오버라이드다. 기존 기능을 가져오되 상속받는 자식클래스들의 특성을 보고 내용을 조금씩 바꿔주는것이다.
  • 오버라이드는 부모 클래스의 기능(메서드)을 재정의 하는 것을 뜻한다. 재정의라고 하면 덮어씌우긴가 싶은데 그건 아니고, 그냥 가려지는 형식이다.
  • 예를들어 부모가 자식한테 너도 이제 성인인데 자가용 하나는 끌어야하지 않겠냐며 안쓰던 황금마티즈를 빌려줬다고 해보자. 아, 근데 황금색은 센서티브한 밀레니얼한테 좀 쪽팔린데요. 하고 야매로 흰색으로 도색처리를 했다. 업체를 잘 찾았으면 좋았을텐데, 손톱으로 좀 긁으니 빛나는 황금색이 등장한다. 이게 오버라이드 개념이다. 도색(오버라이드)을 했지만 본질이 황금마티즈인것처럼 원래 부모클래스에 있는 메서드는 바뀌지 않는다.
  • 근데 이러면 또 문제가 생긴다. 변수의 스코프 할때 우린 분명 메서드 호출시 가까운 값, 즉 블럭 안에있는거부터 리턴해준다고 배웠는데, 자식 클래스 안에 있는 재정의된 메서드는 가까워서 이거만 호출이 된다. 난 오리지날, 즉 이 마티즈 색이 원래 무슨색인지 궁금한데 이럴땐 어떡해야할까? 여기서 나온게 super라는 녀석이다.

super

  • 클래스의 상속관계에서 자식 클래스가 부모 클래스를 가리키는 예약어다.
  • 사용방법
    • 멤버변수 이름 앞에 명시 : 부모클래스의 멤버변수를 의미한다. 하지만 부모클래스의 멤버변수는 이미 모두 상속되어 있기 때문에 이 경우에는 this 키워드를 사용하는 것과 동일한 결과이기에 잘 사용하지 않는다. (그렇다. 오버라이드는 메서드에다가 하는건데 변수가지고 해봤자 소용이 없다는 것이다. 참고 : this는 변수를 다루는 것이므로 반대다.)
    • 메서드 이름 앞에 명시 : 부모클래스의 메서드를 의미한다. super를 쓰는 이유는 재정의된 메서드와 원래 오리지날, 즉 부모클래스의 메서드를 구별하기 위해서 쓴다고 했다. 따라서 재정의(오버라이드) 되지 않은 메서드는 똑같이 나온다. 재정의된 메서드에다가 써주면 오리지날인 부모클래스의 메서드가 나온다.
    • 키워드 자체를 메서드 처럼 사용 : 부모클래스의 생성자를 의미한다. (후에서도 다루겠지만 클래스의 생성자는 상속이 되지 않는다. 근데 부모클래스가 생성자를 가지고 있으면 자식클래스도 똑같이 생성자를 가지고 있어야한다. 상속해줘야하는데 안간다니!! 강제로 불러들이는 법이 없을까? 그래서 나온게 super다.)
  • 오버라이드된 메서드 위에 부모클래스의 메서드를 같이 출력하고 싶을때 super를 쓰면, 부모클래스의 메서드만 고쳐도 자식클래스에 그대로 반영이 된다. 두번 고쳐야할걸 한번 고치는 것으로 끝내다니. 근데 비기너 단계에서나 두개지, 부모클래스를 사용하는 자식클래스가 n천개라면? 한번만 쳐도 바꿔진다고? 혁명적인 가성비다.

생성자와 super

  • 생성자는 상속되지 않는다. (왤까? 나중에 찾아봐야지)
  • 부모클래스에 생성자가 파라미터값이 지정된 경우 자식클래스에도 같은 파라미터값이 지정된 생성자가 있어야한다. 상속이 안되는데 있어야한다니, 그래서 언급한 부모클래스를 다짜고짜 자식클래스에 상속하려고하면 오류가 예쁘게 뜬다.
  • 자연스럽게 오면 좋으련만 안온다니까 어쩌겠는가? 자식 클래스의 생성자를 통해서 부모 생성자를 강제 호출한다. 이때 쓰이는게 super(변수명)이다.

<실습 03>

public class Unit {
	private String name;
	
	// 생성자 정의
	public Unit(String name) {
		super();
		this.name = name;
	}
	
	public void setName(String name) {
		this.name = name;
	}
	
	public String getName() {
		return this.name;
	}
	
	// 재정의할 메서드
	public void attack() {
		System.out.println(this.name + " >> 공격준비");
	}
}
public class Army extends Unit {
	public Army(String name) { // 부모클래스의 생성자를 강제호출
		super(name);
	}
	
	@Override
	public void attack() {
		super.attack();
		System.out.println(super.getName() + " >> 지상공격 실행함");
	}
	
	public void tank() {
		System.out.println(super.getName() + " >> 탱크공격");
	}
}
public class Navy extends Unit {
	public Navy(String name) {
		super(name);
	}
	
	@Override
	public void attack() {
		super.attack();
		System.out.println(this.getName() + " >> 어뢰 발사!!");
		System.out.println(this.getName() + " >> 지상 상륙");
	}
	
	public void nucleus() {
		System.out.println(this.getName() + " >> 핵미사일");
	}
}
public class AirForce extends Unit {
	public AirForce(String name) {
		super(name);
	}
	
	@Override
	public void attack() {
		super.attack();
		System.out.println(this.getName() + " >> 이륙");
		System.out.println(this.getName() + " >> 공중공격 실행함");
	}
	
	public void bombing() {
		System.out.println(this.getName() + " >> 폭격");
	}
}
public class Main01 {
	public static void main(String[] args) {
		Army am = new Army("육군");
		Navy nv = new Navy("해군");
		AirForce af = new AirForce("공군");
		
		am.attack();
		am.tank();
		
		nv.attack();
		nv.nucleus();
		
		af.attack();
		af.bombing();
	}

}

@Override : 메서드 재정의 하는 과정에서 오타가 발생하면 어떻게 될까? 아! 새로운 메서드 정의구나! 하고 그냥 오류없이 진행된다. (잘못되었는데 잘 진행되는것만큼 무서운 것도 없다... 나중에 어떻게 돌아올지 모르기 때문이다.) 그래서 이건 재정의 하는거니까 오타 있으면 알려달라는 의미에서 쓰는게 @Override 이다. 재정의되는 모든 메서드 위에 붙여서 오타를 예방하자.

오버로드

  • System.out.println() 의 고찰 : 이 메서드의 괄호안엔 뭘 넣어도 출력이 된다. 아니 메서드에 파라미터값의 데이터 형식을 지정하는 것은 국룰인데 쟤는 왜 넣어도 다 출력이 될까? 답은 자바에서 기본 메서드를 만들며 온갖 자료형을 다 받을 수 있도록 해놨기 때문이다. 그래서 프로그래머들은 아주 편하게 온갖 데이터 값을 다 넣어도 출력할 수 있는 것이다. 작성자 편의를 위해 만든 것이라고 보면 좋겠다.
  • 메서드 오버로드 : 원칙적으로 하나의 클래스 안에서는 동일한 이름의 메서드가 두개 이상 존재 할 수 없지만, 이를 가능하게 하는 예외적인 처리기법이다.
  • 조건 : 메서드간의 파라미터가 서로 달라야한다. (데이터 타입이 다르고, 개수가 다르고, 서로 다른 데이터형을 갖는 파라미터들의 전달 순서가 달라야한다.)
  • 리턴형이 다른 경우에는 오버로드의 성립에 아무런 영향을 주지 않는다.
  • 오버로드는 하나의 메서드를 호출할 수 있는 모든 경우의 수를 미리 준비해 놓음으로서, 메서드를 만드는 측은 번거로울수 있지만 호출하는 측은 데이터 타입을 신경쓰지 않고 편리하게 사용할수 있게 하기 위함이다.

<실습 04> - 캐릭터의 이름과 나이를 설정하기 위한 다양한 방법을 준비

public class Charactor {
	private String job;
	private int age;
	
	public void setProperty(String job) {
		this.job = job;
	}
	
	public void setProperty(int age) {
		this.age = age;
	}
	
	public void setProperty(String job, int age) {
		this.job = job;
		this.age = age;
	}
	
	public void setProperty(int age, String job) {
		this.job = job;
		this.age = age;
	}

	@Override
	public String toString() {
		return "Charactor [job=" + job + ", age=" + age + "]";
	}
	
}
  • toString() : 자바의 모든 클래스는 특별히 명시하지 않을 경우 Object라는 클래스를 자동으로 상속받는다. 이 클래스에는 toString()이라는 이름의 메서드가 존재하며, 기본적으로 객체의 메모리 주소를 반환한다.
  • toString() 메서드를 '객체의 상태를 문자열로 리턴하는 용도'로 Beans에서 재정의하면, 객체에 저장된 데이터를 확인하는데 용이하다. (갓 단축키 Alt + Shift + S 가능)
public class Main01 {
	public static void main(String[] args) {
		Charactor c = new Charactor();
		System.out.println(c.toString()); // null값과 0이 출력된다.
		
		c.setProperty(19);
		System.out.println(c.toString());
		
		c.setProperty("회사원");
		System.out.println(c.toString());
		
		c.setProperty("자영업", 20);
		System.out.println(c.toString());
		
		c.setProperty(30, "교수");
		System.out.println(c.toString());
	}
}

<객체의 멤버변수 값이 할당되지 않은 경우의 초기값>

  • null : 문자열 및 객체. 알 수 없는 값이지만 0은 아니다. 메모리는 새거가 아닌 이상 데이터가 기록되어있는, 깔끔한 상태가 아닌데 거기에 변수를 임의로 할당한 것이라 안에 무슨 값이 들어가있을지 모른다. 식기세척기(자바)에 돌린 그릇(변수)을 찬장에 넣어뒀다가 다시 꺼내면 먼지가 묻어있어서 다시 물에 헹구는(초기화)것에 비유를 들면 쉽다.
  • 0 : 숫자계열
  • false : Boolean

<객체 생성 방법의 다양화>

  • 생성자도 메서드의 한 종류이므로 오버로드가 가능하다.
  • 생성자에 오버로드를 하면 해당 클래스에 대해 객체를 생성하는 방법을 다양하게 준비할 수 있게 된다.

<실습 05> - 다양한 방법으로 생성이 가능한 Member클래스

public class Member {
	private String job;
	private int age;
	
	public Member() { }
	
	public Member(int age) { this.age = age; }
	
	public Member(String job) { this.job = job; }
	
	public Member(String job, int age) {
		this.job = job;
		this.age = age;
	}
	
	@Override // why? toString은 최상위 객체인 Object에서 가져온 메서드기 때문
	public String toString() {
		return "Charactor [job=" + job + ", age=" + age + "]";
	}

}
public class Main02 {
	public static void main(String[] args) {
		Member m1 = new Member();
		System.out.println(m1.toString());
		
		Member m2 = new Member(19);
		System.out.println(m2.toString());
		
		Member m3 = new Member("학생");
		System.out.println(m3.toString());
		
		Member m4 = new Member("강사", 20);
		System.out.println(m4.toString());
	}
}

<this 키워드를 사용한 생성자 오버로드>

  • this키워드를 메서드처럼 사용할 경우 현재 클래스의 다른 생성자를 의미한다.
  • 파라미터가 서로 다른 생성자들이 하나의 완전한 생성자를 호출하도록 하여 데이터 초기화를 한 곳에서 일괄적으로 처리하도록 구현할 수 있다.

<실습 06> - 정의된 멤버변수들을 모두 초기화 하는 완전한 형태의 생성자를 정의한다.

public class Article {
	private int seq;
	private String subject;
	private String writer;
	
	public Article(int seq, String subject, String writer) {
		super();
		this.seq = seq;
		this.subject = subject;
		this.writer = writer;
	}

// 파라미터가 서로 다른 생성자들이 하나의 완전한 생성자를 호출하도록 하며,
// 데이터의 초기화를 한 곳에서 일괄적으로 처리하도록 구현할 수 있다.

	public Article(int seq) {
		this(seq, "제목없음", "익명");
	}
	
	public Article(int seq, String subject) {
		this(seq, subject, "익명");
	}
	
	@Override
	public String toString() {
		return "Article [seq=" + seq + ", subject=" + subject + ", writer=" + writer + "]";
	}
}
public class Main03 {
	public static void main(String[] args) {
		// Article 클래스의 기능 확인
		Article a1 = new Article(1);
		System.out.println(a1.toString());
		
		Article a2 = new Article(2, "테스트 게시물");
		System.out.println(a2.toString());
		
		Article a3 = new Article(3, "두 번째 게시물", "자바학생");
		System.out.println(a3.toString());
	}
}
이 포스트는 itpaper.co.kr에서 제공되는 강의자료를 바탕으로 작성되었습니다.
profile
그림 그리는 백엔드 개발자

0개의 댓글