
다시 시작
추상클래스
앞서 클래스를 객체의 상태, 기능들을 보관한 설계도라고 표현했는데, 추상클래스는 미완성 설계도라고 볼 수 있겠다. '미완성'의 뜻은 미완성 메서드(기능이 없이 선언만된 메서드)를 포함하고 있다는 뜻이다.
따라서 추상클래스는 그 자체로는 제대로된 기능을 할 수 없으나 새로운 클래스를 만드는데 사용되는 틀, 템플릿의 기능을 하고, 그 자체로는 기능을 할 수 없기 때문에 추상클래스의 인스턴스는 만들 수 없다.
abstract class Member { }

이 앞 게시물에서 다뤘던 예시들을 그대로 사용 중이다. 여기서는 Member 클래스를 추상클래스로 선언 후 인스턴스를 만들면 터지는 오류
위의 예시처럼 추상클래스는 단지 선언부에 abstract만 붙이면 된다. 추상메서드가 포함되었는지 아닌지의 여부는 상관하지 않는다.
추상메서드
추상메서드는 선언부만 작성된 메서드다. 따라서 기능이 없다. 실제로 구현할 기능은 하위클래스가 상속받아서 작성해야한다.
예를들어서
package hongikUniv;
import javax.swing.*;
abstract class Member {
int age;
String name;
public Member() {
age = 30;
name = "최원준";
}
public abstract void display();
}
class Student extends Member {
int grade;
public Student() {
super();
grade = 4;
}
public void display() {
System.out.println("이름 : " + this.name + " 나이 : " + this.age + " 학년 : " + this.grade);
}
}
class Programmer extends Member{
String language;
public Programmer() {
super();
language = "java";
}
public void display() {
System.out.println("이름 : " + this.name + " 나이 : " + this.age + " 사용언어 : " + this.language);
}
}
이렇게 멤버의 정보를 알려주는 display() 메서드가 있을때, Member 클래스에 추상메서드로 display()를 선언한 후 그에 맞춰 코드를 작성하면 된다.
사실 메서드에 굳이 abstract 를 붙이지 않아도 추상메서드로 인식된다. 다만 abstract를 붙이는 이유는 abstract가 붙었다면 하위클래스가 반드시 그 클래스를 구현해야하기 때문이다. 일종의 명시적인 강제성부과를 통해 기능구현이 누락되는 것을 방지하기 위함이다.
그래서 abstract로 지정된 추상클래스를 상속받는 하위클래스는 오버라이딩을 통하여 모든 추상메서드를 구현해야한다. 그렇지 않다면 하위클래스도 추상클래스로 지정해야한다.
public static void main(String[] args) {
Member member1 = new Student();
Member member2 = new Programmer();
member1.display();
member2.display();
}
지금이야 간단한 코드이기 때문에 어떤 객체가 생성되고 기능하는지 명확하게 알지만, 나중이되면 해당 메서드가 실제로 어떻게 구현되어있지 몰라도 선언부, 메서드명, 매개변수만 넘겨주어서 실행이 가능하도록 기능할 수 있다. 나중에 디자인패턴 같은거 공부하다보면 인터페이스와 함께 메서드나 객체가 어떻게 생겼는지, 어떤 객체인지 몰라도 원활히 기능할 수 있도록 코드를 짤 수 있고, 그렇게 해야한다.
가령 멤버들을 배열에 넣어두고나서 display() 를 실행해야한다면
public static void main(String[] args) {
Member[] members = new Member[3];
members[0] = new Student();
members[1] = new Programmer();
members[2] = new Student();
for (Member member : members) {
member.display();
}
}
이렇게 반복문을 돌면서 상위클래스인 Member 객체의 display() 메서드만 호출해도 Student와 Programmer의 display() 메서드가 자동으로 실행된다.

추상클래스를 사용하지 않았다면 Programmer와 Student의 공통된 멤버가 없어서 배열을 따로 관리해야했을 것이다.
이렇듯 추상클래스, 추상메서드는 클래스의 공통된 부분을 뽑아서 하나의 조상클래스를 만들어, 공통된 기능의 틀을 제공한다. 이를 추상화라고하며 이와 반대되는, 상속을 통해 클래스를 구현하며 메서드의 기능을 작성하는 것을 구체화라고한다.
인터페이스
인터페이스는 극단적인 추상클래스다. 인터페이스에서는 추상메서드 이외의 멤버 (변수도 포함한다)를 가질 수 없다. 오직 추상메서드로만 구상되어있어야 한다. 다만 JDK8부터는 정적메서드와 디폴트메서드는 추가할 수 있다. JDK9부터는 private 메서드도 추가할 수 있다. 근데 왜지
다만 정적메서드, 디폴트메서드, private 메서드는 부가기능이고, 인터페이스의 본질은 추상메서드의 집합이다.
interface Member {
void display();
}
Member 클래스를 인터페이스로 바꿨을 때
메서드를 display() 메서드만 가지고 있었으므로 이렇게 선언할 수 있다. 모든 메서드는 public abstract로 선언되어야하기 때문에 생략되어도 컴파일러가 자동으로 추가해주고, 변수 역시 public static final로 선언되야하기 때문에 변수만 선언해도 컴파일러가 자동으로 추가해준다.
interface displayable {
void display();
}
interface movable {
void move(String destination);
}
interface groups extends displayable, movable {
}
따라서 인터페이스에는 특정 기능구현만을 해야한다. 위의 예시에서는 display 기능과 move 기능만을 구현했고, 이를 상속받은 groups 인터페이스에는 위 두 개의 기능이 모두 구현되어있다. 일반클래스와 달리 인터페이스는 다중상속이 가능하다.
그래서 인터페이스의 이름에는 행위에 필요한 메서드를 제공해서 ~able로 끝나는 이름이 많다.
interface displayable {
void display();
}
interface movable {
void move(String destination);
}
interface groups extends displayable, movable {
}
abstract class Member implements groups {
int age = 28;
String name = "윤마치";
}
인스턴스 변수가 필요한 기능에 대해서는 하나의 상위클래스가 인터페이스의 구현체가 되어 거기서 인스턴스 변수를 선언해야한다.
인터페이스 역시 그 자체로는 인스턴스가 될 수 없다. 따라서 선언만된 메서드의 실제 기능을 구현해야하는데, 이를 구현체라고 한다. 구현체를 만든다는 것은 인터페이스의 기능을 구현하는 것을 뜻한다.
그래서 상속과는 개념이 달라, extends가 아닌 implements를 사용한다.
역시 모든 기능을 구현해야하며, 그렇지 않을 경우 추상클래스로 등록해야한다.
class Student extends Member {
int grade;
public Student() {
super();
grade = 4;
}
public void display() {
System.out.println("이름 : " + this.name + " 나이 : " + this.age + " 학년 : " + this.grade);
}
public void move(String destination) {
System.out.println(destination);
}
}
class Programmer extends Member{
String language;
public Programmer() {
super();
language = "java";
}
public void display() {
System.out.println("이름 : " + this.name + " 나이 : " + this.age + " 사용언어 : " + this.language);
}
public void move(String destination) {
System.out.println(destination);
}
이렇게하면 나머지 기능은 기존과 동일하게 기능한다.
이러면 move와 display의 기능이 groups 인터페이스에 상속되고, 그 groups 인터페이스의 구현체인 Member 추상클래스를 Student와 Programmer가 상속받아 인터페이스의 기능을 구현해 사용할 수 있는 것이다,
또 인터페이스에서 메서드를 받아 사용할 경우, 반드시 public으로 구현해야한다. 왜냐하면 인터페이스에서의 메서드는 오직 public abstract로만 선언할 수 있기 때문에, 구현하는 메서드 역시 public으로 작성해야한다. 오버라이딩 원칙
인터페이스는 다중상속인가요?
헤드퍼스트자바를 읽을 때, 자바는 다중상속이 불가하지만 인터페이스를 이용해 이를 해결할 수 있다는 투로 설명되긴 했다. 다만 이런 경우는 거의 없다.
인터페이스의 변수의 경우에는 오직 정적변수만 선언가능하기 때문에 클래스명.변수명을 사용하면 끝이라 중복이나 혼동의 여지가 없다. 또 메서드는 어차피 선언부만 있으므로 상위클래스에서 받은 내용과 선언부가 일치하는 경우에는 그냥 상위클래스의 내용을 따르면 된다.
interface movable {
void move(String destination);
}
interface groups extends displayable, movable {
void move(String destination);
}
이래봤자 어차피 껍데기만 있을 뿐 내용은 없으므로 그냥 상위클래스의 것을 사용하면 된다.
이러면 다중상속문제를 인터페이스와 함께 해결할 수 있는데,
interface displayable {
void display();
}
interface movable {
void move(String destination);
}
class People {
public void walk() {
System.out.println("walk");
}
}
abstract class Member {
int age = 28;
String name = "윤마치";
}
class Student extends Member implements movable {
int grade;
People people = new People();
public void walk() {
people.walk();
}
}
약간 억지로 만들어낸 예시긴한데, Student 클래스가 Member 클래스와 People 클래스를 다중상속해야할 때, People 클래스의 인스턴스를 인스턴스변수로 선언한 후에 구현할 기능에 대한 인터페이스의 구현체에 인스턴스의 메서드를 넣어주는 방식으로 해야한다.
인터페이스의 다형성
다형성을 배울 때 하위클래스의 인스턴스를 상위클래스의 참조변수로 참조하는 것이 가능하다고 했고, 인터페이스에서도 동일하다. 이 때 구현체의 인스턴스를 하위클래스의 참조변수로 참조할 수 있고, 매개변수로 넘겨주는 등 다형성으로 할 수 있는 전반의 것이 인터페이스에도 적용된다. 형변환도 마찬가지고 으 머리아파
다형성을 사용할 때 형변환은 반드시 상속관계에서만 가능했으나 인터페이스의 경우에는 인터페이스와 구현체 간에서만 가능하다.
groups[] groupList = new groups[3];
groupList[0] = new Student();
groupList[1] = new Programmer();
groupList[2] = new Student();
for (groups group : groupList) {
group.display();
}
이런식으로.. groups 인터페이스가 displayable 인터페이스와 movable 인터페이스를 상속했으므로 display() 기능을 지원한다.
abstract class Member implements groups {
int age = 28;
String name = "윤마치";
public groups disdis() {
groups gr = new Student();
return gr;
}
}
Member 클래스에 다음과 같은 메서드를 만들고
System.out.println(member.disdis().getClass().getSimpleName());
이 코드를 실행시키면

이렇게 뜬다.
즉, 리턴타입을 인스턴스로 설정하는 것은 인터페이스가 반환되는 것이 아닌 인터페이스의 구현체가 반환된다. 실제로 인터페이스는 인스턴스를 만들 수 없으므로 진짜 인터페이스가 반환되지 못하기도 하고
또 인터페이스만 전달되면 거기서 사용가능한 멤버가 없기 때문에 (인터페이스에는 변수가 등록되어있지도 않고 단지 인터페이스에 해당하는 메서드만 불러올 수 있기 때문에..) 필요한 객체로 형변환을 해야한다. instanceof 연산을 활용해서 형변환을 해야한다.
인터페이스의 장점
그렇다면 이 인터페이스는 왜 쓰는 것일까? 사실 나도 처음 개발할 때는 인터페이스를 왜 작성하는지 몰랐다. 어차피 구현체에 수정이 발생하면 인터페이스도 수정해야하고 귀찮기도하고.. 물론 현재는 설계만 제대로하면 인터페이스의 능력을 십분 활용할 수 있기 때문에 요긴하게 쓰이는 편이다.
암튼
1. 표준화가 가능
이게 제일 큰 특징이 아닐까 생각한다. 유연한 개발을 하기 위해서는 각자의 클래스 / 메서드가 이게 어느 객체인지 몰라도 그냥 실행만 시키고 리턴값을 반환하고 끝나는 형태로 이루어져야한다고 생각하는데 인터페이스를 사용하면 이게 학생이거나, 프로그래머거나, 작가이거나에 상관없이 그냥 공통된 인터페이스를 구현하고 있다면 인터페이스를 불러와서 실행시키만하면 되기 때문이다. 하나의 인터페이스를 공유하면 여러 개발자들이 개발을 진행해도 정형화된 프로그래밍이 가능하다.
2. 개발시간 단축
이 객체가 어떤 기능을 담아야할지 생각하지 않고 설계과정에서 인터페이스의 구현체로만 설정해준다면 메서드의 선언부만 알면 되기 때문에 개발시간이 단축된다.
3. 서로 무관한 클래스에게 관계를 맺어줄 수 있다.
예를들어.. 토끼 객체와 사람 객체가 있다고할 때, 어차피 움직임은 그냥 좌표값만 조정하면 되는데 그렇다고 하나의 상위클래스로 묶자니 움직임 이외에는 여러 가지가 다르다. 따라서 그냥 Movable 인터페이스의 구현체로만 설정해주면 되기 때문에 관계없는 클래스 간 관계를 맺을 수 있다.
4. 독립적인 프로그래밍 가능
OCP 원칙인 변경에 열려있다는 원칙을 지킬 수 있다. 직접적인 관계를 이용한다면 메서드나 클래스 이름 등에 수정사항이 생기면 이를 하나하나 변경했어야했는데 인터페이스를 구현하고 인터페이스를 호출하는 방식으로 변경하면 하나의 클래스가 변경되어도 딱히 영향을 끼치지 않는다. 왜냐하면 뼈대는 이미 인터페이스가 갖고 있기 때문에 거기서 자잘한 수정사항만 있기 때문이다.
이게 인터페이스로 개발을 해보거나 디자인패턴을 조금 배워보면 약간의 감을 잡을 수 있을 것 같다. 그래서 헤드퍼스트자바나 헤퍼디 정도는 한 번정도 읽어보면 좋을 것 같다. 진짜 인터페이스를 사용하는 원인은 디자인패턴과 관련있기 때문에..
또 인터페이스의 구현체끼리 기능도 공유하지만, 중복상속 때문에 상속을 통해 기능공유가 불가능할 경우, 해당 인터페이스의 공통된 기능을 구현한 구현체를 만든 후, 이 구현체의 인스턴스를 불러와 공통된 기능의 구현체로 삼으면 된다. 이는 자바의정석 7장 425페이지를 확인해보면 이해하기 쉽다.
인터페이스
인터페이스는 두 가지 사항을 알고있어야한다.
- 클래스를 사용하는 쪽과 클래스를 제공하는 쪽이 있다.
- 메서드를 호출하는 쪽에서는 선언부만 알면 된다.
직접적인 클래스 간 연결의 경우에는 한쪽이 변경되면 다른쪽도 변경되어야한다.
Member m = new Programmer();
m.display();
꾸준히 나오는 이 예시에서 만약 display가 인터페이스로 구현되어있지 않고 그냥 클래스에 작성된 코드인데, 만약 갑자기 Programmer 객체가 없어지고 모든 객체가 Writer 객체로 변경된다면? 저 Programmer 객체를 하나하나 확인해가며 객체를 변경해야하고, 또 만약 display() 메서드에 수정사항이 생긴다면 메서드를 호출하는 부분도 수정해야한다.
interface displayable {
void display();
}
interface movable {
void move(String destination);
}
interface groups extends displayable, movable {
}
abstract class Member implements groups { //내용작성하기 }
근데 만약에 display() 메서드를 인터페이스에 담으면?
Programmer 객체가 Writer 객체로 변경된다고하더라도 Writer 객체를 displayable의 구현체로 선언하면 그냥 display() 메서드를 불러올 수 있다. 또 매개변수로 받을 때도 displayable 타입을 매개변수로 받으면 어느 객체가 들어와도 display() 메서드를 실행시킬 수 있다. 이렇게 어느 한 타입에 고정되지 않고 유연하게, 널널하게 개발할 수 있다. 이 때 display() 메서드를 불러오는 메서드는 실제 구현체가 존재하는지, 어느 클래스인지 몰라도된다. 단지 displayable의 구현체인지만 알면 된다.
추가로 인터페이스를 매개변수로 받아도 Object 클래스의 멤버를 호출할 수 있다.
디폴트 / static / private
인터페이스에 JDK8부터 디폴트와 static, JDK9부터 private 메서드를 추가할 수 있다. 추상메서드가 아니다. 우선 static은 이미 알고있기도하고, 인스턴스를 생성하지 않고 클래스이름.메서드이름 으로 불러올 수 있으므로 인터페이스 내에서 static 메서드의 선언은 이전부터 문제될 것이 없었다. 물론 static메서드만을 예외로 둘 수는 없어서 그냥 불가처리했던 것
디폴트메서드는 추상메서드의 기본적인 구현을하는 메서드다. 추상메서드가 아니기 때문에 구현체들이 구현할 필요가 없다. 다만 디폴트메서드의 이름이 충돌하는 경우가 있는데,
만일 여러 인터페이스 간 디폴트메서드가 겹친다면 구현체가 반드시 오버라이딩을 해야하며,
디폴트메서드와 상속받은 메서드가 겹칠 경우에는 디폴트메서드가 버려진다.
private 메서드의 경우에는 구현체나 하위클래스에 영향을 미치지 않기 때문에 추가되어도 문제가 없다. 주로 코드 정리, 중복코드 제거 등에 사용된다. 반드시 구현되어야하며, 정적메서드로도 선언할 수 있다.
일케 끝
엄... 이렇게 인터페이스와 추상클래스에 대해서도 알아봤다. 바로 전에 다형성할 때도 얘기했지만 역시 입문서 뒤에는 하나의 심화교재가 필요하다. 인터페이스와 추상클래스는 헤드퍼스트자바로 배웠지만, 디자인패턴을 배우기 전까지는 왜 써야하는지, 어떻게 써야하는지는 잘몰랐는데 책을 읽고나니 그래도 어느정도 느낌을 알 것 같다. 그치만 자바의정석을 읽다가 이펙티브자바를 보게되면 너무 어렵단말이지..
다음글은 아마 내부클래스가 될 것 같은데.. 헤드퍼스트자바에서는 진짜 아주 간략하게 다뤘던 내용이라 좀 궁금하긴하다. 왜 쓸까? 그리고 가끔 내부클래스를 사용한 예시를 봤는데 어떻게 쓸지, 어느 기능으로 구현될지 궁금하다.
'언어공부 > Java | Kotlin' 카테고리의 다른 글
| [CS] 이펙티브자바 독서 #1 - 아이템15 ~ 17 (0) | 2025.10.11 |
|---|---|
| [CS] 자바의 정석 독서 #13 - 내부클래스 (0) | 2025.10.10 |
| [CS] 자바의 정석 독서 #11 - 다형성 (1) | 2025.10.09 |
| [CS] 자바의 정석 독서 #10 - 패키지와 접근제어자 (0) | 2025.10.02 |
| [CS] 자바의 정석 독서 #9 - 상속 (0) | 2025.10.02 |