Spring Bean 이름 중복 문제 해결(동일한 class 이름을 가지면서 다른 패키지에 존재)

박철현·2025년 6월 18일
0

문제해결

목록 보기
15/17
  • 다양한 프로젝트를 하다보면 이걸 왜..? 하는 경우도 많이 만나는 것 같다.
  • 이번 사례에서도 우연치않게 문제의 상황을 마주쳤다.
    • 사실상 두 Service를 하나로 합치고 두개의 메서드로 분리하는것이 좋을 것을 껄껄.. 하지만 그럴수 없는 상황이 오는 경우가 있다는 것..
    • 그럼 어쩌것나 해결해야지..

문제 상황

1. 패키지만 다르고 동일한 이름의 class를 Bean으로 등록하려 시도

// ────────────────────────────────────────────────────────────────────────
// 1) com.example.glasses.service 패키지
package com.example.glasses.service;

@Service
public class GlassesService {
    // ... 로직 생략
}

// ────────────────────────────────────────────────────────────────────────
// 2) com.example.sunglasses.service 패키지
package com.example.sunglasses.service;

@Service
public class GlassesService {
    // ... 로직 생략
}

2. 충돌 나는 컨트롤러 주입 시도

// com.example.glasses.controller
package com.example.glasses.controller;

import com.example.glasses.service.GlassesService;
import org.springframework.stereotype.Controller;
import lombok.RequiredArgsConstructor;

@Controller
@RequiredArgsConstructor
public class GlassesController {
    private final GlassesService glassesService;  // simple name 동일 → 충돌
}

-------------------------------------------------------------------

// com.example.sunglasses.controller
package com.example.sunglasses.controller;

import com.example.sunglasses.service.GlassesService;
import org.springframework.stereotype.Controller;
import lombok.RequiredArgsConstructor;

@Controller
@RequiredArgsConstructor
public class SunglassesController {
    private final GlassesService glassesService;  // simple name 동일 → 충돌
}

바로 문제 발생!

  • Spring에서는 Type 기반으로 Bean 주입이 일어난다.
    • 생성자 주입 시 아래와 같이 결국에는 해당 Class에 존재하는 속성에 맞는 객체를 넣어줘야 한다.
    • 그래서 SingleTon 객체도 당연히 Type 기반이다.
// 어디 패키지에 속한 컬럼을 SunglassesController가 가지는지 정의하고 있음
import com.example.sunglasses.service.GlassesService; 

@Controller
public class SunglassesController {
    private final GlassesService glassesService;
    
    // 주입 받아서 결국에는 SunglassesController 객체를 생성한다는 것 -> 
    // GlassesService의 명확한 package 경로가 필요하단것 (주입 받을 때 명확한 패키지(타입) 알아야 한다는 것) -> 
    // 그럼 결국 GlassesService bean 등록 자체도 특정 패키지(타입)에 맞춰 등록되어야 있어야 한다.
    public SunglassesController(GlassesService glassesService) {
    	this.glassesService = glassesService;
    }
}
  • 그러면, GlassesService의 Package가 다르고, 주입받는 속성이 다른데 왜 안되지?!

원인

  • Spring Bean Naming 규칙을 깜빡쓰..

Spring Bean 네이밍 & 등록 규칙

  • 기본 Bean 이름 : simple class name 규칙
    • Class 이름의 첫 글자만 소문자로 지정

com.example.glasses.service.GlassesService → "glassesService"
com.example.sunglasses.service.GlassesService → "glassesService"

  • Bean의 Name은 고유해야한다!
    • 중복 시 BeanDefinitionOverrideException 등 충돌 오류!

Spring Container 내부 구조

  • 이름 기반 Map(name -> BeanDefinition)
    • Key : 빈 이름 (String)
    • Value : BeanDefinition 객체
      • 메타 데이터 예시
        • beanClassName: 실제 클래스 이름 (com.example.…GlassesService)
        • scope: 싱글톤/프로토타입 등
        • constructor args, property values
        • qualifiers, lazy-init 여부
          (런타임 시점엔 이 BeanDefinition을 바탕으로 실제 빈 인스턴스가 생성되고, 싱글톤이면 컨테이너 내부의 캐시(예: singletonObjects)에 객체 레퍼런스가 저장됩니다.)
nameToDefinition:
{
  "com.example.glasses.service.GlassesService" : BeanDefinition{
     beanClassName="com.example.glasses.service.GlassesService",
     scope="singleton",
     lazyInit=false,
     ... // 기타 메타데이터
  },
  "com.example.sunglasses.service.GlassesService" : BeanDefinition{}
}
  • 타입 기반 Map(type -> [beanName, ...])_
    • Key : Bean의 Class<?> 객체
    • Value : 그 타입에 맞는 해당 빈 이름 목록(List<String>)
      • 생성자 주입 시 getBean(RequiredType.class) 처럼 타입 조회를 하면 이 리스트에서 후보 빈 이름을 꺼내서 실제 빈을 불러옵니다.
typeToNames:
{
  com.example.glasses.service.GlassesService.class : [
    "com.example.glasses.service.GlassesService"
  ],
  com.example.sunglasses.service.GlassesService.class : [
    "com.example.sunglasses.service.GlassesService"
  ]
}
  • 이 두 맵 덕분에 스프링은
    • 이름 조회: 특정 이름으로 빈 정의(또는 인스턴스)를 빠르게 찾음
    • 타입 조회: 파라미터 타입에 딱 맞는 빈 이름 목록을 가져와 후보를 좁혀
      • 후보가 하나면 바로 주입
      • 여러 개면 @Qualifier/@Primary 등으로 더 좁힘

해결

전략Bean Name충돌 여부
기본(simple)glassesService ×2있음
FCQN 전략com.example…GlassesService, com.example…GlassesService없음
  • Bean의 Naming 규칙을 FCQN 네이밍 전략 사용하기
    • 전체 패키지 구조로 Naming !
@SpringBootApplication(
    scanBasePackages = "com.example",
    nameGenerator      = FullyQualifiedAnnotationBeanNameGenerator.class
)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

결과

두 Service의 Bean Name이 달라짐 - Bean 등록 성공

"com.example.glasses.service.GlassesService"
"com.example.sunglasses.service.GlassesService"

의존성 주입(생성자 주입) - 변경 불필요

// com.example.glasses.controller
package com.example.glasses.controller;

import com.example.glasses.service.GlassesService;
import org.springframework.stereotype.Controller;
import lombok.RequiredArgsConstructor;

@Controller
@RequiredArgsConstructor
public class GlassesController {
    private final GlassesService glassesService;  // Type이 다르기 때문에 가능
}

-------------------------------------------------------------------

// com.example.sunglasses.controller
package com.example.sunglasses.controller;

import com.example.sunglasses.service.GlassesService;
import org.springframework.stereotype.Controller;
import lombok.RequiredArgsConstructor;

@Controller
@RequiredArgsConstructor
public class SunglassesController {
    private final GlassesService glassesService;  // Type이 다르기 때문에 가능
}
  • 이제 각 Type 별로 Bean이 따로 Application Context에 등록되어 문제 해결됨
    • 즉 의존성 주입받는 쪽은 문제가 아니였고 Bean 등록 과정에서 Name이 중복되는 문제였다!!

회고

  • 사실 이 문제는 안만나는게 거의 맞을꺼같고 우연하게 마주친다면 리팩토링이 가장 최선이 아닐까?
  • 어쩔수 없다면 Bean Name 규칙을 지정하던가 아니면 @Service("abc"), @Qualifier("abc") 와 같이 임시로 Bean Name을 겹치지 않게 지정해도 된다!
  • 만약 구현체가 여러개라면 어쩔수없이 지정자를 사용해야하는데 이건 이 포스트와 무관하다!
profile
비슷한 어려움을 겪는 누군가에게 도움이 되길

0개의 댓글