반응형

객체지향설계에서 클래스는 반드시 느슨하게 결합되어야 한다. 느슨한 결합은 한 클래스의 변경이 다른 클래스들에게 영향을 미치지 않는 것을 의미한다. 그래서 어플리케이션을 지속가능하고 확장성있게 만든다.

 

클래스 간의 느슨한 결합을 위해 다양한 원칙, 디자인패턴, 프레임워크가 존재한다. 그 중에서 디자인 원칙인 IoC, DIP와 디자인 패턴인 DI, 프레임워크인 IoC Container를 통해서 어떻게 클래스 간의 결합을 느슨하게 하는 지 알아보겠다.

 

https://www.tutorialsteacher.com/Content/images/ioc/principles-and-patterns.png

 

IoC 원칙만 도입한다고 해서 클래스간의 결합을 느슨하게 할 수 있는 것은 아니기 때문에 DIP 원칙, DI 패턴(IoC 원칙을 구현하는 디자인 패턴 중 하나)을 함께 사용해야한다. 그리고 이 모든 과정을 마법처럼 처리해주는 IoC Container에 대해서도 알아보겠다.

 

먼저 아래에 있는 '강하게 결합된 클래스' 예제를 살펴보고 IoC, DIP 원칙과 디자인 패턴을 통해 어떻게 클래스간 결합을 느슨하게 하는 지 차근차근 살펴보자.

해당 글은 IoC, DIP, DI, IoC Container를 이해하기 위해 세 가지 주요 포스팅을 참조하였습니다(출처는 참고자료 확인). 주로 https://www.tutorialsteacher.com/ioc 포스팅의 내용을 의역하였고, 해당 포스팅의 C# 예제를 Java로 변환하였습니다. 보다 정밀한 정보 습득을 원하시는 분은 참고문헌을 읽어보시길 권합니다.


Step 0. 강하게 결합된 두 클래스

IoC에 대해 이해도를 높이기 위해 실무에서 자주 사용되는 전형적인 n-tier 아키텍쳐(UI ↔ Service ↔ BusinessLogic ↔ DataAccess)의 예제를 살펴보겠다. Step 0 예제에서는 BusinessLogic과 DataAccess만 다룬다.

 

CustomerBusinessLogic

public class CustomerBusinessLogic {
    DataAccess dataAccess;

    public CustomerBusinessLogic() {
        // 강하게 결합된 클래스 (CustomerBusinessLogic와 DataAccess)
        // 1. DataAccess 클래스의 구현체의 주소를 포함하고 있음
        // 2. DataAccess 클래스의 객체를 생성하고 생명주기를 관리함
        dataAccess = new DataAccess();
    }

    public String getCustomerName(int id) {
        return dataAccess.getCustomerName(id);
    }
}

 

DataAccess

public class DataAccess {

    public DataAccess() {
    }

    public String getCustomerName(int id) {
        return "customer name"; // from DB
    }
}

위 예제는 Github 에서 확인하실 수 있습니다.

코드 설명

  • DataAccess 클래스 : 데이터베이스에 접근하여 데이터 제공
  • CustomerBusinessLogic 클래스 : 고객 관련 도메인 로직을 담는 클래스
  • CustomerBusinessLogic 클래스는 '고객명 조회'라는 임무를 완료하기 위해 dataAccess.getCustomerName(id)를 호출한다. 이것은 CustomerBusinessLogic 클래스가 DataAccess 클래스가 없이는 해당 임무를 완료할 수 없음을 의미함. 그래서 "CustomerBusinessLogic 클래스는 DataAccess 클래스에게 종속(dependent)되었다." 혹은 "DataAccess 클래스는 CustomerBusinessLogic 클래스의 dependency이다."라고 표현함
  • CustomerBusinessLogic 클래스는 DataAccess 클래스의 객체를 생성하고 생명주기(life time)을 관리한다. 본능적으로 종속성을 가진 클래스의 객체를 제어하게 되는 것임
  • CustomerBusinessLogic 클래스가 구상 클래스인 DataAccess 클래스를 참조하고 있으며(CustomerBusinessLogic class includes the reference of the concrete DataAccess class), DataAccess 클래스의 객체를 생성하고 생명주기를 관리하기 때문에 두 클래스는 강하게 결합되어 있음 ****(concrete라는 표현을 번역하기 어려워 원문을 가져왔습니다. '추상'과 반대되는, 구체화되거나 모습을 갖추었다는 개념으로 생각하시면 접근하시기 편할 것 같습니다.)

클래스간 강한 결합의 문제점

  • DataAccess 의 변화가 CustomerBusinessLogic 에도 영향을 미칠 수 있음
  • (DataAccess 클래스가 DB에 연결되는 클래스라고 가정할때) 요구사항 변경으로 DataAccess 클래스와 클래스와 유사한 클래스(다른 데이터베이스 연동)가 생성되고, 이를 CustomerBusinessLogic 클래스가 참조해야한다면 CustomerBusinessLogic 클래스가 변경되야함
  • CustomerBusinessLogicnew 키워드를 통해 DataAccess 의 객체 생성함
    DataAccess 가 클래스명을 변경하면 new 키워드를 사용하여 DataAccess 객체를 참조하는 모든 클래스를 찾아 코드를 변경해주어야함 (동일 클래스의 객체를 만들고 의존성을 유지하기 위한 반복적인 코드)
  • CustomerBusinessLogic 클래스가 DataAccess 객체를 생성하기 때문에 독립적으로 테스트가 불가능하다 (TDD) => DataAccess 클래스가 mock 클래스로 대체될 수 없기 때문

Step 1. 제어의 역전(IoC, Inversion of Control)

IoC(제어의 역전)은 일종의 지침서로 best practices를 지향하지만 세부적인 구현 방법을 안내하지는 않는다. IoC는 객체지향 설계에서 클래스간의 결합도를 느슨하게 하기 위해 다양한 종류의 제어를 반전시킬 것을 권장하는 디자인 원칙이다. IoC를 구현하여 느슨하게 결합된 클래스를 만들어서 테스트가 용이하고, 지속가능하며 확장성있게 만들 수 있다.

IoC의 핵심은 제어를 역전하는 것이다. laymen은 IoC를 다음과 같이 설명했다. "당신이 차를 운전하여 출근한다. IoC는 당신을 대신할 운전자(택시, 대리운전사)를 고용해서 차를 끌게 하는 것이다. 이것을 제어의 역전이라 한다. 이렇게 함으로써 당신은 더 중요한 일에 집중할 수 있게 된다."

 

IoC 원칙은 제어의 역전을 제안한다. 이것은 제어를 다른 클래스에 위임하는 것을 의미한다. 종속성을 가진 클래스의 객체 생성 제어를 역전하는 것이다. 아래 예시를 통해 DataAccess 클래스의 객체 생성을 다른 클래스에 위임해보겠다. (CustomerBusinessLogic 클래스는 종속된 클래스의 객체 생성 제어에 관여하지 않게 된다!)

 

프로그램의 프로세스를 제어하기(Control Over the Flow of a Program)

또 다른 예로는 콘솔창에서 이름을 입력받아 저장할지 묻는 콘솔 프로그램이 있다고 가정해보자. 만약 이 프로그램을 GUI로 만든다면, 기존에 일어나던 사용자와의 커뮤니케이션 부분(이름을 입력받고 저장여부를 묻는 것)에 대한 제어를 GUI를 통해 IoC를 적용한 것이 된다.

 

종속된 객체 생성을 제어하기 (Control Over the Dependent Object Creation)

IoC는 종속된 클래스의 객체를 생성하는 때도 적용할 수 있다. 우선 종속(Dependent) 을 이해하기 위해 예제는 Step 0으로 다시 읽어보도록 하자.

https://www.tutorialsteacher.com/Content/images/ioc/ioc-patterns.png

IoC 원칙을 적용하기 위한 패턴은 여러 가지가 있다. 이번 예제에서는 Factory 패턴을 통해 IoC 원칙을 구현해보겠다.

 

IoC 원칙 구현을 위한 Factory 패턴 적용

IoC 원칙을 더 이해하기 위해 Step 0 예제에 팩토리 패턴을 적용해보도록 하자.

 

CustomerBusinessLogic

public class CustomerBusinessLogic {

    public CustomerBusinessLogic() {
    }

    public String getCustomerName(int id) {
        CustomerDataAccess customerDataAccess = DataAccessFactory.getDataAccessObject();
        return customerDataAccess.getCustomerName(id);
    }
}

 

DataAccessFactory

public class DataAccessFactory {
    public static CustomerDataAccess getDataAccessObject() {
        return new CustomerDataAccess();
    }
}

 

CustomerDataAccess

public class CustomerDataAccess {
    public CustomerDataAccess() {

    }

    public String getCustomerName(int id) {
        return "홍길동"; // from DB
    }
}

간단한 IoC 구현 예제를 살펴봤다. IoC는 충분히 느슨한 설계를 달성하기 위한 첫 단계이다. 하지만 IoC 원칙만 적용했기 때문에 완전히 느슨한 결합 을 달성하지 못했다.

위 예제는 Github에서 확인할 수 있습니다.

코드 설명

  • 종속된 객체 생성을 제어하기 (Control Over the Dependent Object Creation)
  • CustomerBusinessLogic 클래스가 CustomerDataAccess(이전 명칭 : DataAccess) 클래스에 대한 제어를 Factory 패턴을 적용한 DataAccessFactory 클래스에게 위임함으로써 CustomerDataAccess 클래스에게 종속을 회피

여전히 남아있는 문제점

  • CustomerBusinessLogic 클래스는 여전히 CustomerDataAccess 클래스의 구상체(?)를 사용하고 있음 But the CustomerBusinessLogic class uses the concrete CustomerDataAccess class.
  • 이것은 IoC를 통해 객체 생성 제어를 다른 클래스에 위임했음에도 클래스가 여전히 강하게 결합된 것을 의미함

위 문제를 해결하고 느슨하게 결합된 설계를 위해, IoC와 DIP가 함께 적용되야한다. 다음 단계에서 DIP 원칙을 살펴보고 예제에 적용해보자.


Step 2. DIP 원칙 (Dependency Inversion Principle)

DIP 역시 클래스간의 느슨한 결합을 위한 디자인 원칙이다. 이전에 언급했듯이 느슨한 결합을 위해 DIP와 IoC를 함께 사용하기를 강력하게 권장하고 있다.

DIP는 SOLID 원칙 중 하나로 상위 레벨의 모듈이 하위 레벨의 모듈에 의존하지 않고 둘 다 추상화에 의존해야한다고 말한다.

 

DIP 정의 (혹은 규칙)

  • 상위 레벨의 모듈은 절대 하위 레벨 모듈에 의존하지 않는다. 둘 다 추상화에 의존해야한다. High-level modules should not depend on low-level modules. Both should depend on the abstraction.
  • 추상화는 절대 세부 사항(details)에 의존하면 안된다. Abstractions should not depend on details. Details should depend on abstractions.

 

예제를 통해 DIP 원칙 구현해보기

첫번째 규칙, 상위 레벨 모듈은 하위 레벨 모듈에 의존하지 않고, 두 모듈 모두 추상화에 의존한다.

DIP 정의를 참고하여 DIP 구현을 위해 첫번째로 해야할 것은 상위 레벨 모듈(클래스)와 하위 레벨 모듈(클래스)를 결정하는 것이다. 본 포스팅의 예제에서는 CustomerBusinessLogic 클래스가 CustomerDataAccess 클래스에 종속되어 있다. 그렇기 때문에 CustomerBusinessLogi 는 상위 레벨이고, CustomerDataAccess 는 하위 레벨이다. DIP의 첫번째 규칙을 따르자면 CustomerBusinessLogic 클래스는 CustomerDataAccess 의 구상체(?)에 의존하는 대신, 두 클래스 모두 추상화에 의존해야한다.

 

두번째 규칙, Abstractions should not depend on details. Details should depend on abstractions.

추상화는 캡슐화와 더불어 객체지향적 프로그래밍에 중요한 원칙 중 하나이다. 이것에 대한 정의는 무수히 많지만 추상화를 이해하기 위해 예제를 살펴보자.

추상화란 non-concrete 한 어떤 것을 의미한다. abstraction means something which is non-concrete

프로그래밍적 표현으로는 CustomerBusinessLogic 클래스와 CustomerDataAccess 클래스는 개발자가 객체를 생성할 수 있는 concrete 클래스이다. 그래서 프로그래밍적 표현으로 추상화는 non-concrete한 인터페이스나 추상 클래스를 생성하는 것을 의미한다. (=인터페이스나 추상 클래스의 객체를 생성할 수 없음을 의미하기도 함)

DIP에 따르면 CustomerBusinessLogic 는 상위 레벨 모듈이기 때문에 하위 레벨 모듈인 the concrete CustomerDataAccess 클래스에 의존하면 안된다. 두 클래스는 모두 추상화에 의존해야하며, 이것은 두 클래스 모두 인터페이스나 추상클래스에 의존해야하는 것을 뜻한다.

 

ICustomerDataAccess (신규로 추가되었다)

// 신규로 추가
public interface ICustomerDataAccess {
    String getCustomerName(int id);
}

 

CustomerBusinessLogic

public class CustomerBusinessLogic {
    private ICustomerDataAccess customerDataAccess;

    public CustomerBusinessLogic() {
        // CustomerDataAccess 클래스 대신 ICustomerDataAccess 인터페이스 타입으로 받음
        customerDataAccess = DataAccessFactory.getDataAccessObject();
    }

    public String getCustomerName(int id) {
        return customerDataAccess.getCustomerName(id);
    }
}

 

DataAccessFactory

public class DataAccessFactory {
  // CustomerDataAccess 대신 ICustomerDataAccess 타입으로 반환
    public static ICustomerDataAccess getDataAccessObject() {
        return new CustomerDataAccess();
    }
}

 

CustomerDataAccess

// ICustomerDataAccess 인터페이스를 구현
public class CustomerDataAccess implements ICustomerDataAccess {
    public CustomerDataAccess() {
    }

    public String getCustomerName(int id) {
        return "홍길동"; // from DB
    }
}
위 예제는 Github에서 확인할 수 있습니다.

코드 설명

  • 상위 레벨 모듈(CustomerBusinessLogic)과 하위 레벨 모듈(CustomerDataAccess)을 추상화(ICustomerDataAccess)에 의존함으로써 DIP를 구현했다. 또한 추상화(ICustomerDataAccess)는 details(CustomerDataAccess)에 의존하지 않고, details(CustomerDataAccess)는 추상화의 의존적이다.
  • 위 예제에서 DIP 원칙을 구현하여 얻는 장점은 CustomerBusinessLogicCustomerDataAccess 의 느슨한 결합이다. 두 클래스는 추상화(ICustomerDataAccess)를 바라보고 있다. 앞으로 ICustomerDataAccess 를 구현한 다른 클래스들 또한 쉽고 안전하게(?) 사용할 수 있게 되었다.

문제점

그러나 여전히 느슨한 결합 을 충분히 이뤄냈다고 말하기 어렵다.

왜냐하면 CustomerBusinessLogic 클래스는 ICustomerDataAccess 를 얻기 위해 팩토리 클래스(DataAccessFactory)를 참조하고 있기 때문이다. 이 부분이 바로 DI(Dependency Injection) 패턴의 적용이 필요한 부분이다!

 

다음 포스팅에서는 DI 패턴을 사용하여 남아있는 문제를 해결해보겠다.

반응형