config
package com.codeit.bean.config;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Configurer {
private String option;
}
역할
- 단 하나의 설정값(option)을 갖는 단순한 설정 객체
- 외부 설정 바인딩, 내부 옵션 전달 등에 유용하게 사용 가능
MyConfig
package com.codeit.bean.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyConfig {
@Bean
public Configurer myConfigurer(){
Configurer configurer = new Configurer();
configurer.setOption("option");
System.out.println("생성 : " + configurer);
return configurer;
}
}
등록된 Bean 사용 예시
@Service
@RequiredArgsConstructor
public class SomeService {
private final Configurer configurer;
public void printConfig() {
System.out.println("현재 옵션: " + configurer.getOption());
}
}
MyConfig2
@Configuration
public class MyConfig2 {
@Bean
public Configurer dbConfigurer() {
Configurer configurer = new Configurer();
configurer.setOption("DB 설정 값");
return configurer;
}
@Bean
public Configurer apiConfigurer() {
Configurer configurer = new Configurer();
configurer.setOption("API 설정 값");
return configurer;
}
@Bean
public Configurer combiConfigurer(Configurer dbConfigurer,
Configurer apiConfigurer) {
Configurer configurer = new Configurer();
configurer.setOption(dbConfigurer.getOption() + ", " + apiConfigurer.getOption());
return configurer;
}
}
핵심 개념 요약
| 항목 | 설명 |
|---|
@Bean | 반환 객체를 Bean으로 등록함 |
| Bean 이름 | 메서드 이름이 Bean ID가 됨 (dbConfigurer, apiConfigurer, ...) |
| 동일 타입 Bean 주입 | 메서드 이름으로 구분하여 주입 가능 |
| 조합 Bean | 다른 Bean들을 파라미터로 받아 새로운 Bean 구성 가능 |
출력 결과 예시 (실행 시)
생성 : Configurer(option=DB 설정 값)
생성 : Configurer(option=API 설정 값)
생성 : Configurer(option=DB 설정 값, API 설정 값)
요약
- 같은 타입의 Bean을 여러 개 등록할 수 있다.
- 이때 Bean의 식별은 메서드명 기반으로 진행된다.
- 다른 Bean들을 조합해서 새로운 Bean을 생성하는 것도 가능하다.
- 타입 충돌 걱정 없이 설정 조합 가능
MyConfig3
@Configuration
public class MyConfig3 {
@Bean("mainDB")
public Configurer maindbConfigurer() {
Configurer configurer = new Configurer();
configurer.setOption("Main DB 설정 값");
return configurer;
}
@Bean(name = "subDB")
public Configurer subdbConfigurer() {
Configurer configurer = new Configurer();
configurer.setOption("Sub DB 설정 값");
return configurer;
}
@Bean(name = {"combi", "dual"})
public Configurer combiConfigurer2(@Qualifier("mainDB") Configurer maindbConfigurer,
@Qualifier("subDB") Configurer subdbConfigurer)
핵심 개념 정리
| 개념 | 설명 |
|---|
@Bean("mainDB") | 해당 Bean의 이름을 "mainDB"로 지정 |
@Bean(name = "subDB") | 이름 지정 방식은 동일 |
@Bean(name = {"combi", "dual"}) | 한 개의 Bean에 여러 별칭 부여 가능 |
@Qualifier("mainDB") | 이름이 "mainDB"인 Bean을 주입하라는 의미 |
@Qualifier가 필요한 이유
- 타입이 같은 Bean이 2개 이상 존재할 경우, 어떤 Bean을 주입할지 모호해짐
- 이때
@Qualifier로 Bean 이름을 직접 지정해 주입 대상을 명확히 설정할 수 있음
@Qualifier("mainDB") Configurer configurer
@Autowired + @Qualifier 조합은 많이 사용됨
사용 예시
@Service
@RequiredArgsConstructor
public class DatabaseRouter {
@Qualifier("mainDB")
private final Configurer mainDb;
@Qualifier("subDB")
private final Configurer subDb;
}
정리
| 항목 | 설명 |
|---|
@Bean(name = "...") | Bean 이름 커스터마이징 가능 |
@Qualifier("...") | Bean 주입 시 이름으로 정확히 지정 |
| 중복된 타입 문제 해결 | 타입이 같은 Bean이 여럿 있을 때 충돌 방지 가능 |
다중 이름 (@Bean(name = {"a", "b"})) | 여러 이름으로 하나의 Bean 등록 가능 |
ScanConfig
@Configuration
@ComponentScan(
basePackages = "com.common",
excludeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = CommonBean.class
)
)
public class ScanConfig {
@Bean
public Configurer commonConfigurer(Optional<CommonBean> commonBean) {
Configurer configurer = new Configurer();
CommonBean commonBean1 = commonBean.orElse(new CommonBean("비었습니다."));
configurer.setOption("CommonBean : " + commonBean1);
return configurer;
}
@Bean
public Configurer testConfigurer(TestBean testBean) {
Configurer configurer = new Configurer();
configurer.setOption("test name : " + testBean.getName());
return configurer;
}
}
주요 개념 정리
| 항목 | 설명 |
|---|
@ComponentScan | 지정된 패키지 내의 @Component, @Service 등 자동 탐지 |
basePackages | 스캔 대상 지정 |
excludeFilters | 특정 클래스를 스캔 대상에서 제외 (예: CommonBean) |
@Bean | Java Config 방식으로 Bean 등록 |
Optional<Bean> | 주입 대상이 없어도 NPE 발생 방지 가능 |
Optional<CommonBean> 사용 이유
@Bean
public Configurer commonConfigurer(Optional<CommonBean> commonBean)
CommonBean이 컴포넌트 스캔 제외되어도 예외 없이 처리 가능
orElse()로 대체 객체 지정 가능 → 유연한 Bean 의존성 처리 가능
@ComponentScan.Filter
@ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = CommonBean.class
)
- 특정 타입(
CommonBean)을 스캔 대상에서 제외하려는 경우 사용
- 이 덕분에
CommonBean은 Spring이 관리하지 않음 → 직접 생성 or Optional 활용
출력 예시 (실행 시)
Configurer(option=CommonBean : com.common.bean.CommonBean@xxxxx)
Configurer(option=test name : [TestBean에서 주입된 이름])
controller
ConfigController
package com.codeit.bean.controller;
import com.codeit.bean.config.Configurer;
import com.common.bean.SetBean;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequiredArgsConstructor
public class ConfigController {
{
System.out.println("ConfigController 생성되었습니다.");
}
private final Configurer myConfigurer;
private final Configurer dbConfigurer;
private final Configurer apiConfigurer;
private final Configurer combiConfigurer;
@Autowired
@Qualifier("mainDB")
private Configurer maindbConfigurer;
private Configurer subdbConfigurer;
private Configurer combiConfigurer2;
private final Configurer commonConfigurer;
private final Configurer testConfigurer;
@Autowired
@Qualifier("subDB")
public void setSubdbConfigurer(Configurer subdbConfigurer) {
this.subdbConfigurer = subdbConfigurer;
}
@Autowired
@Qualifier("combi")
public void setCombiConfigurer2(Configurer combiConfigurer2) {
this.combiConfigurer2 = combiConfigurer2;
}
@GetMapping("/config/allBean")
public Map<String, Object> allBean(){
return Map.of("myConfigurer",myConfigurer,
"dbConfigurer",dbConfigurer,
"apiConfigurer",apiConfigurer,
"combiConfigurer",combiConfigurer,
"maindbConfigurer",maindbConfigurer,
"subdbConfigurer",subdbConfigurer,
"combiConfigurer2",combiConfigurer2
);
}
@Autowired
SetBean setBean;
@GetMapping("/config/scanBean")
public Map<String, Object> scanBean(){
return Map.of("commonConfigurer",commonConfigurer,
"testConfigurer", testConfigurer,
"setBean", setBean
);
}
}
| 항목 | 설명 |
|---|
@RequiredArgsConstructor | final 필드 생성자 자동 생성 |
@Autowired + @Qualifier | 동일 타입 Bean 주입 시 이름으로 지정 |
@ComponentScan + @Bean | SetBean, CommonBean 등 스캔된 Bean 주입 가능 |
@GetMapping | 등록된 Bean 확인용 테스트 API 제공 (/config/allBean, /config/scanBean) |
테스트 결과 (예상 JSON 응답 구조)
{
"myConfigurer": {
"option": "option"
},
"dbConfigurer": {
"option": "DB 설정 값"
},
"apiConfigurer": {
"option": "API 설정 값"
},
"combiConfigurer": {
"option": "DB 설정 값, API 설정 값"
},
"maindbConfigurer": {
"option": "Main DB 설정 값"
},
"subdbConfigurer": {
"option": "Sub DB 설정 값"
},
"combiConfigurer2": {
"option": "Main DB 설정 값, Sub DB 설정 값"
}
}
DiController
@RestController
public class DiController {
@Autowired
@Qualifier("kakaoPaymentService")
private PaymentService kakaoPaymentService;
private final PaymentService naverPaymentService;
public DiController(@Qualifier("naverPaymentService") PaymentService service){
this.naverPaymentService = service;
}
private PaymentService tossPaymentService;
@Autowired
@Qualifier("tossPaymentService")
public void setTossPaymentService(@Nullable PaymentService tossPaymentService) {
if(tossPaymentService == null){
tossPaymentService = new TossPaymentService();
}
this.tossPaymentService = tossPaymentService;
}
public void setTossPaymentService2(Optional<PaymentService> tossPaymentService) {
this.tossPaymentService = tossPaymentService.orElseGet(TossPaymentService::new);
}
@Autowired
private CustomerService customerService;
@Autowired
private OrderService orderService;
@GetMapping("/pay/kakao")
public String payWithKakao(int amount) {
return kakaoPaymentService.pay(amount);
}
@GetMapping("/pay/naver")
public String payWithNaver(int amount) {
return naverPaymentService.pay(amount);
}
@GetMapping("/pay/toss")
public String payWithToss(int amount) {
return tossPaymentService.pay(amount);
}
@GetMapping("/order")
public String order(String product, int amount) {
String result1 = orderSer
순환 참조 문제
CustomerService → OrderService, OrderService → CustomerService 식으로 상호 참조
- 에러 발생: Spring은 생성자 주입 방식에서 순환 참조를 감지하면 애플리케이션을 실행하지 않음
- 해결 방법:
@Lazy 사용 (한쪽을 지연 초기화)
- setter 주입 방식으로 전환
@Autowired
@Lazy
private CustomerService customerService;
LifeCycleController
@RestController
@RequiredArgsConstructor
public class LifeCycleController {
private final MyBean myBean;
private final PrototypeBean prototypeBean;
private final SingletonBean singletonBean;
@GetMapping("/test-singleton")
public String testSingleton() {
return singletonBean.message() + "<br>" + myBean.message();
}
@GetMapping("/test-prototype")
public String testPrototype() {
return prototypeBean.message();
}
}
| Bean 범위 | 설명 | 생성 시점 |
|---|
@Singleton (기본) | 한 번만 생성되어 재사용됨 | 컨테이너 초기화 시 |
@Prototype | 요청할 때마다 새로 생성 | Bean을 요청하는 시점마다 |
테스트 설명
/test-singleton
singletonBean 내부에서 prototypeBean을 매번 요청하게 구현돼 있다면
- 요청할 때마다
prototypeBean의 인스턴스가 새로 생성되어 출력 값이 매번 달라질 수 있음
/test-prototype
- 컨트롤러 생성 시 한 번 주입된
prototypeBean을 사용하므로
- 요청을 반복해도 같은 인스턴스의 message() 결과가 출력됨
실습 포인트
- 프로토타입을 싱글톤에 주입하면 항상 같은 인스턴스를 사용하게 됨
- 이를 해결하려면 Provider 또는 ObjectFactory로 매번 새로 요청해야 함
@Component
@Scope("singleton")
public class SingletonBean {
@Autowired
private ObjectProvider<PrototypeBean> prototypeProvider;
public String message() {
return "싱글톤에서 프로토타입 사용 → " + prototypeProvider.getObject().message();
}
}
SettingController
@RestController
@RequiredArgsConstructor
public class SettingController {
@Value("Hello Java World!")
private String hello;
@Value("${app.name}")
private String appName;
@Value("${spring.profiles.active}")
private String activeMode;
@Value("#{5 * 10}")
private int number;
@Value("#{systemProperties['os.name']}")
private String osName;
@Value("#{systemProperties['os.name'].toLowerCase().contains('win') ? 'c:\\dev' : '/user/dev'}")
private String path;
@Value("#{someBean.someProperty}")
private String someProperty;
@Value("${app.description:Default App Description}")
private String description;
@Value("${server.port:8080}")
private int port;
@Value("${feature.enabled:false}")
private boolean featureEnabled;
private final AppProperties appProperties;
@Value("#{appProperties.name == 'MySpringApp' ? 'MySpringApp 입니다.' : 'MySpringApp 아닙니다'}")
private String value;
private final String serverUrl;
private MyService myService;
@Autowired
public void setMyService(MyService myService) {
this.myService = myService;
}
@GetMapping("/settings")
public String getSettings() {
return String.format("appName : %s<br>activeMode : %s<br>number : %d<br>osName : %s<br>path : %s<br>description : %s<br>port : %d<br>featureEnabled : %s<br> value : %s"
, appName, activeMode, number, osName, path, description, port, featureEnabled, value);
}
@GetMapping("/app-properties")
public AppProperties getAppProperties() {
return appProperties;
}
@GetMapping("/profile-server-url")
public String getProfileServerUrl() {
return serverUrl;
}
@GetMapping("/condition")
public String condition() {
return myService == null ? "null 입니다" : myService.getString();
}
}
| 항목 | 설명 |
|---|
@Value | 문자열, YAML 설정값, SpEL 등 다양한 형태로 주입 가능 |
@Value("${property:default}") | 속성이 없을 경우 기본값 지정 가능 |
SpEL | 수식, 삼항연산자, 시스템 속성, Bean 참조 등 동적 표현 지원 |
@ConfigurationProperties | yml 파일을 자바 객체로 바인딩 |
@Profile | 프로파일 별 Bean 또는 설정 분기 |
@Conditional | 조건에 따라 Bean 등록 제어 |
Optional, @Nullable | 의존성 주입 실패를 안전하게 처리하는 방법 |
테스트 API
| 경로 | 설명 |
|---|
/settings | 모든 설정값 요약 보기 |
/app-properties | AppProperties 객체 그대로 반환 |
/profile-server-url | 현재 profile에 따른 서버 URL |
/condition | 조건부 Bean 주입 테스트 (MyService 존재 여부) |
실무 팁
@Value는 간단한 설정 주입에 유용하지만, 복잡한 설정은 @ConfigurationProperties로
SpEL은 적당히만 사용할 것 (가독성 저하 주의)
@Profile, @Conditional은 운영/개발 환경 분리에 필수