[강의] Spring MVC practice 2

Jerry·2025년 8월 6일

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;


// 무언가 설정하는 클래스 (DB, JPA, Log, Security ...)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration // 설정 파일임을 알리는 어노테이션 이자 Bean으로 관리
public class MyConfig {

    
    @Bean // 해당 메서드 이름으로 bean을 생성하는 어노테이션
    public Configurer myConfigurer(){ // bean 이름 : 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)
@BeanJava 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;

    // scan 관련 config
    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
        );
    }

    // ScanConfig에서 수행한 @ComponentScan(scan + bean 생성)을 통해 setBean을 다른곳에서 사용할수 있다.
    @Autowired
    SetBean setBean;

    @GetMapping("/config/scanBean")
    public Map<String, Object> scanBean(){
        return Map.of("commonConfigurer",commonConfigurer,
                        "testConfigurer", testConfigurer,
                        "setBean", setBean
                        );
    }
}
항목설명
@RequiredArgsConstructorfinal 필드 생성자 자동 생성
@Autowired + @Qualifier동일 타입 Bean 주입 시 이름으로 지정
@ComponentScan + @BeanSetBean, 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;
    }

    // setter 주입
    private PaymentService tossPaymentService;

    @Autowired
    @Qualifier("tossPaymentService")
    public void setTossPaymentService(@Nullable PaymentService tossPaymentService) {
        if(tossPaymentService == null){
            tossPaymentService = new TossPaymentService(); // fallback
        }
        this.tossPaymentService = tossPaymentService;
    }

    // Optional 주입
    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

순환 참조 문제

  • CustomerServiceOrderService, OrderServiceCustomerService 식으로 상호 참조
  • 에러 발생: 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을 요청하는 시점마다

테스트 설명

  1. /test-singleton
    • singletonBean 내부에서 prototypeBean을 매번 요청하게 구현돼 있다면
    • 요청할 때마다 prototypeBean의 인스턴스가 새로 생성되어 출력 값이 매번 달라질 수 있음
  2. /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}") // yml에서 설정값 읽기
    private String appName;

    @Value("${spring.profiles.active}")
    private String activeMode;

    @Value("#{5 * 10}") // SpEL (Spring Expression Language)
    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;

    // ConfigurationProperties 주입
    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 참조 등 동적 표현 지원
@ConfigurationPropertiesyml 파일을 자바 객체로 바인딩
@Profile프로파일 별 Bean 또는 설정 분기
@Conditional조건에 따라 Bean 등록 제어
Optional, @Nullable의존성 주입 실패를 안전하게 처리하는 방법

테스트 API

경로설명
/settings모든 설정값 요약 보기
/app-propertiesAppProperties 객체 그대로 반환
/profile-server-url현재 profile에 따른 서버 URL
/condition조건부 Bean 주입 테스트 (MyService 존재 여부)

실무 팁

  • @Value는 간단한 설정 주입에 유용하지만, 복잡한 설정은 @ConfigurationProperties
  • SpEL은 적당히만 사용할 것 (가독성 저하 주의)
  • @Profile, @Conditional은 운영/개발 환경 분리에 필수
profile
Backend engineer

0개의 댓글