인터페이스란 추상클래스와 유사하다.
인터페이스란 우리가 앞에서 배운 추상클래스와 상당히 유사하다. 인터페이스는 추상클래스와 같이 추상메소드를 가질 수 있으며, 추상클래스와 동일하게 인터페이스 자체만으로 인스턴스를 생성할 수 없다. 그러나, 추상클래스보다 추상화 정도가 높아서 일반 메소드는 가질 수 없다. 즉, 추상클래스는 추상메소드와 일반 메소드를 모두 가질 수 있는 반면, 인터페이스는 오직 추상 메소드만으로 이루어져있다.
또한, 인터페이스는 추상클래스와 마찬가지로 자식 클래스에서 추상메소드를 오버라이딩 해야 한다.
일반적으로 인터페이스는 반드시 메소드를 구현해야 하기 때문에 코드의 가이드라인을 잡는데 사용한다.
예를들어 100명이 개발을 할때 로그인 기능을 하는 메소드를logIn()
, Login()
, userLogin()
등 각자의 스타일에 맞게 이름을 작성하다 보면 유지보수가 상당히 어려워진다. 그럴때 코드 이름을 통일할때 인터페이스로 구현해서 인터페이스를 상속받아 사용하도록 하면서 코드의 유지보수를 쉽게 할 수 있도록 돕는다.
인터페이스는
interface
키워드를 이용해 작성한다.
인터페이스는 엄연히 말하면 클래스랑은 다르다. 따라서 인터페이스를 작성할때는 아래와 같이 class
대신 interface
라는 키워드를 사용해 작성을 한다.
public interface TestInterface {
}
또한, 인터페이스의 접근제어자는 public
과 default
만 가능하다.
인터페이스를 작성할때는 멤버변수는 public static final
만을 사용해야 하며, 메소드는 public abstract
키워드를 붙여야만 한다.
이를 생략하면 컴파일시 컴파일러가 자동으로 추가한다.
추가적으로, jdk1.8(JAVA8)이상 버전에서는 인터페이스에 static
키워드를 붙이는 static 메소드와 디폴트 메소드의 추가를 허용하고 있다.
public interface TestInterface {
int data1=10;// public static int data1=10;랑 같음.
public static float pi=3.14f;
public abstract void method1();
default void method2() {// default 메소드(JAVA8이상)
System.out.println("default method");
}
static void method3() {// static 메소드(JAVA8이상)
System.out.println("static method");
}
}
인터페이스는
implements
를 이용해 구현한다.
인터페이스 역시 추상클래스와 같이 그 자체로 인스턴스를 생성할 수 없기에 추상클래스가 상속을 받아 인스턴스를 생성하듯이 인터페이스는 구현
이라는 것을 이용해 인스턴스를 생성해야 한다.
인터페이스를 구현하기 위해서는 implements
키워드를 사용해 다음과 같이 클래스에 구현을 한다.
public class 클래스_이름 implements 인터페이스_이름{
...// 인터페이스에 있는 추상 메소드 정의
}
만악 인터페이스에 있는 메소드 중에서 일부만 사용하고 싶을 경우에는 추상클래스로 선언을 해서 구현을 해야 한다.
abstract class 클래스_이름 implements 인터페이스_이름{
...// 인터페이스에 있는 추상 메소드중 일부만 구현해도 됨
}
또한, 인터페이스는 상속과 동시에 구현이 가능하다.
abstract class A extends B implements 인터페이스_이름{
...// 인터페이스에 있는 추상 메소드중 일부만 구현해도 됨
}
이러한 경우 관계는 다음과 같다.
인터페이스는 인터페이스로부터만 상속받을 수 있으며, 다중상속이 가능하다.
앞서 말한바 있으나, 다시 한번 강조하면 인터페이스가 추상클래스와 비슷해서 인터페이스도 클래스의 한 종류라고 생각할 수 있으나, 인터페이스와 클래스는 다르다.
대표적인 차이점으로는 상속에 대한 차이점이 있다. 인터페이스 역시 클래스와 동일하게 extends
키워드로 상속받을 수 있다. 그러나, 인터페이스는 다중상속이 가능하다.
즉, 부모 인터페이스를 여러개 둘 수 있다.
java에서는 클래스의 경우 다중상속이 불가능하다. 그러나, 인터페이스는 클래스가 아니기 때문에 아래와 같이 다중상속이 가능하다. 그렇기에 유연한 프로그래밍이 가능하다.
public interface TestInterface extends interface1, interface2{
...// 인터페이스 내용
}
그렇다면 실제로 앞에서 배운 인터페이스의 구현과 다중상속을 어떻게 사용하는지 알아보자.
우리가 게임 캐릭터를 만든다 가정하자. 캐릭터 클래스는 Character
라고 하며, 이는 Unit
클래스를 상속받는다 가정하자.
Unit
public class Unit {
private float hp;// hp
private float mp;// mp
Unit(float hp, float mp){// 기본 hp,, mp설정
this.hp=hp;
this.mp=mp;
}
public boolean isDead() {// 유닛이 죽었는지 확인
if(hp>0) {
return true;
}
else {
return false;
}
}
public float getHp() {// 체력값 가져옴
return this.hp;
}
public float getMp() {// 마력값 가져옴
return this.mp;
}
public void SetHp(float hp) {// 체력값 설정
this.hp=hp;
}
public void setMp(float mp) {// 마력값 설정
this.mp=mp;
}
}
Character
public class Character extends Unit{
Character(){
super(100.0f, 100.0f);
}
Character(float hp, float mp){
super(hp, mp);
}
}
이때, 캐릭터에서 공격할 수 있는 기능을 인터페이스를 이용해 구현한다면 다음과 같다.(관계도에서 주황색은 인터페이스라고 하자.)
Fightable
public interface Fightable {
}
Character
public class Character extends Unit implements Fightable{
Character(){
super(100.0f, 100.0f);
}
Character(float hp, float mp){
super(hp, mp);
}
}
이때, 공격방법도 이동해서 공격하는 방법과 제자리에서 공격하는 방법, 공중으로 날아서 공격하는 방법이 있을 것이다. 물론, 이들을 전부 Fightable
인터페이스에 구현해도 되겠지만, 추후 비행하는 기능이 다른곳에서 필요한 경우 비행
자체의 기능을 다시 구현해야 할 수 있기 때문에 번거롭기 때문에 각각의 기능을 인터페이스로 구현을 하면 다음과 같다.
Movable
public interface Movable {
public abstract void walk(int x, int y);
public abstract void run(int x, int y);
public abstract void teleport(int x, int y);
}
Attackable
public interface Attackable {
public abstract void flatHit();// 평타
public abstract void skillHit(float damage);// 스킬
}
Flyable
public interface Flyable {
public abstract void slowFly(int x, int y);
public abstract void fastFly(int x, int y);
}
이때, 비행, 공격, 이동기능은 공격기능에 포함되기 때문에 Fightable
인터페이스에 상속을 한다.
Fightable
public interface Fightable extends Movable, Flyable, Attackable{
}
Character
public class Character extends Unit implements Fightable{
Character(){
super(100.0f, 100.0f);
}
Character(float hp, float mp){
super(hp, mp);
}
@Override
public void walk(int x, int y) {
// 걷기
}
@Override
public void run(int x, int y) {
// 달리기
}
@Override
public void teleport(int x, int y) {
// 순간이동
}
@Override
public void flatHit() {
// 평타
}
@Override
public void skillHit(float damage) {
// 스킬 공격
}
@Override
public void slowFly(int x, int y) {
// 저속비행
}
@Override
public void fastFly(int x, int y) {
// 고속비행
}
}
이와같이 Character
클래스에서는 싸우는 인터페이스만 implement
를 해도 인터페이스의 다중상속을 이용해 싸우기 위해 필요한 기능들을 구현할 수 있다. 뿐만 아니라, Fightable
인터페이스만 확인해도 전투를 위해서는 어떤 동작(기능)이 필요한지 구조적으로 쉽게 알 수 있다.
인터페이스 역시 추상클래스와 마찬가지로 다형성을 사용할 수 있다.
인터페이스에서 구현을 하면 추상클래스의 상속과 마찬가지로 다형성을 사용할 수 있다.
직접 코드로 알아보자.
앞에서 만든 게임에서 내용을 추가해보자. 캐릭터를 힐러, 전사, 마법사로 분류하고, 전사와 마법사만 싸울 수 있다고 가정하자.
그렇다면 싸울 수 있다는 Fightable
인터페이스의 관계는 다음과 같아져야 한다.
Unit
public class Unit {
private float hp;// hp
private float mp;// mp
Unit(float hp, float mp){// 기본 hp,, mp설정
this.hp=hp;
this.mp=mp;
}
public boolean isDead() {// 유닛이 죽었는지 확인
if(hp>0) {
return true;
}
else {
return false;
}
}
public float getHp() {// 체력값 가져옴
return this.hp;
}
public float getMp() {// 마력값 가져옴
return this.mp;
}
public void SetHp(float hp) {// 체력값 설정
this.hp=hp;
}
public void setMp(float mp) {// 마력값 설정
this.mp=mp;
}
public String getName() {
return "기본유닛";
}
}
Character
public class Character extends Unit implements Movable{
Character(){
super(100.0f, 100.0f);
}
Character(float hp, float mp){
super(hp, mp);
}
@Override
public void walk(int x, int y) {
// 걷기
}
@Override
public void run(int x, int y) {
// 달리기
}
@Override
public void teleport(int x, int y) {
// 순간이동
}
}
전사
public class Warrior extends Character implements Fightable, Attackable{
@Override
public void flatHit() {
// 평타
}
@Override
public void skillHit(float damage) {
// 스킬공격
}
public String getName() {
return "전사";
}
}
마법사
public class Wizard extends Character{
public String getName() {
return "마법사";
}
}
힐러
public class Healer extends Character{
public String getName() {
return "힐러";
}
}
이때, 전사, 마법사, 힐러는 결국 캐릭터이기에 Character
클래스를 상속받았다.
이를 관계도로 나타내면 다음과 같다.
관계도를 설명하면 다음과 같다.
기본적으로, 캐릭터는 움직이는 기능이 있기 때문에 Movable
인터페이스를 구현한다. 또한, 싸울 수 있는 캐릭터는 전사이기에 Fightable
인터페이스는 Warrior
클래스에 구현을 한 것이다.
이제, 무기상점에서는 싸울 수 있는(Fightable
인터페이스를 구현한) 캐릭터의 경우 무기를 구매할 수 있다고 가정하고 코드를 작성하면 다음과 같다.
public class WeaponStore implements Fightable{
void BuyWeapon(Fightable f) {
System.out.println("무기 구매 완료!");
}
}
위 코드를 보면 Fightable
을 인자값으로 받았음을 알 수 있다. 이는 Fightable
인터페이스를 구현하거나 상속받은 모든 클래스에서 적용이 가능하다.
이를 기반으로 메인함수를 작성하면 다음과 같다.
public class Main {
public static void main(String[] args) {
Unit[] characters= {
new Warrior(),
new Healer(),
new Wizard()
};
WeaponStore store = new WeaponStore();
for(Unit u: characters) {
System.out.print(u.getName()+": ");
if(u instanceof Fightable) {// 무기구매 여부 확인
store.BuyWeapon( (Fightable)u );
}
else {
System.out.println("무기 구매 불가.");
}
}
}
}
output
전사: 무기 구매 완료!
힐러: 무기 구매 불가.
마법사: 무기 구매 불가.
이와같이 다형성을 이용해 특정 인터페이스에서만 무기를 구매하는 코드 역사 작성할 수 있다.
지금까지 인터페이스에 대해 알아봤다. 인터페이스는 추상클래스와 비슷하면서도 조금 다른것을 알 수 있었다. 그렇다면 이러한 인터페이스의 장점에 대해서 알아보자.
인터페이스의 장점은 다음과 같다.
1. 개발시간 단축
2. 코드의 표준화
mySql.connect()
에서 oracleDB.connect()
로 전부 바꿔야 할 것이다. 그러나, 데이터베이스 접근을 인터페이스로 규정하면 선언부 이름이 동일하기 때문에 추가적으로 수정을 하지 않고, 인터페이스 구현부(데이터베이스 제어하는 부분)만 수정하거나 새로 정의하면 된다.getHp()
, 몬스터의 체력을 가져오는것은 getMonsterHp()
등 체력 데이터를 가져오는 동일한 작업에도 여러 스타일의 코드를 작성하게 될 수 있다. 이러한 일을 방지하기 위해 먼저 인터페이스를 만들고, 개발자가 메소드를 만들때는 인터페이스를 구현하는 방식으로 작성하게 하면 메소드 이름을 통일할 수 있어 가독성과 유지보수가 쉬워진다.만약 인터페이스가 작성이 되면 이를 기반으로 메소드의 호출이 가능하다. 이때, 메소드 내부의 내용이 바뀌어도 메소드의 선언부는 바뀌지 않기 때문에 언제든지 호출이 가능하며, 인터페이스를 구현하는 클래스에서는 메소드의 선언부만 유지하면 되기 때문에 자유롭게 개발을 할 수 있다.
인터페이스에 대해 조금 더 자세한 이해를 위해 앞의 데이터베이스 예를 구현해 보면서 알아보자.
데이터베이스의 경우 각 데이터베이스 회사마다 연결방법이 차이가 있어 알아서 잘
연결해주는 드라이버
라는것이 있다. 문제는 이 드라이버들이 이름도 각각 다르고, 코드도 다르다.
예를 들어보자.
mysql이라는 데이터베이스를 연결하고 싶다면 mysql.connect();
을 이용한다 하자. 또한, oracleDB라는 데이터베이스를 연결하고 싶을때는 oracle.connect();
라는 메소드를 사용한다 가정하자.
우리가 기존에 mysql을 사용하는 코드를 인터페이스 없이 작성하면 다음과 같을 것이다.
제조사에서 제공하는 코드(예시)
public class MySQL {
public void connect() {
System.out.println("mysql 연결됨.");
}
}
우리가 작성하는 코드
public class Main {
public static void main(String[] args) {
MySQL db = new MySQL();
db.connect();
}
}
output
mysql 연결됨.
그러나, 만약 어떠한 이유로 인해 mysql에서 oracleDB로 데이터베이스를 바꾼다면 코드는 다음과 같이 바뀌어야 할 것이다.
제조사에서 제공하는 코드(예시)
public class OracleDB {
public void connect() {
System.out.println("oracle DB 연결됨.");
}
}
우리가 작성하는 코드
public class Main {
public static void main(String[] args) {
OracleDB db = new OracleDB();
db.connect();
}
}
우리가 직접 작성해야 하는 코드가 달라졌음을 알 수 있다. 또한, 실제로 웹개발을 할때는 여러 회사의 데이터베이스를 사용하는 경우가 많이 있다. 만약 mysql과 오라클 데이터베이스를 둘 다 사용한다고 가정하면 코드가 이렇게 바뀌게 된다.
public class Main {
public static void main(String[] args) {
OracleDB oracle = new OracleDB();
MySQL mysql = new MySQL();
mysql.connect();
oracle.connect();
}
}
코드를 보면 분명 데이터베이스를 연결
하는 동일한 작업임에도 불구하고 데이터베이스가 다르다는 이유로 데이터베이스를 연결
하는 동일한 동작을 하는 코드가 중복이 됬다. 이때, 인터페이스를 사용해추상화를 하면 코드의 중복을 막을 수 있다.
oracle DB나 mysql이나 결국 데이터베이스에 연결을 하는것이기 때문에 DataBase
라는 이름으로 인터페이스를 만들어 추상화를 하면 다음과 같다.
인터페이스
public interface DataBase {
public abstract void connect();
}
oracle
public class OracleDB implements DataBase{
public void connect() {
System.out.println("oracle DB 연결됨.");
}
}
mysql
public class MySQL implements DataBase{
public void connect() {
System.out.println("mysql 연결됨.");
}
}
이제 어떤 데이터베이스를 선택할지 입력하면 알아서 잘
데이터베이스를 선택해주는 드라이버 메니저
라는것을 만들면 다음과 같다.
public class DriverManager implements DataBase{
private DataBase db;
DriverManager(String str){
setDB(str);
}
public void setDB(String db) {
if(db=="oracle") {
this.db = new OracleDB();
}
else if(db=="mysql") {
this.db = new MySQL();
}
else {
System.out.println("잘못 입력했습니다.");
}
}
@Override
public void connect() {
db.connect();
}
}
이제 메인메소드를 작성하면 다음과 같다.
public class Main {
public static void main(String[] args) {
DriverManager[] db= {
new DriverManager("oracle"),
new DriverManager("mysql"),
};
for(DriverManager database : db) {
database.connect();
}
}
}
output
oracle DB 연결됨.
mysql 연결됨.
이와같이 데이터베이스 회사가 다르더라도 인터페이스를 이용해 다형성을 적용하면 간단한 설정(인자값 입력등..)만으로도 각자 다른 데이터베이스에 접근할 수 있다. 실제로 java에서 데이터베이스를 제어하는 JDBC
라는것이 이와같이 인터페이스를 이용해 추상화를 해서 여러 데이터베이스에 접근 및 제어를 할 수 있도록 설계되 있다.
인터페이스와 추상클래스는 비슷한 부분이 많다. 둘 다 추상메소드를 사용하며, 상속이 가능하다. 반대로, 인터페이스는 다중상속이 가능한 반면, 추상클래스는 다중상속이 불가능하다. 또한, 인터페이스는 추상메소드로만 이루어져 있는 반면 추상클래스는 일반 메소드도 같이 있을 수 있다는 차이가 있다.
이와 같이 이 둘은 서로 비슷하지만 명백한 차이점(대표적으로 다중상속)이 있기 때문에 시용 용도가 다르다.
사용용도는 다음과 같다.
상속
을 통해서만 메소드의 기능구현이 가능하기 때문에 의존성이 높다. 쉽게 말해서 우리가 클래스 다이어그램을 그릴때 집합의 포함관계 혹은 화살표로 상속을 나타낸 것을 의존성
이라고 생각하자(의존성에 대해서는 추후에 다루도록 하겠다). 추상클래스로 구현을 하면 이 의존성이 높아지면서 객체간의 관계가 명확해진다. 그렇기 때문에 객체간의 관계를 명확하게 정의
할때 주로 사용하며, 멤버변수를 통합
할때 역시 많이 사용된다.한줄 요약 하면 우리가 상속에서 봤던 is-a에 해당하는 경우에 추상클래스를 사용한다.
구현
을 통해 메소드의 기능구현이 가능하기 때문에 어떠한 클래스의 기능
에 초점을 맞춰서 구현을 한다. 예를들어 Bird
라는 클래스의 인터페이스는 비행하는Flyable
, 독수리처럼 사냥을 하는 Huntable
과 같이 클래스의 기능
에 초점을 맞춰 개발을 한다. 또한, 관계없는 클래스를 하나의 공통기능으로 묶는데에도 사용을 한다.
예를들어 새를 나타내는 Bird
클래스와 호랑이를 나타내는 Tiger
라는 클래스는 조류와 포유류라는 전혀 다른 클래스임에도 불구하고, 사냥이 가능한 Huntable
로 묶을 수 있다. 이렇게 상속관계가 전혀 없는 클래스라도 인터페이스를 통해 묶어 자유롭게 추상화를 할 수 있다.한줄 요약 하면 has-a에 해당하는 경우에 추상클래스를 사용한다.
정리:
추상클래스: 상속관계를 만들고 싶을때(포유류->호랑이, 조류->독수리)
--> is a에 맞을때 사용인터페이스: 클래스의 기능을 만들고 싶을때(독수리(사냥), 호랑이(사냥))
-->has a, can에 맞을때
공감하며 읽었습니다. 좋은 글 감사드립니다.