도서 요약 / / 2023. 1. 30. 21:22

의존성 역전 원칙 : DIP(Dependency Inversion Principle)

이 내용은 udemy의 SOLID Design Principles를 정리한 내용입니다.

https://www.udemy.com/course/design-patterns-in-java-concepts-hands-on-projects/


Dependency Inversion Principle에 대해 많이 들어봤을 것이다. 스프링을 사용해 본 적이 있으면 이 원칙을 실제 사용을 해본 것이다.

Dependency Inversion Principle의 정의를 한번 알아보자.

A. 고수준 모듈은 저수준 모듈을 의존해서는 안된다. 양쪽 모두 추상화에 의존해야 한다.
B. 추상화는 세부 사항에 의존해서는 안된다. 세부 사항은 추상화에 의존해야 한다.

의존성(Dependency)은 무엇인가?

public void printMe() {
  System.out.println("Hello!");
}

이 코드는 실제 System 클래스에 정의된 객체 out을 의존하고 있고 println을 사용하고 있다. 즉, out 객체가 의존성이다.

의존성에 대해 좀 더 알아보기 위해 보고서를 출력하는 메소드를 작성하고 있다고 해보자.

public void writeReport() {
  Report report = new Report();
  // Build the report
  JSONFormatter formatter = new JSONFormatter();
  String report = formatter.format(report);
  FileWriter writer = new FileWriter("report.json");
  // write out report
}

여기서 의존성은 무엇인가?

우선, 리포트 객체에서 특정 객체, 어떤 모듈이 필요하고 JSON으로 변경을 한다. 그러한 특정 객체가 의존성이다.
그리고 나서 실제 JSON 문자열이 필요하고 디스크에 쓰게 된다. Java IO를 통해 디스크에 쓰게 되는데 이것도 의존성이다.

우리가 작성하는 코드에 기능을 제공하기 위해 필요한 어떤 것이다. 의존성 역전이란 우리가 일반적으로 하는 것과 반대로 어떤 것을 하는 것을 말한다.

public void writeReport() {
  Report report = new Report();
  // Build the report
  JSONFormatter formatter = new JSONFormatter();
  String report = formatter.format(report);
  FileWriter writer = new FileWriter("report.json");
  // write out report
}

위의 코드에서 특정 작업을 하기 위해 객체를 생성하고 있다. 이 작업을 하는데 리포트 생성과 강하게 결합되어 있는 이유이다.

만약, 누군가가 HTML 포맷으로 출력되길 원한다면, 어떻게 해야할까? 보고서 생성 메소드를 수정해야만 할 것이다.
만일, 누군가 디스크에 쓰는 것은 좋지 않은 방법이어서 다른 서버로 리포트를 전송해야 한다고 한다면 어떻게 해야 할까?
기존 리포트 생성 메소드를 수정해야 하고 이미 작성이 완료됬고 테스트도 끝난 코드는 엄청 복잡해질 것이고 보다 많은 버그 혹은 결함이 생길 가능성이 있을 것이다. 의존성 역전 원칙이 이러한 문제를 해결해 주는 것이다.

의존성 역전 원칙은 강한 결합을 사용하지 않고 고수준 모듈의 기존 비즈니스 로직을 구현하는 것이다.
저수준 모듈은 기본적으로 간단해서 어디서나 사용할 수 있다. (예: Java 객체를 JSON으로 변경하는 것)

그래서 고수준 모듈은 저수준 모듈을 의존해서는 안된다.

이제 이 원칙이 무슨 얘기를 하는지 이해가 되는가? 구체 클래스에 강하게 결합되어서는 안된다라고 말하고 있는 것이다. 양쪽 모두 추상화에 의존해야 한다는 것이다.

추상화는 무엇인가? 간단히 인터페이스가 될 수 있다. 그래서 ObjectMapper 혹은 파일 객체를 새로 만드는 대신 Formatter와 같은 인터페이스를 사용할 수 있다. Formatter를 사용해서 고수준 모듈을 작성할 것이다.

public void writeReport(Formatter formatter, Writer writer) {
  Report report = new Report();
  // Build the report
  String report = formatter.format(report);
  // write out report
  writer.write("myreport");
}

메소드에서 새 객체를 생성하지 않고 특정 파라미터로 주입받는 것이다.
그러면 코드가 더 이상 구체 클래스에 강하게 결합되지 않게 된다.

보고서를 만들기 원하는 누구나 HTML 보고서를 만들 수 있고 다른 포맷 구현체를 만들 수 있다.
XML 포맷을 원한다면 XML 보고서를 출력할 수 있다.

이런 식으로 Dependency Inversion이 가능하게 한다. 의존성을 초기화하는 대신 누군가가 의존성을 주입시켜 주는 것이다.
고수준에서 새 인스턴스를 생성하지 않고 누군가가 우리에게 주입시켜 주는 것이다.

예제

메시지를 출력하는 예제를 한번 보자.

public class MessagePrinter {

    // write message to a file
    public void writeMessage(Message msg, String filename) throws IOException {
        Formatter formatter = new JSONFormatter();
        try(PrintWriter writer = new PrintWriter(new FileWriter(filename))) {
            writer.println(formatter.format(msg));
            writer.flush();
        }
    }
}

MessagePrint를 보면 JSON 포맷팅을 하는 JSONFormatter와 출력하는 PrintWriter에 대한 객체를 생성하고 있다.

그리고 메시지를 출력하는 MessagePrinter이고 이를 실행하는 Main 클래스가 있다.

public class Main {

    public static void main(String[] args) throws IOException {
        Message msg = new Message("This is a message");
        MessagePrinter printer = new MessagePrinter();
        printer.writeMessage(msg, "test_msg.txt");
    }
}

Message 클래스 msg와 timestamp를 가지고 있다.

@Getter
public class Message {

    private String msg;

    private LocalDateTime timestamp;

    public Message(String msg) {
        this.msg = msg;
        this.timestamp = LocalDateTime.now(ZoneId.of("UTC"));
    }
}

JsonFormatter는 Message를 JSON으로 포맷팅 하는 클래스이다.

public class JSONFormatter implements Formatter {

    public String format(Message message) throws FormatException {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.writeValueAsString(message);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            throw new FormatException(e);
        }
    }
}

MessagePrinter 코드를 다시 보자.

public class MessagePrinter {

    // write message to a file
    public void writeMessage(Message msg, String filename) throws IOException {
        Formatter formatter = new JSONFormatter();
        try(PrintWriter writer = new PrintWriter(new FileWriter(filename))) {
            writer.println(formatter.format(msg));
            writer.flush();
        }
    }
}

이런 구현방식의 문제는 무엇인가?

콘솔에 출력하려면 이 방식은 동작하지 않을 것이고 PrintWriter 대신 다른 메소드를 작성해야 한다.

두 번째 문제는 메시지 포맷을 변경하려고 하면 다시 수정을 해야 한다. 이런 방식이 메소드 사이에 강하게 결합되어 있는 전통적인 소프트웨어 개발 방식인 것이다.

Dependency Inversion이 이런 의존성을 없어도 가능하게 해준다. writeMessage는 JSONFormatter에 강한 결합을 가져서는 안되고 추상화에 의존해야 한다. 여기서 추상화는 Formatter이다. 추상화에 의존할 때 코드 수정없이 객체 변경을 할 수가 있다.

이 작업을 어떻게 할 수 있는가?

MessagePrinter의 writerMessage에서 Formatter와 PrintWriter의 의존성을 주입받을 수 있도록 매개변수로 받자. 그래서 이 메소드를 사용하는 시점에 의존성을 주입할 수 있다.

public class MessagePrinter {

    // write message to a file
    public void writeMessage(Message msg, Formatter formatter, PrintWriter writer) throws IOException {
        writer.println(formatter.format(msg));
        writer.flush();
    }
}

그리고 Main 클래스를 보자.

public class Main {

    public static void main(String[] args) throws IOException {
        Message msg = new Message("This is a message");
        MessagePrinter printer = new MessagePrinter();
        try (PrintWriter writer = new PrintWriter(new FileWriter("test_msg.txt"))) {
            printer.writeMessage(msg, new JSONFormatter(), writer);
        }
    }
}

Main에서 호출하는 시점에 PrinteWriter와 JSONFormatter를 생성하여 주입하면 된다.

만일 콘솔로 출력하고 싶다면 아래와 같이 사용하면 된다.

public class Main {

    public static void main(String[] args) throws IOException {
        Message msg = new Message("This is a message");
        MessagePrinter printer = new MessagePrinter();
        try (PrintWriter writer = new PrintWriter(System.out) {
            printer.writeMessage(msg, new JSONFormatter(), writer);
        }
    }
}

이것이 Dependency Inversion의 위력이다.

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유