강의 4 – 빈(Bean)과 생명주기

·

,






스프링 프레임워크 강의 4: 스프링 빈(Bean)과 생명주기


스프링 프레임워크 강의 #4

스프링 빈(Bean)과 생명주기

초급~중급
소요시간: 약 60분

1. 스프링 빈(Bean) 소개

스프링 프레임워크의 핵심 개념 중 하나는 빈(Bean)입니다. 스프링 빈은 스프링 IoC(Inversion of Control) 컨테이너에 의해 생성, 관리되는 객체입니다. 이전 강의에서 배운 의존성 주입(DI)을 통해 빈들이 서로 연결되어 애플리케이션을 구성합니다.

스프링 빈은 단순한 자바 객체(POJO: Plain Old Java Object)지만, 스프링 컨테이너에 의해 관리되면서 여러 특별한 기능과 생명주기를 갖게 됩니다. 이번 강의에서는 스프링 빈이 무엇인지, 어떻게 정의되고 관리되는지, 그리고 생명주기는 어떻게 작동하는지 자세히 알아보겠습니다.

2. 스프링 빈의 정의

스프링 빈은 다음과 같은 방법으로 정의할 수 있습니다:

2.1 XML 기반 구성

전통적인 방식으로, XML 파일에 빈을 정의합니다:

<!-- applicationContext.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userService" class="com.example.service.UserServiceImpl">
        <property name="userRepository" ref="userRepository"/>
    </bean>
    
    <bean id="userRepository" class="com.example.repository.JdbcUserRepository">
        <constructor-arg ref="dataSource"/>
    </bean>
    
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
        <property name="username" value="root"/>
        <property name="password" value="password"/>
    </bean>
</beans>

2.2 Java 기반 구성

최신 스프링 애플리케이션에서 더 많이 사용되는 방식으로, Java 클래스에 빈을 정의합니다:

@Configuration
public class AppConfig {
    
    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/mydb");
        dataSource.setUsername("root");
        dataSource.setPassword("password");
        return dataSource;
    }
    
    @Bean
    public UserRepository userRepository() {
        return new JdbcUserRepository(dataSource());
    }
    
    @Bean
    public UserService userService() {
        UserServiceImpl service = new UserServiceImpl();
        service.setUserRepository(userRepository());
        return service;
    }
}

2.3 컴포넌트 스캔

어노테이션을 사용하여 자동으로 빈을 검색하고 등록하는 방식입니다:

@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
    // 구성 클래스 내용
}

// 컴포넌트 스캔으로 자동 감지될 클래스들:
@Component
public class SimpleBean {
    // 클래스 내용
}

@Service
public class UserServiceImpl implements UserService {
    // 서비스 구현
}

@Repository
public class JdbcUserRepository implements UserRepository {
    // 리포지토리 구현
}

3. 스프링 빈의 스코프

스프링 빈은 다양한 스코프(범위)를 가질 수 있으며, 이는 빈의 생성 방식과 수명에 영향을 줍니다.

스코프 설명 예시
singleton 기본 스코프. 스프링 컨테이너당 하나의 인스턴스만 생성됩니다. 서비스, 저장소 등
prototype 요청할 때마다 새로운 인스턴스를 생성합니다. 상태를 가진 빈들
request HTTP 요청당 하나의 인스턴스가 생성됩니다. (웹 애플리케이션) HTTP 요청 처리기
session HTTP 세션당 하나의 인스턴스가 생성됩니다. (웹 애플리케이션) 사용자 세션 데이터
application ServletContext 생명주기 동안 하나의 인스턴스가 생성됩니다. (웹 애플리케이션) 애플리케이션 설정
websocket WebSocket 생명주기 동안 하나의 인스턴스가 생성됩니다. WebSocket 관리자

스코프 설정 예제:

XML 기반:

<bean id="singletonBean" class="com.example.SingletonBean" scope="singleton"/>
<bean id="prototypeBean" class="com.example.PrototypeBean" scope="prototype"/>

Java 기반:

@Bean
@Scope("singleton")
public SingletonBean singletonBean() {
    return new SingletonBean();
}

@Bean
@Scope("prototype")
public PrototypeBean prototypeBean() {
    return new PrototypeBean();
}

어노테이션 기반:

@Component
@Scope("singleton")
public class SingletonBean {
    // ...
}

@Component
@Scope("prototype")
public class PrototypeBean {
    // ...
}

주의사항: singleton 빈이 prototype 빈을 주입받을 때 발생할 수 있는 문제

singleton 빈은 한 번만 생성되므로, prototype 빈을 직접 주입받으면 항상 같은 인스턴스를 사용하게 됩니다. 이를 해결하기 위해 ObjectFactory, Provider 또는 스코프 프록시를 사용할 수 있습니다.

@Component
public class SingletonBean {
    @Autowired
    private Provider<PrototypeBean> prototypeBeanProvider;
    
    public void doSomething() {
        // 매번 새로운 prototype 인스턴스를 얻음
        PrototypeBean prototypeBean = prototypeBeanProvider.get();
        // ...
    }
}

4. 스프링 빈의 생명주기

스프링 빈은 생성부터 소멸까지 일련의 생명주기를 가집니다. 스프링 컨테이너는 이 생명주기를 관리하며, 개발자는 생명주기의 특정 시점에 로직을 실행하도록 설정할 수 있습니다.

스프링 빈 생명주기

인스턴스화 프로퍼티 설정 초기화 콜백 소멸 콜백

생성자 호출 의존성 주입 @PostConstruct, InitializingBean afterPropertiesSet(), init-method @PreDestroy, DisposableBean destroy(), destroy-method

스프링 컨테이너

스프링 빈의 기본 생명주기는 다음과 같습니다:

  1. 인스턴스화: 스프링 컨테이너가 빈의 인스턴스를 생성합니다.
  2. 의존성 주입: 설정된 의존성이 빈에 주입됩니다.
  3. 초기화 콜백: 모든 의존성이 주입된 후 초기화 콜백이 호출됩니다.
  4. 사용: 빈이 애플리케이션에서 사용됩니다.
  5. 소멸 콜백: 컨테이너가 종료될 때 소멸 콜백이 호출됩니다.

5. 초기화 및 소멸 콜백

스프링은 빈의 초기화와 소멸 시점에 특정 메서드를 호출하는 여러 방법을 제공합니다.

5.1 인터페이스 기반 콜백

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.DisposableBean;

@Component
public class DatabaseService implements InitializingBean, DisposableBean {
    
    private Connection connection;
    
    @Override
    public void afterPropertiesSet() throws Exception {
        // 초기화 로직
        System.out.println("데이터베이스 연결 중...");
        this.connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "pass");
        System.out.println("데이터베이스 연결 완료");
    }
    
    @Override
    public void destroy() throws Exception {
        // 소멸 로직
        System.out.println("데이터베이스 연결 종료 중...");
        if (this.connection != null && !this.connection.isClosed()) {
            this.connection.close();
        }
        System.out.println("데이터베이스 연결 종료 완료");
    }
    
    // 비즈니스 메서드
    public void executeQuery(String sql) {
        // 쿼리 실행 로직
    }
}

5.2 어노테이션 기반 콜백

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

@Component
public class CacheService {
    
    private Map<String, Object> cache;
    
    @PostConstruct
    public void init() {
        System.out.println("캐시 초기화 중...");
        this.cache = new ConcurrentHashMap<>();
        System.out.println("캐시 초기화 완료");
    }
    
    @PreDestroy
    public void cleanup() {
        System.out.println("캐시 정리 중...");
        if (this.cache != null) {
            this.cache.clear();
            this.cache = null;
        }
        System.out.println("캐시 정리 완료");
    }
    
    // 비즈니스 메서드
    public void put(String key, Object value) {
        this.cache.put(key, value);
    }
    
    public Object get(String key) {
        return this.cache.get(key);
    }
}

5.3 XML 설정 기반 콜백

<bean id="fileService" class="com.example.FileService" 
      init-method="initialize" destroy-method="cleanup"/>

5.4 Java 설정 기반 콜백

@Bean(initMethod = "initialize", destroyMethod = "cleanup")
public FileService fileService() {
    return new FileService();
}

권장사항: 어노테이션 기반 방식(@PostConstruct, @PreDestroy)이 가장 권장되는 방식입니다. 스프링 독립적인 코드를 작성하려면 XML이나 Java 설정 방식을 사용하는 것이 좋습니다.

6. 어노테이션 기반 빈 구성

최신 스프링 애플리케이션에서는 XML 대신 어노테이션을 사용하여 빈을 구성하는 것이 일반적입니다. 이는 더 간결하고 타입 안전한 방식으로 빈을 정의할 수 있습니다.

6.1 컴포넌트 스캔 관련 어노테이션

  • @Component: 일반 스프링 관리 컴포넌트를 나타냅니다.
  • @Service: 비즈니스 로직을 담당하는 서비스 계층의 컴포넌트를 나타냅니다.
  • @Repository: 데이터 접근 계층의 컴포넌트를 나타냅니다.
  • @Controller: 웹 요청을 처리하는 컴포넌트를 나타냅니다.
  • @RestController: REST API 엔드포인트를 제공하는 컴포넌트를 나타냅니다.

6.2 의존성 주입 관련 어노테이션

  • @Autowired: 의존성을 자동으로 주입합니다.
  • @Qualifier: 같은 타입의 여러 빈 중 특정 빈을 지정합니다.
  • @Value: 속성 값을 주입합니다.
  • @Required: 필수 의존성을 표시합니다 (Spring 5.1부터 deprecated).

6.3 설정 관련 어노테이션

  • @Configuration: 빈 정의를 포함하는 클래스를 나타냅니다.
  • @Bean: 메서드가 스프링 빈을 생성함을 나타냅니다.
  • @ComponentScan: 컴포넌트 스캔 범위를 지정합니다.
  • @PropertySource: 속성 파일을 로드합니다.
  • @Profile: 특정 프로필에서만 활성화되는 빈을 나타냅니다.
@Configuration
@ComponentScan(basePackages = "com.example")
@PropertySource("classpath:application.properties")
public class AppConfig {

    @Bean
    public DataSource dataSource(
            @Value("${db.driver}") String driver,
            @Value("${db.url}") String url,
            @Value("${db.username}") String username,
            @Value("${db.password}") String password) {
        
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }
    
    @Bean
    @Profile("development")
    public DataSource devDataSource() {
        // 개발용 데이터 소스 설정
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:mem:devdb");
        dataSource.setUsername("dev");
        dataSource.setPassword("dev");
        return dataSource;
    }
}

예제: 어노테이션 기반 빈 구성

// 도메인 클래스
public class User {
    private Long id;
    private String username;
    private String email;
    
    // getters, setters, constructors
}

// Repository 계층
@Repository
public class UserRepository {
    private JdbcTemplate jdbcTemplate;
    
    @Autowired
    public UserRepository(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
    
    public User findById(Long id) {
        return jdbcTemplate.queryForObject(
            "SELECT id, username, email FROM users WHERE id = ?",
            new Object[] { id },
            (rs, rowNum) -> {
                User user = new User();
                user.setId(rs.getLong("id"));
                user.setUsername(rs.getString("username"));
                user.setEmail(rs.getString("email"));
                return user;
            }
        );
    }
    
    // 기타 CRUD 메서드
}

// Service 계층
@Service
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;
    
    @Autowired
    public UserService(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }
    
    public User getUserById(Long id) {
        return userRepository.findById(id);
    }
    
    @PostConstruct
    public void init() {
        System.out.println("UserService가 초기화되었습니다");
    }
    
    @PreDestroy
    public void cleanup() {
        System.out.println("UserService가 종료됩니다");
    }
    
    // 기타 비즈니스 메서드
}

// 설정 클래스
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
    
    @Bean
    public DataSource dataSource() {
        // 데이터 소스 설정
    }
    
    @Bean
    public EmailService emailService() {
        return new EmailServiceImpl();
    }
}

7. 실습 과제

빈 생명주기 실습

다음 요구사항에 맞는 스프링 빈을 작성하고 테스트해보세요:

  1. 파일 로거(FileLogger) 클래스 작성

    • 초기화 시 로그 파일을 생성하고 열기
    • 소멸 시 로그 파일을 안전하게 닫기
    • log(String message) 메서드 제공
    • 적절한 생명주기 콜백 사용
    • 프로토타입 스코프로 설정
  2. 설정 로더(ConfigurationLoader) 클래스 작성

    • 초기화 시 application.properties 파일에서 설정 로드
    • getProperty(String key) 메서드 제공
    • 싱글톤 스코프로 설정
  3. 서비스 클래스(ApplicationService) 작성

    • ConfigurationLoader와 FileLogger 의존성 주입
    • FileLogger는 Provider를 통해 주입(prototype)
    • start() 메서드 구현: 로거에 시작 메시지 기록
    • stop() 메서드 구현: 로거에 종료 메시지 기록
  4. Java 기반 설정 클래스 작성

    • 위 세 클래스를 적절한 빈으로 등록
    • 필요한 의존성 설정
  5. 메인 애플리케이션 클래스 작성

    • ApplicationContext를 생성하고 ApplicationService 빈 가져오기
    • start() 메서드 호출
    • stop() 메서드 호출
    • 컨텍스트 종료

힌트:

@Component
@Scope("prototype")
public class FileLogger {
    private FileWriter fileWriter;
    private String logFile;
    
    @Value("${logger.file:application.log}")
    public void setLogFile(String logFile) {
        this.logFile = logFile;
    }
    
    @PostConstruct
    public void init() throws IOException {
        System.out.println("로그 파일 열기: " + logFile);
        this.fileWriter = new FileWriter(logFile, true);
    }
    
    public void log(String message) throws IOException {
        if (fileWriter != null) {
            fileWriter.write(message + "\n");
            fileWriter.flush();
        }
    }
    
    @PreDestroy
    public void cleanup() throws IOException {
        System.out.println("로그 파일 닫기: " + logFile);
        if (fileWriter != null) {
            fileWriter.close();
        }
    }
}

8. 요약 및 정리

학습 내용 요약

  • 스프링 빈의 정의: 스프링 IoC 컨테이너에 의해 관리되는 객체
  • 스프링 빈 구성 방법: XML 기반, Java 기반, 어노테이션 기반 구성
  • 스프링 빈 스코프: singleton, prototype, request, session, application, websocket
  • 스프링 빈 생명주기: 인스턴스화 → 의존성 주입 → 초기화 → 사용 → 소멸
  • 생명주기 콜백: 인터페이스 기반(InitializingBean, DisposableBean), 어노테이션 기반(@PostConstruct, @PreDestroy), 메서드 지정(init-method, destroy-method)
  • 어노테이션 기반 빈 구성: @Component, @Service, @Repository, @Controller, @Configuration, @Bean 등

다음 단계

  • 스프링 AOP(Aspect-Oriented Programming) 학습
  • 스프링 MVC를 이용한 웹 애플리케이션 개발
  • 스프링 데이터 JPA를 이용한 데이터 접근 계층 구현
  • 스프링 시큐리티를 이용한 인증 및 권한 부여

© 2023 스프링 프레임워크 강의 시리즈 – 강의 #4: 스프링 빈(Bean)과 생명주기

질문이나 피드백은 instructor@springcourse.com으로 보내주세요.


Comments

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다