본문 바로가기
가치관 쌓기/개발 돌아보기

개발자는 '추상화 이해하기'를 수련해야 한다.

by simplify-len 2022. 2. 22.

 

객체지향 프로그래밍에서 객체지향프로그래밍에서 말하는 중요한 원칙이 있습니다. 바로 SOLID 이다.

여기서도 'I' 에 해당하는 Interface Segregation Principle (인터페이스 분리 원칙) 에 대해서 먼저 알아보려 합니다.

 

Interface Segregation Principle(인터페이스 분리 원칙)

 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다. 하나의 일반적인 인터페이스보다는, 여러 개의 구체적인 인터페이스가 낫다.

before

 

User1, User2, User3 가 Ops 클래스에 op1, op2, op3 를 사용하고 있다고 가정합니다.

User1 입장에서는 op2, op3 를 사용하고 있지 않음에도 불구하고, 사용할 수 있는 환경입니다. User2 입장에서도 op1, op3 를 사용하지 않지만, 언제든 사용할 수 있습니다.

 

어떻게하면 잠재적으로 위험에 노출되는 상황을 막을 수 있을까요?

after

 

인터페이스를 활용하는 것입니다. UserX 가 필요로 하는 인터페이스를 만들어 이를 구현하게 누군가에게 위임하면 됩니다. 인터페이스는 다중 상속이 가능하니까요.

 이렇게 인터페이스 분리 원칙은 인터페이스를 두어 내부 의존성을 약화시켜, 리팩토링, 수정, 재배포를 쉽게 할 수 있도록 합니다.

 

 이 원칙을 듣다보면 당연한 것 같지만, 실제 개발에서는 잘 활용되지 못합니다. 코드가 떠오르지 않습니다. 그리고 인터페이스를 잘 분리하더라도 개발적으로 맞딱뜨리는 문제도 있습니다. 이 부분은 다음 포스트에서 알아보려고 합니다.

 

이 쯤에서 Interface Segregation Principle(인터페이스 분리 원칙)은 무엇인지 다시금 고민하게 됩니다. 

 

Interface Segregation Principle(인터페이스 분리 원칙)은 무엇일까요? 
추상화는 무엇일까요?
왜 이렇게 인터페이스? 추상화? 를 하는걸까요? 어떤 의미를 지니는 걸까요?

 

 

 이렇게 중간에 인터페이스를 둔다는 것은 과거에 스스로 생각하길 '추후 변경되는 요구사항에 유연하게 변경되어지기 위해서 사용되는 것'이라 생각했습니다. 실제로 리팩토링의 저자 마틴파울러가 말하길 '언제 인터페이스를 사용하나요?' 라는 질문에 처음에는 Class 로 구현한 뒤, 요구사항이 변경되어지면 그때 상황에 맞쳐 인터페이스로 리팩토링한다 말을 했었습니다. 저도 이것이 맞다고 생각해왔습니다.

 

그러나, 최근 프로젝트를 진행하면서 생각이 달라지고 있습니다. 인터페이스라는 것은 단순히 요구사항의 변경에 유연하게 대처하기 위한 용도만은 아니라고 생각합니다.

 인터페이스를 통해 추상화를 한다는 것은 풀고자 하는 문제에 필요한 최소한의 재료로 활용 할 수 있도록 사고방식을 전향해줄수 있는 도구라는 사실을 깨달았습니다.

 

 이번 프로젝트를 진행하면서 했던 가장 큰 실수는 '구현체에 의존하는 프로그래밍' 이였습니다. 코드로 살펴보겠습니다.

public class EnvelopeNotification {

    @Id
    private EnvelopeNotificationId id;

    private NotificationType type;

    @With
    private NotificationStatus status;

    private Notification notification;

    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime modifiedAt;

    @Version
    private Long version;
}

위 객체는 RDBMS에 담기 위해 만든 Domain 코드입니다. 만약 해당 객체를 사용하는 Client 에서 NotificiationType을 통해 분기 처리를 하게 된다면 아래와 같은 코드가 될 것입니다.

void execute() {
        final List<EnvelopeNotification> envelopNotifications = targetProvider.getTargets();

        for (EnvelopeNotification target : envelopNotifications) {

            Notificator notificator = factory.findBy(target.getType());
            RequestResult result = notificator.execute(target);
			
            ...
        }
    }

위 코드의 문제점은 무엇일까요? 문제가 없어보이나요? 저도 당장에 바라보기에는 문제가 없어보였습니다. 그러나 잘생각해봅니다. Client.execute() 에서 필요로 하는 것은 오직 type 밖에 없습니다. 그럼에도 불구하고 우리는 EnvelopeNotification 라는 구현체를 활용하고 있습니다.

 만약, EnvelopeNotification 이 아니라, 다른 구현체가 등장하게 되면 어떻게 될까요? 해당코드의 테스트 코드는 모두 깨지게 될 것입니다. 또한 Client의 코드도 수정되어져야 합니다. 이는 OCP(Open Closed Principle) 원칙 또한 위반합니다.

 

어떻게 해야될까요?

 

필요로 하는 것에만 집중합니다.

public interface NotificationTarget {
    NotificationType getType();
}
public class EnvelopeNotification implements NotificationTarget {

    @Id
    private EnvelopeNotificationId id;

    private NotificationType type;
	
    ...
}

 

NotificationTarget 을 추상화 단계를 올립니다.

 

아주 미미한 변화이지만, 이렇게 함으로써 우리가 얻을 수 있는 효과는 굉장합니다.

void execute() {
    final List<NotificationTarget> notificationTargets = targetProvider.getTargets();

    for (NotificationTarget target : notificationTargets) {

        Notificator notificator = factory.findBy(target.getType());
        RequestResult result = notificator.execute(target);

		...
    }
}

 

더이상 구현체에 의존하지 않음으로써 OCP 를 위반하지 않으며, Client 관점에서도 자신이 어떤것(NotificationTarget)을 활용하고 있는지 알 수 없게 됩니다. 

 

제가 가진 생각이 잘못된 부분이 있을 수 있습니다. 혹시라도 이상하거나 다른 생각을 가지고 있다면 댓글 부탁드립니다.

댓글