셋이서 만들기 — 회원가입 하나로 배우는 협업

이 페이지는 딱 회원가입 기능 하나만 가지고 세 사람이 어떻게 동시에 일하는지를 보여줍니다. 전체 데이터 흐름을 먼저 보고, 그 다음 각자의 역할 페이지로 들어가서 자세히 배우는 구조입니다. 이 한 가지를 제대로 이해하면 로그인·게시판 같은 나머지 기능은 같은 패턴의 반복이에요.

전제 — 시작 시점의 상태

  • member 테이블은 이미 만들어져 있다 — Day 0에 DB 담당이 만들어둠
  • 컬럼: user_id, password, nickname, email, phone, hobbies, favorite_color, job, reg_date
  • 이제 회원가입 기능을 세 사람이 동시에 개발하기 시작하는 순간이 Day 1

한땀한땀 따라가는 데이터 흐름 — 회원가입 성공 케이스

먼저 완성된 기능이 어떻게 동작하는지 전체 흐름을 처음부터 끝까지 봅시다. Step 1부터 Step 10까지 천천히 따라가면서 "사용자가 가입 버튼을 누른 순간부터, DB에 저장되어 성공 화면이 뜨기까지" 의 여정을 한 번에 이해하는 게 목적이에요. 이 10단계만 손에 잡히면 나머지는 응용입니다.

사용자가 회원가입 페이지에 도착

FRONT브라우저가 GET /join.jsp 요청 → Tomcat이 정적 JSP 반환

사용자가 상단 메뉴에서 "회원가입" 링크를 누르면 브라우저가 /join.jsp 를 요청합니다. 이 시점엔 서버 쪽 로직이 거의 없어요 — Tomcat이 JSP 파일을 읽고 HTML로 변환해서 브라우저에게 돌려주기만 합니다.

회원가입 페이지 초기 진입
Step 1 결과 · 브라우저에 이런 빈 회원가입 폼이 뜸 (시안 A)

사용자가 정보를 입력하고 "가입하기" 클릭

FRONT이 단계는 완전히 클라이언트 안에서만 일어남 — 서버 트래픽 없음

사용자가 아이디·비밀번호·닉네임·이메일·취미·색깔·직업을 채우는 동안 서버로는 아무것도 안 갑니다. 브라우저 안에서만 일어나는 일이에요. 버튼을 눌러야 그때 서버 요청이 시작됩니다.

브라우저가 POST /register 요청 전송

FRONT브라우저가 HTTP POST 요청 생성 → Tomcat으로 송신

"가입하기" 버튼이 눌리는 순간 브라우저는 모든 <input>name과 value를 쌍으로 묶어 HTTP 요청 본문에 담습니다:

POST /test/register HTTP/1.1
Host: localhost:8081
Content-Type: application/x-www-form-urlencoded

userId=test_user&password=1234&passwordConfirm=1234
&nickname=테스트유저&email=test@example.com&phone=010-1234-5678
&hobbies=독서&hobbies=운동&hobbies=음악
&favoriteColor=파랑&job=학생

주목할 점: hobbies 가 세 번 반복 됩니다. 사용자가 체크박스 3개를 선택했으니까요. 이게 나중에 getParameterValues 가 배열을 돌려주는 이유입니다.

Tomcat이 URL → Servlet 매핑 확인

BACKTomcat이 @WebServlet("/register") 어노테이션 표를 보고 RegisterServlet 호출

Tomcat은 내부 매핑 표를 보고 "/register 요청 → RegisterServlet.service() 호출" 을 결정합니다. 여기서 개발자 코드에 if-else가 없다는 점이 중요해요 — Tomcat이 알아서 합니다. URL 라우팅 페이지 참고.

Servlet이 form 값을 변수로 추출

BACKRegisterServlet.service() 안에서 getParameter × 8개 + getParameterValues × 1개

Controller의 첫 번째 일. req.getParameter("userId") 같은 호출들이 차례로 실행되면서 HTTP 문자열 파라미터들이 Java 변수에 담깁니다. 특히 hobbiesgetParameterValuesString 배열을 받아옵니다.

서버 콘솔에 찍힌 실제 값들
Step 5 결과 · 서버 콘솔에 System.out.println 으로 찍힌 값들 — 모든 필드가 올바르게 추출됨

Controller가 배열을 CSV로, 변수들을 DTO에 담기

BACKHTTP 세계 → Java 객체 세계 번역

여기가 Controller가 "진짜 일" 하는 순간입니다. hobbies 배열은 String.join(",", hobbiesArr) 로 CSV 문자열("독서,운동,음악")이 되고, 흩어져있던 변수들은 한 덩어리 Member DTO 객체로 재조립됩니다. 이제 DB가 삼킬 수 있는 형태가 됐어요. (DTO가 무엇인지는 Backend 가이드 에서 자세히 다룹니다.)

Member newMember = new Member();
newMember.setUserId("test_user");
newMember.setHobbies("독서,운동,음악");  // ← CSV로 번역됨
newMember.setFavoriteColor("파랑");
newMember.setJob("학생");
// ... 나머지 필드도 동일

Controller가 DAO.insert() 호출

BACKDBMyBatis가 중간에서 쿼리 실행 담당

memberDao.insert(newMember) 한 줄이 Controller의 마지막 책임입니다. 이 호출이 MyBatis에게 넘어가고, MyBatis는 MemberMapper.xml<insert id="insert"> 쿼리를 찾아서 #{userId}, #{hobbies} 같은 자리에 Member DTO의 필드 값을 채워넣습니다.

DB가 member 테이블에 INSERT 실행, 1 row affected

DBMariaDB가 실제로 디스크에 새 행 기록

MyBatis가 만든 최종 SQL이 MariaDB에 도달하고, 실제로 member 테이블에 새 행이 추가됩니다. DB는 "1 row affected" 를 MyBatis에게 돌려주고, MyBatis는 그 결과를 int 로 DAO에게, DAO는 Servlet에게 리턴합니다.

DB에 실제 INSERT된 모습
Step 8 결과 · DBeaver에서 SELECT해보면 방금 가입한 test_user가 맨 위에 있음 (reg_date는 DB가 자동 설정)

Servlet이 응답 결정 — sendRedirect

BACK처리 결과에 따라 어느 JSP로 보낼지 결정

DAO가 "1 row affected" 를 돌려줬으니 가입 성공. Servlet은 resp.sendRedirect("join_success.jsp") 로 브라우저에게 "이제 이 URL로 다시 가" 라고 알려줍니다. 이게 Servlet의 마지막 한 줄이에요.

브라우저가 성공 페이지 수신 · 사용자에게 완료 화면

FRONT브라우저가 302 Redirect를 받고 join_success.jsp 요청 → 화면 갱신

브라우저는 302 응답을 받고 주소창 URL을 /test/join_success.jsp 로 바꾼 뒤 새 요청을 보냅니다. 이 두 번째 요청은 다시 Tomcat → JSP → HTML 로 흘러가서 사용자에게 "가입 완료!" 화면을 보여줍니다. 사용자 여정 끝.

정리하면 — 회원가입 한 번에 요청이 총 2번 발생합니다. 첫 번째는 form POST (Step 3~9), 두 번째는 sendRedirect로 인한 GET (Step 10). 이게 sendRedirect 의 특징이에요 — 두 번 요청, URL 바뀜, request 데이터 유지 안 됨.

각자의 역할 — 나의 영역으로 들어가기

위 10단계 흐름을 봤다면, 이제 자기 역할의 자세한 가이드를 보러 갈 차례입니다. 각 페이지는 독립적으로 완결되어 있어서, 자기 담당 페이지 하나만 봐도 충분히 작업을 시작할 수 있습니다.

사용자 입장에서 본 회원가입 흐름

개발자 관점(form name, SQL 쿼리)으로만 협업을 보면 중요한 걸 놓치기 쉽습니다. 사용자가 어떤 경험을 하는지부터 먼저 그려봐야, "무엇을 주고받을지" 와 "UI를 어떻게 만들지" 가 자연스럽게 드러납니다. 세 사람이 같이 이 여정을 그리는 게 Day 1 회의의 진짜 출발점이에요.

사용자의 여정 — 7단계

  1. Step 1 — 메인 페이지 도착: localhost:8081/test/ 접속, 상단 메뉴에 "회원가입" 링크
  2. Step 2 — "회원가입" 링크 클릭: GET /join.jsp → 빈 폼 페이지 로드
  3. Step 3 — 폼 화면 진입 · 정보 입력: 아이디·비번·닉네임·이메일·취미·색·직업 입력 (서버 트래픽 없음)
  4. Step 4 — "가입하기" 버튼 클릭: POST /register 로 form 전송
  5. Step 5 — 서버 처리: RegisterServlet → DTO 조립 → DAO → INSERT
  6. Step 6 — 결과 피드백: 성공 시 sendRedirect("join_success.jsp"), 실패 시 ?error=...
  7. Step 7 — 다음 행동 유도: "로그인하러 가기" 버튼 또는 자동 메인 이동
각 Step의 UI/UX 선택지 — "이렇게도, 저렇게도":
· Step 3: 한 페이지 vs 여러 스텝으로 나누기
· Step 3: 아이디 중복 확인을 실시간 AJAX로 vs 제출 후 일괄로
· Step 6: 에러를 alert vs 폼 위 배너 vs 인라인 메시지로
· Step 7: 자동 로그인 vs 수동 로그인 페이지 이동
이런 선택들이 Frontend 디자인뿐 아니라 Backend 로직과 DB 쿼리에도 영향을 줍니다. Day 1에 같이 결정해야 하는 이유예요.

협업의 핵심 — 무엇을 주고받는가

회원가입 기능을 만드는 동안, 세 사람 사이에는 딱 3가지 주고받음이 있습니다. 이것만 선명하면 나머지는 각자 자기 자리에서 만듭니다.

주고받음 ① · Frontend → Backend

전달 내용: form이 서버로 보낼 필드 명세

"name="userId", name="password" ... 이런 값들을 <input> 으로 보낼 거예요" 라는 약속. Backend는 이 명세만 있으면 자기 쪽 코드를 시작할 수 있습니다.

주고받음 ② · Backend → Database

전달 내용: "INSERT 할 때 쓸 DTO 필드 목록"

"Member DTO의 8개 필드를 member 테이블에 넣어야 해요" 라는 요청. DB는 이걸 받아서 실제 INSERT 쿼리 문장을 다듬습니다.

주고받음 ③ · Database → Backend

전달 내용: 완성된 INSERT 쿼리 (MyBatis XML 형태)

"가짜 데이터로 수십 번 넣어보고 다듬은 최종본이에요. 그대로 써도 돼요" 라는 결과물. Backend는 자기 Servlet에서 이 쿼리를 호출하기만 하면 됩니다.

순서 중요: ① 이 먼저 나와야 ② 가 나올 수 있고, ② 가 나와야 ③ 이 시작됩니다. 하지만 각 주고받음이 끝날 때까지 기다리지는 않습니다. Backend는 ① 받자마자 자기 일 시작, DB는 ② 오기 전에도 가짜 데이터로 쿼리 연습 시작. 이게 "동시 개발"의 비밀입니다.

잠깐 — "Mockup" 이라는 단어부터

이 페이지(와 역할별 가이드)에서 앞으로 "mockup", "mock", "가짜 데이터" 라는 표현이 자주 나옵니다. 처음 듣는 단어일 수 있으니 먼저 짚고 갈게요.

Mockup이란?

자동차 회사가 신차 출시 전에 점토로 모형을 만드는 걸 본 적 있죠? 굴러가진 않지만 모양은 진짜 같아서 디자인을 미리 보고 결정할 수 있게 해줍니다. 이게 mockup이에요. "진짜는 아닌데 진짜처럼 보이는 가짜".

소프트웨어에서도 똑같습니다. 이 수업에서 만나게 될 mockup은 3가지 종류가 있어요:

① 디자인 Mockup
(화면 시제품)

Frontend가 만드는 "이렇게 생기면 어떨까" 의 화면 미리보기. 기능은 동작 안 하지만 모양만 진짜 같음. 우리 페이지의 "시안 A / 시안 B" 가 이 종류예요.

② Mock 데이터
(가짜 값)

DB나 서버가 아직 준비 안 됐을 때 화면이나 코드에 임시로 박아넣는 값. 예를 들어 게시판 화면에 "공지사항입니다" 같은 글 6건을 일단 하드코딩해두는 거.

③ Mock 구현체
(가짜 객체)

진짜 DAO 대신 흉내만 내는 Java 클래스. MemberDaoMock 같은 이름. DB 없이도 메모리 안에서 가짜로 동작합니다.

왜 가짜를 만드는가? "진짜를 기다리지 않고 자기 일을 시작하기 위해" 입니다.
Frontend가 Backend 완성을 기다리지 않고 화면 디자인을 끝낼 수 있고, Backend가 DB 완성을 기다리지 않고 Servlet 로직을 검증할 수 있어요.
나중에 진짜가 준비되면 가짜를 빼고 그 자리에 진짜를 끼워넣습니다. 같은 모양(같은 인터페이스)이라서 가능한 일이에요. 이게 협업의 핵심 트릭입니다.

Day 1 회의 — 3가지만 합의하면 된다

회원가입 하나에 대해 세 사람이 첫 회의에서 정해야 할 건 단 세 가지입니다.

이 세 가지가 종이 한 장에 적히면 Day 1은 끝. 나머지는 각자 자기 영역의 가이드 페이지로 가서 작업 시작입니다.

에러 상황에서의 팀 조율 — "이미 존재하는 회원" 시나리오

정상 흐름만 다루면 실제 개발의 절반도 안 본 거예요. 회원가입에서 가장 흔한 에러 케이스 — 이미 가입된 아이디로 다시 시도 — 를 예로, 세 사람이 어떻게 조율해야 하는지 따라가봅시다.

사용자가 이미 존재하는 아이디로 가입 시도

FRONT브라우저가 POST /register 전송 (Step 1~4는 성공 케이스와 동일)

사용자는 자기 아이디가 이미 있는지 모릅니다. 여기까지는 성공 케이스와 완전히 동일합니다. Frontend는 에러를 "예상" 하지 않고 정상적으로 폼을 제출합니다.

Servlet이 평소처럼 DAO.insert() 호출

BACKServlet은 "이미 있는 아이디" 를 사전에 모름 — 일단 시도

Controller는 파라미터를 받고 Member DTO를 조립하고 memberDao.insert(newMember) 를 호출합니다. 실패하면 그때 대응해요.

DB가 Primary Key 위반 에러를 던진다

DBMariaDB가 SQLIntegrityConstraintViolationException 발생

user_id 가 PK이므로 동일한 user_id로 INSERT 하면 DB가 거부합니다. 에러 코드는 MariaDB의 1062번, "Duplicate entry 'test_user' for key 'member.PRIMARY'". 이 예외가 MyBatis로 올라가고, MyBatis는 이걸 PersistenceException 으로 감싸서 Backend에게 던집니다.

Servlet의 try-catch가 예외를 감지

BACK개발자가 미리 준비해둔 catch 블록이 실행됨

Backend가 insert() 호출을 try 블록 안에 넣어뒀다면, 위 예외가 catch 블록으로 흘러듭니다. 여기서 Backend가 해야 할 일: 사용자 친화적 에러 코드로 변환해서 리다이렉트.

try {
  memberDao.insert(newMember);
  resp.sendRedirect("join_success.jsp");
} catch (PersistenceException e) {
  // PK 위반을 감지
  if (e.getMessage().contains("Duplicate entry")) {
    resp.sendRedirect("join.jsp?error=dup_id");  // ← 약속된 error 코드
    return;
  }
  throw e;
}
서버 콘솔에 찍힌 중복 에러 stacktrace
Step 4 결과 · Tomcat 콘솔에 찍힌 stacktrace — Caused by: Duplicate entry 'test_user' 가 진짜 원인

Servlet이 에러 코드 담아서 join.jsp로 리다이렉트

BACKFRONT두 사람의 약속 — ?error=dup_id

Backend는 join.jsp?error=dup_id 로 보낸다고 약속했고, Frontend는 error=dup_id 가 오면 "이미 가입된 아이디" 메시지를 보여준다고 약속했습니다. 이 약속이 Day 1에 문서화되어 있어야 합니다.

Frontend가 에러 파라미터 읽어서 사용자에게 표시

FRONTjoin.jsp에서 EL로 ${param.error} 체크

Frontend는 join.jsp 에서 ${param.error} 값을 확인하고, 거기에 맞는 메시지를 출력합니다. 빨간 배너 + 문제 필드 하이라이트 + 친절한 안내문까지 붙이면 사용자 입장에서 무엇이 잘못됐는지 한눈에 알 수 있어요.

<%-- join.jsp 상단에 에러 배너 --%>
<c:if test="${param.error == 'dup_id'}">
  <div class="error-banner">
    이미 가입된 아이디입니다. 다른 아이디를 사용해주세요.
  </div>
</c:if>
사용자가 보는 중복 에러 화면
Step 6 결과 · 사용자는 "이미 가입된 아이디" 배너와 해당 필드의 빨간 테두리로 문제를 바로 파악

이 시나리오가 동작하려면 — Day 1에 합의해야 할 "에러 처리 프로토콜"

위 6단계가 매끄럽게 돌아가려면 세 사람이 Day 1 회의에서 에러 처리 규약을 명시적으로 정해야 합니다:

  • DB DB 담당이 공유할 것: "중복 PK 에러는 MariaDB 1062 코드, Duplicate entry 문자열이 메시지에 포함됩니다."
  • BACK Backend 담당이 결정할 것: "어떤 예외를 어떤 error 코드로 변환할지 매핑 표 문서화. 예: 중복 PK → dup_id, 비번 불일치 → pw_mismatch."
  • FRONT Frontend 담당이 결정할 것: "각 error 코드에 대응되는 사용자 메시지와 UI (배너·필드 하이라이트·모달 등)."

다른 에러 케이스도 같은 원리 — 빠른 참조표

에러 상황 DB 반응 / 담당 Backend 처리 / 담당 Frontend 표시 / 담당
중복 아이디 DB PK 위반
Duplicate entry
BACK catch → ?error=dup_id FRONT "이미 가입된 아이디"
비밀번호 불일치 — (DB까지 가지도 않음) BACK !password.equals(confirm) 체크 FRONT 실시간 JS + 서버 메시지
필수값 누락 DB NOT NULL 위반 BACK null/빈값 체크 FRONT HTML required 속성
이메일 형식 오류 — (DB는 형식 검증 안 함) BACK 정규식 검증 FRONT type="email"
DB 연결 실패 DB SQLException BACK 500 에러 + 로그 FRONT 500 전용 페이지
핵심 원칙 — "3중 방어": 중요한 검증은 Frontend(JavaScript), Backend(Java), DB(제약조건) 세 곳 모두에서 합니다. Frontend 검증은 UX용(즉각 피드백), Backend 검증은 보안용(프론트 우회 공격 방어), DB 검증은 최후 방어선이에요.

5일간 타임라인 시뮬레이터

각자의 작업이 5일간 어떻게 맞물리는지 따라가봅시다.

Frontend

화면 담당 · JSP / HTML / CSS
-

Backend

서버 로직 담당 · Servlet
-

Database

쿼리 담당 · SQL / MyBatis
-
HINT위 타임라인에서 DAY 1부터 순서대로 눌러보세요.

한 문장 요약

회원가입 하나를 만드는데 세 사람이 주고받는 건 단 3가지입니다: form 필드 명세(Front → Back), DTO 필드 목록(Back → DB), 완성된 쿼리(DB → Back). 이 세 개가 명확하면 나머지는 각자 가짜 데이터와 Simple Page로 혼자 진도를 뺄 수 있습니다. 이 한 가지 흐름만 몸에 익히면 로그인·게시판 같은 다른 기능들은 같은 패턴의 반복이에요.