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

개요

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

기본 정보

NestJS

  • 언어: TypeScript (JavaScript)
  • 런타임: Node.js
  • 출시: 2017년
  • 철학: Angular에서 영감을 받은 프로그레시브 Node.js 프레임워크
  • 기반: Express.js (또는 Fastify)

Spring Boot

  • 언어: Java (Kotlin, Groovy 지원)
  • 런타임: JVM
  • 출시: 2014년
  • 철학: Spring Framework 기반의 빠른 애플리케이션 개발
  • 기반: Spring Framework

주요 공통점

1. 의존성 주입 (Dependency Injection)

두 프레임워크 모두 의존성 주입을 핵심으로 사용합니다.

NestJS

@Injectable()
export class UsersService {
constructor(
private readonly usersRepository: Repository<User>,
private readonly emailService: EmailService,
) {}
}

Spring Boot

@Service
public class UsersService {
private final UsersRepository usersRepository;
private final EmailService emailService;

@Autowired
public UsersService(UsersRepository usersRepository, EmailService emailService) {
this.usersRepository = usersRepository;
this.emailService = emailService;
}
}

2. 데코레이터/어노테이션 기반 개발

NestJS - 데코레이터

@Controller('users')
export class UsersController {
@Get()
findAll() {
return this.usersService.findAll();
}

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

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

Spring Boot - 어노테이션

@RestController
@RequestMapping("/users")
public class UsersController {
@GetMapping
public List<User> findAll() {
return usersService.findAll();
}

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

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

3. 모듈 기반 아키텍처

NestJS

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

Spring Boot

@Configuration
@ComponentScan(basePackages = "com.example.users")
public class UsersConfig {
// 설정 및 빈 정의
}

4. 계층화된 아키텍처

두 프레임워크 모두 Controller → Service → Repository 패턴을 사용합니다.

공통 구조

Controller Layer (API 엔드포인트)

Service Layer (비즈니스 로직)

Repository Layer (데이터 액세스)

Database

5. AOP (Aspect-Oriented Programming) 지원

NestJS - 인터셉터, 가드, 파이프

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

@UseInterceptors(LoggingInterceptor)
@Controller('users')
export class UsersController {}

Spring Boot - AOP

@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.example.controller.*.*(..))")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object proceed = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - start;
System.out.println(joinPoint.getSignature() + " executed in " + executionTime + "ms");
return proceed;
}
}

6. ORM 통합

NestJS - TypeORM

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

@Column()
name: string;

@Column()
email: string;

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

Spring Boot - JPA/Hibernate

@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column
private String name;

@Column
private String email;

@OneToMany(mappedBy = "user")
private List<Post> posts;
}

7. 예외 처리

NestJS - Exception Filters

@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,
});
}
}

Spring Boot - Exception Handler

@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage()
);
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
}

8. 환경 설정 관리

NestJS - ConfigModule

// .env
DATABASE_HOST=localhost
DATABASE_PORT=3306

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

// 사용
constructor(private configService: ConfigService) {
const host = this.configService.get('DATABASE_HOST');
}

Spring Boot - application.properties

# application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root

# 사용
@Value("${spring.datasource.url}")
private String databaseUrl;

주요 차이점

1. 언어 및 타입 시스템

NestJS - TypeScript

interface User {
id: number;
name: string;
email: string;
}

class UsersService {
async findOne(id: number): Promise<User | null> {
// 타입 안정성 제공, 하지만 런타임에는 없어짐
return null;
}
}

장점:

  • 동적 타이핑의 유연성
  • 빠른 개발 속도
  • JavaScript 생태계 활용

단점:

  • 런타임 타입 체크 불가
  • 상대적으로 약한 타입 시스템

Spring Boot - Java

public class UsersService {
public Optional<User> findOne(Long id) {
// 컴파일 타임 + 런타임 타입 안정성
return Optional.empty();
}
}

장점:

  • 강력한 타입 시스템
  • 컴파일 타임 에러 검출
  • 런타임 타입 안정성

단점:

  • 상대적으로 긴 코드
  • 보일러플레이트 코드

2. 성능 특성

NestJS (Node.js)

// 비동기 I/O에 최적화
@Get()
async findAll() {
const users = await this.usersService.findAll();
const posts = await this.postsService.findAll();
// 동시 실행
const [users, posts] = await Promise.all([
this.usersService.findAll(),
this.postsService.findAll(),
]);
return { users, posts };
}

특징:

  • 단일 스레드, 이벤트 루프 기반
  • I/O 집약적 작업에 우수
  • 낮은 메모리 사용량
  • 동시성 처리에 강함

적합한 경우:

  • 실시간 애플리케이션
  • 마이크로서비스
  • API 게이트웨이
  • WebSocket 서버

Spring Boot (Java)

// 멀티스레드 기반
@GetMapping
public Map<String, Object> findAll() {
// 각 요청이 별도 스레드에서 처리
List<User> users = usersService.findAll();
List<Post> posts = postsService.findAll();
return Map.of("users", users, "posts", posts);
}

// CompletableFuture로 비동기 처리
@GetMapping("/async")
public CompletableFuture<Map<String, Object>> findAllAsync() {
CompletableFuture<List<User>> usersFuture =
CompletableFuture.supplyAsync(() -> usersService.findAll());
CompletableFuture<List<Post>> postsFuture =
CompletableFuture.supplyAsync(() -> postsService.findAll());

return usersFuture.thenCombine(postsFuture,
(users, posts) -> Map.of("users", users, "posts", posts));
}

특징:

  • 멀티스레드 기반
  • CPU 집약적 작업에 우수
  • JVM 최적화
  • 안정적인 성능

적합한 경우:

  • 대규모 엔터프라이즈 애플리케이션
  • CPU 집약적 작업
  • 복잡한 비즈니스 로직
  • 금융, 의료 등 미션 크리티컬 시스템

3. 생태계 및 패키지 관리

NestJS - npm/yarn

# package.json
{
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/typeorm": "^10.0.0",
"express": "^4.18.0"
}
}

npm install @nestjs/passport

특징:

  • npm: 세계 최대 패키지 레지스트리
  • 빠른 패키지 설치
  • 다양한 라이브러리

단점:

  • 의존성 관리 복잡도
  • 보안 취약점 관리 필요

Spring Boot - Maven/Gradle

<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.0</version>
</dependency>

특징:

  • 안정적인 의존성 관리
  • 중앙 집중식 저장소
  • 엄격한 버전 관리

4. 데이터베이스 마이그레이션

NestJS - TypeORM Migrations

// migration/1234567890-CreateUsersTable.ts
export class CreateUsersTable1234567890 implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'users',
columns: [
{ name: 'id', type: 'int', isPrimary: true, isGenerated: true },
{ name: 'name', type: 'varchar' },
{ name: 'email', type: 'varchar' },
],
}),
);
}

async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('users');
}
}

Spring Boot - Flyway/Liquibase

-- V1__Create_users_table.sql
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

5. 테스트

NestJS - Jest

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().mockResolvedValue([]),
},
},
],
}).compile();

service = module.get<UsersService>(UsersService);
});

it('should find all users', async () => {
const result = await service.findAll();
expect(result).toEqual([]);
});
});

Spring Boot - JUnit

@SpringBootTest
class UsersServiceTest {
@Autowired
private UsersService usersService;

@MockBean
private UsersRepository usersRepository;

@Test
void shouldFindAllUsers() {
when(usersRepository.findAll()).thenReturn(Collections.emptyList());

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

assertThat(result).isEmpty();
}
}

6. 비동기 처리

NestJS - async/await (기본)

@Injectable()
export class UsersService {
async createUser(dto: CreateUserDto): Promise<User> {
const user = await this.usersRepository.save(dto);
await this.emailService.sendWelcome(user.email);
await this.logService.logUserCreation(user.id);
return user;
}

// 병렬 처리
async getUserData(userId: number) {
const [user, posts, comments] = await Promise.all([
this.usersRepository.findOne(userId),
this.postsRepository.findByUser(userId),
this.commentsRepository.findByUser(userId),
]);
return { user, posts, comments };
}
}

Spring Boot - @Async 또는 CompletableFuture

@Service
public class UsersService {
@Async
public CompletableFuture<User> createUser(CreateUserDto dto) {
User user = usersRepository.save(dto);
emailService.sendWelcome(user.getEmail());
logService.logUserCreation(user.getId());
return CompletableFuture.completedFuture(user);
}

// 병렬 처리
public UserData getUserData(Long userId) {
CompletableFuture<User> userFuture =
CompletableFuture.supplyAsync(() -> usersRepository.findById(userId).orElse(null));
CompletableFuture<List<Post>> postsFuture =
CompletableFuture.supplyAsync(() -> postsRepository.findByUserId(userId));
CompletableFuture<List<Comment>> commentsFuture =
CompletableFuture.supplyAsync(() -> commentsRepository.findByUserId(userId));

return new UserData(
userFuture.join(),
postsFuture.join(),
commentsFuture.join()
);
}
}

7. 실시간 통신

NestJS - WebSocket (기본 지원)

@WebSocketGateway()
export class ChatGateway {
@SubscribeMessage('message')
handleMessage(@MessageBody() data: string): string {
return 'Hello ' + data;
}

@WebSocketServer()
server: Server;

sendToAll(message: string) {
this.server.emit('message', message);
}
}

Spring Boot - WebSocket

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
}

@Controller
public class ChatController {
@MessageMapping("/message")
@SendTo("/topic/messages")
public String handleMessage(String message) {
return "Hello " + message;
}
}

8. 메모리 사용량 및 시작 시간

NestJS

# 시작 시간: 1-2초
# 메모리: 30-50MB (기본 애플리케이션)

$ npm run start
Application started in 1.5s

특징:

  • 빠른 시작 시간
  • 낮은 메모리 사용량
  • 컨테이너 환경에 최적

Spring Boot

# 시작 시간: 3-5초 (일반), GraalVM Native는 더 빠름
# 메모리: 300-500MB (기본 애플리케이션)

$ java -jar app.jar
Application started in 4.2s

특징:

  • 상대적으로 긴 시작 시간
  • 높은 메모리 사용량
  • 안정적인 장기 실행

9. 마이크로서비스 지원

NestJS

// 마이크로서비스 설정
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.TCP,
options: { port: 3001 },
},
);

// 메시지 패턴
@MessagePattern({ cmd: 'get-user' })
getUser(userId: number): User {
return this.usersService.findOne(userId);
}

지원:

  • TCP, Redis, NATS, RabbitMQ, Kafka, gRPC

Spring Boot

// Spring Cloud를 통한 마이크로서비스
@SpringBootApplication
@EnableEurekaClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}

@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/users/{id}")
User getUser(@PathVariable Long id);
}

지원:

  • Spring Cloud (Eureka, Config Server, Gateway)
  • 완전한 마이크로서비스 생태계

프로젝트 구조 비교

NestJS

src/
├── app.module.ts
├── main.ts
├── users/
│ ├── users.module.ts
│ ├── users.controller.ts
│ ├── users.service.ts
│ ├── users.entity.ts
│ └── dto/
│ ├── create-user.dto.ts
│ └── update-user.dto.ts
├── posts/
│ ├── posts.module.ts
│ ├── posts.controller.ts
│ └── posts.service.ts
└── common/
├── guards/
├── interceptors/
└── filters/

Spring Boot

src/main/java/com/example/
├── Application.java
├── config/
├── controller/
│ ├── UsersController.java
│ └── PostsController.java
├── service/
│ ├── UsersService.java
│ └── PostsService.java
├── repository/
│ ├── UsersRepository.java
│ └── PostsRepository.java
├── model/
│ ├── User.java
│ └── Post.java
└── dto/
├── CreateUserDto.java
└── UpdateUserDto.java

언제 어떤 프레임워크를 선택할까?

NestJS를 선택하는 경우

추천 상황:

  • 실시간 애플리케이션 (채팅, 알림)
  • 빠른 프로토타이핑
  • JavaScript/TypeScript 팀
  • 마이크로서비스 아키텍처
  • WebSocket 중심 애플리케이션
  • 낮은 리소스 환경
  • 스타트업 및 MVP 개발

예시 프로젝트:

  • 실시간 대시보드
  • 채팅 애플리케이션
  • IoT 플랫폼
  • API 게이트웨이
  • 스트리밍 서비스

Spring Boot를 선택하는 경우

추천 상황:

  • 대규모 엔터프라이즈 애플리케이션
  • 복잡한 비즈니스 로직
  • 금융, 의료 등 미션 크리티컬 시스템
  • CPU 집약적 작업
  • 장기 유지보수가 필요한 프로젝트
  • Java 생태계 활용
  • 엄격한 타입 안정성 요구

예시 프로젝트:

  • 은행 시스템
  • ERP 시스템
  • 대규모 전자상거래 플랫폼
  • 데이터 처리 파이프라인
  • 레거시 시스템 통합

성능 비교

간단한 CRUD API 벤치마크 (참고용)

동시 사용자: 1000명
요청 수: 10,000

NestJS (Express):
- 평균 응답 시간: 15ms
- 처리량: 8,000 req/s
- 메모리: 120MB

NestJS (Fastify):
- 평균 응답 시간: 12ms
- 처리량: 10,000 req/s
- 메모리: 100MB

Spring Boot (Tomcat):
- 평균 응답 시간: 20ms
- 처리량: 7,000 req/s
- 메모리: 400MB

Spring Boot (WebFlux):
- 평균 응답 시간: 18ms
- 처리량: 9,000 req/s
- 메모리: 350MB

실제 성능은 애플리케이션 특성에 따라 크게 달라질 수 있습니다.

학습 곡선

NestJS

  • 진입 장벽: 중간
  • 사전 지식: JavaScript/TypeScript, Node.js
  • 학습 시간: 1-2주 (기본), 1-2개월 (숙련)
  • 문서화: 우수
  • 커뮤니티: 빠르게 성장 중

Spring Boot

  • 진입 장벽: 중간-높음
  • 사전 지식: Java, OOP, Spring Framework
  • 학습 시간: 2-4주 (기본), 2-3개월 (숙련)
  • 문서화: 매우 우수
  • 커뮤니티: 매우 크고 성숙

채용 시장

NestJS

  • 스타트업과 모던 기업에서 증가 추세
  • Node.js 개발자 전환이 용이
  • 풀스택 개발자에게 인기

Spring Boot

  • 전통적으로 강한 수요
  • 대기업 및 금융권에서 선호
  • 안정적인 채용 시장

결론

유사점 요약

  • 의존성 주입 패턴
  • 데코레이터/어노테이션 기반
  • 모듈화된 아키텍처
  • 계층화된 구조 (Controller-Service-Repository)
  • ORM 통합
  • AOP 지원
  • 강력한 생태계

주요 차이점 요약

특성 NestJS Spring Boot
언어 TypeScript Java
성능 모델 단일 스레드, 이벤트 루프 멀티 스레드
시작 시간 빠름 (1-2초) 느림 (3-5초)
메모리 낮음 (30-50MB) 높음 (300-500MB)
최적 용도 I/O 집약적, 실시간 CPU 집약적, 복잡한 로직
타입 안정성 컴파일 타임 컴파일+런타임
생태계 npm (빠르게 성장) Maven/Gradle (성숙)

최종 선택 가이드

NestJS: 빠른 개발, 실시간 기능, 마이크로서비스, 낮은 리소스

Spring Boot: 안정성, 복잡한 로직, 엔터프라이즈, 장기 유지보수

둘 다 훌륭한 선택: 팀의 기술 스택, 프로젝트 요구사항, 조직 문화에 따라 선택하세요!

참고 자료

Share