관리 메뉴

나구리의 개발공부기록

의존관계 자동주입, 의존관계 주입방법, 옵션처리, 생성자 주입 선택, 롬복과 최신 트랜드 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 핵심원리 - 기본편

의존관계 자동주입, 의존관계 주입방법, 옵션처리, 생성자 주입 선택, 롬복과 최신 트랜드

소소한나구리 2024. 1. 31. 12:00

  출처 : 인프런 - 스프링 핵심원리 - 기본편(유료) / 김영한님  
  유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용  

https://inf.run/kCYMv


1. 다양한 의존관계 주입방법

1) 의존관계 주입 방법

  • 생성자 주입
  • 수정자 주입(setter 주입)
  • 필드 주입
  • 일반 메서드 주입

(1) 생성자 주입

  • 생성자를 통해서 의존 관계를 주입하는 방법(지금까지 계속 진행 해왔던 방법)
  • 생성자 호출 시점에 딱 1번만 호출 되는 것이 보장되며 불변, 필수 의존 관계에 사용
  • 생성자가 딱 1개만 있으면 @Autowired를 생략해도 스프링 빈에 자동 주입되며 생략 하는 방식을 주로 사용함
@Component
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired // 생략해도 의존관계가 자동으로 주입이 됨
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

 

(2) 수정자 주입

  • setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입
  • 선택, 변경 가능성이 있는 의존관계에 사용하며 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법임
@Component
public class OrderServiceImpl implements OrderService {

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}

 

** 참고

  • @Autowired의 기본 동작은 주입할 대상이 없으면 오류가 발생하며 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false)로 지정
  • 생성자 주입과 수정자 주입이 동시에 있다면 생성자 주입이 먼저 실행 됨(객체를 생성할 때 애초에 생성자를 호출하기 때문)
  • 수정자 주입은 실행 순서가 보장이 안되기 때문에 멀티쓰레드 환경에서 순서를 보장 할 수 없음

** 자바빈 프로퍼티 규약

  • 자바에서는 과거부터 필드의 값을 직접 변경하지 않고, setXxx, getXxx라는 메서드를 통해서 값을 읽거나 수정하는 규칙을 만듦
  • 자세한 내용은 자바빈프로퍼티 검색
  • 자바빈 프로퍼티 규약 예시
class Data {
   private int age;
   public void setAge(int age) {
     this.age = age;
   }
   public int getAge() {
     return age;
  } 
}

 

(3) 필드 주입

  • 이름 그대로 필드에 바로 주입하는 방식
  • 코드가 간결하여 과거에 많이 사용 했지만 외부에서 변경이 불가능해서 테스트 하기 힘들다는 치명적인 단점이 있음
  • DI 프레임워크가 없으면 아무것도 할 수 없음
  • 애플리케이션의 실제 코드와 관계없는 테스트 코드, 스프링 설정을 목적으로하는 @Configuration 같이 특별한 곳 말고는 사용 금지!
@Component
public class OrderServiceImpl implements OrderService {

    @Autowired private final MemberRepository memberRepository;
    @Autowired private final DiscountPolicy discountPolicy;
}

 

** 참고

  • 순수한 자바 테스트 코드에는 @Autowired가 동작하지 않으므로 @SpringBootTest처럼 스프링 컨테이너를 테스트에 통합한 경우에만 사용 가능
  • @Bean에서 파라미터에 의존관계는 자동 주입이되어 수동 등록시 자동 등록된 빈의 의존관계가 필요할 때 문제를 해결 할 수 있음
@Bean
OrderService orderService(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    return new OrderServiceImpl(memberRepository, discountPolicy);
}

 

(4) 일반 메서드 주입

  • 일반 메서드를 통해서 주입
  • 한번에 여러 필드를 주입 받을 수 있으나 일반적으로 잘 사용하지 않음
@Component
public class OrderServiceImpl implements OrderService {

    private MemberRepository memberRepository; 
    private DiscountPolicy discountPolicy;

    @Autowired
    public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
}

 

** 참고

  • 당연한 이야기지만 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작함
  • 스프링 빈이 아닌 일반 자바 클래스에서 @Autowired를 적용해도 아무 기능도 작동하지 않음

2. 옵션 처리

1) @Autowired의 옵션

  • 주입할 스프링 빈이 없어도 의존관계 자동 주입이 동작해야 할 때가 있음
  • 그러나 @Autowired만 사용하면 required옵션의 기본값이 true로 되어있어서 자동 주입 대상이 없으면 오류가 발생함

(1) 자동 주입 대상을 옵션으로 처리하는 방법

  • @Autowired(required = false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
  • @Nullable : 자동 주입할 대상이 없으면 null이 입력됨
  • Optional<> : 자동 주입할 대상이 없으면 Optional.empty가 입력됨

** 참고

  • @Nullable, Optional은 스프링 전반에 지원이 되기 때문에 생성자 자동 주입에서 특정 필드에만 적용하는 등으로 사용할 수 있음
  • 해당 테스트에서 주입받는 Member가 스프링 빈이 아니기 때문에 IDE에서 @Autowired가 빨간줄로 표시되지만 테스트 자체는 정상적으로 동작함

(2) 옵션 테스트 및 출력 결과

  • @Autowired가 제대로 동작하지 않는 상황을 테스트
  • 스프링 빈이 아닌 Member클래스를 수정자 의존관계로 주입
  • 테스트를 실행해보면 setNoBean은 애초에 호출 자체가 안되며 @Nullable 옵션을 사용한 테스트는 null이 입력되고 Optional을 사용한 테스트는 Optional.empty가 입력된 것을 확인할 수 있음
package hello.core.autowired;

public class AutowiredTest {

    private static final Logger log = LoggerFactory.getLogger(AutowiredTest.class);

    @Test
    void autowired() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
    }


    static class TestBean {

        // Bean으로 등록되지 않은 Member클래스를 @Autowired로 주입
        @Autowired(required = false)    // 호출 자체가 안됨
        public void setNoBean1(Member noBean1) {
            System.out.println("noBean1 = " + noBean1);
        }

        @Autowired
        public void setNoBean2(@Nullable Member noBean2) {
            System.out.println("TestBean.setNoBean2 = " + noBean2);
        }

        @Autowired
        public void setNoBean3(Optional<Member> noBean3) {
            System.out.println("TestBean.setNoBean3 = " + noBean3);
        }
    }
}

/* 실행 결과
TestBean.setNoBean2 = null
TestBean.setNoBean3 = Optional.empty
*/

3. 생성자 주입을 선택해라

1) 생성자 주입을 권장하는 이유

  • 과거에는 수정자주입과 필드주입을 많이 사용 했으나 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장함

(1) 불변

  • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 변경할 일이 없고 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안됨(불변해야함)
  • 수정자 주입 사용시 setXxx메서드를 public으로 열어두어야 하는데 누군가 실수로 변경할 수도 있고 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아님
  • 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없기에 불변하게 설계 할 수 있음

(2-1) 누락

  • 수정자 주입의 경우 순수한 자바 코드를 단위 테스트 하는 경우에 코드 누락이 발생할 수 있음

(2-2) 수정자 주입

  • 의존관계를 수정자 주입(setter)로 작성
  • @Autowired가 프레임워크 안에서 동작할 때에는 의존관계가 없으면 오류가 발생하지만 이번에는 자바 코드로만 단위 테스트를 수행
@Component
public class OrderServiceImpl implements OrderService {
    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    // 수정자 주입
    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
    // ... 구현된 코드 생략
}

 

(2-3) 순수한 자바코드로 테스트

  • 순수한 자바 코드로만 수정자로 의존관계를 주입 후 OrderServiceImpl의 메서드를 테스트하기위해 코드를 작성할 때 Repository와 할인정책을 입력하지 않아도 컴파일 에러가 발생하지 않음
  • 그러나 실행하면 Null Point Exception이 발생하는데 memberRepository, discountPolicy 모두 의존관계 주입이 누락이 되었기 때문임
  • 만약 애플리케이션에 이렇게 적용이되어 애플리케이션이 실행이 되면 해당 코드에 사용자가 접근 했을 때 오류가 발생하기 때문에 상당히 치명적인 버그가 발생하게 되는 것임
package hello.core.order;

class OrderServiceImplTest {
    @Test
    void createOrder() {
        OrderServiceImpl orderService = new OrderServiceImpl();
        Order order = orderService.createOrder(1L, "itemA", 10000);
    }
}

 

(2-4) 생성자 주입과 final키워드

  • 생성자 주입을 사용하면 위 테스트 코드에서처럼 주입 데이터를 누락했을 때 바로 컴파일 오류가 발생하여 개발 시점에 오류를 바로잡을 수 있으며 IDE에서 바로 어떤 값을 필수로 주입해야 하는지 알 수 있음
  • 또한 생성자 주입을 사용하면 주입대상인 필드에 final 키워드를 사용할 수 있어 생성자에서 값을 설정하는 코드를 빼먹으면 컴파일오류가 발생하여 컴파일 시점에 오류를 막아줌
  • 아래의 예시처럼 final 키워드를 사용하면 생성자의 값을 생성하기위한 필수 필드인 discountPolicy의 값이 누락이 되면 컴파일 시점에 오류 발생함
  • 컴파일 오류는 세상에서 가장 빠르고 좋은 오류임

** 참고

  • 생성자 주입을 제외한 나머지 주입방식은 모두 생성자 이후에 호출 되므로 필드에 final 키워드를 사용할 수 없음
  • 오직 생성자 주입만 final 사용가능함
@Component
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
         this.memberRepository = memberRepository;
      // this.discountPolicy = discountPolicy; 누락시 컴파일 오류
    }
}

 

(2-5) 생성자 주입을 사용하여 테스트 진행

  • OrderServiceImpl에 생성자를 사용하면 순수 자바로 테스트 코드를 작성할 때 필드를 빼먹지 않도록 모두 컴파일 오류가 발생함
  • 올바른 테스트를 수행하기위해, 리포지토리와 할인정책을 생성하여 OrderServiceImpl의 객체를 생성할때 인수로 입력하면 오류 없이 테스트 코드를 작성할 수 있고 동작도 정상적으로 됨
package hello.core.order;

@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    // 생성자를 통해서 각 객체를 주입
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
    // 기존 코드 생략
}

// 정상적인 테스트
package hello.core.order;

class OrderServiceImplTest {

    @Test
    void createOrder() {
        MemberRepository memberRepository = new MemoryMemberRepository();
        memberRepository.save(new Member(1L, "MemberA", Grade.VIP));

        OrderServiceImpl orderService = new OrderServiceImpl(memberRepository, new RateDiscountPolicy());
        Order order = orderService.createOrder(1L, "itemA", 10000);

        assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}

 

(3) 정리

  • 생성자 주입 방식을 선택하는 이유는 여러가지가 있지만 생성자 주입 방식은 프레임워크에 의존하지 않고 순수한 자바 언어의 특징을 잘 살리는 방법임
  • 기본으로 생성자 주입을 사용하고 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하는 것을 권장함 (동시에 사용 가능함)
  • 그리고 필드 주입은 테스트코드외에는 사용하지 않는 것을 권장함

4. 롬복과 최신 트랜드

1) 최적화

  • 개발을 해보면 대부분의 의존관계가 불변이여서 필드에 final을 사용하게 되는데 생성자도 만들어야하고 주입 받은 값을 대입하는 코드도 만들어야 하는 등 번거로운 부분이 많아짐
  • 생성자주입을 필드주입처럼 편하게 사용하도록 최적화를 진행

(1) @Autowired 생략

  • 아래의 코드처럼 생성자가 딱 1개만 있으면 자동 주입하는 @Autowired를 생각할 수 있음
  • 그러나 여전히 생성자가 남아있음
@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

//    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
    // 기존 코드 생략
}

 

(2) 롬복 라이브러리 적용

  • build.gradle에 아래의 환경설정 및 라이브러리 추가 후 gradle을 새로고침(코끼리 모양)
  • 과거에는 설정 -> Annotation Processors 검색 -> Enable annotation processing 체크 후 재시작해야 했지만 인텔리제이가 롬복을 내장 탑재한 이후 해당 설정은 하지 않아도 동작함
// gourp, version 바로 아래에 추가
//lombok 설정 추가 시작
configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}
//lombok 설정 추가 끝


dependencies {

    // dependencies에 lombok 라이브러리 추가 시작
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testCompileOnly 'org.projectlombok:lombok'
	testAnnotationProcessor 'org.projectlombok:lombok'
	//lombok 라이브러리 추가 끝

}

 

(3) 롬복 동작 확인, HelloLombok클래스 생성

  • @Getter, @Setter : 선언된 변수를 가지고 setter, getter를 만들어 줌
  • @ToString : 선언된 변수를 가지고 toString을 전부 만들어 줌
  • 롬복은 실무에서 정말 많이 사용함
@Getter
@Setter
@ToString
public class HelloLombok {

    private String name;
    private int age;

    public static void main(String[] args) {
        HelloLombok helloLombok = new HelloLombok();
        helloLombok.setName("hello");
        helloLombok.setAge(20);

        System.out.println("helloLombok.getName() = " + helloLombok.getName());
        System.out.println("helloLombok.getAge() = " + helloLombok.getAge());
        
        // @ToString 적용
        System.out.println("helloLombok = " + helloLombok);
    }
}

 

(4) 롬복으로 OrderServiceImpl 최적화 적용

  • 롬복의 @RequiredArgsConstructor 애노테이션을 사용 하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어 줌 (코드에는 보이지 않지만 실제 호출이 가능)
  • build.classes에 컴파일된 클래스를 열어보면 기존의 생성자가 그대로 추가 되어있는 것을 확인 할 수 있음
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
	
    // 생성자 주입 전체를 생략할 수 있음
    /*
    @Autowired  
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    } */
    // ... 기존 코드 생략
}

 

(5) 정리

  • 최근에는 생성자를 딱 1개 두고 @Autowired를 생략하는 방법을 주로 사용함
  • 여기에 Lombok 라이브러리의 @RequiredArgsConstructor를 함께 사용하여 기능은 제공하고 코드를 깔끔하게 할 수 있음