[Spring Boot] IoC(제어의 역전)와 DI(의존주입) 스프링 컨테이너

 

 

우리는 자바란 프로그래밍 언어를 이용해서 프로젝트를 개발 시에 스프링이라는 프레임워크를 이용해서 개발한다. 스프링 프레임워크를 사용하는 이유는 여러가지가 있겠지만 가장 주된 이유로는 자바가 객체지향프로그래밍 언어라는 이유일 것이다.

 

객체지향 프로그래밍은 추상화, 캡슐화, 상속, 다형성을 가지며 이 특징들로 SOLID라는 5가지의 원칙을 만족하는 프로그래밍 방식을 의미한다. 이러한 특징들을 가진 프로그래밍 방식으로 여러개의 독립된 단위로 나누며 각각의 객체들의 역할과 구현방식으로 구분하여 프로그램을 유연하고 변경이 용이 하도록 만들 수 있다. 그러므로 확장이나 재 사용에 이점을 보인다.

 

설계를 할 때, 인터페이스와 구현체를 가지고 다형성을 만족하는 객체 지향 설계를 할 수있다. 하지만 다형성 만으로는 완전한 객체 지향 설계를 했다고 보기는 힘들다.

 

예를 들어

public class Member {

    private Long id;
    private String name;
    private boolean check; // 회원 여부
    private int count = 10000; // 가진 금액

    public Member(Long id, String name, boolean check) {
        this.id = id;
        this.name = name;
        this.check = check;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isCheck() {
        return check;
    }

    public void setCheck(boolean check) {
        this.check = check;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

}

Member라는 도메인과

 

public class MemberMain {

    public static void main(String[] args) {

        MemberService memberService = new MemberServiceImpl();

        Member member = new Member(1L, "memberA", true);

        memberService.enter(member);
    }

}

로직을 수행 할 Main

 

public interface MemberService {

    void enter(Member member);

}
public class MemberServiceImpl implements MemberService {

    private GradeMember gradeMember = new CommonGradeMember();

    @Override
    public void enter(Member member) {
        gradeMember.countInfo(member);
    }
}

회원 여부 별 입장에 관한 로직을 수행할 서비스 인터페이스와 서비스 구현체를 나눴다.

 

package com.example.demo.memberGrade;

import com.example.demo.domain.Member;

public interface GradeMember {

    void countInfo(Member member);

}
package com.example.demo.memberGrade;

import com.example.demo.domain.Member;

public class CommonGradeMember implements GradeMember {

    private final int MEMBER_PRICE = 4000;
    private final int COMMON_PRICE = 10000;

    @Override
    public void countInfo(Member member) {
        System.out.println("------------------------");
        System.out.println(member.getName() + "님, 회원 여부 : " + member.isCheck());

        checkResult(member);

        System.out.println("------------------------");
    }

    private void checkResult(Member member) {
        if (member.isCheck()){
            System.out.println("결제 될 금액은 " + MEMBER_PRICE + "원 입니다");
            System.out.println("남은 금액: " + (member.getCount() - MEMBER_PRICE) + "원");
        }else{
            System.out.println("결제 될 금액은 " + COMMON_PRICE + "원 입니다");
            System.out.println("남은 금액: " + (member.getCount() - COMMON_PRICE) + "원");
        }
    }
}

지불해야 할 금액에 관한 상세 인터페이스와 구현체로 나눴다.

 

 

Member의 정보 별로 입장에 관한 인터페이스와 구현체, Member의 회원 여부에 따른 금액 측정에 관한 인터페이스와 구현체로 나눈 것이다. main 함수를 실행하면

------------------------
memberA님, 회원 여부 : true
결제 될 금액은 4000원 입니다
남은 금액: 6000원
------------------------

의 형태로 출력된다.

 

 

인터페이스(역할)과 구현체(구현)을 분리하여 개발했고 다형성(polymorphism)을 통해 하나의 객체 타입으로 여러가지 타입을 가질 수 있게, 즉 인터페이스를 통해 해당 인터페이스를 구현한 여러가지 구현체를 가질 수 있도록 만들었다.

 

하지만 만약에 여기서 회원정보 별로 결제될 금액이 고정 값이 아니라 할인율을 통해서 할인이 된다고 바뀐다고하면 위에서 말한 SOLID의 5원칙을 지키지 못하게 된다.

 

public class VIPGradeMember implements GradeMember {

    private final int MEMBER_PRICE = 10;
    private final int COMMON_PRICE = 10000;

    @Override
    public void countInfo(Member member) {
        System.out.println("------------------------");
        System.out.println(member.getName() + "님, 회원 여부 : " + member.isCheck());

        checkResult(member);

        System.out.println("------------------------");
    }

    private void checkResult(Member member) {

        if (member.isCheck()){
            int discount = (COMMON_PRICE / MEMBER_PRICE);
            int resultCount = (member.getCount() - discount);

            System.out.println("할인 될 금액은 " + discount + "원 입니다");
            System.out.println("남은 금액: " + resultCount + "원");
        }else{
            System.out.println("결제 될 금액은 " + COMMON_PRICE + "원 입니다");
            System.out.println("남은 금액: " + (member.getCount() - COMMON_PRICE) + "원");
        }

    }
}

할인을 고정 값이 아닌 할인율을 통해 지정하는 구현체이다.

 

 

MemberServiceImpl 구현체가 GradeMember 인터페이스를 의존하면서 CommonGradeMember 구현체까지 의존하고 있다.

 

 

위의 VIPGradeMember 구현체로 변경하게되면 MemberServiceImpl 구현체에서 아래와 같은 작업을 해야한다.

// private GradeMember gradeMember = new CommonGradeMember();
private GradeMember gradeMember = new VIPGradeMember();

 

 

SOLID 5원칙 중 DIP(Dependency inversion principle)의 원칙에 따라 구현체에 의존하지 않으며 인터페이스에 의존해야 하기때문에 설계를 변경해야한다.

 

// private GradeMember gradeMember = new CommonGradeMember();
private final GradeMember gradeMember;

인터페이스에만 의존하도록 설계를 변경하게되어 실제로 구현체가 없어 예외가 발생한다. 그렇기 때문에 다른 곳에서 GradeMember의 구현 객체를 대신 생성하여 주입해야한다.

 

public class MemberServiceImpl implements MemberService {

    /*private GradeMember gradeMember = new CommonGradeMember();*/
    private final GradeMember gradeMember;

    public MemberServiceImpl(GradeMember gradeMember) {
        this.gradeMember = gradeMember;
    }

    @Override
    public void enter(Member member) {
        gradeMember.countInfo(member);
    }
}

MemberServiceImpl에서는 생성자를 통해 주입된 구현체의 인터페이스만 의존하여 처리하도록 변경했기때문에 GradeMember를 의존하는 구현체는 어떠한 것이든 받을 수 있고 구현체가 바뀌어도 따로 구조를 변경하지 않아도 된다.

 

public class MemberConfig {

    public MemberService memberServiceImpl() {

//        return new MemberServiceImpl(new CommonGradeMember());
        return new MemberServiceImpl(new VIPGradeMember());
    }
}
public class MemberMain {

    public static void main(String[] args) {

        MemberConfig memberConfig = new MemberConfig();
        MemberService memberService = memberConfig.memberServiceImpl();
//        MemberService memberService = new MemberServiceImpl();

        Member member = new Member(1L, "memberA", true);

        memberService.enter(member);
    }

}

위의 MemberConfig라는 클래스를 생성했다. 해당 클래스는 앞에서 할인율을 어떻게 적용할지 변경 할 때, 인터페이스와 구현체의 로직 변경은 일어나지 않고 단순히 구성 부분만 변경함으로 할인율을 변경 할 수 있다.

 

MemberConfig를 통해서 전체 동작 방식을 구성할 수 있다. 해당 클래스에서선 구현체를 생성하고 연결하는 역할을 하게 된다. 이로써 각 구현 객체들은 어떤 객체가 들어오게 되던 자신의 로직을 실행하는 역할만을 담당하고 전체 프로그램의 제어의 흐름은 MemberConfig 객체가 하게된다. 이렇게 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라고 한다.

 

 

프로그램의 의존관계를 보게되면 MemberServiceImpl은 GradeMember에 의존하지만 어떤 구현체가 사용 될지는 모른다. 의존 관계는 정적 클래스 의존관계와 동적 객체 인스턴스 의존 관계로 나누어진다.

 

- 정적 클래스 의존관계

  • 클래스가 사용하는 코드만 보고 의존관계를 판단 할 수 있다. MemberServiceImpl은 GradeMember에 의존한다.

- 동적 객체 인스턴스 의존관계

  • 애플리케이션 실행 시점에서 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계이다. CommonGradeMember객체를 구현체로 쓸건지 VIPGradeMember 구현체인지는 실행 시에 결정된다. 런타임 시에 객체를 생성하고 클라이언트와 서버의 실제 구현체와의 의존관계가 연결 되는 것을 의존관계 주입(DI)이라 한다. 

MemberConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC 컨테이너 또는 DI 컨테이너라고 한다. 

 


 

지금까지 작성한 IoC, DI는 순수하게 자바로만 구현한 것이다. 추가되어야 할 부분이 많지만 기본 틀은 이러한 방식을 사용했다. 스프링을 이용하게되면 IoC와 DI 컨테이너와 함께 그에 관련된 수 많은 추가 기능들을 제공한다. 앞에선 개발자가 MemberConfig와 같은 제어하는 객체를 직접 만들고 의존관계를 주입했지만 스프링에선 스프링이 의존관계를 주입해준다. 그것을 스프링 컨테이너라고 하고 여기에 등록된 객체를 스프링 빈이라고 한다.

 

@Configuration
public class MemberConfig {

    @Bean
    public MemberService memberServiceImpl() {

//        return new MemberServiceImpl(new CommonGradeMember());
        return new MemberServiceImpl(new VIPGradeMember());
    }
}
public class MemberMain {

    public static void main(String[] args) {

        ApplicationContext ac = new AnnotationConfigApplicationContext(MemberConfig.class);
        MemberService memberService = ac.getBean("memberServiceImpl", MemberService.class);

//        MemberConfig memberConfig = new MemberConfig();
//        MemberService memberService = memberConfig.memberServiceImpl();

//        MemberService memberService = new MemberServiceImpl();

        Member member = new Member(1L, "memberA", true);

        memberService.enter(member);
    }

}

기존에 DI 컨테이너로 사용하던 MemberConfig와 데이터를 입력하는 MemberMain을 스프링에서 지원하는 어노테이션 방식으로 사용을 변경했다. main함수에서 사용된 ApplicationContext 객체는 스프링 컨테이너이며 스프링컨테이너는 @Configuration이 붙은 MemberConfig를 설정 정보로 사용한다. 그리고 @Bean이 붙은 메서드를 모두 호출해서 반환된 객체로 스프링 컨테이너에 등록한다. 다시 말해서 @Bean이 붙은 객체는 스프링 컨테이너에 등록되어 있으며 그것을 스프링 빈이라고 한다.

 

이처럼 스프링은 제어의역전(IoC)과 의존주입(DI)에 대해 기본적인 설정들을 편리하게 제공하며, 공통적으로 누구나 사용하면서 고려해볼 수 있는 무수히 많은 설정들을 제공한다.

+ Recent posts