Spring Bean 생명주기 완벽 이해하기

geoson·2025년 6월 7일

Spring & 백엔드

목록 보기
16/18

1. Bean이 뭐예요?

🤔 Bean = Spring이 관리하는 객체

쉽게 말해서 Spring이 대신 만들어주고 관리해주는 객체예요!

일반 객체 vs Spring Bean

// 🙅‍♂️ 일반적인 방법 - 내가 직접 만들어야 함
public class MyService {
    public static void main(String[] args) {
        UserService userService = new UserService(); // 내가 직접 new
        OrderService orderService = new OrderService(); // 내가 직접 new
        
        // 각 객체들 연결도 내가 해야 함 😰
        orderService.setUserService(userService);
    }
}
// 🙆‍♂️ Spring Bean - Spring이 알아서 다 해줌!
@Service // "Spring아, 이 클래스 알아서 만들어줘!"
public class UserService {
    // Spring이 알아서 만들어줌 😎
}

@Service
public class OrderService {
    @Autowired // "Spring아, UserService 넣어줘!"
    private UserService userService;
    // Spring이 알아서 연결해줌 😍
}

💡 Bean의 장점

  1. 자동 생성 - 내가 new 안 써도 됨
  2. 자동 연결 - 객체들 연결 알아서 해줌
  3. 싱글톤 - 메모리 효율적 (하나만 만들어서 공유)
  4. 생명주기 관리 - 언제 만들고 언제 없앨지 Spring이 결정

2. Bean의 일생

👶 Bean이 태어나서 죽을 때까지

Bean의 인생을 사람으로 비유해볼게요!

👶 탄생 (생성자) 
   ↓
📚 교육 (의존성 주입)
   ↓
🎓 성인식 (@PostConstruct) ← 진짜 일할 준비 완료!
   ↓
💼 일하는 중... (우리가 사용)
   ↓
👋 은퇴식 (@PreDestroy) ← 정리하고 떠날 준비
   ↓
💀 사망 (소멸)

🔍 실제 코드로 보기

@Component
public class MyBean {
    
    // 1️⃣ 탄생 - 생성자
    public MyBean() {
        System.out.println("👶 MyBean 태어남!");
    }
    
    // 2️⃣ 교육 완료 후 성인식 - 진짜 일할 준비 완료!
    @PostConstruct
    public void 성인식() {
        System.out.println("🎓 MyBean 성인됨! 이제 일할 수 있어요!");
        // 진짜 중요한 초기화 작업은 여기서!
    }
    
    // 3️⃣ 일하는 중...
    public void doWork() {
        System.out.println("💼 MyBean 열심히 일하는 중...");
    }
    
    // 4️⃣ 은퇴식 - 정리하고 떠날 준비
    @PreDestroy
    public void 은퇴식() {
        System.out.println("👋 MyBean 은퇴! 뒷정리 하고 갑니다~");
        // 정리 작업은 여기서!
    }
}

📝 실행 결과

👶 MyBean 태어남!
🎓 MyBean 성인됨! 이제 일할 수 있어요!
💼 MyBean 열심히 일하는 중...
💼 MyBean 열심히 일하는 중...
👋 MyBean 은퇴! 뒷정리 하고 갑니다~

3. 콜백 메서드 쉽게 이해하기

🤔 콜백 메서드가 뭐예요?

"특정 상황이 되면 자동으로 호출되는 메서드"예요!

일상 생활 예시

📱 "알람 8시에 울려줘" → 8시 되면 자동으로 울림
🏠 "현관문 열리면 '어서오세요' 말해줘" → 문 열리면 자동으로 말함

Spring에서는?

🏗️ "Bean 준비 완료되면 init() 호출해줘" → @PostConstruct
🗑️ "Bean 사라지기 전에 cleanup() 호출해줘" → @PreDestroy

💡 왜 쓰는거예요?

❌ 콜백 없이 (위험!)

@Component
public class DatabaseService {
    private Connection connection;
    
    public DatabaseService() {
        // 생성자에서 DB 연결? 위험해요! 😰
        // 아직 설정이 안 끝났을 수도 있어요!
        this.connection = DriverManager.getConnection("...");
    }
}

✅ 콜백 사용 (안전!)

@Component
public class DatabaseService {
    private Connection connection;
    
    public DatabaseService() {
        System.out.println("DatabaseService 객체만 일단 만들었어요");
        // 연결은 나중에...
    }
    
    @PostConstruct
    public void connectDB() {
        System.out.println("이제 모든 준비 끝! DB 연결해요!");
        this.connection = DriverManager.getConnection("...");
    }
    
    @PreDestroy  
    public void disconnectDB() {
        System.out.println("앱 종료하기 전에 DB 연결 끊을게요!");
        if (connection != null) {
            connection.close();
        }
    }
}

🎯 콜백 메서드 3가지 방법

1️⃣ @PostConstruct / @PreDestroy (가장 쉽고 많이 씀!)

@Component
public class SimpleService {
    
    @PostConstruct
    public void init() {
        System.out.println("초기화 완료!");
    }
    
    @PreDestroy
    public void cleanup() {
        System.out.println("정리 완료!");
    }
}

2️⃣ 인터페이스 구현 (조금 복잡)

@Component
public class InterfaceService implements InitializingBean, DisposableBean {
    
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("인터페이스로 초기화!");
    }
    
    @Override
    public void destroy() throws Exception {
        System.out.println("인터페이스로 정리!");
    }
}

3️⃣ @Bean 설정 (외부 라이브러리용)

@Configuration
public class MyConfig {
    
    @Bean(initMethod = "start", destroyMethod = "stop")
    public ExternalLibrary externalLibrary() {
        return new ExternalLibrary();
    }
}

// 외부 라이브러리 (수정 불가능한 클래스)
public class ExternalLibrary {
    public void start() {
        System.out.println("외부 라이브러리 시작!");
    }
    
    public void stop() {
        System.out.println("외부 라이브러리 종료!");
    }
}

4. 실제로 써보기

🎮 간단한 게임 서비스 만들기

@Service
public class GameService {
    private List<String> players;
    private boolean gameStarted;
    
    public GameService() {
        System.out.println("🎮 게임 서비스 객체 생성!");
    }
    
    @PostConstruct
    public void setupGame() {
        System.out.println("🏁 게임 초기 설정 시작!");
        
        // 게임 초기화
        players = new ArrayList<>();
        gameStarted = false;
        
        // 기본 플레이어 추가
        players.add("Player1");
        
        System.out.println("✅ 게임 준비 완료! 플레이어 수: " + players.size());
    }
    
    public void addPlayer(String playerName) {
        players.add(playerName);
        System.out.println("👤 새 플레이어 추가: " + playerName);
    }
    
    public void startGame() {
        gameStarted = true;
        System.out.println("🚀 게임 시작! 총 " + players.size() + "명 참여");
    }
    
    @PreDestroy
    public void endGame() {
        System.out.println("🏁 게임 종료 중...");
        
        // 게임 결과 저장
        System.out.println("💾 게임 결과를 파일에 저장합니다");
        
        // 플레이어들에게 알림
        System.out.println("📧 플레이어들에게 종료 알림 발송");
        
        // 리소스 정리
        players.clear();
        gameStarted = false;
        
        System.out.println("✅ 게임 종료 완료!");
    }
}

🌐 웹 컨트롤러에서 사용하기

@RestController
public class GameController {
    
    @Autowired
    private GameService gameService; // Spring이 자동으로 넣어줌!
    
    @PostMapping("/game/player")
    public String addPlayer(@RequestParam String name) {
        gameService.addPlayer(name);
        return "플레이어 추가 완료: " + name;
    }
    
    @PostMapping("/game/start")
    public String startGame() {
        gameService.startGame();
        return "게임 시작!";
    }
}

🖥️ 실행 과정

1. Spring 시작
   👶 게임 서비스 객체 생성!
   🏁 게임 초기 설정 시작!
   ✅ 게임 준비 완료! 플레이어 수: 1

2. API 호출: POST /game/player?name=김철수
   👤 새 플레이어 추가: 김철수

3. API 호출: POST /game/start  
   🚀 게임 시작! 총 2명 참여

4. Spring 종료
   🏁 게임 종료 중...
   💾 게임 결과를 파일에 저장합니다
   📧 플레이어들에게 종료 알림 발송
   ✅ 게임 종료 완료!

5. Bean의 종류들

🏠 Bean이 사는 집의 종류 (스코프)

1️⃣ Singleton (기본값) - 아파트 1호실에 혼자 살기

@Component // 기본이 Singleton
public class SingletonBean {
    private int count = 0;
    
    public void increase() {
        count++;
        System.out.println("카운트: " + count + ", 객체: " + this.hashCode());
    }
}

// 테스트
@Test
void singletonTest() {
    SingletonBean bean1 = context.getBean(SingletonBean.class);
    SingletonBean bean2 = context.getBean(SingletonBean.class);
    
    bean1.increase(); // 카운트: 1, 객체: 12345
    bean2.increase(); // 카운트: 2, 객체: 12345 (같은 객체!)
    
    System.out.println(bean1 == bean2); // true - 똑같은 객체!
}

특징:

  • 앱 전체에서 딱 하나만 만들어짐
  • 메모리 효율적
  • 상태를 가지면 안됨 (여러 곳에서 공유하니까)

2️⃣ Prototype - 매번 새 집으로 이사가기

@Component
@Scope("prototype")
public class PrototypeBean {
    private String id = UUID.randomUUID().toString();
    
    public String getId() {
        return id;
    }
}

// 테스트
@Test
void prototypeTest() {
    PrototypeBean bean1 = context.getBean(PrototypeBean.class);
    PrototypeBean bean2 = context.getBean(PrototypeBean.class);
    
    System.out.println(bean1.getId()); // abc-123-def
    System.out.println(bean2.getId()); // xyz-456-ghi (다름!)
    
    System.out.println(bean1 == bean2); // false - 다른 객체!
}

특징:

  • 요청할 때마다 새로운 객체 생성
  • 상태를 가져도 됨 (각자 다른 객체니까)

3️⃣ Request - 웹 요청마다 새 집 (웹에서만)

@Component
@Scope("request")
public class RequestBean {
    private String requestId = UUID.randomUUID().toString();
    
    @PostConstruct
    public void init() {
        System.out.println("Request Bean 생성: " + requestId);
    }
    
    @PreDestroy
    public void destroy() {
        System.out.println("Request Bean 소멸: " + requestId);
    }
}

특징:

  • HTTP 요청마다 새로운 객체
  • 요청 끝나면 자동으로 소멸

6. 실무에서 자주 쓰는 패턴

💾 1. 데이터베이스 연결 관리

@Component
public class DatabaseManager {
    private Connection connection;
    private boolean connected = false;
    
    @PostConstruct
    public void connect() {
        System.out.println("📡 데이터베이스 연결 중...");
        try {
            // 실제로는 application.yml에서 설정 읽어옴
            connection = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/mydb", 
                "username", 
                "password"
            );
            connected = true;
            System.out.println("✅ 데이터베이스 연결 성공!");
        } catch (Exception e) {
            System.out.println("❌ 데이터베이스 연결 실패: " + e.getMessage());
        }
    }
    
    @PreDestroy
    public void disconnect() {
        System.out.println("📡 데이터베이스 연결 해제 중...");
        try {
            if (connection != null && !connection.isClosed()) {
                connection.close();
                System.out.println("✅ 데이터베이스 연결 해제 완료");
            }
        } catch (Exception e) {
            System.out.println("❌ 연결 해제 중 오류: " + e.getMessage());
        }
    }
    
    public boolean isConnected() {
        return connected;
    }
}

🗄️ 2. 캐시 서비스

@Component
public class CacheService {
    private Map<String, Object> cache;
    
    @PostConstruct
    public void initCache() {
        System.out.println("🗄️ 캐시 서비스 초기화 중...");
        cache = new HashMap<>();
        
        // 자주 쓰는 데이터 미리 로드
        cache.put("app_name", "내 멋진 앱");
        cache.put("version", "1.0.0");
        
        System.out.println("✅ 캐시 초기화 완료! 기본 데이터 " + cache.size() + "개 로드");
    }
    
    public void put(String key, Object value) {
        cache.put(key, value);
        System.out.println("💾 캐시 저장: " + key);
    }
    
    public Object get(String key) {
        Object value = cache.get(key);
        System.out.println("📖 캐시 조회: " + key + " → " + value);
        return value;
    }
    
    @PreDestroy
    public void clearCache() {
        System.out.println("🗑️ 캐시 정리 중...");
        if (cache != null) {
            System.out.println("📊 캐시 정리 전 데이터 수: " + cache.size());
            cache.clear();
            System.out.println("✅ 캐시 정리 완료");
        }
    }
}

📧 3. 이메일 서비스

@Component
public class EmailService {
    private boolean emailServerConnected = false;
    
    @PostConstruct
    public void connectEmailServer() {
        System.out.println("📧 이메일 서버 연결 중...");
        
        // 실제로는 SMTP 서버 연결
        try {
            Thread.sleep(1000); // 연결 시뮬레이션
            emailServerConnected = true;
            System.out.println("✅ 이메일 서버 연결 완료!");
        } catch (Exception e) {
            System.out.println("❌ 이메일 서버 연결 실패");
        }
    }
    
    public void sendEmail(String to, String subject, String content) {
        if (emailServerConnected) {
            System.out.println("📤 이메일 발송: " + to + " - " + subject);
        } else {
            System.out.println("❌ 이메일 서버 연결되지 않음");
        }
    }
    
    @PreDestroy
    public void disconnectEmailServer() {
        System.out.println("📧 이메일 서버 연결 해제 중...");
        emailServerConnected = false;
        System.out.println("✅ 이메일 서버 연결 해제 완료");
    }
}

7. 자주 하는 실수들

❌ 실수 1: 생성자에서 위험한 작업

// 🚫 이렇게 하지 마세요!
@Component
public class BadService {
    @Autowired
    private DatabaseService dbService;
    
    public BadService() {
        // 생성자에서 DB 작업? 위험해요! 😰
        // 아직 dbService가 주입되지 않았을 수도 있어요!
        dbService.connect(); // NullPointerException 발생 가능!
    }
}
// ✅ 이렇게 해주세요!
@Component
public class GoodService {
    @Autowired
    private DatabaseService dbService;
    
    public GoodService() {
        System.out.println("객체만 생성했어요. 아직 일할 준비 안됨!");
    }
    
    @PostConstruct
    public void init() {
        System.out.println("이제 모든 준비 끝! 일할 수 있어요!");
        dbService.connect(); // 안전하게 사용 가능!
    }
}

❌ 실수 2: @PreDestroy 안 쓰기

// 🚫 이렇게 하면 리소스 낭비!
@Component  
public class ResourceWasteService {
    private FileInputStream fileStream;
    
    @PostConstruct
    public void openFile() {
        try {
            fileStream = new FileInputStream("large-file.txt");
        } catch (Exception e) {
            // 파일 열기
        }
    }
    
    // @PreDestroy가 없어서 파일이 계속 열려있음! 😱
}
// ✅ 이렇게 정리해주세요!
@Component
public class CleanService {
    private FileInputStream fileStream;
    
    @PostConstruct
    public void openFile() {
        try {
            fileStream = new FileInputStream("large-file.txt");
            System.out.println("📁 파일 열었어요!");
        } catch (Exception e) {
            System.out.println("❌ 파일 열기 실패");
        }
    }
    
    @PreDestroy
    public void closeFile() {
        try {
            if (fileStream != null) {
                fileStream.close();
                System.out.println("📁 파일 닫았어요!");
            }
        } catch (Exception e) {
            System.out.println("❌ 파일 닫기 실패");
        }
    }
}

❌ 실수 3: Prototype Bean 잘못 사용

// 🚫 Prototype인데 Singleton처럼 주입받기
@Service
public class BadPrototypeUser {
    @Autowired
    private PrototypeBean prototypeBean; // 한 번만 주입됨! 😰
    
    public void doSomething() {
        // 매번 같은 객체 사용... Prototype 의미 없음
        prototypeBean.doWork();
    }
}
// ✅ Prototype은 필요할 때마다 새로 받기
@Service  
public class GoodPrototypeUser {
    @Autowired
    private ApplicationContext context;
    
    public void doSomething() {
        // 필요할 때마다 새로운 Prototype Bean 받기
        PrototypeBean bean = context.getBean(PrototypeBean.class);
        bean.doWork();
    }
}

🎯 핵심 정리

✨ 꼭 기억할 것들

  1. Bean = Spring이 관리하는 객체
  2. @PostConstruct = 진짜 일할 준비 완료된 후 호출
  3. @PreDestroy = 사라지기 전 정리 작업
  4. 생성자는 가볍게, 무거운 작업은 @PostConstruct에서
  5. 리소스 사용했으면 @PreDestroy에서 정리

🚀 실무 패턴

@Component
public class 실무Bean {
    
    // 생성자는 간단하게
    public 실무Bean() {
        System.out.println("객체 생성 완료");
    }
    
    // 초기화는 여기서
    @PostConstruct
    public void 초기화() {
        System.out.println("초기화 작업 시작");
        // DB 연결, 파일 열기, 캐시 로드 등
        System.out.println("초기화 완료 - 이제 일할 수 있어요!");
    }
    
    // 정리는 여기서
    @PreDestroy
    public void 정리() {
        System.out.println("정리 작업 시작");
        // DB 연결 해제, 파일 닫기, 캐시 정리 등
        System.out.println("정리 완료 - 안녕히 가세요!");
    }
}

📝 연습 문제

Q1. 다음 코드의 실행 순서는?

@Component
public class QuizBean {
    public QuizBean() {
        System.out.println("A: 생성자 호출");
    }
    
    @PostConstruct
    public void init() {
        System.out.println("B: PostConstruct 호출");
    }
    
    public void work() {
        System.out.println("C: work 메서드 호출");
    }
    
    @PreDestroy
    public void destroy() {
        System.out.println("D: PreDestroy 호출");
    }
}
💡 정답 보기

실행 순서: A → B → C → D

  1. A: 생성자 호출 - 객체 생성
  2. B: PostConstruct 호출 - 초기화 완료
  3. C: work 메서드 호출 - 실제 사용 (여러 번 가능)
  4. D: PreDestroy 호출 - 앱 종료시 정리

핵심: 생성자 → @PostConstruct → 사용 → @PreDestroy 순서!


Q2. 이 코드의 문제점은?

@Component
public class ProblemBean {
    @Autowired
    private DatabaseService dbService;
    
    public ProblemBean() {
        dbService.connect(); // 문제가 있나요?
    }
}
💡 정답 보기

문제점: 생성자에서 의존성 사용 시도

왜 문제인가?

  • 생성자 실행 시점에는 아직 @Autowired가 완료되지 않았을 수 있음
  • dbServicenull이어서 NullPointerException 발생 가능

해결책:

@Component
public class FixedBean {
    @Autowired
    private DatabaseService dbService;
    
    public FixedBean() {
        System.out.println("객체 생성만 완료");
    }
    
    @PostConstruct
    public void init() {
        dbService.connect(); // 여기서 안전하게 사용!
    }
}

핵심: 의존성이 필요한 작업은 @PostConstruct에서!


Spring Bean 생명주기를 이해하면 Spring의 동작 원리가 훨씬 명확해져요! 🚀

0개의 댓글