[JAVA] 02. 다형성(Polymorphism)

jae_s_a·2022년 9월 28일
0

JAVA

목록 보기
2/2
post-thumbnail

객체지향개념에서 다형성이란 여러 가지 형태를 가질 수 있는 능력을 의미하는데, 자바에서는 부모클래스의 타입의 참조변수로 자식클래스의 인스턴스를 참조할 수 있음을 의미한다.

너무 추상적이게 들릴 것 같아서 추가적인 설명을 해보면 다형성은 부모-자식 상속 관계에서 부모 클래스가 동일한 호출로 자식 클래스들을 다르게 동작시키는 객체 지향 원리라고 할 수 있다.
이 다형성을 활용하면 부모 클래스가 자식 클래스가 어떻게 구현되어 있는지 몰라도 오버라이딩을 통해 자식 클래스에 접근할 수 있게 된다.

Q> 부모 클래스는 자식 클래스가 무슨 일을 하는지 몰라도 어떻게 자식 클래스의 메서드를 호출시킬 수 있을까?
A> 이유는 동적 바인딩 덕분이다.

동적 바인딩을 설명하기에 앞서 다들 잘 아는 오버라이딩(Overriding)과 오버로딩(Overloading)에 대해 잠시 설명을 하자면

  • 오버라이딩은 상속된 클래스내에서 부모 클래스에 존재하는 메서드를 재정의하는 것
  • 오버로딩은 한 클래스내에서 각각 다른 파라미터를 가진 동명의 메서드를 정의한 것

위 두 개념은 그리 어렵지 않은 내용이라 크게 어렵지 않을 듯한데 오버라이딩과 오버로딩은 실행될 때 차이점이 있다.

그 차이는 바로 오버라이딩과 오버로딩된 동명의 메서드들 중 실제 사용할 메서드를 결정하는 시점이다.

  • 오버라이딩된 메서드는 런타임(Runtime)시에 어떤 메서드를 실행할지 결정된다.
  • 오버로딩된 메서드는 컴파일(Compile)시에 어떤 메서드를 실행할지 결정된다.

런타임 시점/컴파일 시점 또는 런타임 에러/컴파일 에러이라는 용어를 한 번 쯤은 들어봤을 것이라 생각된다. 런타임 시점과 컴파일 시점에 대해 보다 쉬운 이해를 위해 런타임 에러와 컴파일 에러를 예시로 들면,

  • 런타임 에러는 이름 그대로 프로그램 실행중에 발생하는 오류이다. 다시 말해, 프로그램을 실행시켜야만 알 수 있는 오류이다.
    • 예를 들면, 0으로 나누는 경우(java.lang.ArithmeticException)나 NullPointException과 같이 프로그램이 동작되어야 알 수 있는 오류이다.
  • 컴파일 에러는 일반적으로 컴파일 도중 발생하는 문법적인 오류를 의미하는데 대중적으로 사용되는 IDE인 Eclipse나 Intellij에서 빨간 줄이 그어지면서 보이는 게 컴파일 에러가 발생할 수 있음을 나타낸 것이다.
    • 예를 들면, 문법적으로 틀리거나 타입이 일치하지 않는 경우에 발생하는 오류이다.

다시 본론으로 돌아가서, 본문 처음에 다형성은 무엇인가에 써놓은 부분을 보면

"다형성을 활용하면 부모 클래스가 자식 클래스가 어떻게 구현되어 있는지 몰라도 오버라이딩을 통해 자식 클래스에 접근할 수 있게 된다."

라고 정의했다.

그리고 이것이 가능한 이유가 동적 바인딩(Dynamic binding)때문이라고 했는데,
우선, 바인딩(Binding)은 프로그램에서 사용될 변수나 메서드를 결정짓는 행위를 뜻한다.

그렇다면 동적 바인딩은 다음과 같이 정의할 수 있다.

동적 바인딩(Dynamic Binding): 런타임 시점. 즉, 파일을 실행하는 시점에 성격이 결정되는 행위
즉, 동적 바인딩은 런타임 시점에 실제로 실행될 메서드를 호출하는 것

그래서 흔히 오버라이딩은 동적 바인딩과, 오버로딩을 정적 바인딩과 연관 짓는 이유가 이러한 이유 때문이다.

동적 바인딩과 오버라이딩이 어떻게 연관되어 있는지 약간의 이해를 돕기 위해 간단한 코드 예시를 추가했다.
예시 코드를 UML 다이어그램으로 표현하게 되면 아래와 같은 형태로 나타낼 수 있는데 단순히 ChildClazz extends ParentClazz를 표현한 다이어그램이다.

코드는 다음과 같다.

public class PolyParentChild {
    public static void main(String[] args) {
		// point 1 
        ParentClazz parentClazz = new ParentClazz();
        parentClazz.prt(); // ParentClazz.prt

		// point 2
        ChildClazz childClazz = new ChildClazz();
        childClazz.prt();  // ChildClazz.prt

        System.out.println("==========");
		
        // point 3
        ParentClazz polyClazz = new ChildClazz();
        polyClazz.prt();   // ChildClazz.prt
    }
}

class ParentClazz {
    public void prt() {
        System.out.println("ParentClazz.prt");
    }

}

class ChildClazz extends ParentClazz{
    @Override
    public void prt() {
        System.out.println("ChildClazz.prt");
    }
}
[실행결과]
ParentClazz.prt
ChildClazz.prt
==========
ChildClazz.prt

위 코드에서 ChildClazzParentClazz를 상속받고 있고, 서로 부모-자식 관계로 이루어져 있다.
point1point2는 인스턴스의 타입과 일치하는 타입으로 참조변수를 생성했기 때문에 prt()메서드를 호출할 경우 각각의 클래스에 정의된 prt()메서드가 실행되게 된다.

point1: ParentClazz.prt
point2: ChildClazz.prt

이제 다형성으로 넘어가서, 다형성은 "자바에서는 부모클래스의 타입의 참조변수로 자식클래스의 인스턴스를 참조할 수 있다."고 앞서 설명드린 바 있다.

ParentClazzChildClazz는 부모-자식 관계이기 때문에 부모클래스의 타입(ParentClazz)으로 자식클래스의 인스턴스(ChildClazz)를 참조할 수 있다.

point3이 다형성의 예시에 해당하는 부분이고, 부모의 타입으로 자식의 인스턴스를 참조 가능하기에 해당 부분의 코드가 ParentClazz polyClazz = new ChildClazz();로 되어있는 것을 알 수 있다.

그리고 이 참조변수로(다형성이 적용된) prt() 메서드를 호출하게 되면 자식 클래스에 의해 오버라이딩된 prt메서드가 호출됨을 확인할 수 있다.
즉, 다형성에 의해 부모 클래스의 참조변수로 prt메서드를 호출했지만, 자식 클래스에서 재정의된 prt메서드가 호출됐다.

이처럼 다형성은 실행되기 전에는 어떤 메소드를 호출할 지 컴파일 시점에는 알 수 없고, 실행이 되어야만(런타임) 실제로 어떤 메서드를 호출할 지 결정할 수 있다.
이런 방식으로 다형성을 잘 활용하면 동적 바인딩에 의해 자식 클래스에서 오버라이딩된 메서드를 호출할 수 있게 된다.


김영한 님의 스프링 MVC 1편 강의를 듣기 전까지 개념은 이해했지만 원리는 잘 이해되지 않았는데 섹션 4. MVC 프레임워크 만들기 에 DispacherServlet을 약식으로 만들어 보면서 다형성에 대해 원리와 개념을 완전히 이해했다.

특히, HandlerAdapter(interface)를 구현할 때 해당 Controller를 지원하는 인스턴스인지 확인하기 위해 supports가 필요했는데 이 부분에서 다형성에 대해 확실히 알게 된 것 같다.

코드의 일부를 첨부하자면,

@WebServlet(name = "frontControllerServletVx", urlPatterns = "/front-controller/vx/*")
public class FrontControllerServletVx extends HttpServlet {

	...
    
    public FrontControllerServletVx() {
        initHandlerMappingMap();
        initHandlerAdapters();
    }
    
    private void initHandlerAdapters() {
        handlerAdapters.add(new ControllerV3HandlerAdapter());
        handlerAdapters.add(new ControllerV4HandlerAdapter());
    }
    
    ...
    
	@Override
    protected void service(...) throws ServletException, IOException {

        // URL 매핑
        Object handler = getHandler(request);

        if(handler == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // support()
        MyHandlerAdapter adapter = getHandlerAdapter(handler);
        
    ...


    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if(adapter.supports(handler)) {
                return adapter;
            }
        }
        throw new IllegalArgumentException(...);
    }
    
    ...
public class ControllerVxHandlerAdapter implements MyHandlerAdapter {
    @Override
    public boolean supports(Object handler) {

        // ControllerVx의 인스턴스 중 하나인가?
        return (handler instanceof ControllerVx);
    }
    
    ...

전체 코드가 아니기에 이해하기 어려울 수 있을 것 같다고 생각하지만.. 모든 코드를 올릴 수는 없는 노릇이라 support와 관련된 코드는 이정도로 정리할 수 있을 것 같다.


[참고]

오버로딩 & 오버라이딩의 결정 시점 차이 (Overloading & Overriding)
[JAVA] 자바 다형성(POLYMORPHISM) 개념부터 응용 쉬운 설명
다형성과 동적 바인딩이 담겨있는 짤막한 코드
[JAVA] 자바 다형성 기본 및 활용

profile
if not now, when

0개의 댓글