본문 바로가기
IT

[Spring] @RequiredArgsConstructor vs @Autowired - 의존성 주입 제대로 알고 쓰자

by urosie 2026. 1. 30.
반응형

스프링 백엔드 개발을 하다 보면 가장 기본적으로 사용하는 것이 바로 **의존성 주입(Dependency Injection)**입니다. 그런데 같은 의존성 주입이라도 @Autowired를 쓰는 방식과 @RequiredArgsConstructor를 쓰는 방식이 있죠.

"둘 다 되는데 뭐가 다른 거야?"라고 생각할 수 있지만, 실무에서는 명확한 차이가 있고 스프링 공식 문서에서도 생성자 주입을 권장합니다. 오늘은 그 이유를 실제 코드 예시와 함께 알아보겠습니다.


두 가지 방식 비교

1. @RequiredArgsConstructor 방식 (생성자 주입) ✅ 권장

@Service
@RequiredArgsConstructor
public class FinMngRegService {
    private final BatchMapper mapper;
    private final AnotherService service;
    
    public void register() {
        mapper.insert(...);
    }
}

2. @Autowired 방식 (필드 주입) ❌ 비권장

@Service
public class FinMngRegService {
    @Autowired
    private BatchMapper mapper;
    
    @Autowired
    private AnotherService service;
    
    public void register() {
        mapper.insert(...);
    }
}

얼핏 보면 비슷해 보이지만, 실무에서는 큰 차이가 있습니다.


@RequiredArgsConstructor가 더 나은 5가지 이유

1️⃣ final 키워드 사용 가능 → 불변성 보장

❌ 필드 주입의 문제점

@Service
public class PaymentService {
    @Autowired
    private PaymentMapper mapper;  // final 사용 불가
    
    public void someMethod() {
        this.mapper = null;  // 😱 실수로 null 할당 가능!
        // 나중에 NPE 발생...
    }
}

✅ 생성자 주입의 안전성

@Service
@RequiredArgsConstructor
public class PaymentService {
    private final PaymentMapper mapper;  // final로 불변 보장
    
    public void someMethod() {
        this.mapper = null;  // ❌ 컴파일 에러! 바꿀 수 없음
    }
}

실무 포인트: 멀티스레드 환경에서 객체 상태가 변경되지 않음을 보장하여 안전합니다.


2️⃣ 테스트 코드 작성이 훨씬 쉬움

❌ 필드 주입은 테스트하기 복잡

@Service
public class FinMngRegService {
    @Autowired
    private BatchMapper mapper;
    
    public void register(String id) {
        mapper.insert(id);
    }
}

// 테스트 코드
@Test
void testRegister() {
    FinMngRegService service = new FinMngRegService();
    BatchMapper mockMapper = mock(BatchMapper.class);
    
    // 문제: mapper에 mock을 어떻게 넣지? 
    // ReflectionTestUtils 같은 특수 도구 필요 😓
    ReflectionTestUtils.setField(service, "mapper", mockMapper);
    
    service.register("123");
}

✅ 생성자 주입은 테스트가 간단

@Service
@RequiredArgsConstructor
public class FinMngRegService {
    private final BatchMapper mapper;
    
    public void register(String id) {
        mapper.insert(id);
    }
}

// 테스트 코드
@Test
void testRegister() {
    BatchMapper mockMapper = mock(BatchMapper.class);
    FinMngRegService service = new FinMngRegService(mockMapper);  // 간단!
    
    service.register("123");
    verify(mockMapper).insert("123");
}

실무 포인트: 단위 테스트 작성 시 스프링 컨텍스트 없이도 순수 자바로 테스트 가능합니다.


3️⃣ 순환 참조를 컴파일 단계에서 발견

❌ 필드 주입 - 런타임에서 발견 (앱 실행해야 알 수 있음)

@Service
public class AService {
    @Autowired
    private BService bService;  // B를 참조
}

@Service
public class BService {
    @Autowired
    private AService aService;  // A를 참조 (순환 참조!)
}

// 앱 실행 → 스프링 컨텍스트 로딩 중 에러 💥
// "The dependencies of some of the beans in the application context form a cycle"

✅ 생성자 주입 - IDE와 컴파일러가 미리 경고

@Service
@RequiredArgsConstructor
public class AService {
    private final BService bService;
    // IDE가 순환 참조 경고 표시
}

@Service
@RequiredArgsConstructor
public class BService {
    private final AService aService;
    // 컴파일 시 에러 또는 IDE 경고
}

실무 포인트: 개발 단계에서 미리 문제를 발견할 수 있어 배포 후 장애를 예방할 수 있습니다.


4️⃣ 의존성을 명확하게 파악 가능 (SRP 위반 체크)

❌ 필드 주입 - 의존성이 숨어있음

@Service
public class FinMngRegService {
    @Autowired private BatchMapper mapper1;
    @Autowired private BatchMapper mapper2;
    @Autowired private AService service1;
    @Autowired private BService service2;
    @Autowired private CService service3;
    @Autowired private DService service4;
    @Autowired private EService service5;
    @Autowired private FService service6;
    @Autowired private GService service7;
    // 스크롤 내려야 보임... 의존성이 무려 7개!
    // 이 클래스 책임이 너무 많은 거 아닌가? 🤔
}

✅ 생성자 주입 - 의존성이 한눈에 보임

@Service
@RequiredArgsConstructor
public class FinMngRegService {
    private final BatchMapper mapper1;
    private final BatchMapper mapper2;
    private final AService service1;
    private final BService service2;
    private final CService service3;
    private final DService service4;
    private final EService service5;
    private final FService service6;
    private final GService service7;
}
// 생성자 파라미터 7개를 보는 순간: "이거 너무 많은데?"
// → 클래스 분리 필요성을 바로 인지 가능

실무 포인트: 생성자 파라미터가 많아지면 자연스럽게 리팩토링을 고려하게 되어 **단일 책임 원칙(SRP)**을 지키는 데 도움이 됩니다.


5️⃣ NPE(NullPointerException) 방지

❌ 필드 주입의 위험한 시나리오

@Service
public class PaymentService {
    @Autowired
    private PaymentMapper mapper;
    
    private BigDecimal feeRate;
    
    public PaymentService() {
        this.feeRate = new BigDecimal("0.03");
        // 이 시점에 mapper는 아직 null!
        // @Autowired는 생성자 실행 후에 주입됨
    }
    
    public void processPayment(BigDecimal amount) {
        BigDecimal fee = amount.multiply(feeRate);
        mapper.insertPayment(amount, fee);  // mapper가 null일 수도!
    }
}

✅ 생성자 주입은 항상 안전

@Service
@RequiredArgsConstructor
public class PaymentService {
    private final PaymentMapper mapper;  // 생성자 실행 시 반드시 주입
    private final BigDecimal feeRate = new BigDecimal("0.03");
    
    public void processPayment(BigDecimal amount) {
        BigDecimal fee = amount.multiply(feeRate);
        mapper.insertPayment(amount, fee);  // mapper는 절대 null이 아님!
    }
}

실무 포인트: 생성자 주입을 사용하면 객체 생성 시점에 모든 의존성이 주입되므로 NPE 걱정이 없습니다.


정리: 어떤 방식을 써야 할까?

스프링 공식 권장 순위

  1. 생성자 주입 (Constructor Injection) ✅ 강력 추천
    • @RequiredArgsConstructor 사용
    • 또는 생성자 직접 작성
  2. Setter 주입 (Setter Injection) - 선택적 의존성에만 사용
  3. 필드 주입 (Field Injection) ❌ 비권장

Lombok 없이 생성자 주입 하는 법

@Service
public class FinMngRegService {
    private final BatchMapper mapper;
    private final AnotherService service;
    
    // 생성자 직접 작성
    public FinMngRegService(BatchMapper mapper, AnotherService service) {
        this.mapper = mapper;
        this.service = service;
    }
}

// 참고: Spring 4.3부터는 생성자가 1개면 @Autowired 생략 가능

실무 코드 리뷰에서

// 신입이 이렇게 짜면
@Autowired
private BatchMapper mapper;
// 👨‍💼 시니어: "생성자 주입으로 바꿔주세요"

// 이렇게 짜면
@RequiredArgsConstructor
private final BatchMapper mapper;
// 👨‍💼 시니어: "Good!"

마치며

의존성 주입은 스프링의 핵심 기능이지만, 어떻게 주입하느냐에 따라 코드 품질이 크게 달라집니다.

  • 불변성 보장
  • 테스트 용이성
  • 순환 참조 조기 발견
  • 명확한 의존성 파악
  • NPE 방지

이 다섯 가지 이유만으로도 @RequiredArgsConstructor를 사용할 충분한 이유가 됩니다.

아직 @Autowired 필드 주입을 사용하고 계신다면, 이번 기회에 생성자 주입으로 리팩토링해보시는 것을 추천드립니다!


참고 자료

 #Spring #SpringBoot #DependencyInjection #Lombok #BackEnd #Java

반응형

댓글