본문 바로가기
Java/Java

익명 클래스, 인터페이스 익명 구현 객체

by oneny 2023. 4. 14.
함수형 인터페이스에 대해 공부하던 중에,,

 

분명히 interface는 객체를 생성할 수 없다고 배웠는데 Consumer 인터페이스를 통해서 객체를 생성하고 있는 것에 대해 이해가 되지 않았다. 그래서 검색을 통해 찾아보니 인터페이스 익명 구현 객체라는 것을 통해서 구현을 할 수 있다는 것을 알게 되었다.

 

익명 클래스

익명 클래스는 내부 클래스(Inner class) 일종으로 단어 그대로 이름이 없는 클래스를 말한다.

익명, 이름이 없다는 것은 별로 기억되지 않아도 된다는 것이며, 나중에 다시 불러질 이유 즉, 재사용할 일이 없다는 뜻으로 프로그램에서 일시적(단발적)으로 한 번만 사용되고 버려지는 객체라고 볼 수 있다.

 

왜 생겼을까?

일회성의 구현 객체를 만들기 위해 소스파일을 만들고 클래스를 선언하는 것은 비효율적이다. 자바는 소스 파일을 만들지 않고도 구현 객체를 만들 수 있는 익명 구현 객체를 제공한다.

 

부모클래스 인스턴스 = new 부모클래스() {
  // 익명 클래스 내부
};

 

즉, 부모 클래스를 통해서 객체를 생성할 필요 없이 단일 객체를 만들어 부모 클래스에서 추상 메서드의 실제 행위를 정의하여 코드의 양을 줄이는 일종의 기법이라고 할 수 있다.

 

익명 클래스를 사용해야하는 경우
  1. 프로그램 내에서 일시적으로(단발성으로) 한 번만 사용되어야 하는 객체일 경우
    -> UI 이벤트 처리, 스레드 객체 등 (단발성 이벤트 처리)
  2. 재사용성이 없고, 확장성을 활용하는 것이 유지보수에 더 불리할 때
    -> 비즈니스 로직이 정말 재각각이며, 재사용성이 전혀없어 매번 클래스를 생성해야하는 비용이 더 큰 경우

 

익명 클래스 컴파일

내부 클래스를 컴파일 하면 $ 기호가 들어간 .class 파일을 얻게 된다.

익명 클래스도 내부 클래스의 일종이니 마찬가지다.

Main.java 파일에서 익명 클래스를 정의했기 때문에 컴파일 될 때 Main.class Main$.class 이렇게 두 개 클래스 파일이 생긴 것을 확인할 수 있다. 익명 클래스 파일이 $가 들어간 파일에 해당한다.

 

익명 클래스를 활용한 테스트

먼저, 한 가지 제한사항 추가하자면 레거시 코드에 대해 리팩토링해야하는 과정에서 메서드 시그니처를 바꾸기 힘든 상황이 존재할 수가 있다. 그래서 move() 메서드의 메서드 시그니처를 바꾸지 않은 상태에서 테스트 가능하도록 만들어 보자.

그리고 다음과 같은 코드가 봤을때 당연히 move() 메서드는 getRandom() 메서드에 의존적이고, 이 getRandom() 메서드에 반환되는 값에 따라 테스트 코드를 작성하기란 너무 어려운 일이다. 물론 이러한 Random를 따로 분리하여 Random과 Car의 의존을 없애버리는 것이 가장 좋은 방법이지만 다음과 같은 방법도 있다는 것을 보여주기 위해 private을 protected 접근 제어자로 바꿔줌으로써 익명 클래스를 통해 Override할 수 있도록 만들어주면 아래와 같이 테스트 코드를 작성할 수 있게 된다.

 

테스트 코드 수정 전

 

테스트 코드를 수정하기 전에 다음과 같은 코드로 이동과 정지라는 테스트를 작성하면 어떤 경우에는 성공하고 어떤 경우에는 실패한다. 단위 테스트를 하는 이유는 기능에 대한 불확실성을 감소시키기 위해 작성하는 것인데 이렇게 되면 그 의미가 없어진다. 그래서 이 코드를 수정해보자.

 

테스트 코드 수정 후

 

테스트 케이스는 가능한 경계값에서 버그가 많이 발생하기 때문에 경계값을 사용하는 것이 좋다. 요구사항에는 4 이상인 경우에는 이동, 3 이하인 경우에는 정지하기 때문에 다음과 같이 경계값에 해당하는 4와 3을 가지고 테스트했다.

하지만 private -> protect 접근 제어자를 그냥 바꾼 것을 반영하는게 맞을까? 과도기적인 단계로 보고 반영하는 것이 맞다고 생각하면 된다. private 메서드를 테스트해야하는 상황이라면 따로 분리해야하는 것이 아닌지 먼저 생각하고, 이에 따라 의존을 제거하는게 가장 좋지만 앞에서 말한 제한사항이 발생한 상황이라면 잠시 protected 접근 제어자를 통해 테스트 코드들을 작성하는 것도 하나의 방법이라고 할 수 있다.

대신 이렇게 테스트 코드를 많이 만들고, 안정화된다면 메서드 시그니처를 바꿔 리팩토링하고, 그리고 처음 말했던 Random을 따로 분리하여 Car와의 의존 관계를 끊어버리도록 점진적으로 하나씩 리팩토링하는 것이 최종 목표이다.

 

인터페이스 익명 구현 객체

인터페이스의 경우 구현 클래스를 따로 구현하여 사용하는 것이 일반적이다. 그리고 그렇게만 알고 있었다. 하지만 일회성으로 사용할 경우가 생긴다면, 구현체 클래스를 직접 구현하여 인터페이스를 사용하는 것을 비효율적이다. 때문에 익명 클래스처럼 일회성 익명 구현 객체를 선언하여 사용할 수 있도록 기능을 제공하고 있다.

 

인터페이스 인스턴스 = new 인터페이스() {
  // 인터페이스에 선언된 추상 메서드의 실체 메서드 선언
};

 

익명 클래스와의 차이점

익명 클래스와는 다르게 인터페이스는 강제적으로 메서드 정의를 통해 사용해야하는 규약이 있기 때문에 규격화에 도움이 된다.

 

인터페이스 익명 구현 객체 예시

위에서 보여준 코드이지만 printWords 객체는 Consumer 인터페이스 익명 구현 객체이다.

Consumer는 void accept(T t); 추상 메서드를 갖고 있고 행위를 정의하여 다음과 같이 사용할 수 있다.

 

람다식

람다식은 일반적으로 함수형 인터페이스(Functional Interface)를 구현하기 위한 코드 블록이고, 위의 코드가 그 예시이다. 즉, 함수형 인터페이스는 단 하나의 추상 메서드를 가지는 인터페이스를 의미한다.

 

즉, 함수형 인터페이스인 경우에는 람다식을 사용하면 이전 보다는 적은 코드로 인터페이스 익명 구현 객체 기능을 사용할 수 있다. 더 자세한 내용을 다음 게시글로 작성하자.

 

 

출처

https://limkydev.tistory.com/226

https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%9D%B5%EB%AA%85-%ED%81%B4%EB%9E%98%EC%8A%A4Anonymous-Class-%EC%82%AC%EC%9A%A9%EB%B2%95-%EB%A7%88%EC%8A%A4%ED%84%B0%ED%95%98%EA%B8%B0