Spring이 Singleton·FactoryBean·Proxy AOP로 패턴을 구현하는 방식

Spring 컨테이너의 기본 스코프인 Singleton, 복잡한 생성을 캡슐화하는 FactoryBean, 횡단 관심사를 분리하는 Proxy 기반 AOP가 각각 어떤 디자인 패턴을 구현하는지 코드로 정리한다.

2026-08-13 · 최초 발행 2025-10-14

Spring 프레임워크는 IoC 컨테이너와 AOP를 중심으로 여러 디자인 패턴을 내장 적용하는 플랫폼이다. Bean 하나를 등록하는 순간부터 Singleton 패턴이 작동하고, @Transactional 하나를 붙이는 순간 Proxy 패턴이 개입한다 — 의식하지 않고 쓰는 패턴이 대부분이라는 뜻이다. 이 글은 Singleton 기반 Bean 관리, FactoryBean, Proxy 기반 AOP의 원리·절차·활용을 실무 중심으로 정리한다.

Spring 컨테이너가 내장한 메커니즘

Singleton은 Bean 관리 스코프로 구현된다. Spring 컨테이너의 기본 스코프가 싱글톤이라, 컨테이너 당 인스턴스 하나를 생성·캐시·주입한다. 상태 없는(Stateless) 서비스, 커넥션 풀, 클라이언트 SDK 같은 공용 리소스를 효율적으로 공유하는 데 쓴다.

FactoryBean은 Factory Method·Abstract Factory의 Spring식 구현이다. 복잡한 객체 생성 로직을 Bean 정의 외부로 추상화하는 팩토리 역할 빈이다. 컨테이너는 FactoryBean#getObject()의 반환값을 실제 빈으로 노출하고, 팩토리 자체에 접근하려면 & 프리픽스를 쓴다.

Proxy 기반 AOP는 Proxy·Decorator 패턴의 런타임 적용이다. JDK 동적 프록시(인터페이스 기반)나 CGLIB(클래스 서브클래싱)로 조인포인트에 어드바이스 체인을 적용한다. 트랜잭션, 보안, 로깅 같은 횡단 관심사를 서비스 코드와 분리해 모듈화한다.

컨테이너 라이프사이클과 프록시 체인

컨테이너 라이프사이클과 스코프에서는 싱글톤 캐시, 프로토타입(요청 시 매번 생성), 웹 스코프(request/session)를 제공한다. BeanPostProcessor를 통한 후처리, 순환 참조 탐지와 조기 레퍼런스 노출 메커니즘에 주의해야 한다.

FactoryBean 메커니즘은 getObject(), getObjectType(), isSingleton() 계약을 기반으로 한다. 지연 초기화와 캐싱 전략을 명확히 해야 하고, 외부 자원(SDK, 네이티브 핸들)의 초기화·해제 정책을 팩토리 내부에서 일관되게 관리해야 한다.

프록시·어드바이스 체인은 Before/After/AfterReturning/AfterThrowing/Around로 구성되며, 포인트컷으로 적용 범위를 제어한다. self-invocation, final 클래스·메서드, 프록시 노출(exposeProxy) 이슈는 설계·테스트 단계에서 반드시 짚고 넘어가야 한다.

스레드 안정성과 일관성 측면에서는 싱글톤 빈에 가변 공유 상태를 최소화하고 불변·스레드 안전 컬렉션을 쓰는 게 권장된다. 트랜잭션 경계(프록시)를 명확히 해야 일관성과 롤백 의미론이 확보된다.

요청이 컨테이너를 거치는 과정

아니오아니오컨테이너 시작BeanDefinition 로딩FactoryBean 여부FactoryBean 인스턴스화getObject() 호출대상 인스턴스화후처리/타겟 후보포인트컷 매칭프록시 생성 (JDK/CGLIB)타겟 그대로 사용의존성 주입/초기화 완료런타임 메서드 호출어드바이스 체인 실행타겟 호출 반환

메커니즘의 운영 특성

항목 Singleton Bean 관리 FactoryBean Proxy 기반 AOP
성능 생성 1회, 호출 O(1), 매우 저오버헤드 초기 생성 비용 흡수, 런타임 호출 저오버헤드 호출당 수 μs~수십 μs 오버헤드(어드바이스·매칭 수에 비례)
확장성 인스턴스 공유로 메모리 효율 복잡 객체 생성 분리로 구성 확장 용이 횡단 관심사의 수평 확장 용이
일관성 상태 공유 위험, 불변 설계 권장 생성 정책 일관성 보장 트랜잭션/보안 정책 일관 적용
안정성 스레드 안전성 확보 필요 외부 자원 예외 캡슐화 self-invocation/프록시 제한 주의
운영 편의 모니터링/메트릭 집중 관리 용이 설정·시크릿 관리 일원화 로깅/추적/정책 변경 무중단 반영 용이

싱글턴·프록시·템플릿을 Spring 코드로 보면

전제조건은 Java 17, Spring Boot 3.3+, Gradle·Maven 표준 구성이다. spring-boot-starter-aop를 쓰면 AOP가 자동 설정되지만, 순수 Spring에서는 @EnableAspectJAutoProxy가 필요하다.

공용 클라이언트·서비스 관리(Singleton)

비용이 큰 리소스(HTTP 클라이언트, SDK, Mapper)를 한 번만 생성해 재사용하는 것이 목적이다. 가변 상태를 제거하고 풀·타임아웃·커넥션 설정에 안전한 기본값을 두는 것이 체크포인트다.

// build.gradle (필요시)
dependencies {
  implementation("org.springframework.boot:spring-boot-starter-web")
}

// 공용 HTTP 클라이언트 (싱글톤)
@Configuration
public class HttpClientConfig {
  @Bean
  public WebClient webClient(WebClient.Builder builder) {
    return builder
        .baseUrl("https://api.example.com")
        .build(); // 기본 싱글톤 스코프
  }
}

@Service
public class UserService {
  private final WebClient webClient;
  public UserService(WebClient webClient) { this.webClient = webClient; }

  public Mono<String> getProfile(String id) {
    return webClient.get().uri("/users/{id}", id)
        .retrieve().bodyToMono(String.class);
  }
}

호출마다 상태를 분리해야 하는 객체만 @Scope("prototype")을 적용한다. 컨테이너가 수명을 관리하지 않으므로 수동 close가 필요할 수 있다는 점에 주의해야 한다.

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
class TaskContext {
  private final UUID id = UUID.randomUUID();
  UUID id() { return id; }
}

복잡한 생성 로직의 캡슐화(FactoryBean)

외부 SDK·네이티브 핸들 초기화, 시크릿·엔드포인트·옵션 조합을 팩토리로 은닉하는 것이 목적이다. isSingleton() 반환과 캐시 전략을 명확히 하고, 종료 훅(DisposableBean/SmartLifecycle)을 고려해야 한다.

// 외부 클라이언트 예시
public class CloudClient {
  private final String endpoint;
  private final String token;
  public CloudClient(String endpoint, String token) { this.endpoint = endpoint; this.token = token; }
  public String ping() { return "ok@" + endpoint; }
}

@Component("cloudClient")
public class CloudClientFactoryBean implements FactoryBean<CloudClient>, InitializingBean {
  @Value("${cloud.endpoint}") private String endpoint;
  @Value("${cloud.token}") private String token;

  private CloudClient client;

  @Override
  public void afterPropertiesSet() {
    // 유효성 검사 및 생성
    this.client = new CloudClient(endpoint, token);
  }

  @Override
  public CloudClient getObject() { return client; }

  @Override
  public Class<?> getObjectType() { return CloudClient.class; }

  @Override
  public boolean isSingleton() { return true; }
}

@Service
class CloudService {
  private final CloudClient client; // getObject() 결과 주입
  CloudService(CloudClient client) { this.client = client; }
  String health() { return client.ping(); }
}

// 팩토리 자체에 접근이 필요하면 & 프리픽스 사용 (예: 테스트)
@Autowired
private BeanFactory beanFactory;
// CloudClientFactoryBean factory = (CloudClientFactoryBean) beanFactory.getBean("&cloudClient");

application.yml

cloud:
  endpoint: https://cloud.example.com
  token: ${CLOUD_TOKEN}

로깅·트랜잭션·보안 분리(Proxy 기반 AOP)

서비스 코드에서 횡단 관심사를 제거하고 정책을 일관되게 적용하는 것이 목적이다. 인터페이스 유무에 따라 JDK와 CGLIB 중 선택하고, self-invocation을 피하며, 프록시 노출이 필요하면 exposeProxy=true를 쓴다.

// build.gradle
dependencies {
  implementation("org.springframework.boot:spring-boot-starter-aop")
  implementation("org.springframework.boot:spring-boot-starter")
  implementation("org.springframework.boot:spring-boot-starter-data-jpa") // 예시: 트랜잭션
}

// @EnableAspectJAutoProxy는 Boot AOP starter 사용 시 생략 가능
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)
class AopConfig {}

@Aspect
@Component
class LoggingAspect {
  @Around("execution(* com.example.demo..service..*(..))")
  public Object around(ProceedingJoinPoint pjp) throws Throwable {
    long start = System.nanoTime();
    try {
      return pjp.proceed();
    } finally {
      long elapsedUs = (System.nanoTime() - start) / 1_000;
      Signature sig = pjp.getSignature();
      System.out.printf("[AOP] %s.%s elapsed=%dus%n",
          sig.getDeclaringTypeName(), sig.getName(), elapsedUs);
    }
  }
}

@Service
class OrderService {
  @Transactional
  public Long place(String itemId) {
    // 도메인 로직 + JPA 작업
    return 1L;
  }

  // self-invocation 회피 예: AopContext 사용(권장 최소화)
  public void outer() {
    OrderService proxy = (OrderService) AopContext.currentProxy();
    proxy.place("A-1");
  }
}

프록시가 실제로 적용됐는지는 AopUtils.isAopProxy(bean)으로 확인하고, 내부 호출 바운더리는 반드시 테스트해야 한다.

Bean 스코프·FactoryBean·AOP를 쓸 때 감수할 것

Singleton은 불변 설계를 기본으로 하고 구성은 생성자 주입으로 받으며, 동시성 제약이 있는 라이브러리는 풀이나 동기화 래퍼를 쓴다. 다만 숨은 공유 상태로 결합이 늘어날 위험이 있어 TestConfiguration이나 컨텍스트 분리로 테스트 독립성을 확보해야 한다. AOP 기반 로깅·트레이싱·권한 검증을 도입하면 서비스 레이어 LOC가 5~15% 줄어드는 경험치가 있고, 정책 변경 시 코드 편집 범위가 최소화돼 회귀 리스크도 줄어든다.

FactoryBean은 유효성 검증·리트라이·시크릿 로딩을 팩토리에 캡슐화하고 isSingleton()·destroy 메커니즘을 명시하는 것이 모범사례다. 다만 추상화가 과하면 가시성이 떨어지고, 디버깅 시 & 접근이 필요한 등 학습 비용이 따른다. 싱글톤 공유로 객체 생성 비용이 절감되고 GC 압력이 완화되며, 고비용 SDK 초기화 횟수가 1회로 제한된다는 이점도 있다. FactoryBean은 외부 자원 초기화 실패를 조기에 탐지해 장애 격리를 쉽게 한다.

Proxy 기반 AOP는 좁은 포인트컷, 가벼운 어드바이스, @Transactional 같은 공용 메타애노테이션으로 정책을 표현하는 것이 모범사례다. self-invocation, final 클래스, 프록시 체인 중첩에 따른 오버헤드·복잡도가 트레이드오프이므로 성능 크리티컬 경로는 상황별로 측정 후 적용해야 한다. AOP 오버헤드는 어드바이스 수·포인트컷 복잡도에 비례해 CPU 바운드 경로에서 μs 단위로 증가하지만, 전체 처리량에 미치는 영향은 일반적으로 경미하다. 트랜잭션·보안 정책을 일괄 적용하면 운영 오류가 줄고 감사·추적 로그의 일관성도 확보된다.

SpringSingleton 패턴FactoryBeanProxy 패턴AOP