<TIL> 124. IoC/DI

YUJIN LEE·2023년 6월 1일
0

개발log

목록 보기
114/149

의존성 주입방식 (생성자, Setter, Field)

의존성 주입(Dependency Injection)은 객체 간의 의존 관계를 느슨하게 만들기 위한 디자인 패턴.
의존성 주입은 객체가 직접 필요로 하는 의존 객체를 생성하는 대신, 의존 객체를 외부에서 주입해 사용하는 방식.

주로 생성자, setter, 필드 세 가지 방식으로 의존성 주입을 구현할 수 있다.

생성자

생성자를 통한 의존성 주입은 객체를 생성할 때 필요한 의존 객체를 생성자의 매개변수로 전달하는 방식.

public class Client {
    private Service service;

    public Client(Service service) {
        this.service = service;
    }

    public void doSomething() {
        service.execute();
    }
}

public interface Service {
    void execute();
}

public class ServiceImpl implements Service {
    @Override
    public void execute() {
        // 서비스 실행 로직
    }
}

// 사용 예시
Service service = new ServiceImpl();
Client client = new Client(service);
client.doSomething();

위의 예시에서 Client 클래스는 Service 인터페이스에 의존한다.
생성자에서 Service 객체를 받아 멤버변수인 service에 할당.

doSomething 메서드에서는 주입받은 service 객체의 execute 메서드를 호출해 작업 수행.

Service 인터페이스는 execute 메서드를 정의,
ServiceImpl 클래스는 이 인터페이스를 구현해 execute 메서드 구현

사용예시에서 ServiceImpl클래스의 객체를 생성해 Service 타입의 변수 service에 할당,
Client 객체를 생성시 생성자를 통해 service 객체 주입.
doSomething 메서드를 호출해 의존성 주입 통해 주입된 service객체의 execute 메서드 실행

-> 생성자 주입은 의존성의 불변성과 명시적인 의존성 표현을 강조해 코드의 안정성과 가독성을 높이는데 도움.

Setter

Setter 메서드를 통한 의존성 주입은 의존 객체를 설정하는 Setter 메서드를 통해 주입하는 방식.

public class Client {
    private IService service;

    public void setService(IService service) {
        this.service = service;
    }

    public void doSomething() {
        service.execute();
    }
}

Client 클래스는 IService 인터페이스에 의존하며,
setService 메서드를 통해 의존 객체를 주입받는다.
doSomething 메서드에서는 주입받은 service 객체의 execute 메서드를 호출하여 작업 수행

public interface IService {
    void execute();
}

IService 인터페이스는 execute 메서드를 정의하고 있다.
이 인터페이스를 구현하는 클래스가 주입.

public class ServiceImpl implements IService {
    public void execute() {
        // 서비스 실행 로직
    }
}

ServiceImpl 클래스는 IService 인터페이스를 구현하고 있어 execute 메서드에서 실제 서비스 실행 로직 구현

// 사용 예시
IService service = new ServiceImpl();
Client client = new Client();
client.setService(service);
client.doSomething();

위의 예시에서의 ServiceImpl 클래스의 객체를 생성해 IService 타입의 변수 service에 할당.
Client 객체를 생성한 후, setService 메서드를 통해 service 객체를 주입한다.
마지막으로 doSomething 메서드를 호출하여 의존성을 주입을 통해 주입된 service 객체의 execute 메서드를 실행한다.

Field

필드를 통한 의존성 주입은 의존 객체를 클래스의 필드로 선언하고, 외부에서 직접 주입하는 방식.

public class Client {
    private IService service;

    public void doSomething() {
        service.execute();
    }
}

public interface IService {
    void execute();
}

public class ServiceImpl implements IService {
    public void execute() {
        // 서비스 실행 로직
    }
}

위의 코드에서 Client 클래스는 IService 인터페이스에 의존하며,
service라는 필드를 가짐.

의존성 주입을 수행하기 위해 Client 클래스의 필드 service에 의존 객체를 주입할 수 있다.

IService service = new ServiceImpl();
Client client = new Client();
client.service = service;
client.doSomething();

위의 코드에서는 ServiceImpl 클래스의 객체를 생성하여 IService 타입의 변수 service 에 할당.
그리고 Client 객체의 Service 필드에 주입.
마지막으로 doSomething 메서드를 호출해 의존성 주입을 통해 주입된 service 객체의 execute 메서드를 실행.

필드를 통한 의존성 주입은 직접적으로 필드에 접근해 주입해,
일반적으로 생성자나 setter 메서드를 사용하는 것보다 더 느슨한 결합도를 가짐.
그러나 필드에 직접 접근해 캡슐화의 원칙에 위배할 수 있어 주의해야함.

스프링 공식 문서에서 생성자 주입 방식 사용을 정식으로 권고하는데, 그 이유?

의존성의 불변성(Immutability of Dependencies)

  • 생성자 주입은 의존성을 객체 생성 시점에 한번만 설정하고, 이후에 변경할 수 없도록 만듬.
    즉, 한번 설정된 의존성은 불변하게 유지.
  • 이는 의존성이 프로그램 실행 중 예기치 않게 변경되거나 오염될 위험을 줄임.
  • 불변성은 코드의 안정성과 예측 가능성을 높이는데 도움을 줌.

명시적인 의존성 표현(Explicit Dependency Expression)

  • 생성자 주입은 의존성을 생성자 매개변수로 명시적으로 표현
    -> 코드의 가독성과 이해성을 높임
  • 생성자를 통해 어떤 의존성이 필요한지 명확하게 드러나기 때문에, 코드를 이해하고 테스트하거나 리팩토링하는데 용이.

의존성 주입을 통한 테스트 용이성(Testablilty through Dependency Injection)

  • 생성자 주입은 의존성을 외부에서 주입받아, 의존성을 모의 객체(Mock)나 다른 테스트용 객체로 쉽게 대체 가능
    -> 단위테스트나 통합테스트에서 의존성을 대체해 테스트를 더욱 효과적으로 수행할 수 있게 도와줌.

의존성 해결과 순환 참조 방지(Dependency Resolution and Circular References)

  • 생성자 주입은 스프링의 의존성 해결(Dependency Resolution) 메커니즘과 잘 호환.
  • 스프링 컨테이너가 객체를 생성하고 의존성을 자동으로 해결하는데 적합.
  • 순환참조(circular reference)문제를 방지하는데 도움을 줌

객체지향 관점에서 DI 방식으로 의존성 문제를 해결하면 어떤 장점?

느슨한 결합(Loose Coupling)

  • DI는 의존성을 주입함으로써 객체간의 결합도를 낮춤.
  • 의존 객체를 직접 생성하거나 참조하는 대신 외부에서 주입받아, 객체 사이의 의존성 감소
    -> 유연성과 확장성을 높여 변경에 더 잘 대응할 수 있는 코드를 작성할 수 있게함.

단일 책임 원칙(Single Responsibility Principle)

  • DI는 객체가 자신의 주요 책임에 집중할 수 있게함.
  • 의존성을 주입받는 객체는 주입받은 의존 객체에 특정 기능이나 작업을 위임.
    -> 객체의 역할이나 책임을 분리하고 단일 책임 원칙을 준수하는데 도움

테스트 용이성(Testability)

  • DI는 의존성을 외부에서 주입받아 테스트할 때 의존 객체를 모의 객체로 대체 가능
  • 의존 객체의 동작을 테스트해 외부 리소스에 의존하지 않고도 단위테스트, 통합테스트 수행
  • 의존 객체를 대체하거나 가짜 객체로 대체해 테스트를 더욱 쉽고 신속하게 작성.

코드 재사용성(Code Reusability)

  • DI를 통해 객체를 주입받은 의존 객체를 사용해 동일한 의존 객체를 다른 객체에서도 쉽게 재사용 가능.
  • 의존성 주입을 통해 객체 간 의존성 관리를 중앙화, 중복 코드를 피할 수 있다.

관심사의 분리(Separation of Concerns)

  • DI는 객체가 자신의 주요 기능에 집중할 수 있도록 함, 의존 객체의 생성 및 관리에 대한 책임을 외부에 위임
    -> 객체의 역할과 책임 분리, 코드의 가독성 유지보수성 향상

수업때 진행한 내용들

스프링 핵심기능은 @Annotaion/XML/Groovy/자바 클래스 파일을 통해 객체를 생성 및 초기화

이렇게 생성되고 초기화된 객체를 Bean 객체라고 말한다.

우리는 레고처럼 여러개의 객체를 조합해 하나의 프로그램을 완성시킨다.
그럼 B객체에서 A객체의 기능(메서드)를 사용ㅎ아는 경우 우리는 어떻게 할 수 있을까?

의존성?
B클래스가 A클래스의 메서드를 호출할 때
이를 B클래스는 A클래스의 의존적이다.

의존성주입 방법

의존 객체를 전달 받는 방식 vs 의존 객체를 직접 생성 방식

spring에서는 Bean 객체를 IoC 컨테이너라는 곳에 저장해놓고 Bean객체에 의존하고 있는 객체에 전달.
이렇게하면 어떤점이 좋을까 ?

  1. 의존 관리의 용이성 -> IoC 컨테이너가 Bean 객체를 생성하고 주입해줌으로 의존성 자동 해결
  2. 객체 생명주기 관리 -> IoC 컨테이너는 객체의 생성, 초기화, 소멸과 같은 생명주기 관리. Bean 객체의 생성과 소멸 시점을 컨테이너가 알고있어, 필요에 따라 객체를 싱글톤으로 관리하거나 요청에 맞게 새로운 인스턴스 생성
  3. 코드의 재사용성과 모듈화 -> 유지보수성 향상, 코드 재사용성 증가
  4. 테스트 용이성 -> 의존 객체를 목으로 대체 가능.
  5. AOP 지원 -> 스프링은 IoC 컨테이너 통해 AOP 지원.
    AOP는 핵심 비즈니스 로직과 관련 없는 공통적인 부가기능 분리해 모듈화 할 수 있게함.
  1. 의존성 주입방식 3가지(생성자, Setter, Field)에 대해서 예제 코드와 함께 이론 정리
  2. 스프링 공식 문서에서 생성자 주입 방식 사용을 정식으로 권고하고 있는데 그 이유가 무엇인지 정리하고 예제코드 작성
  3. 객체지향 관점에서 DI 방식으로 의존성 문제를 해결하면 어떤 장점이 있는지 예제코드 정리

블로그 추천

https://mangkyu.tistory.com/
https://goddaehee.tistory.com/

spring psa ?
https://ch4njun.tistory.com/270#:~:text=Spring%20PSA%20%EB%8A%94%20%EC%B6%94%EC%83%81%ED%99%94%20%EA%B3%84%EC%B8%B5,%EA%B3%84%EC%B8%B5%EC%9D%98%20%ED%95%B5%EC%8B%AC%EC%82%AC%ED%95%AD%EC%9D%B4%

profile
인정받는 개발자가 되고싶습니다.

0개의 댓글