GYUD-TECH

[스터디: 오브젝트] 유연한 설계 본문

스터디

[스터디: 오브젝트] 유연한 설계

GYUD 2024. 2. 26. 19:30

 

앞선 챕터에서 유연한 설계를 위한 의존성 설계 방법을 공부했다.

이번 챕터에서는 앞선 챕터의 내용을 명시적인 원칙으로 소개하여 앞장의 내용을 정리해 주었다.

 

이번 챕터를 읽을 떄 앞 장을 복습하고 읽으니 더 이해가 잘 되었던 것 같아서 이를 참고하는 것을 추천한다.

https://gyuwon-tech.tistory.com/35

 

[스터디: 오브젝트] 의존성 관리

이번 장에서는 의존성, 의존성 전이, 의존성 해결에 관해 이야기 한다. 객체간의 협력을 위해서는 의존은 필수적이다. 하지만 과도한 의존은 나쁜 설계를 만든다. 시스템의 특징과 트레이드오프

gyuwon-tech.tistory.com

 

추가적으로 자바의 모듈과 패키지에 대한 내용이 짧게 등장하여 관련 내용을 찾아보았다.

모듈의 개념과 도입 배경 및 간단한 사용법에 대해서 정리하였으며, 모던 자바 인 액션 책을 읽으면서 깊이있는 내용을 정리할 예정이다.

 


개방 폐쇄 원칙

SOLID 원칙 중 O에해당하는 원칙으로 소프트웨어는 확장에는 열려있어야하고, 수정에는 닫혀있어야 한다는 뜻이다.

 

Spring을 공부를 하며 이 원칙을 처음 접했었는데, 처음에는 이게 어떻게 가능한지 이해되지 않았다.

이후 객체지향에 대해서 공부를 하면서, 다형성을 활용한 의존관계를 설계를 통해 이게 가능하다는 것을 이해할 수 있었다.

 

위 설계에서는 Movie 객체가 DiscountPolicy라는 추상화된 인터페이스에 의존한다.

만약 NoneDiscountPolicy를 추가하고 싶다면 의존관계를 유지한 채로 DiscountPolicy를 상속받는 구현체를 새로 만들면 된다.

 

이렇게 하면 NoneDiscountPolicy 객체의 추가라는 확장에는 열려있고, DiscountPolicy의 변경에는 닫혀있는 설계를 완성했다.

결국에는 추상화를 통해 바뀌지 않는 부분만 남기고, 구현은 생략함으로써 OCP 원칙을 지킬 수 있다.

 


생성 사용 분리

말 그대로 객체를 생성하는 책임과 객체를 사용하는 책임을 분리해야 한다는 것이다.

한개의 객체에서 객체의 생성과 사용을 동시에 하게되면 객체를 생성하면서 필요한 구체적인 구현에 의존하게 된다.

public class Client {
    public Money getAvartarFee() {
        Movie avartar = new Movie("아바타", Duration.ofMinutes(120),
                                Money.wons(10000), new AmountDiscountPolicy(...));
        return avatar.getFee();
    }
}

위 코드에서는 Movie를 사용하는 Client 클래스에서 Movie 객체를 생성과 사용을 동시에 수행한다.

전문가 패턴에 의하여 Client 객체가 Movie 객체를 가장 잘 알기 때문에 Movie 객체를 생성하는 책임은 Client 객체에게 할당되었다.

 

하지만 이로 인해 Client는 Movie 객체의 구현에 대해 알게되었다.

 

만일 Movie 객체를 생성하는 곳이 Client 외에도 있다고 가정하자.

Movie 객체의 구현이 변경된다면, 곳곳에 퍼져있는 Movie 객체의 생성 코드를 수정해야한다.

전문가 패턴에 의해서 책임을 할당했지만, 응집도가 낮고, 결합도가 높은 설계가 만들어 진 것이다.

 

이를 해결하기 위해 Client 객체에서는 Movie 객체를 사용하기만 하고, Movie 객체의 생성을 관리하는 새로운 클래스를 만들 수 있다.

public class Factory {
    public Movie createAvartarMovie() {
        return new Movie("아바타", Duration.ofMinutes(120),
                        Money.wons(10000), new AmountDiscountPolicy(...));
    }
}
public class Client {
    private Factory facotry;

    public Client(Factory factory) {
        this.factory = factory;
    }

    public Money getAvartarFee() {
        Movie avatar = factory.createAvartarMovie();
        return avatar.getFee();
    }
}

이렇게 하면 Client 객체는 Movie 객체를 사용하기만 하면 되고, 모든 Movie의 생성은 Factory 객체에서 수행한다.

 

만일 Movie 객체의 구현이 변경된다 하더라도 Client 객체는 Movie 객체의 구현에는 의존하지 않기 때문에 상관없다.

일일이 Movie 객체를 의존하는 클래스들을 다 변경해줄 필요 없이, Factory 클래스의 구현만 변경해주면 되는 것이다.

 

아무리 다형성을 지키더라도, 결국 객체를 생성해주는 단계에서는 구현에 의존할 수 밖에 없다.

이로 인해 객체간 의존관계가 높아질 수 밖에 없다.

 

따라서 생성만을 책임지는 Factory 객체를 분리하면 Factory 객체만 다른 객체들을 의존하고, 협력 관계의 객체들은 인터페이스만을 의존할 수 있다.

 

스프링에서 스프링 컨테이너가 Factory 객체라고 할 수 있을 것이다.

연관관계를 맺어주기 위해서 해당 구현체들을 bean으로 등록하고, 이를 협력관계의 객체들에게 주입해준다.

오로지 객체를 생성해주는 역할의 객체가 Factory 객체이고, @Autowired와 같은 어노테이션을 통해서 의존성을 주입받을 수 있다.

 

책에서는 스프링의 DI 지원 기능에 대해서 언급하진 않았지만, Factory 객체에 어떤 것이 있을까를 고민하니 자연스럽게 스프링 컨테이너가 떠올랐다.

결국 스프링 컨테이너의 목적객체들의 의존관계를 줄이고, 높은 응집도와 낮은 결합도를 만들기 위한 것이다.

 

도메인 모델에서 출발해서 설계를 시작하지만, 유연성을 위해 책임을 옮기다 보면 도메인 모델에 포함되지 않는 순수한 가공물을 추가하게 되는데 이를 Pure Fabrication 패턴 이라고 한다.

 


의존성 주입

생성과 사용을 분리하면 외부에서 생성된 객체를 사용을 위해 주입받아야 하는데 이를 의존성 주입이라고 한다.

앞장에서 공부한 것과 같이 의존성 주입의 방법에는 3가지가 있다.

 

1. 생성자 주입

Movie avatar = Movie("아바타", Duration.ofMinutes(120),
                    Money.wons(10000), new AmountDiscountPolicy(...));

 

2. setter 주입

avatar.setDiscountPolicy(new AmountDiscountPolicy(...));

 

3. 메서드 주입

avatar.calculateDiscountAmount(screening, new AmountDiscountPolicy(...));

 

이 외에도 추가적으로 ServiceLocator 패턴을 통한 의존성 주입 방법을 소개한다.

 

ServiceLocator 패턴에서는 static 객체에 의존성을 저장해놓고 이를 활용하여 반복적으로 의존성을 주입하는 방법을 의미한다.

이렇게 하면 메서드나 생성자, 필드를 통해서 의존성을 주입하지 않아도 되기 때문에 의존성 주입이 간단해지는 장점을 가진다.

public class Movie {
    ...
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runningTime, Money fee) {
        this.title = title;
        this.runningTime = runningTime;
        this.fee = fee;
        this.discountPolicy = ServiceLocator.discountPolicy();
    }
}

하지만 위 코드만 봐서는 discountPolicy 인스턴스 변수에 어떤 구현체가 들어가는지 전혀 알 수 없다.

의존관계를 숨기는 것은 오히려 디버깅과 오류정정을 어렵게 만들기 때문에 좋지 않은 방법이다.

또한 static을 사용하기 때문에 단위 테스트 시에도 각각의 테스트가 서로 고립되지 않는다는 단점도 발생해 추천하지 않는다.

 


의존성 역전 원칙

전통적인 절차지향 프로그래밍에 의해 설계된 구조는 상위 수준의 모듈이 하위 수준의 모듈을 의존하며, 인터페이스가 아닌 구체적인 구현에 대해서 의존한다.

 

이와 반대로 객체지향 프로그래밍에서는 상위 모듈과 하위 모듈은 모두 추상화에 의존해야하며, 구체적인 구현에 의존해서는 안된다.

객체지향의 의존관계는 기존의 절차지향적 의존과계와 반대된다는 뜻으로 의존성 역전 원칙이라고 부른다.

이 원칙은 좋은 객체지향을 위한 SOLID 원칙 중 D에 해당되는 DIP 원칙이다.

 

객체지향에서는 의존성의 역전 뿐만 아니라 인터페이스의 소유권 역시 역전된다.

전통적인 절차지향적 설계에서는 아래와 같이 패키지를 설계한다.

 

 

민약 Movie 객체와 DiscountPolicy를 사용하는 중에 AmountDiscountPolicy가 수정되었다고 가정하자.

수정된 코드를 반영하기 위해서 AmountDiscountPolicy가 속한 패키지만 빌드하면 될 것 같지만, 이로 인해 DiscountPolicy도 다시 빌드 되면서 DiscountPolicy를 의존하는 Movie 객체가 속한 패키지도 빌드된다.

 

Movie와 DiscountPolicy와 같이 서로 연관성 있는 클래스를 다른 패키지에 두고, 해당 패키지에 불필요한 객체를 포함하였기 때문에 여러 패키지를 동시에 수정해야하는 문제가 발생한 것이다.

 

이를 해결하기 위해서 추상화된 DiscountPolicy를 클라이언트엔 Movie가 속한 패키지에 포함시킬 수 있다.

 

이렇게 하면 AmountDiscountPolicy가 수정된다 하더라도, DiscountPolicy는 빌드 될 필요가 없고 재사용이 가능하다.

이를 인터페이스가 분리되었다고 해서  SEPARATED INTERFACE 패턴이라고 부른다.

 

해당 내용을 설명하기 위해서 저자는 모듈이라는 개념을 언급한다.

자바에서 모듈을 구현하기 위한 방법이 패키지이라고 설명하며, 이후에는 패키지와 모듈을 동일하게 이야기한다.

 

이전에 자바에 대해 공부하면서, 모듈과 패키지는 다른 개념이라고 들었던 것 같아서 관련 내용을 추가로 학습하였다.

 

모듈

모듈은 패키지들을 담는 컨테이너로 패키지의 상위 개념이다.

상위 개념이기 때문에 모듈에는 패키지 뿐만 아니라 이미지나 파일과 같은 리소스까지 포함되어 있다.

 

패키지를 import 하듯이 모듈을 아래와 같이 정의하고, requires 키워드를 통해서 사용할 모듈을 지정할 수 있다.

module com.module {
    exports com.module;
}
module com.hello { 
    requires com.module;
}

package에서도 import 구문을 활용하여 패키지를 가져올 수 있는데 왜 모듈이라는 개념이 도입되었는지 궁금하여 관련 자료를 찾아보았다.

 

모듈은 패키지를 사용하였을 때 발생하는 아래 2가지 단점을 극복하기 위해 도입된 개념이다.

 

1. 패키지만으로는 패키지를 올바르게 캡슐화 할 수 없다.

  • 자바의 접근제어자는 메서드와 클래스간의 캡슐화를 정의하기 위한 용도일 뿐, 패키지간의 모든 접근성은 공개된다.
  • 이로 인해 어떤 모듈을 공개하고, 어떤 모듈은 숨겨야 할지 지정할 수 없었다.

모듈을 통해서는 어떤 모듈을 공개하고 숨겨야할지 접근제어를 지정할 수 있다.

 

 

2. 패키지 전체를 로드하는 것은 컴퓨터에 많은 부하를 일으킨다.

  • 자바의 새로운 기능들이 추가되면서 jdk의 덩치도 점점 커졌지만, jdk의 모든 기능을 사용하는 경우는 거의 없었다.
  • 특히 모바일이나 클라우드 환경과 같이 일부 기능만 사용하는 환경에서 전체 jdk 패키지를 로드 하는 것은 큰 부하가 되었다.

자바 9에서 패키지 구조를 분리하여 여러개의 모듈을 구성함으로써 필요한 모듈만 가져와 사용하도록 하였다.

이를 통해서 확장성과 재사용성을 높여 사물인터넷이나 클라우드 에서도 자바 런타임을 사용할 수 있게 되었다.

 

책에서 저자가 소개한 모듈의 내용은 자바 9에 도입된 모듈을 의미하기 보다는 특정한 기능을 수행하는 단위로 이해할 수 있을 것 같다.

 

위에서 정리한 모듈의 개념은 참고자료 블로그와 '모던 자바 인 액션'의 일부를 참고하여 작성하였다.

모던 자바 인 액션에 자바 9 모듈에 대한 깊은 내용이 소개되어 있어, 해당 내용만 미리 읽고 모듈에 대해 더 자세히 정리할 예정이다.

 


2장에 걸쳐서 유연한 설계를 하는 원칙에 대해서 소개하였다.

 

하지만 항상 위의 원칙을 지켜가며 코드를 짜야하는 것은 아니다.

추상화에 과도하게 의존한다면, 협력관계가 명시적으로 보이지 않고 코드가 복잡해진다.

 

의존성 관리 원칙은 유연한 설계를 위한 도구일 뿐 객체 지향 설계의 핵심은 역할, 책임, 협력이다.

먼저 역할, 협력에 기반하여 객체에게 책임을 할당하고, 이후에 의존성 관리 원칙을 통해서 유연한 설계를 만들어야 한다.

 


참고자료

https://velog.io/@kkywalk2/Java의-module-1

 

Java의 module

module? Java9 이전까지 순수하게 Java의 컴파일러와 JVM의 기능만으론 라이브러리 버전의 종속성이나 import할 Class 혹은 Package에 대한 정보를 명시 할 방법이 존재하지 않았다. Java9에 추가된 module기능

velog.io