본문 바로가기

Java/Methodology

[SOLID] 객체지향 설계 5원칙 - DIP

  • DIP (Dependency Inversion Principle)

위키에 다음과 같이 정의되어있다.

상위 계층(정책 결정)이 하위 계층(세부 사항)에 의존하는 전통적인 의존관계를 반전(역전)시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있다.
이 원칙은 다음과 같은 내용을 담고 있다.
첫째, 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
둘째, 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.

상당히 어렵게 설명이 되어 있는데 한 마디로 구현체가 아닌 Interface를 의존해야한다는 의미다.
이번에는 구현체를 숨기고 외부에서 의존성을 주입받는 방식에 대해서 알아보도록 한다.
Spring을 사용한다면 쉽게 자동화가 가능하지만 이번에는 실제 코드로 외부에서 주입하는 코드를 만들어 보겠다.

예시는 실제로 필자가 재직중인 회사에서 Jandi 메신저에서 Slack 메신저로 변경하였는데 DIP를 지키지않아서
발생했던 상황을 재현하였고 DIP를 지키면서 해결한 방법을 알아보겠다.

상황을 만들어보자.
User 클래스는 오류가 발생하면 Sender 인스턴스의 sendError 메소드 호출하여 Error Message를 전달한다.
Sender 클래스는 JandiClient를 통해서 메시지를 전달한다.

public class User {
    private Sender sender = new Sender();
    public void errorOccur() {
        sender.sendError("에러 발생");
    }
}

public class Sender {
    private JandiClient jandiClient = new JandiClient();
    public void sendError(String message) {
        jandiClient.sendMessage(message);
    }
    public void sendSuccess(String message) {
        jandiClient.sendMesssage(message);
    }
}

public class JandiClient {
    public void sendMessage(String message) {
        System.out.println(message);
    }
}

메신저의 종류가 변경되지 않는다면 아무런 문제가 없는 코드다.
그러던 어느 날 회사에서 메신저를 Slack으로 바꾸라는 오더가 내려온다.

SlackClient 만들어서 구현하고..
Sender에서 JandiClient 걷어내고 SlackClient 붙이고..
바뀐 코드를 살펴보자.

public class User {
    private Sender sender = new Sender();
    public void errorOccur() {
        sender.sendError("에러 발생");
    }
}

public class Sender {
    private SlackClient slackClient = new SlackClient();
    public void sendError(String message) {
        slackClient.sendMessage(message);
    }
    public void sendSuccess(String message) {
        slackClient.sendMesssage(message);
    }
}

public class SlackClient {
    public void sendMessage(String message) {
        System.out.println(message);
    }

}

메신저가 변경되었다는 이유로 Sender클래스의 코드까지 많은 부분 변경되었다.
만약 JandiClient를 통해서 메시지를 전송하는 부분이 많다면 상당히 많은 코드를 수정해야할 것이다.
이러한 이유로 자주 변경이 일어날 수 있는 부분은 절대로 구현체를 의존하고 있으면 안된다.

이제부터 좋은 코드를 알아보도록 한다.
먼저 MessageClient Interface를 만들고 JandiClient와 SlackClient가 이를 구현하도록 만들어본다.

public interface MessageClient {
    void sendMessage(String message);
}

public class JandiClient implements MessageClient {
    @Override
    public void sendMessage(String message) {
        System.out.println(message);
    }
}

public class SlackClient implements MessageClient {
    @Override
    public void sendMessage(String message) {
        System.out.println(message);
    }
}

이번에는 Sender 클래스가 구현체가 아닌 Interface에 의존하도록 만들어본다.
이때 의존성을 외부에서 주입받을 수 있도록 생성자를 하나 만들어준다.

public class Sender {
    private final MessageClient messageClient;
    public Sender (MessageClient messageClient) {
        this.messageClient = messageClient;
    }
    public void sendError(String message) {
        messageClient.sendMessage(message);
    }
    public void sendSuccess(String message) {
        messageClient.sendMessage(message);
    }
}

이제 User 클래스만 조금 수정한다면 정말 간단하게 Jandi 메신저와 Slack 메신저를 바꾸며 메시지를 보낼 수 있다.

  • Jandi로 메시지를 보내고 싶은 경우의 User 클래스
public class User {
    private JandiClient jandiCleint = new JandiClient();
    private Sender sender = new Sender(jandiCleint);
    public void errorOccur() {
        sender.sendError("에러 발생");
    }
}
  • Slack으로 메시지를 보내고 싶은 경우의 User 클래스
public class User {
    private SlackClient = new SlackClient();
    private Sender sender = new Sender(SlackClient);
    public void errorOccur() {
        sender.sendError("에러 발생");
    }
}

이제 Sender 클래스는 생성될 때 자신을 사용하는 곳에서 의존성을 주입하기 때문에 어느 메신저로 메시지를 보내는지 몰라도된다.
언제든 User 클래스에서 Sender 클래스를 생성할 때 주입하는 구현체를 통해서 원하는 메신저로 메시지를 보낼 수 있다.

여기까지 진행했다면...
결국 User 클래스가 변경되는건 변함없는거 아닌가요??? 라는 의문이 생길 수 있다.
맞다. User 클래스의 변경을 막을 수는 없었다.
여기부터는 Spring이 해결해준다.
Spring에서는 DIP를 어떻게 지키는지 확인해본다. (편의를 위해서 Field Injection을 사용하겠다.)

@Autowired 어노테이션을 사용하여 Spring으로 부터 의존성을 주입받기 때문에
메신저가 변경되어도 User와 Sender 클래스는 단 한줄의 코드 변경도 필요없다.

public class User {
    private Sender sender = new Sender();
    public void errorOccur() {
        sender.sendError("에러 발생");
    }
}

public class Sender {
    @Autowired
    private MessageClient messageClient;
    public Sender (MessageClient messageClient) {
        this.messageClient = messageClient;
    }
    public void sendError(String message) {
        messageClient.sendMessage(message);
    }
}

public interface MessageClient {
    void sendMessage(String message);
}
  • Jandi로 메시지를 보내고 싶은 경우
@Primary
@Component
public class JandiClient implements MessageClient {
    @Override
    public void sendMessage(String message) {
        System.out.println(message);
    }
}

@Component
public class SlackClient implements MessageClient {
    @Override
    public void snedMessage(String message) {
        System.out.println(message);
    }
}
  • Slack으로 메시지를 보내고 싶은 경우
@Component
public class JandiClient implements MessageClient {
    @Override
    public void sendMessage(String message) {
        System.out.println(message);
    }
}

@Primary
@Component
public class SlackClient implements MessageClient {
    @Override
    public void snedMessage(String message) {
        System.out.println(message);
    }
}

정말 간단해졌다.
@Primary 어노테이션의 유무만으로 메신저의 종류를 변경할 수 있게 되었다.