IoC, DI

PLC·2024년 9월 24일

Spring

목록 보기
1/1
post-thumbnail

스프링의 기본 컨셉에 대한 정리

IoC - Inversion of Control

  1. 정의
    1. 제어의 역전
  2. 예시
    1. controller나 service 같은 객체들의 동작을 구현은하지만, 해당 객체들이 어느 시점에 호출될지는 신경 안 쓴다. 프레임워크가 해당 객체를 생성하고 메서드 호출하고 소멸시킨다
    2. interface에 메서드를 정의하고 해당 interface를 상속받은 class에서 해당 메서드의 실제 구현을한다. 이러면 구현은 하위 클래스에서 하긴하지만 해당 메서드의 호출을 상위에 있는 interface에서 하므로 제어의 역전이 일어남 (템플릿 메서드 패턴에서도 동일)
  3. 장점
    1. 프로그램의 진행 흐름과 구체적인 구현을 분리할 수 있음
    2. 개발자는 비즈니스 로직에 집중 가능
    3. 구현체 사이의 변경이 용이
    4. 객체 간의 의존성이 낮아짐
  4. 참고
    1. 라이브러리와 프레임워크의 차이는 IoC로 설명 가능하다. 라이브러리는 어플리케이션 제어의 흐름을 가져가지 않는다

DI - Dependency Injection

  1. 정의
    1. 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것
    2. 객체 인스턴스를 생성하고 그 참조값을 전달해서 연결
    3. 의존대상을 직접 생성 혹은 결정하는 것이 아니라 외부로부터 주입받는 것
    4. 참고: 의존성 - B에서 변경이 일어났는데 A에도 영향을 미치는 것
  2. 주입 방법
    1. 생성자주입, 수정자 주입, 필드 주입, 일반 메서드 주입
  3. 구현 방법
    1. 설정정보 - config파일 직접작성 후 객체 직접 생성해서 주입
    2. 설정정보 - config파일 직접작성 후 @Configuration 달아서 주입
    3. 컴포넌트 스캔 - 설정정보 없이도 자동으로 스프링 빈 등록하는 기능 + Autowired
  4. 장점
    1. 의존성이 줄어든다: 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다
    2. 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다

2. 주입 방법

생성자 주입 (권장)

말 그대로 생성자를 통해 주입

인스턴스 선언 시에는 final 붙이고, 생성자에서 주입해줌 (생성자에 @Autowired 붙음)

장점: final 때문에 불변성 보장

수정자 주입

setter 같은 수정자 통해 주입 (마찬가지로 setter에 @Autowired 붙음)

선택, 변경 가능성이 있는 의존관계에서 사용

필드 주입

인스턴스 선언시 거기에 @Autowired붙임

코드는 간결하지만 외부에서 변경이 불가능하기 때문에 테스트하기 힘들다는 치명적 단점

사용하지마라~!

일반 메서드 주입

말 그래도 일반 메서드 주입

잘 안 씀

3. 구현 방법

3-b. @Configuration 사용 예제 코드

이론을 이해하기 위해 작성했고, 현재는 컴포넌트 스캔을 더 자주 쓰는 것 같음

public interface SpeakingLanguage {
    String introduceSelf();
}

public class French implements SpeakingLanguage {
    @Override
    public String introduceSelf() {
        return "Salut Je parle français";
    }
}

public class English implements SpeakingLanguage {
    @Override
    public String introduceSelf() {
        return "Hi I speak English";
    }
}
@Configuration
public class LanguageConfig {
    @Bean
    public SpeakingLanguage speakingLanguage() { return new French(); }
}
public class LanguageTest {

    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(LanguageConfig.class);
    SpeakingLanguage speakingLanguage = applicationContext.getBean("speakingLanguage", SpeakingLanguage.class);

    @Test
    public void 현재_설정된_언어(){
        System.out.println(speakingLanguage.introduceSelf());
    }
}

  1. @Configuration
    1. 스프링 컨테이너 사용
    2. @Configuration이 붙은 파일을 설정 정보로 사용. 여기에 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록 → 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 함
    3. 스프링 빈은 메서드의 명을 스프링 빈 이름으로 사용함
  2. 언어를 바꾸고싶다면 설정정보 파일에서만 바꾸면 됨
    1. 의존성 주입을 이용하면 의존관계 변경을 위한 수정이 필요가 없다고하던데 어쨌든 코드에 변경이 있는거 아니냐는 의문이 길게 남았다. 근데 이건 설정 정보 파일에서만 수정이 일어나고 클라이언트 코드인 테스트코드나 language인터페이스에서는 수정이 없기 때문에 수정이 없다고 보는 것. 클라이언트 코드 입장에서 하위 코드의 수정이 있다는 걸 몰라도 된다면 수정이 없는 거다

3-c. @Component (컴포넌트 스캔) 사용 예제 코드

public interface SpeakingLanguage {
    String introduceSelf();
}

@Component
@Primary
public class French implements SpeakingLanguage {
    @Override
    public String introduceSelf() {
        return "Salut Je parle français";
    }
}

@Component
public class English implements SpeakingLanguage {
    @Override
    public String introduceSelf() {
        return "Hi I speak English";
    }
}
@SpringBootTest
public class LanguageTest {
    @Autowired
    private SpeakingLanguage speakingLanguage;

    @Test
    public void 현재_설정된_언어(){
        System.out.println(speakingLanguage.introduceSelf());
    }
}

  1. 인터페이스에 configuration을 붙이는 대신 하위 구현체들에 @Component를 붙임
    1. 해당 애노테이션 추가하면 컴포넌트 스캔의 대상이 된다
  2. Autowired는 의존관계를 자동으로 주입해준다
    1. 룰을 간단하게만 설명하자면
      1. 먼저 타입 매칭을 시도한 후
      2. 이때 찾아진 빈이 여러개라면 필드 이름으로 매칭
      3. 또 여러개라면 파라미터 이름으로 매칭한다
      4. 다른 것들 더 있는데 여기서는 패스

내가 느낀 DI의 장점?

  1. 프로젝트 시작 전에 사용할 인터페이스를 정의한다. 인터페이스가 약속되고나면 팀원 각각은 해당 클래스가 어떻게 구현되는지는 신경쓰지 않아도 되기 때문에
    1. 개발 속도가 빨라진다.
    2. 테스트 코드 작성에 용이하다.
  2. 어떠한 클래스의 인스턴스를 생성할 때 해당 클래스의 생성자가 어떤식으로 정의되어있는지 신경쓰지 않아도 된다.

개인적인 정리

DI와 객체지향을 정리하다보니 둘의 장점이 구분하기 힘들다고 느꼈다. 생각해보니 DI는 객체지향의 장점을 더 극대화 시킨 방식이라고 느꼈다. 기본적인 의미에서의 객체지향에서는 해당 클래스가 구체적으로 어떻게 구현되어있는지 몰라도 쓸 수 있었지만, 어떻게 생성하는지는 알아야한다. 하지만 DI를 사용한다면 구체적 구현 뿐만 아니라 생성자의 형태도 몰라도 된다. 라고 느꼈다.

public class NotificationService {
    private EmailService emailService = new EmailService(); // 강한 결합

    public void sendNotification(String message) {
        emailService.sendEmail(message);
    }
}

public class NotificationService {
    private final EmailService emailService;

    @Autowired
    public NotificationService(EmailService emailService) {
        this.emailService = emailService; // DI를 통해 주입
    }

    public void sendNotification(String message) {
        emailService.sendEmail(message);
    }
}

상위코드가 단순히 객체지향만을 생각했을 때고

하위코드가 DI까지 사용했을 때의 코드이다

현재는 생성자에 따로 넘겨줄 인자가 없기 때문에 비슷해 보이지만, 생성자에 넘겨줘야할 정보가 있다고 생각하면 차이점이 좀 더 잘 보일 것이다.


되돌아보니 아는 게 제대로 없다는 생각에서 시작된 바닥부터 다시 공부하기

하면 할 수록 자괴감들지만 언젠가 이게 밑거름이 되길 바라며

profile
jusqu'au dernier silence

0개의 댓글