본문 바로가기
개인 공부용 프로젝트/calculator

인터페이스, 추상클래스.. 사용 가능할 것 같은데? <8>

by pon9 2024. 11. 18.

어제 리팩토링을 하고, 자기 전에 고민을 했다.

과연 내 계산기에서 인터페이스나 추상 클래스를 사용할 곳이 한 군데도 없을까? 진짜로?

사용할 수 있을 것 같은데?

 

해서, 인터페이스와 추상 클래스의 개념을 간단하게 다시 잡고, 방향성을 정했다.

추상 클래스는 "공통적인 로직" 을 기반으로 하고, 인터페이스는 "확장성" 에 초점을 맞춘다.

특징 추상 클래스 인터페이스
목적 코드 재사용, 공통 기능 제공 설계 강제, 다양한 구현체 허용
공통 로직 포함 여부 가능 (메서드 구현, 필드 포함) 불가능 (Java 8부터 default 메서드 허용)
다중 상속 불가능 가능
유연성 제한적 (계층 구조를 강제) 매우 유연 (다양한 구현 가능)

 

이보다 더 간단히 비유를 해 보자면,

인터페이스는 "레고 블록은 이렇게 생겨야 한다" 라는 규정만 하고, 조립 방법은 각자 알아서 구현한다.

추상 클래스는 레고 박스처럼 기본적인 틀과 조립된 블록을 제공한다. 조립 방법은 정해져 있고, 틀 안에서만 수정이 가능하다.

내 계산기에서 "확장성"이 필요한 것은 History의 검색 방식들이고,  "공통적인 로직" 을 기반으로 한 것은 Operation이다.

HistorySearch Interface 를 구현한다면, 지금처럼 id만 검색 가능한 것이 아닌 특정 값보다 큰 결과값 불러오기, 특정 연산자를 활용한 계산만 불러오기 등의 기능을 쉽게 추가할 수 있다.

Operation Abstract Class를 구현한다면, 기존처럼 a와 b값을 입력받아서 연산하는 것은 동일하지만, 다양한 연산을 쉽게 추가할 수 있다. 일전에 추상 클래스들을 삭제하고 enum을 도입했는데, enum과 추상 클래스를 같이 쓸 생각을 이제야 하다니..


우선 인터페이스 먼저 손대는 걸로 방향을 잡고 interface 에 대해 조금 더 공부해보다가, Predicate 라는 것에 대해 알게되었다.

Predicate는 인수를 받아서 boolean 값을 반환하는 함수형 인터페이스다. stream의 filter는 인수로 predicate를 받는다.

@Test
public void whenFilterListWithCombinedPredicatesUsingAnd_thenSuccess(){
    Predicate<String> predicate1 =  str -> str.startsWith("A");
    Predicate<String> predicate2 =  str -> str.length() < 5;
  
    List<String> result = names.stream()
      .filter(predicate1.and(predicate2))
      .collect(Collectors.toList());
        
    assertEquals(1, result.size());
    assertThat(result, contains("Adam"));
}

그러니 이런 형태로 predicate를 통해 여러 조건을 선언하고 filter로 필요한 조건만 걸러낼 수 있다.

사용자 입력을 받아 처리하기도 쉬운 구조로 되어있다.

https://www.baeldung.com/java-predicate-chain

 

이제 내 코드에 적용해보자.

HistorySearch interface는 search 기능을 구현하도록 "블럭의 생김새" 즉 "메서드의 생김새" 를 정해준다.

구현체에서는, 역시나 Predicate<History>의 조건을 매개변수로 받고 그 조건에 따라 filtering 한 값을 return 한다.

구현체에게 매개변수를 보내주는 클래스다. Map을 사용해봤다.

 

그리고 코드 길이가 너무 길어지는 것 같아서 class를 나눠줬다.

이정도면 직관..적인가..?

배보다 배꼽이 더 커졌다 ㅋㅋ

 

사용자가 "h"를 입력하면 HistoryInteraction의 showhistory가 호출된다.

그곳에서 HistoryInput의 getUserInput을 통해 사용자의 입력값을 받고(id, r, o),

searchHandler에서 입력값에 따른 검색기를 호출한다
(id면 같은id 검색, r이면 입력값보다 큰 결과만 검색, o면 입력한 연산자를 사용한 기록 검색).

HistoryDataSearchManager가 알맞은 검색 조건을 골라주고,
HistorySearch를 상속받은 구현체 HistoryData에게 그 조건을 보내서 검색을 수행한다. 

마지막으로 검색기록이 HistoryOutput을 통해 제공되고, 계산기로 돌아갈 것인지 검색을 더 할것인지를 입력받는다.

 

 

근데 어쩌다보니 HistorySearch를 상속받은 자식이 한명 뿐인데.. 이거 옳은 방법 맞나? 싶었지만,

확장성과 유연성을 고려하면 괜찮은 것 같다.

 

오늘 interface를 마무리 했으니, 내일은 operation을 추상 클래스로 바꿔봐야겠다~~

겸사겸사 제곱근이나 제곱 계산도 추가해야겠다 ㅎㅎ

 

https://github.com/roqkfchqh/CalculatorApp