728x90

최근 아키텍처 공부를 하면서 흔히 접할 수 있는 Clean ArchitectureHexagonal Architecture를 만나게 되었습니다.

 

여러 블로그들과 글들을 읽으면서 Clean Architecture와 Hexagonal Architecture의 다른 점이 무엇인지 점점 모호해지는 것을 느꼈습니다.

 

왜냐면 두 아키텍처에서는 Entity를 중심에 두고 그다음 UseCase를 통해 로직을 정의 한 다음 그다음 DIP를 적용하여 의존성을 내부 Core로 의존관계를 형성하면서, 외부의 변경 ( API변경, DB변경 등 )으로부터 내부 구현체들은 안전하다는 장점을 띄고 있기 때문입니다.

 

그렇다면 두 아키텍처는 어떤 점에서 차이를 보이고 있는지 알아보도록 하겠습니다.


Clean Architecture - Robert C. Martin

클린 아키텍처를 한 번이라도 보았다면 많이 익숙한 그림일 것입니다.

해당 그림은 의존관계를 내부로 의존하도록 하는 것을 가시화하여 보여주고 있습니다.

 

그림과 같은 의존관계를 형성한다면 파란색 원 즉 UI, Web, DB와 같은 외부 시스템 변경이 있다 하더라도 Application내부에는 영향이 없는 관계를 형성할 수 있습니다.

 

즉, 우리가 보호하려 하는 도메인의 정책 및 비즈니스 규칙들이 보호된다는 강점을 가질 수 있습니다.

 

클린 아키텍처에서는 다음과 같은 특징을 가집니다.

  • 프레임워크에 독립적이다.
  • 비즈니스 로직들 즉 내부 로직들은 외부 통신 없이 테스트 가능하다.
  • 내부 영향 없이 쉽게 외부 시스템을 변경할 수 있다.
  • 데이터베이스 종류와 관계없이 변경할 수 있다.
  • 외부 시스템에서는 Application 내부의 일에 대해 알 수 없다.

이러한 특징들은 외부와 분리된 흔히 모듈화 된 Application를 만들 수 있다는 점이 있습니다.

 

그럼 클린 아키텍처에서는 어떤 점을 중요하게 보아야 할까? 바로 종속성 규칙을 가장 중요하게 보면 될 거 같습니다.

종속성 규칙이 유효하다면 위 그림에 존재하는 원의 개수가 달라진다 하더라도 클린 아키텍처를 따르고 있다고 볼 수 있습니다.

 

다만, 종속성 규칙을 위배한다면 클린 아키텍처를 온전히 적용하였다고 보기는 어려울 것 같습니다.


Hexagonal Architecture - Alistair Cockburn

해당 아키텍처는 2005년에 처음 소개하면서 "Ports and Adapters"아키텍처라고 불렸습니다.

처음 소개 된 이름을 보면 감이 오시겠지만 핵사고 날 아키텍처에서 가장 중요한 개념은 Ports와 Adapters입니다.

 

Ports는 시스템이 외부와 통신하는 인터페이스를 정의하며,

AdaptersPorts와 외부 시스템 간의 실제 구현을 담당합니다.

 

즉, 이 또한 DIP를 활용하여 Application에서 외부에 의존하지 않고, Ports라는 Interface를 제공함으로써 Application이 하나의 모듈화 되는 아키텍처입니다.

 

그렇다면 왜 이 아키텍처 이름은 "Hexagonal Architecture"라 불리게 되었는지 의문이 들게 됩니다.

왜 "Hexagonal"인가?

  1. 다양한 외부 인터페이스 표현
    육각형은 여러 방향에서 포트를 연결할 수 있는 모양을 상징합니다. 이는 애플리케이션이 다양한 외부 시스템과 상호작용할 수 있다는 점을 강조합니다.
  2. 대칭성
    육각형의 대칭성은 애플리케이션이 외부 시스템과의 관계에서 특정 방향에 종속되지 않고, 내부 도메인 로직이 외부 의존성에서 독립적이어야 한다는 철학을 나타냅니다.
  3. 시각적 단순화
    Cockburn은 육각형이 포트와 어댑터를 배치하는 데 직관적인 도형이라고 보았습니다. 이 도형은 외부 시스템과 내부 도메인 간의 관계를 명확히 표현할 수 있습니다.

위와 같은 이유로 "Hexagonal Architecture"로 불리고 있습니다.

 

그럼 위 그림을 보면 Adapters두 육각형을 연결하는 역할 즉, 실질적인 구현체라는 것은 알겠지만,

Ports는 어디에 있는 걸까요?

 

그림을 자세히 보면 내부 육각형의 면이 굵은 것을 확인할 수 있습니다.

바로 이 육각형의 면이 외부 시스템과 연결을 위해 제공되는 Interface 즉, Ports입니다.

 

Hexagonal Architecture는 추후 Clean Architecture와 Onion architecture의 철학에 영향을 미치게 됩니다.


Clean Architecture와 Hexagonal Architecture의 차이

두 아키텍처는 기본적으로 같은 목표를 가진 설계 철학을 가지고 있어 혼동하기 쉽습니다.

 

그게 그거 아닌가!라고 생각 할 수 있지만, 딱 잡고 말하자면

Clean Architecture는 애플리케이션을 여러 계층으로 나누고, 의존성 방향을 바깥에서 안쪽으로 향하는 아키텍처이고,

Hexagonal ArchitecturePortsAdapters를 통해 외부 시스템과 내부 시스템을 분리하여 의존 방향이 Application 내부로 향하는 아키텍처입니다.

 

 즉, 두 아키텍처는 유지보수성과 확장성을 높이고, Core를 외부 시스템으로부터 분리하는데 초점을 맞춘다는 공통점이 존재합니다.

 

그럼 어떤 아키텍처를 사용하는 것이 좋을까요?

 

만약 애플리케이션의 종속성 규칙을 엄격하게 지키고 싶다면 Clean Architecture를,

외부 시스템과 내부 시스템의 엄격한 분리를 원한다면 Hexagonal Architecture를,

만약 둘 다 엄격하게 가져가고 싶다면 섞어 사용하면 될 것 같습니다.

 

결국 우리가 이야기하는 아키텍처는 하나의 시스템을 설계하는 방법일 뿐 상황에 맞춰 최선의 방법으로 설계하는 것이 가장 중요하다 생각합니다.

 

물론, 다른 개발자들을 설득하는 것은 본인의 몫이라는 것은 비밀입니다. 


Reference

Clean Architecture

Hexagonal Architecture

 

728x90
728x90

우리가 웹브라우저 주소창에 도메인 주소(ex. www.naver.com)를 입력하고 나면, 

특정 사이트로 페이지를 이동하는 것을 확인할 수 있습니다.

 

하지만 개발을 하다 보면 도메인 주소 즉 사이트의 이름으로는 사이트가 인터넷이라는 바다의 어디에 있는지 알 수 있는 방법이 없습니다.

 

그렇다면 우리는 어떻게 해당 사이트를 찾아갈 수 있는 것일까요??

 

오늘은 사이트 이름을 가지고 해당 사이트의 주소를 찾아가는 이야기를 다뤄보려 합니다.


Domain과 IP의 개념

우선 우리가 왜 주소를 찾아야 하는지 알기 위해서는 Domain IP가 무엇을 의미하는지 아는 것부터 시작할 수 있습니다.

 

흔히 우리는 친구 집을 찾아간다 할 때 뭐라고 부르나요?

##네 집이라고 부르지 않나요??

 

이때 고민해봐야 하는 점은 왜 우리는 친구의 집서울특별시...라고 자세하게 말하지 않고 ##네 집이라고 부르는 것일까요?

 

여러 이유가 있겠지만 이미 어디인지 알기 때문에 간단하게 기억할 수 있는 이름으로 기억하는 것이 가장 크지 않을까요?

 

이처럼 우리는 이미 특정 위치를 말할 때는 부르기 쉬운 것으로 기억한다는 것을 알 수 있습니다.

 

그럼 다시 주제로 돌아와 IP는 어떤 것일까요?

IP는 인터넷에 특정 위치에 있는 사이트의 주소를 나타내게 됩니다.

 

그렇다면 Domain는 어떤 것일까요?

Domain는 사이트의 주소를 부르기 쉬운 명칭이라 볼 수 있습니다.

 

이로써 가벼운 개념은 정리가 된 것 같네요.

이 글에서 IP는 친구집 주소, Domain은 친구집 호칭으로 봐주시면 좋을 것 같습니다.


IP를 찾기 위한 모험

그렇다면 만약 전학생의 집에 놀러 가고 싶어 주소가 필요하다면 어떻게 해야 할까요?

 

우리는 이미 전학생네 집이라는 Domain을 가지고 있습니다.

만약 학교라면 선생님께 여쭤보고, 선생님이 모른다면 전산시스템에 들어갈 것이고, 이도 모른다면 못 찾아갈 것입니다.

또한, 만약 주소가 잘못되었다면 우리는 결국 찾아가지 못할 것입니다.

 

이러한 시스템은 인터넷에서도 동일한데요.

우리가 사용하는 컴퓨터에 hosts라는 파일은 우리가 Domain을 방문하고 싶을 때 해당 IP정보를 가지고 있는 파일입니다.

하지만, 여기에 존재하지 않을 때 우리 컴퓨터는 누구에게 물어볼 수 있을 까요?

1. Caching Resolver

캐싱 리졸버는 사용자의 로컬 네트워크에 있는 서버이며, 우리가 사이트에 한번 들어가게 되면 해당 IP와 Domain은 Caching Resolver에 저장되어 다음번 방문 때 훨씬 빠르게 도와주는 서버입니다.

 

즉 우리가 빠르게 접근할 수 있는 긴급연락망이 되겠네요!

2. Local DNS

해당 서버는 기지국 DNS라고도 부르며, 우리가 흔히 사용하는 통신사(SKT, KT, LG 등...)를 의미합니다.

 

로컬 PC에서 사이트 IP를 찾지 못한 경우 Local DNSIP를 요청하게 되며, 존재하지 않을 경우 다음 단계로 넘어가게 됩니다.

 

해당 서버는 교무실이라고 볼 수 있겠네요!

3.  Root DNS

이런!! 교무실에 방문했더니 다른 학년이라 학생 정보가 없는 걸까요?!

 

그럼 우린 더 많은 정보를 접할 수 있게 교육청에 찾아가도록 하죠!

Root DNS는  ICANN이 관리하는 DNS시스템의 최상위 계층에 존재하는 서버입니다.

 

Root DNS에서 우리가 원하는 전학생네 집 주소가 있는지 확인합니다.

이때! 있으면 정말 다행이지만, 없다면 동사무소에 찾아가 보기로 합니다.

4. TLD( Top-Level-Domain ) DNS

여기서 동사무소"www.naver.com"에 접속한다 하면,

맨 뒤에 작성된 ". com"을 소유한 주소들을 관리하는 서버입니다. 

 

즉, ". com"를 소유한 주소들을 가진 서버에서 확인한다는 의미입니다.

 

TLD DNS에서 DomainIP를 찾아보고 없다면 하위 Second Level Domain DNS로 넘겨 최종적으로 www.naver.com  Domain DNS Authoritative DNS Server에 접근하여 IP를 얻어낼 수 있습니다.

 

그럼 우리는 모든 상황에서 전학생네 집을 찾았을까요??

아쉽게도 하나의 가정이 더 남아있네요...

 

만약, 동사무소도 주소를 모른다면 어떻게 할까요?

 

물론 이미 해당 레벨에서 한 번이라도 사람이 방문한 적 있다면 주소를 얻을 수 있겠지만,

만약 없다면 친구는 무인도에 살고 있을 확률이 높으므로 다음날 학교에서 물어보도록 합시다.


여행의 끝

우리는 위의 전학생 주소 찾기를 통해 어떻게 주소를 찾아야 할지를 비교적 간단하게 알아보았습니다.

 

추가적인 내용으로는 국가 코드, 일반 코드 등 상세하게 나눠지는 부분은 당연히 존재합니다.

하지만, 이 글의 목적은 어떤 흐름으로 Domain으로 IP를 가져오는지에 대한 니즈를 해결하기 위한 글이므로 다음 고급 편에서 더욱 세세하게 다뤄보도록 하겠습니다.

 

이렇게 IP를 가져왔다면 IP를 해석하며 서버를 찾아가는 과정도 존재하겠죠?

해당 내용은 "너에게 닿기를"이란 주제로 찾아뵙겠습니다.


Reference

https://www.cloudflare.com/ko-kr/learning/dns/what-is-dns/

https://aws.amazon.com/ko/route53/what-is-dns/

DNS 트래픽 쿼리 관련 논문: https://arxiv.org/pdf/2308.07966

 

https://namu.wiki/w/%EB%84%88%EC%9D%98%20%EC%9D%B4%EB%A6%84%EC%9D%80.

 

너의 이름은.

아직 만난 적 없는 너를, 찾고 있어. まだ会ったことのない君を、探している。 캐치프레이즈 2016년 8월 26일

namu.wiki

 

728x90
728x90

개요

개발을 하거나 면접을 보게 되면 Spring Data Jpa관련 단골 질문으로 가장 많이 나오는 내용이라 생각합니다.

 

그럼 도대체 OSIV가 뭐길래 JPA에서 계속 말이 나오게 되는 걸까? 한번 알아보도록 하겠습니다.


OSIV란

OSIV란 Open Session In View의 약자로써 사실 JPA가 도입되면서 Open EntityManager In View로 개념이 확립되었으나 편의상 지금까지 OSIV라고 부르고 있습니다.

말 그대로 OSIV는 View까지 EntityManager를 열어두는 것을 의미합니다.

 

그럼 JPA에서 EntityManager이라 함은 무엇일까요?

바로 영속성 컨텍스트를 의미합니다.

 

영속성 컨텍스트는 요청이 들어오면 생성되고 사라지는 휘발성 콘텍스트입니다.

해당 콘텍스트는 기본적으로 요청이 들어오면 생성되고, 요청이 끝나면 사라지게끔 되어있는데, 이는 OSIV 설정이 기본으로 True로 설정되어 있어 가능한 일입니다.

 

만약 OSIV가 False로 설정되어 있다면 영속성 콘텍스트는 Transaction범위에 따라 생성되고 Transaction이 끝날 시 사라지게 됩니다.

 

이로써 OSIV가 어떤 설정인지 알았으니 해당 설정이 기능과 성능에 미치는 영향에 대해 알아보겠습니다.


OSIV가 미치는 영향

OSIV 설정은 아래 그림으로 한 번에 확인할 수 있습니다.

위와 같이 데이터를 생성, 수정, 삭제 기능들은 트랜잭션 범위 내에서 작업이 가능합니다.

다만 읽기 기능의 경우 영속성 콘텍스트 범위 내에 있을 경우 읽기가 가능한데,

OSIV는 영속성 콘텍스트를 view까지 늘려 트랜잭션이 아닌 요청의 생존 범위까지 늘리는 것을 의미합니다.

 

즉 생성, 수정, 삭제는 OSIV의 목적은 아니고, 읽기 범위를 늘리는 것이 주목적이라고 보시면 됩니다.

 

그럼 왜! 읽기 범위를 늘리는 것인가?

이는 Lazy Loading과 밀접한 관련이 있습니다.

 

Lazy Loading이란 지연 읽기 기능인데, 처음 데이터베이스에서 조회하는 것이 아닌 추후 해당 데이터를 사용할 때 조회하는 기능입니다.

 

해당 기능을 사용할 때, 영속성 콘텍스트의 생존범위 내에서 로딩하는지가 중요한데 이를 어디까지 허용할 것인지를 조절할 수 있다고 보시면 좋을 것 같습니다.

 

그럼 해당 기능이 왜 성능 향상에 기여할 수 있는가? 영속성 콘텍스트를 유지하는 것 또한 많은 자원을 소모하게 됩니다.

이를 View까지 유지하지 않고 트랜잭션 범위에 맞춰 종료하게 되면 OSIV가 TRUE인 상태보다 좀 더 효율적으로 자원을 사용할 수 있게 됩니다.

 

해당 옵션은 위에서 말했듯 default True인 옵션입니다.

만약 애플리케이션 성능 향상에 있어 관심이 많다면 테스트해보시길 바랍니다.


Reference

https://docs.spring.io/spring-boot/reference/data/sql.html#data.sql.jpa-and-spring-data.open-entity-manager-in-view

 

SQL Databases :: Spring Boot

Spring’s JdbcTemplate and NamedParameterJdbcTemplate classes are auto-configured, and you can autowire them directly into your own beans, as shown in the following example: import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.ste

docs.spring.io

 

 

728x90
728x90

개요

우리들에게 URL은 매일 수십 번을 적고, 복사하고, 붙여 넣는 혹은 알게 모르게 사용하고 있는 자원입니다.

 

이렇듯 우리가 여러 사이트들을 접근하기 위해서는 URL을 사용하여 접속을 해야 하는데,

어떻게 접근할 수 있는 것인지 그리고, URL자원들은 어떤 의미를 가지는지를 알아보도록 하겠습니다.


URL

우리가 URL를 통해 어떻게 사이트를 호출할 수 있는지를 알기 위해서는 URL이 뭔지 그리고 내부 요소들은 무엇이 존재하고, 어떤 의미를 가지는지를 먼저 이해하는 게 중요합니다.

 

URL은 인터넷에서 고유한 식별 주소입니다.

여기서 중요한 게 URIURL를 혼동할 수 있는데,

URI식별자이고, URL은 식별자 + 프로토콜 + 행위 등을 전부 포함한 것입니다.

 

그럼 식별자는 무엇이고, 프로토콜, 행위들이 URL의 어떤 부분인지 알아보도록 하겠습니다.


URL해부

Naver의 환율 계산하는 사이트의 주소를 해부해 보도록 하겠습니다.

https://finance.naver.com/marketindex/?tabSel=exchange#tab_section 

Schema

https://finance.naver.com/marketindex/?tabSel=exchange#tab_section 

처음 나오는 부분은 Schema입니다.

 

이는 브라우저가 요청을 처리할 시 사용할 프로토콜을 나타내게 됩니다.


Authority

https://finance.naver.com/marketindex/?tabSel=exchange#tab_section 

권한 부분입니다.

 

이는 많은 URL마다 다른 부분인데 현재 색칠되어 있는 부분 finance.naver.com은 도메인이라고 부릅니다.

 

원래 IP와 Port가 명시되어 있어야 하지만 사이트마다 다 다른 IP와 Port를 외우는 것은 너무 힘들겠죠??

그리하여 DNS라는 도메인과 IP정보를 가지고 있는 서버를 사용하여 도메인을 명시할 경우 서버에게 해당 도메인의 IP를 받아와서 호출하고자 하는 서버를 호출하는 방식으로 통신을 하고 있습니다.


Path to resource

https://finance.naver.com/marketindex/?tabSel=exchange#tab_section 

경로입니다.

이는 앞서 도메인을 통해 서버를 호출하였다면, 해당 경로 정보를 통해 원하는 콘텐츠를 제공하는 API 혹은 페이지를 접근할 수 있도록 즉 웹서버에서 해당 요청을 처리할 수 있는 위치를 특정하는 경로입니다.


Parameters

https://finance.naver.com/marketindex/?tabSel=exchange#tab_section 

매개 변수입니다.

콘텐츠를 제공받기 위해 필요한 정보를 나타내며,

시작지점은 /가 아닌?로 표기하게 되고 이 뒤에 붙는 자원들은 Key Value형태로 명시되게 됩니다.

 

여러 개를 요청할 경우 &를 붙여 동일하게 Key Value형태로 명시하게 됩니다.

 

이는 Path to resource를 통해 콘텐츠를 제공해 줄 수 있는 서버 내 위치를 특정하고,

콘텐츠를 제공받기 위해 받아야 하는 정보들을 Parameters로 제공을 받게 됩니다.


Anchor

https://finance.naver.com/marketindex/?tabSel=exchange#tab_section 

해당 페이지 내 행위입니다.

 

이는 페이지 내에서 원하는 행위를 표기하게 됩니다.

이는 무엇인가??

 

우리는 앞서 원하는 콘텐츠를 제공받기 위해 서버의 위치를 특정하고, Parameters로 원하는 정보까지 넘겨줬습니다.

하지만 우리가 받은 콘텐츠에서 어떤 행위를 의도하고 싶으면 어떻게 해야 할까요??

 

아리송한 말이죠? 우리가 네이버에 접속했다고 생각해 봅시다.

만약 우리가 네이버에만 접속하는 게 아닌, 접속했을 때 달력이 펼쳐져 있길 원한다면 어떻게 해야 할까요?

 

이렇듯 서버로부터 제공받은 콘텐츠에서 어떤 행위가 발생한 시점으로 처음부터 가기를 원한다면 Anchor를 통해 명시한다로 보면 될 것 같습니다.

 

콘텐츠를 서버로부터 받고 난 이후 처리하는 자원이기 때문에 당연히 서버에서는 사용하지 않는 자원입니다.


Reference

https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Web_mechanics/What_is_a_URL

 

What is a URL? - Learn web development | MDN

A URL (Uniform Resource Locator) is the address of a unique resource on the internet. It is one of the key mechanisms used by browsers to retrieve published resources, such as HTML pages, CSS documents, images, and so on.

developer.mozilla.org

 

728x90
728x90

개요

Web Developoer로써 활동을 하고 있으면서 평소 당연하게 생각한 것들에 대해 다시 돌아보는 와중

WebServer와 WAS에 대해 설명할 수 있나 라는 생각이 들었습니다.

 

이에 기본기를 돌아보며 정리를 해보려 합니다.


WebServer란?

WebServer는 주로 HTTP통신을 처리하는 서버입니다.

 

클라이언트가 웹브라우저에서 URL를 통해 서버로 요청을 보내면, 서버는 해당 요청을 받아 일치하는 정적 콘텐츠( HTML, CSS, Javascript, image,... )를 제공하게 됩니다.

 

WebServer의 특징으로는 동일한 요청에 대해 동일한 파일을 반환합니다.

 

즉 WebServer는 웹 브라우저의 화면을 구성하는 요소들을 반환한다고 보면 좋을 것 같습니다.

주로 Apache Server, Nginx 등을 사용하고 있습니다.


WAS(Web Application Server)란?

WAS란 Web Server와 Web Container를 가지는 서버입니다.

 

주로 사용자의 요청을 받아 비즈니스 로직을 통해 처리하며, 데이터베이스 연동이 이뤄지는 작업이 있을 때 사용하는 서버로, 동적 콘텐츠를 제공받게 됩니다.

 

WebServer와 다르게 화면을 구성하는 요소들을 반환하는 것이 아닌,

화면을 구성하는데 필요한 데이터들을 연산, 데이터베이스 연동을 통한 CRUD 등의 작업들을 수행하고 반환하는 역할을 담당합니다.

주로 Tomcat 등을 사용하고 있습니다.


사용처

위에서 소개했듯이 용도에 따라 사용하는 서버가 다른데요,

페이지를 만들 때는 WebServer, 데이터를 필요로 할 때는 WAS를 사용하면 될 것으로 보입니다.

 

그럼 실제로는 어떻게 사용하고 있을 까요??

 

실제로는 두 개를 병합하여 사용하고 있습니다.

 

Tomcat의 경우 Apache Tomcat으로도 부르는데 이는 2008년부터 Tomcat에 Apache 즉 WebServer가 추가되어 있기 때문입니다.

 

그럼 Tomcat은 WAS이니 Tomcat만 쓰면 되겠네요???

제가 경험한 개발은 상황에 따라 다르게 사용하는 점이 많다는 것인데요.

 

Apache Tomcat만 사용해도 제작은 가능합니다.

 

하지만 Web Browser에서 WebServer로 요청을 보내고, WebServer에서 데이터를 얻기 위해 WAS에 요청을 보내게 됩니다.

 

이렇듯 서비스가 제대로 동작하려면 다수의 요청을 수용가능해야 하는데요,

WAS로 띄워버린다면 하나의 서버가 너무 많은 요청을 처리해야 하게 되는데 이는 좋지 않은 상황을 발생할 수 있습니다.

 

또한 WebServer는 요청에 대한 처리가 정적 콘텐츠를 제공하는 만큼 많은 자원을 필요로 하지는 않지만,

WAS의 경우 다양한 비즈니스로직 처리, DB와의 연동을 통해 데이터를 주고받으려면 어쩔 수 없이 많은 자원을 소모할 수밖에 없습니다.

 

그럼 많은 요청을 처리하기 위해서는 어떻게 해야 할까요??

 

만약 Apache Tomcat으로 모든 것을 처리할 경우 WAS만 여러 대 띄우게 될 것입니다.

 

이는 WebServer도 같이 여러 대 뜬다는 것인데요, 이는 불필요하게 서버 자원을 소모하는 요인으로 만약 이렇게 한다면 서버를 띄우는 컴퓨터 혹은 클라우드의 비용이 들게 됩니다.

 

그렇기에 WebServer와 WAS를 분리하여 가져가며 WebServer는 하나, WAS는 여러 개를 띄움으로써 많은 요청들을 효율적이게 처리하게 하는 방법을 현재 많이 사용하고 있습니다.

 

하지만 사용자가 많지 않고 WAS로 띄운 서버 성능도 별로 좋지 않아도 된다 하면,

Apache Tomcat에 한 번에 개발하는 것이 번거롭지 않고 편하겠죠.

 

즉 상황에 맞게 적용하시는 것이 좋을 것으로 보입니다.


References

https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Web_mechanics/What_is_a_web_server

 

What is a web server? - Learn web development | MDN

The term web server can refer to hardware or software, or both of them working together.

developer.mozilla.org

https://www.ibm.com/topics/web-server-application-server

 

Web Server Versus Application Server | IBM

By strict definition, a web server is a common subset of an application server.

www.ibm.com

 

728x90
728x90

스펙

OS: MAC OS

VM Tool: UTM

설치 OS: Ubuntu-22.04.3

설치

UTM 설정

 1. Virtualize와 Emulate중 선택

2. 설치할 OS선택

3. 설치할 Linux image 선택 및 적용

4. 스펙 설정

5. 저장공간 설정

6. VM이름 설정 및 Setting

7. VM설치시 처음 접하는 화면 

Try or Install Ubuntu Server 선택 이후 기다리면 다시시작된다.

8. 언어 설정

9.

10. 언어설정

11.

12. 네트워크 설정

13. 프록시 설정

14. storage layout설정

15. Storage 설정

16. Ubuntu Pro 업그레이드 설정

17. SSH 설정

18. 인기많은 서버 Snaps 설정

19. Ubuntu 기본 셋팅 

기다리다 Reboot now 버튼이 생기면 Reboot now버튼을 클릭하여 리붓

20. 설치완료

이후 다시 접속 시 아래와 같이 UbuntuOs VM완성 초기 설정한 ID와 Password로 접속 가능하다.

728x90
728x90

소프트웨어는 다양한 상황에서 장애가 발생할 수 있습니다. 이러한 장애에 효과적으로 대응하기 위해서는 다음 세 가지 원칙을 반드시 기억하고 실천해야 합니다:

  1. 예방 가능한 장애를 방지할 것
    사전에 발생할 수 있는 문제를 예방하는 것이 최우선입니다.
  2. 장애 발생 시 신속하게 전파하고 해결할 것
    문제가 발생했다면 즉시 공유하고 빠르게 해결해야 합니다.
  3. 재발 방지 대책을 마련할 것
    동일한 장애가 반복되지 않도록 원인을 분석하고 조치를 취해야 합니다.

장애 발생에 대비하기 위해서는 사전 테스트를 통해 발생 가능한 상황을 시뮬레이션할 필요가 있습니다. 이를 위해 부하 테스트를 활용할 수 있습니다.

부하 테스트는 소프트웨어가 특정 시나리오에서 안정적으로 동작하는지를 검증하는 과정으로, 주요 목적은 다음과 같습니다:

  1. 예상 TPS(Transaction Per Second) 검증
    예상되는 초당 트랜잭션 처리량을 시스템이 제대로 처리할 수 있는지 확인합니다.
  2. 응답 시간 검증
    평균, 중앙값, 최대 응답 시간을 측정하여 성능을 평가합니다.
  3. 동시성 이슈 검증
    다량의 트래픽이 유입될 때 동시 처리에 문제가 없는지 확인합니다.

부하 테스트는 테스트 시간과 트래픽 양에 따라 다음 네 가지 유형으로 구분됩니다:

  1. Load Test (부하 테스트)
    • 시스템이 예상 부하를 정상적으로 처리할 수 있는지 평가합니다.
    • 특정 부하를 일정 시간 동안 가해 이상 여부를 파악합니다.
    • 목표 부하를 설정하고, 이에 맞는 애플리케이션 배포 스펙을 고려할 수 있습니다.
  2. Endurance Test (내구성 테스트)
    • 시스템이 장기간 안정적으로 운영될 수 있는지 평가합니다.
    • 장기간 부하가 가해졌을 때 발생할 수 있는 문제를 파악합니다.
    • 메모리 누수, 느린 쿼리 등 장기 운영 시 나타날 수 있는 숨겨진 문제를 발견합니다.
  3. Stress Test (스트레스 테스트)
    • 시스템이 증가하는 부하를 얼마나 잘 처리할 수 있는지 평가합니다.
    • 부하가 점진적으로 증가할 때 발생하는 문제를 파악합니다.
    • 장기적인 운영 계획과 확장성을 검토할 수 있습니다.
  4. Peak Test (최고 부하 테스트)
    • 일시적으로 많은 부하가 가해졌을 때 시스템이 잘 처리하는지 평가합니다.
    • 임계 부하를 순간적으로 제공했을 때 정상적으로 동작하는지 파악합니다.
    • 선착순 이벤트 등에서 정상적인 서비스 제공 가능 여부를 평가할 수 있습니다.

이번 부하 테스트에서는 일반적으로 많이 사용하는 Load Test와 Peak Test를 통해 시스템의 성능을 검증해 보도록 하겠습니다.

부하 테스트

시나리오 작성

부하 테스트를 진행하기 전 우선 서비스 특성에 맞춰 테스트를 진행할 상황을 먼저 생각해보아야 합니다.

  • 트래픽이 많이 몰릴 것인가?
  • 서비스 입장 토큰을 사용하지 않고 호출하는 API인가?

위 내용들로 생각을 해보면 현재 서비스에서 제공하는 시나리오들을 확인을 해보겠습니다.

1. 대기열 참여 및 토큰 조회

대기열은 콘서트를 임시예약 및 결제를 하기 위해서는 필수적으로 거쳐야 하는 API입니다.

 

입장인원을 2000명으로 통제하고 있지만 대기열 진입하는 요청은 무제한으로 들어올 수 있기 때문에 대기열 참여, 대기순위 및 토큰 조회 API는 부하 테스트를 진행해야 한다 생각하였습니다.

Waitng Scenario Script ( Load Test )

import http from 'k6/http';
import {check, sleep} from 'k6';
import {randomIntBetween} from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';

export let options = {
    scenarios: {
        waiting_scenario: {
            executor: 'ramping-vus',
            startVUs: 100,
            stages: [
                { duration: '10s', target: 200 },  
                { duration: '10s', target: 300 },  
                { duration: '10s', target: 400 },  
                { duration: '10s', target: 500 },  
                { duration: '10s', target: 0 }  
            ],
            exec: 'waiting_scenario'
        }
    }
};

export function waiting_scenario() {

    let userId = randomIntBetween(1, 100000);
    let seriesId = randomIntBetween(1, 10000);
    waiting(userId, seriesId);
	sleep(1);
    waitingCheck(userId, seriesId);
}

function waiting(userId, seriesId) {
        
    let res = http.post(
                    'http://localhost:8080/waiting-queue/join',
                    JSON.stringify({ userId, seriesId }),
                    {
                        headers: {'Content-Type': 'application/json'},
                    }
                )
    check(res, {
        'status was 200': (r) => r.status === 200
    })
}

function waitingCheck(userId, seriesId) {
    let res = http.get('http://localhost:8080/waiting-queue/waiting-count?userId=' + userId +'&seriesId=' + seriesId);
    check(res, {
        'status was 200': (r) => r.status === 200
    })
}

성능 측정 ( Load Test ) 

 

위 성능 측정 표를 확인해 보면 1건의 실패 케이스가 나온 것을 확인할 수 있습니다.

 

하지만 이는 성능 문제가 아닌 UserId와 SeriesId로 대기열에 입장하는데 해당 아이디들이 중복으로 들어갈 경우 실패하는 것이라 올바른 상황으로 볼 수 있습니다.

 

아래 stage로 Peak Test를 진행해 보았습니다.

 

극단적으로 5000 Target으로 올렸지만 성능을 확인해 보았을 때 정상적으로 처리하고 있는 것으로 보았습니다.

export let options = {
    scenarios: {
        waiting_scenario: {
            executor: 'ramping-vus',
            startVUs: 100,
            stages: [
                { duration: '10s', target: 2000 },  
                { duration: '10s', target: 3000 },  
                { duration: '10s', target: 4000 },  
                { duration: '10s', target: 5000 },  
                { duration: '10s', target: 0 }  
            ],
            exec: 'waiting_scenario'
        }
    }
};

성능 측정( Peak Test )

2. 콘서트 결제

콘서트 결제의 경우 앞에서 했던 대기열 입장 및 서비스 입장, 토큰 발급, 콘서트 시리즈 및 좌석 조회, 포인트 충전, 임시예약, 결제 순으로 시나리오가 이루어집니다.

Concert Payment Scenario Script ( Load Test ) 

import http from 'k6/http';
import {check, sleep} from 'k6';
import {randomIntBetween, randomItem} from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';

let concertId = '0006740c-54d6-11ef-8c39-0242ac110002'

export let options = {
    scenarios: {
        payment_scenario: {
            executor: 'ramping-vus',
            startVUs: 100,
            stages: [
                { duration: '10s', target: 200 },  
                { duration: '10s', target: 300 },  
                { duration: '10s', target: 400 },  
                { duration: '10s', target: 500 }
            ],
            exec: 'payment_scenario'
        }
    }
};

export function payment_scenario() {

    let userId = randomIntBetween(1, 100000);
    let seriesId = getSeriesId();

    chargePoint(userId);
    waiting(userId, seriesId);
	sleep(1);
    let tokenId = getTokenId(userId, seriesId);

    let seat = getSeat(tokenId, seriesId)
    let temporaryReservationId = temporaryReservation(tokenId, userId, seriesId, seat.seatId)
}

// ConcertSeries조회
function getSeriesId() {
    let res = http.get('http://localhost:8080/concert/series/' + concertId);
    check(res, {
        'status was 200': (r) => r.status === 200
    })
    let concertSeriesList = res.json().seriesList;
    return concertSeriesList[0].seriesId;
}

// ConcertSeries조회
function getSeat(tokenId, seriesId) {
    let res = http.get('http://localhost:8080/concert/seat/' + seriesId, {
        headers: {
            tokenId
        }
    });
    check(res, {
        'status was 200': (r) => r.status === 200
    })
    let concertSeatList = res.json().seatList;
    return randomItem(concertSeatList);
}

//포인트 충전
function chargePoint(userId) {
        
    let res = http.patch(
                    'http://localhost:8080/point/charge',
                    JSON.stringify({ userId, amount: 10000 }),
                    {
                        headers: {'Content-Type': 'application/json'},
                    }
                )
    check(res, {
        'status was 200': (r) => r.status === 200
    })
}

// 대기열 진입
function waiting(userId, seriesId) {
        
    let res = http.post(
                    'http://localhost:8080/waiting-queue/join',
                    JSON.stringify({ userId, seriesId }),
                    {
                        headers: {'Content-Type': 'application/json'},
                    }
                )
    check(res, {
        'status was 200': (r) => r.status === 200
    })
}

// 토큰아이디 조회
function getTokenId(userId, seriesId) {
    let tokenId = '';
    while(tokenId === '') {
        let res = http.get('http://localhost:8080/waiting-queue/waiting-count?userId=' + userId +'&seriesId=' + seriesId);
        check(res, {
            'status was 200': (r) => r.status === 200
        })
        let waitingData = res.json();
        tokenId = waitingData.tokenId;
        if(tokenId === '') sleep(1)
    }
    
    return tokenId;
}

// 임시예약
function temporaryReservation(tokenId, userId, seriesId, seatId) {
        
    let res = http.post(
                    'http://localhost:8080/temporary-reservation',
                    JSON.stringify({ 
                        userId,
                        concertId,
                        seriesId,
                        seatId
                    }),
                    {
                        headers: {
                            'Content-Type': 'application/json',
                            tokenId
                        },
                    }
                )
    check(res, {
        'status was 200': (r) => r.status === 200
    })

    if(res.status === 200) {
        temporaryReservationPayment(tokenId, userId, temporaryReservationId);
    }
    return res.body;
}

// 임시예약 결제
function temporaryReservationPayment(tokenId, userId, temporaryReservationId) {
    let res = http.post(
                    'http://localhost:8080/payment',
                    JSON.stringify({ 
                        userId,
                        temporaryReservationId
                    }),
                    {
                        headers: {
                            'Content-Type': 'application/json',
                            tokenId
                        },
                    }
                )
    check(res, {
        'status was 200': (r) => r.status === 200
    })
    return res.body;
}

성능 측정( Load Test )

 

테스트 결과를 보면 14%의 실패 케이스가 존재합니다.

이때 테스트 중 발생할 수 있는 케이스는 아래와 같습니다.

  • seat가 이미 결제된 경우 - 동시성 처리로 인해 발생하는 오류 정상케이스
  • 임시예약이 이미 결제된 경우 - 중복 결제처리 방지로 인해 발생하는 오류 정상케이스

이리하여 제공 중인 API들의 성능에는 문제가 없다 생각합니다.

Peak Test를 진행하였을 때는 아래와 같습니다.

성능 측정( Peak Test )

 

2000, 3000, 4000, 5000이 각 10s동안 요청이 가도록 설정하였고, 성능적으로 생각보다 느린 것을 확인할 수 있습니다.

다만 이번에도 실패 케이스는 위에서 말한 동시성 처리로 인한 의도적 오류 발생이므로 문제는 없다 생각합니다.

 

시나리오를 설정하고 테스트를 진행해 보니 따로 문제가 발생하는 점은 발견하지 못하였습니다.

 

다만, 테스트 중 성느억 문제가 있다 판단되는 API들이 존재하여 이를 개선해보고자 합니다.


문제 되는 API 개선 사항

Concert

1. Concert 목록 조회

개인적으로 가장 많은 TPS가 몰리는 API라 생각합니다.

그 이유는 대기열에 입장하지 않더라도 조회 가능한 API이고, 해당 서비스에서 가장 먼저 조회되는 데이터이기 때문입니다.

 

즉, 모든 유저들은 해당 서비스를 사용하려면 해당 API를 호출해야 하는 상황인 것입니다.

그러므로 TPS 100 이상이 나와주는 것이 적절한 성능이라 생각합니다.

기존 로직으로 조회했을 때의 성능입니다.

 

현재 성능으로 보았을 때 TPS는 1m 30s동안 1550건의 트래픽을 처리하였으므로 1550/90 = 17.2... 약 17 TPS의 성능을 가지고 있습니다.

 

이는 매우 심각한 상황으로 개선의 필요성이 많이 있다고 볼 수 있습니다.

 

여기서 성능을 개선하는 방향으로 Paging을 적용해 보았습니다.

Paiging을 적용 후 TPS를 측정해 보았을 때 1m 동안 5900건의 트래픽을 처리하였으므로 5900/60 = 98.3... 약 98 TPS의 성능이 나왔습니다. 즉, 5배 이상 되는 성능이 향상된 것을 확인해 볼 수 있었습니다.

2. ConcertSeries 목록 조회

콘서트 시리즈목록 조회의 경우 concertId로 조회가 되므로 페이징과 캐시를 제거하였음에 더 아래와 같이 5657/90 = 62.85... 로써

63 TPS의 성능을 제공할 수 있습니다.

 

3. ConcertSeat 목록 조회

ConcertSeat목록의 경우 ConcertSeriesId에 의해 적은 양의 데이터가 조회되고, Seat조회부터는 토큰을 가지고 접근하는 API이기 때문에 부하 테스트를 실행하지 않아도 된다 판단하였습니다.

 

이와 같이 서버의 장애를 대응하는 것은 매우 중요한 사항이라 볼 수 있습니다.

 

현재 상황의 경우 특정 API의 조회 성능 문제가 있었는데, 조회가 느린 게 왜 장애인가?? 할 수 있지만 서비스 자체의 성능을 저하시키는 요인으로 페이징 처리를 하지 않아 데이터가 많아질수록 성능 저하가 발행하는 엄연히 장애라고 판단할 수 있습니다.

 

 


Repository

https://github.com/KrongDev/hhplus-concert

 

GitHub - KrongDev/hhplus-concert: 콘서트 예약 서비스입니다.

콘서트 예약 서비스입니다. Contribute to KrongDev/hhplus-concert development by creating an account on GitHub.

github.com

 

항해를 마무리하면서 최종 발표자료

https://docs.google.com/presentation/d/14Tpj91PwHInYeVTrdihKlyE1dNZWB2djiMpayMHz7aY/edit?usp=sharing

728x90
728x90

728x90

+ Recent posts