본문 바로가기
개발/자바

다형성이란 무엇일까? 자바 인터페이스를 사용해서 적용해보자.

by 루 프란체 2022. 4. 13.
728x90

어려운 개념, 용어는 다 제쳐두고 다형성이라는 것에 대해서만 적어보겠다. 원래 적을 생각은 없었지만 우리 최매씨에게 보여줄 다형성에 관한 글을 찾다보니 마음에 드는 글이 없어서 직접 작성한다. 물론 나도 완벽하게 작성한다는 보장은 없다.

 

어쨌든 예를 들어 Avante 클래스와 Genesis 클래스가 있다고 치자. 보통의 경우 해당 클래스에 있는 자원(메소드, 변수 등등)을 이용하기 위해서는 다음과 같은 방식을 통해 해당 클래스를 인스턴스화하고 사용하게 된다.

 

Avante avante = new Avante();
Genesis genesis = new Genesis();

 

누가 봐도 avante 는 Avante 클래스고 genesis 는 Genesis 클래스다. 이렇게 사용하는 게 나쁜 건 아니다. 무조건 이렇게 사용하면 안 된다는 법도 없다. 하지만 이렇게 사용하지 않아도 좋을 때가 있다. 어느 때냐면 여러 클래스가 공통의 동작을 할 때라고 보면 이해가 쉽겠다.

 

위에서 예로 든 Avante 와 Genesis 도 그렇다. 서로 이름은 다르지만 누가 봐도 이건 '' 라는 범주에 있고 '' 라고 하면 누가 생각해도 달리고 멈추고 회전하는 등의 조작을 할 수 있다는 걸 알고 있다. 그렇다면 이 두 개의 클래스는 위에도 적어놓은 공통의 동작을 하고 있다는 조건을 만족한다.

 

그렇다면 우선 달리고, 멈추는 기능을 공통의 명세서(interface/인터페이스)로 따로 작성을 해보겠다. 이것 이외의 기능, 예를 들자면 와이퍼니 깜빡이니 하는 것도 많지만 그런 걸 일일이 적으면 글이 너무 길어지므로 이 두 개만 작성해본다.

 

interface Car {
	void accel();
	void brake();
}

 

누가 봐도 '' 라는 범주에 있으므로 인터페이스에 Car 라는 이름을 주고 accel 과 brake 기능이 있을 거라고 정의했다. 인터페이스가 무엇인지까지 다루면 이 글이 너무나도 길어지므로 그건 다음 기회로 미루고... 인터페이스 내의 코드를 보면 Car 라는 인터페이스를 구현할 구현체는 반드시 accel 과 brake 를 포함해야 한다는 걸 알 수 있다.

 

어떻게 알 수 있는지 그래도 간략하게 이유를 적자면 인터페이스가 원래 그런 놈이다. 나를 기준으로 삼을 놈은 반드시 내가 두리뭉실하게 가지고 있는 이것들을 구현해야 한다! 라는 것이 존재 이유다. 그러면 사용하겠다고 해놓고 구현하지 않으면 어떻게 되느냐? 컴파일 에러난다.

 

 

그러면 굳이 Car 라는 인터페이스를 사용해야 하나요? 그냥 따로따로 구현하면 안 되나요? 라는 질문이 있을 수도 있다. 물론 사용하지 않아도 된다. 하지만 개발을 하다보면 표준이라는 게 있고 하다못해 동네 김밥천국을 가더라도 이 메뉴에는 이걸 넣는다는 레시피가 존재한다.

 

이 인터페이스를 사용함으로써 얻는 이득은 위에도 적었지만 구현하지 않은 내용이 있는 경우 컴파일 에러가 발생하므로 기능 구현의 누락을 막을 수 있다는 점이다. 김밥천국을 예로 들었었는데 내가 식당에 갈 때마다 요리하는 분이 깜빡하고 이 재료를 안 넣었다가, 저 재료를 안 넣었다가... 하면 맛이 계속해서 바뀔텐데 그러면 계속 가고 싶을까?

 

하여튼 지금은 Avante 와 Genesis 밖에 없어서 와닿지 않을 수도 있는데 공통의 모듈을 사용하는 클래스가 늘어나면 늘어날수록 이 인터페이스가 반드시 필요하다고 느낄 때가 온다. 아, 글이 길어지므로 인터페이스에 대한 설명은 여기까지 한다. 빨리 쓰고 자야 된다.

 

class Avante implements Car {
	@Override
	public void accel() {
		System.out.println("시속 100km 로 달립니다.");
	}
	@Override
	public void brake() {
		System.out.println("멈추는데 3초!");
	}
}

 

우선 Avante 를 구현해보도록 하자. Car 를 사용하겠다고 선언(implements)했다. 그렇다면 accel 와 brake 를 구현해야 하는데 사실 이것들은 Car 안에 정의되어 있는 것을 실제 구현만 Avante 에서 하는 것이다. 위에도 적었듯이 accel 과 brake 를 구현하지 않으면 컴파일 에러가 발생한다.

 

그럼 이것을 어떻게 구현하느냐? 우리가 알고 있는 것 중에는 오버라이드 라는 것이 있다. 오버라이드는 덮어쓰는 것이다. Car 에서 정의되어 있는 accel 과 brake 를 Avante 의 입맛에 맞게 구현해서 덮어쓴다라는 것이 인터페이스 구현의 핵심이다. 그리고 Car 에서 정의된 accel 과 brake 를 구현한 Avante 는 구현체가 되는 것이다.

 

class Genesis implements Car {
	@Override
	public void accel() {
		System.out.println("시속 200km 로 달립니다.");
	}
	@Override
	public void brake() {
		System.out.println("멈추는데 1초!");		
	}
}

 

그럼 이번엔 Genesis 를 구현해보도록 하자. 마찬가지로 Car 를 사용하겠다고 선언(implements)했다. 그리고 마찬가지로 accel 과 brake 를 구현했다. 이번에는 시속 200km 로 달릴 수 있고 1초 만에 멈출 수 있는 스펙으로 구현했다. 

 

Avante 와 Genesis 는 공통적으로 Car 라는 인터페이스를 사용하지만 각각 accel 와 brake 를 구현한 내용은 다르다. 위에도 적었지만 인터페이스는 이것과 이것을 구현해라! 라고 할 뿐이지, 실제 내용까지는 관여하지 않는다. 접근제한자, 리턴값, 변수명이 같다면 내용이 어떻게 되든 신경쓰지 않는다는 것이다.

 

자, 그렇다면 이제 다형성이라는 걸 설명할 차례다. 여기까지는 사실상 인터페이스에 대한 설명이라고 봐도 무방한 느낌도 든다... 여기까지 오는데 참 힘들었다. 먼저 Car mycar; 를 선언한다. 

 

Car mycar;

 

이건 사실 인터페이스를 변수로 할당만 한 것이므로 아무런 기능도 없다. 실제로 인터페이스에서는 어떤 기능을 사용할 지 이름만 정의해두고 내용은 아무 것도 적지 않았으니까. 정확히는 인스턴스화 하지 않았으므로 할당도 되지 않은 상태다. 

 

그러면 이제 실질적으로 Car 를 인스턴스화 할 차례다. 위에도 적었지만 이 인터페이스는 아무런 기능도 없다. 인스턴스화 할 필요도 없고 해봤자 아무런 기능도 없다. 그렇다면 인터페이스는 어떻게 인스턴스화 할까? 정답은 아래와 같다.

 

 

Car mycar;
		
// Person 이 Avante 라는 Car 를 구입
mycar = new Avante();
mycar.accel();
mycar.brake();
		
// Person 이 Avante 에 질려서 Genesis 로 갈아탐
// Avante 도 Car, Genesis 도 Car 니까 Avante 와 똑같이 accel 기능이 있고 brake 기능이 있음
mycar = new Genesis();
mycar.accel();
mycar.brake();

 

다형성은 말 그대로 형태가 여러 개 있을 수 있다는 말이다. Avante 클래스와 Genesis 클래스는 둘 다 Car 인터페이스를 구현했으므로 위와 같은 방식으로 Avante 가 들어갈 수도 있고 Genesis 가 들어갈 수도 있다. 사실 다형성은 이게 끝이다. 그리고 둘은 공통적으로 Car 인터페이스에 명시 된 accel, brake 기능을 구현했으므로 동일한 메소드를 사용해 기능을 작동 시킬 수도 있다.

 

물론 내부적인 상세 로직은 다를 수도 있다. 그리고 Avante 와 Genesis 를 한 페이지 내에 다 사용했기 때문에 글쎄요? 이래서는 어떨 때 써야 좋은 지 모르겠는데요? 라고 할 수도 있다. 충분히 합리적인 의심인데, 사실 다형성은 개발보다 유지보수에 초점을 맞췄다고 볼 수 있다.

 

실제 돌아가는 소스에서 기존에 Avante 를 사용하고 있었는데 이걸 Genesis 로 바꿔달라는 요청이 왔다고 치자. 만약 Car 인터페이스를 사용하지 않고 그냥 내 멋대로 Avante 는 졸라빨라요, 짱빨라요, 너무잘나가요 이런 식으로 구현을 해놨다면 Genesis 클래스로 바꿔야할 때 여러가지 사이드 이펙트가 발생할 수 있다.

 

예를 들면 메소드 이름을 하나 바꿨다고 여기저기에서 에러가 빵빵 터질 수도 있고 리턴값이 달라지는 경우 또다시 해당 리턴값에 맞게 호출부를 수정해야 하는 등의 여러 문제가 발생할 수 있는데 해당 스펙들을 인터페이스에 정의해두고 사용한다면 이러한 리스크를 조금이라도 줄일 수 있다는 것이다.

 

적어놓고 보니 다형성이라기보다 인터페이스에 관한 글이 된 것 같은데 대충 이런 느낌으로 사용하면 된다는 것만 알아두면 된다. 그럼 끝!

728x90

댓글0