[Spring] 의존성 주입(DI)

    반응형

     

    이 글은 코드로 배우는 스프링 웹 프로젝트(남가람북스, 구멍가게 코딩단)을 읽고 공부한 내용을 바탕으로 정리한 글입니다.


     

    의존성 주입은 스프링 프레임워크의 가장 큰 특징이라고 말할 수 있습니다. 

    1. 의존성 주입(DI)

    의존성(Dependency)란, 하나의 객체가 다른 객체 없이 제대로 된 역할을 할 수 없다는 것을 의미합니다. 예시로 Restaurant 객체가 제대로 역할을 하려면 Chef 객체가 꼭 필요한 경우를 들 수 있습니다.

    주입(Injection)은 말 그대로 외부에서 '밀어 넣는 것'을 의미합니다. 즉, 의존성 주입은 어떤 객체가 역할을 하기 위해 필요한 객체를 외부에서 밀어넣는 것으로 생각할 수 있습니다. 그렇다면 왜 외부에서 객체를 주입하는 방식이 필요한 것일까요?

    그 이유는 당연히 '편리하기 때문'입니다. 의존성을 주입받으면 나는 내가 만드는 객체만 신경쓰고, 남이 만들어놓은 객체는 사용할 때 달라고 요구만 하면됩니다. 비유를 하자면, 의존성을 주입받는 것은 프랜차이즈 가맹점이 본사로부터 매일 재료를 납품받는 것에 해당해요. 가맹점주는 재료에는 신경쓸 필요가 없어요! 매일 본사가 내 가게 앞으로 가져다 주니까요. 가맹점주님은 받은 재료로 음식을 만들어내기만하면 됩니다. (프랜차이즈 음식점이 아니라면 매일 아침에 시장에 가서 재료를 직접 사야겠죠!) 이것이 의존성 주입의 핵심이고, 스프링이 현재까지도 가장 많이 쓰이는 프레임워크로 자리잡고 있는 이유입니다.

    2. Setter 주입 예제 (RestaurantChef 주입)

    예시로 설명을 해보도록 하겠습니다. Restuarant 객체를 만들고 Chef 객체를 Restaurant 객체에 주입하는 예제를 만들어보겠습니다.

    우선, Chef 클래스를 만듭니다.

    import lombok.Data;
    import org.springframework.stereotype.Component;
    
    @Component
    @Data
    public class Chef {
    }

     

    • @Component 어노테이션은 해당 클래스를 객체로 만들어서 스프링에서 관리하는 component로 만들어달라는 코드입니다. 일반적으로 오랫동안 살아남아 일을 해야 하는 객체를 빈으로 등록해서 관리합니다.
    • @Data 어노테이션은 Lombok의 setter를 생성하는 기능과 생성자, toString(), equals() 등을 자동으로 생성하도록합니다.
      (인텔리제이에서 Lombok으로 생성된 클래스 정보는 ⌘(cmd) +7로 확인할 수 있습니다.)

    다음으로 Restaurant 클래스를 만들겠습니다.

    import lombok.Data;
    import lombok.Setter;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    @Component
    @Data
    public class Restaurant {
        @Setter(onMethod_ = { @Autowired })
        private Chef chef;
    }
    • @Setter는 setter 메서드를 생성해주는 역할입니다. @SetteronMethod 속성에는 setter 메서드 생성 시 메서드에 추가할 어노테이션을 지정합니다. 위 코드에서는 @Autowired 어노테이션을 지정하였습니다. 
      @Setter 어노테이션을 이용해서 의존성을 주입하는 것을 Setter 주입이라고 합니다. Setter 주입은 set 메서드를 작성하고 @Autowirted 어노테이션을 통해서 스프링으로부터 자신이 필요한 객체를 주입해 주도록 합니다.
      Structure에서 setChef() 메서드가 생성되어 있는 것을 확인할 수 있습니다.
      • 좀 더 간단하게는 @Autowired로 주입할 수도 있습니다.(필드 주입)

     

    다음으로 applicationContext.xml을 수정해보겠습니다.

    <context:component-scan base-package="com.hellomygreenworld.ex00"/>

    applicationContext.xml에 위의 코드가 존재하도록 합니다. component-scan은 패키지들에서 @Component를 발견하면 스프링이 관리하도록합니다.

    이렇게 2개의 클래스와 applicatonContext.xml이 어떻게 동작하는지 시간 순서대로 보겠습니다.

    • 스프링 프레임워크가 시작되면 스프링이 사용하는 메모리 영역(Context)을 만듭니다. 스프링에서는 ApplicaiotnContext라는 이름의 객체가 만들어집니다.
    • 스프링은 자신이 객체를 생성하고 관리해야 하는 객체들에 대한 설정이 필요합니다. 저는 이에 대한 설정을 applilcationContext.xml 파일로 해주었습니다.
    • applicationContext.xml에 설정되어 있는 <context:component-scan> 태그의 내용을 통해서 com.hellomygreenworld.ex00 패키지를 스캔합니다.
    • 해당 패키지에 있는 클래스들 중에서 스프링이 사용하는 @Component라는 어노테이션이 존재하는 클래스의 인스턴스를 생성합니다.
    • Restaurant 객체는 Chef 객체가 필요하다는 어노테이션(@Autowired) 설정이 있으므로, 스프링은 Chef 객체의 레퍼런스를 Restaurant 객체에 주입합니다.

    테스트 코드를 통해 확인해보겠습니다.

    import static org.junit.Assert.assertNotNull;
    
    import com.hellomygreenworld.ex00.sample.Restaurant;
    import lombok.Setter;
    import lombok.extern.log4j.Log4j;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    import lombok.Setter;
    import lombok.extern.log4j.Log4j;
    import org.springframework.test.context.web.WebAppConfiguration;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @WebAppConfiguration
    @ContextConfiguration("file:src/main/webapp/WEB-INF/spring-config/applicationContext.xml")
    @Log4j
    public class SampleTests {
        @Setter(onMethod_ = { @Autowired })
        private Restaurant restaurant;
    
        @Test
        public void testExist() {
            assertNotNull(restaurant);
            log.info(restaurant);
            log.info("------------------------------");
            log.info(restaurant.getChef());
        }
    }
    • @ContextConfiguration 어노테이션은 스프링이 실행되면서 어떤 설정 정보를 읽어 들여야 하는지를 명시합니다.

    Junit test를 돌려보면 위와 같이 레스토랑 객체와 셰프 객체가 모두 만들어져 있음을 확인할 수 있습니다.

    3. 생성자 주입 예제(HotelChef 주입)

    이번에는 생성자 주입으로 객체를 생성해보겠습니다. 

    import lombok.Getter;
    import lombok.ToString;
    import org.springframework.stereotype.Component;
    
    @Component
    @ToString
    @Getter
    public class Hotel {
    
        private Chef chef;
        public Hotel(Chef chef) {
            this.chef = chef;
        }
    
    }

    코드를 보면 앞서 setter 주입과는 달리 생성자를 선언하고 Chef를 주입하도록 작성되어있습니다. @Autowired 어노테이션 없이 처리되고 있습니다.

    최근에는 Spring을 포함한 DI 프레임워크의 대부분이 생성자 주입을 권장하고 있습니다. 그 이유는 다음과 같습니다. (참고: https://mangkyu.tistory.com/125)

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

    Hotel에 대한 테스트 코드도 작성해보겠습니다.

    import static org.junit.Assert.assertNotNull;
    
    import com.hellomygreenworld.ex00.sample.Hotel;
    import lombok.Setter;
    import lombok.extern.log4j.Log4j;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    import org.springframework.test.context.web.WebAppConfiguration;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @WebAppConfiguration
    @ContextConfiguration("file:src/main/webapp/WEB-INF/spring-config/applicationContext.xml")
    @Log4j
    public class HotelTests {
    
        @Setter(onMethod_ = { @Autowired })
        private Hotel hotel;
    
        @Test
        public void testExist() {
            assertNotNull(hotel);
    
            log.info(hotel);
            log.info("-----------------------------");
            log.info(hotel.getChef());
        }
    
    }

    2번 예제와 마찬가지로 Hotel에도 Chef가 잘 주입되어있음을 확인할 수 있습니다.

     

    + 단일 생성자 주입

    생성자의 자동 주입과 Lombok을 결합하면 Hotel 클래스를 다음과 같이 변경할 수도 있습니다.

    import lombok.AllArgsConstructor;
    import lombok.Getter;
    import lombok.ToString;
    import lombok.extern.log4j.Log4j;
    import org.springframework.stereotype.Component;
    
    @Component
    @ToString
    @Getter
    @AllArgsConstructor
    public class Hotel {
    
        private Chef chef;
    
    }

    이렇게 작성하면 Hotel 클래스 안에서 정말 아무것도 안해주고 있습니다.

    + final 필드 자동 주입

    최근에 매우 권장되고 있는 주입 방식으로, @RequiredArgsConstructor 어노테이션을 이용하는 방식이 있습니다. @RequiredArgsConstructor@NotNull이나 final이 붙은 인스턴스 변수에 대한 생성자를 만들어줍니다.

    import lombok.Getter;
    import lombok.RequiredArgsConstructor;
    import lombok.ToString;
    import lombok.extern.log4j.Log4j;
    import org.springframework.stereotype.Component;
    
    @Component
    @ToString
    @Getter
    @RequiredArgsConstructor
    public class Hotel {
    
        private final Chef chef;
    
    }
    반응형

    'Java > Spring' 카테고리의 다른 글

    [Spring MVC] Controller  (0) 2023.01.03
    [Spring MVC] 기본 구조  (0) 2023.01.02
    [Spring] MyBatis와 스프링 연동 (+log4jdbc)  (0) 2023.01.02
    [Spring] JDBC(MySQL) 연결  (0) 2023.01.02
    [Spring MVC] IntelliJ에서 Project 생성하기  (0) 2022.12.29

    댓글