면접에서 인터페이스에 대해 물어보는 질문에 당연히 알고 있다고 생각했는데 막상 말하려고 하니 제대로 떠오르지가 않아서 아쉬움이 있었다. 앞으로 공부를 할 때는 이걸 왜 사용하는지 어떤 이점이 있는지 이런 부분을 좀 더 생각하고 정리해야겠다.

 

먼저 인터페이스를 알기 위해서는 추상 클래스에 대해 알아야 하는데 추상 클래스(abstract class)란 미완성 클래스로 미완성 상태의 추상 메서드를 포함하고 있는 것이다. 추상 메서드는 abstract 키워드를 사용하며 구현 코드 없이 선언부만 존재한다. 추상적인 클래스(설계도)이기 때문에 자체로 인스턴스를 생성할 수 없고 이를 상속 받은 자식 클래스는 반드시 추상 메서드를 구현해야한다. 

 

추상 메서드를 선언하는 이유는 자식 클래스에게 구현을 강요하기 위함으로 추상 메서드를 구현하지 않으면 에러가 발생하기 때문에 실수를 방지할 수 있고 추상화를 통해 확장성을 넓힐 수 있다. 그리고 추상 클래스를 상속 받은 자식 클래스를 부모 타입의 참조 변수로 참조하면 여러 자식 클래스를 하나의 배열로 다룰 수도 있다.

 

인터페이스란?

 

인터페이스는 일종의 추상 클래스로 추상 클래스보다 추상화 정도가 더 높으며 추상 메서드와 상수로 이루어져 있다. (JDK 1.8부터는 인터페이스에 추상메서드 외 static, default 메서드도 선언이 가능하다.)

 

인터페이스는 다음과 같은 제약이 있다.

 

1. 멤버 변수는 public static final이어야 하며 생략이 가능하다.

2. 메서드는 public abstract이어야 하며, 생략이 가능하다. 

 

인터페이스는 왜 사용하는가?

 

인터페이스를 사용하면 클래스 사이(객체 사이)에 새로운 관계를 맺어 줄 수 있고 다형성을 통해 의존관계를 추상화하여 런타임 시점에 외부에서 객체를 생성하고 주입할 수 있다. 이로 인해 클래스 간에 결합도를 낮출 수 있고 객체 지향 프로그래밍의 설계 원칙인 SOLID 5원칙 중 단일 책임 원칙, 개방 폐쇄 원칙, 의존관계 역전 원칙을 지킬 수 있다.

 

- 단일 책임 원칙(SRP, 하나의 클래스는 하나의 책임만 가진다)

- 개방 폐쇄 원칙(OCP, 변경에는 닫혀있고 확장에는 열려 있어야 한다)

- 의존관계 역전 원칙(DIP, 구체화가 아닌 추상화에 의존해야한다)

 

 

 

 

[참고]

자바의 정석

이것이 자바다

https://tecoble.techcourse.co.kr/post/2021-04-27-dependency-injection/

http://www.tcpschool.com/java/java_polymorphism_interface

 

'Raw use of parameterized class 'List' 라는 경고를 보고 Raw use가 무엇인지 찾아봤는데 이펙티브 자바에 있는 내용이라 아직 초반부를 보고 있지만 해당 부분을 읽어보았다.

 

제네릭 타입은 일련의 매개변수화 타입(parameterized type)을 정의하는데 List<String>의 경우 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입인 것이다.

 

Raw Type이란 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말하는데 아래와 같이 원소의 타입을 정의하지 않고 List를 그대로 사용한 것이다.

 

List<String> listA = new ArrayList<>();

//Raw use of parameterized class 'List'
List listB = new ArrayList<>();

 

Raw Type은 제네릭이 도입되기 이전에 수 많은 코드들과 호환되도록 하기 위해 있는 것인데 가장 큰 문제는 타입 오류를 런타임에서야 발견할 수 있는 것이다.

 

제네릭을 활용하면 엉뚱한 타입의 인스턴스를 넣으려 할 때, 컴파일 오류가 발생해서 런타임 되기 전에 오류를 알아차릴 수 있다. Raw Type을 쓰면 제네릭이 주는 안전성과 표현력을 잃게 되는 것이다.

 

제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않으면 Raw Type보다 비한정적 와일드 카드를 사용하는 것이 좋다.

 

예외적으로 class 리터럴에는 매개변수화 타입을 사용할 수 없기 때문에 List.class 형태의 Raw Type으로 사용해야 하며, instanceOf 연산자도 런타임시에는 제네릭 정보가 지워지기 때문에 매개변수화 타입에는 적용할 수 없어 다음과 같이 검사 형변환(checked cast)으로 사용하는 것이 좋다.

 

if (o instanceof Set) {
    Set<?> s = (Set<?>) o;
    ...
}

 

아직 제네릭에 대한 공부가 더 필요하지만 Raw Type으로 선언 시 컴파일시에 문제점을 발견할 수 있는 제네릭의 장점을 살리지 못하고 런타임 오류가 발생할 수 있는 위험에 대해 알 수 있었다.

HashMap을 사용할 때, put(key, value)으로 데이터를 저장할 수 있는데 HashMap의 경우 key 값이 존재할 경우 value 값을 대체하고 기존의 value 값을 반환해준다. 하지만 key 값이 존재하지 않는 경우에는 새로 데이터를 저장하고 null을 반환한다. (기존 value 값이 없기 때문)

 

put()으로 데이터를 저장하고 반환된 value 객체를 참조할려고 하니 NullPointerException이 발생한다는 경고를 보고 어떻게 구현이 되어있나 확인해보았다.

 

HashMap에서 해싱이란 해시 함수를 이용해서 데이터를 해시테이블에 저장하고 검색하는 방식을 말하는데 해시함수가 데이터가 저장되어 있는 곳을 알려주기 때문에 다량의 데이터 중에서 원하는 데이터를 빠르게 찾을 수 있다.

해싱에서 사용하는 자료구조는 배열과 링크드 리스트의 조합으로 되어 있는데 key 값을 해시함수에 넣으면 배열의 한 요소를 얻게 되고, 다시 그 곳에 연결되어 있는 링크드 리스트에 저장이 된다.

해시함수의 계산 결과인 해시코드로 해당 값이 저장되어 있는 링크드 리스트를 찾고, 링크드 리스트에서 검색한 키와 일치하는 데이터를 찾는다. (링크드 리스트는 검색에 불리하기 때문에 해시코드가 서로 중복되지 않도록 하는 전략이 좋다.)

 

HashMap의 경우 Object 클래스에 정의된 hashCode()를 해시 함수로 사용하는데 이는 객체의 주소를 이용하는 알고리즘으로 모든 객체에 대해 hashCode() 결과가 서로 유일하게 된다. 

 

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

 

그래서 실제 HashMap의 put() 메서드를 보면 putVal() 메서드를 호출하면서 해시함수 hash(key)를 실행하고 해시 코드를 넘겨준다.

 

 

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
	    ...
        
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

 

putVal() 메서드의 일부를 보면 key가 존재하는지 확인을 하고 value를 교체한 뒤 oldValue를 반환하거나 그렇지 않을 경우 null을 반환한다.

 

 

Stack 자료구조의 특정 객체를 파라미터로 받아서 해당 요소가 있으면 위치를 반환해주는 search() 메서드를 구현해보던 중 equals()로 비교를 할 때, 전달 받은 파라미터가 null인 경우를 따로 체크하는 부분이 있었다.

 

public synchronized int lastIndexOf(Object o, int index) {
    if (index >= elementCount)
        throw new IndexOutOfBoundsException(index + " >= "+ elementCount);

    // Obect o 객체가 null 인 경우
    if (o == null) {
        for (int i = index; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = index; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

 

equals()를 사용할 때는 주의해야 될 부분이 있는데 값을 비교할 때 앞에 null인 객체가 들어올 경우 NullPointerException 예외가 발생하는 것이다. 

public int lastIndexOf(Object value) {
    int index = array.length - 1;
    for (int i = index; i >= 0; i--) {
        // value가 Null인 경우
        if (value.equals(array[i])) {
            return i;
        }
    }
    return -1;
}

 

객체 참조를 하기 때문에 당연한 부분이지만 전에도 이런 실수를 많이 했던 것 같다. 그래서 객체와 문자열의 값을 비교하는 경우에는 문자열을 왼쪽에 입력하고, 객체끼리 비교하는 경우에는 null 값을 고려해서 코드를 짜도록 해야겠다.

+ Recent posts