[내일배움캠프 Spring 3기] Bean

jiiim_ni·2026년 2월 10일

Bean

스프링 IoC 컨테이너가 관리하는 객체를 의미

  • Spring 컨테이너에 의해 생성, 관리, 소멸됨
  • 애플리케이션 전역에서 재사용 가능
  • 기본적으로 싱글톤 스코프로 관리

싱글톤

싱글톤(Singleton)은 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴
애플리케이션 전체에서 해당 클래스의 객체를 하나만 만들고, 그것을 공유해서 사용함

싱글톤을 왜 사용할까?

  • 메모리 효율성: 최초 한 번만 객체를 생성하므로 메모리 낭비를 방지할 수 있습니다.
  • 데이터 공유와 일관성: 시스템 전반의 설정 정보나 공통 자원을 관리할 때 데이터의 일관성을 유지하기 쉽습니다.

스프링 IoC Container

  • Bean의 생성 및 생명주기 관리
  • 의존성 주입 (DI)
  • Bean 설정 정보 관리
  • Bean 간의 의존 관계 설정


Spring에서 IoC/DI vs new

IoC/DI를 사용해야 하는 경우

재사용되거나 교체 가능한 비즈니스 로직/인프라

//  ✅ Spring Bean으로 관리
@Service
public class UserService { }  // 비즈니스 로직

@Repository
public class UserRepository { }  // 데이터 접근

@Component
public class EmailSender { }  // 인프라

@Controller
public class UserController { }  // 컨트롤러

new를 사용해야 하는 경우

매번 새로운 상태를 가지는 데이터 객체

// ❌ Bean으로 만들면 안 됨!
public class User {
    private String name;
    private int age;
}

public class OrderRequest {
    private Long userId;
    private List<Item> items;
}

public class SearchCondition {
    private String keyword;
    private LocalDate startDate;
}

어노테이션 합성

어노테이션 합성은 여러 개의 어노테이션을 조합하여 하나의 새로운 어노테이션을 만드는 것을 의미

스프링은 어노테이션을 분석할 때, 해당 어노테이션 위에 붙어있는 다른 어노테이션(메타 어노테이션)까지 재귀적으로 탐색하여 그 기능을 모두 적용해 줌

어노테이션도 어노테이션을 가질 수 있음!


스프링 Bean 등록

자동 등록과 수동 등록 방식이 있음

Bean 자동 등록

Spring이 @Component 클래스를 찾아서 Bean으로 등록하는 방식

  • @ComponentScan에 설정되어있는 패키지를 기준으로 하위의 모든 @Component 클래스를 탐색하여 Bean으로 등록

Bean 수동 등록

Spring의 @Configuration 클래스와 @Bean 메소드를 사용하여 명시적으로 Bean을 등록하는 방식

  • @Configuration 클래스에 정의된 @Bean 메소드의 반환 객체가 Spring IoC 컨테이너에 Bean으로 등록

기본적으로 자동 등로이 우선, 불가피한 경우에만 수동 등록 활용


DI(Dependency Injection) 방식 비교

Spring에서 의존성 주입은 '생성자 주입', '세터 주입', '필드 주입' 방식으로 구현할 수 있음

필드 주입(Field Injection)

@Autowired를 필드에 직접 선언하여 의존성을 주입받는 방식

-> 더 이상 사용하지 않음!
여러 문제가 있지만, 대표적으로 final 키워드 사용이 불가능하기 때문에 변수에 할당된 객체를 아래의 코드 예처럼 null 혹은 다른 객체로 바꿀 수 있어 안전하지 않음

세터 주입 (Setter Injection)

Setter 메소드를 통해 의존성을 주입받는 방식

-> 더 이상 사용하지 않음!
필드 주입 방식과 같은 문제가 있어 안전하지 않음

생성자 주입 (Constructor Injection)

생성자를 통해 의존성을 주입받는 방식

-> 권장 방식

@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
}
  • private final MemberRepository memberRepository;
    final로 선언하여 런타임에 의존성이 변경될 위험이 없음

이를 불변성(Immutability) 보장이라고도 함


Bean 우선 순위

스프링 컨테이너에 똑같은 타입의 Bean이 2개 이상 등록되면 어떻게 될까?

해결 방법 1 - @Primary

@Primary는 여러 빈 중에서 우선적으로 선택될 기본(Default) 빈을 지정하는 어노테이션
-> 별다른 설정이 없다면 @Primary가 붙은 빈이 자동으로 주입됨

해결 방법 2 - @Qualifier

@Qualifier라는 이름으로 특정 빈을 직접 지정하여 주입하는 어노테이션
@Primary보다 우선순위가 높으며, 더 구체적인 선택이 필요할 때 사용


Bean 스코프

Bean 스코프는 스프링 빈이 얼마나 오래, 그리고 어떻게 존재할지를 정의하는 개념
기본값은 싱글톤

싱글톤 스코프

스프링 컨테이너가 시작될 때 단 한 번만 생성되고, 애플리케이션이 끝날 때까지 계속 재사용되는 방식
대부분의 빈은 싱글톤으로 관리되며, 메모리 효율성이 매우 좋음

프로토타입 스코프

@Scope("prototype")으로 지정된 빈은, 요청이 올 때마다 계속 새로운 객체를 생성하여 반환
스프링 컨테이너는 생성만 책임지고, 그 이후의 관리는 하지 않음


라이프사이클 콜백

  • 스프링 빈은 생성 -> 의존성 주입 -> 초기화 -> 사용 -> 소멸 이라는 생명주기(Lifecycle)를 가짐
  • 이때, 초기화와 소멸 단계에서 특정 작업을 수행하도록 콜백(Callback) 메서드를 지정할 수 있음
  • 초기화 단계
    • @PostConstruct 어노테이션이 붙은 메서드는 빈의 생성과 모든 의존성 주입이 완료된 직후에 딱 한 번 호출됨
    • 주로 주입받은 의존성을 사용하여 외부 리소스를 가져오거나, 초기 설정 값을 세팅하는 등 무거운 초기화 작업에 사용
  • 소멸 단계
    • @PreDestroy는 스프링 컨테이너에서 빈이 제거되기 직전에 호출
    • 주로 사용하던 외부 리소스의 연결을 안전하게 종료하거나, 임시 파일 삭제 등 뒷정리(Clean-up) 작업에 사용
@Component
public class MusicPlayer {

    private List<String> playlist = new ArrayList<>();

    // 초기화 콜백: 의존성 주입이 끝난 후 실행
    @PostConstruct
    public void loadPlaylist() {
        System.out.println("--- @PostConstruct 호출 ---");
        playlist.add("아이유 - 라일락");
        playlist.add("BTS - Dynamite");
        System.out.println("플레이리스트 로딩 완료!");
    }

    // 소멸 전 콜백: 빈이 사라지기 직전 실행
    @PreDestroy
    public void saveProgress() {
        System.out.println("--- @PreDestroy 호출 ---");
        System.out.println("뮤직 플레이어를 종료합니다...");
    }
}

TIL

Bean을 그냥 스프링이 만들어주는 객체라고만 알고 있었는데, 오늘 정리하면서 생명주기까지 포함한 관리 대상이라는 걸 확실히 이해했다.

싱글톤이 “객체 1개”라는 의미에서 끝나는 게 아니라, 같은 객체를 여러 곳에서 공유하니까 상태를 가지면 위험해질 수 있다는 점이 인상 깊었다. 그래서 Service/Repository는 보통 stateless 하게 만드는 이유가 납득됐다.

예전엔 new로 객체 만들면 편하다고 생각했는데, IoC/DI를 쓰면 교체(확장), 테스트, 유지보수가 쉬워지는 구조가 된다는 걸 체감했다. “편한 코드”보다 “바꾸기 쉬운 코드”가 더 중요하다는 느낌.

DI 방식 중 필드 주입이 위험한 이유를 “그냥 쓰지 말라고 해서”가 아니라, final 불가 -> 불변성 깨짐 -> 테스트/안전성 떨어짐으로 논리적으로 이해하게 됐다. 이제는 생성자 주입이 자연스럽게 기본 선택이 될 것 같다.

@Primary와 @Qualifier는 단순한 문법이 아니라, Bean이 여러 개일 때 스프링이 어떤 기준으로 선택할지 ‘명확한 의도’를 코드로 남기는 장치라는 걸 알게 됐다.

@PostConstruct, @PreDestroy는 “있으면 편한 기능” 정도로 봤는데, 실제로는 외부 리소스 초기화/정리 같은 안정성을 책임지는 포인트라서 운영 관점에서 더 중요하다는 걸 깨달았다.

0개의 댓글