Skip to main content

설계 원칙/ DI와 서비스 로케이터

설계 원칙 : SOLID#

SOLID란?#

다섯가지의 원칙으로 구성됨

  • 단일 책임 원칙(SRP, Single Responsibility principle)
  • 개방-패쇄 원칙(OCP, Open-closed principle)
  • 리스코프 치환 원칙(LSP, Liskov substituion principle)
  • 인터페이스 분리 원칙(ISP, Interface segregation principle)
  • 의존 역전 원칙(DIP, Dependency inversion principle)

단일 책임 원칙(SRP, Single Responsibility principle)#

정의 : 클래스는 단 한개의 책임을 가져야 한다.

위반하는 경우.#

  • 절차지향적으로 구성되어 변경이 어려워집니다. 그 이유는책임의 단위는 변화되는 부분과 관련된다.

지키는 방법#

  • 메서드를 실행하는 주체를 아는 것이 중요하다.
  • 일반적으로 사용자들이 다른 경우는 다른 책임에 속할 확률이 높기 때문에 책임 분리 후보가 될 수 있습니다.

개방-패쇄 원칙(OCP, Open-closed principle)#

정의 : 확장에는 열려있어야하고, 변경에는 닫혀 있어야한다.

  • 기능을 변경하거나 확장할 수 있으며
  • 그 기능을 사용하는 코드는 수정하지 않는다.

즉, 기능을 확장하면서도 기능을 사용하는 기존 코드는 변경되지 않는 것입니다. 이를 구현할 수 있는 이유는 변화되는 부분을 추상화 했기 때문입니다.

이 외에도 개방 폐쇄 원칙을 구현하는 방법은 상속입니다. 오버라이딩을 통해서 이 원칙을 지킬 수 있습니다.

위반 하는 경우#

추상화와 다형성을 이용해서 일반적으로 개방 폐쇄 원칙을 구현하는데, 추상화나 다형성을 제대로 지키지 않은 코드는 개방폐쇄 원칙을 어기게 됩니다.

대표적인 예시

  • 다운 캐스팅을 합니다.
  • 비슷한 if-else 블록이 존재합니다.

개방 폐쇄 원칙은 유연함에 대한 것입니다.#

개방 폐쇄 원칙은 변경의 유연함과 관련된 원칙입니다. 즉, 변화가 예상되는 곳을 추상화해서 변경의 유연함을 얻도록 해줍니다. 따라서 코드에 대한 변화 요구가 발생하면, 변화와 관련된 구현을 추상화해서 개방 폐쇄 원칙에 맞게 수정할 수 있는지 확인하는 습관을 갖도록 해야합니다.

리스코프 치환 원칙(LSP, Liskov substituion principle)#

정의 : 상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야합니다. 일밙적으로 개방 페쇄 원칙을 받혀 주는 다형성에 관한 원칙을 제공합니다.

위반 하는 경우#

예시

  • instanceof 연산자 사용
  • 상위 타입에서 지정한 리턴 값의 범위에 해당되지 않는 값을 리턴

위반 사례

  • 명시적 명세에서 벗어난 값을 리턴합니다.
  • 명시된 명세에서 벗어난 익셉션이 발생합니다.
  • 명시된 명세에서 벗어난 기능을 수행합니다.

리스코프 치환 원칙은 계약과 확장에 대한 것#

일반적으로 리스코프 치환 원칙을 어기면 개방 폐쇄 원칙을 어길 가능성이 높아집니다.

인터페이스 분리 원칙(ISP, Interface segregation principle)#

정의 : 인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야합니다.

인터페이스 분리 원칙#

  • 자신이 사용하는 메서드에만 의존해야 한다는 원칙
  • 단일 책임 원칙이 잘지켜질 때, 인터페이스와 콘크리트 클래스의 재사용 가능성을 높일 수 있으므로 인터페이스 분리 원칙은 결국 인터페이스와 콘크리트 클래스의 재사용성을 높여줍니다.

의존 역전 원칙(DIP, Dependency inversion principle)#

정의 : 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안됩니다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야합니다.

고수준 모듈

  • 어떤 의미가 있는 단일 기능을 제공하는 모듈
  • ex) 바이트 데이터를 읽어와 암호화 하고, 결과 바이트 데이트를 쓴다

저수준 모듈

  • 고수준 모듈 들의 기능을 구현하기 위해 필요한 하위 기능의 실제구현
  • ex) 파일에서 바이트 데이터를 읽어옴, AES 알고리즘으로 암호화, 파일에 바이트 데이터를 씀

위반 하는 경우, 고수준 모듈이 저수준 모듈에 의존할 때#

프로젝트 초기에 요구사항이 어느 정도 안정화되면 이후부터는 큰틀에서 프로그램이 변경되기 보다는 상세 수준에서의 변경이 발행할 가능성이 높아집니다. 그렇기 때문에 변경이 어려워집니다.

의존 역전 원칙을 통해 변경의 유연함 확보#

추상화를 통해서 유연함을 얻을 수 있습니다. 고수준 모듈의 변경없이 저수준 모듈을 변경할 수 있는 유연함을 얻습니다.

즉, 리스코프 치환 원칙과 함께 개방 폐쇄 원칙을 따르는 설계를 만들어 주는 기반이됩니다.

의존 역전 원칙과 패키지#

  • 의존 역전 원칙은 타입의 소유도 역전시킵니다.
  • 이를 통해, 각 패키지를 독립적으로 배포할 수 있게 해줍니다.

SOLID 정리#

변화에 유연하게 대처할 수 있는 설계 원칙

  • 단일 책임 원칙과 인터페이스 분리 원칙을 통해 객체가 커지는 것을 막아줍니다. 이를 통해 객체가 단일 책임을 가지고 변경이 다른 곳에 미치는 영향을 최소화합니다.
  • 리스코프 치환 원칙과 의존 역전 원칙은 개방 폐쇄 원칙을 지원합니다.
  • 개방 폐쇄 원칙은 변화되는 부분을 추상화하고 다형성을 이용함으로써 기능을 확장하면서 기존 코드를 수정하지 않도록 만들어줍니다.

사용자 입장에서의 기능 사용을 중시합니다. 즉, SOLID 원칙은 사용자 관점에서의 설계를 지향합니다.


DI와 서비스 로케이터#

로버트 C 마틴은 소프트웨어를 2개의 영역으로 구분해서 설명합니다.

  • 고수준 정책 및 저수준 구현을 포함한 어플리케이션 영역
  • 어플리케이션이 동작하도록 각 객체들을 연결해 주는 메인 영역

여기서 메인 영역에서 객체를 연결하기 위해 사용되는 방법으로 DI가 있습니다.

메인 영역은 다음의 작업을 수행합니다. (즉, 해당 책임을 가집니다)

  • 어플리케이션 영역에서 사용될 객체를 생성합니다.
  • 각 객체 간의 의존 관계를 설정합니다.
  • 어플리케이션을 실행합니다.

서비스 로케이터 방식은 로케이터를 통해 필요로 하는 객체를 직접 찾는 방식이지만, 단점이 많아 외부에서 객체를 주입해주는 DI(Dependency Injection) 방식을 사용하는 것이 중요합니다.

DI을 이용한 의존 객체 사용#

사용할 객체를 직접 생성할 경우, 아래 코드처럼 콘크리트 클래스에 대한 의존이 발생하게 됩니다. 이는 의존 역전 원칙을 위반하게 되고, 결과적으로 확장 폐쇄 원칙을 위반하게 됩니다.

이를 해결하는 방법은 DI(Dependency Injection, 의존성 주입)입니다. DI는 필요한 객체를 직접 생성하거나 찾지 않고 외부에서 넣어주는 방식입니다.

DI를 적용하려면 객체를 전달받을 수 있는 방법을 제공해야하는데, 이 방법으로는 2가지 방법이 있습니다.

  • 생성자 방식
  • 설정 메서드 방식

생성자 방식#

생성자 방식은 생성자를 통해서 의존 객체를 전달받는 방식입니다.

public class JobCLI {
private JobQueue jobQueue;
public JobCLI(JobQueue jobQueue) {
this.jobQueue = jobQueue;
}
...
}
  • 일반적으로 좀 더 선호되는 방식입니다.
  • 객체를 생성하는 시점에서 필요한 모든 의존 객체를 준비할 수 있습니다.
  • 객체를 생성하는 시점에서 의존 객체가 정상적인지를 확인할 수 있습니다.

설정 메서드 방식#

설정 메서드 방식은 메서드를 이용해서 의존 객체를 전달받습니다.

public class Worker {
private JobQueue jobQueue;
public void setJobQueue(JobQueue jobQueue) {
this.jobQueue = jobQueue;
}
...
}
  • 객체를 생성한 이후에 의존 객체를 설정할 수 있기 때문에, 어던 이유로 인해 의존할 객체가 나중에 생성되는 경우에는 설정 메서드 방식을 사용해야합니다.
  • 의존 객체가 많은 경우, 설정 메서드 방식은 메서드 이름을 통해 어떤 의존 객체가 설정되는지 쉽게 알 수 있습니다.

스프링 프레임워크#

스프링 프레임워크는 생성자 방식과 설정 메서드 방식을 모두 지원합니다.

  • 생성자 방식, <constructor-arg>
  • 설정 메서드 방식, <property>

서비스 로케이터를 이용한 의존 객체 사용#

프로그램 개발 환경이나 사용하는 프레임워크의 제약으로 인해 DI 패턴을 적용할 수 없는 경우가 있습니다. (ex. 안드로이드)

public class MainActivity extends Activity {
private SomeService someService;
// 안드로이드 프레임워크가 실행해주지 않음, DI할 수 없음
public void setSomeService(SomeService someService) {
this.someService = someService;
}
// 안드로이드 프레임워크에 의해 실행됨.
@Override
public void onCreate(...) {...}
}

이러한 경우, 서비스 로케이터를 사용합니다.

서비스 로케이터는 애플리케이션에서 필요로 하는 객체를 제공하는 책임을 갖습니다. 서비스 로케이터는 애플리케이션 영역의 객체에서 직접 접근합니다.

객체 등록 방식의 서비스 로케이터 구현#

일반적으로 서비스 로케이터를 구현하는 쉬운 방법은 다음과 같습니다.

  • 서비스 로케이터를 생성할 때 사용할 객체를 전달합니다.
  • 서비스 로케이터 인스턴스를 지정하고 참조하기 위한 static 메서드를 제공합니다.
// 생성자를 이용해서 객체를 등록 받는 서비스 로케이터 방식
public class ServiceLocator {
private JobQueue jobQueue;
private Transcoder transcoder;
public ServiceLocater(JobQueue jobQueue, Transcoder transcoder) {
this.jobQueue = jobQueue;
this.transcoder = transcoder;
}
// get 메서드 생략.
...
// 서비스 로케이터 접근을 위한 static 메서드
public static ServiceLocater instance;
public static void load(ServiceLocater locater) {
ServiceLocater.instance = instance;
}
public static ServiceLocater getInstance() {
return instance;
}
}

다만, 모든 객체를 전달하는 것은 코드 가독성을 떨어트리게 됩니다.

이와는 반대로 객체를 등록하는 방식은 있는데 이 방식은 서비스 로케이터 구현이 쉽습니다. 다만, 서비스 로케이터는 객체를 등록할 때 인터페이스가 노출되어 있기 때무넹 의존 객체를 언제든지 바꿀수 있습니다.

상속을 통한 서비스 로케이터 구현#

  • 객체를 구하는 추상 메서드를 제공하는 상위 타입 구현
  • 상위 타입을 상속받은 하위 타입에서 사용할 객체 설정

제네릭/템플릿을 이용한 서비스 로케이터 구현#

서비스 로케이터의 단점은 인터페이스 분리 원칙을 위반합니다. 이 문제를 해결하기 위해서는 의존 객체마다 서비스 로케이터를 작성해줘야합니다. 다만, 클래스르를 중복해서 만드는 문제가 발생할 수 있습니다.

서비스 로케이터의 단점#

  • 객체가 다수 필요한 결우, 객체 별로 제공 메서드를 만들어줘야합니다.
  • 인터페이스 분리 원칙을 위배합니다.

따라서, DI를 쓸 수 있는 환경이라면 DI를 쓰는 것이 중요합니다.

Last updated on