1. 라이브러리
  2. HTTP와 웹
  3. 웹 아키텍처

업데이트됨 1개월 전

은행 잔액을 확인할 때, 화면에 나타나는 숫자는 어딘가에 파일로 저장되어 있다가 전달되는 게 아닙니다. 그 순간, 바로 당신을 위해 계산됩니다—데이터베이스를 조회하고, 규칙을 적용하고, 권한을 확인하고, 응답을 조립하면서요. 그 일을 하는 소프트웨어가 바로 애플리케이션 서버입니다.

웹 서버는 대화를 담당합니다. HTTP 요청을 받고, 연결을 관리하고, 정적 파일을 제공합니다. 애플리케이션 서버는 생각을 담당합니다. 코드를 실행하고, 데이터를 처리하고, 비즈니스 로직을 적용하고, 동적 콘텐츠를 생성합니다. 웹 서버는 말하는 법을 압니다. 애플리케이션 서버는 무슨 말을 해야 하는지를 압니다.

애플리케이션 서버가 실제로 하는 일

온라인에서 일어나는 모든 동적 상호작용은 애플리케이션 서버를 거칩니다. 상품을 검색하면 애플리케이션 서버는 재고 데이터베이스를 조회하고, 필터를 적용하고, 관련도에 따라 결과를 정렬하고, 배송 견적을 위해 위치를 확인한 다음 응답을 조립합니다. 소셜 미디어에 게시물을 올리면 애플리케이션 서버는 콘텐츠를 검증하고, 데이터베이스에 저장하고, 팔로워들의 피드를 업데이트하고, 알림을 발송하고, 분석을 위해 활동을 기록합니다.

애플리케이션 서버는 이 코드를 실행하는 런타임 환경을 제공하며, 거의 모든 애플리케이션에 필요한 서비스들도 함께 제공합니다. 데이터베이스 연결, 트랜잭션 관리, 보안 적용, 세션 추적, 메시징이 그것입니다. 이런 서비스가 없다면 개발자마다 모든 애플리케이션에서 동일한 인프라를 처음부터 다시 구축해야 할 것입니다.

요청 흐름

동적 콘텐츠에 대한 요청은 예측 가능한 경로를 따릅니다. 웹 서버가 HTTP 요청을 받고, 이것이 애플리케이션 처리가 필요하다는 걸 인식합니다—정적 파일이 아니기 때문입니다. 웹 서버는 요청을 애플리케이션 서버로 전달하고, 애플리케이션 서버는 URL, HTTP 메서드 또는 다른 규칙에 따라 적절한 코드로 라우팅합니다.

애플리케이션 코드가 실행됩니다. 사용자를 인증하고, 데이터베이스를 조회하고, 외부 API를 호출하고, 제출된 데이터를 검증하고, 비즈니스 규칙을 적용하고, 출력을 생성할 수 있습니다. 애플리케이션 서버는 이 실행을 관리합니다—데이터베이스 접근을 제공하고, 오류를 처리하고, 보안을 적용하고, 메모리를 관리하면서요.

코드가 응답을 생성하면, 다시 흘러갑니다. 애플리케이션 서버에서 웹 서버로, 그리고 클라이언트로. 전체 과정은 보통 몇 밀리초면 끝납니다.

핵심 서비스

데이터베이스 커넥션 풀링은 비용이 큰 문제를 해결합니다. 매 요청마다 새 데이터베이스 연결을 만드는 건 느립니다—TCP 핸드셰이크, 인증, 설정이 필요하죠. 커넥션 풀은 요청들이 공유할 수 있는 연결을 미리 준비해두어 성능을 크게 높이고 데이터베이스 부하를 줄입니다.

트랜잭션 관리는 여러 단계에 걸친 작업의 일관성을 보장합니다. 계좌 간 송금을 생각해보세요. 한 계좌에서 출금하고, 다른 계좌에 입금합니다. 출금은 성공했는데 입금이 실패하면 돈이 사라집니다. 트랜잭션 관리는 완료되지 않은 작업을 롤백하여 개발자가 롤백 로직을 직접 작성하지 않아도 데이터 무결성을 유지합니다.

보안 적용은 인증(당신이 누구인지), 인가(당신이 무엇을 할 수 있는지), 그리고 공격 방어를 처리합니다. 애플리케이션 서버는 LDAP 같은 엔터프라이즈 시스템과 통합하고, 역할 기반 접근 제어를 관리하며, SQL 인젝션, 크로스 사이트 스크립팅 등 다양한 위협으로부터 보호합니다.

세션 관리는 요청 전반에 걸쳐 사용자 상태를 추적합니다. HTTP는 무상태입니다—각 요청은 독립적입니다. 하지만 사용자는 연속성을 기대합니다. 장바구니는 유지되고, 로그인 상태는 지속되고, 설정은 기억됩니다. 애플리케이션 서버는 이 세션 데이터를 저장하고 불러와서, 무상태 프로토콜 위에서 상태가 있는 경험을 제공합니다.

비동기 처리는 느린 작업을 사용자 경로 밖으로 밀어냅니다. 확인 이메일 발송, 업로드된 파일 처리, 보고서 생성—이런 작업들이 응답을 막아서는 안 됩니다. 메시지 큐는 이런 작업들을 백그라운드로 넘길 수 있게 해줍니다.

캐싱은 계산 비용이 높은 결과를 메모리에 저장합니다. 데이터베이스를 한 번 조회하고, 결과를 캐시하고, 이후 요청은 캐시에서 처리합니다. 핵심 과제는 무효화—캐시된 데이터가 낡았는지 파악하는 것입니다.

언어 생태계

언어마다 애플리케이션 서빙에 다르게 접근합니다.

Java는 가장 풍부한 애플리케이션 서버 전통을 가지고 있습니다. WildFly 같은 완전한 Jakarta EE 서버는 포괄적인 엔터프라이즈 서비스를 제공합니다. Tomcat 같은 가벼운 서블릿 컨테이너는 웹 애플리케이션에 집중합니다. 현대의 Spring Boot 애플리케이션은 Tomcat을 직접 내장하는 경우가 많습니다—애플리케이션 자체가 서버가 됩니다.

Node.js는 이벤트 기반 런타임을 통해 HTTP를 직접 처리합니다. Express 같은 프레임워크가 라우팅과 미들웨어를 추가합니다. 단일 스레드 이벤트 루프는 I/O 집약적인 워크로드를 효율적으로 처리합니다.

Python 애플리케이션은 Gunicorn이나 uWSGI 같은 서버에서 실행되며, Django나 FastAPI 같은 프레임워크를 호스팅합니다. WSGI 표준은 서버와 애플리케이션이 어떻게 통신할지를 정의합니다.

Ruby는 Rails 애플리케이션을 호스팅하기 위해 Puma나 Unicorn을 사용하며, Rack 표준이 인터페이스 계약을 제공합니다.

.NET 애플리케이션은 IIS(Windows) 또는 Kestrel(크로스 플랫폼)에서 실행되며, 웹 서버와 애플리케이션 서버 역할을 결합하는 경우가 많습니다.

PHP는 Apache의 mod_php나 PHP-FPM을 통해 실행되며, 웹 서버가 각 요청에 대해 PHP를 호출합니다.

웹 서버 vs. 애플리케이션 서버

경계가 흐릿해질 때도 이 구분은 중요합니다.

웹 서버는 HTTP에 강합니다. 요청을 파싱하고, 연결을 관리하고, 파일을 제공하고, SSL을 종료하고, 백엔드로 라우팅합니다. I/O와 네트워크 작업에 최적화되어 있습니다.

애플리케이션 서버는 연산에 강합니다. 코드를 실행하고, 트랜잭션을 관리하고, 비즈니스 규칙을 적용하고, 데이터베이스와 통합합니다. 처리 및 데이터 작업에 최적화되어 있습니다.

전통적인 아키텍처에서는 이것이 명확하게 분리되어 있었습니다. Apache가 웹 서버, WebLogic이 애플리케이션 서버. 현대 아키텍처에서는 종종 결합됩니다—Node.js 애플리케이션이 하나의 프로세스에서 HTTP와 비즈니스 로직을 모두 처리합니다.

하지만 결합된 시스템에서도 개념적 분리는 여전히 유용합니다. HTTP 관련 코드와 비즈니스 로직을 처리하는 코드가 어떤 것인지 이해하면 아키텍처가 개선됩니다. 그리고 독립형 애플리케이션도 보통은 SSL, 정적 파일, 로드 밸런싱을 처리하는 Nginx나 다른 리버스 프록시 뒤에 자리합니다.

물리적 분리는 사라질 수 있습니다. 개념적 분리는 여전히 가치 있습니다.

배포 패턴

단일 인스턴스는 하나의 서버에서 모든 것을 실행합니다. 배포와 디버깅이 간단합니다. 단일 장애점이 됩니다. 하드웨어를 업그레이드하는 방법으로만 확장할 수 있습니다.

수평 확장은 로드 밸런서 뒤에 여러 인스턴스를 실행합니다. 이중화—하나의 인스턴스가 실패해도 다른 인스턴스가 계속 작동합니다. 다운타임 없는 롤링 배포. 인스턴스를 추가해서 확장합니다. 무상태 설계나 공유 세션 저장소가 필요합니다.

마이크로서비스는 애플리케이션을 각자의 애플리케이션 서버를 가진 독립적인 서비스들로 분리합니다. 서비스마다 다른 기술을 사용하고, 독립적으로 확장하고, 독립적으로 배포할 수 있습니다. 복잡성은 서비스 간 조율, 네트워크 오버헤드, 분산 디버깅으로 이동합니다.

컨테이너는 애플리케이션과 서버를 함께 패키징해서 환경 간 일관성을 보장합니다. Kubernetes와 유사한 플랫폼이 컨테이너 배포를 관리합니다. 이제 현대 애플리케이션의 표준 패턴으로 자리 잡았습니다.

서버리스는 서버 관리를 완전히 없앱니다. AWS Lambda와 유사한 플랫폼은 이벤트에 반응해 코드를 실행하며, 0에서 수천 개까지 자동으로 확장됩니다. 관리할 인프라가 없고, 실행 시간에 대해서만 비용을 냅니다. 제약으로는 실행 시간 제한, 콜드 스타트 지연, 엄격한 무상태 요구사항이 있습니다.

성능 고려사항

동시성 모델은 서버가 동시 요청을 어떻게 처리하는지를 결정합니다. 요청마다 스레드를 할당하는 방식은 단순하지만 리소스를 많이 소모합니다. 이벤트 기반 모델은 논블로킹 I/O를 통해 적은 스레드로 많은 요청을 처리합니다. 워커 풀은 리소스 사용과 처리량 사이의 균형을 맞춥니다.

리소스 튜닝은 스레드 풀 크기, 연결 제한, 메모리 할당, 타임아웃을 조정하는 작업입니다. 리소스가 너무 적으면 처리량이 제한됩니다. 너무 많으면 고갈됩니다.

캐싱은 잘 적용하면 극적인 성능 향상을 가져옵니다. 어려운 문제는 무효화—캐시된 데이터가 낡았는지 파악하는 것입니다.

데이터베이스 최적화는 종종 전체 성능을 좌우합니다. 커넥션 풀링, 쿼리 효율성, 적절한 인덱싱, 전략적 캐싱.

백그라운드 처리는 느린 작업을 미룸으로써 응답을 빠르게 유지합니다. 이메일 발송이나 보고서 생성을 사용자가 기다릴 필요는 없습니다.

모니터링

프로덕션 시스템에는 가시성이 필요합니다.

APM 도구는 지연 시간, 오류, 처리량, 리소스 사용을 추적합니다. 느린 쿼리, 문제 있는 API 호출, 비효율적인 코드 경로를 찾아냅니다.

로깅은 이벤트와 오류를 기록합니다. 구조화된 JSON 로깅은 강력한 분석을 가능하게 합니다. 중앙화된 시스템은 모든 인스턴스의 로그를 수집합니다.

메트릭은 정량적 데이터를 제공합니다. 초당 요청 수, 응답 시간, 오류율, 큐 깊이. 시계열 데이터베이스가 이것을 시각화와 알림을 위해 저장합니다.

분산 추적은 서비스 전반에 걸쳐 요청을 따라가며, 하나의 사용자 행동이 수많은 시스템에 영향을 미치는 마이크로서비스 디버깅에 필수적입니다.

헬스 체크는 로드 밸런서가 비정상 인스턴스를 감지하고 우회할 수 있게 합니다.

애플리케이션 서버에 대한 자주 묻는 질문

Node.js나 Spring Boot를 사용한다면 별도의 애플리케이션 서버가 필요한가요?

꼭 그렇지는 않습니다. 이 프레임워크들은 애플리케이션 서버 기능을 직접 내장합니다. 애플리케이션이 HTTP와 비즈니스 로직을 모두 처리합니다. 하지만 SSL 종료, 정적 파일 제공, 로드 밸런싱을 위해 앞단에 Nginx 같은 리버스 프록시를 두는 것이 좋습니다.

애플리케이션 서버와 API의 차이는 무엇인가요?

API는 인터페이스입니다—클라이언트가 서비스를 요청하는 방법을 정의하는 계약입니다. 애플리케이션 서버는 그 API를 구현하고 실행하는 소프트웨어입니다. API는 메뉴이고, 애플리케이션 서버는 주방입니다.

서버리스와 전통적인 애플리케이션 서버 중 어떻게 선택하나요?

서버리스는 변동이 심한 워크로드, 이벤트 기반 처리, 인프라 관리 없이 운영하고 싶을 때 탁월합니다. 전통적인 서버는 안정적인 트래픽, 장시간 실행되는 프로세스, 또는 런타임 환경을 세밀하게 제어해야 할 때 더 잘 맞습니다. 많은 아키텍처가 둘 다를 함께 사용합니다.

Java 애플리케이션에는 왜 그렇게 많은 애플리케이션 서버 옵션이 있나요?

Java의 엔터프라이즈 역사는 여러 벤더가 구현한 포괄적인 표준(Java EE, 현재는 Jakarta EE)을 낳았습니다. 생태계는 모든 기능을 갖춘 완전한 엔터프라이즈 서버부터 웹 애플리케이션만 실행하는 최소한의 컨테이너까지 다양합니다. 이 분화는 서로 다른 필요를 반영합니다—일부 애플리케이션은 분산 트랜잭션과 메시징이 필요하고, 다른 것들은 HTTP 요청만 처리하면 충분합니다.

이 페이지가 도움이 되었나요?

😔
🤨
😃