brunch

You can make anything
by writing

C.S.Lewis

by YJ Min 민윤정 May 08. 2023

백엔드, 프론트엔드, DB의 역할

백엔드, 프론트엔드, DB 각각의 역할을 하자

오랜만에 본업 글.

개인적으로 난 프론트엔드와 백엔드가 서로 Dependency 없이 RESTFul 하게 통신하는 모델을 좋아한다. 그리고 SSR(Server Side Rendering) 정말 싫다. 요즘 핸드폰 디바이스가 얼마나 좋은데, 아주 특정 용도 아니면 CSR(Clinet Side Rendering)이 정말 맞다.

어떤 언어를 쓰건 이런 구성을 할 때, 좋은 개발 프레임워크의 각각의 역할을 정리해보고자 한다.

내가 그리 친절하게 그림을 그리고 기본 개념을 설명할 정도의 시간은 없기 때문에 읽다가 알아들을만하다 할 때 계속 읽자. ICT서비스 개발리드를 해봤던 분들이라면 충분히 이해할 내용일 것이다.


DB

DB가 RDB만 있는 게 아니다. NoSQL 용도 있고, 메모리 DB, 객체지향 DB도 많이 있지만, 아직 많은 B2C, B2B ICT 서비스에 많이 사용되는 RDB를 먼저 예로 들면, RDB는 말 그대로 관계형 데이터 저장소이다.

적어도 RDB를 쓴다면, RDB는 엔티티 간의 Join을 전제로 만들어진 저장소이고, 인덱스와 엔티티를 잘 정형화해서 설계해 두면 I/O와 대량 연산, 추출이 빠른 시간 안에 가능하다. 내가 정말 싫어하는 방식이 무제한 데이터 Read, 십 여개의 테이블에 한 트랜잭션에서 write 하는 등의 일이다.

난 RDB를 쓰고 있는 팀이면 어느 정도 성장하면 DBA를 채용하자는 주의다. 객체지향언어를 주력으로 하는 백엔드 개발자들의 경우, 데이터 적재, 탐색 특화된 SQL에 대해서도 택틱 위주로만 알고 있을 가능성이 있고, 데이터 모델링도 상당히 클래스 설계하듯 하는 경향들이 있다. 그리고 JPA가 좋긴 한데 조인관계나, 객체 선언을 다차원으로 해야 효율을 얻을 수 있다. 이유는 RDB는 그렇게 구현/설계되어 있으니까.. 벤더마다 특징이 있지만, 뭐 대동소이하다. 결국 Key를 통한 조인, 조합, 데이터 적재와 관리 시스템이 그 요체다.

https://www.geeksforgeeks.org/rdbms-architecture/

내가 싫어하는 RDB 활용 사례이다.

- 특정 벤더에서만 지원하는 function이나 DB System 특징의 남발. 디버깅을 어렵게 하고, 객체의 속성이 실제 그 Type이 맞는지 확인하는 게 어렵다. 실제 업무에서 있었던 사례인데, 칼로리를 계산하는데, 1,000 칼로리가 넘어갈 때는 문제가 발생하는데, 그 이하일 때는 문제가 없는 거다. 원인을 찾아보니, 객체 binding은 Long/Double로 했는데, MySQL내 Function을 오해해서, 1K 당 소수점을 찍어서 String Type으로 return 하는 함수를 썼던 게 잘못. 현상을 찾기도 어렵고 테스트 코드로도 짜기 힘들어서 이런 경우 가공은 백엔드에는 소수점 절사 등 계산을 Display는 Frontend에서 하는 게 맞다는 생각이다.

- Trigger, Function은 절대 안 쓰고 다 백엔드에서 수동으로 해주겠다는 생각. 예를 들어 사용자별로 포인트를 적재하는 테이블이 별도로 있고, 사용자 테이블에는 최종 잔액을 기록해서, 조회 시 효율을 높여내는 데이터 구조를 생각해 보자. 이 경우, DB의 트리거가 훨씬 낫다. 간혹 개발자들 중에 DB의 트랜잭션과 I/O 경합을 이해하지 못하고 Table Lock 가능성 등을 이유로 애플리케이션에서 할게요.라고 쉽게 생각하는 경우. 트랜잭션 관리의 실수 발생포인트가 높고, 저 경우, 적재 테이블에는 Update를 만들지 말고(+, - 만 INSERT로 구현), INSERT 발생 시 사용자 테이블에 +, - 연산을 하는 아주 간단한 트리거를 짜면 된다. 트리거는 보통 해당 액션 발생과 거의 동시에 실행되고 한 트랜잭션 관리가 되고, 상용화된 RDBMS에서는 상당히 튜닝이 잘되어 있다. 백엔드에서 구현한다고 하면, 매번 트랜잭션 관리를 해주면서, 포인트를 적재하는 모든 end point에서 처리를 해줘야 하고 side effect가 있을 수 있다.

- 한 테이블에 60개 70개 컬럼 늘려서 덕지덕지 반정규화만 해서 쓰는 경우. 역시 RDB의 저장특성을 오해하는데, 모든 데이터는 데이터 저장소에 저장되는데 그 데이터를 읽을 때 컬럼이 많건, 레코드가 많건, 읽는 양은 같이 많아진다. 거기다가 컬럼이 많은 경우와 반정규화를 많이 해두면 write 부담이 발생한다. 연관된 모든 반정규화해둔 컬럼을 업데이트 쳐야 하는 경우가 많기 때문이다. RDB는 관계 설계를 하고, Join을 해서 가져올 수 있다면 인덱스와 실행 플랜만 잘 설계해도 훨씬 효율적일 수 있다. 진짜 이런 거 쓰기 싫다면 Object DB를 쓰자. RDB를 Document DB처럼 쓰는 모든 경우는 성능도 안 나오고, 유지보수도 힘들고 가성비가 안 나온다.


백엔드 역할

내 경력의 상당수도 백엔드 개발자이고, (살려고 프론트엔드를 일부 했지만) 백엔드 역할에 대해서도 하기 쉬운 실수나, 더 잘할 포인트를 찍어보고자 한다.

- SQL 튜닝을 잘했다고 해도, 여러 한계는 있기 마련이다. 예를 들어, 10여 건 데이터를 가져오는데 표현 순서를 바꾼다거나 몇 가지 값의 사칙연산, 간단한 계산식이 포함된 경우를 생각해 보자. RDB에는 인덱스가 없는 순서로 가져올 때 Java을 예로 들면 List를 가져와서 order를 바꾸는 일, 아주 쉽다. 사칙연산결과값을 매번 계산해서 중복해서 기록해 두거나, 뭐가 들어올지 몰라서 모든 필드별로 인덱스를 만들면 DB는 INSERT / WRITE부하가 늘어난다. Contraint Check와 인덱스 영역 업데이트가 같이 일어나기 때문이다. 아주 대용량 데이터가 아니라면 백엔드에서 가져다가 가공해서 결과값을 프론트엔드에 전달하는 게 훨씬 효과적인 일이다. 간혹 DB Table = Object로 구현하고 조인은 최소화, 입출력 메모리 방만하게 쓰는 건 나 몰라라 하고 DB는 Join효율을 전혀 잘 못 사용하는 경우도 여기에 해당한다. I/O 성능의 기본은 적게 쓰고 적게 읽는 거다. SQL플랜을 확인하고 Join 등 최적화된 플랜으로 구현된 SQL의 결과값이 객체화돼야 한다.

- 대다수 백엔드 API서비스들은, 인터넷상에서 https상(제발 https는 쓰자. http 다 들여다볼 수 있다.)에서 통신하는 구성일 것이다. 보통 옛날에 API나 서비스 짜던 분들 스타일인데, 멀쩡히 http status는 200 또는 500과 같은 서버 에러 상태를 주고, Body에 별도 코드와 별도 메시지로 에러처리를 하는 스타일이다. 프론트엔드와 백엔드 서비스 간 입장에서 보면 프론트엔드가 Client, 백엔드 서버가 Server로 정의하는 통신형태로 볼 수 있다. 아래와 같이 http표준은 이미 상당히 다양한 코드를 http status로 제공한다. 특히 Client Side 오류코드를 살펴보자.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses

상당수 클라이언트 사이드에서 발생할 수 있는 오류들은 Http 표준코드를 따르자. 이미 정의되어 있다.

즉, 요청 Request Payload가 불완전하다면 400 Bad Request 면 충분하다. 클라이언트가 에러인지 여부를 구분할 때 데이터를 파싱 해서 해야 한다는 건 불편한 일이다.

Http 표준 프로토콜을 쓰면 브라우저에서 detect 하기도 쉬울뿐더러, 에러메시지를 읽기 위해 Body를 읽어서 처리해야 하는 불편함도 사라진다. 특히, 무조건 500 Server에러를 내면서 에러코드를 던지는 경우, 에러 Exception이 나면 Response Body를 리턴 받지 못하는 API나 클라이언트 모듈도 많다. 어쩌란 말인가. Status는 상황에 맞는 표준 코드를 사용하고, Body는 성공 시에 정해진 프로토콜로 전달하는 게 맞겠다.

특히 요즘 프론트엔드 Javascript 도 React를 쓰건, VueJS 쓰건 객체화해서 처리하는 게 기본인데, Status를 200으로 주고 Response Body가 구조 자체가 바뀌는 건 더 최악이다. Javascript promise를 구사할 때 어쩌란 말인가?

- 하위버전 호환에 대해서 신경 쓰자. 프론트엔드와 백엔드가 완전히 분리된 경우, 정확한 한날한시에 배포가 일어나기는 힘든 게 현실이다. JSP, Template, NodeJS Backend+Frontend을 써서 프론트엔드가 백엔드와 섞여 있는 경우가 아니라면, 신버전 Endpoint는 추가하거나(구버전 Endpoint 호출방식이 동작해야 한다.), 부족한 데이터, Unknown Data를 받아도 동작할 수 있게 제공해야 한다. 흔히 너무 쉽게 프론트엔드가 바뀔 거니까 Request, Response를 바꿔버리거나, 엔드포인트를 제거하거나 하는 실수들을 흔히 한다. API형태의 백엔드 서비스는 특히 deprecated 전략을 쓰는 게 좋겠다. Java를 쓴다면, @Deprecated  어노테이션 및 @deprecated Javadoc 태그를 쓰자. 오픈소스처럼 예고하고 지금 사용가능하게 유예기간을 두는 거다. 완전히 안 쓰는 경우나 큰 버전업이 될 때 제거하는 거다.



프론트엔드

정말 요즘 RN, React 코드를 보면 세상 참 빨리 변한다는 생각이 든다. Typescript를 쓰면 에러나 자동완성도 훨씬 쉬워지고 생산성이 대폭 향상되었다는 생각이다. 개인적으로 프론트엔드의 본질은 Display단과 Seamless 한 UI/UX 구현이라고 생각한다. 간혹 백엔드에 대한 작은 지식으로 프론트엔드에 너무 많은 비즈니스 로직이나 연산을 넣는 경우들이 있는데 난 반대다. 왜냐고 디버깅이 힘들고, 아무래도 백엔드 서비스 쪽이 좀 더 모듈화가 잘되어 있을 수 있고, 그놈의 웹뷰 호환성이 문제다. 이제 해결되었나 모르겠는데 안드로이드 기기의 인앱 웹뷰 크롬과 충돌이 난다거나, iOS업그레이드 시 사파리 업데이트 시 Regular Expression처리등이 불안정했던 버전들도 있었다.

- 예를 들어, 1,000,000과 같이 천 단위 소수점을 마킹해서 가독성 좋게 보여주기. 자바스크립트를 쓴다면 얼마든지 쉽게 구현할 수 있는 일이다. 이 정도 내용은 원천 데이터를 정확하게 백엔드 <> 프론트엔드가 주고받고, 프론트엔드에서 공통 유틸리티 함수 및 컴포넌트를 만들어서 표시해 주는 게 훨씬 편하다. 반면 여러 객체들의 값을 합산해서 총합, 평균, 구간 평균 등을 보여주는 연산을 프론트엔드에서 할 이유는 없다. 백엔드 또는 DB의 영역이다. 이미 특화되어 있는 서버, 데이터 시스템이 있는데 왜 이런 일을 프론트엔드에서 하는가?  

- 프론트엔드의 기본인데 요즘 오래 걸리는 리소스나 데이터를 받아오는 동안 기다릴 수 있도록 Skeleton UI나 Loading Progressive Bar 구사는 기본 중의 기본이다. 하얀 화면만 뜨는데 뚫어지게 쳐다보고 있을 사용자는 없다. Skeleton을 그려서 어떤 컨텐츠가 표시될지 미리 예측하게 하고 각 컴포넌트가 로딩될 수 있게 구사하는 일이다. 백엔드는 단위 처리가 최대한의 성능(많은 Throughput, 짧은 Latency)을 내게 노력해야겠지만 프론트엔드(특히 네트워크 상황이 들쭉날쑥한 모바일 디바이스용은 더더욱)가 이런 처리는 하는 건 다양한 롤이다.

- 내가 싫어하는 게 무한정 기다리기, 무제한 데이터 로딩을 애초부터 설계하는 거다. 어떤 정보를 목록 형태로 보여줄 때 Infinate Scroll 은 너무 당연하다. 스크롤하면 계속 무한정 데이터가 나오는 것 같지만, 실은 데이터를 10개, 20개로 제한을 둬서 가져오고, 그걸 스크린에 보여주는 방식이다. Page Navigation과 같은 로직이다. 구글 캘린더로 내가 전에 사업할 때 테스트 해본 건데 캘린더는 있는 일정을 다 보여주는데 한 월 달력 안에 수천 개가 되니까 구글 캘린더도 느려지더라. 숫자 제한, 적절한 타임아웃은 필수다. 무한정 기다리고 무제한 데이터를 가져오는 데 버틸 HTTP 프로토콜 통신은 불가능하다. 끊임없이 데이터를 보여주려면 별도의 Streaming 전략을 짜야지 HTTP단순 Request <> Response구조로 대체할 수 없다.

- 프론트엔드의 에러 처리 : 아까도 얘기했지만, 사용자 실수의 경우, 사전 실수 방지는 클라이언트의 역할이다. 필수값을 입력 안 해서, 포맷을 잘못 입력해서, 사용자가 서버까지 갔다 올 시간을 기다리고 에러를 받는 건 좋은 사용자 경험을 아닐 거다. 사용자 입력 실수는 사전에 입력할 때 차단하자. 귀찮더라도 친절한 안내, 가이드는 사용자가 입력하는 시점에 고민 없이 바로바로 확인 가능해야 한다. 미리 차단할 수 없는 실수, 예를 들어, 이미 사용기간이 만료되었거나, 이미 일일 주간 이용제한을 초과해서 사용시도를 한 경우는 서버사이드 체크를 해야겠지만, "작업 중 오류가 발생했습니다"라는 애매한 워딩보다는 "이용제한을 초과했거나 사용가능한 이용권이 없습니다"가 더 친절한 설명이다. 회원을 조회하려고 시도했는데 갑자기 회원이 다른 관리자에 의해 삭제된 경우, "사용자가 존재하지 않습니다"가 정확한 표현이지, "404 Not Found"를 그대로 노출하는 건 친절한 UI가 아니라는 생각이다.


맺으며.

두서없이 써봤지만, 좋은 데이터 모델러, DBA, 백엔드 엔지니어, 프론트 엔지니어가 어떤 문제를 알고리즘으로 풀어낸다고 했을 때는 그 문제를 탁월한 사용자 경험을 제공하는 인터페이스로 싼 비용에 풀어내는 게 중요하다. 비용이란 당장 현금을 말하는 건 아니고, 사용자가 어떤 액션을 하고 그에 대한 Interaction을 주고받는 데 걸리는 시간, 번거로운 조작을 포괄하는 말이다.

이건 DB에서, 이건 백엔드에서 이건 프론트엔드에서 집중해서 하면 더 탁월하게 문제를 더 저렴한 비용 - 더 작은 컴퓨팅 파워, 메모리, 시간 - 으로 풀 수 있는데 역할을 혼재해서 잘 못 사용한 경우, 저 비용은 증가하고, 유지보수는 어려워지며, 사용자는 좋은 경험을 할 수 없을 것이다.




브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari