객체 지향 언어의 핵심은 추상화입니다.
가장 관련된 개념인 추상클래스와 인터페이스에 대해 알아보도록 합시다!
문제 상황 정의
1. 대학 구성원에 대한 엔티티를 설계하고 있습니다. 학부생, 대학원생, 교수가 대학 구성원에 속합니다.
학부생, 대학원생, 교수 모두 공통적으로 K 대학교의 멤버 (Member) 입니다. 멤버로서 공통적인 인스턴스 변수를 가질 것이기 때문에 Member 클래스를 정의하고 이를 상속받아 학부생(Undergraduate), 대학원생(Graduate), 교수(Professor) 를 정의하도록 해보겠습니다.
Member 클래스 정의 - 추상클래스 (Abstract Class)
public abstract class Member {
String identificationNumber;
String name;
abstract void introduce();
public Member(String identificationNumber, String name) {
this.identificationNumber = identificationNumber;
this.name = name;
}
}
먼저 추상메소드란 구현부가 없는 메소드를 가르킵니다. 예시에서는 introduce() 가 정의되어 있을 뿐 구현부 (몸통) 가 없습니다.
미완성 메소드인 추상 메서드를 가진 클래스를 추상 클래스라고 정의합니다. 위의 코드에서는 추상 클래스 Member 가 추상 메소드 introduce() 를 가집니다.
그렇다면 왜 추상 클래스를 선언할까요? Member 클래스를 UnderGraduate, Graduate, Professor 에 상속시킬거면 일반 클래스로 선언해서 상속시켜도 상관없지 않을까요?
public class Main {
public static void main(String[] args) {
Member member = new Member(); // 컴파일 에러!!
}
}
추상 클래스는 미완성 설계도 라고 불립니다. 미완성 설계도로는 작품을 완성할 수 없습니다. Member 라는 추상 클래스는 미완성 클래스이기 때문에 그 자체로 인스턴스화 할 수 없습니다. 프로그래머에게 Member (추상 클래스) 를 인스턴스화 할 수 없음을 알리는데 쓰이는 것입니다. (추상 클래스는 일반 클래스에 상속시켜 구현해야 한다는 강제성을 부여합니다.)
추상 클래스는 추상 클래스를 상속 받을 수 있다!
새로운 조건이 추가되었습니다. 학부생과 대학원생 모두 학생 (Student) 입니다. 대학교의 학생은 모두 지도교수를 인스턴스 변수로 가집니다.
문제 상황 정의
2. 학부생(Undergraduate) 과 대학원생 (Graduate) 는 Student 이다. Student 는 지도교수를 가진다.
public abstract class Student extends Member{
String advisorProfessor;
public Student(String identificationNumber, String name, String advisorProfessor) {
super(identificationNumber, name);
this.advisorProfessor = advisorProfessor;
}
}
public class Main {
public static void main(String[] args) {
Student student = new Student(); // 컴파일 에러
}
}
Student 추상 클래스를 정의하며 advisorProfessor (지도교수) 인스턴스 변수를 추가로 만들었습니다. 추상 클래스이기 때문에 Student 클래스를 인스턴스화 할 수 없습니다.
❗갑자기 든 의문! 왜 인스턴스화 할 수 없는 추상 클래스에 생성자 선언을 할까?
상속 받는 클래스에서 생성자를 사용하기 위함입니다. Member 를 상속받는 Student 의 경우에도 super() 생성자를 호출하여 인스턴스 변수 초기화를 하였습니다. 두 가지 상황을 비교해보겠습니다.
- 부모 클래스에 생성자가 정의되어 있다면
- 부모 클래스에 생성자가 없다면
// 1. 부모 클래스에 생성자가 정의되어 있다면
public abstract class Student extends Member{
String advisorProfessor;
public Student(String identificationNumber, String name, String advisorProfessor) {
super(identificationNumber, name);
this.advisorProfessor = advisorProfessor;
}
}
// 2. 부모 클래스에 생성자가 없다면
public abstract class Student extends Member{
String advisorProfessor;
public Student(String identificationNumber, String name, String advisorProfessor) {
// super(); 컴파일러가 자동 주입
this.identificationNumber = identificationNumber;
this.name = name;
this.advisorProfessor = advisorProfessor;
}
}
2023.07.14 - [📌Programming Language/Java] - [Java] 생성자 super() 와 참조변수 super
생성자에 대한 기억을 되살려봅시다!
Object 클래스를 제외한 모든 클래스의 생성자 첫 줄에는 생성자 (같은 클래스의 다른 생성자 또는 조상의 생성자) 를 호출해야 합니다. 그렇지 않으면 컴파일러가 자동적으로 super(); 를 생성자의 첫 줄에 삽입합니다.
2번 케이스의 경우 컴파일러가 super()를 주입합니다. 하지만 이 차이 빼고는 코드에 아무 지장이 없긴 합니다.
그렇다면 왜 부모 추상 클래스에 생성자를 선언하고 자손 추상 클래스에서 super() 로 호출할까요? 고민해보았는데 이건 캡슐화 이슈 때문인거 같습니다. 부모 클래스의 인스턴스 변수는 부모 클래스 안에서 해결이 되도록 감추는 것! 자식 클래스는 부모 클래스의 인스턴스 변수를 건드리지 않도록 하는 것이 바람직해서가 아닐까 싶습니다.
(지극히 개인적인 의견이므로 다른 견해를 알려주신다면 감사하겠습니다..)
나머지 코드를 완성해봅니다.
public class UnderGraduate extends Student {
@Override
public void introduce() {
System.out.println("안녕하세요. 학부생이고 학번은 " + identificationNumber + " 이름은 " + this.name + "입니다. " +
"지도 교수님은 " + advisorProfessor + "입니다.");
}
public UnderGraduate(String identificationNumber, String name, String advisorProfessor) {
super(identificationNumber, name, advisorProfessor);
}
}
public class Graduate extends Student {
@Override
public void introduce() {
System.out.println("안녕하세요. 대학원생이고 학번은 " + identificationNumber + " 이름은 " + this.name + "입니다. " +
"지도 교수님은 " + advisorProfessor + "입니다.");
}
public Graduate(String identificationNumber, String name, String advisorProfessor) {
super(identificationNumber, name, advisorProfessor);
}
}
public class Professor extends Member{
@Override
void introduce() {
System.out.println("안녕하세요. 교수고 학번은 " + identificationNumber + " 이름은 " + this.name + "입니다. ");
}
public Professor(String identificationNumber, String name) {
super(identificationNumber, name);
}
}
public class Main {
public static void main(String[] args) {
Professor professor = new Professor("1998127127", "오박사");
UnderGraduate underGraduate = new UnderGraduate("2010123123", "김학부생", "오박사");
Graduate graduate = new Graduate("2023203712", "이대학원생", "오박사");
System.out.println("professor.identificationNumber = " + professor.identificationNumber);
professor.introduce();
underGraduate.introduce();
graduate.introduce();
}
}
❗이슈 발생 - identificationNumber 인스턴스 변수가 그대로 접근이 가능하다
professor.identificationNumber 로 그대로 접근 가능하다는 것은 캡슐화를 위반한다. 그렇다면 인스턴스 변수는 private 으로 선언하고 getter 를 public 로 열어두도록 한다.
getter 메소드까지 상속되기 때문에 이를 사용해서 인스턴스 변수에 접근하도록 한다.
결과 코드
public abstract class Member {
private String identificationNumber;
private String name;
abstract void introduce();
public Member(String identificationNumber, String name) {
this.identificationNumber = identificationNumber;
this.name = name;
}
public String getIdentificationNumber() {
return this.identificationNumber;
}
public String getName() {
return this.name;
}
}
public abstract class Student extends Member{
private String advisorProfessor;
public Student(String identificationNumber, String name, String advisorProfessor) {
super(identificationNumber, name);
this.advisorProfessor = advisorProfessor;
}
public String getAdvisorProfessor() {
return advisorProfessor;
}
}
public class UnderGraduate extends Student {
@Override
public void introduce() {
System.out.println("안녕하세요. 학부생이고 학번은 " + getIdentificationNumber() + " 이름은 " + getName() + "입니다. " +
"지도 교수님은 " + getAdvisorProfessor() + "입니다.");
}
public UnderGraduate(String identificationNumber, String name, String advisorProfessor) {
super(identificationNumber, name, advisorProfessor);
}
}
public class Graduate extends Student {
@Override
public void introduce() {
System.out.println("안녕하세요. 대학원생이고 학번은 " + getIdentificationNumber() + " 이름은 " + getName() + "입니다. " +
"지도 교수님은 " + getAdvisorProfessor() + "입니다.");
}
public Graduate(String identificationNumber, String name, String advisorProfessor) {
super(identificationNumber, name, advisorProfessor);
}
}
public class Professor extends Member{
@Override
void introduce() {
System.out.println("안녕하세요. 교수고 학번은 " + getIdentificationNumber() + " 이름은 " + getName() + "입니다. ");
}
public Professor(String identificationNumber, String name) {
super(identificationNumber, name);
}
}
public class Main {
public static void main(String[] args) {
Professor professor = new Professor("1998127127", "오박사");
UnderGraduate underGraduate = new UnderGraduate("2010123123", "김학부생", "오박사");
Graduate graduate = new Graduate("2023203712", "이대학원생", "오박사");
System.out.println("professor.identificationNumber = " + professor.getIdentificationNumber());
professor.introduce();
underGraduate.introduce();
graduate.introduce();
}
}
지금까지 추상 클래스를 이용한 추상화에 대해 알아보았습니다.
하지만 추상화에 쓰이는 또 다른 상속 방법인 인터페이스가 남았습니다.
인터페이스에 대해 알아보고 예시에 적용해보도록 하겠습니다.
인터페이스 (interface)
추상메서드만 가지는 집합입니다. (엄밀히 말하면 다른 메소드, 상수를 가질 수 있긴 하지만 핵심은 추상메소드입니다.) 인터페이스의 모든 메소드는 디폴트가 public abstract 이고 상수는 public final static 입니다. 생략도 가능합니다.
interface PlayingCard {
public static final int SPADE = 10; // 상수
final int DIAMOND = 3; // public static 생략 가능
static int HEART = 2;
int CLOVER = 1;
public abstract String getCardNumber(); // 추상 메소드
String getCardKind(); // public abstract 생략 가능
}
또한 인터페이스의 조상은 인터페이스만 가능합니다. 그리고 인터페이스는 다중 상속이 가능한데 이는 추상메서드는 구현부가 없어서 충돌해도 문제가 없기 때문입니다.
인터페이스는 구현을 통해 추상메소드를 완성합니다. (implements) 그리고 리턴타입으로도 지정 가능합니다.
이제 개념을 다 훑었고 예시로 돌아가서 적용해보도록 하겠습니다.
문제 상황 정의
3. 랩실에는 대학원생과 교수님만 출입할 수 있습니다. 학부생은 포함되지 않습니다.
추상 클래스만 배운 상태에서 보면 대학원생 (Graduate) 과 교수 (Professor) 의 공통 분모가 있습니다. 그렇다면 상속이 떠오르긴 하는데 추상 클래스로 또 상속한다면? 랩출입가능 클래스 (labEnterable) 를 어디에 끼워줘야할까요?
대학원생 (Graduate) 과 교수 (Professor) 의 부모 클래스를 만들자니 상당히 복잡합니다! 이 때 쓸 수 있는 것이 (인터페이스) interface 입니다.
인터페이스는 서로 관계없는 클래스들을 관계를 맺어줄 수 있습니다. 코드로 확인해보도록 하겠습니다.
public interface LabEnterable {
void enter();
}
public class Graduate extends Student implements LabEnterable{
@Override
public void introduce() {
System.out.println("안녕하세요. 대학원생이고 학번은 " + getIdentificationNumber() + " 이름은 " + getName() + "입니다. " +
"지도 교수님은 " + getAdvisorProfessor() + "입니다.");
}
public Graduate(String identificationNumber, String name, String advisorProfessor) {
super(identificationNumber, name, advisorProfessor);
}
@Override
public void enter() {
System.out.println("대학원생 " + getName() + " 가 출입합니다.");
}
}
public class Professor extends Member implements LabEnterable{
@Override
void introduce() {
System.out.println("안녕하세요. 교수고 학번은 " + getIdentificationNumber() + " 이름은 " + getName() + "입니다. ");
}
public Professor(String identificationNumber, String name) {
super(identificationNumber, name);
}
@Override
public void enter() {
System.out.println("교수 " + getName() + " 가 출입합니다.");
}
}
public class Main {
public static void main(String[] args) {
Professor professor = new Professor("1998127127", "오박사");
UnderGraduate underGraduate = new UnderGraduate("2010123123", "김학부생", "오박사");
Graduate graduate = new Graduate("2023203712", "이대학원생", "오박사");
// underGraduate.enter() 컴파일 에러!!
professor.enter();
graduate.enter();
LabEnterable labEnterable = new Professor("1998127127", "오박사"); // 리턴 타입으로 가능
labEnterable.enter();
}
}
인터페이스는 리턴 타입으로도 사용 가능합니다. 위의 코드처럼 다형성 측면에서도 활용이 좋아보입니다.
추상클래스 vs. 인터페이스 무엇을 어디에 써야할까?
추상클래스와 인터페이스 모두 추상화에 쓰입니다. 하지만 여기서 차이는 인터페이스는 인스턴스 변수를 갖지 않는다는 것입니다. 대신, 인터페이스는 상수만을 가질 수 있으며 상속 가능합니다.
- 인스턴스 변수와 메소드 모두 상속시켜야할 때 - 추상 클래스
- 메소드만 상속시킬 때 - 인터페이스
하지만! 둘 다 써볼 수 있다면 인터페이스를 사용하도록 하자!
추상 클래스는 다중 상속이 불가하지만 인터페이스는 가능하다. 다형성을 유용하게 이용하려면 인터페이스가 좋다.
'📗Java' 카테고리의 다른 글
[Java] 쓰레드 동기화 (0) | 2023.08.29 |
---|---|
[Java] String 클래스 메소드, StringBuffer, StringBuilder (0) | 2023.07.23 |
[Java] Chained Exception (0) | 2023.07.20 |
[Java] default 메소드는 왜 생겼는가? (0) | 2023.07.19 |
[Java] 생성자 super() 와 참조변수 super (0) | 2023.07.14 |