월급날까지 3일밖에 남지 않았지만, 월급날 당일 신혼집 생활용품 주문 및 집들이로 산 음식값들로 인해 빠져나갈 돈 + 이자랑 이것저것하면 월급날 월급이 다 빠져나갈 것 같은 4주차를 앞둔 3주차의 회고록이다.
Background Music
내 최애장르중 하나인 시티팝 그 중에서도 좋아하는 노래를 꼽자면 플라스틱 러브를 좋아한다.
일본 시티팝이 전체적으로 한창 호황기였던 일본의 시대를 잘 보여준다. 거품이 너무 많이껴서 뭔가 공허하게 사라질 것까지 보여주는 것 같다.
ATDD 2주차 완료 및 3주차 진행
운이좋은건지 아니면 안좋은건지 모르겠지만, 현재 거의 모든 미션과정을 1번에 바로 통과하거나 한 번정도의 피드백적용 후 통과하고있다. 그래서 보통 목요일에 새로운 주차에 대한 세미나 이후 금요일부터 시작을 하는편인데 2주차는 일요일에 모든 미션을 클리어했다. 3주차 미션도 마지막 미션 하나만 남은 상태인데, 피드백이 좀 더 빨라서 일요일에도 피드백이 일찍 왔다면 오늘 내로 3주차도 완료할 수 있었을 것 같아 아쉽긴하다.
이전에 들었던 내용 중 TDD - ATDD - DDD 순으로 과정들을 들으면 된다는 말을 들을때는 별 생각이 없었는데, 확실히 TDD과정에서 겪은 수많은 시행착오들이 ATDD과정의 템포를 한 템포 더 높히는데 큰 도움이 되는 것 같다.
그리고 현재 듣고 있는 김영한님의 스프링 핵심원리강의도 큰 도움이 되는 것 같다. 객체지향 프로그래밍 원칙중 OCP, DIP는 실무를 2년정도 진행하면서도 그냥 Autowired에 의존하여 그냥 자동으로 DI되는걸 쓰기만했기에 따로 고심하거나 객체간의 참조를 끊는것에 대해 고려해본적이 없었다.
그렇기에 2년전 진행했던 조영호님의 Object책과 객체지향의 사실과 오해 두 책에 대한 스터디를 끝내고나서도 크게 공감하거나 깨달음을 얻지못하고 그냥 머리로만 아는 지식이였는데, 해당 강의를 듣고 ATDD미션을 진행하면서 3주차에서 인증객체와 사용자 도메인객체의 의존관계를 끊음으로써 DIP와 OCP원칙을 지킴으로써
각각의 구성영역과 실행영역을 분리하고 확장과 변경에 유연한 인프라가 구성되었다.
3주차 미션은 대체로 스프링 시큐리티의 개념을 차용한 미션과 인수테스트 진행과정이였는데, 여기서 작년에 정리했던 스프링 시큐리티가 많은 도움이 되었다. 스프링 시큐리티를 공부하지 않았다면 인증객체, 인증객체토큰, SecurityContextHolder, SecurityContext등 많은 키워드와 개념들부터 공부하느라 미션 진행에 걸림돌이 될 뻔했다고 생각한다.
그리고 이 과정의 3주차에서 내가 느끼고 배워야겠다는 점을 정리해보자면 테스트적인 부분과 기술적인 부분인데,
테스트적인 부분
기본적으로 BDD방식의 테스트에서는 given, when, then 으로 스탭을 나눠 테스트에 필요한 데이터를 제공하여 특정상 상황에 어떤 결과를 반환하는지 테스트한다.
그럼 다음과 같이 지하철 노선 삭제 시나리오를 보자.
Scenario: 지하철 노선을 제거한다.
Given 지하철 노선이 등록되어 있다
When 지하철 노선을 삭제 요청한다.
Then 지하철 노선이 삭제된다.
Java
복사
위 시나리오를 테스트로 구현하면
•
given: 지하철 노선 등록 request 전송
•
when: 지하철 노선 삭제 request 전송
•
then: 지하철 노선 삭제 확인.
여기서 고민해야 할 부분은 then부분이다 지하철 노선 삭제 여부를 어떻게 검증할 것인가? 간단하게는 삭제 요청의 응답결과 Resposne의 응답코드를 확인하는 것이다.
하지만, 이것만으로는 확신이 들지 않을 경우에는 다시 지하철 노선을 조회한 뒤 해당 노선이 삭제되었는지 확인 할 필요가 있다. 그렇다면 then 이후 다시 지하철 노선을 조회하는 when과 해당 노선을 확인하는 then스텝이 필요하다. 그럼 시나리오는 다음과 같이 작성되어야 한다.
Scenario: 지하철 노선을 제거한다.
Given 지하철 노선이 등록되어 있다
When 지하철 노선을 삭제 요청한다.
Then 지하철 노선이 삭제된다.
When 지하철 노선목록을 요청한다.
Then 지하철 노선 목록을 확인한다.
Java
복사
여기서 지하철 노선목록 조회및 목록 확인 부분은 삭제 테스트가 아닌 생성 및 조회 테스트에서도 중복되는 것 같지 않은가?
이럴 경우 우리는 인수 테스트 통합을 할 수 있다.
인수 테스트 통합
Feature: 즐겨찾기를 관리한다.
Background
Given 지하철역 등록되어 있음
And 지하철 노선 등록되어 있음
And 지하철 노선에 지하철역 등록되어 있음
And 회원 등록되어 있음
And 로그인 되어있음
Scenario: 즐겨찾기를 관리
When 즐겨찾기 생성을 요청
Then 즐겨찾기 생성됨
When 즐겨찾기 목록 조회 요청
Then 즐겨찾기 목록 조회됨
When 즐겨찾기 삭제 요청
Then 즐겨찾기 삭제됨
Java
복사
물론, 이렇게 통합을 할 경우 하나의 테스트에서 검사할 항목이 많아지고 테스트가 어려워질 수 있다. 하지만, 사용자 관점에서 전체적인 흐름을 확인할 수 있으며 테스트의 중복을 제거할 수 있다. 그리고 자연스러운 전체의 한 흐름을 검사하기에 사용자 스토리 검증이 가능하다. 하지만, 처음부터 이 시나리오를 다 작성하는 것은 쉽지 않다. 그렇기에 우리는 기존처럼 하나하나 유닛테스트를 하듯이 테스트를 만들고 이를 위처럼 하나로 통합하는 과정을 통해 인수테스트 통합을 한다.
물론, 처음부터 하나의 메서드로 한 스텝씩 검증하면서 구현하는 것 역시 가능하다.
이런 인수 테스트 통합은 비용소모가 너무 크고 변화에 취약하기에 너무 많이 만드는 것 역시 좋지 않다고 한다.
그러니 이런 장점과 단점들을 잘 고려해서 작성하는게 중요할 것 같다.
기술적인 부분
전체적인 스프링 시큐리티의 필터 및 아키텍처의 흐름이다.
3주차에서는 스프링 시큐리티의 방대한 부분에서 인증부분만을 떼와서 사용했는데, Form 방식의 세션관리방식과 Token을 이용한 방식을 제공한다. 위 이미지에서 UsernamePasswordAuthenticationFilter 이후 플로우에 대해서는 진행하지 않지만 이는 인가정책과도 맞물려있기에, 차후 따로 학습을 하는 것으로 하고, 그 이전 인증처리는 넥스트스텝에서 자체적으로 네이티브로 구축한 개념으로 스프링 시큐리티에서 인증객체(Authentication)을 만들어 SecurityContext에 저장한다음 해당 Conext를 SecurityContextHolder에 넣어준다.
그 덕에 스프링 영역내에서 SecurityContext를 꺼내 로그인 여부및 유저정보 확인이 가능하다.
미션을 진행할때는 위와같이 구축되있는 부분의 중복코드의 추상화 및, 구성영역(AuthConfig)에서 구현체를 주입해줌으로써 인증인터셉터의 DIP 원칙 위배를 해결하고 유저정보와 인증정보 패키지간 의존관계가 양방향으로 의존되어 생기는 DIP 원칙위배 문제를 스프링 시큐리티의 User객체 개념을 도입해 해결해줬다.
아쉬운점은 User밑에 UserDetail를 만들고 이를 사용해 username과 password관리를 떠나 인증객체별로 실제 유저정보 엔티티가 다르더라도 호환이 되도록 만들고싶었으나, 인증객체주입 어노테이션 구현부분(HandlerMethodArgumentResolver)에서 필드 타입을 원시타입으로 한정지어 만들어두었기에 이를 굳이 다시 풀어서 정리하자니 의존성이 생기고 이를 구성영역에서 특정 타입을 주입해주기에는 코드가 지저분해지는 것 같아서 나중을 기약하기로 했다.
이제 남은 3주차 미션인 즐겨찾기 기능을 구현해야하는데, 현재 프로젝트의 즐겨찾기를 해야할 노선과 지하철역 코드부분이 리팩토링이 안된 버전이라서 이전주차에 내가 리팩토링한 코드를 다시 적용해야 할 것 같다.
스프링 핵심원리 이해2 - 객체 지향 원리 적용
이번 챕터에서는 드디어 계속 문제로 말했던 OCP, DIP 원칙 위배를 어떻게 해결하는지에 대한 내용이다.
ATDD에서 진행된 부분과 내용이 많이 겹치는데 결국 관심사를 분리함으로써 각자 자기 배역만 담당하면 되도록 하는 것으로 여기서 생기는 이슈인 내가 사용할 인터페이스의 구현체를 어떻게 생성하느냐에 대해 내가 직접 생성하는게 아닌 구성영역(AppConfig)을 만들어 각각의 서비스계층에서 필요한 구현체들을 생성해 주입해주는 식으로 해결을 했다.
이번 챕터를 진행하며 내가 생각없이 사용하던 것들이 어떻게 돌아가는지 다시 한 번확인할 수 있는 시간이였고, 누구는 당연히 기본으로 알고 있는 부분중에 내가 모르는게 있을 수 있다는 점을 깨달았다.
사실 더 많은 챕터를 진행했어야 했으나, 미션진행 및 회사 업무로 많이 진행을 못한 것 같다.
회사 업무
VOIP이슈를 해결하고 이제 대부분의 문제가 해결되었다 생각하기 무섭게 이번에는 영상이 LTE환경에서 안나오기 시작한다. 특히 안드로이드(LTE) → 아이폰(WI-FI)에서 영상이 안나온다.
둘 다 Wi-FI로 할 경우 에는 문제없이 잘 접속이 되기에 문제가 없는줄 알았는데 크리티컬한 이슈가 발생했다.
1.
STUN/TURN 서버 확인
:접속한 기억도 한달이 넘어서 가물가물해진 AWS에 올라간 서버를 확인해보기로 했다. 우선 해당 서버에서 turnserver가 구동되어 돌아가고있는지 확인을 했다.
ps -ef | grep coturn
Shell
복사
잘 돌아가고 있다는게 확인이 되었고 이번엔 로그를 확인해보기로 한다.
아... 너무 많다.
음.. 상당히 방대한 로그가 나를 반긴다. 그래서 우선 최신 로그를 확인해보기 시작했다. 에러 내용은 다양했는데 Fobidden IP(403)부터 Authrization Error 까지... 근데 이런 부분들은 잘 확인해보니 잘 도달하는 상황에서도 생기고 있고, 양 측 모바일 장비가 서로의 IP를 탐색하는과정에서 생기기에 큰 이슈로 파악되지 않았다.
그리고 Trickle ICE 사이트에서 TURN 서버의 ICE Candidate 확인을 해본 결과 정상적으로 도달을 한다.
그럼 무엇이 문제인가... 슬슬 멘탈이 나가기 시작한다.
2. 서버 재설치 및 configuration 재확인
: 무언가 도달을 못하고 에러가 나긴하니 서버가 문제일 것으로 판단. 해당 Coturn서버를 재설치하기로 결정.
여차저차 다시 삭제하고 재설치. 그리고 가물가물한 기억을 되살려 다시 configuration 설정.
해당 파일은 /etc/turnserver.conf위치에 있다.
listening-port=3478
tls-listening-port=5349
external-ip=<PUBLIC IP>/<PRIVATE IP>
verbose
fingerprint
lt-cred-mech
server-name=서버명(Domain DNS할당 받은걸로 등록)
user=유저아이디:비밀번호
realm=렐름명
Shell
복사
turnserver.conf 설정 예시
이제 설정도 완료했고 다시 구동을했고 신나게 어플을 디버그모드로 동작해서 전화통화 터치!
그리고 실패.
피곤이 몰려온다.
Trickle을 통해 성공을해도 결국 영상통화가 계속실패를 하니 급속도로 피곤이 몰려온다. 여기서 더한 문제는 이런 이슈는 웹에서는 발생한적 없는 이슈였고, WI-FI로하면 STUN서버로 접속이되어 영상이 정상작동하니 문제 확인이 힘들었다. 그리고 심지어 IOS로 전화를 걸면 영상이 나온다. .....하..
3. 어플리케이션 코드확인
이번에는 그래서 TURN 서버를 내가 만든서버가 아닌 작년에 진행했던 프로젝트의 고객사 TURN서버 접속이 가능해 해당 서버로 설정해두고 동작을 했다. 근데 신기하게도 화면이 나온다. 이사님도 그럼 서버가 문젠가? 하시고 나 역시 코드가 아니라 서버가 문젠가... 하지만, 설정을 정말 다 똑같은데 뭐가 문제지... 하는 상황.
그리고 잘 살펴보니 무조건 첫 통화 시도는 실패하고 두 번째 시도부터 통화가 성공한다. 또한 반대에서 먼저 전화를 건 다음 내가 다시 전화를 하면 다시 첫 시도는 실패.. 이게 무슨 해괴망측한 상황이란 말인가.
슬슬 무당불러서 굿을 해야하는게 아닌가 싶은상황이 계속된다.
ice handshake sequence diagram
그러던 와중 양쪽 기기의 connect handshake flow를 확인하려고 디버깅을 하는 와중에 상대 유저가 방에 입장했을때 호출되는 sessionDescription 케이스에서 대략 10초정도 시간을 두고 확인이 끝난 뒤 계속을 눌러 진행을 했는데 영상이 나오는게 아닌가?!
그래서 다시 시도를 해보니 다시 안나오길래 다시 브레이크포인트를 잡고 10초 뒤 계속을 누르니 영상이 나오기시작하며 상황은 새로운 국면에 돌입한다.
4. 원인 파악
case 'sessionDescription':
{
var peerid = data['peerid'];
var session = _sessions[peerid]; // break point
if (session == null) {
print('no session');
var userid = data['userid'];
_sessions.removeWhere((key, sess) => sess.uid == userid);
session = await _createSession(session, peerid, userid);
_sessions[peerid] = session;
}
var description = data['session_description'];
await session.pc.setRemoteDescription(
RTCSessionDescription(description['sdp'], description['type']));
if (description['type'] == 'offer') {
await _createAnswer(session);
}
onCallStateChange?.call(session, CallState.CallStateConnect);
}
break;
Dart
복사
해당 위치에 브레이크 포인트를 걸고 정보를 확인해보니 _sessions[peerid]에 계속 값이 null인 경우에는 영상이 제대로 나오지 않았는데, session이 null일 경우 Session을 자체적으로 생성(createSession)해서 _sessions[peerid]에 대입하게되는 것인데, 이게 어째서 문제가 되느냐면 다음 코드 바로 addPeer부분을 확인해야 한다.
case 'addPeer':
{
var peerid = data['peerid'];
if (_peers.contains(peerid)) {
print('[$peerid] duplicated peer.');
break;
}
var userid = data['userid'];
_peers.add(peerid);
if (_sessions.containsKey(peerid)) {
print('[$peerid] duplicated session.');
break;
}
var session = _sessions[peerid];
var newSession = await _createSession(session, peerid, userid);
_sessions[peerid] = newSession;
bool shouldCreateOffer = data['should_create_offer'];
if (shouldCreateOffer) {
_createOffer(newSession);
}
onCallStateChange?.call(session, CallState.CallStateConnect);
}
break;
Dart
복사
유저가 방에 입장하면 addPeer가 최초 호출되는데 여기서 _createSession(session, peerid, userid); 메서드를 통해 새로운 세션을 만들어서 _sessions[peerid] = newSession; 으로 세션을 넣어준다.
그럼 정리해보자. 방에 유저가 입장하면 addPeer가 최초 호출되어 세션이 만들어지고 그다음 sessionDescription을 통해 상대방의 세션에 Description을 설정해 관계수립을 해야하는데, 어떻게 _session[peerid]가 null인 것일까?
그 이유는 비동기호출이기 때문에 생긴 문제였는데, _createSession을 await으로 호출한다고 하지만 상대방기기에서 다시 sessionDescription 이벤트가 emit되었을 때 해당 로직이 있는 onMessage 메서드는 새로 호출되는 것이고 새로 호출되는 메서드내에서는 바로 sessionDescription 케이스를 타기 때문에 이 호출 시점이 안맞으면 이렇게 에러가 발생하는 것이였다.
web은 자체적으로 순서를 보장하기때문에 생기지 않는 이슈였는데, appRTC는 unOfficial 이기 때문에 이런 이슈가 자동으로 처리되어 있지 않아서 생긴 문제였다.
5. 문제 해결
여기서 그럼 순서를 보장해줘야한다는 것인데 이를 어떻게 해결할 것인가? 정말 무식한 방법으로는 Timer를 돌리는 것일텐데, 이건 고려할 가치가 없고, 그 다음으로 고려한 방법은 두가지로 Mutex와 Completer를 이용한 방식으로 1:1 대화만 가지고 한다면 Completer를 이용해 간단하게 구현을 했겠지만 나중에 다중 사용자간 화상채팅을 고려해 Mutex를 사용하기로 했다.
•
Completer
Future<Null> isWorking = null;
void onMessage(event, data) async {
...
if (isWorking != null) {
await isWorking;
return onMessage(event, data);
}
var completer = new Completer<Null>();
isWorking = completer.future;
...
completer.complete();
isWorking = null;
}
Dart
복사
이렇게 Completer를 작성하면 최초로 들어온 addPeer동작시 해당 작업들이 다 끝나기전에 completer는 complete 되지 않기때문에 그 다음에 진입한 메서드들은 await으로 해당 작업을 기다린 뒤 해당 작업이 끝나면 onMessage메서드를 호출함으로써 재진입해 해당 메서드를 점유할 수 있게된다.
테스트 결과 영상도 잘 나오는걸 확인했다. 하지만, 다중 사용자가 계속해서 방에 진입할 경우에 대한 처리를 하기위해선 코드를 또 따로 구현해주거나 사용자별 Completer를 만들어서 지정해줘야하는데 그럼 코드가 지저분해지기에 Mutex를 사용하기로 결정했다.
•
Mutex
void onMessage(event, data) async {
switch (event) {
case 'addPeer':
{
var userid = data['userid'];
var session = Session(userid);
_sessions[userid] = session;
await session.mutex.acquire();
try {
await _initSession(session, userid);
bool shouldCreateOffer = data['should_create_offer'];
if (shouldCreateOffer) await _createOffer(session);
onCallStateChange?.call(session, CallState.CallStateConnect);
} finally {
session.mutex.release();
}
}
break;
case 'sessionDescription':
{
var userid = data['userid'];
var session = _sessions[userid];
assert(session != null);
await session.mutex.acquire();
try {
var description = data['session_description'];
session.pc.setRemoteDescription(
RTCSessionDescription(description['sdp'], description['type']));
if (description['type'] == 'offer') await _createAnswer(session);
onCallStateChange?.call(session, CallState.CallStateConnect);
} finally {
session.mutex.release();
}
}
break;
}
}
Dart
복사
Mutex는 Flutter의 외부 라이브러리로 해당 개념자체는 흔히 운영체제 시간에 배운 Mutex그대로이고 사용법도 간단해서 비동기상화에서 점유경쟁을 방어하는데 큰 도움이 되었다.
오큘러스 퀘스트2
운동도 하겠다는 핑계하에 나자신에게 정말 오랜만에 플렉스를 해줬다.
몇년전 멀티방에서 처음으로 접한 psvr과 그 이후 일산이나 코엑스에서 하는 게임쇼에서 체험해본 VR은 하나같이 크게 감동을 주지못했고, 땀만 나고 무겁기만 했기에 별로였으나 그냥 사람들이 하도 호평을 하고, 이걸로 운동하면 그래도 좀 할 수 있지 않을까 하는 핑계하에 사기로 결정했다. 아내도 요새 1년넘게 따로 이런 물건산적도없고 그저 강의료로만 번 돈의 대부분을 써버리니까 허락을 해줬기에 신나게 중고나라에 가서 검색을 하기 시작했다.
생각보다 매물은 자주 올라왔지만, 그 이상으로 구매희망자가 많아서 문자가 씹히거나 하는경우가 많았고, 그외에는 나는 택배거래가 아닌 직거래를 선호하는데(빨리 하고싶어서), 거리가 다 애매해서 퇴근하고 바로 가서 가져올 수 있는 거리의 매물을 찾다보니 생각보다 매물을 찾기가 힘들었다.
참고로 이 날 한명의 중고나라 판매자에게 사려고 문자를 했었는데, 위치가 거제도라고 해서 너무 멀기에 그냥 다른사람에게 사기로 하고, 답장을 안했는데, 다음날 알고보니 이 사람이 사기꾼이였고 그 하루사이에 사기당해서 글을 올린사람이 있었다 후덜덜;;;;; 그러고 보니 가격이 37만원으로 다른 물건들보다 거의 5~8만원 가량 싸긴했는데,
다른 물건들이 대부분 미개봉상품인데 비해 그 매물은 개봉상품이고 1달좀 넘었기에 급처인가하고 별 신경안썼는데... 역시 중고딩나라같은데서는 싸고 멀리서 파는건 사는게 아닌것 같다.
무튼 각설하고 이렇게 사온 오큘러스 퀘스트2는 바로 동작할 수는 없었고 그 날이 마침 목요일로 ATDD 새로운 주차 세미나 날이기에 10시 언저리까지 얌전히 세미나 참가하고 끝나마자 세팅후 구동을 해보았다.
사실 크게 기대를 했던 것은 아니였는데 정말 생각 이상으로 기술이 많이 발전했다는점을 느꼈다. 이런 점을 다른사람들과 공유하지 못한다는게 안타깝다. 정말 가상현실세계라는 체감과 다양한 경험을 한 것 같다.
게임도 재밌긴했지만 버추얼 데스크탑으로 우주 한가운데서 내 데스크탑을 무선으로 연결해서 실제 PC를 하는데 극장 스크린 크기만한 모니터로 컴퓨터를 하는 느낌은 신선했고, 키보드만 vr용으로 나와서 타자치는데 문제가 없다면 이 기기를 사용해서 가상현실에서 코딩을 해보고싶다는 열망이 생겼다.
그리고 넷플릭스 어플도 있어서 그걸로 요즘 한창 보는 빈센조 드라마도 봤는데 정말 산장에서 소파에 앉아서 큰 화면으로 보는 느낌이라 신기했다.
강력하게 다른사람들에게도 권해주고싶다. 츄라이 츄라이~
그리고 취미로나마 VR 기술에 대해 공부하고 프로그램을 출시해보고자하는 작은 꿈이 생겼다.
식사
신혼집에서 혼자 밥을 해먹어야하는 상황인데 집앞에 대형 롯데마트가 있어서 잘해먹고 사는 것 같다.
9시언저리에 마트를가면 25~30%이상 할인하는 제품들이 많은데, 이 스테이크도 3덩이에 8천원하는걸 사와서 먹은 것인데, 맛이 참 좋다.
근데, 집들이를 너무 많이하면서 하도 고기를 먹었더니 조금 물린것 같다는 생각이 들지만, 그래도 아직은 맛있게 잘 먹고있는다. 물론 뒷처리가 쉽지는 않다 기름때를 청소포로 열심히 문질문질...
하지만, 슬슬 식단관리를 위해 도시락을 사야하나 싶기도 하다.