Spring Bean 과 생성자 주입 (Feat. 순환의존성)

Seong Hyeon Kim·2024년 7월 24일
0

스프링

목록 보기
4/9

1. 빈(Bean)이란 무엇인가?

빈(Bean)은 Spring 프레임워크에서 관리하는 객체를 의미합니다. Spring은 애플리케이션에서 필요한 객체들을 생성하고, 그 객체들 간의 의존성을 관리합니다. 이 객체들을 빈(Bean)이라고 부릅니다.

  • 빈 컨테이너: Spring 컨테이너는 애플리케이션에서 필요한 모든 빈을 생성하고 관리합니다.
  • 빈 등록: @Component, @Service, @Repository, @Controller 등의 어노테이션을 사용하여 클래스가 빈으로 등록될 수 있습니다.

비유: 빈은 마치 주방에서 사용하는 요리 도구(예: 냄비, 프라이팬, 주걱 등)라고 생각하면 돼요.

설명: 주방에서 요리를 할 때 필요한 도구들을 싱크대나 서랍에서 꺼내 사용하는 것처럼, Spring 애플리케이션에서 필요한 객체들을 빈(Bean) 컨테이너에서 꺼내 사용하는 거예요. Spring은 요리 도구를 미리 준비해 놓는 주방이라고 생각하면 돼요.

빈에서의 의미: Spring 컨테이너가 관리하는 객체들을 빈이라고 합니다.




2. @Autowired란 무엇인가?

@Autowired는 Spring에서 의존성 주입(Dependency Injection)을 하기 위해 사용되는 어노테이션입니다. 의존성 주입이란, 클래스가 필요로 하는 의존 객체를 외부에서 제공(주입)하는 것을 말합니다.

@Component
public class Car {
    @Autowired
    private Engine engine;
}
  • 위 코드에서 Car 클래스는 Engine 객체가 필요합니다. @Autowired 어노테이션을 사용하면 Spring이 자동으로 Engine 객체를 생성하여 Car 클래스에 주입해줍니다.

비유: @Autowired는 마치 주방 로봇이 필요한 요리 도구를 자동으로 찾아서 건네주는 기능이라고 생각하면 돼요.

설명: 우리가 요리할 때 "주방 로봇, 냄비 좀 줘"라고 말하면, 주방 로봇이 알아서 냄비를 찾아서 건네주는 것처럼, @Autowired는 Spring이 필요한 빈을 자동으로 찾아서 주입(건네주는 것)해주는 기능이에요.

빈에서의 의미: @Autowired 어노테이션은 Spring이 자동으로 객체를 주입해주는 것을 의미합니다.




3. private final이란 무엇인가?

private final은 Java에서 필드의 값을 한 번만 설정할 수 있도록 하는 접근 제어자입니다. final로 선언된 필드는 한 번 값이 설정되면 변경할 수 없습니다.

  • private: 해당 필드는 클래스 내부에서만 접근할 수 있습니다.
  • final: 해당 필드는 한 번 초기화된 후에 값을 변경할 수 없습니다.
public class Car {
    private final Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }
}

위 코드에서 Engine 객체는 생성자를 통해 초기화되고, 이후에는 변경할 수 없습니다. 이는 클래스의 불변성을 유지하는 데 도움이 됩니다.

비유: private final은 마치 비밀 금고에 넣어두고 절대 바꿀 수 없는 보물과 같아요.

설명: 비밀 금고에 넣은 보물은 한 번 넣으면 누구도 바꿀 수 없잖아요? private final도 마찬가지로, 한 번 값을 설정하면 변경할 수 없도록 하는 거예요. "private"은 금고에 접근할 수 있는 사람을 제한하는 것이고, "final"은 금고 안의 보물을 바꾸지 못하게 하는 거예요.

빈에서의 의미: private final은 한 번 값이 설정되면 변경할 수 없는 필드를 의미합니다.




4. 생성자 주입이란 무엇인가?

생성자 주입은 의존성 주입의 한 방법으로, 객체의 생성자를 통해 의존성을 주입하는 방식입니다.

@Component
public class Car {
    private final Engine engine;

    @Autowired
    public Car(Engine engine) {
        this.engine = engine;
    }
}

위 코드에서 Car 클래스는 생성자를 통해 Engine 객체를 주입받습니다. 생성자 주입의 장점은 의존성이 필수임을 보장하고, 객체가 생성될 때 의존성이 주입되어 객체의 상태가 일관성을 유지할 수 있다는 점입니다.

비유: 생성자 주입은 마치 새로운 요리 도구 세트를 살 때, 미리 도구들을 세트로 준비하는 것과 같아요.

설명: 새로운 요리 세트를 살 때 프라이팬, 냄비, 주걱이 모두 포함된 세트를 사면 편리하잖아요? 생성자 주입도 비슷해요. 객체를 만들 때 필요한 다른 객체들을 미리 준비해서 같이 만드는 거예요.

빈에서의 의미: 생성자 주입은 객체를 생성할 때 필요한 다른 객체들을 생성자에서 주입받는 것을 의미합니다.

이 코드에서 사용된 각 요소와 그 의미를 설명해드릴게요.

1. @Component

비유: @Component는 마치 "이 클래스는 주방 도구 중 하나예요"라고 알려주는 라벨이에요.

설명: @Component 어노테이션은 Spring에게 이 클래스가 빈(Bean)으로 관리되어야 하는 대상임을 알려줍니다. Spring은 이 어노테이션이 붙은 클래스를 스캔하여 빈으로 등록합니다.

의미: @Component는 해당 클래스가 Spring 컨테이너에 의해 관리되는 빈임을 나타냅니다.

2. public class Car

비유: public class Car는 "이것은 Car라는 이름의 요리 도구예요"라고 말하는 것과 같아요.

설명: public class CarCar라는 이름의 클래스를 정의합니다. 이 클래스는 객체를 생성할 수 있는 청사진(템플릿) 역할을 합니다.

의미: public class Car는 Car라는 클래스를 정의합니다.

3. private final Engine engine;

비유: private final Engine engine는 "이 Car는 항상 같은 엔진을 사용해요"라고 말하는 것과 같아요.

설명: private 접근 제어자는 이 필드가 클래스 내부에서만 접근 가능함을 의미합니다. final 키워드는 이 필드가 한 번 초기화된 후에 변경할 수 없음을 의미합니다. 즉, 이 Car 객체가 생성될 때 엔진이 설정되면, 이후에는 그 엔진을 변경할 수 없습니다.

의미: private final Engine engine는 Car 클래스 내에서만 접근 가능한 변경할 수 없는 필드를 정의합니다.

4. @Autowired

비유: @Autowired는 "Spring 로봇이 자동으로 필요한 엔진을 이 Car에 제공해줘요"라고 말하는 것과 같아요.

설명: @Autowired 어노테이션은 Spring에게 해당 필드나 생성자 또는 메서드에 의존성을 자동으로 주입하라고 지시합니다. Spring은 빈 컨테이너에서 해당 타입의 빈을 찾아서 주입합니다.

의미: @Autowired는 Spring이 자동으로 의존성을 주입하도록 지시합니다.

5. public Car(Engine engine)

비유: public Car(Engine engine)는 "이 Car는 엔진을 받아서 만들어져요"라고 말하는 것과 같아요.

설명: 생성자는 객체가 생성될 때 호출되는 특별한 메서드입니다. 이 생성자는 Engine 타입의 파라미터를 받아서 Car 객체를 초기화합니다.

의미: public Car(Engine engine)는 Engine 객체를 파라미터로 받아 Car 객체를 초기화하는 생성자를 정의합니다.

6. this.engine = engine;

비유: this.engine = engine는 "이 Car는 받아온 엔진을 자신의 엔진으로 설정해요"라고 말하는 것과 같아요.

설명: this.engine은 현재 객체의 엔진 필드를 가리킵니다. engine은 생성자 파라미터로 전달된 엔진 객체입니다. this.engine = engine;은 전달된 엔진 객체를 현재 객체의 엔진 필드에 할당합니다.

의미: this.engine = engine;는 생성자 파라미터로 받은 엔진 객체를 현재 객체의 엔진 필드에 할당합니다.

전체적인 의미

이 코드는 Car 클래스가 Spring 컨테이너에 의해 관리되는 빈임을 나타내고, 생성자를 통해 Engine 객체를 주입받아 초기화함을 의미합니다. @Autowired 어노테이션을 사용하여 Spring이 자동으로 Engine 객체를 찾아 Car 객체에 주입합니다. 한 번 주입된 Engine 객체는 final 키워드로 인해 변경할 수 없습니다.




5. 빈 순환 의존성이란 무엇인가?

  • 순환 의존성(Circular Dependency)은 두 개 이상의 빈이 서로를 참조하는 상황을 말합니다.

  • 예를 들어, ClassAClassB를 참조하고, ClassB가 다시 ClassA를 참조하면 순환 의존성이 발생합니다.

  • 이는 Spring 컨테이너가 빈을 생성하는 과정에서 무한 루프에 빠져서 애플리케이션이 제대로 동작하지 않게 만듭니다.

@Component
public class ClassA {
    @Autowired
    private ClassB classB;
}

@Component
public class ClassB {
    @Autowired
    private ClassA classA;
}

비유: 빈 순환 의존성은 마치 두 친구가 서로의 집 열쇠를 가지고 있어야만 집에 들어갈 수 있는 상황과 같아요.

설명: 친구 A와 친구 B가 서로의 집 열쇠를 가지고 있어야만 집에 들어갈 수 있는데, A가 B의 열쇠를 가지고 있고, B가 A의 열쇠를 가지고 있다면, 둘 다 집에 들어갈 수 없겠죠? 빈 순환 의존성도 비슷해요. 두 객체가 서로를 필요로 하면, 어느 것도 먼저 만들어질 수 없어서 문제가 생기는 거예요.

빈에서의 의미: 빈 순환 의존성은 두 개 이상의 빈이 서로를 참조할 때 발생하는 문제입니다.




6. 순환 의존성 해결 방법

  • 생성자 주입 대신 필드 주입 또는 세터 주입 사용: 순환 의존성을 피할 수 있습니다.
  • @Lazy 어노테이션 사용: 주입을 지연시켜 순환 의존성을 완화할 수 있습니다.

예제 코드

ImageController.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/image")
public class ImageController {

    private final UserService userService;
    private final S3Service s3Service;

    @Autowired
    public ImageController(@Lazy UserService userService, S3Service s3Service) {
        this.userService = userService;
        this.s3Service = s3Service;
    }

    // 기타 메서드
}

UserService.java

import org.springframework.stereotype.Service;

@Service
public class UserService {

    // UserController를 참조하지 않도록 설계 변경

    // 기타 메서드
}

하지만 이러한 lazy 의 사용은 가능한 사용하지 않는것을 권장하고 최대한 서로간의 의존성이 부딪치지 않게 설계하는것이 더 중요합니다.

이 경우 서로 순환되지 않게 의존성을 변경하거나 혹은 공동으로 사용되는 클래스를 따로 빼서 지정하는 방법도 유용합니다

순환 의존성 문제 설명

순환 의존성 예시

  1. UserController -> UserService
  2. UserService -> ImageController
  3. ImageController -> UserService

이 구조에서는 서로가 서로를 참조하고 있어서, Spring이 어떤 빈을 먼저 생성해야 할지 알 수 없게 됩니다.

UserController ---> UserService
     ^                   |
     |                   v
ImageController <--- ImageService

문제 해결: TokenService 분리

TokenService를 분리하면, 서로 얽히지 않고 명확한 참조 관계를 유지할 수 있습니다.

TokenService 분리 후

  1. UserController -> UserService
  2. ImageController -> UserService
  3. UserService -> TokenService
  4. ImageController -> TokenService

이 구조에서는 UserService와 ImageController가 TokenService를 참조하지만, 서로를 참조하지 않기 때문에 순환 의존성이 발생하지 않습니다.

UserController ---> UserService ---> TokenService
                    ^
                    |
ImageController ---> TokenService

요약

  1. 순환 의존성: UserController와 UserService, ImageController가 서로 얽혀서 참조하는 구조에서 순환 의존성 문제가 발생합니다.
  2. 문제 해결: TokenService를 분리하여 UserService와 ImageController가 독립적으로 TokenService를 참조하게 하면, 순환 의존성 문제를 해결할 수 있습니다.




마무리

이제 각 개념을 비유와 함께 설명해드렸습니다. 조금 더 이해하기 쉬워졌기를 바랍니다. Spring 프레임워크를 배우는 과정에서 익숙치 않더라도 천천히 익혀가면 더 쉽게 이해하실 수 있을 거예요.

profile
삽질도 100번 하면 요령이 생긴다. 부족한 건 경험으로 채우는 개발자

0개의 댓글