티스토리 뷰

Java & Kotlin

[Java] 언제 추상 클래스(Abstract Class) 또는 인터페이스(Interface)를 사용해야 하는가?

망나니개발자 2023. 12. 19. 10:00
반응형

 

 

 

1. 언제 추상 클래스(Abstract Class) 또는 인터페이스(Interface)를 사용해야 하는가?


[ 인터페이스와 추상 클래스의 특징 ]

인터페이스

인터페이스(Interface)는 상호 작용 방식을 명세해둔 것이다. 인터페이스의 호출자는 인터페이스의 구현에 대한 지식 없이도 원하는 기능을 수행할 수 있어야 한다. 예를 들어 우리는 자동차가 어떻게 움직이는지 세부 구현을 모르지만, 엑셀을 밟으면 앞으로 가고 브레이크를 밟으면 멈춘다는 약속을 통해 자동차를 운전할 수 있다. 따라서 인터페이스는 일종의 계약(contract)이라고도 불린다.

 

 

추상 클래스

추상 클래스(Abstract Class)는 인터페이스와 유사하다. 인터페이스와 마찬가지로 인스턴스화 할 수 없으며, 구현을 포함하거나 포함하지 않은 메서드 모두를 가질 수 있다.

하지만 인터페이스와 비교하여 추상 클래스가 가질 수 있는 중요한 특징 중 하나는 필드를 가질 수 있다는 점이다. 그리고 해당 필드는 static 혹은 final이 아닐 수 있으며, public이나 protected 또는 private으로 선언할 수도 있다. 인터페이스를 사용하면 모든 필드가 public, static, final으로 처리되며 모든 메서드는 public이다. 또한 단일 클래스만 extends 할 수 있는 반면, 인터페이스는 여러 개를 구현할 수 있다.

 

 

추상 클래스와 인터페이스의 특징 비교

  • 추상 클래스의 특징
    • 인스턴스화 할 수 없으며 단일 상속만 가능하다.
    • static, final이 아니며, protected 또는 private인 속성을 가질 수 있다.
    • 하위 클래스는 추상 클래스의 모든 추상 메서드를 구현해야 한다.
  • 인터페이스의 특성
    • 인스턴스화 할 수 없으며 다중 상속이 가능하다.
    • public static final인 속성만 가질 수 있다.
    • 구현 클래스는 인터페이스의 모든 메서드를 구현해야 한다.

 

 

 

[ 추상 클래스와 인터페이스의 의미 ]

추상 클래스는 코드 재사용을 위해 사용된다. 하위 클래스는 추상 클래스를 상속받아 추상 클래스의 속성과 메서드를 사용할 수 있으므로 동일한 코드를 재작성하지 않아도 된다. 이는 일반 클래스를 통해서도 달성할 수 있지만 추상 클래스를 사용한다면 추상 메서드의 구현을 컴파일 시점에 확인할 수 있고, 추상 클래스의 객체 생성을 막을 수 있다는 점에서 유용하다.

이러한 역할은 인터페이스도 비슷해 보이지만, 인터페이스가 필요한 이유는 다르다. 추상 클래스가 코드 재사용에 초점을 맞춘다면, 인터페이스는 디커플링에 초점을 맞추고 있다. 인터페이스는 일련의 프로토콜 또는 계약을 추상화한 것이므로, 호출자는 인터페이스에만 주의를 기울이고 구현 방식 자체에 대해서는 알 필요가 없다. 인터페이스를 통해 구현을 분리하여 코드의 결합도를 줄이고 확장성을 높일 수 있다.

이를 인터페이스 기반 프로그래밍이라고 하며, 이는 1994년에 출판된 “GOF의 디자인 패턴”에 처음 등장한 개념으로 1995년에 발표된 자바보다 먼저 등장하였다.

인터페이스 기반 프로그래밍을 통해 구현을 캡슐화하여 추상화에 기반한 프로그래밍을 할 수 있지만, 그렇다고 인터페이스를 남용하는 것은 바람직하지 않다. 예를 들어 서비스 계층 처럼 구현이 하나 뿐이고 다른 구현으로 대체할 일이 없다면 굳이 정의할 필요가 없다. 모든 것에 인터페이스를 만든다면 개발에 부담이 되며 오히려 코드 추적을 어렵게 만드는 등의 단점이 있다.

 

 

 

[ 언제 추상 클래스 또는 인터페이스를 사용해야 하는가? ]

추상 클래스는 상속을 사용하므로 is-a 관계인데 반해, 인터페이스는 특정 기능이 있음을 나타내는 has-a 관계다. 따라서 인터페이스를 사용할 지 추상 클래스를 사용할 지 판단하는 기준은 명확하다. is-a 관계를 나타내며 코드 재사용 문제를 해결하려면 추상 클래스를 사용하고, has-a 관계를 나타내며 코드 재사용이 아닌 추상화 문제를 해결하려면 인터페이스를 사용하면 된다.

클래스 상속의 관점에서 추상 클래스는 상향식 설계 방식이다. 먼저 하위 클래스의 코드를 반복한 다음 상위 클래스를 추상화하면서 생성되는 것이 추상 클래스다. 반면에 인터페이스는 반대로 하향식 설계 방식이다. 일반적으로 먼저 인터페이스를 설계한 다음 특정 구현을 고려하게 된다.

 

 

 

 

2. 스프링에서 살펴보는 추상 클래스와 인터페이스 예시


[ 스프링에서 살펴보는 추상 클래스와 인터페이스 예시 ]

스프링은 객체 지향의 철학을 철저히 준수하는 프레임워크이다. 따라서 추상 클래스와 인터페이스를 적당한 위치에 적절히 사용해주고 있다. 대표적으로 디스패처 서블릿에서 사용 예시를 살펴보도록 하자.

디스패처 서블릿은 프레임워크 수준에서 필요로 하는 공통 기능들을 담당한다. 대표적으로 요청을 처리할 컨트롤러를 찾고, 예외 처리기를 통해 예외를 처리하고, 뷰를 반환하는 작업 등을 처리한다. 요청을 처리할 컨트롤러를 찾는 방법이나 예외 처리기를 통해 예외를 처리하는 방법은 매우 다양하다. 따라서 스프링은 이러한 부분을 추상화한 인터페이스를 갖고 있다.

 

 

다음은 스프링이 예외를 처리하는 핸들러를 추상화한 인터페이스이다. 해당 인터페이스는 에러 처리 기능에 대한 계약이다.

public interface HandlerExceptionResolver {

    @Nullable
    ModelAndView resolveException(
        HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);

}

 

 

스프링의 디스패처 서블릿은 해당 인터페이스에 의존하여 예외 처리를 핸들링한다. 앞서 살펴본 대로 인터페이스를 has-a 관계로 정확하게 사용하고 있음을 확인할 수 있다.

public class DispatcherServlet extends FrameworkServlet {

    ...

    @Nullable
    private List<HandlerExceptionResolver> handlerExceptionResolvers;

    ...

    @Nullable
    protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
        @Nullable Object handler, Exception ex) throws Exception {

    ...

    // Check registered HandlerExceptionResolvers...
    ModelAndView exMv = null;
    if (this.handlerExceptionResolvers != null) {
        for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
            exMv = resolver.resolveException(request, response, handler, ex);
            if (exMv != null) {
                break;
            }
        }
    }

    ...
  }
}

 

 

이렇듯 여러 개의 예외 처리기를 추가하다 보니 공통된 기능이 예외 처리기에 반복되게 되었다. 따라서 스프링은 여러 개의 추상 클래스를 도입하고 반복되는 예외 처리 코드를 해결하고자 했다. 즉, 상속 is-a 관계를 통해 코드 재사용 문제를 해결한 것이다. 이들 중에 일부는 다시 추상 메서드로 선언하여 자식 클래스로 구현을 위임하였다.

public abstract class AbstractHandlerExceptionResolver implements HandlerExceptionResolver, Ordered {
    ...
}

 

 

여기서 또 하나 흥미로운 부분은 인터페이스와 추상 클래스가 만들어진 시점이다.

예외 처리를 위한 인터페이스는 2003년에 상향식 설계를 기반으로 만들어졌다. 반면에, 예외 처리를 위한 추상 클래스는 스프링 3.0의 출시(2009년)와 함께 하향식 설계를 기반으로 만들어졌다는 것이다.

섣부른 추상화를 통해 만들어지는 추상 클래스는 문제 지점이 되는 경우가 많다. 이로 인해 상속보다는 합성을 사용해야 한다는 주장도 상당히 많이 보인다. 하지만 상속 역시 올바르게만 사용된다면 강력한 도구가 될 수 있으니, 올바른 사용법을 알아둘 필요가 있다.

 

 

 

 

 

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함