중복 코드가 있을 경우, 이를 반드시 어떤 기능을 하는 모듈로 만들어서 써야 한다. 그 이유는 해당 코드를 수정해야할 경우, 같은 역할을 하는 나머지 코드도 수정해야 하기 때문이다. 한 모듈만 수정해서 적용할 수 있다면 유지보수성이 매우 좋은 코드일 것이다.
일반적인 코드는 메서드로 만들면 되니 익숙하다. 하지만 여러 클래스의 메서드가 거의 같은 코드를 반복하는 경우도 있다. 이때는 추상 클래스로 중복을 제거해야 하는데 어떻게 사용하면 좋은지 알아보자.
추상 클래스란 말 그대로 실체가 없기에 아직은 미완성 된 클래스를 말한다. 미완성이라는 건 모듈이 가지는 여러 기능 중 적어도 1개 이상은 구체화되지 않았다는 얘기다. 때문에 추상 클래스에서 완전히 정의된 기능은 하위 클래스에서도 당연히 사용할 수 있고, 정의되지 않은 기능은 하위 클래스에서 반드시 정의해서 완성 시켜줘야한다.
public class DeliveryEventSubscriber implements MessageListener {
private final RedisMessageListenerContainer container;
private final DeliveryEventHandler deliveryEventHandler;
@PostConstruct
public void registerListener() {
container.addMessageListener(this,
new PatternTopic(RedisTopicPattern.DELIVERY_EVENTS.getPattern()));
}
@Override
public void onMessage(@NonNull Message message, byte[] pattern) {
String topicStr = new String(message.getChannel());
String json = new String(message.getBody());
log.debug("Received Redis event topic={}, body={}", topicStr, json);
RedisTopic topic = RedisTopic.fromTopic(topicStr);
if (topic == null) {
log.warn("Unknown topic received: {}", topicStr);
return;
}
deliveryEventHandler.handle(topic, json);
}
}
public class PaymentEventSubscriber implements MessageListener {
private final RedisMessageListenerContainer container;
private final PaymentEventHandler paymentEventHandler;
@PostConstruct
public void registerListener() {
container.addMessageListener(this,
new PatternTopic(RedisTopicPattern.PAYMENT_EVENTS.getPattern()));
}
@Override
public void onMessage(@NonNull Message message, byte[] pattern) {
String topicStr = new String(message.getChannel());
String json = new String(message.getBody());
log.debug("Received Redis event topic={}, body={}", topicStr, json);
RedisTopic topic = RedisTopic.fromTopic(topicStr);
if (topic == null) {
log.warn("Unknown topic received: {}", topicStr);
return;
}
paymentEventHandler.handle(topic, json);
}
}
두 코드를 보면면 굉장히 유사하다. 다른 거라곤
private final DeliveryEventHandler deliveryEventHandler;
RedisTopicPattern.DELIVERY_EVENTS.getPattern()
deliveryEventHandler.handle(topic, json);
VS
private final PaymentEventHandler paymentEventHandler;
RedisTopicPattern.PAYMENT_EVENTS.getPattern()
paymentEventHandler.handle(topic, json);
이것 뿐이다.
하위 클래스가 추상 클래스를 상속받을 때, 어떻게 하면 중복되는 코드가 없게 만들 수 있을까? 먼저, 공통된 건 동일하게 작동하면되니 추상 클래스에서 그대로 정의해준다. 나머지는 하위 클래스만의 고유한 특성이므로 자식 클래스에서 구현하도록 한다.
public abstract class AbstractRedisEventSubscriber implements MessageListener {
@Autowired
private RedisMessageListenerContainer container;
@PostConstruct
public void registerListener() {
container.addMessageListener(this, new PatternTopic(getTopicPattern().getPattern()));
}
@Override
public void onMessage(@NonNull Message message, byte[] pattern) {
String topicStr = new String(message.getChannel());
String json = new String(message.getBody());
log.debug("Received Redis event topic={}, body={}", topicStr, json);
RedisTopic topic = RedisTopic.fromTopic(topicStr);
if (topic == null) {
log.warn("Unknown topic received: {}", topicStr);
return;
}
getHandler().handle(topic, json);
}
protected abstract RedisTopicPattern getTopicPattern();
protected abstract RedisEventHandler getHandler();
}
public class DeliveryEventSubscriber extends AbstractRedisEventSubscriber {
private final DeliveryEventHandler deliveryEventHandler;
@Override
protected RedisTopicPattern getTopicPattern() {
return RedisTopicPattern.DELIVERY_EVENTS;
}
@Override
protected RedisEventHandler getHandler() {
return deliveryEventHandler;
}
}
public class PaymentEventSubscriber extends AbstractRedisEventSubscriber {
private final PaymentEventHandler paymentEventHandler;
@Override
protected RedisTopicPattern getTopicPattern() {
return RedisTopicPattern.PAYMENT_EVENTS;
}
@Override
protected RedisEventHandler getHandler() {
return paymentEventHandler;
}
}
이처럼 다른 부분은 하위 클래스에서 직접 제공하도록 하여 추상 클래스에서 정의된 메서드가 자식이 제공해주는 걸 사용할 수 있게한다. 결과적으로 일반 클래스에 중복 코드는 없지만 바뀌기 전과 동일한 기능을 하게 되는 것이다.
이렇게 추상 클래스를 만들어서 상속하면 좋은 점이 있다. 바로 구조화다. 다른 사람이 코드를 참조할 때, 해당 모듈을 사용하는 클래스들을 빠르게 파악할 수 있다.
또 다른 장점은 중복 제거다. 초반에는 복잡하지 않고 기능도 얼마 안돼서 관리하기 쉬울 수 있다. 그렇게 같은 기능을 하는게 10개가 된다면… 아마 귀찮고 짜증날 것이다. 10가지 다 수정하는 시간, 깜빡 했을 때의 오류 탐지 시간 등을 고려했을 때, 추상 클래스를 쓴다면 하나의 모듈만 수정하면 된다.
사실
private final RedisMessageListenerContainer container;
이 필드가 필요해서 추상 클래스를 쓴 것인데, 이게 필요하지 않았다면 인터페이스로도 똑같은 기능을 만들 수 있다. 그렇다면 추상 클래스와 인터페이스 2가지 방법이 있다면 뭘 선택해야할까? 결론부터 말하자면 인터페이스다. 추상 클래스는 무조건 단일 상속이다. 반면, 인터페이스는 다중 상속이 가능하다.
이것이 뭘 의미하는가? 어떤 클래스 C에서 이미 완성된 추상 클래스 A를 사용하려 한다고 하자. 그런데 어떤 모듈 B를 인터페이스로 충분히 구현할 수 있었음에도 추상 클래스로 구현했고 C가 B도 사용해야 하는 상황이라고 하자. 이러면 A나 B중 하나만 상속할 수 있다. 즉, 유연함이 떨어지는 것이다.
추상 클래스는 여러 클래스 간 공통된 필드와 중복된 로직이 있을 경우, 중복을 제거하고 공통의 템플릿을 정의하기 위한 방법이다. 변하지 않는 로직을 상위 클래스에 구현하고 하위 클래스마다 다른 부분을 추상 메서드로 위임한다면, 유지보수성이 높은 코드를 짤 수 있다.