2023.01.27 - 안드로이드 앱개발자 과정

CHA·2023년 1월 28일
0

Java



상속(Inheritance)

사용자 정보 데이터 [ 이름 , 나이 ] 를 클래스로, 회원 정보 데이터 [ 이름 , 나이 , 회원번호 ] 를 클래스로 만들고 싶습니다. 그런데, 회원 정보 데이터를 클래스로 만드는 과정에서 잘 생각해보니 사용자 정보 클래스의 내용과 중복되는 부분이 있었습니다! 그 중복된 부분을 활용할 수 있는 방법은 없을까요? 상속을 활용하면, 손쉽게 해결할 수 있습니다.


상속이란?


상속의 개념

부모클래스의 멤버를 그대로 상속받아 자식클래스에서 활용할수 있는 기법을 이야기합니다. 자바에서 상속의 사용법은 자식클래스 이름 옆에 extends 키워드를 붙이고 상속받고자 하는 부모클래스의 이름을 명시합니다.

public class Parent{ . . . }
class Child extends Parent { . . . } 

상속을 받게 되면 자식 클래스에 부모 클래스의 멤버가 들어옵니다. 이때, 부모의 멤버만 가져오는 개념이 아니고 부모의 객체도 같이 생성하는 개념입니다. 즉, 객체 안에 객체가 있다고 도식화해도 좋을것 같습니다. (메모리적인 부분은 당연히 다릅니다.)

상속을 이용한 객체지향적 코딩

간단한 예제를 통해 상속을 이용하는 방법과 객체지향적인 코딩은 어떤것인지 단계를 밟아가며, 가볍게 알아보도록 합시다.

먼저, First 클래스와 Second 클래스를 설계합시다. 그리고 각각의 멤버들을 선언해줍시다. 그리고 상속을 이용해, Second 클래스에서 First 클래스를 상속받아 보겠습니다.

------ First.java
public class First {
	int a;
}
------ Second.java
public class Second extends First{
	int b;
}
------ Main.java
public class Main {
	public static void main(String[] args) {
    	Second s = new Second();
		s.a = 10; 
		s.b = 20;
    }
}

Main 클래스에서 Second 클래스의 객체를 생성했으며, 객체를 통해 Second 멤버변수 b 의 값을 대입했고, 상속받은 First 클래스의 멤버변수 a 에 대해서도 값을 대입해주었습니다. 그런데, 객체지향 코딩에서 멤버변수를 직접 . 연산자를 통해 제어하는것은 권장하지 않습니다. 그 대신, 멤버값을 출력하는 기능을 만들어 사용할것을 권장합니다. 다음 코드를 봅시다.

------ First.java
public class First {
	int a;
    
    public void showFirst() {
    	System.out.println("a : " + a);
    }
}
------ Second.java
public class Second extends First{
	int b;
    
    public void showSecond() {
    	System.out.println("b : " + b);
    }
}
------ Main.java
public class Main {
	public static void main(String[] args) {
    	Second s = new Second();
		s.showFirst();
		s.showSecond();
    }
}

이렇게 하면 각 객체의 멤버변수들은 직접적으로 건드리지 않으면서 원하는 값을 출력할 수 있게됩니다. 그런데, 위 코드를 가만히 살펴보니 Second 의 객체는 a와 b 모두 사용하려고 만든 객체입니다. 그러니 출력기능도 a와 b 모두 한꺼번에 출력해주는것이 좀 더 합리적으로 보입니다. 다음과 같이 바꿔봅시다.

------ First.java
public class First {
	int a;
    
    public void showFirst() {
    	System.out.println("a : " + a);
    }
}
------ Second.java
public class Second extends First{
	int b;
    
    public void show() {
    	showFirst();
    	System.out.println("b : " + b);
    }
}
------ Main.java
public class Main {
	public static void main(String[] args) {
    	Second s = new Second();
        s.show();
    }
}

만들고 보니 Main 클래스의 기능 코드가 훨씬 간단해졌음이 느껴질까요? 또한 show() 메소드 내부를 보면 showFirst() 를 호출하고 있습니다. First 클래스를 상속받았기에 객체를 생성할 필요없이 바로 호출이 가능함을 알 수 있습니다. 물론, System.out.println("a : " + a); 처럼 출력을 해도 같은 결과 이지만, 객체지향적으로 봤을 때, a의 값을 출력하는 기능은 a 를 멤버변수로 갖는 First 클래스 에서 하는것이 바람직합니다. 그러면 이번에는 기능을 추가해봅시다. 멤버변수에 값을 대입해주는 기능입니다. 다음 코드를 봅시다.

------ First.java
public class First {
... 중략
	void setFirst(int a) {
    	this.a = a;
    }
}
------ Second.java
public class Second extends First{
... 중략
	void setMembers(int a, int b) {
    	// this.a = a;
        // super.a = a;
        setFirst(a);
        this.b = b;
    }
}
------ Main.java
public class Main {
	public static void main(String[] args) {
    	Second s = new Second();

        s.setMembers(50,60);
        s.show();
    }
}

Main 클래스에서는 setMembers() 호출을 통해 멤버값 설정후 show() 메소드를 호출합니다. First 클래스에서는 멤버변수의 값을 설정해주는 setFirst() 메소드를 멤버로 가지고 있습니다. 유의깊게 봐야할 부분은 Second 클래스의 setMembers() 메소드 입니다. 주석처리를 해놓은 부분을 봅시다. this.a = a 의 경우 문법적으로는 오류가 아닙니다. 앞서 말했듯, 상속을 받게 되면 부모객체를 그대로 물려받는것이기 때문에 this 를 사용하여 부모 객체의 멤버변수에 접근하는것 또한 가능합니다. 다만, this 키워드는 보통 본인 클래스의 객체를 의미하는 키워드 입니다. 부모 객체의 멤버변수에 접근하려고 this 키워드를 쓰는것은 적절치 않아보입니다. 그래서 부모객체를 지칭하는 특별한 키워드가 있습니다. 바로 super 입니다. this 가 본인 객체를 지칭하는 키워드라면, super 는 부모의 객체를 지칭하는 키워드입니다. 그러면 위 코드에서 this.a = a 대신 super.a = a 는 어떨까요? 물론 이부분도 문법적으로는 오류는 없습니다. 다만, 객체지향적인 코딩에는 조금 안맞는것 같습니다. 자식 객체에서 부모 객체의 멤버변수에 접근해서 직접 값을 변경하는것이니까요. 그래서 setFirst() 메소드를 호출해서 값을 변경해주는것이 조금 더 적합해 보입니다.

상속 도식화 및 명칭


상속받은 클래스에서 상속한 클래스로 화살표를 표시합니다. 그리고 각 언어마다 상속받은 클래스와 상속한 클래스를 부르는 용어가 다릅니다. 그런데 시간이 흐르면서 각 언어에서 따로 부르던 용어들이 서로 섞여졌습니다. 그래서 언어별로 따로 부르는것의 의미가 퇴색되었습니다.

C#

parent (부모) - child (자식)

Java

super - sub

C++

base - driven


상속과 생성자


생성자

First 클래스 예제로 생성자에 대한 개념을 되짚어 봅시다. 또한 접근제한자도 활용해봅시다.

public class First {
	private int a;
    
	public void showFirst() {
		System.out.println("a : " + a);
	}
	
	public First(int a) {
		this.a = a;
		System.out.println("First 객체가 생성되었습니다. int");
	}
}

생성자는 객체를 생성(new) 할 때 자동으로 발동하는 특별한 메소드라고 했습니다. 그리고 접근제한자의 지정도 가능하며, 매개변수를 통해 값을 전달받는것도 가능합니다. 다만, 리턴값은 명시하면 안되며, 생성자 메서드의 이름은 클래스 이름과 동일해야 합니다.

상속과 생성자

First - Second 클래스로 상속되었을때, 자식 클래스의 객체를 생성하면 부모 클래스의 생성자도 호출된다!~
super 키워드

이번에는 First 클래스와 Second 클래스를 통해 상속과 생성자에 대해 알아봅시다. Second 클래스는 First 클래스를 상속받는 클래스 이며, 객체 생성시 생성자는 어떻게 호출되는지 알아보겠습니다.

--------- First.java
public class First {
	private int a;
    
	public void showFirst() {
		System.out.println("a : " + a);
	}
	
	public First() {
		System.out.println("First 객체가 생성되었습니다.");
	}
}
--------- Second.java
public class Second extends First {
	private int b;

	public void show() {
		showFirst(); 
		System.out.println("b: " + b);
	}
	
	public Second() {
		System.out.println("Second 객체를 생성했습니다! ");
	}
}

--------- Main.java
public class Main {
	public static void main(String[] args) {
		First f = new First();
        System.out.println();
		Second s = new Second();
	}
}

출력결과 
First 객체가 생성되었습니다. 

First 객체가 생성되었습니다.
Second 객체를 생성했습니다!

Main 클래스에서 First와 Second 클래스의 객체를 생성했습니다. 그에 따라 각각 객체들의 생성자가 호출 되었습니다. 출력결과에서 볼 수 있듯, First 를 상속받은 Second 클래스의 생성자가 호출될 때, 부모 클래스의 생성자 또한 호출되는것을 알 수 있었습니다. 또한 그 순서가 부모 클래스가 먼저라는 점 또한 알 수 있습니다. 즉, 자식 클래스의 객체를 생성할 때 부모 클래스의 객체 또한 생성이 된다는 것입니다. 그러면 이번에는 생성자를 통해 값을 대입해 봅시다.

--------- First.java
public class First {
	... 중략 
    
   	public First(int a) {
		this.a = a;
		System.out.println("First 객체가 생성되었습니다. int");
	}
}
--------- Second.java
public class Second extends First {
	... 중략
    
   	public Second(int a, int b) { 
	super(a); 
	this.b = b;
	}
}

--------- Main.java
public class Main {
	public static void main(String[] args) {
    	... 중략
        
        Second s2 = new Second(10,20);
	}
}

각각 객체의 생성자를 오버로딩 해서 값을 대입할 수 있게 짠 코드입니다. Second 의 오버로딩된 생성자에서 super(a) 를 통해 부모의 생성자를 호출했습니다. super.a = a; 를 이용할 경우 부모의 멤버가 private 로 접근제한자가 되어있기 때문에 에러를 발생시킵니다. 그래서 this() 생성자와 기능이 비슷한 super() 생성자를 통해 부모의 생성자를 호출하는 방식을 이용했습니다. 그러면 First 클래스의 오버로딩된 생성자를 통해 First 멤버 변수의 값을 제어할 수 있게됩니다. 그런데 앞에서자식 클래스의 객체를 생성할 때, 부모 클래스의 객체 또한 생성된다고 이야기했습니다. 사실, 자식 클래스의 생성자에는 부모 클래스의 생성자를 호출하는 문법이 숨겨져있습니다.

public Second() {
	// super();
    ... 중략
}

위 처럼 자식 생성자 안에 super(); 가 숨겨져있는 형태입니다. 그래서 자동으로 부모의 생성자를 호출할 수 있는것 입니다.

상속의 여러가지 방식

  • 줄줄이 상속

    First , Second, Third 클래스를 통해 상속의 상속을 알아봅시다!
    public class Third extends Second{
    	public Third() {
    		super()
    		System.out.println("Third 객체 생성");
    	}
    }
    First 클래스를 상속받은 Second 클래스, 다시 Second 클래스를 상속받은 Third 클래스가 있습니다. 여기서 Third 클래스의 객체를 생성하게 되면 부모클래스들인 First 클래스와 Second 클래스의 생성자도 함께 호출됩니다. 이 역시 마찬가지로 First 클래스의 생성자 호출이 가장 먼저 이뤄지며, 그 뒤로 Second 생성자, Third 생성자가 호출됩니다.
  • 너만 입이냐 ?

    First 클래스는 Second 클래스에게 상속을 해주었습니다. 그와 동시에 다른 클래스에게도 상속이 가능합니다. 아래 코드는 Test 클래스를 설계하고 First 클래스를 상속받은 코드입니다.
    public class Test extends First {
    		public Test() {
          	System.out.println("Test 생성자");
          }
    }
    출력결과 
    First 객체가 생성되었습니다. 
    Test 생성자

상속의 기능은 부모 클래스의 기능과 변수들을 편리하게 사용할 수 있어서 아주 편리한 문법입니다. 다만, 상속을 받아 사용한다면 부모 클래스에서 정의해놓은 기능을 그대로 받아서 써야합니다. 확장성에서 좋지 못한 결과를 낳을것 같습니다. 만일, 부모 클래스의 기능을 그대로 받아오긴 하되, 조금씩 변경을 할 수 있다면 어떨까요?


메소드 오버라이드(Override)


상속을 하면 부모클래스의 기능을 그대로 전수가능합니다. 다만, 약간의 불편함은 부모클래스에서 정의된 기능을 그대로 써야한다는 점이죠. 그래서 탄생한 개념이 메소드의 오버라이드 입니다. 다음 로봇 예제를 통해 메소드 오버라이드에 대해 알아봅시다!

1. 효율적으로 새로운 로봇 만들기

Robot 클래스를 설계하고, Robot 클래스를 상속받는 FlyRobot 클래스를 만들어 봅시다. 이러한 과정을 통해 새로운 객체를 만들때 상속이 얼마나 효과적인지 다시한번 되짚어 봅시다.

-------- Robot.java
public class Robot {
	int HP;
	
	void move() {
		System.out.println("아장아장~");
	}
	
	void attack() {
		System.out.println("주먹발사!");
	}
}
-------- FlyRobot.java
public class FlyRobot extends Robot{
	void fly() {
		System.out.println("날자~");
	}
}
-------- Main.java
public class Main {

	public static void main(String[] args) {
    	Robot r = new Robot();
		r.move();
		r.attack();
		
		FlyRobot fr = new FlyRobot();
		fr.move();
		fr.attack(); 
		fr.fly(); 
	}
}
/* 출력결과

아장아장~
주먹발사!

아장아장~
주먹발사!
날자~

FlyRobot 또한 로봇입니다. 로봇이 가지고 있는 기능은 그대로 가지고 있으면서, 추가로 나는 기능을 추가하고싶습니다. 그래서 기존 Robot 클래스를 상속받았으며, 추가로 나는 기능을 만들어줬습니다. 그래서 출력을 해보니 잘 상속이 되었음을 알 수 있습니다.

2. 우리는 새로운 기능을 원한다!

상속은 잘 되었음을 확인했습니다. 그런데, 나는 기능을 가진 로봇이 주먹발사를? 이건 좀 이상한것 같습니다. 그래서 우리는 부모 클래스로부터 상속받은 기능을 지금부터 재정의 합니다. 이를 메소드 오버라이드 라고 합니다.

-------- Robot.java
public class Robot {
	int HP;
	
	void move() {
		System.out.println("아장아장~");
	}
	
	void attack() {
		System.out.println("주먹발사!");
	}
}
-------- FlyRobot.java
public class FlyRobot extends Robot{
	void fly() {
		System.out.println("날자~");
	}
    void attack() {
		System.out.println("미사일 발사!");
	}
}
-------- Main.java
public class Main {

	public static void main(String[] args) {
    	Robot r = new Robot();
		r.move();
		r.attack();
		
		FlyRobot fr = new FlyRobot();
		fr.move();
		fr.attack(); 
		fr.fly(); 
	}
}
/* 출력결과 
아장아장~
주먹발사!

아장아장~
미사일 발사!
날자~

메소드의 이름, 리턴타입 등은 그대로 작성해주고, 실행문 부분만 바꿔주어 기능을 달리하게 하는 기법이 메소드 오버라이드 입니다. 당연하게, 부모 객체와 자식 객체는 서로 다른 객체이기 때문에 같은 메소드를 작성한다고 해서 오류는 아닙니다. 다만, . 연산자를 이용하면 자식 클래스의 메소드 부터 보이게 됩니다.

3. 구관이 명관이지

앞서 보았던 예제중에서 나는 기능을 가진 로봇은 주먹발사가 아닌 미사일을 발사해야했습니다. 즉, 기존에 로봇이 가지고 있던 기능 대신 미사일 발사로 대체된겁니다. 물론 새로운 기능을 통째로 만드는것도 좋지만 기존에 가지고 있던 기능을 살리려면 어떤 방법을 사용해야 할까요? super 키워드를 사용하면 해결됩니다. FlyRobot 이 미사일 발사 기능외에 기존 기능인 주먹발사 기능도 포함해야 한다고 합시다.

-------- FlyRobot.java
public class FlyRobot extends Robot{
	void fly() {
		System.out.println("날자~");
	}
    void attack() {
    	super.attack();
		System.out.println("미사일 발사!");
	}
}

부모 객체의 주솟값을 가지고 있는 super 키워드를 통해 부모 객체의 attack() 메소드를 호출했습니다. 그러면 이제 FlyRobot 은 미사일 발사 기능 뿐 아니라 주먹 발사의 기능 또한 가질 수 있게 되었습니다.


상속 종합 예제 - 대학교 정보제공 프로그램


우리는 상속, 생성자, 접근제한자, 오버라이드, super 등을 활용하여 상속과 관련한 예제들을 살펴봤습니다. 이제 대학교 정보제공 프로그램 하나를 만들면서 종합적인 복습을 해보겠습니다.

이 프로그램은 회원정보를 저장하는 기능을 갖는 프로그램 입니다. 각각 회원의 정보를 전달받아 출력하는 코드를 짜봅시다.

이 프로그램을 사용하는 회원은 일반회원, 학생회원, 교수회원, 근로학생회원 으로 총 4종류의 회원이 있습니다. 각각 다음과 같은 정보를 갖습니다.

  1. 일 반 : 이름, 나이
  2. 학 생 : 이름, 나이, 전공
  3. 교 수 : 이름, 나이, 연구과제
  4. 근로 학생 : 이름, 나이, 전공, 업무

그러면 각각의 회원을 클래스로 만들고, 각 회원의 정보를 받을 멤버변수, 객체에 정보를 저장할 기능을 만들어 출력하는 코드를 짜봅시다.

먼저, 일반회원 클래스 입니다.

public class Person {
	private String name;
    private int age;
    
    public Person() {
    	this.name = "no name";
        this,age = 0;
    }
    public Person(String name, int age) {
    	this.name = name;
        this.age = age;
    }
    
    public void show() {
    	System.out.println("name : " + name);
        System.out.println("age : " + age);
    }
}

사용자가 입력을 해주지 않았을 경우의 생성자와, 입력했을 때의 생성자를 오버라이드 하여 생성했습니다. show() 메서드를 통해 정보를 출력합니다.

두번째, 학생회원 클래스 입니다.

public class Student extends Person{
	private String major;
    
    public Student() {
    	super();
        this.major = "";
    }
    public Student(String name, int age, String major) {
    	super(name, age)
        this.major = major;
    }
    
    public void show() {
    	super.show();
        System.out.println("major : " + this.major);
}

Student 클래스의 객체를 생성하면 Person 클래스의 객체도 생성됩니다. 상속이 되어있기 때문에 super() 를 이용하여 멤버변수의 값을 초기화 해줄 수 있습니다. 또한, show() 메서드를 호출하여 메소드 오버라이드도 적용해보았습니다.

세번째, 교수회원 클래스 입니다.

public class Professor extends Person {
	private String subject = "";
    
    public Professor() {
    
    }
    public Professor(String name, int age, String subject) {
    	super(name,age);
        this.subject = subject;
    }
    
    public void show() {
    	super.show();
        System.out.println("subject : " + subject);
    }
}

멤버변수 subject 의 초기화를 진행했습니다. 그와 더불어 public Professor() 생성자에는 빈 실행문으로 작성했습니다. 자식 생성자를 생성하면 부모 생성자는 자동으로 생성이 되기 때문에 사실 super() 가 실행된것과 동일한 결과를 보여줍니다. 나머지 부분은 Student 클래스와 같습니다.

네번째, 근로학생회원 클래스 입니다.

public class AlbaStudent extends Student{
	private String task;
    
    public AlbaStudent() {
    	super();
        this.task = "";
    }
    public AlbaStudent(String name, int age, String major, String task) {
    	super(name, age, major);
        this.task = task;
    }
    
    public void show() {
    	super.show();
        System.out.println("task : " + task);
    }
}

상속의 상속 개념을 이용했습니다. Person 을 상속받는 Student 를 AlbaStudent 가 상속받아 활용해보았습니다. 나머지는 동일합니다.

마지막으로 정보전달과 출력을 위한 Main 클래스 입니다.

public class Main {

	public static void main(String[] args) {
	
    Person p = new Person("sam",20);
	p.show();
		
	Student stu = new Student("robin",23,"android");
	stu.show();
	
	Professor pro = new Professor("park",45,"mobile");
	pro.show();
	
	AlbaStudent alba = new AlbaStudent("hong",27,"ios","pc Manager");
	alba.show();
	}
}

이렇게 해서 상속이 마무리 되었습니다. 상속은 우리에게 아주 편리한 기능을 마련해주는 장치 입니다. 꼭 숙지해서 잘 활용하도록 합시다.


Final 키워드


final 이란?

단어의 뜻 그대로, 마지막 이라는 뜻을 담고 있습니다. 이 키워드는 변수, 메소드, 클래스에 각각 사용할 수 있는데 마지막 변수, 마지막 메소드, 마지막 클래스 라는 기능을 하게 해줍니다. 이게 어떤 말인지는 알아보면서 느껴봅시다.


변수, 메소드, 클래스, 그리고 final

  1. 변수 : final [데이터타입][변수이름]
    멤버 변수에 붙는 final은 한번 결정된 변수의 값을 변경하지 못하게 해줍니다. 값을 변경하려 하면 에러를 발생시킵니다. 또한 변수 이름은 대문자로, 두 단어 이상일 경우 _ 를 이용해서 작성해야 합니다. static 변수와 매개변수에도 적용할 수 있습니다.

  2. 메소드 : [접근제한자] final [리턴타입][메소드명] ()
    메소드에도 final 키워드를 적용할 수 있습니다. 그러면 마지막 메소드라는 의미는 무엇일까요? final 변수가 값의 변경을 막았던것 처럼 메소드도 변경을 막는 의미입니다. 즉, 메소드의 오버라이드를 막는 기능을 합니다.

  3. 클래스 : [접근제한자] final class [클래스명]
    클래스도 마찬가지 입니다. final 클래스의 경우 상속되는것을 막아주는 기능을 합니다. 단, 다른 클래스로부터 상속을 받을수는 있습니다.


final에 대해 자바가 특이한 점

자바에서는 다른 언어들과 다르게 final 로 변수를 설정할 때 초기화를 하지 않아도 에러를 띄우지 않습니다. 자동으로 개발자의 실수라고 판단하여 에러를 띄우지 않다가 추후에 값의 변경이 1번 일어나면 그 이후로 변경이 일어나면 그때부터 에러를 띄웁니다. 단, 이 부분은 오해의 소지가 있을 수 있기 떄문에 사용하는것을 자제합시다.




Q. 매개변수가 없는 생성자를 만들면, 사용자 입장에서는 오히려 오류를 일으킬 수 있지 않을까?

나는 기본생성자가 있으면 사용자로부터 입력을 받을 때 오히려 오류를 만들어낼거라고 생각했는데 생성자를 만들때에는 매개변수가 없는 기본생성자와 매개변수가 있는 생성자 두 개를 만들어놓는것이 정보처리 측면에서 더 효율적이라고 한다! 기본 생성자를 만드는것은 데이터의 유연성을 위한것! 기본생성자 없이 만들면 객체를 만들때 반드시 값을 결정하게 됨. 생성자 만들때 결정하고 싶지 않고 나중에 결정하고 싶을때 setter 와 getter 를 이용해서 값을 결정해줄수 있기 때문에 기본 생성자도 만들어 놓는것이 낫다! 후에 안드로이드에서 이 부분이 활용될만한 부분이 있을것!

Q. 상속의 메모리 구조?

상속을 받으면, 부모 객체의 멤버들을 자식 객체에 그대로 복사를 하는걸까? 아니면 객체의 주솟값을 어딘가의 참조변수로 할당을 하는건가?

상속을 하고 자식 객체를 생성하면 자식의 객체와 부모의 객체가 heap 영역에 생성된다. 그리고 자식클래스 타입의 참조변수에 자식 객체의 주솟값을 담았다고 하자, 그러면 부모 객체는 어떻게 되는걸까?

profile
Developer

0개의 댓글