Wecode - Project 1 (부트캠프)/Project 1 과정

Project1- 3일 차: Layered pattern 개념 (API Architecture, 관심사 분리,)

Queen Julia 2023. 9. 18. 10:42
 

project1- 2일차(3): 회원가입 기능 layered pattern

project 1 복사 붙여놓기 복사본은 깃 반영 안 됨 ls- al 해서 .git 떠도 반영 안 된다. ls-al 해서 나오는 건 git 삭제 remote 연결 안 될 것 단방향에 따라서, 각 파일의 앞단에서 export로 내보낸거를 require

pm-developer-justdoit.tistory.com

 

 

Layered pattern

📍개요

🗣 특별한 구조 없이 한 파일에 모든 코드를 구현: 코드의 양이 많지 않을 때는 간단,

but 코드의 양이 조금만 많아져도 오히려 코드 유지 보수가 어려워진다.

 

실제 기업의 시스템에서는 논리적으로 혹은 기능적으로 영역을 구분하여 코드를 관리하는 것이 좋다.

코드가 구성되어지는 아키텍처를 학습해보고, 이후  API의 코드를 더욱 체계적이고 효율적인 구조로 바꿔보도록

🚀 학습 목표

  1. 효율적인 아키텍처 구상 요소: 확장성, 재사용성, 유지 보수 가능성, 가독성, 테스트 가능성
  2. Layered pattern을 구성하는 세가지 Layer(Presentation, Business, Persistence)의 역할을 각각 설명할 수 있습니다.
  3. Layered pattern의 핵심요소 두가지(단방향 의존성, 관심사 분리)에 대해 설명할 수 있습니다.
  4. 회원가입 엔드포인트를 Layered Pattern에 맞게 모듈화 

[API] API Architecture

1. 코드 구조의 중요성

지금까지는 app.js 라는 하나의 파일에 모든 코드가 들어가 있었습니다.

하나의 파일에 모든 코드를 구현하는 방법의 장점은 간단

모든 코드를 한 파일에서 확인할 수 있으므로 신경써야 할 파일의 개수가 줄어들고, 개발이 전체적으로 더욱 간단

 

하지만 한 파일에 코드의 양이 조금만 많아져도 오히려 코드의 유지 보수가 어려워집니다.

실제 기업 시스템에서 하나의 파일에 모든 코드를 넣는 경우는 거의 없다.

 

실제로는 논리적으로 또는 기능적으로 영역을 구분하여

개별 코드를 관리

 

코드의 구조를 더 체계적으로 그리고 효율적으로 구현해 놓은 것= 코드의 아키텍처(architecture)

어떤 기준으로 코드를 나누어 구조화해야 하는지 알아보자

1-1. 확장성

모든 코드는 처음에는 조그마한 규모에서 시작

처음에는 간단한 시스템이라도 서비스가 발전할수록 시스템도 커져갈 수 밖에 --> 시스템 구현 시 확장성이 중요

확장성을 고려하지 않고 구현한 코드들은 시스템의 규모가 커질수록 문제가 많이 생길 확률이 높아짐

그러므로 확장성이 높은 구조로 코드를 구현하는 것이 중요

1-2. 재사용성

재사용성은 한 번 작성한 코드를 필요에 따라 다른 여러 곳에서 사용 가능

--> 코드의 양도 적어지고 개발의 속도도 높아짐

--> 더 안전하고 견고한 코드를 구현하기 쉽다

 

코드 구조상의 재사용성: 일반 코드 레벨의 재사용성, 즉 함수나 클래스를 재사용하는 수준의 재사용성 < 구조적인 재사용성

 

1-3. 유지 보수 가능성

코드는 한 번 작성된다고 끝이 아니라, 여러 개발자와 여러 팀이 수정하고 유지 보수 (내가 퇴사 후에도 쓰임)

--> 소프트웨어 지속 가능과 연결된 중요한 요소

 

구조적으로 로직이 잘 정리가 되고 나뉘어 있어야

반대로 여러 로직이 함께 뒤엉켜 있는 코드일수록 유지 보수가 힘듬 =스파게티 코드 (spaghetti code)

스파게티처럼 뒤엉켜 있는 코드  --> 일 능률 저하

 

 함수나 클래스 등을 사용하여 코드를 추상화(abstraction),

서로 독립적인 로직을 분리 --> 필요한 곳에 적절하게 사용되도록 하는 코드를 구현

1-4. 가독성

다른 개발자들이 코드를 읽었을 때 얼마나 이해하기 쉬운가

 

어려운 로직일수록 더욱 가독성이 높게 구현해야

어려운 로직을 어렵고 복잡하게 구현하는 것은 누구나 할 수 있지만,

어려운 로직을 쉽고 간단하게 구현하는 것을 할 줄 알아야만 좋은 개발자

 

코드의 구조 또한 이해하기 쉽게 구현해야

구조 자체가 복잡하게 구현된 소스 코드는 다른 개발자들이 이해하기가 힘들기 때문에,

유지 보수 또한 어려워질 수 밖에 없고 오류가 많이 생길 수 밖에 없습니다.

1-5. 테스트 가능성

코드는 작성된 이후로, 제대로 동작하는지 테스트 -->  로직이 간결하고 깔끔할수록 테스트 진행 수월

 

추상화가 잘 되어있고, 한 가지 역할만 하는 코드

추상화가 잘 구현되어 있고 담당하는 역할이 잘 나뉘어 있는 구조가 테스트하기 쉬운 구조 

 

테스팅을 하는 주된 이유는 

비용감소, 재반복성, 코드를 프로그래밍할 수 있는 능력, 재사용성, 서비스에 대한 퀄리티 향상

 

내가 만든 코드를 다른 코드로 테스트 --> 코드가 동작 문제를 어느 곳에서 문제가 났는지 파악하기 쉬움.

여기에서 문제가 났구나, 그러면 연쇄적으로 이곳과 이곳을 확인해봐야겠네 --> 담당하는 기능별로 세분화, 독립성 띠는 구조화 -> 테스트 가능성과 퀄리티 높아진다

 

로직 간결 깔끔할 수록,

담당하는 역할이 잘 나눠진 구조,  

구조화잘 된, (위의 특징들 다 아우름)  

향후 코드 견고성과 성능 검증


2. 관심사 분리, Separation Of Concerns (SOC)

2-1. 역할이 분리되어 있지 않다는 것

API 서버를 레스토랑에 비유

손님(고객)이 요리를 주문(request)하면,

직원(server)은 냉장고(database)에서 재료(data)를 준비하고, 요리를 해 손님에게 요리를 서빙(response)

 

직원이 하는 일 굉장히 많습니다.

레스토랑의 규모가 작을 때 (서비스의 규모가 작을 때에는) 혼자 모든 업무를 담당하는 것이 더 편합니다.

의사결정 속도가 빠르고, 일 처리를 간결

 

 

식당이 조금 더 커지고, 고객이 많아진 상황에서는 한 사람이 하는 업무가 많고 복잡하면 다음과 같은 문제

  • 직원의 역할이 명확하지 않아 실수할 확률이 높다.
  • 실수의 원인을 파악하기 어렵다.
  • 하고 있는 일이 많기 때문에 직원에게 문제가 발생해도 대체할 수 있는 자원을 찾기가 어렵다.
  • 요리의 질이 낮았을 것, 고객 만족도 낮아짐

 

분업이 필요

레스토랑의 업무가 효율적으로 그리고 혼선없이 진행되기 위한 역할 분리가 이루어진 것

각각 전문가로서 독립된 기능은 한 명이 맡고, 다른 특성 지는 작업 유형은 다른 직원

  • 확장성: 각 직원이 맡은 역할에 집중하여, 레스토랑이 더욱 확장되어도 업무의 혼선이 적다.
  • 직원1: 고객의 요청사항에 집중
  • 직원2: 직원들의 의사소통에 집중하고, 식당 내부 과정을 파악
  • 직원3: 냉장고의 상황과 요리에 집중
  • 재사용성 : 기존에는 홀 주문만 받던 식당배달의 민족으로 주문도 가능하도록 새롭게 주문 기능을 추가 --> 배달의 민족 주문만 확인해서 요리사에게 전달만 해주면 되기 때문에, 사람을 새롭게 채용할 필요 없이 기존에 주문을 확인하던 직원2가 역할을 동일하게 수행
  • 유지 보수 가능성: 외부 문제 상황 발생시 (고객의 잘못된 주문, 요리 재료의 손상) 문제 파악이 원활하며, 대응 속도가 빨라진다.
  • 가독성: 어떤 직원이 어떤 역할을 수행하고 있는지 파악이 수월하다.
  • 테스트 가능성 : 각 역할의 수행 정도를 가늠하기 편리하다.

2-2. 역할에 따른 코드 분리

역할에 따라 코드를 분리하여 구조화

 

‘구조': 꼭 파일로서 분리하는 것은 아닐 수

프로그램 자체의 구조를 뜻하기도 

편의상 파일로 분리하는 과정을 진행 후,

점진적으로 소프트웨어 아키텍처 자체를 이해하는 것으로 폭을 넓혀가는 것


HTTP 통신을 처리하는 서버의 상황도 마찬가지.

고객(손님)의 요청(주문)이 들어오면

 

서버: 데이터(재료)를 가공하여 요청에 알맞은 응답(요리) 반환합니다.

 

서버가 하는 역할을 자세히 뜯어보면 굉장히 다양한 기능이 혼재

 


회원가입하는 로직

  • 클라이언트로부터 요청을 받는다.
  • Request에 누락된 Key가 없는지 확인 하여 Key Error를 캐치한다.
  • 비밀번호의 길이를 확인하고, 유저의 존재 유무를 확인한다.
  • 비밀번호를 암호화한다.
  • Database에 INSERT INTO 문을 전송하여 데이터를 저장한다.
  • 제대로 데이터가 저장되었음을 응답으로 알려준다.

이렇게 많은 기능이 app.js 파일 하나에서 일어나고 있을 것

 

---> 스파게티 코드

---> 하나의 기능을 수정하거나 교체하려고 할 때, 파일 내부의 모든 다른 코드들이 지장받을 가능성이 높다

----> 특정 파일에서 오류가 발생하면 해당 파일 내부에 있는 다른 코드들 또한 오류에 영향을 받을 수 

---->코드가 조금만 많아져도 파일이 복잡해져 가독성이 떨어지는 문제


코드의 역할에 따라 파일 분리

기준

  • [파일 1] ---> HTTP Request/Response 처리
  • [파일 2] --->  Business Logic 처리
  • [파일 3] ---> Database 통신 처리

 

파일 1: Client로부터 받은 요청 자체에 관련된 일 처리

  • Request body 에서 데이터를 꺼낸다.
  • JSON Response로 제품 정보를 전달한다.

파일 2: 우리 서비스가 결정한 규칙에 따른 일들 처리합니다.

  • 비밀번호의 길이는 8자리 이상으로 규정한다.
  • 이미 가입한 이메일로는 중복으로 가입할 수 없도록 한다.
  • 비밀번호는 암호화해서 저장한다.

파일 3: 데이터베이스와의 통신만 처리합니다.

  • 데이터를 INSERT INTO로 저장한다.
  • SELECT로 데이터를 가져온다.

 

코드의 확장성 높아진다

각 파일에 포함되어 있는 코드의 목적이 명확, 범위도 확실 --> 코드의 구조 파악 쉽다

각 파일이 서로 독립적이고 역할 분명--> 서로에게 끼치는 영향을 최소화하면서 확장하거나 수정

 

주된 역할, 집중해야할 기능을 분리, 서로 다른 기능을 하는 파일은 신경을 쓰지 않도록 하는 것

= ‘Seperation Of Concern, 관심사의 분리 ( SOC) '


[Node] Layered Pattern

01. Layered Pattern이란?

코드의 구조 구상할 때 여러가지 고려

코드 구조 하나하나가 모두 중요하기 때문에, 매번 좋은 코드의 구조 쉽지 않다.

다행히 “코드의 구조를 어떻게 구성하고 관리해야 하는가 ”  이미 많은 개발자들이 다루었던 문제이고,

그에 관한 정석같은 패턴이 굉장히 많이 나와있다.

 

레이어드(layered) 패턴-->  백엔드 API 코드에 가장 널리 적용되는 패턴 중 하나


Multi-tier 아키텍처 패턴= Layered 아키텍처: 

코드를 논리적인 부분 혹은 역할에 따라 독립된 모듈로 나누어서 구성하는 패턴

각 모듈이 서로 의존도에 따라 층층히 쌓듯이 연결, 전체의 시스템을 구현하는 구조 ---> 마치 레이어(layer)를 쌓아 놓은 것 같은 형태

 

즉, 역할에 따라 분리했던 파일들= 하나의 레이어라 볼 수 있습니다.

 

각각의 기능별로 layer 이름 붙임 


보통 3개의 레이어 존재 (Presentation Layer / Business Layer / Persistence Layer)

클라이언트와 데이터베이스 사이 

1-1. Presentation Layer

클라이언트와 가장 맞닿아있는 계층. 무언가 보여준다는 뜻 = present 

시스템 사용하는 사용자 혹은 클라이언트 시스템과 직접적으로 연결되어 맞닿은.

 

백엔드 API 엔드포인트 담당 

API의 엔드포인트들을 정의 내리고,

전송된 HTTP 요청(request)을 읽어 들이는 역할  --> express 구조 속에서는 controller의 역할로

그 이상의 역할은 담당하지 않음

 

실제 시스템이 구현하는 비즈니스 로직은 다음 레이어(layer)로 넘김 

1-2. Business Layer

말그대로 비즈니스 로직을 구현

 

실제 회사의 서비스 기획인 반영되는 곳.

시스템이 구현해야 하는 로직들을 이 레이어에서 구현

 

예를 들어, 인스타그램 API - 회원가입 기능

-> 비즈니스 레이어: 각각의 비밀번호의 유효성과 복잡도 (최소길이, 대소문자, 특수문자 등 특별한 설정 여부)

--> 외부에서 들어오는 다양한 요청을 걸러주는 역할

 

 

presentation layer를 통해서 들어오는 부분인데, 회사의 서비스적 기획이 녹아들어져서,

이곳을 통과하지 못하면, 

다음 persistence layer(데이터베이스와 소통하는)로 해당 정보를 전달하지 않는.  

 

즉 우리 회사의 비즈니스를 운영하기 위해 필요한 로직

 

1-3. Persistence Layer

 

클라이언트와 가장 멀리 떨어져 있고, 데이터베이스와 가장 맞닿아 있는. 

 

데이터베이스와 관련된 로직을 구현

Business Layer에서 필요한 데이터 생성, 수정, 읽기 (CRUD) 관련 처리

실제로 데이터베이스에서 데이터를 저장, 수정, 읽어 들이기  --> SQL,ORM 적 어휘라던지 데이터베이스가 알아들을 수 있는 프로그래밍적 소통방식 택


02. Layered 아키텍처의 핵심 요소

2-1. 단방향 의존성

각각의 레이어는 오직 자기보다 하위에 있는 레이어에만 의존하고 있음

자신이 보고 있는 가장 가까운 주체를 건너뛰어서 소통할 수 없음! 

  • Presentation layer --> business layer에게 의존, business layer --> persistence layer에게만 의존 (건너뛰어서 바라볼 수 없다)
  • Presentation layer --> Business layer를 건너뛰고 Persistence layer에 접근하는 일은 절대 일어나지 않는다

 

  • Business Layer --> presentation layer에 대해 (뒤에 있는 layer에) 완전히 독립적, persistence layer(앞에 있는 layer) 바라보는, 의존
  • Persistence layer --> (뒤에 있는 layer에) business layer나 presentation layer에 대해 완전히 독립적

 

의존성 관련해서는, 네모칸 화살표에 집중!

의존한다 = node express에서 "require하고 있다"


2-2. 관심사 분리(SOC)

 각 레이어 별 역할이 구분되어 명확하다 --> 즉 역할의 중첩이 없다

 

Presentaion Layer - Controller
Business Layer - Service
Persistence Layer - Model

 

각각의 역할이 분리되었기에, 서로의 역할과 기능이 무엇인지 알 필요 없음.

모델이 데이터베이스와 어떻게 소통, 처리했는지 알 필요 없음. 

  • Presentation layer : 비즈니스 로직이 전혀 구현되어 있지 않다
  • Business layer: 데이터베이스 관련 로직이 전혀 구현되어 있지 않다/ 데이터베이스 처리를 하기 위해서는 persistence layer의 코드를 호출해서 사용해야 
  • 요리사가 핫소스 넣었는제, 어떤 과정을 거쳐서 요리를 하는지 중간 직원은 전혀 알 수 없습니다. 직원은 요리사에게 요리를 받아서, 고객에게 전달만 하면 됨 과정을 거친 결과물인 요리만을 전달하기 때문입니다. 중간과정의 절차에 대한 관심은 끄면 됨.  직원은, 서비스 단에서, 고객의 요청을 잘 전달하고, 응답을 전달하는, 자신이 맡은 역할과 기능에만 집중하면 됨

 

서로 로직이 완전히 분리, 각각의 기능이 분리 --> workflow 

controller,
service,
Dao (model; Data Access Object, 데이터에 접근할 수 있는 객체, mysql/rdbms와 주고받을 수 있는) 

*회사의 naming convention에 따라 

app.js --> <회원가입> 기능 빼낼 때
user controller,
user service,
user Dao 

2-3. Layered 아키텍처 적용시 장점

  • 확장성 : 각 레이어가 서로 독립적이고 역할이 분명하므로 서로에게 끼치는 영향을 최소화하면서 확장하거나 수정
  • 가독성 : 레이어가 완벽하게 분리되어 있고 역할이 명확하므로 가독성도 높아지니다. 코드의 구조를 파악하기가 쉬울 뿐만 아니라 각 레이어의 코드의 목적이 명확하고 범위도 확실하기 때문 (코드는 쓰여지는 시간보다, 읽어지는 시간이 길다!) 
  • 재사용성 : 레이어가 독립적이므로 a 라는 기능을 하는 a layer를 b와 c 기능을 하는 b,c layer에서도 차용 가능 (역할 중복되지 않으니까) / business layer는 여러 다른 presentation layer에 적용될 수 / Express 기반의 API 엔드포인트에 적용된 business layer가 다른 프레임워크를 사용한 엔드포인트에 사용될 수도 있습니다.
  • 테스트 가능성 : 이미 명확한 역할에 이해 나뉘어 있으므로 각 레이어를 테스트하는 코드도 레이어가 나뉘는 것처럼 명확하게 나눌 수 있으며, 복잡한 로직이 아니라 명확하고 범위가 확실한 기능만을 테스트하기 쉽다. 명확한 범위 내에 내가 의도한 작은 레고, 부품 조각들만 테스트해서, 그 테스트 조각들을 모아서, 적용 가능/ 또한 레이어들 자체가 다른 레이어에서 사용하는 (맞물리는) 구조이므로, 테스트에서 호출해서 테스트해보기가 쉽다.

03. Layered 아키텍처 적용하기

single-layered pattern 에서 multi-layered pattern으로 어떻게 변환되는지 과정에 집중!

코드 심화 과정 
1.
app.use(cors()); 
app.use(morgan('dev'));
app.use(express.json());  
--> 서버를 돌릴 때 항시 발동할 수 있게, 'middleware'로서 넣음

2. app.listen (server.listen 를 포함하는 다양한 종합적 기능) vs server.listen (listening만 하는 기능 국한)

3-1. 역할에 따른 폴더 및 파일 생성

인스타그램의 회원가입 기능에 layered architecture를 접목

 

현재 app.js 파일에 속해있는 signUp 함수

--> 기능을 역할에 따라 나누어 서로 다른 모듈에서 진행하도록 분리

 

각 레이어에 속하는 모듈의 이름 칭하기

  • Presentation layer → userController.js
  • Business layer → userService.js
  • Persistence layer → userDao.js (Data Access Objective)

그러므로 app.js 단일 파일로만 존재하던 구조를 아래 형태로 분리

routes / services/ controllers /models 

userRoute / userService/ userController/ userDao

westagram
├── **node_modules**
├── package.json
├── routes
│		├── index.js
│   └── userRoute.js
├── services
│   └── userService.js
├── controllers
│   └── userController.js
├── models
│   └── userDao.js
└── app.js

 

폴더를 다음과 같이 나눈다. 

위 일련의 과정들이 어떻게 app.js에서 출발돼서 소스코드에 
연쇄 과정으로서 녹아져 있는지 확인!

app.js
처음에 의도한 다양한 패키지 모듈들 활용할 수 있게, cors, morgan, express, routes 

3-2. 역할에 따른 코드 분리

위 그림과 같이 app.js 에 작성 될 수 있는 signUp() 함수를 역할에 따라 각각 controller, service, model 모듈로 분리

 

기존 코드

// app.js

const express = require('express');

const cors = require('cors');
const morgan = require('morgan');
const dotenv = require("dotenv")
dotenv.config()

const { DataSource } = require('typeorm');

const myDataSource = new DataSource({
	  type: process.env.TYPEORM_CONNECTION,
    host: process.env.TYPEORM_HOST,
    port: process.env.TYPEORM_PORT,
    username: process.env.TYPEORM_USERNAME,
    password: process.env.TYPEORM_PASSWORD,
    database: process.env.TYPEORM_DATABASE
})

myDataSource.initialize()
    .then(() => {
        console.log("Data Source has been initialized!")
    })

const PORT = 3000;
const app = express();

app.use(cors());
app.use(morgan('combined'));
app.use(express.json());

// health check
app.get("/ping", (req, res) => {
  res.status(200).json({"message" : "pong"});
})

// user signup logic
app.post("/users/signup", async (req, res) => {
	const { name, email, profile_image, password } = req.body
    
	await myDataSource.query(
		`INSERT INTO users(
			name,
			email,
			profile_image,
			password
		) VALUES (?, ?, ?, ?);
		`,
		[ name, email, profile_image, password ]
	); 
   res.status(201).json({ message : "successfully created" });
});

mulit-layer로 분리 후

3-2-1. App: 앱의 초기화

mulit-layer로 분리 후 app.js


폴더를 다음과 같이 나눈다.

위 일련의 과정들이 어떻게 app.js에서 출발돼서 소스코드에 
연쇄 과정으로서 녹아져 있는지 확인!

app.js
처음에 의도한 다양한 패키지 모듈들 활용할 수 있게, cors, morgan, express, routes 

*routes: 일련의 과정이 app.js에서 무조건 시작된다
// app.js

const http = require("http");
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');

const dotenv = require("dotenv")
dotenv.config()

const routes = require("./routes");

const app = express();

app.use(cors());
app.use(morgan('combined'));
app.use(express.json());
app.use(routes);

app.get("/ping", (req, res) => {
  res.json({ message: "pong" });
});

const server = http.createServer(app);
const PORT = process.env.PORT;

const start = async () => {
  try {
    server.listen(PORT, () => console.log(`Server is listening on ${PORT}`));
  } catch (err) {
    console.error(err);
  }
};

start();

app.js 가 "cors, morgan, express, routes "라는 하부모델들과

어떻게 연쇄적으로 연결됐는지 확인해보자!


1. Controller (Presentation Layer)

 API의 endpoint들을 정의하고

전송된 HTTP 요청(request)들을 읽어 들이는 로직을 구현

 

비즈니스 로직으로 흘러들어가야할 데이터들이 올바른 형태를 띄고 있는지 선검증 작업

 

아래는 다양한 예시 중, 서버에서 원하는 특정 데이터의 ‘Key’ 값이 요청시 전해지지 않았을 때 발생하는 Key Error 를 사전에 에러처리로 검열한 모습

 

- 아래 코드 분석

userService라는 변수 안에, Service에서 불러와야 할 모듈에 대한 require문 (=import)

try, catch : signup 회원가입 관련 필요한 controller 안의 기능 
 - 안에 name, email, passowr, profileimage 가 구조분해 할당돼서, req의 body 안에 들어있음.

에러핸들링 if 

위에서 async 문을 받아왔으니, 밑에서 await 문으로
async- await 비동기 처리 : userService 안에 있는 signup이라는 또다른 함수를 불러와서, 데이터를 넣는 작업

** controller에서 service단을 require하면서 바라보고 ,
service에게 무언가를 계속 원하고 있다 --> service에서 signup 모듈을 받아와서, controller는 구조분해 할당한 4가지 변수의 인자를 넣어주는 모습 
그림 참고
흐름 참고

** controller --> 또다시 스스로 모듈이 돼서, 외부의 호출도 기다릴 수 있는 상태가 돼야 함 (route 가 기능하기에)

 

//controller/userController.js

const userService = require('../services/userService');

const signUp = async (req, res) => {
  try {
    const { name, email, password, profileImage } = req.body;

    if ( !name || !email || !password || !profileImage ) {
      return res.status(400).json({ message: 'KEY_ERROR' });
    }

    await userService.signUp( name, email, password, profileImage );
    return res.status(201).json({
      message: 'SIGNUP_SUCCESS',
    });
  } catch (err) {
    console.log(err);
    return res.status(err.statusCode || 500).json({ message: err.message });
  }
};

module.exports = {
	signUp
}

* Presentation Layer: API의 endpoint들을 정의하고

전송된 HTTP 요청(request)들을 읽어 들이는 로직을 구현

 

 ---> Presentation Layer에서

user controller.js 말고,  같이 힘을 합해서 그 기능을 구현하는 친구는, routes 안에 있는 파일들!

 

controllers + routes 안의 다양한 endpoint 분기점들이 그 역할 

 

 

그럼 routes를 살펴보자.


2. Routes : endpoint 경로 라우팅

 Router 파일: 외부에서 들어오는 요청을, 가장 먼저 맞이하여 하위 폴더로 안내하는 길잡이 역할

 

routes 안의 index.js

--> 외부에서 들어온 요청을 대분기 하는 역할 

 

index.js : (향후 확장성을 고려하여 생성될 수 있는)  Routes 에 존재하는 userRouter, postRouter, likeRouter, productRouter 등을

index.js에서 일단 한 차례 한 곳에 모으고, 

모아진 요청을, 세부 라우터들로 분기 시켜주는 역할 

index.js // -아래 코드 분석 
router.user("/users", userRouter.router); --> (동일 선상의) routes 폴더 안의, userRouter.js 내용 안의, router 라는 모듈을 긁어서 불러와서 발동시킨다.
//routes/index.js

const express = require("express");
const router = express.Router();

const userRouter = require("./userRouter");
router.use("/users", userRouter.router);

module.exports = router;

//routes/userRouter.js

const express = require('express');
const userController = require('../controllers/userController');

const router = express.Router();

router.post('/signup', userController.signUp);

module.exports = {
	router
}

 


3-2-4. Service

다음은 실제 비즈니스 규칙(Rule)과 로직(Logic)들이 접목되는 Service 레이어의 코드 입니다. 어플리케이션을 직접 다루는 운영자 입장에서 기획한 비즈니스 모델들이 접목 되어야 합니다. 아래는 비밀번호의 조합이 특정 형태를 띄어야 하는 규칙을 위해 정규표현식을 도입한 모습입니다.

//service/userService.js

const userDao = require('../models/userDao')

const signUp = async (name, email, password, profileImage) => {
    // password validation using REGEX
    const pwValidation = new RegExp(
      '^(?=.*[A-Za-z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,20})'
    );
    if (!pwValidation.test(password)) {
      const err = new Error('PASSWORD_IS_NOT_VALID');
      err.statusCode = 409;
      throw err;
    }
      const createUser = await userDao.createUser(
          name,
          email,
          password,
          profileImage
        );
      
        return createUser;
      };
  
  module.exports = {
      signUp
  }

 


3-2-5. Models

다음은 실질적으로 데이터베이스와 소통하는 Model 레이어 입니다. 아래는 typeorm을 이용하여 db에 연결한 후, 앞선 레이어들을 모두 무사히 통과한 데이터들을 db에 직접 Insert 하는 모습을 띄고 있습니다.

//models/userDao.js

const { DataSource } = require('typeorm');

const myDataSource = new DataSource({
	type: process.env.TYPEORM_CONNECTION,
    host: process.env.TYPEORM_HOST,
    port: process.env.TYPEORM_PORT,
    username: process.env.TYPEORM_USERNAME,
    password: process.env.TYPEORM_PASSWORD,
    database: process.env.TYPEORM_DATABASE
})

myDataSource.initialize()
  .then(() => {
    console.log("Data Source has been initialized!");
  })
  .catch((err) => {
    console.error("Error occurred during Data Source initialization", err);
	  myDataSource.destroy();
  });

const createUser = async ( name, email, password, profileImage ) => {
	try {
		return await myDataSource.query(
		`INSERT INTO users(
			name,
			email,
			password
			profile_image,
		) VALUES (?, ?, ?, ?);
		`,
		[ name, email, password, profileImage ]
	  );
	} catch (err) {
		const error = new Error('INVALID_DATA_INPUT');
		error.statusCode = 500;
		throw error;
	}
};

module.exports = {
  createUser
}

3-3. Layer dependency(의존성) 순서

App → Router → Controller → Service → Models 순으로 갈수록 데이터베이스의 접근에 가까워진다.

 

각각의 파일에서 export 한 module들을 어느 파일들이 require하고 있는지 그 연결흐름을 잘 살펴보면,

상위 레이어에서 오직 하위 레이어로만 의존하는 방향성의 특성

 

 

실제 Express를 이용한 서버 웹 개발시, Layered Pattern을 이루는 기본 구조

  • app.js | server.js: Express App 으로 서버를 여는 로직입니다. 그리고 Express App 인스턴스를 만들고, 필요한 미들웨어를 붙이는 로직입니다. 경우에 따라서 app.js 와 server.js 에 각기 다른 기능을 유도하여 두가지 파일 모두를 유지할 수도 있고, 둘의 기능을 한데 모아서 한 파일만을 유지할 수도 있습니다. 개발자의 의도에 맞게 파생되어질 수 있는 다양한 경우의 수에 유의하며 코드를 작성해주시기 바랍니다.
  • routes: 라우팅(엔드 포인트 나누기) 로직을 담당합니다.
  • controllers: 엔드포인트에 해당하는 함수 로직 - http 요청에 따른 에러 핸들링, service 로직에서 데이터를 받아와서 응답으로 내보내는 로직입니다.
  • services: controller 에서 넘겨받은 인자로 다양한 알고리즘(필터, 정렬 등..)을 처리해서 데이터에 접근하는 로직입니다.
  • models: 데이터베이스에 접근하기 위한 모델(DAO)이 정의되어 있는 폴더

아래 모듈은 의존성 없이 다양한 레이어에서 사용될 수 있지만 반복되는 로직이기에 분리해 놓은 폴더 입니다.

  • middlewares: 컨트롤러에 닿기 전에 반복되는 로직을 모듈화 해 놓는 폴더입니다. (ex. validateToken - 인증 / 인가)
  • utils: 의존성 없이 모든 레이어에서 공통적으로 자주 사용되는 로직을 모듈화 해 놓는 폴더입니다. (ex. errorGenerator)
  • .env: 프로젝트 내에서 사용할 환경 변수를 선언해 놓는 곳이며 샘플 양식은 .env.sample에 따로 기입하여 공유합니다.
  • node_modules: 노드 패키지 모듈
  • .gitignore: 위의 두 모듈을 깃이 관리하지 않도록 
  • package.json: 노드 모듈을 관리하는 파일

4. Summary

  • Layered pattern은 기본적으로 세가지 layer(Presentation, Business, Persistence) 구조로 구성
  • Layered pattern은 ‘단방향 의존성’과 ‘관심사 분리’ 라는 두가지 핵심 원리에 기반한 개념
  • 각각의 Layer들은 외부 요청을 처리하는 Controller - Service - Model (상위 → 하위)의 의 방향으로 의존성이 부여되어 있다

Assignment 1 | Project Layered pattern 적용

1. 과제 안내

  • 지금까지 app.js 단일 파일로 만든 API에 Layered Pattern 적용하여 재구성합니다.
  • app.js에 모두 모여 있던 소스 코드를 각 기능별로 routes, controllers, services, models 모듈화하여 분리해주세요.
    • routes : endpoint를 분리해서 해당 endpoint에 집중합니다. endpoint와 controller를 연결합니다.
    • controllers : request와 response에 대한 처리만을 담당합니다. service layer를 호출하고, response를 반환해야 합니다.
    • services : 필요한 비즈니스 로직을 담당한 후, model layer를 호출해야하고, controller layer로 결과를 반환해야 합니다.
    • models : 데이터베이스와의 통신만을 담당한 후, service layer로 결과를 반환해야 합니다.

2. 제출 방법

  • 완성된 코드를 repository에 push 후 Pull Request를 남겨주세요.