[OOP] 우리는 왜 List list = new ArrayList(); 라고 쓰는가

@itsmo · July 25, 2023 · 7 min read

개요

우리는 왜 인스턴스를 선언할 때 타입을 클래스 대신 인터페이스로 선언할까? import 두 번 하기 귀찮은데…

Java와 같은 객체 지향 언어에서 다양한 자료구조를 사용하다 보니 문득 의문이 든다. ArrayListHashMap과 같은 자료구조를 사용할 때, 어째서 우리는 아래와 같이 클래스 대신 인터페이스 타입으로 선언해서 사용할까? 그냥 사용하고자 하는 클래스 그대로 선언하여 사용하면 편하지 않을까?

List<String> list = new ArrayList<>();
ArrayList<String> list = new ArrayList<>(); // 왜 이렇게 쓰지 않을까?

Map<String, Integer> map = new HashMap<>();
HashMap<String, Integer> map = new HashMap<>(); // 왜??

위 코드 2번, 5번 줄처럼 list와 map을 선언해도 동작엔 무리가 없다. 그러나 당신이 저런 방식으로 위 인스턴스를 생성해 왔다면, 당신은 객체지향을 제대로 사용하고 있지 않은 것이다.

이 글에서는 어째서 우리는 인터페이스로 타입을 선언해야 하는지, 객체지향적인 관점에서 확인해 볼 것이다.

다형성(Polymorphism)

💡 多形性 - 하나의 객체가 많은 형(타입)을 가질 수 있는 성질

객체지향 언어를 공부한 적이 있는 사람이라면 객체지향의 네 가지 특징인 추상화Abstraction, 다형성Polymorphism, 상속Inheritance, 캡슐화Encapsulation에 대해서 한 번쯤은 들어 보았을 것이다. 그중에서도 오늘의 주제는 다형성이 어째서 중요한지를 알려주는 좋은 예시이다.

객체지향 언어를 처음 공부할 때로 되돌아가 복습해 보자. 다형성이란 무엇인가? 다형성이란, 하나의 객체가 많은 타입을 가질 수 있는 성질을 말한다. 두 클래스가 상속 관계에 있다면, 우리는 조상 클래스 타입으로 자식 객체를 레퍼런스 할 수 있다.

class Animal{}
class Person extends Animal{}

Animal animal = new Person(); // 사람은 동물이다.

왜 이런 일이 가능할까? 바로 자식 클래스는 조상 클래스의 모든 기능을 물려받았기 때문이다. 즉, 위의 코드처럼 Animal 클래스를 상속받고 있는 Person 클래스는 Animal 클래스가 가지고 있는 모든 기능들을 사용할 수 있으므로 타입을 Person이 아닌 Animal로 지정하더라도 전혀 문제가 없다. 바로 이 이유가 오늘 문제의 답이다.

인터페이스로 타입을 선언하는 이유

당신이 열심히 ArrayList list = new ArrayList();를 사용해 프로그램을 개발했다고 생각해 보자. 단순히 선언뿐만 아니라, 이 ArrayList를 매개변수로 입력받거나 반환하는 메소드도 무수히 만들었다. 어느 날, 당신의 상사이든 클라이언트든 당신이 명령을 거부할 수 없는 어떤 누군가가 와서 이렇게 말한다.

😈: 우리 로직에는 ArrayList보다 LinkedList가 효율이 더 좋을 것 같으니 그걸로 바꿔주세요!

단순히 ArrayList를 모두 LinkedList로 치환하기만 하면 끝나는 일이라기엔 변경해야 할 사항이 너무나도 많다. 그러나 우리가 ArrayList를 클래스가 아닌 인터페이스인 List로 선언했다면? 간단히 인스턴스 변수 자체만 LinkedList로 변경함으로써 주어진 모든 문제를 깔끔하게 해결할 수 있다.

public ArrayList<String> something() {
...
} // 인터페이스 사용 없이 구체 클래스만으로 메소드를 구현한다면?
//반환 타입이 변경될 때마다 코드를 수정해야 한다.

public List<E> something() {
...
} // 인터페이스와 제네릭으로 메소드를 구현한다면 구현체가 변경되더라도 메소드 사용에 문제가 없다.

낮은 결합도의 중요성

결합도가 낮을 수록, 응집도가 높을 수록 이상적인 코드라고 한다.
결합도가 낮을 수록, 응집도가 높을 수록 이상적인 코드라고 한다.

우리는 위와 같이 인터페이스로 타입을 선언함으로써 클래스 간의 결합도Coupling를 낮추었다. 객체지향에서 흔히들 *‘결합도가 낮은 코드가 좋은 코드다’*라고들 말하는데, 위의 경우가 바로 결합도를 낮춤으로써 코드의 재사용성을 높인 케이스라고 할 수 있겠다.

결론

인터페이스를 구체 클래스의 타입으로 선언하는 것은 낮은 결합도를 보장하기 위한 핵심이다. 프로그램의 규모가 작고 간단히 실습하는 코드에서야 구현체를 직접 타입으로 선언하더라도 동작엔 무리가 없겠지만, 프로그램의 규모가 커질수록 이러한 객체지향적인 설계가 중요해진다. 이러한 다형성은 Spring Framework의 핵심 원리인 DIDependency Injection를 설명하기 위한 특징이므로, 어째서 이렇게 사용해야만 하는지에 대해 정확히 짚고 넘어갈 필요가 있겠다.

참고

@itsmo
배운 것을 잊지 않기 위해 틈틈히 기록합니다.