@Autowired를 필드에 바로 사용하는 것은 좋지않다.

이규훈·2023년 5월 27일
0

스프링 정리

목록 보기
27/30

틀린 코드


위의 그림처럼 사용하면 안됩니다.


@Autowired란?

@Autowired는 스프링 프레임워크에서 제공하는 어노테이션 중 하나로, 의존성 주입(Dependency Injection)을 자동으로 처리해주는 역할을 합니다. 이 어노테이션은 주로 필드, 생성자, 메소드에 사용되며, 사용된 위치에 따라 스프링이 해당 객체에 적절한 의존성을 주입해줍니다.

스프링이 @Autowired 어노테이션을 보면, 해당 타입의 빈(bean)을 스프링 애플리케이션 컨텍스트에서 찾아서 자동으로 주입합니다. 여기서 빈(bean)이란, 스프링이 직접 제어권을 가지고 생성과 생명주기를 관리하는 객체를 의미합니다.

  1. 필드 인젝션: @Autowired 어노테이션이 필드에 적용되면, 스프링은 해당 필드 타입과 일치하는 빈을 찾아서 주입합니다. 이 방식은 간결하긴 하지만, 몇 가지 단점이 있습니다. 오늘 글의 주제입니다.
@Autowired
private SomeService someService;
  1. 생성자 인젝션: @Autowired 어노테이션을 생성자에 사용하면, 스프링은 생성자의 매개변수 타입과 일치하는 빈들을 찾아서 주입합니다. 이 방식은 필드 인젝션의 단점을 해결하면서, 필수 의존성을 명확히 보여줍니다.
private final SomeService someService;

@Autowired
public SomeClass(SomeService someService) {
    this.someService = someService;
}
  1. 세터 인젝션: @Autowired 어노테이션을 세터 메소드에 사용하면, 스프링은 해당 세터 메소드의 매개변수 타입과 일치하는 빈을 찾아서 주입합니다. 이 방식은 선택적 의존성을 처리하는데 유용합니다.
private SomeService someService;

@Autowired
public void setSomeService(SomeService someService) {
    this.someService = someService;
}

세터 메소드(setter method)란?

세터 메소드(setter method)는 객체지향 프로그래밍에서 사용되는 개념 중 하나로, 객체의 특정 속성(일반적으로 private 속성)의 값을 설정하거나 변경하는 데 사용됩니다.
Java에서는 일반적으로 "set"이라는 접두사와 함께 속성 이름을 사용하여 세터 메소드를 정의합니다. 예를 들어, name이라는 속성을 가진 클래스가 있다면, 해당 속성의 값을 설정하는 세터 메소드는 setName이 될 수 있습니다.
세터 메소드는 일반적으로 void 타입을 반환하며, 설정하려는 값에 해당하는 타입의 매개변수를 하나 가집니다.

스프링 4.3 버전부터는 클래스가 하나의 생성자만 가지고 있으면, @Autowired 어노테이션을 생략할 수 있습니다. 이는 생성자 인젝션을 좀 더 간결하게 만들어줍니다.


@Autowired를 필드에 바로 사용하는 것은 좋지않다.

@Autowired를 필드에 바로 사용하는 것, 즉 필드 인젝션은 몇 가지 문제점을 가지고 있어서 권장되지 않는 방법입니다.
이러한 방식의 주된 문제점은 스프링 컨텍스트 없이는 해당 필드에 값을 주입할 수 없다는 것입니다. 즉, 스프링 컨텍스트를 로딩해야만 인스턴스가 주입됩니다.


스프링 컨텍스트?

스프링 컨텍스트(Spring Context), 또는 애플리케이션 컨텍스트(Application Context)는 스프링 애플리케이션의 런타임 환경을 의미합니다. 이는 스프링 프레임워크의 핵심 부분으로, 애플리케이션에서 필요로 하는 모든 객체(Bean)들의 생성과 관리를 담당하며, 그들 간의 의존성을 처리합니다.

스프링 컨텍스트는 다음과 같은 기능을 제공합니다:

  1. Bean Factory: 스프링 컨텍스트는 빈 팩토리의 기능을 가지며, 애플리케이션의 빈들을 생성, 구성, 제공하는 역할을 합니다. 이를 통해 개발자는 직접 객체를 생성하고 관리하는 복잡함 없이 필요한 빈을 얻을 수 있습니다.

  2. Dependency Injection (DI): 스프링 컨텍스트는 빈들 간의 의존성을 자동으로 처리하는 DI 컨테이너의 역할을 합니다. 개발자는 구성 파일이나 애너테이션을 통해 빈 간의 의존성을 명시하면, 스프링 컨텍스트가 이를 해석하여 적절한 빈을 주입해줍니다.

  3. Integration with other Spring modules: 스프링 컨텍스트는 다른 스프링 모듈들과의 통합을 지원합니다. 예를 들어, 스프링 AOP, 스프링 Security 등의 모듈이 스프링 컨텍스트와 함께 작동하여 애플리케이션의 기능을 확장합니다.


빈(Bean)?

스프링 프레임워크에서 "빈(Bean)"은 애플리케이션의 핵심 기능을 수행하는 객체를 의미합니다. 스프링 빈은 스프링 IoC(Inversion of Control) 컨테이너에 의해 인스턴스화, 구성 및 관리되는 객체입니다.

스프링 빈은 일반적으로 애플리케이션의 비즈니스 로직을 담당하는 서비스 클래스, 데이터 액세스를 처리하는 DAO 클래스, 또는 표현 계층을 구성하는 컨트롤러 클래스와 같은 애플리케이션의 핵심 부분을 구성합니다.

빈의 설정 및 생명 주기는 스프링 컨테이너에 의해 관리되며, 스프링 빈 사이의 의존성은 스프링의 의존성 주입(Dependency Injection) 메커니즘을 통해 처리됩니다. 이는 개발자가 직접 객체를 생성하고, 의존성을 관리하고, 생명 주기를 제어하는 등의 작업을 수행하지 않아도 되게 합니다.

스프링 빈은 보통 XML 파일, Java 설정 클래스, 또는 애너테이션을 사용하여 구성되며, 이를 통해 스프링 컨테이너는 빈의 인스턴스를 생성하고 의존성을 주입할 수 있습니다.


추후에 위의 내용을 정리하여 업로드 하겠습니다.
다시 본론으로 돌아갑니다.


  1. 테스트하기 어렵다

    필드 인젝션을 사용하면, 해당 필드를 갖는 클래스의 인스턴스를 생성할 때 스프링 프레임워크가 필요합니다. 즉, 스프링 컨텍스트를 로딩해야 해당 필드에 의존성이 주입됩니다. 이로 인해 단위 테스트를 작성하는 것이 어려워집니다.

단위 테스트에서는 일반적으로 테스트 대상 클래스의 인스턴스만을 생성하고, 의존성은 모의 객체(mock object)로 대체하여 테스트를 진행합니다. 필드 인젝션을 사용하는 경우, 아래와 같이 직접 필드에 모의 객체를 주입할 방법이 없습니다:

public class SomeServiceTest {

    @Mock
    private AnotherService anotherServiceMock;

    private SomeService someService; // 어떻게 anotherServiceMock을 주입할까?

    //...
}

반면에, 생성자 인젝션을 사용하면 스프링 컨텍스트 없이도 의존성을 주입할 수 있습니다. 생성자 인젝션은 의존성을 주입하는 생성자를 제공하므로, 테스트 코드에서는 이 생성자를 사용하여 모의 객체를 주입할 수 있습니다:

public class SomeService {

    private final AnotherService anotherService;

    public SomeService(AnotherService anotherService) {
        this.anotherService = anotherService;
    }

    //...
}

위와 같이 생성자를 통해 의존성을 주입받으면, 테스트 코드에서는 다음과 같이 쉽게 모의 객체를 주입할 수 있습니다:

public class SomeServiceTest {

    @Mock
    private AnotherService anotherServiceMock;

    private SomeService someService;

    @BeforeEach
    public void setUp() {
        someService = new SomeService(anotherServiceMock); // 모의 객체를 직접 주입
    }

    //...
}

이러한 방식은 스프링 컨텍스트를 로딩하지 않아도 되므로, 테스트의 복잡성을 줄이고 실행 시간을 단축시킵니다.

왜 직접 필드에 모의 객체 주입할 방법이 없는거지?

public class SomeServiceTest {

    @Mock
    private AnotherService anotherServiceMock;

    private SomeService someService; // 어떻게 anotherServiceMock을 주입할까?

    //...
}

위의 코드에서 AnotherService는 SomeService 내부의 private 필드로 선언되어 있습니다. Java의 접근 제한자(access modifier)에 따르면, private 필드는 해당 클래스 외부에서 접근할 수 없습니다. 즉, SomeService의 인스턴스를 생성한 후 외부에서 AnotherService를 직접 주입하는 것은 불가능합니다.

그러나 Mockito와 같은 mocking 라이브러리를 사용하면, 테스트 클래스 내에서 private 필드에 접근하여 모의 객체를 주입하는 것이 가능합니다. 이는 리플렉션(reflection) 기능을 사용하여 내부적으로 수행되며, 아래와 같이 @InjectMocks 애너테이션을 사용하여 적용할 수 있습니다:

public class SomeServiceTest {

    @Mock
    private AnotherService anotherServiceMock;

    @InjectMocks
    private SomeService someService;

    //...
}

위의 코드에서 @InjectMocks 애너테이션은 Mockito에게 SomeService 인스턴스를 생성하고, 그 안에 있는 AnotherService 필드에 anotherServiceMock 모의 객체를 주입하도록 지시합니다.

그러나 이 방식은 리플렉션을 사용하므로 비효율적이고, 코드 가독성을 저하시키며, 암묵적인 의존성 주입을 수행하므로 코드의 명확성을 떨어뜨릴 수 있습니다. 따라서 생성자를 통한 명시적인 의존성 주입(constructor injection)이 권장됩니다.

리플렉션?
리플렉션(Reflection)은 자바에서 클래스나 메서드, 필드, 생성자 등에 대한 정보를 알아내거나 조작하는 기능을 말합니다. 런타임 시점에 동적으로 특정 클래스의 정보를 분석하거나, 인스턴스를 생성하고, 메소드를 호출하는 등의 작업을 수행할 수 있습니다.

SomeService의 인스턴스를 생성한 후 외부에서 AnotherService를 직접 주입하는 것은 불가능합니다?

이 문장의 의미를 이해하려면 "의존성 주입(Dependency Injection)"에 대한 개념을 먼저 이해해야 합니다. 의존성 주입은 객체가 필요로 하는 다른 객체(즉, 의존성)를 외부에서 제공(주입)하는 디자인 패턴입니다. 이는 각 클래스가 다른 클래스에 대해 가능한 한 적은 지식만 가지고 있게 하여, 코드의 결합도를 낮추고 유지보수성을 높이는 데 도움이 됩니다.

"외부에서 직접 주입하는 것이 불가능하다"라는 문장의 의미는, 일반적으로 필드는 클래스의 외부에서 직접 접근하거나 수정할 수 없기 때문입니다. 이는 캡슐화라는 객체지향 프로그래밍의 원칙에 따른 것입니다. 필드는 보통 private 접근 제어자를 가지므로, 해당 클래스의 메서드를 통해서만 접근하거나 변경할 수 있습니다.

따라서 SomeService 클래스의 인스턴스를 생성한 후에, 그 인스턴스의 필드인 AnotherService를 외부에서 직접 접근해서 바꾸는 것은 불가능합니다. 대신, SomeService 클래스가 제공하는 메서드를 통해 AnotherService를 주입받아야 합니다. 이러한 메서드를 통해 의존성을 주입받는 방식을 "세터 주입"이라고 부릅니다.

보통 setter메서드로 의존성 주입받는것이 이에 해당합니다.

스프링 프레임워크에서는 @Autowired 애너테이션을 이용해 이러한 의존성 주입을 자동으로 처리해줍니다. 스프링이 관리하는 빈(Bean) 중에서 타입이 일치하는 것을 찾아서 자동으로 주입해주는 것입니다.


  1. 순환 의존성

    순환 의존성은 클래스 A가 클래스 B에 의존하고, 동시에 클래스 B가 클래스 A에 의존하는 상황을 가리킵니다.
    예시코드

    class A {
      private B b;
    
      public A(B b) {
          this.b = b;
      }
    
      public void doSomething() {
          // ...    
          b.doSomethingElse();
          // ...
      }
    }
    
    	class B {
      private A a;
    
      public B(A a) {
          this.a = a;
      }
    
      public void doSomethingElse() {
         
          a.doSomething();
          
      }
    }
    ...

예시에서 A는 B에 의존하고, B는 A에 의존합니다. 이런 경우, 두 클래스가 서로에게 의존하므로 순환 의존성이 발생합니다.

이런 상황은 설계상 문제가 있을 수 있으며, 종종 코드의 복잡성을 증가시키고 유지 관리를 어렵게 만듭니다. 일반적으로 순환 의존성은 피하는 것이 좋으며, 이를 해결하기 위한 방법 중 하나는 인터페이스나 추상 클래스를 사용하여 의존성을 분리하는 것입니다.

필드 인젝션을 사용하는 경우, 클래스가 인스턴스화 될 때 필드는 null 상태입니다. 스프링은 인스턴스 생성 후 필드에 의존성을 주입하는데, 이 때 순환 의존성이 발생하면 어느 한 쪽의 의존성이 null 상태로 남게 됩니다.

반면에 생성자 인젝션을 사용하면, 클래스 인스턴스가 생성될 때 생성자에 필요한 모든 의존성이 주입됩니다. 이 경우 순환 의존성이 있는 클래스를 인스턴스화 할 수 없으므로, 이러한 문제를 미리 감지하고 수정할 수 있습니다.

  1. 불변성

    필드 인젝션을 사용하면 필드가 final로 선언되지 않습니다. 즉, 필드는 불변이 아니며, 객체가 생성된 후에도 변경될 수 있습니다. 이는 객체의 안정성을 해칠 수 있습니다.

    따라서, 대신에 생성자 인젝션(constructor injection)을 사용하는 것이 좋습니다. 생성자 인젝션을 사용하면 위에서 언급한 문제점들을 대부분 해결할 수 있습니다. 또한 생성자 인젝션은 필수적인 의존성을 명확하게 나타내어 코드의 가독성을 높입니다.

다음은 생성자 인젝션의 예시입니다:

@Service
public class BecarefulService {
    private final SomeDependency someDependency;

    public BecarefulService(SomeDependency someDependency) {
        this.someDependency = someDependency;
    }

    // ...
}

이 코드에서 BecarefulService는 SomeDependency를 필수적으로 요구하는 것을 명확하게 보여주며, 필드는 final로 선언되어 불변성이 보장됩니다. 또한, 순환 의존성 문제를 방지하며, 테스트를 작성하기 더 쉽습니다.

Lombok 라이브러리를 사용한다면, @RequiredArgsConstructor 어노테이션을 사용하여 생성자를 자동으로 만들 수 있습니다.

@Service
@RequiredArgsConstructor
public class BecarefulService {
    private final SomeDependency someDependency;

    // ...
}

옳게된 코드

예시 1)

예시 2)


추가설명

안정성

final 키워드를 사용하여 필드를 선언하면, 해당 필드는 한 번 초기화된 후에는 변경할 수 없습니다. 이를 통해 해당 필드가 예상치 못한 방식으로 변경되는 것을 방지하고, 해당 객체의 상태를 안정적으로 유지할 수 있습니다. 이러한 불변성은 특히 멀티 스레드 환경에서 중요하며, 객체의 상태를 예측 가능하게 만들어 버그를 줄이는 데 도움이 됩니다.

필드 인젝션을 사용하면 필드가 final로 선언될 수 없습니다. 따라서 필드 값이 예상치 못한 시점에 변경될 수 있고, 이는 코드의 안정성을 해칠 수 있습니다. 반면에 생성자 인젝션을 사용하면 필드를 final로 선언할 수 있으므로, 이러한 문제를 방지할 수 있습니다.

필드 인젝션을 사용하면 필드가 final로 선언되지 않습니다. --> 왜?

필드 인젝션(field injection)을 사용하게 되면, 스프링은 클래스의 인스턴스를 먼저 생성하고 나서 그 후에 필드에 해당하는 빈을 주입하게 됩니다. 이렇게 되면, 필드가 초기에 null 상태에서 시작하여, 후에 스프링이 관리하는 라이프사이클 동안에 값이 주입되게 됩니다.

그런데, final 필드는 선언 시점 혹은 생성자에서 반드시 초기화되어야 합니다. 한 번 초기화된 final 필드의 값은 변경할 수 없습니다. 즉, final 필드는 객체 생성과 동시에 반드시 값을 가지고 있어야 하므로, 필드 인젝션 방식과는 상반됩니다.

필드 인젝션을 사용할 경우, final을 사용하면 컴파일 오류가 발생합니다. 그 이유는 스프링이 빈을 주입하는 시점이 생성자 호출 이후이기 때문입니다. 이러한 이유로 필드 인젝션과 final 필드는 함께 사용할 수 없습니다.

반면에 생성자 인젝션을 사용하면 final 필드를 사용할 수 있습니다. 생성자 인젝션은 생성자의 인자를 통해 의존성을 주입받습니다. 이 경우 생성자에서 final 필드를 초기화할 수 있으므로 final 키워드를 사용하는 것이 가능합니다. 이는 필드가 한 번 설정된 후에는 변경되지 않음을 보장하여 코드의 안정성을 높이는 데 도움이 됩니다.

이 부분에 대해서 자세하게 설명하겠습니다.

먼저 필드 인젝션의 경우를 보겠습니다. 필드 인젝션을 사용하면, final 키워드를 사용할 수 없습니다.

import org.springframework.beans.factory.annotation.Autowired;

public class FieldInjectionExample {
    @Autowired
    private SomeService someService;
    // 컴파일 오류: final 필드 someService는 초기화되지 않았습니다.
    // private final SomeService someService;
}

위의 예에서 final 필드 someService는 초기화되지 않았기 때문에 컴파일 오류가 발생합니다. 스프링은 FieldInjectionExample 객체를 먼저 생성하고, 그 이후에 someService 필드에 SomeService 빈을 주입합니다. 그러나 final 필드는 선언 시점이나 생성자에서 반드시 초기화되어야 합니다.

이제 생성자 인젝션의 경우를 보겠습니다. 생성자 인젝션을 사용하면, final 필드를 사용할 수 있습니다.

import org.springframework.beans.factory.annotation.Autowired;

public class ConstructorInjectionExample {
    private final SomeService someService;

    @Autowired
    public ConstructorInjectionExample(SomeService someService) {
        this.someService = someService;
    }
}

위의 예에서는 SomeService 빈이 생성자 인자를 통해 someService 필드에 주입됩니다. 이 필드는 final이기 때문에 한 번 초기화된 후에는 변경될 수 없습니다. 생성자 인젝션을 사용하면, 객체의 생성 시점에 모든 의존성이 주입되기 때문에 필드를 final로 선언할 수 있습니다. 이는 불변성을 보장하고 코드의 안정성을 향상시킵니다.

의존성 주입(DI)란?

의존성 주입(Dependency Injection, DI)은 객체지향 프로그래밍에서 의존성 문제를 해결하는 디자인 패턴 중 하나입니다. DI는 클래스 내부에서 새로운 객체를 생성하지 않고, 외부에서 생성된 객체를 주입받아 사용하는 방식을 의미합니다.

객체지향 프로그래밍에서 클래스 간의 관계는 필수적인 요소입니다. 한 클래스가 다른 클래스의 메소드를 사용하면, 그 클래스는 다른 클래스에 의존하게 됩니다. 이렇게 되면 클래스 간의 결합도(coupling)가 높아져서 코드의 유연성과 재사용성이 떨어지게 됩니다. 코드의 유연성을 높이기 위해 의존성 주입이 사용됩니다.

예를 들어, 어떤 클래스 A가 다른 클래스 B의 메소드를 사용하려면, A는 B의 인스턴스를 가지고 있어야 합니다. 이를 "클래스 A가 클래스 B에 의존한다"고 합니다. 이 의존성을 직접 관리하는 대신, A는 자신이 사용할 B의 인스턴스를 외부로부터 주입받습니다. 이것이 바로 의존성 주입입니다.

DI의 주요 장점은 다음과 같습니다:

  1. 모듈성: 각 클래스는 자신의 역할에 집중할 수 있습니다. 의존성 관리는 외부에 위임되므로, 코드가 간결하고 이해하기 쉬워집니다.

  2. 테스트 용이성: 의존성을 주입하면, 테스트 중에 실제 의존성을 가짜(mock) 객체로 쉽게 교체할 수 있습니다. 이는 단위 테스트를 보다 쉽게 작성하는 데 도움이 됩니다.

  3. 유연성: 클래스는 구체적인 의존성이 아닌 인터페이스에 의존하게 됩니다. 따라서 실행 시점에 의존성을 변경하거나, 다양한 구현을 쉽게 교체할 수 있습니다.

이런 장점들 때문에, 의존성 주입은 스프링 프레임워크 등 많은 프레임워크에서 채택하고 있는 중요한 디자인 패턴입니다.

DI 예시

의존성 주입(Dependency Injection, DI)의 기본적인 예시를 Java 언어를 통해 보여드리겠습니다.

먼저, 의존성 주입을 사용하지 않는 경우를 보겠습니다. 아래의 코드에서 TextEditor 클래스는 SpellChecker 클래스에 의존하고 있습니다. TextEditor는 직접 SpellChecker의 객체를 생성하고 있습니다.

class SpellChecker {
    public SpellChecker() {
        //...
    }
    public void checkSpelling() {
        //...
    }
}

class TextEditor {
    private SpellChecker spellChecker;

    public TextEditor() {
        this.spellChecker = new SpellChecker();
    }
}

하지만, 위의 코드는 의존성이 고정되어 있어, 유연성이 떨어집니다. SpellChecker의 다른 버전이나 모의 객체(mock object)를 사용하기 어렵습니다.

유연성이 떨어진다?

코드의 "유연성이 떨어진다"는 의미는 클래스나 메소드가 특정 구현에 강하게 결합되어 있어서, 다른 구현이나 환경에 쉽게 대응하지 못하는 상태를 가리킵니다.

Mock Object?

모의 객체(mock object)는 주로 단위 테스트에서 사용되며, 테스트를 위해 다른 객체를 흉내내는 객체입니다. 모의 객체는 실제 객체를 대체하여 테스트하는 데 사용되며, 특정 메소드가 호출되었는지 확인하거나, 메소드가 호출될 때 어떤 인자를 받았는지 등을 검증할 수 있습니다.

이제 의존성 주입을 사용한 경우를 보겠습니다. 아래의 코드에서는 TextEditor 클래스가 직접 SpellChecker의 객체를 생성하는 대신, 생성자를 통해 SpellChecker의 객체를 주입받습니다.

class SpellChecker {
    public SpellChecker() {
        //...
    }
    public void checkSpelling() {
        //...
    }
}

class TextEditor {
    private SpellChecker spellChecker;

    public TextEditor(SpellChecker spellChecker) {
        this.spellChecker = spellChecker;
    }
}

위의 코드에서 TextEditor는 SpellChecker의 구체적인 구현에 의존하지 않고, SpellChecker 객체를 주입받아 사용합니다. 이로 인해 TextEditor 클래스의 테스트가 용이해지고, SpellChecker의 다른 구현체를 사용하기 쉬워집니다. 이것이 의존성 주입이 주는 이점입니다.

profile
개발취준생

1개의 댓글

comment-user-thumbnail
2023년 5월 28일

멋지네요. !!!

답글 달기

관련 채용 정보