티스토리 뷰
어플리케이션(서비스)의 본질은 무엇일까?
* 서비스: 해당 글에서는 Web / Mobile 등에서 사용하는 Application을 지칭합니다.
사용자는 서비스를 사용할 때 어떤 행위를 할까요?
사용자는 서비스을 사용하면서 다양한 행동을 취합니다. 회원가입을 수행하고 게시글을 등록하기도 하며 다른 사람과 친구를 맺어 채팅을 하기도 합니다. 사용자의 이런 행동들은 어떤 결과를 만들고 우리는 왜 가치있다고 여길까요?
이런 행위를 통해서 사용자들은 자신만의 데이터를 생성해나가기 때문입니다. 이런 정보들이 나만을 위한 편리함을 제공할 때 사용자는 내가 사용하고 있는 서비스가 가치있다고 생각합니다.
결국 서비스는 사용자들의 입맛에 맞게 데이터를 생성하고 이용하는데 본질을 두고 있다고 말할 수 있습니다. 따라서 서비스는 어떤 데이터를 저장하는냐에 따라 그 모습이 결정된다고 말할 수도 있겠죠
서비스의 본질은 데이터다.
아키텍쳐는 무엇인가요?
Chat GPT한테 아키텍쳐가 무엇인가? 라고 물어봤습니다.
어플리케이션 아키텍처는 소프트웨어 애플리케이션의 구조나 디자인을 설계하고 구현하는 방법을 나타내는 개념입니다. 이는 소프트웨어 시스템이 어떻게 구성되고 상호 작용하는지를 결정하는 중요한 틀을 제공합니다. 아키텍처는 애플리케이션의 성능, 확장성, 유지보수성 및 보안과 같은 다양한 측면을 고려하여 결정됩니다. 일반적으로 소프트웨어 아키텍처는 여러 구성 요소, 모듈, 레이어, 그리고 이러한 요소 간의 상호 작용 방식을 정의합니다.
전체 시스템이나 애플리케이션이 어떻게 동작하고,
데이터가 어떻게 흐르며,
각 부분이 어떻게 역할을 수행하는지에 대한 개념적인 모델을 제공합니다.
아키텍쳐는 서비스가 생성하는 모든 데이터를 잘 관리할 수 있도록 만드는데 그 목적을 가지고 있습니다.
우리(개발자)들은 사용자가 만든 데이터를 목적에 맞게 분리하고 추상화하여 관리를 해야하는 의무가 있습니다. 데이터의 파이프라인을 따라 DB와 같은 저장소에 안전하게 저장해야하며 사용자가 원할때에는 데이터를 다시 조합하여 사용자가 원하는 모습으로 만들어주기도 해야합니다. 이런 데이터의 흐름, 파이프라인을 관리하는 방법 또는 그 파이프라인 자체를 아키텍처라고 정의할 수 있습니다.
아키텍쳐가 정해져 있지 않은 서비스들이 겪는 어려움
만약 우리가 개발할때 아키텍쳐가 정해져 있지않다면 어떤 어려움이 있을까요?
결국 아키텍쳐가 없다는 말은 사용자의 상호작용으로 만들어진 데이터가 관리되는 단일 파이프라인이 존재하지 않는다는 의미를 가집니다.
그럼 결국 위의 그림처럼 우리는 서비스에서 저장소로 데이터를 보내는 아주 많은 파이프라인을 만들게 될 겁니다. 심지어 한명이 아닌 여러 개발자가 같이 협업하는 구조라면 더더욱 이런 파이프라인은 많고 다양하며 복잡해질 겁니다.
심지어 다른 사람이 만든 파이프라인을 회피하는 필요없는 코드까지 생성되겠죠?
그래서 데이터가 생성되고 저장될때까지 그리고 다시 그 데이터를 사용자가 사용할 수 있도록 전달되는 과정에서 관리되는 하나의 파이프라인을 만들 필요가 있습니다. 이것을 아키텍처라고 말합니다.
이런 파이프라인을 실제 복잡해도 괜찮습니다. 모두가 관리하고 유지하며 지속적으로 따를 수 있다면 그건 아키텍쳐로서 역할을 수행하고 있다고 말할 수 있습니다. 하지만 복잡한 아키텍처는 개발자에게 많은 스트레스와 어려움을 주게 됩니다.
나쁜 아키텍쳐는 무엇인가?
좋지 않은 아키텍쳐는 우리에게 어떤 어려움을 가져다 줄까요?
- 데이터 흐름의 불확실성
- 반복적인 양방향 바인딩으로 인한 데이터 흐름 방해
- 확장이 불가한 구조
- 단순한 기능을 위해 많은 코드를 작성해야하는 유지보수성
- 명확하지 않은 함수 / 객체의 존재
- 도메인에 적합하지 않은 모듈
- 등등등…
→ 결국 가장 힘든건 확장하거나 개발하기 힘든 구조
이런 단점을 지닌 아키텍처는 서비스에 안좋은 영향을 미치게 되는데요, 시간이 지남에 따라 서비스의 크기가 증가하지만 개발속도는 현저하게 떨어지는 주요한 요인이 됩니다.
복잡한 아키텍처는 어떤것이 있을까?
복잡한 아키텍처의 예시 중 좋은 예가 하나 있습니다. React가 태동하고 가장 먼저 Global State의 최강자로 군림했었던 Redux와 Redux의 배경이라 할 수 있는 Flux 패턴이 그 예시입니다.
Flux 패턴 자체는 그 철학과 데이터 흐름의 독립성을 보장하는 좋은 구조를 가지고 있습니다. 하지만 우리는 이 패턴을 쓸때 아주 큰 고통을 겪었습니다. 흐름을 만들기 위한 Action과 Store 구조 그리고 이를 dispatch하는 복잡성이 아주 많은 코드를 양산하게 만들었습니다.
- 단순한 기능을 위해 많은 코드를 작성해야하는 유지보수성
- 확장이 불가한 구조
결국 이런 문제들에 부딪히면서 우리의 기억속에서 Flux와 Redux는 현재 채택되지 않고 있습니다.
좋은 아키텍처란 무엇인가?
그럼 위와 반대로 좋은 아키텍처는 어떤 아키텍처일까요?
데이터 흐름의 불확실성→ 예측 가능한 데이터의 흐름반복적인 양방향 바인딩으로 인한 데이터 흐름 방해→ 데이터의 적절한 의존과 일관된 흐름확장이 불가한 구조→ 확장하기 좋은 구조단순한 기능을 위해 많은 코드를 작성해야하는 유지보수성→ 반복되는 코드의 최소화명확하지 않은 함수 / 객체의 존재→ 명확한 역할을 지닌 함수 / 객체 (레이어)의 존재도메인에 적합하지 않은 모듈→ 도메인에 잘 설계된 모듈- 등등등…
→ 역시 제일 중요한건 확장이 편하며 예측한대로 배치되어 있는 코드 구조
모든 서비스는 시간이 지나고 그 크기가 커짐에 따라 개발속도가 줄어들게 됩니다. 하지만 좋은 아키텍처를 이용한다면 이런 속도는 지속 가능하게 유지할 수도 있습니다.
MVC 패턴
현재 Backend에서 주력으로 사용되는 아키텍처입니다. MVC를 기반으로 DDD와 같은 설계 철학의 일부를 차용하는 등 다양한 앱에서 활용되고 사용되고 있습니다.
MVC 패턴은 Controller에 요청을 보내면 Model을 View에 끼워서 내려주는 패턴을 의미합니다. 레이어를 간단하게 끼얹으면 아래와 같아집니다.
이런 모던 아키텍처에서는 Service가 비즈니스 로직을 수행하고 Repository를 통해 Entity를 꺼내옵니다. 그리고 필요한 모델로 생성하여 Controller로 전송하게 됩니다. 그럼 고전 SSR에서는 HTML에 모델을 껴서 내려주게되며 ajax와 같은 형태에서는 XML이나 JSON이라는 형태의 View에 Model을 장착해서 결과를 보내줍니다.
public class UserController{
private final UserService userService;
@GetMapping()
public UserDto addFriend(){
return userService.addFriend();
}
}
---
public class UserService{
private final UserRepo userRepo;
private final EmailBox emailBox; // infra layer
public UserDto addFriend(){
... 비즈니스 로직...
emailBox.send("email@gmail.com", "~~~~");
user.addFriend(friend);
... 비즈니스 로직...
return convert(userRepo.findById(), new UserDto())
}
}
---
// DB또는 저장소에 데이터 CRUD역할 수행
interface UserRepo{
public User findById();
}
---
// 도메인 (Entity) 객체의 역할 수행
public class User{
int id;
String name;
List<User> friends;
public void addFriend(User friend){
friends.add(friend);
}
}
위 코드의 특징을 보면 위에서 설명했던 좋은 아키텍처의 특징을 찾을 수 있습니다.
각 코드별로 예측가능한 역할을 수행합니다.
Controller는 요청을 처리하고 결과 View를 제공하고 Service는 비즈니스 로직을 수행합니다. Repository에 정보를 요청하거나 Infra Component를 활용하여 Email을 전송하기도 합니다. Repository는 DB나 저장소에서 데이터를 CRUD합니다. Repository를 통해 꺼내진 User객체는 도메인 로직을 수행하거나 데이터를 Service에 제공합니다.
이러한 구조는 데이터의 흐름 또한 매끄럽습니다. Controller부터 Entiry까지 선형으로 데이터가 이어져 흘러갑니다.
하지만 아쉽게도 MVC는 모든 상황에서 적합한 아키텍처는 아닙니다. 이런 아키텍처는 각 요청을 Controller에서 명확하게 분리하여 요청받는 구조로 최적화 되어 있습니다. 결국 고전 SSR 또는 API와 같이 요청과 결과가 1대1로 명확할때 유리합니다.
유저가 사용하는 네이티브 앱이나 Single Page Application은 사용자가 지속적으로 소통을 하며 상호작용이 1대1로 이루어지지 않습니다. 그래서 Frontend나 Native환경에서는 데이터의 흐름을 제어하기 까다롭고 확장에 용의하지도 않게 됩니다. 모든 사용자의 상호작용은 Controller화 할 수 없으니까요
MVVM 패턴
그래서 모던 아키텍처에서는 이런 Frontend와 Native에서 MVVM 패턴을 이용합니다.
MVVM 패턴은 Controller가 아닌 View에서 사용자의 요청을 받게 됩니다. 그래서 각 View에 다양한 이벤트를 연결할 수 있으며 상호작용하기 최적화된 구조를 이끌어 낼 수도 있습니다.
MVVM 패턴의 데이터 흐름은 다음과 같습니다. View는 사용자에게 정보를 보여주며 상호작용을 수행합니다. 여기서 입력받은 데이터는 ViewModel로 이동하고 각 모델의 형태로 저장됩니다. 또는 View에서 데이터를 ViewModel로 요청하게 되고 ViewModel을 Model의 데이터를 꺼내서 View에 최적화된 Model로 재 생성하여 전달합니다.
MVC의 예시처럼 조금 더 레이어를 추가해보자면 ViewModel은 Service를 통해 Model을 가져오게 됩니다. Service는 API를 호출하도록 도와주거나 Local Storage 또는 Global State의 정보를 꺼낼 수 있는 Key를 ViewModel에 전달합니다. ViewModel은 이런 Model을 역시 재조합하여 View에서 사용가능한 형태로 변경해 줍니다.
Backend에서 Model이 DB에 저장된 데이터가 큰 비중을 차지했다면 Frontend나 Native에서 Model은 Backend에서 제공하는 데이터가 큰 비중을 차지하게 됩니다. (물론 서비스 특성에 따라 Local Storage나 Global State가 큰 비중을 차지할 수도 있습니다.)
function MyPageView(){
const { me } = useMyPageViewModel();
<Column>
<Row>
<Typograph text={me.name}/>
</Row>
</Column>
}
---
function useMyPageViewModel(){
const { me } = useQuery({
queryKey: ["users", "me"],
queryFn: ()=>userService.me()
})
function modifyLocalName(){
...View에 대한 비즈니스 로직...
userService.modifyLocalName(name);
}
return { me: me.data, modifyLocalName };
}
---
class UserService{
function me(){
const response = await api.get("/users/me") // api = axios.create({ baseUrl: "url" });
return response.data;
}
function modifyLocalName(name: string){
... Model에 대한 비즈니스 로직 ...
LocalStorage.setItem("", "");
}
}
export const userService = new UserService();
간단한 수도코드를 확인하면 각 레이어의 역할을 명확하게 기대할 수 있습니다.
View에서는 Html과 Css처럼 실제 유저들이 사용하는 화면에 대한 코드를 기대할 수 있으며, ViewModel에서 데이터를 구독하거나 상호작용을 처리하는 비즈니스 로직을 호출합니다.
ViewModel에서는 View의 요청을 처리하며, View에서 사용할 데이터의 상태를 관리합니다. 또한 서버에 API를 호출하는 것 처럼 Model을 사용할 필요가 생기면 Service를 호출하여 데이터를 불러옵니다.
Service는 데이터를 가공하거나 도메인에 최적화된 비즈니스 로직이 처리합니다. 또 ViewModel에서 구독하여 사용할 Global State들을 관리하게 됩니다.
정리
서비스를 지탱하는 아키텍처들을 알아봤는데요.
아키텍처는 우리의 서비스에 적합한 방식으로 만들고 찾아 나가야합니다. 이러한 아키텍처는 설계하는 개발자에게 많은 영향을 받기도 하며 팀마다 세부적인 규칙은 다르기도 합니다.
따라서 정답을 찾기보다는 최적화된 방법을 찾을 수 있도록 유연하게, 열린 마음을 가지고 접근하는게 중요합니다!
'개발' 카테고리의 다른 글
아키텍처를 공부하며 겪었던 성장통 (0) | 2024.01.01 |
---|---|
나아가기 위해 버린다 (0) | 2022.09.15 |
Service와 Interface (0) | 2022.09.14 |
악마는 디테일에 있다 (1) | 2022.07.11 |
My React OOP + MVVM practice (feat. OOP, Typescript) -2- (0) | 2022.07.08 |
- Total
- Today
- Yesterday
- Java
- spring boot
- 게임
- QueryDSL
- 개발
- 인디게임
- 튜토리얼
- 용사
- 보따리장사
- JPQL
- 사이드프로젝트
- frontend
- JPA
- Unity3D
- 우주게임
- 모험
- mobx
- spring
- Lombok
- 턴드림
- 개발일지
- 이명규
- 스크럼
- 인디
- 게임개발
- JIRA
- studio108
- 게임 개발
- 유니티
- 신작
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |