티스토리 뷰
인터페이스는 객체의 사용 방법을 정의하는 데 사용되는 추상 타입이다.
그럼 왜 인터페이스를 사용해야하고, 이점은 무엇이 있는지에 대해 알아보려고 한다.
인터페이스의 다중 구현과 다중 상속
클래스의 상속 관계에서는 다중 상속이 불가능하다.
자식 클래스가 둘 이상의 부모 클래스로부터 동일한 시그니처를 갖는 메서드를 상속 받는다고 했을 때, 자식 클래스는 어떤 부모의 메서드를 상속 받아야 할 지 모르기 때문이다.
반면에, 인터페이스는 다중 상속과 다중 구현이 가능하다.
다중 구현
클래스는 여러 인터페이스를 구현할 수 있다.
클래스는 구현하고자 하는 각 인터페이스에 정의된 메서드를 구현해야 한다.
public interface Drivable {
void forward();
void reverse();
}
public interface Rechargeable {
void charge();
}
public class HybridCar implements Drivable, Rechargeable {
//Drivable 인터페이스
@Override
void forward() {
}
//Drivable 인터페이스
@Override
void reverse() {
}
//Rechargeable 인터페이스
@Override
void charge() {
}
}
다중 상속
인터페이스끼리는 다중 상속이 가능하다.
인터페이스끼리도 extends 키워드를 사용하여 서로를 상속받을 수 있어 여러 인터페이스의 기능을 조합하여 새로운 인터페이스를 만들 수 있다.
public interface Hybridable extends Drivable, Rechargeable {
void switchToElectric();
void switchToGasoline();
}
public interface Drivable {
void forward();
void reverse();
}
public interface Rechargeable {
void charge();
}
Hybridable 인터페이스는 Drivable, Rechargeable 인터페이스를 상속할 수 있다.
Hybridable 을 하위 인터페이스라고하고, Drivable과 Rechargeable을 상위 인터페이스라고 한다. 만약 하위 인터페이스를 구현하는 클래스가 있다고 하면, 하위 인터페이스의 메서드뿐만 아니라 상위 인터페이스의 모든 추상 메서드에 대한 실체 메서드를 가지고 있어야 한다.
public class HybridCar implements Hybridable {
@Override
void switchToElectric() {}
@Override
void switchToGasoline() {}
@Override
void forward() {}
@Override
void reverse() {}
@Override
void charge() {}
}
따라서 Hybridable 인터페이스를 구현하는 HybridCar 는 Hybridable 인터페이스의 메서드와 Hybridable 인터페이스가 상속하는 Drivable, Rechargeable 인터페이스의 메서드를 전부 구현해야 한다.
인터페이스를 사용하는 이유
개발 코드를 수정하지 않고 사용하는 객체를 변경 할 수 있기 때문이다.
이는 DIP 원칙과 관련이 있다.
DIP (Dependency Inversion Principle)
고수준 모듈은 저수준 모듈에 의존해서는 안되며 두 모듈 다 추상화에 의존해야 한다는 것이다.
• 고수준 모듈은 비즈니스 로직을 수행하는 모듈이다.
• 저수준 모듈은 실제 구현 세부 사항을 담고 있는 모듈이다.
//저수준 모듈
public class HybridCar {}
public class ElectricCar {}
//고수준 모듈
class Driver {
HybridCar hybridCar;
ElectricCar electricCar;
public void setElectric(ElectricCar electricCar) {
this.electricCar = electricCar;
}
public void setHybrid(HybridCar hybridCar) {
this.HybridCar = hybridCar;
}
}
public class Main {
public static void main(String[] args) {
Driver driver = new Driver();
//하이브리드차를 운전할 때
HybridCar car = new HybridCar();
driver.setHybrid(car);
//전기차를 운전할 때
ElectricCar car = new ElectricCar();
driver.setElectric(car);
}
}
위 예제에서 고수준 모듈은 Driver 클래스가 되고, 저수준 모듈은 HybridCar 와 ElectricCar 클래스이다.
문제점은, 고수준 모듈이 저수준 모듈(구현체)에 의존하고 있다는 것이다. 다시말해 저수준 모듈이 수정되거나 새롭게 추가될 경우 고수준 모듈도 함께 수정해야한다는 뜻이다. 예를 들어 내연기관차 클래스를 하나 더 생성하게 되면 Driver 클래스에는 내연기관차 클래스를 선언해야한다.
이러한 문제를 해결하기 위해서 인터페이스를 사용한다.
public interface Car {}
//저수준 모듈
public class HybridCar implements Car {}
public class ElectricCar implements Car {}
public class GasolineCar implements Car{}
//고수준 모듈
class Driver {
Car car; //저수준 모듈이 아닌 인터페이스에 의존한다.
public void setCar(Car car) {
this.car = car;
}
}
public class Main {
public static void main(String[] args) {
Driver driver = new Driver();
//하이브리드차를 운전할 때
Car car = new HybridCar();
driver.setCar(car);
//전기차를 운전할 때
Car car = new ElectricCar();
driver.setCar(car);
//가솔린차를 운전할 때
Car car = new GasolineCar();
driver.setCar(car);
}
}
구현체 클래스가 더 추가되어도 고수준 모듈인 Driver 클래스는 수정할 필요가 전혀 없다.
이는 고수준 모듈이 구체적인 클래스가 아닌 변하지 않을 가능성이 높은 인터페이스나 추상화된 상위 클래스에 의존해야 한다는 DIP 원칙을 준수하였기 때문이다.
그리고 만약, Car 인터페이스에 drive 기능이 있다고 가정해본다.
Driver가 drive 메서드를 호출하였을 때, Driver는 운전만 하면 끝이다. 즉, 전기냐 하이브리드냐 가솔린이냐에 따라 운전을 어떻게 하는지에 대해 관심이 없고 운전을 할 수만 있다면 된다는 것이다.
인터페이스를 사용한 이유를 개발 코드, 인터페이스, 구현체 관점에서 이야기 해보면 다음과 같다.
✍︎ 개발 코드 관점
인터페이스를 바라보게 된다면, 구현체가 추가되어도 수정해야 하는 코드가 줄어든다.
반면에 구현체를 바라보게 된다면 새로운 구현체가 추가될 때마다 관련된 모든 코드를 수정해야한다.
또한, 인터페이스에 정의된 기능만 신경쓰면된다. 구현체의 로직에 따로 신경 쓸 필요가 없다는 것이다.
✍︎ 인터페이스 관점
인터페이스는 다양한 구현체에 대해 공통된 기능을 정의한다. 인터페이스는 구현체를 기준으로 정의하는 것이 아니기 때문에 인터페이스는 변하지 않는다.
✍︎ 구현체 관점
구현체는 인터페이스에 정의된 기능에 대한 구현 로직에만 집중할 수 있다.
인터페이스 사용 시 주의해야 할 점
인터페이스를 구현할 때에는 객체지향 설계의 원칙인 ISP와 SRP 원칙을 따라 설계를 해야한다.
ISP (Interface Segregation Principle)
클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙으로, 인터페이스는 구체적이고 작은 단위로 분리되어있어야 한다는 것이다.
public interface Vehicle {
void switchToElectric();
void switchToGasoline();
void forward();
void reverse();
void charge();
}
Vehicle 인터페이스에는 탈 수 있는 모든 교통수단에 대한 기능들을 정의할 수 있다.
하지만, 정의된 기능들을 보면 하이브리드용 교통 수단만 구현할 수 있는 기능인 switch 메서드가 있고 전기를 이용하는 교통수단만 구현할 수 있는 기능인 charge 메서드가 있다.
해당 함수들이 필요하지 않는 구현체들은 Vehicle 인터페이스를 구현할 때에 구현 내용은 비어있을 것이다.
Vehicle 인터페이스는 ISP를 위반한 것이므로 다음과 같이 구체적으로 분리해야한다.
public interface Drivable {
void forward();
void reverse();
}
//전기를 사용하는 기계 전용
public interface Chargeable {
void charge();
}
//하이브리드 전용
public interface Hybridable {
void switchToElectric();
void switchToGasoline();
}
이 처럼 작은 인터페이스로 분리하면, 탈 수 있는 교통 수단들은 자신에게 필요한 인터페이스만 가져다가 구현할 수 있으며, 불필요한 코드가 발생하지 않게 된다.
SRP (Single Responsibility Principle)
클래스는 하나의 책임만 가져야 하며, 그 책임이 변경되는 이유도 하나여야 한다는 원칙이다.
public class Car {
//음악 재생하는 함수
public void playMusic() {
}
//타이어 교체하는 함수
public void changeTire() {
}
//운전하는 함수
public void drive() {
}
//정지하는 함수
public void stop() {
}
}
Car 클래스는 음악 재생, 타이어 교체, 운전에 대한 여러가지의 책임을 가지고 있다.
public class MusicPlayer {
public void playMusic() {
}
}
public class TireChanger {
public void changeTire() {
}
}
public class Driver {
public void drive() {
}
public void stop() {
}
}
이처럼, 책임별로 클래스가 설계되어야하며 각 클래스는 책임에 대한 내용이 변경될 때, 해당 책임과 관련된 클래스만 수정하면 되므로 다른 클래스에 미치는 영향을 최소화할 수 있다.
ISP를 위반할 경우, SRP도 함께 위반하게 되는데 예제로 확인해본다.
public interface Vehicle {
void switchToElectric();
void switchToGasoline();
void forward();
void reverse();
void charge();
}
위 인터페이스는 ISP를 위반한 인터페이스로, 위에서 어떻게 분리해야하는지 설명되어있으니 참고하면 좋다.
public class Airplane implements Vehicle {
//전기모드로 변환
@Override
public void switchToElectric() {
}
//가솔린모드로 변환
@Override
public void switchToGasoline() {
}
//전진
@Override
public void forward() {
}
//후진
@Override
public void reverse() {
}
//전기 충전
@Override
public void charge() {
}
}
ISP를 위반한 Vehicle 인터페이스를 구현한 Airplane 클래스는, Vehicle에서 정의된 모든 메서드를 구현해야 하므로, 실제로 자신에게 필요하지 않은 기능까지 구현해야한다.
이로 인해, 만약 Vehicle 인터페이스에 새로운 메서드가 추가되면 Airplane 클래스는 이를 전부 구현해야하며, 이는 Airplane 클래스가 여러 책임을 가지게 되어 SRP를 위반하는 상황을 초래한다.
ISP와 SRP는 독립적인 원칙이지만, ISP 위반할 경우에 클래스가 여러 책임을 지게 되어 SRP를 위반할 가능성이 커지며 SRP를 위반하면 인터페이스가 커져서 ISP를 위반할 수도 있으므로 주의해서 사용해야한다.
인터페이스는 객체의 사용방법을 정의하는 것으로 끝나는 것이 아니다.
인터페이스는 고수준 모듈은 저수준 모듈의 구체적인 구현에 의존하지 않도록하고, 다양한 구현체를 쉽게 교체할 수 있도록 한다.
인터페이스를 설계할 때에는 ISP, SRP 원칙을 준수하여 설계하여 인터페이스의 책임을 명확하게 하고 불필요한 의존성을 줄여야한다.
'Language > Java' 카테고리의 다른 글
예외와 에러에 대하여 (5) | 2024.11.03 |
---|---|
상속에 대하여 (0) | 2024.10.31 |
추상클래스와 인터페이스 (0) | 2024.10.22 |
싱글톤(Singleton)에 대하여 (0) | 2024.10.20 |
상속과 인터페이스의 다형성 (0) | 2024.10.20 |
- Total
- Today
- Yesterday
- nosql
- 추상클래스
- Red-Black Tree
- object
- Hash
- Sticky Session
- 인스턴스변수
- fail-fast
- @conditional
- Security
- 티스토리챌린지
- 인터페이스
- 자동구성
- HashSet
- syncronized
- fail-safe
- Spring
- spring boot
- AutoConfiguration
- Caching
- java
- 오블완
- Load Balancer
- 다중화
- nginx
- 고정 세션
- JPA
- 로드 밸런서
- 정적변수
- HashMap
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |