Backend 담당 — Servlet과 DTO로 데이터 다루기

이 페이지는 Backend 담당 학생을 위한 전용 가이드입니다. Frontend의 form 데이터를 받아서 DB로 흘려보내는 중간 다리, 그게 Backend의 역할이에요. 여기서는 그 다리를 만들기 위한 필수 도구 — DTOServlet — 을 처음부터 다룹니다. ← 협업 메인으로 돌아가기

한 줄 요약

Backend는 "데이터 번역가" 입니다.
HTTP 요청에 담긴 흩어진 문자열들 → 깔끔한 Java 객체(DTO) → DB 쿼리 호출. 이 세 단계의 변환을 책임지는 게 Backend의 진짜 일이에요. URL → JSP 연결은 부수적입니다.

먼저 알아야 할 것 — DTO란 무엇인가?

DTO = "데이터를 담는 그릇"

DTOData Transfer Object 의 줄임말이에요. 직역하면 "데이터 전달 객체", 쉽게 말하면 "DB 데이터를 온전히 받을 빈 그릇" 입니다.

왜 그릇이 필요할까요? 회원 한 명의 정보를 생각해보세요. 아이디, 비밀번호, 닉네임, 이메일, 연락처, 취미, 색깔, 직업… 흩어진 변수가 8개나 됩니다. 메서드 사이를 이동할 때 매번 이 8개를 따로따로 들고 다니면 매개변수가 8개인 끔찍한 함수가 됩니다:

// 그릇이 없으면 — 매개변수 지옥
public void insert(String userId, String pw, String nickname,
                   String email, String phone, String hobbies,
                   String favoriteColor, String job) {
  // ... 8개를 다 일일이 받아야 함
}

그래서 이 8개를 한 덩어리로 묶어서 들고 다니는 그릇이 필요합니다. 그게 DTO 클래스예요:

// 그릇이 있으면 — 한 덩어리
public void insert(Member member) {
  // member 안에 8개 필드가 다 들어있음
}

DTO의 모양 — 필드 + getter/setter 만으로 충분

DTO는 굉장히 단순한 클래스예요. 로직은 전혀 없고, 데이터를 담을 칸(필드)과 그 칸에 값을 넣고 빼는 메서드(setter / getter)만 있습니다. 이게 다입니다.

// src/main/java/com/edu/model/Member.java
package com.edu.model;

public class Member {

  // ① 필드 — DB 컬럼과 1:1 매칭
  private String userId;
  private String password;
  private String nickname;
  private String email;
  private String phone;
  private String hobbies;         // CSV: "독서,운동,영화"
  private String favoriteColor;
  private String job;

  // ② setter — 값을 그릇에 넣는 메서드
  public void setUserId(String userId)        { this.userId = userId; }
  public void setPassword(String password)    { this.password = password; }
  public void setNickname(String nickname)    { this.nickname = nickname; }
  public void setEmail(String email)          { this.email = email; }
  public void setPhone(String phone)          { this.phone = phone; }
  public void setHobbies(String hobbies)      { this.hobbies = hobbies; }
  public void setFavoriteColor(String c)     { this.favoriteColor = c; }
  public void setJob(String job)              { this.job = job; }

  // ③ getter — 그릇에서 값을 꺼내는 메서드
  public String getUserId()        { return userId; }
  public String getPassword()      { return password; }
  public String getNickname()      { return nickname; }
  public String getEmail()          { return email; }
  public String getPhone()          { return phone; }
  public String getHobbies()       { return hobbies; }
  public String getFavoriteColor() { return favoriteColor; }
  public String getJob()            { return job; }
}
Eclipse 팁: 필드만 작성한 다음 마우스 우클릭 → Source → Generate Getters and Setters... 를 선택하면 setter/getter 가 자동으로 만들어집니다. 손으로 16개 메서드를 다 타이핑할 필요 없어요.

DB 컬럼 ↔ DTO 필드 매핑 규칙

DTO의 필드 이름은 보통 DB 컬럼 이름과 1:1로 매칭되도록 짓습니다. 다만 명명 규칙(naming convention)이 다를 뿐이에요:

DB 컬럼 (스네이크 케이스)DTO 필드 (카멜 케이스)매핑 방법
user_id userId 스네이크 → 카멜 변환
password password 동일
nickname nickname 동일
favorite_color favoriteColor스네이크 → 카멜 변환
reg_date regDate 스네이크 → 카멜 변환

이 매핑은 MyBatis가 자동으로 해주지 않습니다. Day 1 회의에서 두 가지 방식 중 하나를 골라야 해요:

DTO를 어떻게 사용하는가 — 4단계

// ① 빈 그릇 만들기
Member newMember = new Member();

// ② 그릇에 값 채워넣기 (setter 사용)
newMember.setUserId("test_user");
newMember.setPassword("1234");
newMember.setNickname("테스트유저");
newMember.setEmail("test@example.com");
// ... 나머지 필드도

// ③ 그릇을 통째로 다른 메서드에 넘기기
memberDao.insert(newMember);   // 변수 8개 대신 그릇 1개

// ④ 다른 곳에서 그릇 안의 값 꺼내기 (getter 사용)
String name = newMember.getNickname();
System.out.println("안녕하세요 " + name + "님");
한 번 더 핵심: DTO는 로직이 없습니다. 계산하지 않고, 검증하지 않고, DB 호출하지 않아요. 그냥 데이터를 담고 운반하기만 합니다. 이 단순함이 DTO의 장점이에요. 여러 클래스가 같은 DTO를 안전하게 주고받을 수 있거든요.

DTO와 다른 클래스들 — 흔히 혼동하는 용어 정리

약어풀네임역할
DTOData Transfer Object데이터를 담는 그릇 (오늘 배운 거)
VO Value Object DTO와 거의 같음 (옛날 용어, 학원에 따라 혼용)
DAOData Access Object DB에 SQL을 실행하는 클래스 (MemberDao)
Servlet HTTP 요청을 받는 클래스 (RegisterServlet)

대부분의 한국 JSP 교재에서는 DTO와 VO를 같은 의미로 씁니다. 둘 다 "데이터 그릇" 이에요. DAO 는 다릅니다 — DAO는 그릇이 아니라 그릇을 들고 DB로 왔다 갔다 하는 "심부름꾼" 입니다. 헷갈리지 마세요.

Backend의 진짜 역할 — Controller가 하는 일

Backend

서버 로직 담당 · Servlet / DTO / 변수 받기

Controller의 진짜 역할 — "페이지 연결"이 아니라 "데이터 번역"

학생들이 가장 많이 오해하는 지점: "Controller는 URL에 맞는 JSP로 보내주는 거" — 반만 맞습니다. Controller의 80% 일은 받아온 데이터를 DB가 저장할 수 있는 형태로 가공해서 넘기는 것 이에요. JSP로 보내는 건 마지막 한 줄(sendRedirect)에 불과합니다.

회원가입 Controller 하나가 실제로 하는 일들:

  • ① 추출: form 파라미터 문자열들을 String userId = req.getParameter("userId") 로 변수에 담음
  • ② 검증: 빈 값 체크, 비번 일치 확인, 이메일 형식 등 "이 데이터 DB에 넣어도 되는가?"
  • ③ 조립: 여러 변수를 한 덩어리 DTO(Member 객체) 로 재조립 (HTTP → Java 객체 변환)
  • ④ 전달: memberDao.insert(newMember) 로 DAO에 그릇을 통째로 넘김
  • ⑤ 응답: 성공/실패에 따라 어느 JSP로 리다이렉트할지 결정 — 이게 "페이지 연결" 부분, 단 한 줄
핵심: Controller는 HTTP 세계(문자열 파라미터 덩어리) 와 DB 세계(정형화된 DTO 객체) 사이의 번역가 입니다. 이 번역을 Controller가 제대로 안 하면, 나중에 JSP 안에 스크립틀릿 로직이 떠다니거나 DAO가 HTTP 개념을 알아야 하는 기괴한 코드가 됩니다. "받아온 데이터를 어떻게 DTO에 담아서 DB에 넘길지"를 설계하는 것이 Backend 담당의 핵심 고민 이에요.

작업 순서 — "Frontend를 기다리지 않는다"

  1. Step 0 — DTO(Member.java) 작성: 위에서 본 그릇 클래스부터 만들기. 모든 작업의 출발점.
  2. Step 1 — Frontend에서 form 명세 받기: Day 1 회의에서 받은 필드 목록.
  3. Step 2 — 자기 전용 "Simple Page" 만들기: 꾸밈 0, 순수 HTML form 태그만 나열된 페이지. Frontend 완성본이 아니에요. Backend 본인이 Servlet을 테스트하기 위한 임시 페이지 입니다.
  4. Step 3 — RegisterServlet 작성: service 에서 req.getParameter 로 값들을 변수에 담기.
  5. Step 4 — 변수들을 DTO에 담고 콘솔 출력: Member 객체로 조립한 뒤 System.out.println 으로 확인. 여기까지가 DB 쿼리 실행 이전 의 Backend 책임.
  6. Step 5 — DB 담당에게 요청: "이 DTO를 member 테이블에 INSERT 하는 쿼리 필요해요"
  7. Step 6 — DB로부터 완성된 쿼리를 받아서 연결: Mapper XML에 넣고 Servlet에서 호출.

Step 2 · Backend 전용 Simple Page (꾸밈 0)

이 페이지는 Frontend가 만드는 예쁜 회원가입 화면과 다른 파일입니다. Backend가 자기 손에 쥐고 있는 테스트 도구예요. Tomcat을 띄우고 브라우저로 열어서 값 넣고 "가입" 눌러보기 용도. CSS는 아예 없고, 오직 form과 input 태그만. 새 필드들(checkbox·radio·select)도 모두 포함해서, 서버가 모든 입력 타입을 제대로 받는지 한 방에 확인할 수 있어요.

<%-- webapp/_backend_test/register_test.jsp --%>
<%-- ↑ 파일명에 _ 를 붙여서 "테스트용" 임을 표시, 나중에 지울 거 --%>
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>RegisterServlet 테스트</title></head>
<body>
  <h3>[Backend 전용] 회원가입 Servlet 테스트 페이지</h3>

  <form action="register" method="post">
    userId:          <input name="userId"><br>
    password:        <input name="password" type="password"><br>
    passwordConfirm: <input name="passwordConfirm" type="password"><br>
    nickname:        <input name="nickname"><br>
    email:           <input name="email"><br>
    phone:           <input name="phone"><br>
    hobbies:
      <label><input type="checkbox" name="hobbies" value="독서">독서</label>
      <label><input type="checkbox" name="hobbies" value="운동">운동</label>
      <label><input type="checkbox" name="hobbies" value="영화">영화</label><br>
    favoriteColor:
      <label><input type="radio" name="favoriteColor" value="빨강">빨강</label>
      <label><input type="radio" name="favoriteColor" value="파랑">파랑</label><br>
    job:
      <select name="job">
        <option value="학생">학생</option>
        <option value="회사원">회사원</option>
      </select><br><br>
    <button type="submit">전송</button>
  </form>
</body></html>
Backend 전용 Simple Page 스크린샷
Backend의 Simple Page · 꾸밈 0의 못생긴 form — Frontend 시안과 대비해보세요. 하지만 모든 입력 타입이 다 있어서 Servlet 검증엔 충분합니다
왜 이렇게 못생긴 페이지를 따로? Frontend의 join.jsp 가 아직 안 나왔을 수도 있고, 나왔더라도 CSS/JS 때문에 디버깅이 어려울 수도 있거든요. Backend는 "오직 값이 제대로 넘어오는가" 만 확인하면 되니까, 가장 단순한 form 한 장이 최고의 테스트 도구입니다.

Step 3-4 · RegisterServlet — 다양한 입력 타입을 받아 DTO에 담기

여기가 Controller의 "데이터 번역" 이 눈에 보이는 지점입니다. text/radio/select는 평범하게 getParameter 로 받지만, checkbox 복수 선택(hobbies)getParameterValues 로 받아서 String 배열이 돌아옵니다. 이걸 DTO에 담기 좋은 형태인 CSV 문자열("독서,운동,영화")로 번역하는 것도 Controller의 일이에요.

// src/main/java/com/edu/controller/RegisterServlet.java
package com.edu.controller;

import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;
import com.edu.model.Member;   // ← 우리가 만든 DTO

@WebServlet("/register")
public class RegisterServlet extends HttpServlet {

  @Override
  protected void service(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {

    req.setCharacterEncoding("UTF-8");   // 한글 깨짐 방지 (가장 먼저!)

    // ========== ① 단일 값 필드들 — getParameter ==========
    String userId          = req.getParameter("userId");
    String password        = req.getParameter("password");
    String passwordConfirm = req.getParameter("passwordConfirm");
    String nickname        = req.getParameter("nickname");
    String email           = req.getParameter("email");
    String phone           = req.getParameter("phone");
    String favoriteColor   = req.getParameter("favoriteColor");  // radio
    String job             = req.getParameter("job");            // select

    // ========== ② checkbox 복수 선택 — getParameterValues ==========
    String[] hobbiesArr = req.getParameterValues("hobbies");
    // ↑ 사용자가 아무것도 체크 안 하면 null 이 나옴. 주의!

    // ========== ③ String[] → CSV 문자열로 번역 ==========
    String hobbiesCsv;
    if (hobbiesArr == null || hobbiesArr.length == 0) {
      hobbiesCsv = "";                           // 아무것도 선택 안 함
    } else {
      hobbiesCsv = String.join(",", hobbiesArr);  // "독서,운동,영화"
    }

    // ========== ④ 받은 값 확인 (디버깅) ==========
    System.out.println("========= 회원가입 요청 ==========");
    System.out.println("userId        = " + userId);
    System.out.println("password      = " + password);
    System.out.println("nickname      = " + nickname);
    System.out.println("email         = " + email);
    System.out.println("phone         = " + phone);
    System.out.println("hobbies (raw) = " + java.util.Arrays.toString(hobbiesArr));
    System.out.println("hobbies (csv) = " + hobbiesCsv);
    System.out.println("favoriteColor = " + favoriteColor);
    System.out.println("job           = " + job);

    // ========== ⑤ 검증 — 비밀번호 일치 ==========
    if (!password.equals(passwordConfirm)) {
      resp.sendRedirect("join.jsp?error=pw_mismatch");
      return;
    }

    // ========== ⑥ DTO(Member) 그릇에 담기 ==========
    Member newMember = new Member();          // 빈 그릇
    newMember.setUserId(userId);              // 칸 채우기
    newMember.setPassword(password);
    newMember.setNickname(nickname);
    newMember.setEmail(email);
    newMember.setPhone(phone);
    newMember.setHobbies(hobbiesCsv);          // CSV로 번역된 값
    newMember.setFavoriteColor(favoriteColor);
    newMember.setJob(job);
    // ↑ 이제 newMember 안에 회원 정보 8개가 다 들어있음

    // ========== ⑦ DAO에 그릇 통째로 넘기기 ==========
    // 아래 한 줄은 DB 담당이 쿼리를 줄 때까지 주석처리
    // int result = memberDao.insert(newMember);

    resp.sendRedirect("join_success.jsp");
  }
}
Controller의 "번역" 이 일어나는 네 지점을 주목:
HTTP 문자열 파라미터 → Java String 변수 (getParameter)
HTTP의 반복된 파라미터 → Java String 배열 (getParameterValues)
배열 → CSV 문자열 (String.join)
흩어진 String 변수들 → 한 덩어리 DTO 객체 (그릇에 담기)
이 4가지 번역이 끝나면 DAO에게 그릇 하나만 넘기면 됩니다. 깔끔하죠?
서버 콘솔 출력 (성공 케이스)
Eclipse Console 출력 · 서버가 실제로 받은 값들 — hobbies (raw) 는 배열이었지만 hobbies (csv) 로 번역됐음

getParameter vs getParameterValues — 비교표

메서드사용 시점리턴 타입값이 없을 때
getParameter("x") text, password, email, radio, select, 단일 checkbox String null (파라미터 없을 때) 또는 "" (빈 값)
getParameterValues("x") checkbox 복수 선택, 같은 name의 input이 여러 개 String[] null (아무것도 선택 안 함) — 반드시 null 체크!
흔한 실수: checkbox에 getParameter("hobbies") 를 쓰면 첫 번째 체크된 값 하나만 돌아옵니다. 나머지는 조용히 버려져요. 복수 선택 checkbox는 반드시 getParameterValues.

Step 5 · DB 담당에게 넘기는 요청 (문서 또는 메신저)

// DB 담당에게 보낼 메모
[INSERT 쿼리 부탁드립니다]
테이블: member
DTO 클래스: com.edu.model.Member (이 그릇을 통째로 받아주세요)

DTO 필드 → DB 컬럼 매핑:
  - userId        → user_id
  - password      → password
  - nickname      → nickname
  - email         → email
  - phone         → phone
  - hobbies       → hobbies         (CSV 문자열)
  - favoriteColor → favorite_color
  - job           → job
  - reg_date      → DB가 자동 (DEFAULT CURRENT_TIMESTAMP)

리턴은 영향 받은 행 수(int) 로 주세요.

Step 6 · DB가 쿼리 주면 한 줄 바꾸기

아까 주석처리했던 memberDao.insert(newMember) 줄의 주석만 풀면 됩니다. 그 외 코드는 수정 없음.

Backend가 절대 하면 안 되는 것

1. DTO 안에 비즈니스 로직 넣기. DTO는 그릇이에요. 검증·계산·DB 호출은 Controller나 다른 곳에서 합니다.
2. Simple Page를 실제 배포 파일로 쓰기. _backend_test/ 폴더는 Day 5 통합 테스트 후 삭제합니다. 프로덕션엔 Frontend의 예쁜 join.jsp 만 남습니다.
3. Servlet 안에 SQL 직접 쓰기. SQL은 DB 담당의 영역. Servlet은 변수 받아서 DTO에 담고 DAO에 넘기기만.
4. 파라미터 null 체크 없이 바로 사용. form에서 빈 값이 오면 "" 이 아니라 null일 수도 있어요. NPE 주의.

다른 역할의 가이드 보기