본문 바로가기

development/Spring Boot

Spring Boot에서 @RequiredArgsConstructor로 깔끔한 의존성 주입하기

Spring Boot 개발을 하다 보면 의존성 주입(Dependency Injection)을 위해 생성자를 작성하는 일이 매우 빈번합니다.

특히 여러 개의 의존성을 주입받는 클래스에서는 생성자 코드가 길어지고 반복적인 작업이 될 수 있습니다. 이런 문제를 해결해주는 것이 바로 Lombok의 @RequiredArgsConstructor 어노테이션입니다.


@RequiredArgsConstructor란?

@RequiredArgsConstructor는 Lombok에서 제공하는 어노테이션으로, final 필드나 @NonNull 어노테이션이 붙은 필드에 대해서만 생성자를 자동으로 생성해주는 기능입니다.

Spring Boot에서는 주로 final 필드와 함께 사용하여 불변(immutable) 의존성 주입을 구현하는 데 활용됩니다.


기본 사용법

Before: 전통적인 생성자 방식

@Service
public class UserService {
    
    private final UserRepository userRepository;
    private final EmailService emailService;
    private final ValidationService validationService;
    
    // 수동으로 생성자를 작성해야 함
    public UserService(UserRepository userRepository, 
                      EmailService emailService, 
                      ValidationService validationService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
        this.validationService = validationService;
    }
    
    // 비즈니스 로직...
}

After: @RequiredArgsConstructor 사용

@Service
@RequiredArgsConstructor
public class UserService {
    
    private final UserRepository userRepository;
    private final EmailService emailService;
    private final ValidationService validationService;
    
    // 생성자가 자동으로 생성됨!
    
    // 비즈니스 로직...
}

핵심 특징

 

1. final 필드만 대상

@RequiredArgsConstructor는 오직 final 필드만을 생성자 매개변수로 포함합니다.

@RequiredArgsConstructor
public class ExampleService {
    
    private final UserRepository userRepository; // 생성자에 포함됨
    private final EmailService emailService;     // 생성자에 포함됨
    
    private String tempData;                     // 생성자에 포함되지 않음
    private int counter = 0;                     // 생성자에 포함되지 않음
}

 

2. @NonNull 필드도 포함

@NonNull 어노테이션이 붙은 필드도 생성자에 포함됩니다.

@RequiredArgsConstructor
public class MixedService {
    
    private final UserRepository userRepository;  // 포함됨
    
    @NonNull
    private String configValue;                   // 포함됨
    
    private String optionalValue;                 // 포함되지 않음
}

 


실무 활용 예제

 

Controller에서의 활용

@RestController
@RequiredArgsConstructor
public class UserController {
    
    private final UserService userService;
    private final UserMapper userMapper;
    
    // 컨트롤러 로직...
}

 

Repository 계층에서의 활용

@Repository
@RequiredArgsConstructor
public class CustomUserRepositoryImpl {
    
    private final EntityManager entityManager;
    private final JPAQueryFactory queryFactory;
    
    // 리포지토리 로직...
}

 

테스트에서의 활용

@RequiredArgsConstructor를 사용한 클래스는 Mockito의 @InjectMocks와 함께 사용할 수 있습니다.

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @Mock
    private EmailService emailService;
    
    @InjectMocks
    private UserService userService; // @RequiredArgsConstructor가 생성한 생성자를 통해 Mock 주입
    
    @Test
    void testUserService() {
        // given
        User mockUser = new User("test@example.com", "테스트");
        when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
        
        // when
        Optional<User> result = userService.findById(1L);
        
        // then
        assertThat(result).isPresent();
        assertThat(result.get().getEmail()).isEqualTo("test@example.com");
    }
}

또는 수동으로 생성자를 호출하여 테스트할 수도 있습니다.

class UserServiceTest {
    
    private UserRepository mockUserRepository;
    private EmailService mockEmailService;
    private UserService userService;
    
    @BeforeEach
    void setUp() {
        mockUserRepository = mock(UserRepository.class);
        mockEmailService = mock(EmailService.class);
        
        // @RequiredArgsConstructor가 생성한 생성자 직접 호출
        userService = new UserService(mockUserRepository, mockEmailService);
    }
    
    @Test
    void testUserCreation() {
        // 테스트 로직...
    }
}

 

Configuration 클래스에서의 활용

Configuration 클래스에서 Properties를 주입받을 때 매우 유용합니다.

@Configuration
@RequiredArgsConstructor
public class DatabaseConfig {
    
    private final DatabaseProperties databaseProperties; // application.yml에서 주입
    
    @Bean
    public DataSource dataSource() {
        return DataSourceBuilder.create()
                .url(databaseProperties.getUrl())
                .username(databaseProperties.getUsername())
                .password(databaseProperties.getPassword())
                .build();
    }
}

// Properties 클래스
@ConfigurationProperties(prefix = "database")
@Data
public class DatabaseProperties {
    private String url;
    private String username;
    private String password;
}

여러 개의 Properties를 주입받는 경우:

@Configuration
@RequiredArgsConstructor
public class AppConfig {
    
    private final DatabaseProperties databaseProperties;
    private final RedisProperties redisProperties;
    private final EmailProperties emailProperties;
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());
        return template;
    }
    
    @Bean
    public JedisConnectionFactory jedisConnectionFactory() {
        JedisConnectionFactory factory = new JedisConnectionFactory();
        factory.setHostName(redisProperties.getHost());
        factory.setPort(redisProperties.getPort());
        return factory;
    }
}

 


 

베스트 프랙티스

1. 항상 final 키워드 사용

의존성 주입받는 필드는 final로 선언하여 불변성을 보장하세요.

@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository; // 올바른 방법
    private UserRepository userRepository2;      // 잘못된 방법
}

2. 너무 많은 의존성 주입 피하기

한 클래스에 너무 많은 의존성이 있다면 클래스 분리를 고려하세요.

// X 너무 많은 의존성 (Single Responsibility Principle 위반)
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;
    private final SmsService smsService;
    private final PaymentService paymentService;
    private final LogService logService;
    private final FileService fileService;
    // ... 더 많은 의존성들
}

// O 책임을 분리한 적절한 의존성
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
    private final UserNotificationService notificationService; // 알림 관련 로직 분리
}

주의사항

순환 의존성 주의

// 잘못된 예: 순환 의존성 발생 가능
@Service
@RequiredArgsConstructor
public class UserService {
    private final OrderService orderService; // OrderService도 UserService를 의존한다면 문제
}

다른 방식과의 비교

@Autowired vs @RequiredArgsConstructor

// X 필드 주입 (권장하지 않음)
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository; // 필드 주입, 테스트하기 어려움
}

// O 생성자 주입 (@RequiredArgsConstructor 사용)
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository; // 생성자 주입, 불변성 보장
}

@AllArgsConstructor와의 차이점

  • @AllArgsConstructor: 모든 필드를 포함하는 생성자 생성
  • @RequiredArgsConstructor: final이나 @NonNull 필드만 포함하는 생성자 생성
 
@AllArgsConstructor // 모든 필드가 생성자에 포함됨
public class ExampleService {
    private final UserRepository userRepository;
    private String optionalField; // 이것도 생성자에 포함됨
}

@RequiredArgsConstructor // final 필드만 생성자에 포함됨
public class ExampleService {
    private final UserRepository userRepository;
    private String optionalField; // 이것은 생성자에 포함되지 않음
}

 

@RequiredArgsConstructor는 Spring Boot에서 깔끔하고 안전한 의존성 주입을 구현하는 데 매우 유용한 도구입니다. 특히 다음과 같은 장점들을 제공합니다:

  1. 코드 간소화: 반복적인 생성자 작성 불필요
  2. 불변성 보장: final 필드를 통한 안전한 의존성 관리
  3. 테스트 용이성: 생성자 주입을 통한 쉬운 목 객체 주입
  4. 컴파일 타임 안전성: 누락된 의존성에 대한 컴파일 에러

Spring Boot 프로젝트에서 의존성 주입이 필요한 클래스라면 @RequiredArgsConstructor를 적극 활용해보세요. 코드가 훨씬 깔끔해지고 유지보수하기 좋은 구조를 만들 수 있습니다.


Spring Boot 아이콘 출처: Icons8 (https://icons8.com)