NestJS vs Spring Boot - 공통점과 차이점 비교

개요

NestJS와 Spring Boot는 각각 Node.js와 Java 생태계에서 가장 인기 있는 백엔드 프레임워크입니다. 두 프레임워크는 놀라울 정도로 유사한 아키텍처와 개념을 가지고 있습니다.

핵심 철학

공통점

  • 엔터프라이즈급 애플리케이션 개발에 최적화
  • 모듈화된 아키텍처로 확장 가능한 구조
  • 의존성 주입(Dependency Injection) 패턴 사용
  • 데코레이터/어노테이션 기반 개발
  • 강력한 타입 시스템 (TypeScript vs Java)
  • 테스트 친화적 구조

차이점

항목 NestJS Spring Boot
언어 TypeScript/JavaScript Java/Kotlin
런타임 Node.js (단일 스레드) JVM (멀티 스레드)
성숙도 2017년 출시 (비교적 신생) 2014년 출시 (매우 성숙)
생태계 npm 생태계 Maven/Gradle 생태계
학습 곡선 중간 높음

아키텍처 비교

모듈 구조

NestJS

// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
imports: [],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}

Spring Boot

// UserConfiguration.java
@Configuration
@ComponentScan(basePackages = "com.example.users")
public class UserConfiguration {

@Bean
public UsersService usersService() {
return new UsersService();
}
}

의존성 주입

NestJS

import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
constructor(
private readonly userRepository: UserRepository,
private readonly emailService: EmailService,
) {}
}

Spring Boot

@Service
public class UsersService {

private final UserRepository userRepository;
private final EmailService emailService;

@Autowired
public UsersService(UserRepository userRepository,
EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
}

공통점: 생성자 기반 의존성 주입 권장

차이점:

  • NestJS는 @Injectable() 데코레이터 필수
  • Spring Boot는 @Autowired 생략 가능 (생성자가 하나일 때)

컨트롤러 비교

REST API 구현

NestJS

import { Controller, Get, Post, Put, Delete, Body, Param } from '@nestjs/common';

@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}

@Get()
findAll() {
return this.usersService.findAll();
}

@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(+id);
}

@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}

@Put(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.update(+id, updateUserDto);
}

@Delete(':id')
remove(@Param('id') id: string) {
return this.usersService.remove(+id);
}
}

Spring Boot

@RestController
@RequestMapping("/users")
public class UsersController {

private final UsersService usersService;

@Autowired
public UsersController(UsersService usersService) {
this.usersService = usersService;
}

@GetMapping
public List<User> findAll() {
return usersService.findAll();
}

@GetMapping("/{id}")
public User findOne(@PathVariable Long id) {
return usersService.findOne(id);
}

@PostMapping
public User create(@RequestBody CreateUserDto createUserDto) {
return usersService.create(createUserDto);
}

@PutMapping("/{id}")
public User update(@PathVariable Long id,
@RequestBody UpdateUserDto updateUserDto) {
return usersService.update(id, updateUserDto);
}

@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
usersService.remove(id);
}
}

공통점:

  • 데코레이터/어노테이션 기반 라우팅
  • RESTful 패턴 지원
  • 자동 직렬화/역직렬화

차이점:

  • NestJS: @Controller(), @Get(), @Post()
  • Spring Boot: @RestController, @GetMapping(), @PostMapping()

데이터베이스 연동

TypeORM (NestJS) vs JPA (Spring Boot)

NestJS + TypeORM

// user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';

@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;

@Column({ unique: true })
email: string;

@Column()
name: string;

@OneToMany(() => Post, post => post.user)
posts: Post[];
}

// users.service.ts
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}

async findAll(): Promise<User[]> {
return this.usersRepository.find({ relations: ['posts'] });
}

async findOne(id: number): Promise<User> {
return this.usersRepository.findOne({
where: { id },
relations: ['posts']
});
}
}

Spring Boot + JPA

// User.java
@Entity
@Table(name = "users")
public class User {

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

@Column(unique = true)
private String email;

@Column
private String name;

@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
private List<Post> posts;
}

// UserRepository.java
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}

// UsersService.java
@Service
public class UsersService {

private final UserRepository userRepository;

public List<User> findAll() {
return userRepository.findAll();
}

public User findOne(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("User not found"));
}
}

공통점:

  • ORM 기반 데이터베이스 접근
  • Entity 클래스로 테이블 정의
  • 관계 매핑 지원 (OneToMany, ManyToOne 등)
  • Repository 패턴

차이점:

  • NestJS: TypeORM, Prisma, Mongoose 선택 가능
  • Spring Boot: JPA/Hibernate가 사실상 표준
  • Spring Data JPA의 메서드 네이밍 쿼리가 더 강력함

요청 처리 파이프라인

NestJS

Client Request

Middleware (로깅, CORS 등)

Guard (인증/인가)

Interceptor (Before)

Pipe (검증/변환)

Controller

Service

Interceptor (After)

Exception Filter

Client Response

Spring Boot

Client Request

Filter (로깅, CORS 등)

Interceptor (HandlerInterceptor)

Argument Resolver

Controller

Service

Interceptor (postHandle)

Exception Handler (@ExceptionHandler)

Client Response

미들웨어/필터/인터셉터 비교

Middleware vs Filter

NestJS Middleware

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`Request: ${req.method} ${req.url}`);
next();
}
}

Spring Boot Filter

@Component
public class LoggerFilter implements Filter {

@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
System.out.println("Request: " + req.getMethod() + " " + req.getRequestURI());
chain.doFilter(request, response);
}
}

Guard vs Security

NestJS Guard

@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
return this.validateRequest(request);
}
}

Spring Boot Security

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic();
}
}

Interceptor

NestJS Interceptor

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const now = Date.now();
return next.handle().pipe(
tap(() => console.log(`After: ${Date.now() - now}ms`))
);
}
}

Spring Boot Interceptor

@Component
public class LoggingInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
long startTime = System.currentTimeMillis();
request.setAttribute("startTime", startTime);
return true;
}

@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
long startTime = (Long) request.getAttribute("startTime");
long endTime = System.currentTimeMillis();
System.out.println("After: " + (endTime - startTime) + "ms");
}
}

검증(Validation)

NestJS (class-validator)

import { IsEmail, IsNotEmpty, MinLength, Min, Max } from 'class-validator';

export class CreateUserDto {
@IsNotEmpty()
@MinLength(3)
name: string;

@IsEmail()
email: string;

@Min(0)
@Max(120)
age: number;
}

// Controller
@Post()
create(@Body(ValidationPipe) createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}

Spring Boot (Bean Validation)

import javax.validation.constraints.*;

public class CreateUserDto {

@NotEmpty
@Size(min = 3)
private String name;

@Email
private String email;

@Min(0)
@Max(120)
private Integer age;
}

// Controller
@PostMapping
public User create(@Valid @RequestBody CreateUserDto createUserDto) {
return usersService.create(createUserDto);
}

공통점:

  • 데코레이터/어노테이션 기반 검증
  • DTO 클래스에 검증 규칙 정의

차이점:

  • NestJS: ValidationPipe 필요
  • Spring Boot: @Valid 어노테이션 사용

예외 처리

NestJS

// Custom Exception
export class UserNotFoundException extends NotFoundException {
constructor(id: number) {
super(`User with ID ${id} not found`);
}
}

// Exception Filter
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const status = exception.getStatus();

response.status(status).json({
statusCode: status,
message: exception.message,
timestamp: new Date().toISOString(),
});
}
}

Spring Boot

// Custom Exception
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("User with ID " + id + " not found");
}
}

// Exception Handler
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
LocalDateTime.now()
);
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
}

설정 관리

NestJS (ConfigModule)

// .env
DATABASE_HOST=localhost
DATABASE_PORT=5432

// app.module.ts
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
],
})
export class AppModule {}

// Service
@Injectable()
export class AppService {
constructor(private configService: ConfigService) {}

getDatabaseHost(): string {
return this.configService.get<string>('DATABASE_HOST');
}
}

Spring Boot

# application.yml
database:
host: localhost
port: 5432
// Service
@Service
public class AppService {

@Value("${database.host}")
private String databaseHost;

// Or using @ConfigurationProperties
}

@Configuration
@ConfigurationProperties(prefix = "database")
public class DatabaseProperties {
private String host;
private Integer port;
}

테스트

NestJS

describe('UsersService', () => {
let service: UsersService;
let repository: Repository<User>;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: {
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
},
},
],
}).compile();

service = module.get<UsersService>(UsersService);
repository = module.get<Repository<User>>(getRepositoryToken(User));
});

it('should find all users', async () => {
const users = [{ id: 1, name: 'John' }];
jest.spyOn(repository, 'find').mockResolvedValue(users);

expect(await service.findAll()).toEqual(users);
});
});

Spring Boot

@SpringBootTest
class UsersServiceTest {

@Autowired
private UsersService usersService;

@MockBean
private UserRepository userRepository;

@Test
void shouldFindAllUsers() {
List<User> users = Arrays.asList(new User(1L, "John"));
when(userRepository.findAll()).thenReturn(users);

List<User> result = usersService.findAll();

assertEquals(users, result);
}
}

공통점:

  • Mock을 활용한 단위 테스트
  • 통합 테스트 지원

차이점:

  • NestJS: Jest 기본 사용
  • Spring Boot: JUnit + Mockito 사용

성능 특성

NestJS

장점:

  • 단일 스레드 이벤트 루프로 I/O 집약적 작업에 유리
  • 낮은 메모리 사용량
  • 빠른 시작 시간

단점:

  • CPU 집약적 작업에 불리
  • 단일 스레드로 인한 확장성 제한

Spring Boot

장점:

  • 멀티 스레드로 CPU 집약적 작업에 유리
  • 대규모 엔터프라이즈 애플리케이션에 적합
  • JVM 최적화 혜택

단점:

  • 높은 메모리 사용량
  • 느린 시작 시간 (특히 대규모 앱)

배포 및 스케일링

NestJS

# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist ./dist
CMD ["node", "dist/main"]
# 수평 확장 (PM2)
pm2 start dist/main.js -i max

Spring Boot

# Dockerfile
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY target/*.jar app.jar
CMD ["java", "-jar", "app.jar"]
# 수평 확장 (Kubernetes, Docker Swarm 등)
kubectl scale deployment myapp --replicas=5

생태계 및 커뮤니티

NestJS

장점:

  • npm 생태계 활용 (방대한 라이브러리)
  • 빠르게 성장하는 커뮤니티
  • 모던한 개발 경험

단점:

  • 상대적으로 작은 커뮤니티
  • 엔터프라이즈 지원 도구 부족

Spring Boot

장점:

  • 거대한 생태계 (Spring Cloud, Spring Security 등)
  • 오랜 기간 검증된 안정성
  • 풍부한 엔터프라이즈 기능
  • 방대한 문서와 자료

단점:

  • 학습 곡선이 가파름
  • 보일러플레이트 코드 많음 (개선되고 있음)

사용 사례

NestJS가 적합한 경우

  • 실시간 애플리케이션 (채팅, 알림)
  • I/O 집약적 마이크로서비스
  • JavaScript/TypeScript 팀
  • 빠른 프로토타이핑
  • 풀스택 JavaScript 환경

Spring Boot가 적합한 경우

  • 대규모 엔터프라이즈 애플리케이션
  • CPU 집약적 작업
  • 복잡한 트랜잭션 처리
  • Java 생태계 활용이 필요한 경우
  • 금융, 은행 등 고도의 안정성이 필요한 시스템

코드 비교 요약표

기능 NestJS Spring Boot
컨트롤러 @Controller() @RestController
서비스 @Injectable() @Service
의존성 주입 constructor(private service) @Autowired
모듈 @Module() @Configuration
라우팅 @Get(), @Post() @GetMapping(), @PostMapping()
경로 변수 @Param() @PathVariable
요청 본문 @Body() @RequestBody
쿼리 파라미터 @Query() @RequestParam
Entity TypeORM @Entity() JPA @Entity
Repository Repository<T> JpaRepository<T, ID>

마이그레이션 시 고려사항

Spring Boot → NestJS

장점:

  • TypeScript의 타입 안정성
  • npm 생태계 활용
  • 낮은 인프라 비용

고려사항:

  • ORM 변경 (JPA → TypeORM)
  • 동기 → 비동기 프로그래밍 패러다임 전환
  • 트랜잭션 처리 방식 차이

NestJS → Spring Boot

장점:

  • 엔터프라이즈급 기능
  • 더 나은 멀티스레딩
  • Spring 생태계 활용

고려사항:

  • Java/Kotlin 학습 필요
  • 높은 메모리 요구사항
  • 더 긴 빌드/시작 시간

결론

선택 가이드

NestJS를 선택하세요:

  • ✅ JavaScript/TypeScript 개발자
  • ✅ 빠른 개발 속도가 중요
  • ✅ 실시간/I/O 집약적 애플리케이션
  • ✅ 작은 팀, 스타트업

Spring Boot를 선택하세요:

  • ✅ Java 개발자
  • ✅ 대규모 엔터프라이즈 시스템
  • ✅ 복잡한 비즈니스 로직
  • ✅ 금융/은행권 등 고도의 안정성 필요

공통점 정리

두 프레임워크 모두:

  • 🎯 의존성 주입 기반 아키텍처
  • 🎯 모듈화된 구조
  • 🎯 데코레이터/어노테이션 패턴
  • 🎯 강력한 타입 시스템
  • 🎯 테스트 친화적 설계
  • 🎯 풍부한 생태계

NestJS는 Spring Boot의 우수한 아키텍처를 Node.js 세계로 가져온 것이라 할 수 있으며, Spring Boot 경험이 있다면 NestJS를 쉽게 배울 수 있고 그 반대도 마찬가지입니다!

참고 자료

Share