logo Fonnpo

문의하기
2026년 5월 30일 1분 읽기 Fonnpo

Spring 프레임워크 완전 가이드

Spring은 Java 생태계에서 가장 인기 있는 애플리케이션 프레임워크입니다. 이 가이드는 IoC와 DI의 핵심 개념, Bean 생명주기와 스코프, AOP, Spring MVC 요청 처리, Spring Boot 자동 설정, 트랜잭션 전파와 격리 수준, Spring Data JPA, 그리고 순환 의존성 등 흔한 함정을 체계적으로 다룹니다.

#Java#Spring#Backend

Spring은 Java 생태계에서 가장 인기 있는 애플리케이션 프레임워크로, IoC 컨테이너, AOP, MVC, 트랜잭션 관리 등 다양한 기능을 제공하여 엔터프라이즈 애플리케이션 개발을 크게 단순화합니다.

Spring 핵심 모듈

Spring 프레임워크는 여러 모듈로 구성되어 있으며 필요에 따라 도입할 수 있습니다:

모듈설명
spring-coreIoC 컨테이너의 핵심 기반
spring-beansBeanFactory — Bean 생성 및 관리 담당
spring-contextApplicationContext — beans 기반에 더 풍부한 기능 제공
spring-aop관점 지향 프로그래밍 지원
spring-webmvcSpring MVC 프레임워크
spring-tx트랜잭션 관리
spring-data-jpaJPA 통합
spring-boot관례 우선 설정, 자동 구성

IoC(제어의 역전)와 DI(의존성 주입)

IoC란 무엇인가

전통적인 방식에서는 객체가 직접 의존 객체를 생성합니다:

Java
        public class UserService {
    // 직접 new — 결합도가 높음
    private UserRepository repo = new UserRepositoryImpl();
}

    

IoC(Inversion of Control)는 객체 생성과 의존 관계 관리를 Spring 컨테이너에 위임합니다. 객체는 필요한 것만 선언하고, 컨테이너가 주입합니다:

Java
        @Service
public class UserService {
    private final UserRepository repo;

    // Spring이 자동으로 주입
    public UserService(UserRepository repo) {
        this.repo = repo;
    }
}

    

제어권이 객체 자신에서 컨테이너로 "역전"되는 것 — 이것이 IoC의 핵심 사상입니다.

DI의 세 가지 방식

1. 생성자 주입(권장)

Java
        @Service
public class OrderService {
    private final UserRepository userRepo;
    private final ProductRepository productRepo;

    // 생성자가 하나뿐일 때 @Autowired 생략 가능
    public OrderService(UserRepository userRepo, ProductRepository productRepo) {
        this.userRepo = userRepo;
        this.productRepo = productRepo;
    }
}

    

장점: 의존을 final로 선언할 수 있어 불변성이 보장되고, 테스트하기 쉬우며, 객체 생성 시점에 의존이 준비됩니다.

2. 세터 주입

Java
        @Service
public class NotificationService {
    private EmailSender emailSender;

    @Autowired
    public void setEmailSender(EmailSender emailSender) {
        this.emailSender = emailSender;
    }
}

    

선택적 의존이나 런타임에 구현체를 동적으로 교체해야 하는 시나리오에 적합합니다.

3. 필드 주입(비권장)

Java
        @Service
public class UserService {
    @Autowired
    private UserRepository userRepo; // final 선언 불가, 단위 테스트 어려움
}

    

코드는 간결하지만 final을 사용할 수 없고, 컨테이너 외부에서 인스턴스화하면 의존이 null이 되므로 운영 코드에서는 사용하지 않는 것을 권장합니다.

Bean 관리

Bean 정의 방식

애노테이션 방식(가장 일반적)

Java
        @Component        // 범용 컴포넌트
@Service          // 비즈니스 레이어
@Repository       // 데이터 접근 레이어(JPA 예외 변환도 활성화)
@Controller       // MVC 컨트롤러
@RestController   // = @Controller + @ResponseBody

    

Java Config 방식

Java
        @Configuration
public class AppConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public EmailSender emailSender() {
        return new SmtpEmailSender();
    }
}

    

XML 방식(레거시 프로젝트에서 여전히 사용)

Xml
        <bean id="userService" class="com.example.UserService">
    <constructor-arg ref="userRepository"/>
</bean>

    

Bean 스코프

Java
        @Component
@Scope("singleton")   // 기본값 — 컨테이너에 인스턴스가 하나만 존재
public class AppConfig { }

@Component
@Scope("prototype")   // getBean()마다 새 인스턴스 생성
public class RequestProcessor { }

// Web 환경 전용
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class RequestContext { }

@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class UserSession { }

    
스코프설명
singleton기본값 — 컨테이너에서 유일한 인스턴스
prototype주입 / getBean()마다 새 인스턴스 생성
requestHTTP 요청마다 인스턴스 하나(Web 환경)
sessionHTTP 세션마다 인스턴스 하나(Web 환경)

Bean 생명주기

Properties
        인스턴스화
  → 프로퍼티 주입
  → BeanNameAware / BeanFactoryAware / ApplicationContextAware
  → BeanPostProcessor#postProcessBeforeInitialization
  → @PostConstruct
  → InitializingBean#afterPropertiesSet
  → init-method
  → BeanPostProcessor#postProcessAfterInitialization
  → 【Bean 사용 가능】
  → @PreDestroy
  → DisposableBean#destroy
  → destroy-method
  → 소멸

    

자주 사용하는 생명주기 훅:

Java
        @Component
public class DataSourcePool {

    @PostConstruct
    public void init() {
        // Bean 초기화 완료 후 실행 — 커넥션 풀 워밍업에 적합
        System.out.println("커넥션 풀 초기화 완료");
    }

    @PreDestroy
    public void destroy() {
        // 컨테이너 종료 전 실행 — 리소스 해제에 적합
        System.out.println("커넥션 풀 리소스 해제");
    }
}

    

자주 사용하는 애노테이션

주입과 설정

Java
        // 타입으로 자동 주입
@Autowired
private UserRepository userRepo;

// 같은 타입의 Bean이 여러 개일 때 이름으로 지정
@Autowired
@Qualifier("mysqlUserRepo")
private UserRepository userRepo;

// @Autowired + @Qualifier와 동일, 더 간결(JSR-250 표준)
@Resource(name = "mysqlUserRepo")
private UserRepository userRepo;

// 설정 파일의 값 주입
@Value("${app.name}")
private String appName;

// 기본값과 함께 주입
@Value("${app.timeout:30}")
private int timeout;

// SpEL 표현식 주입
@Value("#{systemProperties['user.home']}")
private String userHome;

    

설정 클래스 애노테이션

Java
        @Configuration                             // 설정 클래스 표시
@ComponentScan("com.example")              // 스캔할 패키지 지정
@PropertySource("classpath:app.properties") // 추가 프로퍼티 파일 로드
@Import(SecurityConfig.class)              // 다른 설정 클래스 임포트
public class AppConfig { }

    

조건부 어셈블리(Spring Boot에서 자주 사용)

Java
        // 프로퍼티가 존재하고 지정된 값일 때만 어셈블
@ConditionalOnProperty(name = "feature.cache.enabled", havingValue = "true")
@Bean
public CacheManager cacheManager() {
    return new RedisCacheManager(...);
}

// 지정된 타입의 Bean이 없을 때만 어셈블(기본 구현 제공)
@ConditionalOnMissingBean(DataSource.class)
@Bean
public DataSource embeddedDataSource() {
    return new EmbeddedDatabaseBuilder().build();
}

// 지정된 클래스가 클래스패스에 있을 때만 어셈블
@ConditionalOnClass(RedisTemplate.class)
@Configuration
public class RedisAutoConfig { }

    

AOP(관점 지향 프로그래밍)

AOP를 사용하면 비즈니스 코드를 수정하지 않고도 로깅, 권한 검사, 성능 모니터링, 트랜잭션 등 횡단 관심사를 일관되게 처리할 수 있습니다.

핵심 개념

개념설명
Aspect(어스펙트)횡단 로직을 캡슐화한 클래스. @Aspect로 애노테이션
Pointcut(포인트컷)어느 메서드에 적용할지를 정의하는 표현식
Advice(어드바이스)포인트컷이 트리거될 때 실행되는 코드
JoinPoint(조인포인트)인터셉트할 수 있는 실행 지점(메서드 호출, 예외 등)
Weaving(위빙)어스펙트 로직을 대상 객체에 적용하는 과정

어드바이스 유형

Java
        @Aspect
@Component
public class LoggingAspect {

    // 포인트컷 표현식 — service 패키지의 모든 메서드에 매칭
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceMethods() {}

    // 메서드 실행 전
    @Before("serviceMethods()")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("메서드 호출:" + joinPoint.getSignature().getName());
    }

    // 메서드 실행 후(예외 여부에 관계없이)
    @After("serviceMethods()")
    public void logAfter(JoinPoint joinPoint) {
        System.out.println("메서드 실행 완료");
    }

    // 정상 반환 후
    @AfterReturning(pointcut = "serviceMethods()", returning = "result")
    public void logAfterReturning(Object result) {
        System.out.println("반환값:" + result);
    }

    // 예외 발생 후
    @AfterThrowing(pointcut = "serviceMethods()", throwing = "ex")
    public void logAfterThrowing(Exception ex) {
        System.out.println("예외:" + ex.getMessage());
    }

    // 어라운드 어드바이스(가장 강력 — 메서드 실행 여부를 제어 가능)
    @Around("serviceMethods()")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            Object result = pjp.proceed(); // 대상 메서드 실행
            long elapsed = System.currentTimeMillis() - start;
            System.out.println(pjp.getSignature() + " 처리 시간:" + elapsed + "ms");
            return result;
        } catch (Exception e) {
            System.out.println("예외 발생:" + e.getMessage());
            throw e;
        }
    }
}

    
접기

포인트컷 표현식

Java
        // 특정 메서드에 정확히 매칭
execution(public String com.example.service.UserService.findById(Long))

// 클래스의 모든 메서드에 매칭
execution(* com.example.service.UserService.*(..))

// 패키지 및 하위 패키지의 모든 메서드에 매칭(..은 임의의 하위 패키지)
execution(* com.example.service..*.*(..))

// 특정 애노테이션이 붙은 메서드에 매칭
@annotation(com.example.annotation.Log)

// 패키지 내 모든 타입에 매칭
within(com.example.service.*)

// 조합(get으로 시작하는 메서드 제외)
@Pointcut("execution(* com.example.service.*.*(..)) && !execution(* com.example.service.*.get*(..))")

    

AOP 실전:통합 운영 로그

Java
        // 커스텀 애노테이션
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLog {
    String value() default "";
}

// 어스펙트 구현
@Aspect
@Component
public class OperationLogAspect {

    @Autowired
    private LogService logService;

    @Around("@annotation(operationLog)")
    public Object record(ProceedingJoinPoint pjp, OperationLog operationLog) throws Throwable {
        String username = SecurityContextHolder.getContext().getAuthentication().getName();
        String operation = operationLog.value();

        try {
            Object result = pjp.proceed();
            logService.save(username, operation, "SUCCESS");
            return result;
        } catch (Exception e) {
            logService.save(username, operation, "FAILED: " + e.getMessage());
            throw e;
        }
    }
}

// 애노테이션 사용
@Service
public class UserService {
    @OperationLog("사용자 삭제")
    public void deleteUser(Long id) {
        // 비즈니스 코드는 로그를 신경 쓰지 않아도 됨
    }
}

    
접기

Spring MVC

요청 처리 흐름

Properties
        클라이언트 요청
  → DispatcherServlet(프론트 컨트롤러)
  → HandlerMapping(해당 Controller 메서드 탐색)
  → HandlerAdapter(Controller 메서드 실행)
  → ModelAndView 또는 @ResponseBody 데이터 반환
  → ViewResolver(뷰 이름이면 해당 뷰 탐색)
  → 렌더링 → 클라이언트 응답

    

Controller 자주 쓰는 애노테이션

Java
        @RestController
@RequestMapping("/api/users")
public class UserController {

    @Autowired
    private UserService userService;

    // GET /api/users
    @GetMapping
    public List<User> list() {
        return userService.findAll();
    }

    // GET /api/users/1
    @GetMapping("/{id}")
    public ResponseEntity<User> getById(@PathVariable Long id) {
        return userService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    // POST /api/users
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public User create(@RequestBody @Valid CreateUserRequest req) {
        return userService.create(req);
    }

    // PUT /api/users/1
    @PutMapping("/{id}")
    public User update(@PathVariable Long id, @RequestBody @Valid UpdateUserRequest req) {
        return userService.update(id, req);
    }

    // DELETE /api/users/1
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id) {
        userService.delete(id);
    }

    // GET /api/users?page=0&size=10&keyword=Alice
    @GetMapping("/search")
    public Page<User> search(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(required = false) String keyword
    ) {
        return userService.search(keyword, PageRequest.of(page, size));
    }
}

    
접기

파라미터 바인딩

Java
        // 경로 파라미터
@GetMapping("/{id}")
public User get(@PathVariable Long id) { ... }

// 쿼리 파라미터(기본값 포함)
@GetMapping
public List<User> list(
    @RequestParam String name,
    @RequestParam(defaultValue = "0") int page
) { ... }

// 요청 바디(JSON → 객체)
@PostMapping
public User create(@RequestBody CreateUserRequest req) { ... }

// 요청 헤더
@GetMapping
public String auth(@RequestHeader("Authorization") String token) { ... }

// Cookie
@GetMapping
public String session(@CookieValue("sessionId") String sid) { ... }

// 쿼리 파라미터를 객체에 자동 바인딩(?name=Alice&age=25 → UserSearchRequest)
@GetMapping
public Page<User> search(UserSearchRequest req) { ... }

    

전역 예외 처리

Java
        @RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(EntityNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleNotFound(EntityNotFoundException ex) {
        return new ErrorResponse("NOT_FOUND", ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult().getFieldErrors()
            .stream()
            .map(f -> f.getField() + ": " + f.getDefaultMessage())
            .collect(Collectors.toList());
        return new ErrorResponse("VALIDATION_FAILED", errors.toString());
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleGeneral(Exception ex) {
        return new ErrorResponse("INTERNAL_ERROR", "서버 내부 오류");
    }
}

    

Spring Boot

자동 설정 원리

Spring Boot는 @SpringBootApplication을 통해 자동 설정을 활성화하며, 이는 세 가지 애노테이션의 조합입니다:

Java
        @SpringBootConfiguration   // @Configuration과 동일
@EnableAutoConfiguration   // 핵심: 자동 설정 활성화
@ComponentScan             // 현재 패키지와 하위 패키지 스캔
public @interface SpringBootApplication { }

    

@EnableAutoConfigurationAutoConfigurationImportSelector를 통해 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports에 나열된 모든 자동 설정 클래스를 읽고, 각 @ConditionalOnXxx 애노테이션의 조건을 평가하여 Bean을 선택적으로 등록합니다.

전체 흐름을 단순화하면:

Properties
        시작
  → AutoConfiguration.imports 스캔
  → @ConditionalOnXxx 조건 순차 확인
  → 조건 충족 → Bean 등록
  → 조건 미충족 → 건너뜀

    

설정 파일

Yaml
        # application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: root
    password: secret
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false

server:
  port: 8080
  servlet:
    context-path: /api

app:
  jwt:
    secret: my-secret-key
    expiration: 86400

    

다중 환경 설정

Yaml
        # application.yml
spring:
  profiles:
    active: dev # 기본으로 dev 환경 활성화

---
spring:
  config:
    activate:
      on-profile: dev
  datasource:
    url: jdbc:h2:mem:testdb

---
spring:
  config:
    activate:
      on-profile: prod
  datasource:
    url: jdbc:mysql://prod-server:3306/mydb

    

설정을 클래스에 바인딩

Java
        @ConfigurationProperties(prefix = "app.jwt")
@Component
public class JwtProperties {
    private String secret;
    private long expiration;
    // getters & setters
}

    

트랜잭션 관리

기본 사용법

Java
        @Service
public class TransferService {

    @Transactional
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepo.findById(fromId).orElseThrow();
        Account to   = accountRepo.findById(toId).orElseThrow();

        from.deduct(amount);
        to.add(amount);

        accountRepo.save(from);
        accountRepo.save(to);
        // 메서드 정상 종료 → 자동 커밋
        // RuntimeException 발생 → 자동 롤백
    }
}

    

트랜잭션 전파

Java
        // REQUIRED(기본값): 기존 트랜잭션에 참여, 없으면 새로 생성
@Transactional(propagation = Propagation.REQUIRED)

// REQUIRES_NEW: 항상 새 트랜잭션 생성, 현재 트랜잭션 일시 중단
@Transactional(propagation = Propagation.REQUIRES_NEW)

// SUPPORTS: 기존 트랜잭션에 참여, 없으면 비트랜잭션으로 실행
@Transactional(propagation = Propagation.SUPPORTS)

// NOT_SUPPORTED: 비트랜잭션으로 실행, 현재 트랜잭션이 있으면 일시 중단
@Transactional(propagation = Propagation.NOT_SUPPORTED)

// NEVER: 트랜잭션 내 실행 금지, 있으면 예외 발생
@Transactional(propagation = Propagation.NEVER)

// MANDATORY: 트랜잭션 내에서만 실행, 없으면 예외 발생
@Transactional(propagation = Propagation.MANDATORY)

// NESTED: 중첩 트랜잭션(세이브포인트 사용). 외부 롤백 시 내부도 롤백, 내부 롤백은 외부에 영향 없음
@Transactional(propagation = Propagation.NESTED)

    

격리 수준

격리 수준더티 읽기반복 불가 읽기팬텀 읽기
READ_UNCOMMITTED✅ 가능✅ 가능✅ 가능
READ_COMMITTED❌ 불가✅ 가능✅ 가능
REPEATABLE_READ❌ 불가❌ 불가✅ 가능
SERIALIZABLE❌ 불가❌ 불가❌ 불가
Java
        @Transactional(isolation = Isolation.READ_COMMITTED)   // Oracle 기본값
@Transactional(isolation = Isolation.REPEATABLE_READ)  // MySQL InnoDB 기본값
@Transactional(isolation = Isolation.SERIALIZABLE)     // 가장 엄격, 성능 최저

    

주의사항

Java
        @Service
public class UserService {

    // ❌ 자기 호출은 트랜잭션을 트리거하지 않음(this는 프록시가 아니므로 AOP가 작동하지 않음)
    public void outer() {
        this.inner();
    }

    @Transactional
    public void inner() { ... }

    // ✅ 컨테이너에서 프록시 객체를 가져와서 호출
    @Autowired
    private ApplicationContext context;

    public void outer2() {
        context.getBean(UserService.class).inner();
    }

    // ❌ 기본적으로 RuntimeException만 롤백, 검사 예외는 롤백하지 않음
    @Transactional
    public void riskyOp() throws IOException {
        // IOException은 기본적으로 롤백하지 않음
    }

    // ✅ 모든 예외에서 롤백하도록 명시적으로 지정
    @Transactional(rollbackFor = Exception.class)
    public void safeOp() throws IOException { ... }

    // ❌ private 메서드의 @Transactional은 무효(AOP는 private 메서드를 프록시할 수 없음)
    @Transactional
    private void internalUpdate() { ... }
}

    
접기

Spring Data JPA

Repository 레이어

Java
        @Repository
public interface UserRepository extends JpaRepository<User, Long> {

    // 메서드 명명 규칙 — Spring이 구현을 자동 생성
    List<User> findByEmail(String email);
    List<User> findByAgeGreaterThan(int age);
    Optional<User> findByEmailAndStatus(String email, UserStatus status);
    boolean existsByEmail(String email);
    long countByStatus(UserStatus status);

    // 페이징 및 정렬
    Page<User> findByStatus(UserStatus status, Pageable pageable);
    List<User> findAll(Sort sort);

    // 커스텀 JPQL
    @Query("SELECT u FROM User u WHERE u.name LIKE %:keyword% OR u.email LIKE %:keyword%")
    List<User> search(@Param("keyword") String keyword);

    // 네이티브 SQL
    @Query(value = "SELECT * FROM users WHERE created_at > :date", nativeQuery = true)
    List<User> findCreatedAfter(@Param("date") LocalDate date);

    // 수정 / 삭제 작업에는 @Modifying 필요
    @Modifying
    @Query("UPDATE User u SET u.status = :status WHERE u.id IN :ids")
    int batchUpdateStatus(@Param("ids") List<Long> ids, @Param("status") UserStatus status);
}

    

엔티티 정의

Java
        @Entity
@Table(name = "users", indexes = {
    @Index(name = "idx_email", columnList = "email", unique = true)
})
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 50)
    private String name;

    @Column(nullable = false, unique = true)
    private String email;

    @Enumerated(EnumType.STRING)
    private UserStatus status;

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    // 지연 로딩 — N+1 문제 방지
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Order> orders;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "department_id")
    private Department department;
}

    
접기

자주 발생하는 문제와 모범 사례

순환 의존성

Spring Boot 2.6+는 기본적으로 순환 의존성을 금지하며, 감지되면 BeanCurrentlyInCreationException을 발생시킵니다.

Java
        // ❌ 순환 의존성(A는 B에 의존하고, B는 A에 의존)
@Service class A { @Autowired B b; }
@Service class B { @Autowired A a; }

// ✅ 해결책 1: 설계 개선 — 공통 로직을 새 클래스 C로 추출(권장)
@Service class C { /* 공통 로직 */ }
@Service class A { @Autowired C c; }
@Service class B { @Autowired C c; }

// ✅ 해결책 2: @Lazy 지연 주입으로 초기화 교착 상태 해소
@Service class A {
    @Autowired @Lazy B b;
}

// ✅ 해결책 3: 세터 주입(근본 해결이 아닌 회피책이므로 비권장)
@Service class A {
    private B b;
    @Autowired
    public void setB(B b) { this.b = b; }
}

    

여러 Bean 중 선택

Java
        // 두 개의 UserRepository 구현체
@Component("mysqlUserRepo")
public class MysqlUserRepository implements UserRepository { ... }

@Component("redisUserRepo")
public class RedisUserRepository implements UserRepository { ... }

// 방법 1: @Qualifier로 이름 지정
@Autowired
@Qualifier("mysqlUserRepo")
private UserRepository userRepo;

// 방법 2: @Primary로 기본 구현체 표시
@Primary
@Component
public class MysqlUserRepository implements UserRepository { ... }

// 방법 3: 모든 구현체를 주입받아 직접 선택
@Autowired
private List<UserRepository> allRepos;

@Autowired
private Map<String, UserRepository> repoMap; // 키는 bean 이름

    

자주 빠지는 함정

Java
        // 1. private 메서드의 @Transactional은 무효
@Transactional        // ❌ 무효 — AOP는 private 메서드를 프록시할 수 없음
private void internalUpdate() { ... }

// 2. static 메서드의 애노테이션은 무효(AOP는 인스턴스 프록시 기반)
@Cacheable("users")   // ❌ 무효
public static User staticGet(Long id) { ... }

// 3. prototype Bean을 singleton에 주입하면 항상 같은 인스턴스가 반환됨
// ✅ @Lookup을 사용하면 Spring이 호출마다 새로운 prototype 인스턴스를 생성
@Component
public class SingletonBean {
    @Lookup
    public PrototypeBean getPrototype() {
        return null; // Spring이 이 메서드를 오버라이드하여 매번 새 인스턴스를 반환
    }
}

// 4. Session 종료 후 JPA 지연 로딩 관계에 접근하면 LazyInitializationException 발생
// ✅ 해결책: Service 레이어에서 @Transactional로 Session 유지, 또는 JOIN FETCH 사용
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id")
Optional<User> findByIdWithOrders(@Param("id") Long id);

    
Fonnpo