Spring Boot 개발을 하다 보면 의존성 주입(Dependency Injection)을 위해 생성자를 작성하는 일이 매우 빈번합니다.
특히 여러 개의 의존성을 주입받는 클래스에서는 생성자 코드가 길어지고 반복적인 작업이 될 수 있습니다. 이런 문제를 해결해주는 것이 바로 Lombok의 @RequiredArgsConstructor 어노테이션입니다.
@RequiredArgsConstructor는 Lombok에서 제공하는 어노테이션으로, final 필드나 @NonNull 어노테이션이 붙은 필드에 대해서만 생성자를 자동으로 생성해주는 기능입니다.
Spring Boot에서는 주로 final 필드와 함께 사용하여 불변(immutable) 의존성 주입을 구현하는 데 활용됩니다.
@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;
}
// 비즈니스 로직...
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
private final ValidationService validationService;
// 생성자가 자동으로 생성됨!
// 비즈니스 로직...
}
@RequiredArgsConstructor는 오직 final 필드만을 생성자 매개변수로 포함합니다.
@RequiredArgsConstructor
public class ExampleService {
private final UserRepository userRepository; // 생성자에 포함됨
private final EmailService emailService; // 생성자에 포함됨
private String tempData; // 생성자에 포함되지 않음
private int counter = 0; // 생성자에 포함되지 않음
}
@NonNull 어노테이션이 붙은 필드도 생성자에 포함됩니다.
@RequiredArgsConstructor
public class MixedService {
private final UserRepository userRepository; // 포함됨
@NonNull
private String configValue; // 포함됨
private String optionalValue; // 포함되지 않음
}
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
private final UserMapper userMapper;
// 컨트롤러 로직...
}
@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 클래스에서 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;
}
}
의존성 주입받는 필드는 final로 선언하여 불변성을 보장하세요.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository; // 올바른 방법
private UserRepository userRepository2; // 잘못된 방법
}
한 클래스에 너무 많은 의존성이 있다면 클래스 분리를 고려하세요.
// 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를 의존한다면 문제
}
// X 필드 주입 (권장하지 않음)
@Service
public class UserService {
@Autowired
private UserRepository userRepository; // 필드 주입, 테스트하기 어려움
}
// O 생성자 주입 (@RequiredArgsConstructor 사용)
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository; // 생성자 주입, 불변성 보장
}
@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에서 깔끔하고 안전한 의존성 주입을 구현하는 데 매우 유용한 도구입니다. 특히 다음과 같은 장점들을 제공합니다:
Spring Boot 프로젝트에서 의존성 주입이 필요한 클래스라면 @RequiredArgsConstructor를 적극 활용해보세요. 코드가 훨씬 깔끔해지고 유지보수하기 좋은 구조를 만들 수 있습니다.