인터페이스와 abstract를 사용하는 이유
인터페이스와 abstract 클래스에 대해서 제대로 이해하려면 시스템을 만드는 절차를 알아야 한다.
어떤 시스템을 개발하든 간에 “방법론”이라는 것을 사용하여 개발한다.
방법론
: 시스템을 어떻게 만들 것인지에 대한 절차를 설명하고 어떤 산출물을 작성해야 하는지를 정리해 놓은 공동 절차
방법론의 일반적인 절차는 아래와 같다.
- 분석 - 요구사항 분석
- 설계 - 어떤 메서드를 만들 것인지, 데이터는 어떻게 저장할지
- 개발 및 테스트
- 시스템 릴리즈
이게 인터페이스와 abstract랑 뭔 상관인데?
- 설계 단계의 산출물을 문서에만 정리하면 나중에 메서드 관련 내용들이 변경되면 문서도 수정해야 하므로 2중 3중의 일이 된다.
이 설계 단계에서 인터페이스라는 것을 만들어 두면 개발할 때 메서드의 이름을 어떻게 할지, 매개 변수를 어떻게 할지 일일이 고민하지 않아도 된다. - 선언과 구현을 구분할 수 있다. 가장 일반적인 것이 DAO 패턴이다. 이 패턴은 데이터를 저장하는 저장소에서 원하는 값을 요청하고 응답을 받는다. 이 세상에는 여러 가지 종류의 DBMS가 있기 때문에 회원 정보를 확인하는 `MemberDAO`라는 인터페이스를 만든다고 생각했을 때, 어떤 DBMS를 사용해도 상관없도록 만들 것이다. 오라클을 사용하든 MongoDB를 사용하든 간에 결과만 제대로 넘겨주면 된다.
즉,
- 설계시 선언해 두면 개발할 때 기능을 구현하는 데만 집중할 수 있다.
- 개발자의 역량에 따른 메서드의 이름과 매개 변수 선언의 격차를 줄일 수 있다.
- 공통적인 인터페이스와 abstract 클래스를 선언해 놓으면, 선언과 구현을 구분할 수 있다.
인터페이스 예제
public interface MemberManage{}
public class MemberManageImpl implements MemberManage{}
인터페이스의 또 다른 용도는 외부에 노출되는 것을 정의해 놓고자 할 때 사용된다.
다시 말해서 `MemberManager`라는 클래스가 있는데, 이 클래스가 “저한테 직접 이야기하지 마시고요, 공식적인 것은 저의 대변인을 통해서 말씀하세요”라고 내놓는 대변인이 바로 인터페이스다.
만약 아래와 같이 컴파일하면 에러가 발생한다.
`MemberManage member = new MemberManage();`
컴파일러가 아무것도 구현해 놓지 않았는데, 왜 얘로 초기화하려는 것이냐? 라며 에러를 내뿜는 것이다.
`MemberManage member = new MemberManageImpl();`
이렇게 사용해야 한다.
로또 프로그램에서 로또 번호를 생성하는 번호 발급기를 예로 들어보자.
public interface LottoNumberGenerator {
List<Integer> generate();
}
6개의 숫자를 만드는 인터페이스를 생성하고
이 인터페이스를 자동 발급, 수동 발급 2가지 방법으로 구현할 수 있다.
public class AutoLottoNumberGenerator implements LottoNumberGenerator {
private static final int FROM_INDEX = 0;
private static final int TO_INDEX = 6;
private static final int MIN_NUMBER = 1;
private static final int MAX_NUMBER = 45;
private final List<Integer> numbers = new ArrayList<>();
public AutoLottoNumberGenerator() {
for (int i = MIN_NUMBER; i <= MAX_NUMBER; i++) {
numbers.add(i);
}
}
@Override
public List<Integer> generate() {
List<Integer> generatedNumbers = new ArrayList<>(shuffleAndExtractNumbers());
Collections.sort(generatedNumbers);
return generatedNumbers;
}
private List<Integer> shuffleAndExtractNumbers() {
Collections.shuffle(numbers);
return numbers.subList(FROM_INDEX, TO_INDEX);
}
}
public class ManualLottoNumberGenerator implements LottoNumberGenerator {
private static final String DELIMITER = ", ";
private static final Pattern PATTERN = Pattern.compile("^\\\\\\\\d{1,2}, \\\\\\\\d{1,2}, \\\\\\\\d{1,2}, \\\\\\\\d{1,2}, \\\\\\\\d{1,2}, \\\\\\\\d{1,2}$");
private final List<Integer> numbers = new ArrayList<>();
public ManualLottoNumberGenerator(String input) {
validateInput(input);
for (String number : input.split(DELIMITER)) {
numbers.add(Integer.parseInt(number));
}
}
private void validateInput(String input) {
if (isNotMatchPattern(input)) {
throw new IllegalArgumentException("입력값이 형식에 맞지 않습니다. ex) 1, 4, 16, 23, 44, 23");
}
}
private static boolean isNotMatchPattern(String input) {
return !PATTERN.matcher(input).matches();
}
@Override
public List<Integer> generate() {
Collections.sort(numbers);
return this.numbers;
}
}
abstract(추상) 클래스
추상 클래스란 클래스들의 공통되는 필드와 메서드를 정의한 클래스를 말한다.
미완성의 추상 메소드를 포함하고 있고 인터페이스와 달리 구현되어 있는 메서드가 있어도 상관없다.
또 인터페이스는 `static`이나 `final`메서드가 선언되어 있으면 안 되지만 추상 클래스는 있어도 된다.
이 추상 클래스를 상속받아 필요한 메서드나 필드만 추가로 정의하고, 추상 메서드를 오버라이딩하여 클래스를 확장시킬 수 있다.
사용 이유
공통적으로 사용하는 기능을 미리 구현해 놓을 때 유용하다.
개발자들마다 메서드명, 필드명 등을 다르게 정의하면 유지보수 및 관리에 문제가 발생한다.
따라서, 필드와 메서드 이름을 통일하여 유지보수성을 높이고 통일성을 유지할 수 있다.
규격에 맞게 소스가 구현되어 있기 때문에 구현부만 수정하면 손쉽게 기능 수정이 가능하기 때문이다.
언제 interface와 abstract를 사용할지 구분할까?
인터페이스는 'can-do'관계로 객체가 어떤 행동을 할 수 있는지를 정의할 때 사용한다.
- Dog는 Animal이면서 Runnable 인터페이스를 구현하여 달릴 수 있다.
- 여러 클래스가 공통의 행위를 구현해야 할 때 사용한다.
추상 클래스는 'is-a' 관계로 어떤 객체가 특정 타입의 "일종"임을 정의할 때 사용한다
- Dog는 Animal의 한 종류이다.
- 여러 클래스가 공통된 속성이나 동작을 가질 때
Interface를 사용해야 할 때
다양한 클래스에서 공통된 행위를 정의할 때
인터페이스는 서로 다른 클래스 간에 공통적인 행위를 지정할 때 유용하다. 예를 들어, 여러 종류의 동물(새, 고래, 곤충)이 각각 다르게 움직이지만, 모두 "날다" 또는 "수영하다"라는 공통 행위를 가질 수 있다.
다중 상속이 필요할 때
추상 클래스는 단일 상속만 가능하지만 인터페이스는 다중 상속이 가능하다. 즉, 객체가 다양한 역할을 수행해야 한다면 인터페이스를 사용하는 것이 더 적합하다.
ex) 클래스가 Flyable, Runnable, Drivable 같은 여러 행동을 동시에 가질 수 있을 때
public interface Drivable {
void drive();
}
public interface Flyable {
void fly();
}
public class FlyingCar implements Drivable, Flyable {
@Override
public void drive() {
System.out.println("Driving on the road");
}
@Override
public void fly() {
System.out.println("Flying in the air");
}
}
Abstract Class를 사용해야 할 때
상속 관계(계층 구조)를 표현할 때
추상 클래스는 "is-a" 관계를 명확히 나타낼 때 유용하다. 예를 들어 Car와 Truck은 추상 클래스 "Vehicle"의 하위 클래스가 될 수 있다.
상태(필드)를 공유해야 할 때
인터페이스는 public static final로 정의된 상수만 가질 수 있고, 기본적으로 필드를 가질 수 없다. 하지만 추상 클래스는 필드를 가질 수 있다. 즉, 상태(변수)를 저장할 수 있고 이를 상속받은 클래스에서 사용할 수 있다.
public abstract class Animal {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public abstract void eat();
}
public class Dog extends Animal {
@Override
public void eat() {
System.out.println(getName() + " is eating dog food");
}
}
public class Cat extends Animal {
@Override
public void eat() {
System.out.println(getName() + " is eating cat food");
}
}
Dog과 Cat 클래스는 name 필드와 관련 메서드를 상속받아 재사용할 수 있다.
공통된 구현이 필요할 때
추상 클래스는 일부 공통 기능을 직접 구현하고, 나머지는 하위 클래스에서 구체화하도록 한다.
public abstract class Shape {
public void draw() {
System.out.println("Drawing a shape");
}
public abstract double calculateArea();
}
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
모든 도형은 draw 메서드를 공유하지만, 면적 계산은 도형마다 다르므로 calculateArea를 추상 메서드로 선언했다.
인터페이스도 default 메서드로 구현할 수 있는데?
위에서 공통된 구현이 필요할 때 추상 클래스를 사용할 수 있다고 했다.
하지만 자바 8부터는 인터페이스도 default 메서드를 지원한다. 따라서 "구현이 필요한 기본 동작을 제공"하는 기능은 인터페이스에서도 가능하다.
그럼 뭘 써야할까? 🤔
책 '이펙티브 자바'에서는 '추상클래스보다 인터페이스를 우선하라'라고 하며 여러 이유를 제시한다.
차이라면 추상 클래스는 좀 더 상세한 구현과 필드를 가질 수 있고, 인터페이스와 달리 다중상속은 불가능하다는 점이다.
1. 기존 클래스에 손쉽게 새로운 인터페이스를 구현해 넣을 수 있다.
default 메서드 덕분에 기존 클래스에 새로운 인터페이스를 추가하는 작업이 쉬워졌다.
예를 들어 Java 표준 라이브러리에 Comparable, Iterable, AutoCloseable 같은 인터페이스가 도입되었을 때, 기존 클래스가 이를 구현한 채로 릴리즈 될 수 있었다.
2. 인터페이스는 믹스인(Mixin) 정의에 적합하다.
믹스인은 혼합이라는 뜻을 갖고있다. 즉, 클래스가 가진 원래 주 기능 외에 다른 부가적인 기능을 추가하려고 할 때 유용하다는 말이다.
예를 들어 어떤 클래스에 추가적으로 정렬을 하는 기능을 구현하고 싶다! 하면 Comparable 인터페이스를 구현하면 되는 것이다.
이처럼 클래스의 원래 기능에 추가적인 기능을 혼합한다고 해서 믹스인이라고 부른다.
3. 계층구조 없는 타입 프레임워크를 만들 수 있다.
현실 세계에는 엄격한 계층구조로 정의하기 어려운 개념이 많다.
예를 들어, 음악 분야에서 가수와 작곡가를 생각해보자. 가수는 노래를 부르고, 작곡가는 곡을 작곡한다. 그러나 "싱어송라이터"라는 개념은 가수이면서 작곡가이기도 하다. 인터페이스를 사용하면 이런 관계를 쉽게 표현할 수 있다.
책에 등장하는 Singer, Songwriter 예제를 보자.
public interface Singer {
AudioClip sing(Song s);
}
public interface Songwriter {
Song compose(int chartPosition);
}
Singer와 Songwriter는 서로 독립적이다.
따라서 가수이면서 작곡가인 "싱어송라이터"를 구현하려면 `SingerSongwriter`라는 인터페이스를 만들고 두 역할을 혼합하면 된다.
// SingerSongwriter는 두 역할을 모두 수행
public interface SingerSongwriter extends Singer, Songwriter {
AudioClip strum(); // 기타 연주
void actSensitive(); // 감성적인 모습 표현
}
추상 클래스로는 이런 유연한 구조를 만들기 어렵다. 추상 클래스는 단일 상속만 가능하므로 계층구조를 고정해야 한다.
interface vs abstract class의 가장 큰 차이
자바 8 이후로 인터페이스와 추상 클래스의 가장 큰 차이는 단일 상속, 다중 상속이라고 생각한다. 추상 클래스는 단일 상속만 가능하다. 이게 무슨 뜻이냐면 추상클래스를 상속받은 클래스는 반드시 추상 클래스의 하위 클래스가 된다. 만약에 이미 추상클래스를 상속받았는데 또 다른 추상 클래스로 확장하길 원한다면, 그 추상 클래스는 계층 구조 상 최상위 부모 클래스가 되어야 한다. 반면에 인터페이스는 다중 상속을 지원해 여러 개의 인터페이스를 쉽게 구현할 수 있다.
인터페이스가 무조건 좋을까?
인터페이스가 무조건 좋아 보이지만 인터페이스에 default 메서드를 사용하는 것에도 단점이 있다.
인터페이스는 Object 클래스의 equals와 hashcode를 디폴트 메서드로 사용할 수 없다
인터페이스는 인스턴스 필드를 가질 수 없다
인터페이스는 public이 아닌 정적 멤버를 가질 수 없다
이 같은 단점 때문에 디폴트 메서드를 사용할 수 없는 경우가 생긴다. 이럴 때 추상클래스를 “추상 골격 구현 클래스"로 만들어 인터페이스와 같이 사용해 인터페이스와 추상 클래스의 장점을 모두 취할 수도 있다. 인터페이스로는 타입을 정의하고, 메서드 구현이 필요한 부분은 추상 골격 구현 클래스에 구현하는 것이다. 이런 패턴을 템플릿 메서드 패턴이라고 한다.
'💻 Dev > Java' 카테고리의 다른 글
BigDecimal이란? (0) | 2024.10.30 |
---|---|
Immutable(불변성), StringBuffer와 StringBuilder (0) | 2024.10.29 |
자바 코드의 메모리 영역(스택&힙) (0) | 2024.10.29 |
JVM 구조와 동작 과정 (0) | 2024.10.29 |
예외(Checked Exception, Unchecked Exception) (0) | 2024.10.26 |
동등성과 동일성&String.equals() (0) | 2023.11.05 |