2025-09-02-TIL
Today I Learned
Spring의 의존주입(DI) 방식 3가지
- 생성자 주입 (Constructor Injection)
 
- 의존 객체를 생성자의 파라미터로 전달받아 주입하는 방식
 - 불변성(immutability)을 보장하고, 테스트 시에 의존 객체를 명확하게 주입해야 하므로 안정성이 높다.
 - 스프링 4.3 이후에는 생성자가 하나뿐이라면 
@Autowired를 생략해도 자동으로 주입된다. 따라서@RequiredArgsConstructor+final필드 조합으로 주입받는 형태가 흔히 볼 수 있는 모습이다. 
1
2
3
4
5
6
7
8
9
@Component
public class OrderService {
    private final MemberRepository memberRepository;
    @Autowired
    public OrderService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}
@Autowired 생략버전
1
2
3
4
5
6
7
8
9
@Component
public class OrderService {
    private final MemberRepository memberRepository;
  	// @RequiredArgsConstructor로 대체 가능
    public OrderService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}
- 수정자 주입 (Setter Injection)
 
- 의존 객체를 Setter 메서드를 통해 주입하는 방식
 - 선택적 의존 관계를 주입할 때 유용하다.
 - 하지만 의존 객체가 설정되지 않은 채 사용될 가능성이 있어 주의가 필요하다. ()
 
1
2
3
4
5
6
7
8
9
@Component
public class OrderService {
    private MemberRepository memberRepository;
    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}
- 필드 주입 (Field Injection)
 
- 의존 객체를 필드에 직접 주입하는 방식
 - 코드가 간결하지만, 테스트 및 유지보수에 불리하며 권장되지 않음.
 - 주로 간단한 테스트나 프로토타입 코드에서 사용한다.
 
1
2
3
4
5
@Component
public class OrderService {
    @Autowired
    private MemberRepository memberRepository;
}
결론적으로, 가장 권장되는 방식은 생성자 주입이다. Setter 주입은 단점이나 잠재적 위험성이 많아서 기본적으로 지양하는 것이 좋다. setter로 주입받는 다는 것 자체가 객체의 생명주기에서 문제가 되는 지점이 있다. 우선, 생성 시점에 주입되지 않았는데, 의존성의 메서드를 호출하게 되면 NPE가 발생할 수 있다. 또 중간에 setter를 통해서 의존성이 변경될 가능성이 있으므로 위험하다.
따라서 final로 필드의 변경을 막고 불변(immutable)으로 선언하는 것이 안전하다. 그리고 final로 선언하게되면 생성자 주입을 할 수 밖에 없는 것이다. 그래도 setter 주입이 꼭 필요한 부분도 있는데, 이는 선택적이거나 변경이 가능한 의존관계도 있다는 뜻이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
class AuditService {
    private Optional<AuditSink> sink = Optional.empty();
    @Autowired
    public void setSink(Optional<AuditSink> sink) {
        this.sink = sink;
    }
    public void record(Event e) {
        sink.ifPresent(s -> s.write(e)); // 없으면 조용히 패스
    }
}
위와 같이 Optional 필드에 대해서는 setter를 통해서 주입하는것 밖에는 방법이 없다. 생성자를 통해 주입하는 순간 이미 Optional한 필드가 아니게 된다.
필드 주입은 특별한 경우를 제외하고는 피하는 것이 좋다. final을 못 쓴다는 점에서 단점은 충분하다. 하지만 테스트 코드를 작성할때 코드의 가독성이나 간편함을 목적으로 사용할 수는 있다.
테스트 코드는 왜 작성해야 할까?
테스트 코드는 “버그 예방 + 안전한 변경 + 지식의 기록”을 자동으로 해주는 가장 싸고 강력한 도구이다. 덕분에 더 빠르게, 더 자신 있게, 더 오래 유지되는 소프트웨어를 만들 수 있다.
테스트 코드가 주는 핵심 가치
- 회귀(Regression) 차단: 이미 고친 버그가 다시 살아나는 걸(회귀 버그) 막습니다. 한 줄 바꿨을 때 무슨 사이드이펙트가 나는지, 테스트가 즉시 알려줘요.
 - 설계 품질 향상: 테스트하기 쉬운 코드는 대개 결합도가 낮고, SRP(단일 책임)에 가깝습니다. 즉, 테스트 작성 자체가 자연스레 좋은 설계로 압력을 줍니다.
 - 개발 속도 상승: 수동 확인 대비 훨씬 빠르게 동작을 검증합니다. 특히 CI에서 수백 개 테스트가 몇 분 안에 돌아가면, 사람의 QA 시간을 크게 줄여요.
 - 문서화와 실행 가능한 명세: “이 함수는 이렇게 동작해야 한다”는 기대를 코드(예: 
it("총액은 배송료를 포함한다"))로 남깁니다. 새 동료의 온보딩이 쉬워져요. - 리팩터링의 안전망: 동작을 보장하는 안전망이 있으니, 구조 개선을 과감하게 진행할 수 있습니다. “깨지면 테스트가 알려줄 것”이라는 심리적 안전도 큽니다.
 - 운영 리스크와 비용 절감: 운영 중 장애, 롤백, 고객 이탈 같은 비용 폭탄을 미리 줄입니다. 버그는 늦게 발견될수록 수정 비용이 기하급수적으로 커집니다.
 - 협업과 합의의 기준: 제품/기획/QA/개발 간 “기대 동작”을 테스트로 합의하면, 해석 차이로 생기는 불필요한 논쟁이 줄어듭니다.
 - 규정·컴플라이언스 대응: 금융·의료처럼 변경 검증이 필요한 도메인에서, 테스트 결과는 변경 이력과 증빙이 됩니다.
 
단위테스트 vs 통합테스트
단위 테스트(Unit Test)와 통합 테스트(Integration Test)는 소프트웨어 테스트의 두 가지 중요한 유형으로, 목적과 범위가 다릅니다.
단위 테스트(Unit Test)
- 정의: 프로그램의 가장 작은 단위(보통 함수, 메서드, 클래스 등)를 독립적으로 테스트하는 것.
 - 목적: 각 단위가 예상한 대로 동작하는지 검증.
 - 특징:
- 보통 개발자가 직접 작성.
 - 실행 속도가 빠르고 자동화하기 좋음.
 - Mock(가짜 객체)나 Stub을 사용해 외부 의존성을 최소화.
 
 - 예시:
add(2, 3)함수를 테스트해서 결과가5인지 확인.- DB나 API 호출을 실제로 하지 않고, 가짜 객체로 대체.
 
 
통합 테스트(Integration Test)
- 정의: 여러 단위(모듈, 클래스, 시스템 구성 요소)가 서로 올바르게 연동되는지 테스트하는 것.
 - 목적: 단위들이 결합되었을 때 전체 동작이 문제없이 작동하는지 확인.
 - 특징:
- 실제 데이터베이스, 네트워크, 파일시스템 등 외부 리소스를 사용할 수도 있음.
 - 단위 테스트보다 실행 시간이 오래 걸림.
 - 시스템의 상호작용 및 흐름을 검증하는 데 적합.
 
 - 예시:
- 사용자 회원가입 기능 테스트 → DB에 저장, 이메일 발송 API 호출까지 실제 동작 확인.
 - 여러 모듈이 연결된 “로그인 → 대시보드 이동” 시나리오 테스트.
 
 
⚖️ 비교 정리
| 구분 | 단위 테스트 (Unit Test) | 통합 테스트 (Integration Test) | 
|---|---|---|
| 범위 | 함수, 클래스 등 최소 단위 | 여러 모듈, 시스템 구성 요소 결합 | 
| 목적 | 각 단위의 올바른 동작 검증 | 모듈 간 상호작용/흐름 검증 | 
| 속도 | 빠름 | 상대적으로 느림 | 
| 의존성 처리 | Mock, Stub으로 대체 | 실제 리소스나 시스템 사용 가능 | 
| 작성 주체 | 주로 개발자 | 개발자, QA 모두 작성 | 
👉 정리하면, 단위 테스트는 작은 블록이 제대로 작동하는지 확인하고, 통합 테스트는 블록들이 합쳐져서 제대로 작동하는지 확인하는 거예요.
싱글톤 패턴을 직접 구현하는것 vs 스프링을 통해 싱글톤 사용
- 직접 싱글톤 패턴 구현
 
특징
- 개발자가 직접 구현: 보통 private 생성자, static 메서드(
getInstance())를 통해 객체 생성을 제한합니다. - 제어권: 객체 생명주기를 코드에서 직접 관리해야 합니다.
 - 단순성: 작은 프로젝트나 간단한 목적에는 충분합니다.
 - 테스트 어려움: 전역 상태처럼 동작하기 때문에, 의존성 주입이 어렵고 Mocking/단위 테스트가 까다롭습니다.
 - 멀티스레드 문제: 동기화 처리(lock, double-checked locking 등)를 직접 구현해야 안전합니다.
 
- 스프링 컨테이너를 통한 싱글톤
 
스프링 컨테이너는 싱글톤을 관리하고, 공급하는 싱글톤 관리 컨테이너이기도 하다. 따라서 IoC 컨테이너 외에도 싱글톤 레지스트리라고도 부른다.
특징
- 컨테이너 관리: 스프링 IoC 컨테이너(ApplicationContext)가 기본적으로 빈을 싱글톤 스코프로 관리합니다.
 - 생명주기 관리: 생성 → 의존성 주입 → 초기화 → 소멸 과정을 컨테이너가 알아서 관리합니다.
 - 스레드 안전 보장: 특별히 동기화 처리를 하지 않아도, 스프링이 멀티스레드 환경에서 안전하게 싱글톤 빈을 제공합니다.
 - 유연한 설정: 프로토타입, request, session 스코프 등 필요에 따라 다른 범위를 선택 가능.
 - 테스트 용이성: 의존성 주입(DI)으로 Mock 객체 주입이 가능해 단위 테스트가 쉬워집니다.
 - 부가 기능: AOP, 트랜잭션 관리, 라이프사이클 콜백 등 싱글톤 객체에 추가 기능을 쉽게 적용할 수 있습니다.
 
- 핵심 차이 요약
 
| 구분 | 직접 구현 싱글톤 | 스프링 싱글톤 | 
|---|---|---|
| 생성/관리 주체 | 개발자 코드 | 스프링 컨테이너 | 
| 스레드 안전성 | 직접 구현 필요 | 스프링이 보장 | 
| 테스트 | 어렵다 | DI로 용이 | 
| 유연성 | 전역적, 변경 어려움 | 다양한 스코프 제공 | 
| 부가 기능 | 없음 | AOP, 트랜잭션 등 지원 | 
👉 정리하면, 직접 구현한 싱글톤은 “단순히 인스턴스 하나만 만들기”에 초점이 있고, 스프링의 싱글톤은 “애플리케이션 전반의 객체를 컨테이너가 관리하면서, 유연성과 부가 기능까지 제공”하는 점에서 차이가 있습니다.
Spring의 라이프사이클(Spring Bean Life Cycle)은 Spring 컨테이너가 Bean 객체를 생성, 초기화, 사용, 소멸하는 전체 과정을 의미합니다. 이 라이프사이클을 이해하면 Bean의 생명 주기를 제어하고 필요한 시점에 원하는 작업을 수행할 수 있습니다.
🌱 전체 Spring Bean Life Cycle 흐름 요약
1
2
3
4
5
6
7
8
1. Spring 컨테이너 초기화
2. Bean 인스턴스 생성
3. 의존성 주입 (DI)
4. Bean 이름 설정 및 BeanFactory 설정
5. 초기화 콜백 (@PostConstruct, InitializingBean 등)
6. Bean 사용
7. 소멸 콜백 (@PreDestroy, DisposableBean 등)
8. 컨테이너 종료
🔁 상세 순서와 각 단계 설명
| 단계 | 설명 | 관련 인터페이스 / 애노테이션 | 
|---|---|---|
| 1. 객체 생성 | Bean 객체가 생성됨 (싱글톤이면 컨테이너 시작 시) | 생성자 | 
| 2. 의존성 주입 | @Autowired, @Value 등을 통한 DI 수행 | 필드, 생성자, Setter | 
| 3. Aware 설정 | Bean 이름, 컨텍스트 등 주입됨 | BeanNameAware, BeanFactoryAware, ApplicationContextAware | 
| 4. 초기화 콜백 | 초기화 로직 수행 | @PostConstruct, InitializingBean.afterPropertiesSet(), init-method | 
| 5. 사용 | 애플리케이션에서 Bean 사용 | 일반 서비스 로직 | 
| 6. 소멸 콜백 | 컨테이너 종료 시 자원 해제 등 수행 | @PreDestroy, DisposableBean.destroy(), destroy-method | 
| 7. GC 대상 | 컨테이너가 참조 제거 후 GC 대상이 됨 | JVM 수준 | 
🛠 주요 라이프사이클 인터페이스와 애노테이션
✅ 초기화 관련
@PostConstruct→ Bean 초기화 직후 실행InitializingBean.afterPropertiesSet()→ 모든 프로퍼티 주입 후 호출init-method→ XML 또는@Bean(initMethod="...")방식 사용
🧹 소멸 관련
@PreDestroy→ 컨테이너 종료 직전 호출DisposableBean.destroy()→ Bean 소멸 시 실행destroy-method→ XML 또는@Bean(destroyMethod="...")지정
🧪 예시 코드 (Java Config)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class MyBean implements InitializingBean, DisposableBean {
    @PostConstruct
    public void postConstruct() {
        System.out.println("PostConstruct 호출됨");
    }
    @Override
    public void afterPropertiesSet() {
        System.out.println("InitializingBean.afterPropertiesSet 호출됨");
    }
    @PreDestroy
    public void preDestroy() {
        System.out.println("PreDestroy 호출됨");
    }
    @Override
    public void destroy() {
        System.out.println("DisposableBean.destroy 호출됨");
    }
}
✅ ApplicationContext 종료 시 주의사항
스프링 부트에서는 기본적으로 ApplicationContext가 종료되며 @PreDestroy, destroy()가 호출됩니다. 하지만 다음과 같은 상황에서는 소멸 콜백이 호출되지 않을 수 있음:
- 강제 종료 (
System.exit(0)) - 비정상 예외 종료
 - Non-bean 객체 사용
 
→ 이를 위해 ApplicationContext.registerShutdownHook() 수동 호출도 고려 가능
📌 요약 포인트
- Bean 생명 주기는 생성 → DI → 초기화 → 사용 → 소멸
 - 초기화/소멸 시점에 필요한 작업은 
@PostConstruct,@PreDestroy, 인터페이스 등을 통해 구현 - 애플리케이션 성능 최적화 및 자원 관리를 위해 중요
 
좋은 포인트예요 👍 Spring 프레임워크라는 맥락을 걷어내고, 일반적인 컴퓨팅/소프트웨어 아키텍처 관점에서 스케줄러와 배치의 차이를 정리해드릴게요.
1. 개념 차이
스케줄러(Scheduler)
- “언제 실행할지”를 결정하는 도구
 - 시간이나 주기를 기준으로 특정 작업을 실행하도록 관리
 - 예: 
cron(리눅스), Windows Task Scheduler, Airflow의 DAG 스케줄링 등 - 역할: 트리거(trigger)
- 매일 0시 실행
 - 매주 월요일 9시 실행
 - 5분마다 실행
 
 
배치(Batch)
- “무엇을 실행할지”를 정의하는 처리 방식
 - 대량의 데이터를 모아 한꺼번에 처리하는 작업 단위
 - 보통 사람이 개입하지 않고 백그라운드에서 실행
 - 예: 급여 정산, 카드 결제 청구, 로그 집계, 데이터 마이그레이션
 - 역할: 처리 로직(execution)
- 100만 건 로그 집계
 - 월급 산정 후 송금
 - 정기 리포트 생성
 
 
2. 역할 분담
| 구분 | 스케줄러 | 배치 | 
|---|---|---|
| 핵심 질문 | “언제 실행할까?” | “무엇을, 어떻게 실행할까?” | 
| 중점 | 실행 시점·주기 관리 | 대량 처리 로직 설계·실행 | 
| 예시 도구 | cron, Quartz, Airflow, Kubernetes CronJob | Spark Batch, Spring Batch, Hadoop MapReduce, Shell Script Batch | 
| 사용 맥락 | “매일 새벽 2시에 실행시켜 줘” | “고객 100만 명의 정산 데이터를 처리해” | 
| 재시작/이력 관리 | 단순히 다시 실행 | 실패·성공 이력, 체크포인트, 재처리 기능 포함 가능 | 
3. 실제 시스템에서의 관계
실제 현업에서는 스케줄러가 배치 실행을 트리거하는 방식이 일반적입니다.
- 스케줄러: “새벽 3시다, 실행해!”
 - 배치: “알겠습니다, 고객 정산 100만 건을 처리합니다.”
 
즉, 스케줄러는 알람 시계, 배치는 일하는 사람에 비유할 수 있어요. 알람 시계는 언제 일어날지 알려줄 뿐이고, 실제 일을 하는 건 사람(=배치)입니다.
4. 예시 시나리오
- 급여 지급 시스템
- 스케줄러: 매월 25일 0시에 “급여 지급 배치 실행” 트리거
 - 배치: 모든 직원의 근태·성과 데이터를 읽고 → 급여 계산 → 은행 송금 요청
 
 - 로그 데이터 처리
- 스케줄러: 매일 새벽 1시에 배치 실행
 - 배치: 하루 동안 쌓인 수십 GB 로그 파일을 모아 분석하고 집계
 
 
✅ 정리
- 스케줄러 = 실행 시점 제어(시간표 담당)
 - 배치 = 대량 처리 로직(작업자 담당)
 - 둘은 독립적 개념이지만, 실무에서는 보통 스케줄러가 배치를 실행하는 구조
 
혹시 원할까 해서 여쭤봐요 👉 “그럼 현대 시스템(예: Airflow, Kubernetes CronJob, Spark)에서 스케줄러와 배치가 어떻게 조합되는지”까지 사례 중심으로 정리해드릴까요?
@Scheduled는 Spring Framework에서 제공하는 어노테이션으로, 메서드를 주기적으로 실행하도록 스케줄링할 때 사용합니다. 배치 작업, 정기적인 데이터 처리, 캐시 갱신, 로그 정리, 알림 발송 같은 자동화 작업에서 많이 활용됩니다.
아래에서 상세히 정리해드릴게요.
1. 기본 사용 방법
@EnableScheduling을@Configuration클래스에 추가하여 스케줄링 기능을 활성화해야 합니다.@Scheduled어노테이션은@Component또는@Service등 Spring Bean에 등록된 클래스의 메서드에 붙입니다.
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableScheduling
public class SchedulerConfig {
}
@Component
public class MyScheduler {
    @Scheduled(fixedRate = 5000)  // 5초마다 실행
    public void runTask() {
        System.out.println("작업 실행: " + LocalDateTime.now());
    }
}
2. 주요 속성
① fixedRate
- 메서드 실행 시작 시점 기준으로 일정 간격마다 실행
 - 단위: 밀리초(ms)
 
1
@Scheduled(fixedRate = 10000) // 10초마다 실행
② fixedDelay
- 메서드 실행 종료 시점 기준으로 일정 간격마다 실행
 - 실행 시간이 변동될 때 
fixedRate보다 유연함 
1
@Scheduled(fixedDelay = 10000) // 이전 실행이 끝난 후 10초 뒤에 실행
③ initialDelay
- 애플리케이션 시작 직후 바로 실행되는 것을 방지하고, 지정된 시간(ms) 이후 첫 실행
 fixedRate,fixedDelay와 함께 사용
1
@Scheduled(fixedDelay = 5000, initialDelay = 2000)
④ cron
- Cron 표현식을 사용하여 정교한 실행 주기 지정 가능
 - 형식: 
초 분 시 일 월 요일 [년도]- 예) 
"0 0 * * * *"→ 매 정시마다 실행 
 - 예) 
 
1
2
@Scheduled(cron = "0 0 9 * * MON-FRI") 
// 평일 오전 9시에 실행
3. Cron 표현식 예시
| 표현식 | 의미 | 
|---|---|
"0 0/5 * * * *" | 매 5분마다 실행 | 
"0 0 0 * * *" | 매일 자정에 실행 | 
"0 30 9 * * 1-5" | 평일 오전 9시 30분 실행 | 
"*/10 * * * * *" | 10초마다 실행 | 
4. 주의 사항
- Thread Pool 관리
- 기본적으로 단일 스레드로 실행됨 → 여러 작업이 겹치면 지연 발생 가능
 @EnableScheduling대신SchedulingConfigurer를 구현해서TaskScheduler커스터마이징 가능
 - 예외 처리
- 예외가 발생하면 해당 스케줄은 더 이상 실행되지 않을 수 있음 → try-catch로 예외 처리 필요
 
 - 테스트 환경
- 단위 테스트 시 자동 실행을 피하려면 
@Profile을 사용해 특정 환경에서만 스케줄링 활성화 가능 
 - 단위 테스트 시 자동 실행을 피하려면 
 
👉 혹시 원하시는 게 @Scheduled의 실무 활용 패턴(예: 멀티 스레드 실행, 동적 스케줄링 변경, 분산 환경에서의 주의점)까지 포함해서 더 깊이 들어갈까요?
좋은 질문이에요! 😊 Spring에서 의존성 주입(Dependency Injection, DI)을 할 때 @PostConstruct와 Lombok의 @RequiredArgsConstructor를 사용하는 방식은 큰 차이가 있습니다. 하나씩 비교해 드릴게요.
🔹 @PostConstruct 기반 DI
동작 방식
- 의존성을 
@Autowired또는@Resource등으로 주입받은 뒤, Bean 초기화가 끝난 시점에 추가 초기화 로직을 실행할 수 있습니다. - 즉, 생성자나 세터로 의존성이 주입된 후, 
@PostConstruct가 붙은 메서드가 실행됩니다. 
- 의존성을 
 사용 예시
1 2 3 4 5 6 7 8 9 10
@Component public class MyService { @Autowired private final SomeDependency dependency; @PostConstruct public void init() { dependency.prepare(); // 의존성 주입 후 초기화 작업 } }
특징
- Bean 생명주기 콜백에 의존.
 - “주입 이후 초기화”라는 시점을 보장.
 - 필드 주입과 함께 자주 쓰였으나, 최근에는 테스트 용이성 저하 + 순환 참조 문제 때문에 권장되지 않음.
 
🔹 @RequiredArgsConstructor 기반 DI
동작 방식
- Lombok이 
final필드나@NonNull필드를 대상으로 생성자를 자동 생성합니다. - Spring은 생성자가 하나뿐이면 자동으로 의존성을 주입해 줍니다.
 
- Lombok이 
 사용 예시
1 2 3 4 5 6 7 8 9
@Service @RequiredArgsConstructor public class MyService { private final SomeDependency dependency; public void doSomething() { dependency.work(); } }
특징
- 생성자 주입 방식을 깔끔하게 표현.
 - 불변성(
final) 보장 → 의존성 누락 방지. - 테스트 및 유지보수 용이 (필드가 늘어나도 컴파일 타임에 오류 발견 가능).
 - 순환 참조 방지에 유리.
 
🔸 비교 요약
| 구분 | @PostConstruct DI | @RequiredArgsConstructor DI | 
|---|---|---|
| 시점 | Bean 초기화 후 실행 | 객체 생성 시점에서 주입 | 
| 주입 방식 | 보통 필드 주입 + 초기화 로직 | 생성자 주입 (Lombok 자동 생성) | 
| 장점 | 초기화 로직 처리 가능 | 불변성 보장, 테스트 용이, 깔끔한 코드 | 
| 단점 | 순환 참조·테스트 불편 | 초기화 로직 필요 시 따로 메서드 작성 필요 | 
👉 정리하면:
- 일반적인 DI → 
@RequiredArgsConstructor(권장) - 주입 후 추가 설정 필요 → 
@PostConstruct(특수 상황에서만) 
원하시면, 제가 실무에서 언제 @PostConstruct를 꼭 써야 하는 경우와 대체 가능한 패턴도 정리해 드릴까요?