[Spring] IoC, DI, DIP

황시준·2023년 4월 13일
0

개발

목록 보기
3/4

0. 서론

스프링 코드를 작성하는데 DI, IoC, DIP 라는 단어가 자주 보인다.
대충 어떤건지는 아는데 누구한테 설명하라고 하면 못할거 같아 정리한다.
잘못된 부분 있으면 지적 바랍니다.

1. IoC(Inversion Of Control)

IoC(제어의 역전)
개인적으로 많이 헷갈렸던 개념이다.
개발자가 코드의 제어 흐름을 직접 제어하는게 아니라 프레임워크가 코드의 제어 흐름을 관리하는 개념이다.

Spring을 사용하다 보면 @Controller, @Service, @Autowired 등 다양한 Annotation을 사용한다. 이러한 Annotation을 사용해 일반적인 객체를 Bean으로 등록하거나 POJO객체로 등록할 수 있다.

이는 개발자가 의존 객체를 직접 만들어 사용하는게 아니라, Spring에서 생성한 객체를 주입 받아 사용하는 방법을 뜻한다.

스프링 IoC 컨테이너

  • Bean Factory
  • 어플리케이션 컴포넌트의 중앙 저장소
  • Bean 설정 소스로부터 Bean의 정의를 읽어, Bean을 구성하고 제공

Bean은 나중에 다룰건데 우선은 Spring IoC 컨테이너가 관리하는 객체를 뜻하는걸로 이해하면 된다.

2. DIP(Dependency Inversion Principle)

DIP(의존성 역전 원칙으로 객체간의 의존 관계를 느슨하게 만들어야 한다는 원칙이다.
의존관계를 느슨하게 하기 위해 상위 수준의 모듈은 하위 수준의 모듈에 의존하지 않고 추상화에 의존해야 한다는 원칙을 의미한다.
이를 통해 의존성을 역선시켜 더 유연하고 확장 가능한 코드를 작성할 수 있다.

이건 개념을 보면 더 헷갈리니 코드로 확인한다.

// 상위 수준의 모듈, 추상화에 의존
public interface NotificationService {
    void sendNotification(String message);
}

// 하위 수준의 모듈 1
public class EmailNotificationService implements NotificationService {
    @Override
    public void sendNotification(String message) {
        System.out.println("이메일로 알림 보내기: " + message);
    }
}

// 하위 수준의 모듈 2
public class SMSNotificationService implements NotificationService {
    @Override
    public void sendNotification(String message) {
        System.out.println("SMS로 알림 보내기: " + message);
    }
}

// 상위 수준의 모듈에 의존하는 클래스
public class NotificationSender {
    private NotificationService notificationService;

    // 생성자를 통한 의존성 주입
    public NotificationSender(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void sendNotification(String message) {
        notificationService.sendNotification(message);
    }
}

코드를 보면 상위 수준의 모듈에 해당하는 NotificationService가 추상화에 의존하는 Interface를 사용하고 있다.
그리고 하위 수준의 모둘 1, 2는 상위 수준의 모듈을 implemenmts하고 있다.
그리고 NotificationSender는 구체적인 구현 클래스인 EmailNotificationServiceSMSNotificationService에 직접 의존하지 않고 Interface를 통한 의존성을 추상화하는걸 알 수 있다.
이렇게 코드를 작성하면 NotificationSender가 다양한 NotificationService 구현 클래스를 사용할 수 있으며 확장성과 유연성이 높은 코드를 작성할 수 있다.

3. DI(Dependency Injection)

DI(의존관계 주입)은 객체 간의 의존관계를 코드 내에서 명시하지 않고 외부에서 객체의 의존성을 주입하는 개념이다.
즉, class외부에서 객체 생성을 한 후 의존성 있는 객체에 주입하는 방식이다.
이는 강하게 결합된 클래스들을 분리하고 어플리케이션 실행 시점에 객체간의 관계를 결정함으로서 객체간의 결합도를 낮추고 유연성을 확보한다.

DI Injection을 통해 의존성을 주입하는 방법은 다음과 같다.

  • 생성자 주입
  • Setter 주입
  • 필드 주입

3-1. 생성자를 통한 의존성 주입

공식적으로 추천하는 방식이다.
코드를 보며 이해해보자.

// UserService 인터페이스
public interface UserService {
    void addUser(String username);
}

// UserServiceImpl 구현 클래스
public class UserServiceImpl implements UserService {
    @Override
    public void addUser(String username) {
        System.out.println("사용자 추가: " + username);
    }
}

// 생성자 주입을 통한 의존성 주입
public class UserController {
    private UserService userService;

    // 생성자를 통한 의존성 주입(@Autowired 생략)
    public UserController(UserService userService) {
        this.userService = userService;
    }

    public void addUser(String username) {
        userService.addUser(username);
    }
}

생성자 주입은 생성자의 호출 시점에 딱 1번 호출되는것이 보장된다.
그렇기에 주입받은 객체가 변하지 않거나 객체의 주입이 강제되는 경우에 사용할 수 있다.
또한 Spring 에서는 생성자가 1개일 경우에 @Autowired를 생략해도 생성자 주입이 가능하도록 편리성을 제공한다.

3-2. Setter 주입

// 의존성 주입을 받을 UserService 인터페이스
public interface UserService {
    void addUser(String username);
}

// UserServiceImpl 구현 클래스
public class UserServiceImpl implements UserService {
    @Override
    public void addUser(String username) {
        System.out.println("사용자 추가: " + username);
    }
}

// Setter 메서드를 통한 의존성 주입
public class UserController {
    private UserService userService;

    // Setter 메서드를 통한 의존성 주입
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    public void addUser(String username) {
        userService.addUser(username);
    }
}

Setter 주입은 필드 값을 변경하는 Setter를 통해 의존 관계를 주입하는 방법이다.
Setter 주입은 생성자 주입과는 다르게 주입받는 객체가 변경될 가능성이 있는 경우에 사용된다.

3-3. 필드(Field)주입

// 의존성 주입을 받을 UserService 인터페이스
public interface UserService {
    void addUser(String username);
}

// UserServiceImpl 구현 클래스
public class UserServiceImpl implements UserService {
    @Override
    public void addUser(String username) {
        System.out.println("사용자 추가: " + username);
    }
}

// 필드 주입을 통한 의존성 주입
public class UserController {
    @Autowired
    private UserService userService;

    public void addUser(String username) {
        userService.addUser(username);
    }
}

필드 주입은 외부에서 접근이 불가능하다는 단점이 있다.
이는 외부에서 해당 객체를 수정할 수 없기에 지양해야 하는 코드 작성 방식이다. 이는 실제 권장되지 않는 코드 작성 방식이다.

4. 생성자 주입을 사용해야 하는 이유

  1. 객체의 불변성 보장
  2. 테스트 코드 작성
  3. final 키워드 작성 및 Lombok과의 결합
  4. Spring 에 비침투적인 코드 작성
  5. 순호나 참조 에러 방지

우선은 이렇게 적는다.
해당 내용은 다음에 다루도록 한다.
https://mangkyu.tistory.com/125 <- 참조 링크

정리

💡 IoC(제어의 역전) : 개발자가 코드 흐름을 제어하는게 아니라 프레임워크가 코드의 제어 흐름을 관리하는 개념

💡 DIP(Dependency Inversion Principle) : 상위 수준의 모듈은 하위 수준의 모듈에 의존하지 않고 추상화에 의존해야 한다.

💡 DI(Dependency Injection) : 외부에 선언된 객체를 의존성 있는 객체에 주입하여 결합도를 낮추는 방식이며 생성자를 통한 초기화 방식이 권장된다.

profile
하고싶은게 많은 newbie

0개의 댓글