1부 Chapter 02. RxJS가 해결하려고 했던 문제 2 - 상태 전파 문제
1.1 웹 어플리케이션의 상태
웹 어플리케이션은 하나의 큰 상태머신
이를 구성하고 있는 크고 작은 단위들 또한 하나의 상태 머신
이 상태 머신들이 서로 유기적으로 연결되어 있음
이런 상태 머신들은 서로간에 의존성을 가지고 있음
사용자 정보(상태)를 System 클래스가 check() 함수에서 사용하고 있는 예제 System과 User 간에는 다음과 같은 의존성이 존재
class User{
constructor(){
this._state = {
name: '고재훈',
isLogin: false
}
}
getName(){
return this._state.name;
}
isLogin(){
return this._state.isLogin;
}
login(name){
this._state.name = name;
this._state.isLogin = true;
}
logout(){
this._state.name = '';
this._state.isLogin = false;
}
}
class System{
constructor(user){
this._token = null;
this._id = 'System';
this._user = user;
}
check(){
const username = this._user.getName();
if(this._user.isLogin()){
this._token = [...username].reduce((acc,v)=>acc+v.charCodeAt(0),0);
console.log(`[${this._id}] ${username}의 토큰은 ${this._token} 입니다.`);
}else{
this._token = null;
console.log(`[${this._id}] 로그인 되지 않았습니다.`);
}
}
}
let user = new User();
let system = new System();
// System 작업
system.check(); // [System] 로그인 되지 않았습니다.
// User의 상태변화 발생
user.login('jhgo');
// System 작업
system.check(); // [System] jhgo의 토큰은 769 입니다.
// User의 상태변화 발생
user.logout();
// System 작업
system.check(); // [System] 로그인 되지 않았습니다.
System은 User의 로그인 정보에 의해 System의 출력(상태)이 결정 됨
1.2 웹 어플리케이션의 상태 변화로 인한 문제점
1.2.1 첫째, User의 인터페이스가 변경되면 System도 함께 변경해야 한다
System에서는 user의 메소드인 getName과 isLogin을 사용하고 있음
class System{
// ...
check(){
const username = this._user.getName();
if(this._user.isLogin()){
// ...
}else{
// ...
}
}
}
클래스의 크기가 커지면 커질수록 변경에 대한 영향도는 점점 커짐
다른 클래스 A, B, C… 등이 모두 User에 의존도를 가지고 있을 경우 변경에 대한 영향도는 더욱 증가됨
1.2.2 둘째, User 상태를 확인하기 위한 인터페이스에 대한 의사소통 비용이 발생한다
User를 개발한 개발자와 User와 의존 관계가 있는 다른 클래스를 개발한 개발자 사이에는 의사소통에 따른 비용이 발생함
지금은 User의 인터페이스가 getName, isLogin, login, logout 정도이지만 인터페이스가 늘어난다면 이에 대한 비용이 증가
1.2.3 셋째, 다수의 클래스가 User에 의존 관계가 있는 경우라면 User의 변경 여부를 반영하기 위해 다수의 클래스들이 직접 User의 상태를 모두 반영해야 한다
변경에 대한 전파가 원활하게 이루어지지 않음
이는 번거로운 작업일 뿐만 아니라 User와 의존성이 있는 다수의 클래스들과의 의존 관계를 항상 염두에 두고 개발을 해야 하기 때문에 잦은 오류가 발생 할 수 있음
// User 상태 변화 발생
user.login('jhgo');
// User과 의존 관계가 있는 classA
// User과 의존 관계가 있는 classB
// User과 의존 관계가 있는 classC
// User과 의존 관계가 있는 classD
classA.process();
classB.process();
classC.process();
classD.process();
1.3 우리가 이미 알고 있는 솔루션 - 옵서버 패턴(Observer pattern)
1.3.1 Loosely Coupling
옵서버 패턴에서는 상태가 변경될 대상을 서브젝트(Subject) 라고 함
그 상태 변화를 관찰하는 대상을 옵서버(Observer) 라고 함
옵서버 패턴에서는 서브젝트와 옵서버가 서로 느슨하게 연결되어 있음
이는 서브젝트와 옵서버가 서로 상호작용을 하지만 서로 잘 모름
서브젝트가 옵서버에 대해 아는 것은 옵서버가 특정 인터페이스를 구현 한다는 것뿐
옵서버는 언제든지 추가 삭제 가능
새로운 타입의 옵서버를 추가 한다고 해도 서브젝트를 변경할 필요가 없음
서브젝트와 옵서버는 서로 독립적으로 사용 가능
옵서버가 바뀌더라고 서로 영향을 미치지 않음
1.3.2 자동 상태 전파
Pull 방식 : 데이터를 얻고자 하는 대상이 데이터를 직접 가져오는 방식, 매번 요청을 하여 변경사항을 확인해야 함
Push 방식 : 옵서버 패턴, 의존 관계의 대상 즉 서브젝트로 부터 데이터를 제공 받는 방식
Push 방식은 Pull 방식에 비해 상태 전파 문제를 효과적으로 처리 가능
옵서버 패턴은 서브젝트의 상태가 변경 되었을 경우 옵서버에게 자동으로 알려줌
특히 서브젝트와 옵서버가 1:n의 상황에서 더욱 효과적
개발자는 데이터 변경 시점을 신경 쓰지 않고 변경되었다는 신호가 왔을 경우 처리 하면 됨
1.3.3 인터페이스 단일화
인터페이스가 증가할 때마다 개발자 간의 의사소통 비용이 증가하고 변경 영향도도 커짐
인터페이스를 특정 몇개로 통일하면 비용을 줄일 수 있음
변경 사항이 생기더라도 영향도는 기존보다 낮아짐
1.4 옵서버 패턴의 흔한 예
뉴스를 발행하는 신문사와 이를 구독하는 고객의 경우
뉴스 정보를 저장하는 클래스, setNews() 메소드를 제공
class NewsPaper{
setNews(){
}
}
NewsPaper 클래스를 Subject 역할을 할 수 있도록 구현
Observer를 등록 하고 삭제할 수 있는 add와 remove 메소드를 추가
상태 변경이 있어났을 때 각 Observer의 update 메소드를 호출하는 notify 메소드를 추가
class NewsPaper{
constructor(){
this._observers = [];
}
setNews(news){
this.notify(news);
}
add(observer){
this._observers.push(observer);
}
remove(observer){
let idx = this._observers.indexOf(observer);
if(idx != -1){
this._observers.splice(idx,1);
}
}
notify(news){
this._observers.forEach( v => {
v.update(news);
});
}
}
뉴스를 구독하고자 하는 NewsScrapper 와 NewReader Observer를 만들기
class NewScrapper{
update(news){
console.log(`뉴스를 스크랩하자 = ${news}`);
}
}
class NewReader{
update(news){
console.log(`뉴스를 읽자 = ${news}`);
}
}
NewsPaper에 구독을 원하는 Observer(NewScrapper, NewReader)를 등록하고 NewsPaper의 setNews()를 통해 뉴스의 내용을 변경
let newsPaper = new NewsPaper();
// 구독하기
newsPaper.add(new NewScrapper());
newsPaper.add(new NewReader());
// 상태 변경
newsPaper.setNews('에어팟2 가을 출시 예정');
newsPaper.setNews('고프로7 블랙 특별 한정판 더스크 화이트 출시');
1.5 옵서버 패턴 적용하기
상태변화를 관찰할 User를 Subject로 만들고, System을 Observer로 변경
class Subject{
constructor(){
this._observers = [];
}
add(observer){
this._observers.push(observer);
}
remove(observer){
let idx = this._observers.indexOf(observer);
if(idx != -1){
this._observers.splice(idx,1);
}
}
notify(status){
this._observers.forEach( v => {
v.update(status);
});
}
}
class User extends Subject{
constructor(){
super();
this._state = {
name: '고재훈',
isLogin: false
}
}
getName(){
return this._state.name;
}
isLogin(){
return this._state.isLogin;
}
login(name){
this._state.name = name;
this._state.isLogin = true;
this.notify(this._state);
}
logout(){
this._state.name = '';
this._state.isLogin = false;
this.notify(this._state);
}
}
class System{
constructor(){
this._token = null;
this._id = 'System';
}
update(status){
if(status.isLogin){
this._token = Array.prototype.reduce.call(
status.name,
(acc, v) => acc + v.charCodeAt(0),
0
);
console.log(`[${this._id}] ${status.name}의 토큰은 ${this._token} 입니다.`);
}else{
console.log(`[${this._id}] ${this._token}은(는) 로그아웃되었습니다.`);
this._token = null;
}
}
}
let user = new User();
let system = new System();
user.add(system);
// User의 상태변화 발생
user.login('jhgo');
user.logout();
user.login('apple');
System은 생성자에서 더이상 User의 인스턴스를 받지 않음
기존에 비해 의존성은 느슨해졌고 변경 상태도 의존성을 가진 모든 객체에 즉시 전파됨
System 클래스에 생성자의 파라미터로 구분자를 받는다면 다수의 Login 모듈을 User에 등록하여 사용할 수도 있음
class System{
constructor(id){
this._token = null;
this._id = id;
}
// ...
}
이제는 User의 add를 통해 등록만 하면 User의 상태 변화를 모두 감지 가능
let user = new User();
let observer1 = new System('observer1');
let observer2 = new System('observer2');
let observer3 = new System('observer3');
user.add(observer1);
user.add(observer2);
user.add(observer3);
// User의 상태변화 발생
user.login('jhgo');
user.logout();
user.login('apple');
1.6 RxJS는 무엇을 해결하고자 했는가?
RxJS도 상태 변화에 대한 문제를 옵서버 패턴을 기반으로 해결 하려 함
다만 기존의 옵서버 패턴의 아쉬움 몇가지 점을 개선
앞에서 작성한 신문사와 구독자를 예를 기준으로 살펴봄
1.6.1 상태 변화는 언제 종료되는가?
만약, 뉴스 서비스 종료로 더 이상 뉴스를 전달하지 않게 되었다면 어떻게 구독자들(NewScrapper, NewReader)에게 이 내용을 전달할 수 있을까?
다음의 코드처럼 뉴스 서비스 종료라는 특정 문자를 각 구독자들에게 보내고
구독자는 종료라는 상태를 전달 받으면 중지 되었다고 생각하고 별도의 처리를 해야 함
class NewScrapper{
update(news){
if(news === '뉴스서비스 종료'){
console.log('뉴스를 스크랩 서비스가 종료되었음');
}else{
console.log(`뉴스를 스크랩하자 - ${news}`);
}
}
}
class NewReader{
update(news){
if(news === '뉴스서비스 종료'){
console.log('뉴스를 스크랩 서비스가 종료되었음');
}else{
console.log(`뉴스를 읽자 - ${news}`);
}
}
}
let newsPaper = new NewsPaper();
// 구독하기
newsPaper.add(new NewScrapper());
newsPaper.add(new NewReader());
// 상태 변경
newsPaper.setNews('에어팟2 가을 출시 예정');
newsPaper.setNews('고프로7 블랙 특별 한정판 더스크 화이트 출시');
// 서비스 종료
newsPaper.setNews('뉴스서비스 종료');
이는 의사소통 비용에 대한 문제를 완벽하게 해결하지 못한 경우
옵서버 패턴은 상태를 전달하는 Subject의 데이터가 언제 종료되는 Observer들은 알 수 없음
이를 해결하기 위해서 Observer와 Subject 같에 별도의 규칙을 정해야함 함
결국 또 다른 의사소통 비용을 지불해야 하고 그런 이유로 코드에 if문을 만들어야함 함
1.6.2 상태 변화에서 에러가 발생하면?
기능이 정상 작동하는 경우가 대다수지만 반드시 고민해야 할 부분 중 하나가 에러 처리
NewsPaper의 setNews 메소드에서 다음과 같은 에러가 발생하게 된다면??
setNews(news){
throw new Error('NewsPaper Error'); //에러발생
this.notify(news);
}
Subject 자체적으로 에러 처리 가능
try-catch문을 사용하면 에러 발생시 다시 한번 상태 변경을 시도하거나 상태 변경 작업 자체를 무시하는 등의 작업이 가능
setNews(news){
try{
throw new Error('NewsPaper Error'); //에러발생
this.notify(news);
} catch(e){
// 다시 시도
this.notify(news);
}
}
위의 코드처럼 Subject 쪽에서 에러에 대한 처리를 별도로 하면 등록된 Observer들은 상태 변경시 에러가 발생 여부를 모름
하지만, 경우에 따라서는 등록된 Observer 쪽에서 에러 여부를 인지하고 별도의 처리를 해야하는 경우가 필요함
옵서버 패턴은 update 인터페스만을 통해서 Subject의 상태를 Observer에게 전달하기 때문에 이 상황을 처리하기는 어려움
옵서버 패턴에서는 에러 발생 여부를 Observer들에게 전달할 방법이 딱히 없음
1.6.3 Observer에 의해 Subject의 상태가 변경되는 경우는?
신문사 기자이면서 동시에 구독자인 사람이 있는 경우라면?
생각보다 심각한 상황이 됨
class WriterAndReader{
constructor(newsPaper){
this._newPaper = newsPaper;
}
update(news){
if(news === '뉴스서비스 종료'){
console.log('뉴스를 스크랩 서비스가 종료되었음');
}else{
console.log(`전달 받은 뉴스 - ${news}`);
this._newPaper.setNews(`변형된 뉴스 - ${news}`);
}
}
}
// 구독하기
newsPaper.add(new WriterAndReader(newsPaper));
// 상태 변경
newsPaper.setNews('에어팟2 가을 출시 예정');
뉴스는 계속 반복적으로 생성되어 결국 브라우져는 뻗게 됨
이와 같이 Subject를 관찰하는 Observer가 Subject의 상태를 변경하는 경우에는 예상치 못한 상황에 직면할 수 있음
1.7 RxJS는 어떻게 개선하였나?
RxJS에 녹아 있는 옵서버 패턴은 RxJS에서 전달되는 데이터는 모두 Observable 형태로 반환
Observable은 구독(subscribe) 과정 후부터 데이터를 전달받기 시작
const { fromEvent } = rxjs;
const click$ = fromEvent(document,'click');
click$.subscribe(function(v){
console.log(v);
});
//또는
click$.subscribe({
next: function(v){
console.log(v);
}
})
반면, 옵서버 패턴에서는 Subjectdhk Observer가 add 과정 후부터 데이터를 전달 받기 시작
let newsPaper = new Subject();
newsPaper.add({
update: function(v){
console.log(v);
}
});
RxJS의 Observable은 옵서버 패턴의 Subject와 닮았음
Observer는 RxJS에도 있고 옵서버 패턴에도 있음
차이점은 옵서버 패턴의 Observer는 add라는 메소드를 통해 Subject에 전달 됨
반면, RxJS의 Observer는 함수와 객체 둘다 가능하며 subscribe라는 메소드를 통해 Subject에게 전달 됨
RxJS의 Observable과 Subject RxJS에는 Observable도 있고 Subject도 있음 RxJS의 Subject는 기능적으로 정확히 옵서버 패턴의 Subject와 일치 다수의 Observer에게 공통의 데이터를 전달하고 update와 같은 메소드가 존재하여 데이터의 변경도 가능 반면 RxJS의 Observable은 기능적으로는 옵서버 패턴의 Subject와 다름 Observable은 단지 하나의 Observer에게 독립적인 데이터를 전달
1.7.1 인터페이스의 확장
RxJS는 시간의 축으로 데이터를 보기 때문에 데이터의 연속적인 변화를 Observable에서 표현 가능하도록 기존의 update 메소드를 next로 바꿈
또한 옵서버 패턴에서는 종료, 에러 시점에 대한 인터페이스가 존재하지 않지만 RxJS에서는 종료를 나타내는 complete, 에러를 나타내는 error 메소드를 추가 함
왜 RxJS는 객체가 아닌 함수를 사용하는가? Observable.subscribe는 다음과 같이 객체, 함수 모든 형태로든 다 전달 받을 수 있음
const { of } = rxjs;
const numbers$ = of([1,2,3,4,5,6,7,8]);
// next, error, complete 가 있는 객체를 받음
numbers$.subscribe({
next(v){
console.log(v);
},
error(e){
console.log(e);
},
complete(){
console.log('complete');
}
});
// next 함수만 받음
numbers$.subscribe(v => {
console.log(v);
})
// next, error 함수만 받음
numbers$.subscribe(v => {
console.log(v);
},e => {
console.log(e);
});
// next, error, complete 함수만 받음
numbers$.subscribe(v => {
console.log(v);
},e => {
console.log(e);
},() => {
console.log('complete');
});
하지만, RxJS의 subscribe는 특별한 경우를 제외하고는 가급적 함수 형태를 사용
그 이유는 객체는 상태를 가질 수 있기 때문
객체가 상태를 가진다는 의미는 또라는 상태 머신이 될 수 있다는 의미
반면 함수는 상태가 존재하지 않는 기능만을 담당하기 때문에 상태에 관함 문제에서는 자유로움
1.7.2 Observable은 Read-only
‘기자이면서 구독자인 사례’와 같이 데이터가 양방향으로 흐르는 문제를 RxJS는 구조적으로 개선하려고 함
Observable은 subscribe를 통해 전달할 대상에게는 데이터를 전달 할 수 있지만 반대로 받을 수는 없음
즉 데이터는 한방향으로만 흐르게 됨
데이터가 양방향으로 흐르게 되면 사용상 편리할 수는 있지만 복잡도가 증가하므로 오류의 확율도 커짐
최근의 프레임워크들은 모두 단방향 데이터 흐름을 지향
1.8 Observable은 리액티브하다
RxJS는 데이터가 발생하게 되면 Observer에게 자동으로 빠르게 변경된 데이터를 전달
이를 “리액티브하다” 라고 이야기 함
리액티브 프로그래밍은 데이터 흐름과 상태 변화 전파에 중점을 둔 프로그래밍 패러다임이다. 사용되는 프로그래밍 언어에서 데이터 흐름을 쉽게 표현할 수 있어야 하며 기본 실행 모델이 변경 사항을 데이터 흐름을 통해 자동으로 전파한다는 것을 의미한다.
가장 핵심이 되는 단어는 데이터 흐름과 자동으로 전파
즉, 상태 변화의 흐름이 자동으로 전파되는 것을 리액티브하다고 이야기 함
RxJS는 리액티브 프로그래밍을 지향하는 라이브러리