개발놀이터

추상클래스와 인터페이스 본문

Java

추상클래스와 인터페이스

마늘냄새폴폴 2022. 7. 5. 03:38

도입

GOF 디자인 패턴 중 템플릿 메서드 패턴을 공부하던 중에 공통 로직인 AbstractTemplate를 구성하는 요소중 abstract라는 키워드를 발견하게 되었고 이에 대해 검색을 했더니 기능이 인터페이스랑 비슷한것을 발견했다. 둘의 차이점이라곤 추상클래스는 extends를 사용하는 상속이고 인터페이스는 implements를 사용한다는 것이다.

 

둘 다 추상메서드를 가지고 있으며 이 추상메서드를 추상클래스의 자식클래스나 인터페이스의 구현체에서 반드시 구현해야 한다는 공통점을 가지고 있었다.

 

이처럼 공통점을 가지고 있지만 서로 다른 역할을 할것으로 추측해서 둘의 차이점에 대해서 정리해봤다.

 

또한, 추상클래스와 인터페이스를 가벼운 예제코드를 통해 어느 상황에서 사용하는지 이해하는 시간도 가져볼 것이다. 

 

추상클래스

추상 개념은, 객체들 간의 관계를 상속 관계로 정립하면서 생기는, 점차 상위의 클래스를 정의하게 되면서 발생하는 "구체적인 행동 내용은 부모 입장에서 정의할 수 없는 것"을 묘사하기 위해 이용하는 것이라 생각하면 된다.

즉 자식 시점에서는 그 행동을 구체적으로 정의할 수 있지만 부모 입장에선 그렇지 않은 경우이면서 모든 자식 클래스가 반드시 구현해야할 행동인 경우에 추상 클래스를 사용한다. 이때 메소드를 부모 단계에서 추상화하여 "내 시점에선 행동을 특정할 수 없지만 자식 시점에선 반드시 구현되어야 하고 구현될 수 있는 행동이다" 라는 것을 설계 시점에서 명시하면 된다.

 

1. Event 라는 부모클래스가 있다

public class Event {
	...
}

2. Event를 상속받은 자식 클래스 aEvent, bEvent, cEvent가 있으며 각각 isRelevant() 메서드를 구현한다.

public class aEvent extends Event {
	public boolean isRelevant(Data data) {
    	return logic.a;
    }
}
public class bEvent extends Event {
	public boolean isRelevant(Data data) {
    	return logic.b;
    }
}
public class cEvent extends Event {
	public boolean isRelevant(Data data) {
    	return logic.c;
    }
}

3. 그리고 다른 클래스의 어느 메서드에서 Event 타입으로 선언된 배열 events[]의 각 원소를 isRelevant()메소드로 검증하는 로직을 작성했다. 

public class someClass() {
    ...
    Event events[] = new Event[100];
    ...
        
    public void someMethod() {
        for(int i=0; i<n; i++)
            if(events[i].isRelevant(someData)) // 컴파일 에러!!!
                system.out.println( events[i].toString());
    }
}

그런데 isRelevant()메소드에서 컴파일 에러가 발생한다.

 

왜냐하면 events[] 배열은 Event 타입으로 선언되었는데, Event 클래스에는 isRelevant() 메서드가 정의되어 있지 않기 때문이다. 

 

배열의 각 원소가 Event 클래스를 상속받은 클래스의 객체이고, 각 클래스는 isRelevant() 메서드가 구현되어 있지만, 컴파일러는 Event 클래스에 isRelevant() 메서드가 없는 것을 허용하지 않는 것이다. 

 

4. 이때 단순히 컴파일 에러만 피하고자 한다면 아래와 같이 Event 객체에 사용될 일 없는 isRelevant() 메서드를 정의할 수 있을 것이다. 

public class Event {
	public boolean isRElevant(Data data) {
    	return false;
    }
}

위와 같이 메서드를 정의하면 컴파일 에러는 사라지고 정상적으로 동작한다. 하지만 사용되지 않을 메서드를 구현하는 것은 올바른 해결책이 아니다.

 

5. 이 문제를 해결하려면 isRelevant()메서드를 추상 메서드로 만들면 된다. 

public abstract class Evnet {
	public abstract boolean isRelevant(Data data);
}


인터페이스

소스코드를 작성 시 클래스를 처음부터 구현하게 된다면 코드의 가독성도 떨어지고 시간도 오래 걸릴 것이다. 또한, 팀별로 각각 다른 모듈을 만들 시 팀마다 방법이 다르기 때문에 서로 호환성이 없고 일관성이 떨어지게 된다.

인터페이스를 통해 기본적으로 구현해야 할 메소드에 대한 규격들을 알려준다면 훨씬 효율적일 것이다. 즉, 인터페이스란 특정 기능을 개발하는데 있어서 공통적인 기능을 명시하고 강제적으로 구현하게끔 하는 역할이다. 인터페이스를 사용하게 된다면 자바의 다형성을 이용할 수 있고 이를 통해 유지보수성을 높일 수 있다.

 

1. 우리는 현업에서 일하는 개발자라고 가정하자. 오픈 날짜가 정해져있는 애플리케이션을 개발한다고 가정해보자, 이 애플리케이션의 할인 정책을 크게 정률할인정책(RateDiscountPolicy) 정량할인정책(FixDiscountPolicy) 두개로 정한 상태이다. 하지만 사장님이 할인정책은 우리 애플리케이션의 비즈니스 로직에서 중요한 부분을 차지하고 있으니만큼 오픈 직전까지 결정을 보류하고싶다고 말했다. 우리가 할 수 있는건 두가지이다.

-오픈 직전까지 개발을 안한다. (할인정책이 정해지면 개발을 시작한다)

-정률할인정책과 정량할인정책을 둘 다 만들어두고 할인정책이 정해지면 해당 정책으로 갈아 끼운다.

 

첫번째 방법은 있을 수 없는 방법이다. 그럼 우린 두번째 방법을 선택했다. 어떻게 하면 좋을까?

 

자바의 다형성을 이용하면 된다. 

 

2. DiscountPolicy라는 인터페이스를 만든다. 

public interface DiscountPolicy {
	int discount(int money);
}

3. DiscountPolicy를 구현하는 클래스인 FixDiscountPolicy, RateDiscountPolicy를 만든다.

public class FixDiscountPolicy implements DiscountPolicy {
	
    private static final int discount = 1000;
    
    @Override
    public int discount (int money) {
    	return money - discount;
    }
}
public class RateDiscountPolicy implements DiscountPolicy {
	
    private static final int discountRate = 10;
    
    @Override
    public int discount (int money) {
    	return money * (1 - discountRate * 0.01);
    }
}

4. 우선 FixDiscountPolicy라고 가정하고 스프링 빈으로 등록해보자

@Configuration
public class DiscountPolicyConfig {
	
    @Bean
    public DiscountPolicy discountPolicy() {
    	return new FixDicountPolicy();
    }
}

 

5. 이제 우리는 두개의 할인 정책을 만들었다. 이제 할인 정책이 사용되는 코드를 보자

@Controller
@RequiredArgsConstructor
public class DiscountController {
	
    private final DiscountPolicy discountPolicy;
    
    @PostMapping(blahblah)
    public String discount(HttpServletRequest request) {
    	...
    	
    	int money = request.getParameter("money");
    
    	int discountMoney = discountPolicy.discount(money);
        
        ...
    }
    
}

6. 우리는 정량할인정책(FixDiscountPolicy)이라고 가정하고 코드를 구성했다. 갑자기 사장님이 오픈 직전에 소비자 친화적이지않은 정량할인정책보다 정률할인정책이 좋아보인다며 정률할인정책으로 바꾸자고 하셨다. 우리가 인터페이스를 이용해 만든 DiscountPolicy의 스프링빈을 RateDiscountPolicy로 바꿔주기만 하면 된다!

@Configuration
public class DiscountPolicyConfig {
	
    @Bean
    public DiscountPolicy discountPolicy() {
    	return new RateDiscountPolicy();
    }
}

추상클래스와 인터페이스의 차이점

추상클래스와 인터페이스의 차이점을 개념적인 관점에서 생각하면 다음과 같다.

추상클래스 : extends 키워드를 사용하며, 다중 상속은 불가능 함
인터페이스 : implements 키워드를 사용하며, 다중 상속이 가능

개념적인 차이점이 아니라 실제 적용하는 것에서의 차이점을 생각해보면 추상 클래스는 extends (상속, 확장의 느낌) 키워드 그대로 자신의 기능들을 하위로 확장시키는 것으로 볼 수 있다. 인터페이스는 implements (상속, 구현의 느낌) 키워드처럼 인터페이스에 정의된 메서드를 각 클래스의 목적에 맞게 동일한 기능으로 구현하는 것으로 볼 수 있다.

또 다른 관점에서는 추상 클래스는 이를 상속할 각 객체들의 공통점을 찾아 추상화시켜놓은 것으로 상속 관계를 타고 올라갔을 때, 같은 부모 클래스를 상속하며, 부모 클래스가 가진 기능들을 구현해야 하는 경우에 사용한다. 반면 인터페이스는 상속 관계를 타고 올라갔을 때, 다른 부모 클래스를 상속하더라도 같은 기능이 필요한 경우에 사용한다.