스프링 프레임워크(Spring Framework)와 Node.js는 각각 자바(Java)와 자바스크립트(JavaScript) 기반의 애플리케이션 개발에 사용되는 강력한 도구입니다.
두 환경 모두 의존성 관리를 필요로 하지만, 그 방식에는 중요한 차이가 있습니다. 이번 포스트에서는 스프링의 빈(Bean) 관리와 Node.js의 모듈 시스템을 비교하고, 그 차이점과 효율성을 살펴보겠습니다.
Node.js: Node.js에서는 모듈 시스템을 통해 의존성을 관리합니다. 각 파일은 모듈로 취급되며, require
또는 import
를 통해 다른 파일에서 해당 모듈을 불러와 사용합니다. 이때, 불러온 모듈을 new
키워드로 인스턴스화하여 사용하게 됩니다.
스프링: 스프링에서는 '스프링 컨테이너'가 중심이 됩니다. 스프링 컨테이너는 애플리케이션이 실행될 때 빈(Bean)으로 등록된 객체들을 관리하고, 이들 간의 의존성을 주입하여 개발자가 직접 객체를 생성하고 관리하는 부담을 덜어줍니다.
Node.js에서 주문(Order)과 결제(Payment)를 관리하는 서비스를 구현할 때의 코드입니다.
// PaymentService.js
class PaymentService {
processPayment(order) {
console.log(`Processing payment for order ${order.id}`);
// 결제 처리 로직
}
}
module.exports = PaymentService;
// OrderService.js
const PaymentService = require('./PaymentService');
class OrderService {
constructor() {
this.paymentService = new PaymentService();
}
createOrder(order) {
console.log(`Creating order ${order.id}`);
this.paymentService.processPayment(order);
}
}
module.exports = OrderService;
// OrderController.js
const OrderService = require('./OrderService');
class OrderController {
constructor() {
this.orderService = new OrderService();
}
handleOrderRequest(req) {
const order = { id: 1 };
this.orderService.createOrder(order);
}
}
module.exports = OrderController;
같은 기능을 스프링으로 구현한 코드입니다.
// PaymentService.java
@Service
public class PaymentService {
public void processPayment(Order order) {
System.out.println("Processing payment for order " + order.getId());
// 결제 처리 로직
}
}
// OrderService.java
@Service
public class OrderService {
private final PaymentService paymentService;
@Autowired
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void createOrder(Order order) {
System.out.println("Creating order " + order.getId());
paymentService.processPayment(order);
}
}
// OrderController.java
@RestController
public class OrderController {
private final OrderService orderService;
@Autowired
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping("/order")
public void handleOrderRequest(@RequestBody Order order) {
orderService.createOrder(order);
}
}
new
키워드를 사용해 객체를 생성하고, 이를 서비스나 컨트롤러에 주입합니다.@Autowired
)을 추가하기만 하면 됩니다.사실 1번 항목까지만 보면 큰 차이점은 없어 보일수 있습니다. 하지만 다음 항목부터는 극명한 차이점이 보입니다.
Node.js: 객체의 생성과 소멸을 직접 관리해야 합니다. 특히 상태를 가지는 객체의 경우 생명 주기를 직접 제어해야 할 필요가 있습니다.
스프링: 스프링 컨테이너가 빈의 생명 주기를 관리하며, 기본적으로 싱글톤 패턴을 사용하여 애플리케이션 전체에서 동일한 인스턴스를 재사용합니다.
테스트에 대한 차이점을 명확히 이해하기 위해, 간단한 Mocking 예시를 살펴보겠습니다.
Node.js 테스트 예시
// MockPaymentService.js
class MockPaymentService {
processPayment(order) {
console.log(`Mocking payment for order ${order.id}`);
// 결제 처리 모킹
}
}
module.exports = MockPaymentService;
// OrderService.test.js
const MockPaymentService = require('./MockPaymentService');
const OrderService = require('./OrderService');
const mockPaymentService = new MockPaymentService();
const orderService = new OrderService();
orderService.paymentService = mockPaymentService; // 수동으로 모킹 서비스 주입
const order = { id: 1 };
orderService.createOrder(order);
Node.js에서는 테스트를 위해 MockPaymentService
를 생성하고, 이를 수동으로 OrderService
에 주입합니다. 이 과정에서 의존성을 수동으로 교체해야 하므로, 코드가 복잡해질 수 있습니다.
스프링 테스트 예시
@SpringBootTest
public class OrderServiceTest {
@MockBean
private PaymentService paymentService;
@Autowired
private OrderService orderService;
@Test
public void testCreateOrder() {
Order order = new Order();
order.setId(1L);
orderService.createOrder(order);
// 여기서 Mocking된 paymentService가 호출되었는지 확인할 수 있습니다.
verify(paymentService).processPayment(order);
}
}
스프링에서는 @MockBean
을 사용해 PaymentService
를 모킹(Mocking)할 수 있습니다. 테스트 환경에서 이 모킹된 빈이 자동으로 주입되므로, 수동으로 의존성을 관리할 필요가 없습니다. 이는 테스트 코드의 간결함과 유지보수성을 크게 향상시킵니다.
AOP를 통한 공통 기능 적용에 대한 차이를 극단적인 예시로 살펴보겠습니다.
Node.js에서의 AOP 비슷한 구현
// logger.js
function logMethodExecution(target, key, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
console.log(`Executing ${key} with args: ${JSON.stringify(args)}`);
return originalMethod.apply(this, args);
};
return descriptor;
}
// PaymentService.js
class PaymentService {
@logMethodExecution
processPayment(order) {
console.log(`Processing payment for order ${order.id}`);
// 결제 처리 로직
}
}
module.exports = PaymentService;
Node.js에서 AOP와 유사한 기능을 적용하려면 데코레이터나 함수 래핑을 수동으로 구현해야 합니다. 이로 인해 코드가 복잡해지고, 여러 곳에 동일한 로직을 반복해서 적용해야 할 수 있습니다.
스프링의 AOP 예시
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.PaymentService.*(..))")
public void logMethodExecution(JoinPoint joinPoint) {
System.out.println("Executing method: " + joinPoint.getSignature());
}
}
// PaymentService.java
@Service
public class PaymentService {
public void processPayment(Order order) {
System.out.println("Processing payment for order " + order.getId());
// 결제 처리 로직
}
}
스프링에서는 AOP를 사용해 공통 기능(예: 로깅)을 쉽게 적용할 수 있습니다. @Aspect
와 포인트컷을 설정하면, 코드의 수정 없이도 특정 메서드나 클래스에 로깅, 트랜잭션 관리 등의 기능을 적용할 수 있습니다. 이는 코드의 재사용성과 유지보수성을 크게 높여줍니다.
이 포스팅에서의 예시는 스프링의 장점을 설명하기 위해서 극단적으로 사용된 예시입니다.
실제로 Node.js 에서도 보다 효율적인 방법들이 있을 수 있지만 이 게시글에서는 스프링의 장점을 두드러지게 위한 목적이 강조된 예시임을 미리 말씀드립니다.