인터페이스에 대해서 개념적으로 알고 있던 내용들이 몇 가지 있었다. 항상 무엇인가 기능에 대해서 생겼을 때 이게 왜 생긴걸까? 에 대해서 고민을 하는 편인데, 단순히 책에서 나온 내용만을 가지고 아 그냥 단순히 이러한 이유 때문에 나온거구나 하고 말았다. 하지만 실제로 인터페이스에 존재가 어떻게 생기게 되었는지에 대해서 많은 부분을 알 수 있었다.
Interface의 특징
인터페이스는 여러가지 기능을 가지고 있다.
1. 구현의 강제력
- 자바 8 이전에는 무조건적인 구현의 강제력이 있었다. 즉, 인터페이스에 선언된 추상 메서드를 구현하는 구성 클래스는 해당 메서드를 무조건적으로 구현을 해야하는 것이다.
2. 다형성을 제공
- 다형성을 제공해준다는 특징이 있다. 하지만 이 다형성이라는 특성은 단순히 인터페이스만의 기능은 아니고, 상속 관계에서도 있을 수 있기 때문에 이러한 특성도 가지고 있다고 알아두자.
3. 결합도를 낮추는 효과
- 결합도란 객체가 서로 의존하는 정도를 의미하는데, 이 부분에서 동시에 의존성을 역전하라는 얘기가 나왔다. 의존성 역전 원칙은 구상 클래스에 의존하지 말고, 추상 클래스에 의존하라라는 얘기인데, 사실 이렇게 봐서는 느낌이 오지 않았다. 이러한 관점을 이해하는데 있어서 UML로 표현하니 간단하게 이해 할 수 있었다.
이러한 형태의 캐릭터라는 객체는 두 가지 객체에 대해서 전적으로 의존을 하는 상황이 된다. 하지만 이를 인터페이스를 활용해서 의존을 역전시킬수 있다.
무기라는 인터페이스를 두고 도끼와 활이 무기를 구현하게 해두었다. 그리고 캐릭터는 인터페이스인 무기에 의존하게 되었다. 이렇게 의존이 역전이 될 수 있다는 것이다. 기본적으로 우리는 객체자체가 무엇인가를 갖는다고 했을 때 가진 것에 대한 구성체에 대해서 많은 생각을 하게 되는데, 이러한 관점에서 도끼나 활이 결정되는 순간은 클라이언트가 캐릭터에게 어떤 구성체를 주는지에 따라서 다르고, 이는 런타임 시점에 동적으로 결정 될 수 있는 사항이다.
default Method
우리는 default 메서드에 대해서 생긴 이유를 단순하게 다음과 같은 이유로 생각을 하고 있다. 인터페이스에 어떠한 기능을 추가하게 되면 인터페이스를 구현하고 있는 모든 클래스에 해당 기능을 추가해주어야 하기 때문에 이러한 부분을 없애기 위해서 default 메서드를 추가한 것이다! 라고 알고 있다. 하지만 여기에는 별개의 상황이 또 존재했다. 저러한 것도 목적이 될 수 있겠지만 주 목적은 다음과 같다.
사람들은 인터페이스를 쓰면서도 인터페이스가 가진 기능에 대해서 일부만 구현하기를 원했다. 따라서 이를 위해서 Adapter를 사용해서 구현하고자 하는 기능만을 구현하도록 하였다. 예시는 다음과 같이 볼 수 있다.
public interface Action {
void dance();
void fly();
}
public class ActionAdapter implements Action {
@Override
void dance() {}
@Override
void fly() {}
}
public class Person extends ActionAdapter {
@Override
void dance() {
System.out.println("춤을 추어요");
}
}
이렇게 사실은 하나의 클래스에서 인터페이스의 기능을 부분적으로 구현하기 위해서 Adapter를 사용했었다. 그러나, 자바에서의 상속은 단일상속밖에 안된다는 큰 단점을 가지고 있게 되었고 그러한 이유로 여러가지 인터페이스에 대해서 어댑터를 만들 때, 여러가지를 구현하는 어댑터를 추가로 만들면서 또 해당 어댑터를 상속받아서 구현하는 형태를 만들어야 했다.
이러한 문제를 해결하기 위해서 인터페이스에 default 메서드를 사용 할 수 있게끔 바꾼 것이다. 이에 따라 사람들은 이전처럼 특별한 요구사항을 구현하기 위해서 인터페이스의 모든 부분을 오버라이딩 하지 않아도 수행 할 수 있게 된 것이다. 그리고 추가적으로 jdk8 에서 특별히 static 메서드를 가질 수 있게 되었는데, 이를 통해 인터페이스는 함수 제공자가 되었다. 이는 다음에 다룰 함수형 인터페이스와도 크게 관련이 있다.
여기서 한 가지 의문이 들 수 있는 점은 메서드는 무엇이고 함수는 무엇이냐는 것이다. 어떠한 함수를 호출하는 객체가 있는 경우를 메서드라고 말하고, 함수를 호출하는 객체가 없는 경우를 함수라고 한다. 이 말이 잘 이해가 안 될 수 있다. 하지만 뒤에 나올 Functional Interface가 무엇인지 왜 탄생했는지에 대해서 듣게 되면 이해가 더 쉬울 것으로 생각된다.
Functional Interface
함수형 인터페이스를 말하고, 이는 추상메서드가 하나만 존재하는 인터페이스이다. 그리고 @FunctionalInterface 애노테이션을 사용하면 컴파일 단계에서 이 인터페이스가 함수형 인터페이스인지에 대해서도 검증을 해준다. 종종 다음과 같이 구성이 되었을 경우에는 함수형 인터페이스라고 부를 수 없는 것으로 오해를 하는 경우가 있는데 그러한 케이스를 살펴보자.
public interface Example {
void ex1();
default void ex2() {
System.out.println("ex2");
}
static void ex3() {
System.out.println("ex3");
}
}
위와 같은 경우에도 Example 인터페이스는 함수형 인터페이스이다. 실질적으로 존재하는 추상 메서드는 하나밖에 존재하지 않기 때문이다. 그럼 함수형 인터페이스가 어떠한 목적으로 등장을 하게 되었는지에 대해서 알아볼 필요가 있다.
먼저 함수형 프로그래밍에 대한 패러다임에 대해서 먼저 이해를 하고 보는 것이 더 다음 내용을 이해하는데 도움이 될 것이다.
함수형 프로그래밍과 일급 객체란
먼저 함수형 프로그래밍에 대해서 이야기를 하기 전에 프로그래밍 패러다임에 대해서 이야기를 해보자. 패러다임 먼저 패러다임의 정의는 다음과 같다. 어떤 한 시대 사람들의 견해나 사고를
bombo96.tistory.com
결국 함수형 인터페이스는 위의 포스팅에서 드러나는 것처럼 연산에 대한 가독성을 높이기 위해 메서드를 1급 객체로 다룰 수 있게 함으로써 가독성을 높이고, 주어진 연산에만 초점을 맞출 수 있게 되었다. 이러한 함수형 인터페이스를 함수형 프로그래밍처럼 사용하기 위해서는 익명 클래스를 사용하거나 람다를 사용하는 방법이 있는데 이를 사용하게 된 이유를 역사에 따라서 살펴보자.
먼저 인터페이스를 사용한다면 다음과 같은 문제점이 있다.
public interface Example {
void ex();
}
public class ExampleImpl implements Example {
@Override
public void ex() {
System.out.println("이렇게 구현 클래스를 따로 생성해주어야 한다");
}
}
위 처럼 단 하나의 메서드만이 존재함에도 이를 바로 사용하지 못하고, 구현체를 생성해서 사용해야 한다는 것이었다. 이러한 방식은 개발자의 개발 생산성을 떨어뜨리는 일이다. 이를 해결하기 위해서 사람들은 인터페이스도 간단하게 new를 생성할 수는 없을까? 에 대한 관점이 생기게 되었다. 그리고 이러한 방식이 익명 클래스 처럼 동작하는 방식이다. 이제 이어서 익명 클래스를 사용한다면 다음과 같이 동작하도록 할 수 있다.
public interface Example {
void ex();
}
// 1. 기존에 행해야했던 방식
public class Main {
public static void main() {
new Class XXX implements Example {
@Override
public void ex() {
}
}.ex();
}
}
// 2. 간편화
public class Main {
public static void main() {
new Example() {
@Override
public void ex() {
}
}.ex();
}
}
만약에 우리가 인터페이스에 정의된 어떠한 메서드를 사용하기 위해서는 위에서 얘기했듯이 1번 케이스처럼 작성을 해야 했다. 그러나 어떠한 하나만의 구현을 위해서 객체를 생성하게 무엇인가를 실행하는 것은 아까도 말했듯이 개발 생산성을 상당히 낮추는 일이다. 이를 개선하기 위해 2번 방식처럼 익명 클래스 방식을 사용하여, 사용하고자 하는 함수를 바로 사용할 수 있게 되었다.
이러한 케이스를 보고 이제 위에서 말한 함수를 호출하는 객체가 왜 없는 경우인지에 대한 이해가 되었을 것이라고 생각한다. 하지만 지금과 같은 상황에서도 사람들은 다음과 같은 생각을 거듭하게 된다. 이렇게 함수도 사용하지 않는 부분에 대해서 하나밖에 없을 경우에 대해 이렇게 줄이게 되었는데, 하나의 기능만 수행하는 메서드는 줄 일 수 없는가에 대해서도 생각을 하게 된 것이다. 그러한 생각으로 등장하게 된 것이 람다 표현식이다.
람다 표현식
위에서 얘기했듯이 람다 표현식은 하나의 기능을 수행하는 함수에 대해서 굉장히 간단하게 줄여주는 기능을 가지고 있다. 먼저 예시부터 간단하게 살펴보자.
public interface Example {
void ex();
}
// 1. 기존에 행해야 했던 방식
public class Main {
public static void main() {
Example example = new Example() {
@Override
public void ex() {
}
}.ex();
}
}
// 2. 간편화
public class Main {
public static void main() {
Example ex = () -> System.out.print("zz");
}
}
익명 클래스를 사용하지 않고, 단순히 람다 표현식을 사용한 것과 비교해보면 코드의 가독성이 굉장히 올라간 것을 볼 수 있다. 그리고 저렇게 줄이게 된 배경은 다음과 같다. 하나의 인터페이스의 기능을 수행하는 메서드를 오버라이딩 하는 것은 오버라이딩은 항상 존재한다. 따라서 @Override부터 제거를 했고, 그 다음에 메서드도 하나의 인터페이스에서는 하나밖에 존재하지 않으니 메서드 이름도 지워버렸다. 그러나 매개변수에 개수에 대해서는 파악을 할 수 있어야 하기 때문에 괄호는 남겨놓고, 그에 따라 구현을 하도록 하고, 이렇게 했을 경우 기존의 메서드와는 다른 방식으로 구현이 되었다는 것을 비교 할 수 있어야 하기 때문에 -> 를 사용하게 된 것이다.
이러한 배경을 생각해서 람다를 구현해야 겠다고 생각을 하게 된다면, 람다를 어떻게 줄여야하는지에 대해서 간단히 생각할 수 있을 것이다. 그리고 자바는 이러한 비슷한 기능들을 하는 인터페이스에 대해서 공통적인 함수형 인터페이스들을 java.util.function 에 미리 정의를 해두었다.
java.util.function (Java Platform SE 8 )
Interface Summary Interface Description BiConsumer Represents an operation that accepts two input arguments and returns no result. BiFunction Represents a function that accepts two arguments and produces a result. BinaryOperator Represents an operation u
docs.oracle.com
위 공식문서에 여러가지 함수가 있는데, 대표적으로 쓰이는 Supplier, Consumer, Function, Predicate 에 대해서만 이해를 해도 나머지는 이러한 함수형 인터페이스의 확장형이기 때문에 이해하는데 어렵지 않을 것이다.
회고
금일 드디어 코드리뷰 과제를 끝내게 되었다..! 어떻게 보면 처음으로 객체지향적인 사고를 바탕으로 직접 어떠한 애플리케이션 자체를 개발하는 경험이 처음이라고 봐도 무방했다. 처음에서는 어떻게 설계해야하는지에 대해서만 10시간을 넘게 고민했던 것 같다. 하지만 이러한 고민 덕분에 PR을 올리는 과정에서 내가 어떤 부분을 많이 고민했는지에 대해서 자세히 알릴 수 있어서 좋은 경험이었던 것 같다!
멘토님에게도 해당 부분에 대해서 질문을 드렸는데, 우선 구현을 끝내고 리팩토링하는 형태로 진행하는게 오히려 더 빠를 수 있다고 조언해주셨다. 실제로 먼저 객체가 어떻게 구성 될 것인지에 대해서 생각하고 구현하는 건 정말 객체지향개발이 익숙한 개발자가 되어야 가능하지 않을까란 생각이 들고 포비님도 그렇게 말씀해주시기도 하셨다.
이러한 과제가 끝나고 인터페이스 강의를 듣게 되었는데, 강사님이 해주시는 이러한 역사와 생기게 된 계기는 확실히 뇌리에 박히고 왜 사용하게 되었는지가 명확하게 되어서 너무 좋은 것 같다. 아무리 기초라도 다시 돌아가서 배울 때 마다 다시 보이는 새로운 관점들은 항상 신기하고 새롭다.
요즈음엔 하루 하루가 너무 기대되고 즐거운 일상이다!
'프로그래머스 데브코스' 카테고리의 다른 글
프로그래머스 데브코스 6일차 - 전략패턴 (3) | 2023.06.08 |
---|---|
프로그래머스 데브코스 5일차 (0) | 2023.06.08 |
프로그래머스 데브코스 3일차 - OOP 이야기 (0) | 2023.06.06 |
프로그래머스 데브코스 2일차 - 프레임워크를 위한 JAVA (0) | 2023.06.02 |
프로그래머스 데브코스 1일차! (0) | 2023.06.02 |