GROWTH LOG STORIES

함께 만든 성장을,
기록으로 연결합니다.

AI와 함께 배우고, 직접 만들고, 서로 나눈
그로스로그 멤버들의 생생한 성장 기록을 만나보세요.

성장 기록
5기+이어진 커뮤니티
HEALTHIO
PROJECT

사진 한 장으로 시작하는 건강 루틴, Health IO

대표 이야기 읽기

ALL STORIES

성장의 순간을
한곳에서 만나보세요.

프로젝트의 시작부터 모임에서 나눈 인사이트까지,
그로스로그가 함께 만든 기록을 모았습니다.
개의 성장 기록

성장일지

[GL 4기 8회차 ①] 본론이 아닌데 없으면 안 되는 것 — 한 해의 회고, AOP, 시그널링 서버 🌱

GROWTH LOG🌱 2026. 8. 11. 00:12

안녕하세요! 그로스로그 입니다!😊 🌱

미디어를 안 나르는 서버, 비즈니스 로직이 아닌 코드, 개발이 아닌 일. 없으면 전부 무너지는 것들입니다.

성장일지는 멤버들이 2주에 한 번씩 자신의 성장을 기록하는 활동이에요. 매 회차 운영진이 모든 글을 읽고, 그중 특히 마음에 남은 글을 함께 골라 큐레이션합니다.

8회차에는 연말 회고 한 편기술 글 두 편이 올라왔습니다. 세 편 다 "가운데에 있는 것"에 대한 이야기였어요.

 

01

🐍 [회고] 다이나믹했던 2025년을 떠나보내며

정민아 님

정민아 님 원문에 실린 이미지
이런 내용이에요
한 해를 분기별로 나눠 쓴 회고입니다. 1·2월 / 3~6월 / 7~9월 / 10~12월로 끊어서 무슨 일이 있었는지 순서대로 적어요.
회사 이야기가 큽니다. 크런치 타임, 마이크로 매니징, 조직이 크게 흔들린 시기, 그리고 회복까지요.
학교 이야기도 함께 갑니다. 방송대 마지막 학년, 그리고 그로스로그 가입. 마지막은 숫자 하나로 마무리돼요. 회원 13만 명.

5회차 구조화된 데이터 글에 이어 두 번째로 소개하는 저자입니다. 그 글에서 SEO를 개선했다던 서비스가 이 회고에 나오는 그 서비스예요.

조금 더 들어가 보면

첫 문단부터 이 회고의 성격이 나옵니다.

"얼핏 떠오르는 사건들 중에 큰 사건들만 걸러도 굉장히 많아서, 사실상 이렇게 1년치 회고를 한번에 하기에는 너무 양이 많아서 반기에 한 번 아니, 분기에 한 번은 회고를 했어야 하는 게 아닌가 싶다."

1년치를 한 번에 쓰기가 버거웠다는 고백입니다. 그리고 실제로 이 글은 분기별 구조를 택해요. 회고 자체가 그 문제에 대한 대응인 셈입니다.

❄️ 1, 2월 — 버틴 시기

가장 힘들었던 구간을 먼저 씁니다.

"야근에 주말 근무까지 크런치 타임을 가진 게 2024년 10월부터였으니, 그때부터 중간중간 오버타임에 따른 휴가를 추가로 받긴 했어도 일정상 쉴 수 있는 시간이 별로 없었다."

4개월 넘게 이어진 크런치였습니다. 그리고 그 위에 한 겹이 더 얹혀요.

"완전히 새로운 기능을 빠듯한 일정에 맞추어 개발하느라 기술적인 챌린지까지 겪고 있던 상황에서 상사로부터 업무에 대한 압박 + 마이크로 매니징을 받으면서 스트레스가 완전히 극에 달해 폭발하기 직전이었다."

몸으로 나타난 증상까지 적혀 있습니다.

"2주 정도가 지났을 쯤에는 입맛까지 완전히 떨어져서 점심을 거르는 대신 근처 공원을 산책하면서 마인드 컨트롤을 하려고 정말 노력하기도 했었다."

입맛이 떨어졌다는 건 꽤 명확한 신호예요. 그리고 어떻게 넘겼는지가 나옵니다.

"당시에 해당 상사와 오랜 시간 같이 일을 해왔던 동료가 그런 식으로 한 명씩 돌아가면서 압박을 넣는 게 그분의 스타일이라고 했다. 요즘 그분 때문에 힘들어보이는데 괜찮냐, 나도 당해봐서 안다. 신기하게도 그런 공감의 말이 위로가 되었다."

해결책이 아니라 공감이 도움이 됐다고 적었어요. 상황은 하나도 안 바뀌었는데도요.

이게 왜 그러냐면 — 그 순간 가장 무서운 건 "나만 못 견디는 건가"라는 생각이기 때문입니다. "나도 당해봐서 안다"는 그 의심을 걷어내 주거든요. 문제가 나에게 있는 게 아니라는 걸 알려주니까요.

🗣️ 소통이 막히면 조직이 흔들린다

3~6월 구간이 이 회고에서 가장 중요한 대목이라고 봤습니다.

"이전부터 나 혼자 문제라고 느끼는 것인가 했던 부분은 알고 보니 역시나 다른 사람들도 동일하게 문제라고 느끼는 부분이었다. 문제가 되는 부분은 여러 가지가 있었지만, 핵심은 소통이 잘 되지 않는 것이었다."

"나 혼자 문제라고 느끼는 것인가"가 다시 나옵니다. 앞 구간의 그 감각이 조직 차원에서 반복돼요.

그리고 결과가 큽니다.

"결과적으로 말하면 조직적인 차원에서 꽤 큰 변화가 생겼고, 그 과정에서 아쉽게 떠나가는 동료도 있었다."
"소통 때문에 조직 전체가 흔들릴 뻔한 경험을 하면서 소통의 중요성을 다시 한 번 뼈저리게 느낀 때였던 것 같다."

기술 문제가 아니라 소통 문제로 조직이 흔들렸습니다.

7회차 김석현 님 회고에서 "강점도 커뮤니케이션, 단점도 커뮤니케이션"이라고 했던 말이 여기서 다른 각도로 확인됩니다. 개발 조직에서 가장 크게 터지는 게 대개 코드가 아니에요.

🕳️ 자리 하나가 비면 부하가 쏠린다

7~9월 구간에 아주 구체적인 사례가 나옵니다.

"기획자가 2명이 필요한 상황에서 1명이 공석인 상태로 업무를 하려고 하니 매끄럽게 업무가 진행되지 않았다. (…) 그러다 보니 외부에서 갑자기 들어온 요청은 십중팔구 우리 팀의 몫이 되었다. 기획자가 있는 팀에서는 기획이 완료된 개발을 진행하는 중이라서 외부 요청을 처리할 리소스가 없다는 이유에서였다."

한 자리가 비었더니 다른 종류의 일이 몰려온 상황입니다.

이게 왜 생기냐면 — 기획자가 있는 팀은 일정이 이미 잡혀 있어서 새 요청을 받을 수 없다고 말할 수 있습니다. 그런데 기획자가 없는 팀은 "지금 뭘 하고 있다"고 말해줄 사람이 없어요. 그래서 비어 있는 것처럼 보이고, 요청이 그쪽으로 갑니다.

중간에서 정리해주는 자리가 비면, 그 부담이 엉뚱한 데로 흘러갑니다.

그리고 면담을 했지만 안 바뀌었다고 정직하게 적어요.

"이에 대해 몇 차례 면담을 하기도 했지만, 당시의 상태를 유지하는 것만 해도 다들 벅찼기 때문에 그대로 3분기가 흘러갈 수밖에 없었다."

"현상 유지만으로도 벅찼다" — 조직이 무리 중일 때 개선안이 왜 안 굴러가는지를 정확히 설명하는 문장입니다.

결과도 균형 있게 씁니다. 목표 수치는 못 채웠지만 이전보다는 좋아졌다고요. "하지만 팀이 좀 더 잘 운영되었더라면 더 좋은 목표치를 달성할 수 있었겠지..."

🎓 방법을 바꿨더니 여유가 생겼다

10~12월 구간에는 실용적인 팁이 하나 있습니다.

"전에는 과목별로 매일매일 하나씩 돌아가면서 수강을 했다면, 이번에는 한 과목을 듣고 싶은 데까지 쭉 몰아 들은 다음에 다른 과목을 또 몰아서 듣는 방식으로 수강 방식을 바꾸었다. 그랬더니 정말 과제와 기말 시험 준비에 좀 더 여유가 생겼다."

돌아가며 조금씩에서 한 과목씩 몰아서로 바꾼 겁니다.

이게 왜 효과가 있냐면 — 과목을 바꿀 때마다 머릿속을 갈아끼우는 비용이 듭니다. 어제 어디까지 들었는지 떠올리고, 그 과목의 용어에 다시 익숙해져야 해요. 하루에 네 과목을 돌리면 그 비용을 네 번 냅니다.

6회차 신헌주 님은 "매일 강의 1개" 페이스를 권했는데, 이 글은 "한 과목씩 몰아서"를 권합니다. 총량이 아니라 배치를 바꾼 이야기라 서로 어긋나지 않아요.

그리고 예상 못 한 사고 이야기도 있습니다.

"국가정보자원관리원에서 화재가 발생하면서 공공데이터포탈 접속이 불가능하게 되어 공공데이터를 활용한 과제를 할 수가 없었다."

미리 해두고 있었는데도 막힌 상황이에요. "미리 중간 과제를 조금씩 여유롭게 해나가던 나도 마지막 분석 문제만 남겨둔 상황에서 갑자기 과제 진행을 할 수 없게 되어 당황했었는데". 그래도 여유를 두고 시작했기에 다시 할 시간이 있었죠.

🏘️ 동아리에 들어간 이야기

그로스로그 가입 대목이 반가웠습니다.

"방송대 컴퓨터과학과 내에는 이미 역사가 오래된 스터디 동아리가 2개 존재했는데, 나는 공부할 때는 혼자서 공부하는 것을 더 선호하기 때문에 딱히 관심을 두지 않고 있었다."

혼자 공부하는 걸 선호한다고 먼저 밝힙니다. 그런데도 가입한 이유가 이어져요.

"일단 겉에서 보기에 단순히 학내 동아리라기에는 우리가 잘 아는 유명한 IT연합동아리 같은 느낌에 좀 더 가까워서 더 흥미가 가기도 했던 동아리였다."

그리고 망설인 이유도 정직합니다. 업무가 빡세서 학업 병행도 힘든데 동아리까지는 무리라고 생각했다고요.

"하지만 이제 4학년이기도 하고, 올해가 아니면 안 되었기 때문에 용기를 내서 가입까지 하고야 말았던 것. 지금까지도 후회라곤 정말 1도 없는 결정이었다."

"올해가 아니면 안 되었기 때문에" — 미룰 수 없는 시점이 왔을 때 움직인 겁니다.

📊 13만 명

마무리에 숫자가 하나 나옵니다.

"벌써 다음달이면 방송대 졸업을 앞두고 있고, 밑바닥부터 시작한 서비스는 이제 13만 명이 넘는 회원을 가지고 있다."

"맨땅에 헤딩을 하듯이 현장 조사부터 시작해서 서비스를 만들기 시작했다"던 그 서비스입니다. 5회차 글에서 구조화된 데이터를 붙여 유입을 70% 늘렸다던 그 서비스이기도 하고요.

앞 회차의 기술 글이 이 회고 안에서 제자리를 찾는 순간이었습니다. 그 SEO 작업이 어떤 한 해의 어느 지점에 있었는지가 여기서 보여요.

그리고 마지막 문단이 좋았습니다.

"아직 방송대 졸업 이후에는 또 무엇을 할지 정하지 못했다. 하지만 항상 무언가를 정해 두고 살 필요가 있을까? 당분간은 회사 업무도 다시 바빠질 예정이고, 무엇보다 퇴근 후에 공부하지 않고 놀아도 되는 삶을 조금 더 즐기고 싶다."

"다음 목표"로 끝나지 않는 회고입니다. 4년을 쉬지 않고 달린 사람이 쉬겠다고 적었어요. 그것도 계획이라고요.

큐레이터 노트

힘들었던 걸 힘들었다고 쓴 회고라서 뽑았습니다.

연말 회고는 정리하면서 예뻐지기 쉽습니다. 힘들었지만 성장했다, 갈등이 있었지만 배웠다 하는 식으로요. 이 글에도 성장 이야기가 있는데, 그 앞에 상태가 그대로 남아 있어요. 입맛이 떨어졌고, 퇴사 말이 나올 뻔했고, 동료가 떠났고, 목표는 못 채웠습니다.

분기로 끊은 구조도 이 글에 잘 맞았습니다. 한 해를 하나의 이야기로 만들면 결론이 필요해지는데, 분기로 나누면 각 구간이 그 자체로 남거든요. 좋았던 분기와 나빴던 분기가 따로 있는 채로 끝납니다.

소통 문제를 정면으로 다룬 것이 특히 값졌어요. 기술 회고는 많은데 조직이 흔들린 이야기는 쓰기가 어렵습니다. 이 글은 사람을 특정하지 않으면서도 무엇이 문제였는지는 분명히 적었어요.

문체도 이 저자답습니다. "몽골 여행 또 가고 싶다^_T", "갓재화, 당신은 빛과 소금이에요", "이런 시간도 필요하다굿!" 무거운 내용 사이에 이런 문장들이 끼어 있어서 읽는 사람이 같이 무거워지지 않아요.

원문 읽으러 가기  ↗velog.io/@minami/회고-다이나믹했던-2025년을-떠나보내며
 

02

🪝 관점 지향 프로그래밍(AOP)

송영록 님

이런 내용이에요
횡단 관심사(Cross-cutting Concerns)를 다루는 패러다임인 AOP를 정리한 글입니다. 로깅, 트랜잭션, 권한 체크처럼 모든 모듈을 관통하는 기능들을 한 곳에 모으는 방법이에요.
왜 OOP만으로는 안 되는지부터 시작합니다. 그다음 다섯 가지 핵심 용어(Aspect · Join Point · Pointcut · Advice · Weaving)를 짚어요.
그리고 언어별 구현 스펙트럼을 비교합니다. 미들웨어·필터 / 데코레이터·어노테이션 / 프록시 기반. 마지막은 "Spring Bean이 곧 AOP의 관문"으로 마무리돼요.

부제가 정확합니다. "반복되는 부가 기능(로깅, 트랜잭션, 권한 등)을 핵심 로직과 분리하는 프로그래밍 패러다임."

조금 더 들어가 보면

문제 정의가 명확합니다.

"서비스를 만들다 보면 곳곳에서 반복되는 작업이 있다. 로그인 확인, 로그 남기기, 트랜잭션 처리, 캐시 갱신, 권한 체크, 성능 측정 같은 일들이 대표적이다. 이런 기능들은 사용자 요청을 처리하는 거의 모든 모듈을 관통한다."

"관통한다"가 핵심 단어입니다. 이 기능들은 어느 한 곳에 속하지 않아요. 주문 기능에도 있고 회원 기능에도 있고 결제 기능에도 있습니다.

↕️ 세로로는 안 나눠지는 것

OOP로 왜 안 되는지를 설명한 부분이 이 글의 핵심입니다.

"OOP에서는 Logger 클래스를 만들고 각 모듈이 이를 호출하도록 설계할 수 있다. 하지만 결국 모든 메서드가 '로그 찍기 → 원래 로직' 순서로 코드를 써야 한다."

Logger 클래스를 만드는 것만으로는 부족합니다. 중복된 구현은 없앴지만 호출은 여전히 모든 곳에 있어요.

그래서 이런 일이 생깁니다.

"정책이 바뀌면? 예를 들어 로그 포맷을 JSON으로 바꿔야 한다면? 프로젝트 전역을 검색해 수십, 수백 개 파일을 수정해야 한다."

그리고 이 비유가 명쾌합니다.

"이런 수평적 관심사는 객체의 세로 구조(상속·조합)로는 깔끔히 떨어지지 않는다. AOP는 이 지점을 정면으로 겨냥한다. '가로로 퍼져 있는 관심사'를 세로 계층과 분리해 코드의 균형을 잡는다."

세로와 가로라는 그림이 좋습니다.

객체지향은 세로로 나눕니다. 주문은 주문끼리, 회원은 회원끼리요. 그런데 로깅은 가로로 퍼져 있어요. 주문에도 회원에도 결제에도 있습니다. 세로 칸막이를 아무리 잘 세워도 가로줄은 안 정리됩니다.

🧩 다섯 개의 용어

AOP를 배울 때 가장 먼저 만나는 벽이 용어입니다. 이 글은 다섯 개를 짧게 정리했어요.

용어
Aspect 공통 기능을 묶어둔 모듈
Join Point 끼어들 수 있는 지점 (메서드 실행 전·후, 예외 등)
Pointcut 그중 어디에 적용할지 고르는 규칙
Advice 언제 무엇을 할지 (Before / After / Around)
Weaving 비즈니스 코드와 Aspect를 합치는 과정

Join Point와 Pointcut의 관계가 헷갈리기 쉬운데, 글의 설명이 정확합니다. Join Point는 끼어들 수 있는 모든 후보이고, Pointcut은 그중 실제로 고른 것이에요. **"service.* 패키지의 모든 public 메서드"** 같은 식으로요.

후보 전체와 선택 기준의 관계라고 보면 됩니다.

🌐 AOP는 스프링 것이 아니다

이 글에서 특히 좋았던 부분이 언어별 비교입니다.

"AOP는 특정 언어의 전유물이 아니다. 구현 방법은 기술 스택에 따라 달라진다."

세 가지로 나눠요.

1. 미들웨어·필터

— Django/Flask 미들웨어, Express 미들웨어, Servlet Filter. "전역 정책을 한 번에 적용"할 수 있습니다.

2. 데코레이터·어노테이션

— Python의 @login_required, Java의 @Transactional. "정밀 타겟팅"이 가능해요.

3. 프록시 기반

— Spring AOP. 객체를 감싸는 대리 객체를 만들어 호출 전후에 끼어듭니다.

이 분류가 유용한 건 적용 범위가 다르기 때문입니다.

미들웨어는 HTTP 요청 단위로 걸립니다. 그래서 요청이 아닌 경로(스케줄러, 큐 워커)로 들어오는 건 못 잡아요. 반면 프록시 기반은 메서드 호출 단위라 어디서 불리든 걸립니다.

그리고 @Transactional이 미들웨어가 아니라 프록시인 이유도 여기서 나옵니다. 트랜잭션은 HTTP 요청 하나가 아니라 메서드 하나를 감싸야 하니까요.

🫙 "Spring Bean이 곧 AOP의 관문"

글의 후반부가 이 한 문장으로 수렴합니다. 그리고 이게 실무에서 가장 많이 걸려 넘어지는 지점이에요.

"스프링이 빈을 만들 때, AOP 설정이 걸린 클래스라면 원본 객체 대신 프록시 객체를 컨테이너에 넣는다."
"결과적으로 우리가 주입받아 사용하는 것은 '진짜 구현체'가 아니라 '프록시로 감싼 Bean'이다."

내가 쓰는 객체가 사실은 대리인이라는 겁니다.

그래서 이 경고가 따라와요.

"만약 개발자가 new 키워드로 직접 객체를 생성하면 이 프록시 과정을 거치지 않는다. 따라서 AOP는 반드시 스프링 컨테이너가 관리하는 Bean을 대상으로만 동작한다는 점을 기억해야 한다."

new로 만들면 AOP가 안 걸립니다. 그런데 컴파일도 되고 실행도 돼요. 트랜잭션만 조용히 빠집니다.

여기서 한 가지 덧붙이면, 같은 이유로 생기는 더 흔한 함정이 있습니다. 같은 클래스 안에서 자기 메서드를 부르면 AOP가 안 걸립니다.

@Service
public class OrderService {

    public void createOrder() {
        saveOrder();          // ← 여기서 @Transactional이 안 걸린다
    }

    @Transactional
    public void saveOrder() { ... }
}

이유는 프록시 구조를 알면 바로 보입니다. 외부에서 createOrder()를 부르면 프록시를 거쳐 들어와요. 그런데 그 안에서 saveOrder()를 부르는 건 이미 진짜 객체 안입니다. 프록시를 다시 안 거칩니다.

에러가 안 나고 트랜잭션만 없어지는 종류의 문제예요. 이 글이 짚은 "프록시로 감싼 Bean"이라는 사실 하나로 이 현상이 다 설명됩니다.

🎯 마무리의 관점

결론이 좋았습니다.

"AOP는 '프레임워크 기능'이라기보다 설계 철학이다. 핵심은 비즈니스 로직과 공통 부가 기능을 분리해 서로의 변화를 독립적으로 관리하는 것."

"서로의 변화를 독립적으로 관리"가 목적을 정확히 짚습니다. 로그 정책이 바뀌어도 주문 로직은 안 건드리고, 주문 로직이 바뀌어도 로그 정책은 그대로예요.

그리고 스택이 달라도 아이디어는 같다고 정리합니다. "Go나 Rust에서도 인터셉터와 래퍼 패턴으로 구현할 수 있다."

큐레이터 노트

한 프레임워크를 넘어서 정리한 글이라 뽑았습니다.

AOP 글은 대부분 스프링 안에서만 설명합니다. @Aspect 붙이고 @Around 쓰는 법을 알려주죠. 이 글은 먼저 문제를 정의하고, 그다음 여러 언어의 해법을 나란히 놓습니다. 그래서 파이썬 데코레이터를 써본 사람이 스프링 AOP를 이해하는 다리가 돼요.

"OOP로는 왜 안 되는가"를 먼저 다룬 구성도 좋았습니다. 새 도구를 배울 때 가장 궁금한 게 기존 방법으로 안 되는 이유거든요. Logger 클래스를 만들어도 호출은 여전히 흩어져 있다는 지적이 그 자리를 정확히 채웁니다.

"Bean이 곧 AOP의 관문"이라는 결론도 실용적이에요. 이 한 줄이 new로 만들면 왜 안 되는지, 왜 컨테이너가 필요한지를 다 설명합니다. 규칙을 여러 개 외우는 대신 원리 하나를 갖게 돼요.

그리고 참고 링크를 밝힌 것관련 포스팅(Spring Bean)을 연결해둔 것도 눈에 띄었습니다. 시리즈로 쌓아가고 있는 블로그예요.

원문 읽으러 가기  ↗youngroknroll.github.io/posts/AOP/
 

03

📡 [성장일지] 시그널링 서버 만들기

오한솔 님

이런 내용이에요
WebRTC 연결을 성립시키는 시그널링 서버를 스프링 웹소켓으로 만든 기록입니다.
앞부분이 사전 지식 정리예요. 시그널링 서버 / 웹소켓 / WebRTC / SDP / ICE가 각각 무엇인지 짧게 짚습니다.
뒷부분은 구현 코드 세 덩어리입니다. 연결됐을 때, 메시지를 받았을 때, 연결이 끊겼을 때. 세 메서드로 시그널링 서버가 완성돼요.

짧은 글입니다. 그런데 구조가 아주 명확해요. 알아야 할 것 다섯 개를 먼저 정리하고, 코드 세 개로 끝냅니다.

조금 더 들어가 보면

먼저 시그널링 서버가 무엇인지부터요.

"WebRTC 연결을 위해 피어 간에 SDP와 ICE 같은 연결 협상 정보를 전달하는 중개 서버다. 미디어나 데이터는 처리하지 않고, 연결을 성립시키는 데 필요한 메타데이터만 전달한다."

"미디어나 데이터는 처리하지 않고"가 이 서버의 성격을 다 말해줍니다.

화상 통화를 만든다고 하면 보통 서버가 영상을 받아서 상대에게 보내주는 그림을 떠올립니다. 그런데 WebRTC는 그렇게 안 해요. 영상은 두 사람 사이에 직접(P2P) 흐릅니다.

그럼 서버는 뭘 하냐면 — 서로를 소개해줍니다. 그것만 합니다.

🤝 왜 소개가 필요한가

이 글이 정리한 두 개념이 그 답입니다.

"SDP란? 두 피어가 어떤 방식으로 통신할지 정의한 연결 설정 정보다. WebRTC에서는 offer와 answer 형태로 교환된다."

어떤 코덱을 쓸지, 영상인지 음성인지, 해상도는 어떤지 같은 정보예요. 두 브라우저가 서로 다른 기기일 수 있으니 먼저 합을 맞춰야 합니다.

"ICE란? 피어 간에 실제로 통신 가능한 네트워크 경로를 찾기 위한 메커니즘이다. ICE candidate를 교환해 NAT 환경에서도 연결 가능한 경로를 탐색한다."

NAT가 핵심입니다. 우리 대부분은 공유기 뒤에 있어요. 그래서 바깥에서 내 컴퓨터로 직접 연결할 주소가 없습니다. 브라우저는 자기가 쓸 수 있는 주소 후보를 여러 개 모으고(ICE candidate), 상대와 교환해서 어느 경로가 통하는지 하나씩 시험해봅니다.

그런데 여기에 닭과 달걀 문제가 있어요.

두 사람이 직접 연결하려면 서로의 주소를 알아야 합니다. 그런데 주소를 알려주려면 이미 연결이 돼 있어야 하죠.

시그널링 서버가 이 고리를 끊습니다. 둘 다 서버에는 접속할 수 있으니까요. 서버를 통해 주소를 주고받고, 그다음부터는 서버 없이 직접 통신합니다.

그래서 소개가 끝나면 할 일이 없어지는 서버예요. 그런데 없으면 시작 자체가 안 됩니다.

🔀 웹소켓과 WebRTC를 나눠 설명한 것

글이 짚은 이 구분이 아주 유용합니다.

"WebSocket도 프로토콜, WebRTC도 프로토콜인데 각각 어디서 사용되는가?"
"WebSocket은 클라이언트 ↔ 서버 간 메시지 전달에 사용된다. WebRTC는 클라이언트 ↔ 클라이언트(P2P) 간 실시간 통신에 사용된다."

한 시스템 안에 두 프로토콜이 다른 층에서 동시에 도는 구조입니다.

[브라우저 A] ←웹소켓→ [시그널링 서버] ←웹소켓→ [브라우저 B]
       ↑                                              ↑
       └───────────── WebRTC (P2P) ──────────────────┘

위층은 서버를 거치고, 아래층은 직접 갑니다. 이 그림이 잡히면 시그널링이 왜 필요한지가 한 번에 이해돼요.

📝 세 개의 메서드

구현이 놀랄 만큼 짧습니다. 스프링의 TextWebSocketHandler를 상속해 세 메서드만 채우면 끝이에요.

1. 연결됐을 때

 

@Override
public void afterConnectionEstablished(WebSocketSession session) {
    log.info("[WS] connected sessionId={}", session.getId());
}

2. 메시지를 받았을 때

— 여기가 전부입니다.

@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    MessageDto dto = objectMapper.readValue(message.getPayload(), MessageDto.class);

    // 1. 클라이언트 등록
    if ("register".equals(dto.getType())) {
        clientSessions.put(dto.getSender(), session);
        sessionIdToClientId.put(session.getId(), dto.getSender());
        return;
    }

    // 2. 시그널링 메시지 중계 (offer / answer / ice)
    String sender = sessionIdToClientId.get(session.getId());
    dto.setSender(sender);

    WebSocketSession receiverSession = clientSessions.get(dto.getReceiver());
    if (receiverSession != null && receiverSession.isOpen()) {
        receiverSession.sendMessage(
                new TextMessage(objectMapper.writeValueAsString(dto))
        );
    }
}

3. 연결이 끊겼을 때

 

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
    String clientId = sessionIdToClientId.remove(session.getId());
    if (clientId != null) {
        clientSessions.remove(clientId);
    }
}

🗺️ 지도 두 개가 왜 필요한가

코드에서 눈여겨볼 부분이 맵이 둘이라는 점입니다.

· clientSessions클라이언트 ID → 세션

· sessionIdToClientId세션 ID → 클라이언트 ID

같은 관계를 양방향으로 저장해뒀어요. 왜 그런지는 각 메서드가 무엇을 갖고 무엇을 찾아야 하는지를 보면 알 수 있습니다.

메시지를 보낼 때 — 받는 사람의 ID는 아는데 그 사람의 세션이 필요합니다. → clientSessions

보낸 사람을 확인할 때 — 손에 있는 건 세션뿐인데 이 사람이 누구인지 알아야 합니다. → sessionIdToClientId

연결이 끊겼을 때 — 끊긴 세션만 주어지는데 clientSessions에서 지우려면 ID를 알아야 합니다. → sessionIdToClientId

세 번째 경우 때문에 두 번째 맵이 반드시 필요합니다. 하나만 있으면 정리할 때 전체를 훑어야 하거든요.

🔒 보낸 사람을 서버가 덮어쓰는 것

작지만 중요한 줄이 여기 있습니다.

String sender = sessionIdToClientId.get(session.getId());
dto.setSender(sender);

클라이언트가 보낸 sender 값을 그대로 안 씁니다. 서버가 세션으로 확인한 값으로 덮어씁니다.

이게 왜 중요하냐면 — 클라이언트가 보낸 데이터는 얼마든지 조작할 수 있기 때문입니다. sender를 남의 ID로 바꿔 보내면 그 사람인 척 메시지를 보낼 수 있어요.

그런데 세션은 조작할 수 없습니다. 웹소켓 연결 자체가 신원이니까요. 등록할 때 맺어둔 그 관계를 서버가 기억하고 있고, 이후 모든 메시지의 발신자를 그 기억으로 덮어씁니다.

"클라이언트가 보낸 값을 믿지 않는다"는 원칙이 두 줄로 구현된 자리예요.

🧹 정리 코드가 있는 것

afterConnectionClosed가 있다는 것도 짚어둘 만합니다.

연결이 끊겼는데 맵에서 안 지우면 죽은 세션이 계속 쌓입니다. 그리고 그 ID로 메시지를 보내려 하면 실패하죠. 그래서 isOpen() 검사도 함께 들어가 있고요.

3회차 오세명 님의 웹소켓 글에서 다뤘던 연결 종료와 뒷정리 이야기가 여기서 서버 쪽 모습으로 나타납니다. 클라이언트도 서버도 각자 치울 게 있어요.

큐레이터 노트

용어를 먼저 정리하고 들어간 글이라 뽑았습니다.

WebRTC는 약어가 유난히 많은 영역입니다. SDP, ICE, STUN, TURN, NAT, 시그널링. 자료를 읽다 보면 모르는 단어가 모르는 단어로 설명되는 상황에 빠지기 쉬워요.

이 글은 사전 지식을 다섯 개로 추려 각각 두세 줄로 정리하고 시작합니다. 특히 "WebSocket도 프로토콜, WebRTC도 프로토콜인데 각각 어디서 사용되는가?"라는 질문을 스스로 만들어 답한 게 좋았어요. 헷갈렸던 지점을 그대로 노출한 겁니다.

코드를 세 덩어리로 나눈 것도 명확했습니다. 연결 / 메시지 / 종료. 웹소켓 핸들러의 생애주기가 그대로 목차가 됐어요. 이 구조는 시그널링 서버가 아닌 어떤 웹소켓 서버를 만들어도 똑같이 쓰입니다.

참고 링크를 네 개 남긴 것도 눈에 띄었습니다. Baeldung 문서와 GitHub 저장소 셋. 여러 구현을 보고 자기 것을 만든 흔적이에요.

무엇보다 직접 만들어봤다는 게 이 기록의 값입니다. WebRTC는 개념 설명 자료가 아주 많은데, 실제로 서버를 띄워보면 아는 게 완전히 달라집니다. 이 글은 그 지점에 있어요.

원문 읽으러 가기  ↗cypress-sword-cd9.notion.site/2ea3d3a9f26180de832ae53009e2…

💡 세 편을 겹쳐 읽으며

한 해 회고, 스프링 AOP, 그리고 WebRTC 시그널링 서버. 세 편이 다 다른 이야기 같습니다.

그런데 셋 다 본론이 아닌데 없으면 안 되는 것을 다루고 있어요.

시그널링 서버는 미디어를 한 바이트도 안 나릅니다. 영상도 음성도 두 사람 사이에 직접 흘러요. 그런데 이 서버가 없으면 둘이 서로를 찾지 못합니다. 하는 일은 소개뿐인데, 소개가 없으면 아무것도 시작되지 않아요.

AOP가 다루는 것도 비즈니스 로직이 아닙니다. 로깅, 트랜잭션, 권한 체크는 서비스가 파는 기능이 아니에요. 그런데 이게 빠지면 장애가 나도 원인을 모르고, 실패해도 데이터가 안 되돌아가고, 아무나 들어옵니다.

정민아 님의 회고에서 조직이 흔들린 원인도 코드가 아니었습니다. 소통이었어요. 그리고 기획자 한 자리가 비었더니 외부 요청이 전부 한 팀으로 몰렸습니다. 개발이 아닌 자리가 비니까 개발하는 사람들이 무너진 겁니다.

세 경우 다 성격이 같아요.

· 평소엔 잘 안 보입니다. 잘 돌아갈 때는 있는지도 모르죠

· 성과로 잡히지 않습니다. 로그를 잘 남긴 게 기능 하나로 안 세어져요

· 없어질 때만 티가 납니다. 그것도 아주 크게요

그리고 세 글 다 그 자리를 따로 만들어야 한다고 말합니다.

AOP는 흩어져 있던 부가 기능을 한 모듈로 모읍니다. 시그널링 서버는 연결 협상만 하는 전담 서버예요. 정민아 님의 팀도 결국 조직 구성을 바꾸고 새 동료가 합류하면서 분위기가 회복됐습니다.

모아두지 않으면 흩어지고, 흩어지면 아무도 책임지지 않습니다. 각 메서드에 로그를 흩뿌리면 포맷 하나 바꾸는 데 수백 파일을 고쳐야 하고, 중간에서 조율하는 자리가 없으면 요청이 만만한 쪽으로 흘러가요.

마지막으로 하나. 세 글 다 뒷정리 이야기가 있습니다. 시그널링 서버는 afterConnectionClosed에서 맵을 비우고, AOP는 @Around에서 앞뒤를 다 챙기고, 회고는 "퇴근 후에 공부하지 않고 놀아도 되는 삶"을 다음 계획으로 적었어요.

시작한 것을 정리하는 것까지가 한 세트라는 이야기가, 세 층위에서 각각 나옵니다.

이번 회차에서 가져갈 것

1. "나만 힘든가"라는 의심이 가장 무겁다.

상황이 안 바뀌어도 "나도 당해봐서 안다" 한마디가 위로가 됩니다. 옆 사람이 힘들어 보이면 그 말을 먼저 해보세요.

2. 조직은 대개 기술이 아니라 소통으로 흔들린다.

그리고 "나 혼자 문제라고 느끼는 건가" 싶은 것은 대개 다들 느끼고 있습니다.

3. 중간 자리가 비면 부하가 엉뚱한 데로 쏠린다.

기획자가 없는 팀은 "지금 뭘 하고 있다"고 말해줄 사람이 없어서 비어 보이고, 요청이 그쪽으로 갑니다.

4. 공부는 총량보다 배치.

여러 과목을 매일 돌리면 머릿속을 갈아끼우는 비용을 매번 냅니다. 한 과목씩 몰아서 듣는 게 나을 수 있어요.

5. 세로 구조로는 가로줄이 안 정리된다.

Logger 클래스를 만들어도 호출은 여전히 모든 곳에 있습니다. 로그 포맷 하나 바꾸려면 수백 파일을 고쳐야 해요. 이게 AOP가 필요해지는 지점입니다.

6. Join Point는 후보 전체, Pointcut은 그중 고른 것.

Advice는 언제 무엇을, Weaving은 합치는 과정입니다. 이 관계만 잡으면 나머지는 따라옵니다.

7. 적용 범위가 다르다.

미들웨어는 HTTP 요청 단위라 스케줄러나 큐 워커는 못 잡습니다. 프록시 기반은 메서드 호출 단위예요. @Transactional이 프록시인 이유가 여기 있습니다.

8. new로 만들면 AOP가 안 걸린다.

그리고 같은 클래스 안에서 자기 메서드를 부를 때도 안 걸려요. 프록시를 다시 안 거치니까요. 에러 없이 트랜잭션만 사라집니다.

9. WebRTC의 미디어는 서버를 안 거친다.

시그널링 서버는 SDP와 ICE만 중계해요. 소개가 끝나면 할 일이 없어지는데, 없으면 시작 자체가 안 됩니다.

10. 클라이언트가 보낸 발신자 정보를 믿지 말 것.

dto.setSender(세션에서_확인한_ID)처럼 서버가 덮어써야 합니다. 안 그러면 남인 척 메시지를 보낼 수 있어요.

11. 세션 맵은 양방향으로.

연결이 끊겼을 때 세션만 주어지는데 ID로 저장한 맵을 지워야 합니다. 역방향 맵이 없으면 전체를 훑게 돼요.


좋은 글 남겨주신 정민아 님, 송영록 님, 오한솔 님께 감사드립니다! 😊

다음 큐레이션으로 다시 찾아오겠습니다! 🌱

- 에디터 · 성장일지 큐레이터 -


※ 본 큐레이션은 각 저자가 공개한 글을 소개하는 것이며, 모든 원문의 저작권은 저자에게 있습니다. 저자 본인의 요청이 있을 경우 즉시 수정 또는 삭제합니다.

WRITE YOUR GROWTH

오늘의 배움을
내일의 성장으로.

기록은 나를 돌아보게 하고, 공유는 우리를 연결합니다.
당신의 다음 성장 이야기도 그로스로그에서 시작해보세요.

커뮤니티 둘러보기