안녕하세요! 그로스로그 입니다!😊 🌱
화면은 그리기만 하고 로직은 계산만 하게 나누는 패턴, 서비스는 예외만 던지고 응답은 한 곳에서 만드는 구조, 그리고 두 벡터로 세 번째 방향을 얻는 계산. 4기 9회차 세 편입니다.
4기 9회차는 2026년 1월 말입니다. 세 편 모두 역할을 나누고 경계를 긋는 이야기를 합니다.
01
🎭 [Architecture Pattern] MVVM
박현구 님
클라이언트 앱에서 표준처럼 쓰이는 MVVM 패턴을 정리하고, Flutter로 카운터 앱을 만들어본 기록입니다.
핵심을 한 줄로 요약해뒀어요. "화면(View)은 그림만 그리고, 로직(ViewModel)은 계산만 한다."
세 층의 책임을 나열하는데, "모른다"는 표현이 반복됩니다. Model은 나머지를 모르고, ViewModel은 View를 모릅니다.
그리고 순수 데이터 클래스부터 차례로 코드를 붙여갑니다.
6·7·8회차에 이어 만나는 분입니다. 데이터베이스, 클러스터, 리액트 훅을 지나 이번엔 구조 이야기예요.
이 글에서 눈에 띄는 건 "모른다" 를 반복해 적은 것입니다. 무엇을 하는지보다 무엇을 몰라야 하는지로 각 층을 정의했어요.
조금 더 들어가 보면
🙈 "모른다"가 설계의 핵심입니다
저자가 정리한 목록을 다시 보면 이렇습니다.
· Model — ViewModel이나 View를 전혀 모른다. 데이터에만 집중한다
· ViewModel — View를 전혀 모른다. Model에서 가져와 View에 맞는 형태로 가공한다
· View — ViewModel의 데이터를 그리고, 입력을 받아 ViewModel의 함수를 부른다
의존이 한 방향으로만 흐릅니다. View → ViewModel → Model 순서로요. 반대 방향 화살표가 없습니다.
이게 왜 중요하냐면, 화살표가 없는 쪽은 따로 떼어낼 수 있기 때문입니다.
ViewModel이 View를 모르면, 화면 없이도 ViewModel을 테스트할 수 있습니다. 버튼을 누른 것처럼 함수를 부르고 결과를 확인하면 되니까요. 화면을 띄우고 클릭해야만 확인할 수 있는 구조와 완전히 다릅니다.
그리고 화면을 통째로 갈아도 ViewModel은 그대로 씁니다. 모바일 화면과 태블릿 화면이 달라도 계산 로직은 하나면 되고요.
2회차에서 이야기한 서브프로그램의 이점이 여기서 층 단위로 반복됩니다. 이름과 입출력만 맞으면 안을 갈아도 부르는 쪽은 모른다는 것이요.
📢 ViewModel은 View를 어떻게 부르는가
여기서 자연스러운 질문이 나옵니다. ViewModel이 View를 모르는데, 데이터가 바뀐 걸 어떻게 알리는가요.
저자의 목록에 답이 있습니다. "데이터가 바뀌면 View에 Notify한다."
직접 부르는 게 아니라 알림을 뿌리는 것입니다. ViewModel은 "나 바뀌었다"고 방송하고, 듣고 있던 쪽이 알아서 다시 그립니다. 누가 듣고 있는지는 ViewModel이 몰라요.
이 구조의 이름이 여럿입니다. 관찰자 패턴이라고도 하고, 구독이라고도 하고, 프레임워크마다 다른 이름을 붙이기도 하는데 하는 일은 같습니다. 알리는 쪽과 듣는 쪽을 떼어놓는 것이요.
Flutter에서는 상태를 들고 있는 객체가 변경을 알리고, 그걸 듣고 있는 위젯이 다시 그려지는 방식으로 구현됩니다. 저자가 카운터 앱으로 실습한 게 그 최소 형태고요.
⚖️ 세 층 사이에서 자주 헷갈리는 자리
패턴을 배우고 실제로 적용할 때 반드시 부딪히는 질문이 있습니다. 이 코드는 어느 층에 둬야 하는가.
기준을 몇 개 세워두면 편합니다.
| 이런 코드 | 어디에 | 이유 |
|---|---|---|
| 금액을 세 자리마다 쉼표 찍기 | ViewModel | 화면에 보여줄 형태로 가공하는 일입니다 |
| 할인율을 적용해 최종가 계산 | Model 또는 그 아래 | 화면과 무관한 규칙입니다 |
| 버튼을 눌렀을 때 무엇을 할지 | ViewModel | View는 "눌렸다"만 전달합니다 |
| 색깔·애니메이션 | View | 순수하게 보여주는 일입니다 |
헷갈릴 때 쓸 질문이 하나 있습니다. "화면이 바뀌면 이 코드도 바뀌어야 하나?" 그렇다면 View 쪽이고, 아니라면 아래쪽입니다.
🧰 Model 아래에 한 층 더 두는 경우
실제 앱을 만들다 보면 세 층으로 부족해지는 지점이 옵니다. 데이터를 어디서 가져오느냐가 끼어들 때요.
서버에서 받아올 수도 있고, 기기에 저장된 걸 읽을 수도 있고, 캐시에서 꺼낼 수도 있습니다. 이 로직이 ViewModel에 들어가면 금세 커져요. 화면 하나를 위한 계산인지 데이터를 가져오는 절차인지가 섞입니다.
그래서 보통 Model 쪽에 한 층을 더 둡니다. "어디서 가져오는지"를 감추는 층이요. ViewModel은 그냥 "목록 주세요"라고만 하고, 그게 서버에서 왔는지 캐시에서 왔는지는 모릅니다.
여기서도 "모른다"가 반복됩니다. 그리고 이 층이 생기면 이득이 하나 더 붙어요. 테스트할 때 가짜 데이터를 돌려주는 것으로 갈아 끼울 수 있습니다. 서버 없이 ViewModel을 검증할 수 있게 되죠.
저자가 정리한 세 층이 뼈대이고, 실제 프로젝트에서는 이렇게 아래쪽이 더 나뉩니다. 다만 방향은 그대로입니다. 위가 아래를 알고, 아래는 위를 모릅니다.
🔄 상태를 어디에 둘 것인가
또 하나 자주 걸리는 게 화면이 여러 개일 때 상태를 어디 두느냐입니다.
화면마다 ViewModel을 하나씩 두는 게 기본입니다. 그런데 두 화면이 같은 데이터를 봐야 하는 경우가 생겨요. 목록 화면에서 즐겨찾기를 눌렀는데 상세 화면에도 반영돼야 하는 식이요.
방법이 두 갈래입니다.
· 위로 올리기 — 두 화면을 감싸는 자리에 하나 두고 같이 씁니다. 간단한데, 올리다 보면 최상위에 다 모입니다
· 아래에서 공유하기 — 데이터를 다루는 층을 하나로 두고, 두 ViewModel이 같은 것을 바라보게 합니다. 어느 쪽에서 바꿔도 양쪽이 갱신됩니다
두 번째가 대체로 낫습니다. 화면 구조가 바뀌어도 안 흔들리거든요. 그리고 이건 앞에서 말한 데이터 층이 있어야 가능한 방식이기도 합니다.
"어디에 둘 것인가"가 이 패턴을 쓰면서 계속 하게 되는 질문이고, 그 답이 대개 "가장 아래에서 공유"로 수렴합니다.
그리고 ViewModel이 비대해지는 게 가장 흔한 실패입니다. 어디 둘지 애매한 걸 전부 여기 넣게 되거든요. 그러면 이름만 MVVM이고 실제로는 한 덩어리인 상태가 됩니다.
🧩 왜 하필 이 패턴이 표준이 되었나
저자가 "Flutter, Android, iOS 같은 클라이언트 앱 개발에서 가장 표준적으로 사용되는 패턴"이라고 적었는데, 이유를 붙이면 이렇습니다.
클라이언트 앱은 화면이 자주 바뀝니다. 디자인이 개편되고, 기기 크기가 늘어나고, 플랫폼별로 다르게 보여야 하고요. 그런데 그 밑의 규칙은 잘 안 바뀝니다. 할인 계산이나 로그인 절차 같은 것들이요.
자주 바뀌는 것과 안 바뀌는 것을 분리해두면, 바뀔 때 손대는 범위가 줄어듭니다. 이게 이 패턴이 오래 살아남은 이유예요.
그리고 이 원칙은 다른 자리에도 똑같이 적용됩니다. 3회차에서 본 Dockerfile 레이어 순서 — 자주 바뀌는 것을 뒤에 둔다 — 와 같은 발상이거든요. 변경 빈도로 경계를 긋는 것이요.
📝 원문에서는 이렇게 씁니다
가장 아래층부터 코드를 붙여갑니다.
class CountModel {
int count;
CountModel({required this.count});
}
아무것도 안 하는 클래스입니다. 그런데 이게 핵심이에요. 여기에 화면 관련 코드가 한 줄이라도 들어가는 순간 아래층이 위층을 알게 되고, 방향이 깨집니다.
그리고 요약 문장이 이 패턴의 전부입니다.
🧪 그래서 무엇이 테스트 가능해지는가
이 패턴의 실질적인 성과를 한 번 더 짚겠습니다. 확인할 수 있는 범위가 달라집니다.
한 덩어리로 짜면 로직을 확인하려면 앱을 띄우고 화면을 눌러야 합니다. 느리고, 자동화하기 어렵고, 실패했을 때 원인이 화면인지 로직인지 구분이 안 돼요.
나눠두면 ViewModel만 따로 부를 수 있습니다. 함수를 부르고 값을 확인하는 것으로 끝나요. 화면이 없어도 되고, 기기가 없어도 되고, 아주 빠릅니다.
그래서 이런 배치가 나옵니다.
| 무엇을 | 어떻게 | 얼마나 |
|---|---|---|
| 계산 규칙 | 함수 단위로 | 많이, 빠르게 |
| ViewModel의 동작 | 화면 없이 호출해서 | 꽤 많이 |
| 화면 표시 | 실제로 띄워서 | 최소한만 |
위로 갈수록 싸고 아래로 갈수록 비쌉니다. 그래서 아래에서 확인해야 할 것을 줄이는 게 이득이고, 그 줄이는 방법이 관심사를 나누는 것입니다.
"화면은 그림만 그린다"가 지켜지면 화면 쪽에서 확인할 게 적어집니다. 그리기만 하는 코드는 틀릴 여지가 적으니까요.
큐레이터 노트
각 층을 "무엇을 하는가"가 아니라 "무엇을 모르는가"로 정의한 기록이라 뽑았습니다. 패턴 설명에서 잘 안 나오는 각도인데, 실제로 이 패턴의 값어치는 거기서 나옵니다.
그리고 가장 아무것도 안 하는 클래스부터 시작한 순서도 좋았습니다. 의존 방향의 끝에서 출발해 위로 올라가는 구성이라, 읽으면서 화살표가 어디로 향하는지가 자연스럽게 남습니다.
02
🎣 4주차 스터디 내용 — 예외 처리와 제네릭
노현주 님
스터디에서 다룬 두 주제를 정리했습니다. 예외 처리와 제네릭이요.
예외 처리에서는 두 방식을 표로 비교합니다. 컨트롤러에서 응답을 직접 만드는 방식과, 서비스에서 예외를 던지고 한 곳에서 받는 방식이요.
비교 항목이 넷입니다. 처리 위치 · 코드 스타일 · 유지보수 · 주요 용도.
그리고 예외가 던져진 뒤의 흐름을 세 단계로 정리했습니다.
4·6·7·8회차에 이어 다섯 번째입니다. 이번에도 스터디 기록이에요.
이 글의 성격은 선택지를 나란히 놓고 비교한 것입니다. "이렇게 하세요"가 아니라 "둘 중 어느 쪽이 언제 낫다"로 적혀 있어요.
조금 더 들어가 보면
🔀 두 방식이 실제로 다른 지점
저자가 만든 표에서 가장 중요한 줄이 유지보수 항목입니다.
예외를 던지는 방식 — 한 곳만 수정하면 전체 적용됨
이게 두 방식을 가르는 핵심입니다. 나머지 차이는 취향에 가까운데, 이건 규모가 커질수록 확실하게 벌어져요.
에러 응답 형식은 생각보다 자주 바뀝니다. 처음엔 메시지만 내려주다가, 나중에 에러 코드가 필요해지고, 그다음엔 어느 필드가 문제인지도 넣게 되고요. 그때마다 컨트롤러를 전부 찾아다니는 것과 한 클래스만 고치는 것은 완전히 다른 일입니다.
같은 것을 여러 군데 적어두면 바꿀 때 전부 찾아야 한다. 프로그래밍에서 가장 오래된 원칙 중 하나가 여기서도 나옵니다.
💸 트랜잭션 이야기가 결정적입니다
저자가 장점으로 꼽은 것 중 두 번째가 특히 중요합니다.
이건 취향 문제가 아니라 정확성 문제입니다.
스프링에서 트랜잭션은 기본적으로 런타임 예외가 밖으로 나갈 때 롤백됩니다. 그런데 예외를 던지지 않고 "실패했습니다"라는 응답 객체를 정상적으로 돌려주면, 스프링 입장에서는 성공적으로 끝난 것이에요. 그래서 커밋됩니다.
문제는 그 사이에 이미 데이터베이스를 건드렸을 때 생깁니다. 뭔가를 저장하고 나서 검증에 걸려 "실패"를 돌려주면, 실패라고 응답했는데 데이터는 저장된 상태가 됩니다.
6회차의 송금 예제에서 본 그 문제입니다. 메시지는 실패인데 잔액은 이미 줄어 있는 상황이요. 그때는 프로시저 안에서 명시적으로 롤백을 걸어 해결했고, 여기서는 예외를 던져 프레임워크가 롤백하게 만드는 것이죠.
같은 문제에 대한 두 층위의 해법이 4기 안에서 나란히 기록된 셈입니다.
🕸️ 흐름을 세 단계로 정리한 것
저자가 적은 흐름이 이렇습니다.
2. 예외를 던진다 — 흐름 중단: 예외가 발생하는 즉시 이후의 코드는 실행되지 않고, 호출 스택을 따라 예외가 위로 던져짐
3. 전역 예외 처리기가 이 예외를 감지하고 낚아챈다
2번의 설명이 정확합니다. 예외의 본질은 "위로 던진다"가 아니라 "여기서 멈추고 위로 간다" 거든요.
그래서 예외를 던지면 그 아래 코드를 안 써도 됩니다. 반환값으로 실패를 알리는 방식은 부르는 쪽마다 "실패했나?" 하고 확인해야 하는데, 예외는 그 확인이 필요 없어요. 처리할 사람이 나타날 때까지 알아서 올라갑니다.
그리고 3번이 앞의 유지보수 이야기와 이어집니다. 그 "처리할 사람"이 한 명이라, 응답 형식이 거기서만 정해집니다.
🧊 제네릭이 푸는 문제
뒷부분의 제네릭 정리도 같은 결에 있습니다. 저자가 짚은 문제가 이거예요.
여러 종류를 담는 상자를 만들 때, 가장 쉬운 방법은 아무거나 담기는 상자로 만드는 것입니다. 그런데 꺼낼 때 문제가 생겨요. 뭐가 들었는지 모르니 "이건 문자열일 거야" 하고 변환해야 합니다.
그 짐작이 틀리면 실행 중에 터집니다. 컴파일할 때는 아무 문제가 없어 보이고요.
제네릭은 상자를 만들 때 무엇을 담을지 밖에서 정하게 합니다. 그러면 컴파일러가 알아요. 잘못된 걸 넣으려 하면 그 자리에서 막고, 꺼낼 때도 변환이 필요 없습니다.
"실행 중에 터지던 것을 컴파일할 때 막는 것." 이게 제네릭의 전부이고, 앞의 예외 처리와도 통합니다. 문제를 더 이른 단계에서, 더 정해진 자리에서 다루는 것이요.
📨 그런데 어떤 예외를 던질 것인가
이 방식으로 가기로 했다면 다음 질문이 따라옵니다. 예외를 어떻게 설계할 것인가요.
저자가 예로 든 것처럼 업무용 예외 클래스를 하나 두는 게 출발점입니다. 그런데 그 하나만으로는 부족해져요. 전역 처리기에서 "이 예외는 404, 저 예외는 400"으로 나눠야 하는데, 종류가 하나면 나눌 수가 없거든요.
그래서 보통 이렇게 갑니다.
· 공통 부모를 하나 둡니다 — 모든 업무 예외가 여기서 갈라져 나옵니다
· 그 아래를 상황별로 나눕니다 — 찾는 게 없음, 권한 없음, 값이 잘못됨처럼요
· 예외에 코드와 상태를 담아둡니다 — 처리기가 그걸 읽어 응답을 만듭니다
이렇게 두면 처리기가 예외 종류마다 분기하지 않아도 됩니다. 예외가 자기 정보를 들고 오니까 처리기는 꺼내 담기만 하면 되거든요. 새 예외가 생겨도 처리기는 안 고칩니다.
7회차에서 본 선언형 이야기와 같은 자리입니다. "이럴 때 이렇게 하라"고 절차를 적는 대신, 정보를 붙여두고 규칙이 알아서 처리하게 하는 것이요.
🪤 잡아서 삼키면 안 되는 이유
한 가지 더 짚어두겠습니다. 예외를 다루면서 가장 흔한 실수가 잡아놓고 아무것도 안 하는 것입니다.
try {
// ...
} catch (Exception e) {
// 아무것도 안 함
}
이러면 예외가 위로 안 올라갑니다. 전역 처리기도 못 받고, 트랜잭션 롤백도 안 걸립니다. 앞에서 이야기한 문제가 그대로 재현돼요.
그리고 더 나쁜 건 문제가 있었다는 사실 자체가 사라진다는 것입니다. 로그에도 안 남으니 나중에 "왜 데이터가 이상하지"를 추적할 실마리가 없어져요.
그래서 원칙이 이렇습니다. 잡을 거면 처리하고, 처리 못 할 거면 잡지 말 것. 굳이 잡아야 한다면 최소한 로그는 남기고, 필요하면 다시 던져야 합니다.
예외의 값어치는 "여기서 멈추고 위로 간다"에 있는데, 잡아서 삼키면 그 두 가지가 다 없어집니다.
🧯 사용자에게 무엇을 보여줄 것인가
전역 처리기를 두고 나면 다음 결정이 옵니다. 응답에 무엇을 담을 것인가요.
여기서 갈리는 게 오류 메시지의 상세함입니다. 개발할 때는 자세할수록 좋은데, 실제 서비스에서는 그게 위험합니다.
· 스택 추적을 그대로 내보내면 — 쓰는 프레임워크와 버전, 내부 패키지 구조가 다 드러납니다
· 데이터베이스 오류 메시지를 그대로 내보내면 — 테이블과 컬럼 이름이 나갑니다
· "이 아이디는 없습니다"와 "비밀번호가 틀렸습니다"를 구분해주면 — 어떤 아이디가 존재하는지 알려주는 셈이 됩니다
그래서 밖으로 나가는 메시지와 안에 남기는 기록을 나눠야 합니다. 사용자에게는 "일시적인 오류가 발생했습니다"만 주고, 자세한 내용은 로그에 남기는 것이죠.
이때 유용한 게 오류마다 식별자를 붙여 양쪽에 같이 남기는 것입니다. 사용자에게 "오류 코드 A3F9"를 보여주고 로그에도 같은 값을 남기면, 문의가 들어왔을 때 그 한 줄로 해당 요청을 찾을 수 있어요.
전역 처리기가 한 곳이라는 게 여기서 또 이득입니다. 이 정책을 한 자리에서 정하면 되니까요. 컨트롤러마다 응답을 만들고 있었다면 어딘가에는 반드시 자세한 오류가 새어 나갔을 겁니다.
📝 원문에서는 이렇게 비교합니다
표의 마지막 줄이 결론입니다.
어느 쪽이 무조건 낫다고 안 적었습니다. 작은 프로젝트에서는 전역 처리기까지 만드는 게 오히려 과할 수 있으니까요.
그리고 두 어노테이션의 차이를 한 줄로 짚어둔 것도 실용적입니다. 하나는 응답을 JSON으로 내려주는 설정이 이미 붙어 있다는 것이요. 이름이 비슷해서 헷갈리는 자리인데 정확히 정리했습니다.
큐레이터 노트
두 방식을 표로 비교하고, 어느 쪽이 언제 낫다고 적은 기록이라 뽑았습니다. 「선택과 근거가 남아 있는가」에 정확히 해당해요.
그중에서도 트랜잭션 롤백 이야기가 결정적이었습니다. 코드 스타일 비교로 끝날 수 있는 주제에서 "정확성이 달라지는 지점"을 짚었거든요. 6회차의 송금 예제와 겹쳐 읽으면 같은 문제의 두 층위가 보입니다.
4기 열 회차 중 여섯 번을 채운 분입니다. 고치기 · 옮기기 · 읽기 · 스터디로 실무의 여러 국면이 남았어요.
03
📦 선형대수 7장 연습문제(2) 풀이
최태형 님
7장 후반부입니다. 앞 편이 성분과 내적이었다면 여기서는 외적으로 갑니다.
문제 구성이 이래요. 수직임을 보이기, 두 벡터에 수직인 벡터 찾기, 평행사변형의 넓이, 평행육면체의 부피, 세 점을 지나는 평면의 방정식.
계산이 곧바로 도형의 양으로 이어집니다.
앞 회차의 7장 전반부에 이어집니다. 그리고 이번 편에서 이 장의 목적이 드러나요. 두 개로 세 번째를 만드는 계산입니다.
조금 더 들어가 보면
✖️ 외적이 하는 일
내적은 벡터 둘을 넣으면 숫자 하나가 나옵니다. 외적은 벡터 둘을 넣으면 벡터 하나가 나와요.
그리고 그 결과 벡터에는 성질이 셋 있습니다.
· 방향 — 원래 두 벡터 양쪽에 모두 수직입니다
· 길이 — 두 벡터가 만드는 평행사변형의 넓이와 같습니다
· 부호 — 순서를 바꾸면 반대 방향이 됩니다
첫 번째 성질이 이 장의 문제들을 설명합니다. 저자가 푼 "두 벡터에 수직인 벡터를 구하라"가 그대로 외적 계산이에요.
그리고 이건 3차원 그래픽에서 매일 쓰이는 계산입니다. 면의 방향을 구하는 일이 정확히 이것이거든요. 삼각형의 두 변을 벡터로 잡고 외적하면 그 면이 어느 쪽을 향하는지가 나옵니다. 빛을 어느 쪽에서 받을지, 뒷면이라 안 그려도 되는지가 이 계산으로 정해집니다.
세 번째 성질도 중요합니다. 순서를 바꾸면 방향이 뒤집히니까, 면의 앞뒤가 정점 순서에 달려 있습니다. 모델이 안팎이 뒤집혀 보이는 문제의 원인이 대개 여기예요. 5회차에서 이야기한 행렬식의 부호와 같은 자리입니다.
📐 넓이와 부피가 계산으로 나옵니다
저자가 푼 문제 중에 평행사변형의 넓이와 평행육면체의 부피를 구하는 것이 있습니다.
원래 넓이를 구하려면 밑변과 높이를 알아야 하고, 높이를 구하려면 수선을 내려야 합니다. 그림이 필요해요.
그런데 벡터로 하면 곱셈과 뺄셈 몇 번으로 끝납니다. 외적의 길이가 곧 넓이니까요. 부피는 한 단계 더 가서, 세 벡터 중 둘을 외적하고 나머지와 내적하면 나옵니다.
이게 5장의 행렬식과 연결됩니다. 그 부피 계산이 사실 세 벡터를 행으로 놓은 행렬의 행렬식이거든요. 5장에서 "행렬식은 부피의 배율"이라고 이야기한 게 여기서 실제 부피 계산으로 나타납니다.
교재가 5장에서 심어둔 개념을 7장에서 회수하는 구조입니다. 진도를 순서대로 밟은 기록에서만 이 연결이 보여요.
🛬 평면의 방정식은 왜 그렇게 생겼나
마지막 문제인 "세 점을 지나는 평면의 방정식"이 이 장의 마무리로 적절합니다. 앞의 것들이 다 들어가거든요.
세 점이 주어지면 그중 하나를 기준으로 두 개의 벡터를 만들 수 있습니다. 그 둘을 외적하면 평면에 수직인 방향이 나오고요. 그리고 평면 위의 임의의 점에서 기준점으로 가는 벡터는 그 수직 방향과 내적이 0이어야 합니다.
그 조건을 성분으로 풀어 쓴 게 평면의 방정식입니다.
그래서 평면의 방정식에서 각 항의 계수가 사실 수직 방향 벡터의 성분입니다. 식을 보고 바로 "이 평면은 이쪽을 향하고 있구나"를 읽을 수 있어요.
공식을 외우는 것과 어디서 나왔는지 아는 것의 차이가 여기서 나옵니다. 유도 과정을 한 번 통과하면 계수의 의미가 같이 남습니다.
🔁 검산하는 문제를 넣어둔 교재
이 장에서 눈에 띄는 문제가 하나 더 있습니다. 이미 계산으로 구한 사이각을 기하학적 방법으로 다시 구하라는 문제요.
같은 답을 두 번 구하게 하는 셈입니다. 그런데 이게 학습에서 중요한 장치예요.
계산으로 얻은 답은 맞는지 스스로는 알 수 없습니다. 절차를 정확히 따랐는지 확인하려면 절차를 다시 밟는 수밖에 없고, 같은 실수를 두 번 하면 두 번 다 같은 틀린 답이 나옵니다.
다른 방법으로 구해서 같은 답이 나오면 그때 확신이 생깁니다. 방법이 다르니 같은 실수를 반복할 가능성이 낮거든요.
이건 코드에서 테스트를 짤 때와 같은 원리입니다. 구현과 똑같은 방식으로 기대값을 계산하는 테스트는 아무것도 검증하지 못해요. 다른 경로로 얻은 값과 비교해야 의미가 있습니다.
교재가 이 습관을 문제로 만들어둔 것이고, 그걸 건너뛰지 않고 푼 기록이 남았습니다.
🧮 성분 계산이 왜 그렇게 생겼나
외적을 성분으로 계산하는 식은 처음 보면 외우기 어렵게 생겼습니다. 각 자리마다 다른 조합의 곱을 빼는 형태거든요.
그런데 이건 사실 행렬식을 계산하는 절차와 같은 모양입니다. 첫 줄에 좌표축 기호를 놓고, 아래 두 줄에 두 벡터의 성분을 놓고 행렬식을 구하듯 전개하면 그대로 나와요.
5장에서 익힌 것이 여기서 계산 도구로 쓰이는 것이죠. 외우는 대신 "행렬식처럼 펼친다" 로 기억하면 되고, 그러면 3차원이 아닌 경우에 왜 안 되는지도 자연히 이해됩니다. 세 줄짜리 정방행렬이 나와야 성립하니까요.
진도를 순서대로 밟은 사람만 얻는 연결입니다. 5장을 건너뛰고 7장을 봤다면 이 식은 그냥 외울 것이 됐을 거예요.
🌐 3차원에서만 되는 계산
외적에 대해 하나만 더 짚겠습니다. 이 계산은 3차원에서만 지금 형태로 성립합니다.
내적은 차원과 무관합니다. 2차원이든 100차원이든 성분끼리 곱해서 더하면 되고, 결과는 항상 숫자 하나예요. 그래서 데이터 분석에서 수백 차원 벡터의 유사도를 재는 데 그대로 쓰입니다.
외적은 다릅니다. "두 벡터 양쪽에 수직인 방향"이 하나로 정해지는 게 3차원의 특징이거든요. 2차원에서는 그런 방향이 평면 밖으로 나가버리고, 4차원 이상에서는 수직인 방향이 여러 개라 하나를 못 고릅니다.
그래서 외적은 공간을 다루는 자리에서 주로 쓰입니다. 3차원 그래픽, 물리 시뮬레이션, 로봇 팔의 회전 같은 것들이요. 반대로 데이터 쪽에서는 거의 안 보입니다.
같은 장에 있는 두 연산인데 쓰이는 분야가 갈리는 것이죠. 그리고 그 이유가 "차원에 따라 성립하는가"에 있습니다.
교재가 둘을 나란히 두는 게 그래서 좋습니다. 닮아 보이는 두 계산이 실제로는 성질이 다르다는 걸 같이 놓고 봐야 알 수 있으니까요.
📦 넓이와 부피를 코드에서 쓰는 자리
이 장의 계산이 실제로 어디서 도는지도 짚어두겠습니다.
어떤 점이 삼각형 안에 있는가 — 화면에서 클릭한 지점이 어느 면 위인지 판정할 때 씁니다. 삼각형을 세 조각으로 나눠 넓이를 더해보면 되는데, 그 넓이 계산이 외적이에요.
어느 쪽이 앞면인가 — 앞에서 이야기한 면의 방향입니다. 카메라 쪽을 안 보는 면은 그리지 않는데, 그 판정이 외적과 내적의 조합입니다.
세 점이 한 직선 위에 있는가 — 두 벡터를 만들어 외적했을 때 0이면 평행이고, 평행이면 한 직선 위입니다. 지도 데이터에서 불필요한 점을 걸러낼 때 쓰는 판정이고요.
전부 "그림을 그려서 보는" 일을 숫자로 바꾼 것입니다. 사람은 눈으로 즉시 아는데 컴퓨터는 계산으로만 알 수 있는 것들이요.
교재의 문제가 "평행사변형의 넓이", "평행육면체의 부피"처럼 도형으로 되어 있어 추상적으로 느껴질 수 있는데, 그 값들이 실제로는 판정에 쓰입니다. 넓이가 0인지, 부피가 0인지를 보는 경우가 대부분이고요.
그리고 "0인가"라는 질문이 또 나옵니다. 4장의 역행렬, 5장의 행렬식, 7장의 외적까지 — 교재가 계속 같은 것을 묻고 있어요.
그런데 여기서도 6회차에서 짚은 문제가 그대로 따라옵니다. 코드에서는 정확히 0이 잘 안 나옵니다. 그래서 "세 점이 한 직선 위에 있는가" 같은 판정도 실제로는 "아주 작은 값보다 작은가"로 구현합니다. 그리고 그 기준값을 얼마로 잡느냐가 결과를 바꿔요. 너무 크게 잡으면 다른 점까지 지워지고, 너무 작게 잡으면 아무것도 안 걸러집니다. 손으로 풀 때는 없던 결정이 코드에서는 반드시 생깁니다.
📝 판정 근거를 한 줄씩 남기는 방식
이 연재 전체에 걸쳐 유지된 형식을 마지막으로 한 번 짚겠습니다. 계산 뒤에 판정 문장을 붙이는 것이요.
"소거행제형으로 변환하였는데 영행이 존재하므로, 정칙행렬이 아니다" 같은 문장이 매 문제마다 나옵니다. 계산 결과만 적고 넘어가지 않아요.
이게 왜 좋은지는 나중에 다시 볼 때 드러납니다. 계산은 다시 따라가기 힘든데 문장은 바로 읽히거든요. 시험 전에 훑어볼 때 이 문장들만 따라가도 각 문제가 무엇을 묻는 문제였는지가 정리됩니다.
그리고 이건 코드에 주석을 남기는 일과 같은 성격입니다. 무엇을 했는지는 코드에 있고, 왜 그렇게 판단했는지는 따로 적어야 남거든요.
일곱 회차에 걸쳐 이 형식이 흔들리지 않은 게, 이 기록에서 가장 눈에 띄는 부분입니다.
📝 원문에서는 이렇게 씁니다
문제들이 계산에서 도형으로 이어집니다.
14. 다음 두 벡터 A, B에 수직인 벡터를 구하라
16. 두 벡터에 의해 만들어지는 평행사변형의 넓이를 구하라
18. 세 벡터가 이루는 평행육면체의 부피를 구하라
19. 주어진 세 점을 지나는 평면의 방정식을 구하라
판정 → 방향 구하기 → 넓이 → 부피 → 평면. 같은 계산 하나를 점점 큰 대상에 적용해 갑니다.
그리고 17번이 눈에 띕니다. "앞 문제에서 구한 사이각을 기하학적 방법으로 구하라." 계산으로 이미 구한 것을 그림으로 다시 확인하게 하는 문제예요. 답이 맞는지 다른 방법으로 검산하는 습관을 교재가 문제로 만들어둔 셈입니다.
큐레이터 노트
앞 장에서 배운 것이 회수되는 자리를 통과한 기록입니다. 5장의 행렬식이 7장의 부피 계산으로 돌아오는데, 진도를 순서대로 밟은 기록에서만 이 연결이 보여요.
아홉 번째 회차인데 형식이 처음과 같습니다. 중간 단계를 건너뛰지 않고, 근거를 한 줄씩 적는 것이요. 꾸준함이 곧 이 기록의 내용입니다. 10회차에서 8장으로 마무리됩니다.
💡 세 편을 겹쳐 읽으며
각자 자기 일만 하게 나눕니다
세 편이 같은 동작을 합니다. 하나로 뭉쳐 있던 것을 나누고, 나눈 것들 사이의 관계를 정하는 것이요.
MVVM은 그리는 일과 계산하는 일과 데이터를 나눕니다. 예외 처리 구조는 문제를 발견하는 자리와 응답을 만드는 자리를 나눕니다. 외적은 두 벡터에서 그 둘과 무관한 세 번째 축을 뽑아냅니다.
나누고 나면 각자를 따로 다룰 수 있게 됩니다. ViewModel은 화면 없이 테스트하고, 서비스는 응답 형식을 몰라도 되고, 평면의 방향은 평면 위의 점들과 별개로 정해집니다.
경계를 넘는 방법을 정해둡니다
나누기만 하면 안 되고, 넘어가는 방법이 필요합니다. 세 편이 각각 그것도 정해뒀어요.
MVVM에서는 알림입니다. ViewModel이 방송하고 듣던 쪽이 다시 그립니다. 예외 처리에서는 던지기입니다. 서비스가 던지면 처리할 사람이 나타날 때까지 위로 올라갑니다.
둘 다 보내는 쪽이 받는 쪽을 모릅니다. 그게 나눈 상태를 유지하는 방법이거든요. 직접 부르는 순간 다시 붙어버립니다.
앞 회차와 계속 이어집니다
4기 후반의 기록들이 서로 물려 있는 게 이번 회차에서 선명합니다.
예외 처리의 트랜잭션 이야기는 6회차 송금 프로시저의 그 문제입니다. 외적의 부호는 5회차 행렬식의 부호와 같은 이야기고요. MVVM의 변경 빈도로 경계 긋기는 3회차 Dockerfile 레이어 순서와 같은 발상입니다.
같은 원리가 층을 바꿔가며 반복되는 것을 기수 전체를 놓고 보면 확인할 수 있습니다. 2차 큐레이션을 하며 계속 마주치는 장면이에요.
이번 회차에서 가져갈 것
1. 각 층을 "무엇을 모르는가"로 정의하기.
무엇을 하는지보다 무엇을 몰라야 하는지가 경계를 만듭니다. ViewModel이 View를 모르면 화면 없이 테스트할 수 있습니다.
2. 어디에 둘지 헷갈리면 "화면이 바뀌면 이것도 바뀌나"를 묻기.
그렇다면 화면 쪽, 아니라면 아래쪽입니다. 애매한 걸 전부 중간층에 넣으면 이름만 패턴이 됩니다.
3. 실패를 반환값이 아니라 예외로 알리기.
반환값으로 실패를 알리면 트랜잭션이 커밋됩니다. "실패했다고 응답했는데 데이터는 저장된" 상태가 여기서 생깁니다.
4. 에러 응답 형식은 한 곳에서만 만들기.
컨트롤러마다 만들어두면 형식이 바뀔 때 전부 찾아야 합니다. 전역 처리기 하나면 한 번에 끝납니다.
5. 아무거나 담기는 상자 대신 담을 것을 밖에서 정하기.
꺼낼 때마다 변환하는 코드는 실행 중에 터집니다. 제네릭으로 두면 컴파일 시점에 막힙니다.
4기 9회차는 2026년 1월 말이었습니다.
앱 아키텍처와 백엔드 스터디와 전공 교재라는 서로 다른 자리에서, 세 분이 나란히 나누고 경계를 정하는 이야기를 남겼습니다. 그리고 셋 다 나눈 것들이 어떻게 다시 이어지는지까지 적었어요.
성장일지는 멤버들이 2주에 한 번씩 자신의 성장을 기록하는 활동이에요. 지난 기수의 성장일지를 다시 읽으면서, 그때 소개해드리지 못한 글을 하나씩 꺼내고 있습니다.
좋은 글 남겨주신 박현구 님, 노현주 님, 최태형 님께 감사드립니다. 그리고 4기를 함께 채워주신 모든 멤버분들께도요! 😊
다음 큐레이션으로 다시 찾아오겠습니다! 🌱
- 에디터 · 성장일지 큐레이터 -
※ 본 큐레이션은 각 저자가 공개한 글을 소개하는 것이며, 모든 원문의 저작권은 저자에게 있습니다. 저자 본인의 요청이 있을 경우 즉시 수정 또는 삭제합니다.
'성장일지' 카테고리의 다른 글
| [GL 5기 1회차] 약속 대신 구조가 지키게 — CQRS와 의존성 주입 · FSD 아키텍처 🌱 (0) | 2026.08.11 |
|---|---|
| [GL 4기 10회차 ①] 결론을 미루는 설계 — 현대사 검색 앱, 이벤트 기반 아키텍처, 점심 메뉴 투표 🌱 (0) | 2026.08.11 |
| [GL 4기 9회차 ①] 도중에 멈추면 생기는 일 — 배열 정렬 기법, 배포 직후 에러, Grafana 알림 🌱 (0) | 2026.08.11 |
| [GL 4기 8회차 ②] 미리 바꿔두면 나중이 싸집니다 — useMemo 메모이제이션, 파이썬 nonlocal과 클로저, 벡터의 성분 표현 🌱 (0) | 2026.08.11 |
| [GL 4기 8회차 ①] 본론이 아닌데 없으면 안 되는 것 — 한 해의 회고, AOP, 시그널링 서버 🌱 (0) | 2026.08.11 |