객체지향을 위한 의존성 주입

갱밍·2023년 11월 7일

주제 선정 이유

저번 주에 코드리뷰를 하면서 getInstance()로 클래스 인스턴스를 사용한 분을 봤는데,
이게 뭔가 찾아봤더니 싱글톤 패턴이더라고요

저도 싱글톤 패턴을 적용해보고 싶어서 이번 과제에 이곳저곳 썼답니다.
하지만 이렇게 하다 보니 객체 지향 설계가 깨지는 것 같아 이를 해결해보고자 공부했던 내용입니다


클래스 인스턴스 사용 방법

보통의 방법

new 키워드 사용

  • 구상 클래스(Concrete class)에 사용
  • 편하고 직관적
  • 하지만 이렇게 객체를 생성하게 되면 의존 역전 원칙을 위반하게 되며, 결과적으로 확장 폐쇄 원칙을 위반하게 됌
public Class LottoService{
	private final Lotto lotto = new Lotto();
}

싱글톤(Singleton) 패턴

  • 객체의 인스턴스가 오직 1개만 생성되는 패턴
  • 어디서든지 같은 인스턴스에 접근 가능

사용 이유

  1. 메모리 낭비 방지
    • 처음에 한번 생성한 고정된 메모리 영역 사용
  2. 데이터 공유가 쉬움 ( 하나의 인스턴스를 여러 곳에서 사용하기 때문에)

⇒ 인스턴스가 단 하나만 필요하거나, 여러 객체에 걸쳐 상태를 공유해야 할 때 유용

문제점

  1. 코드 자체가 많이 필요함
    • 정적 팩토리 메서드에 객체 생성을 확인하고 생성자를 호출하는 경우
      ⇒ 동시성 문제 해결 위해 syncronized 키워드를 사용
  2. 테스트가 어려움
    • 싱글톤 인스턴스는 전역 상태, 즉 자원을 공유하고 있음
    • 독립적인 테스트를 수행하려면 매번 인스턴스 상태를 초기화 해줘야함
  3. 의존성이 높아짐
    • new 클래스를 직접 사용하여 클래스 안에서 객체 생성 → 당연히 구체 클래스에 의존할 수 밖에 …
    • 다른 클래스로 바꾸고 싶으면? 직접 하나하나 찾아서 수정
// LottoService Class
public Class LottoService{
	private final Lotto lotto;

	public LottoService(){
		this.lotto = Lotto.getInstance();
	}
}

// Lotto class
public Class Lotto{
	private static final Lotto INSTANCE= new Lotto();

	private Lotto(){};

	public static Lotto getInstance() {
        return INSTANCE;
    }
}
💡 각각의 장단점이 있지만 이 친구들은 객체지향 원칙에 위배되네요! 객체지향 원칙을 따르려면 의존성 주입이 필요합니다

객체지향 원칙에 따른 클래스 주입 방법

애플리케이션 영역과 메인 영역

소프트웨어는 기본적으로 2가지 영역으로 구분됌

  • 애플리케이션 영역
    • 고수준 정책, 저수준 구현을 포함
  • 메인 영역
    • 애플리케이션이 동작하도록 각 객체들을 연결 ⇒ DI, 서비스 로케이터
      • 애플리케이션 영역에서 사용할 객체 생성
      • 각 객체 간의 의존 관계 설정
      • 애플리케이션 실행

메인 영역은 어플리케이션 영역의 객체를 생성, 설정, 실행하는 총 책임을 가짐

→ 어플리케이션 영역에서 사용할 하위 수준의 모듈을 변경하고 싶다면 메인 영역을 수정해야함

→ 어떻게? DI, 서비스 로케이터로!


의존성 주입(DI) 방법 3가지

생성자 주입 (Constructor Injection)

  • 객체를 생성할 때 생성자를 통해 의존성을 주입
  • 객체가 생성될 때 필요한 모든 것이 준비되어 있음
  • 테스트 코드 작성도 편함 (모의 객체(Mock)을 쉽게 넣어줄 수 있음)
public class LottoStatistics {
    private final LottoService lottoService;

		@Autowired
    public LottoStatistics(LottoService lottoService) {
        this.lottoService = lottoService;
    }
}

setter 주입 (Setter Injection)

  • setter 메서드를 통해 의존 관계 주입
  • 객체 생성 후 의존성을 바꿀 수 있음
  • final 키워드를 못 씀 (변경 불가능한(immutable) 객체를 만들 수 x)
public class LottoStatistics {
    private LottoService lottoService;

    public void setLottoService(LottoService lottoService) {
        this.lottoService = lottoService;
    }
}

필드 주입 (Field Injection)

  • 말 그대로 필드에 그대로 주입
  • 코드가 간결하다는게 가장 큰 장점
  • 숨겨진 의존성 → 의존 관계 파악하기 힘듦
  • 클래스 외부에서 접근이 불가능해 테스트하기 어려움
  • 앞선 방법은 DI가 없어도 사용이 가능한데, 얘는 DI 프레임워크가 없으면 사용할 수 없음
public class LottoStatistics {
    @Autowired
    private LottoService lottoService;

}

서비스 로케이터

  • 프레임워크 제약 등으로 DI 패턴을 적용할 수 없을 때 사용
    (ex. 안드로이드 플랫폼을 개발하는 모바일 앱의 경우 화면을 생성할 때 Activity 클래스를 상속받도록 하는데 이 경우에는 DI처리를 할 수 없다.)
  • 로케이터를 통해 필요한 객체를 직접 찾는 방식
    → 스스로 어떤 객체를 제공하는지 알아야함
  • 모든 의존성을 한 곳에서 관리
    • 유지보수가 좋음
    • 서비스 로케이터 자체가 전역 상태를 가지게 됌
    • ISP(인터페이스 분리 원칙)을 위반 (한 곳에 모여있으니)
public class ServiceLocator {
    private static final LottoService lottoService = new LottoService();

    public static LottoService getLottoService() {
        return lottoService;
    }
}

public class LottoStatistics {
    private final LottoService lottoService = ServiceLocator.getLottoService();

}
profile
공부 기록 중입니다

0개의 댓글