안녕하세요! 오늘은 Spring Framework와 MyBatis를 활용한 회원가입 및 로그인 시스템을 구현했습니다. 단순히 코드를 따라 치는 것이 아니라, 웹 애플리케이션의 인증 시스템이 어떻게 동작하는지 제대로 이해하는 시간이었어요.
🎯 Today I Learned
📚 학습 내용 요약
✅ Spring MVC 계층형 아키텍처 이해 (Controller-Service-DAO)
✅ MyBatis XML Mapper를 활용한 DB 연동
✅ RESTful API 설계 및 구현
✅ Fetch API를 이용한 비동기 통신
✅ 클라이언트/서버 사이드 유효성 검사
✅ Optional을 활용한 예외 처리
✅ Session 기반 인증 구현
사용 기술 스택
Backend: Spring Framework (STS), MyBatis
Frontend: JSP, Bootstrap 5, Vanilla JavaScript
Database: MySQL 8.0
Tools: Eclipse/STS, MySQL Workbench, Postman
💾 1단계: 데이터베이스 설계
모든 개발의 시작은 데이터 모델링입니다. 오늘은 사용자 정보를 저장할 users 테이블을 설계했어요.
테이블 구조
CREATE TABLE users (
user_seq INT NOT NULL AUTO_INCREMENT,
user_name VARCHAR(100) NOT NULL,
user_password VARCHAR(50) NOT NULL,
user_email VARCHAR(100) NOT NULL,
user_profile_image VARCHAR(500) DEFAULT 'noProfile.png',
user_register_date DATE DEFAULT NULL,
PRIMARY KEY (user_seq),
UNIQUE KEY user_email_unique (user_email)
);
< board - Table - user 테이블 구조 >
💡 설계 포인트
컬럼명 설명 비고
user_seq
사용자 고유 번호
AUTO_INCREMENT로 자동 생성
user_email
이메일 (로그인 ID)
UNIQUE 제약조건으로 중복 방지
user_password
비밀번호
실무에서는 암호화 필수!
user_profile_image
프로필 이미지 경로
기본값 설정
📍 Key Point user_email에 UNIQUE 제약조건을 걸어 중복 가입을 DB 레벨에서 차단합니다. 이는 애플리케이션 로직보다 더 강력한 보호장치예요!
🏗️ 2단계: Spring MVC 아키텍처 이해
계층형 아키텍처 (Layered Architecture)
Spring Framework는 관심사의 분리(Separation of Concerns) 원칙에 따라 계층을 나눕니다.
📱 Client (Browser)
↓ HTTP Request
📮 DispatcherServlet
↓
📮 Controller Layer ← 요청/응답 처리
↓
🎯 Service Layer ← 비즈니스 로직
↓
💾 DAO Layer ← 데이터베이스 접근
↓
🗄️ Database (MySQL)
💡 TIP mapUnderscoreToCamelCase 설정을 true로 하면 DB의 user_name이 자동으로 Java의 userName으로 매핑돼요!
< Spring 설정 파일 구조 >
💻 4단계: 회원가입 기능 구현
4-1. 프론트엔드 - 유효성 검사
회원가입 폼에서는 클라이언트 사이드 검증이 필수입니다. 서버 부하를 줄이고 사용자 경험을 개선할 수 있어요.
< 회원가입 폼 UI 스크린샷 >< 유효성 검사 불일치 회원가입 화면 >< 유효성 검사 일치 회원가입 화면 >
유효성 검사 규칙
// 1. 이름: 2글자 이상
function validateUserName(userName) {
return userName.length >= 2;
}
사용자 이름이 최소 2글자 이상인지 검증합니다. length 속성으로 문자열 길이를 확인하고, 조건을 만족하면 true를 반환합니다.
// 2. 비밀번호: 영문자 + 숫자 + 특수문자 각 1개 이상
function validateUserPassword(userPassword) {
let patternEngAtListOne = new RegExp(/[a-zA-Z]+/);
let patternSpeAtListOne = new RegExp(/[~!@#$%^&*()_+|<>?:{}]+/);
let patternNumAtListOne = new RegExp(/[0-9]+/);
return patternEngAtListOne.test(userPassword) &&
patternSpeAtListOne.test(userPassword) &&
patternNumAtListOne.test(userPassword);
}
new RegExp(): 정규표현식 객체를 생성합니다
/[a-zA-Z]+/: 영문자(대소문자) 1개 이상 포함 여부 검사
/[~!@#$%^&*()_+|<>?:{}]+/: 특수문자 1개 이상 포함 여부 검사
/[0-9]+/: 숫자 1개 이상 포함 여부 검사
test(): 문자열이 패턴과 일치하는지 검사
&& 연산자로 세 조건을 모두 만족해야 true 반환
// 3. 이메일: 이메일 형식 검증
function validateUserEmail(userEmail) {
let regexp = /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i;
return regexp.test(userEmail);
}
^: 문자열의 시작
[0-9a-zA-Z]: 첫 글자는 숫자나 영문자
([-_.]?[0-9a-zA-Z])*: -, _, . 다음에 영문자/숫자가 0회 이상 반복
@: @ 기호 필수
[a-zA-Z]{2,3}: 도메인 확장자는 영문자 2~3글자 (com, net, org 등)
$: 문자열의 끝
i: 대소문자 구분 안 함
< mysql 등록>
회원가입 성공시 이렇게 mysql에 데이터가 등록 됩니다!
Fetch API를 이용한 비동기 통신
async function register() {
let urlParams = new URLSearchParams({
userName: document.querySelector("#userName").value,
userPassword: document.querySelector("#userPassword").value,
userEmail: document.querySelector("#userEmail").value
});
let response = await fetch("/users/register", {
method: "post",
body: urlParams
});
let data = await response.json();
if(data.result == "success") {
alert("회원 가입 성공! 로그인 페이지로 이동합니다.");
window.location.href = "/pages/login";
}
}
async/await: 비동기 처리를 동기 코드처럼 작성하는 문법
URLSearchParams: form-urlencoded 형식으로 데이터 변환 (userName=홍길동&userPassword=abc123)
fetch(): 서버에 HTTP 요청을 보내는 API
await response.json(): 서버 응답을 JSON으로 파싱할 때까지 대기
window.location.href: 페이지 이동 (JavaScript의 GET 방식 이동)
💡 TIP Bootstrap 5의 is-valid / is-invalid 클래스를 활용하면 실시간 유효성 검사 피드백을 깔끔하게 구현할 수 있어요!
4-2. 백엔드 - 계층별 구현
① UserDto - 데이터 전송 객체
public class UserDto {
private int userSeq;
private String userName;
private String userPassword;
private String userEmail;
private String userProfileImage;
private Date userRegisterDate;
// getter, setter, toString 생략
}
DTO (Data Transfer Object): 계층 간 데이터 전달을 위한 객체
DB 테이블의 컬럼과 1:1 매핑되는 필드들
private 접근 제한자로 캡슐화, getter/setter로 접근
비즈니스 로직은 포함하지 않고 순수하게 데이터만 담음
② UserDao - 데이터베이스 인터페이스
@Mapper
public interface UserDao {
int registerUser(UserDto userDto);
}
MyBatis의 @Mapper 어노테이션을 사용하면 인터페이스만 정의해도 MyBatis가 자동으로 구현체를 생성해줘요!
namespace: 어느 DAO 인터페이스와 연결할지 지정 (패키지 경로 포함 전체 경로)
id="registerUser": DAO 인터페이스의 메서드명과 정확히 일치해야 함
parameterType: 파라미터로 전달받을 객체의 타입
#{userName}: UserDto 객체의 getUserName() 메서드를 자동 호출
NOW(): MySQL 함수로 현재 시간 자동 입력
MyBatis가 자동으로 PreparedStatement 생성하여 SQL Injection 방지
④ UserService - 비즈니스 로직
@Service
public class UserServiceImpl implements UserService {
private final UserDao userDao;
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
@Override
public UserResultDto registerUser(UserDto userDto) {
UserResultDto resultDto = new UserResultDto();
int result = userDao.registerUser(userDto);
if(result == 1) {
resultDto.setResult("success");
} else {
resultDto.setResult("fail");
}
return resultDto;
}
}
@Service: Spring이 이 클래스를 Service Bean으로 등록
생성자 주입 방식: Spring이 자동으로 UserDao 구현체를 주입 (DI)
final 키워드: 한 번 주입된 의존성은 변경 불가 (불변성)
int result: DAO의 반환값 (1이면 성공, 0이면 실패)
비즈니스 로직 처리: DB 결과를 표준 응답 형식으로 변환
Service 계층에서 트랜잭션 관리나 복잡한 로직 처리
⑤ UserController - HTTP 요청 처리
@Controller
@ResponseBody
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping("/register")
public Map<String, String> registerUser(UserDto dto) {
Map<String, String> map = new HashMap<>();
UserResultDto resultDto = userService.registerUser(dto);
map.put("result", resultDto.getResult());
return map;
}
}
@Controller: Spring MVC의 Controller로 등록
@ResponseBody: 반환값을 HTTP Response Body에 직접 작성 (JSON으로 자동 변환)
@RequestMapping("/users"): 이 컨트롤러의 기본 URL 경로
@PostMapping("/register"): HTTP POST 요청을 /users/register로 받음
UserDto dto: Spring이 요청 파라미터를 자동으로 DTO에 바인딩 (userName=값&userPassword=값...)
Map<String, String>: JSON 응답 형태 {"result": "success"}
Jackson 라이브러리가 Map을 자동으로 JSON으로 변환
< postman 사용한 post 결과 출력 >
< DB 등록>
Send를 하게 되면 이렇게 mysql workbench에서도 데이터가 등록된 것을 확인해 볼 수 있습니다!
🔐 5단계: 로그인 기능 구현
로그인 플로우
1️⃣ 사용자 이메일/비밀번호 입력
2️⃣ 서버에서 이메일로 사용자 조회
3️⃣ 비밀번호 일치 여부 확인
4️⃣ 성공 시 Session에 사용자 정보 저장
5️⃣ 게시판 페이지로 리다이렉트
< 로그인 화면 스크린샷 >
Optional을 활용한 null 처리
Java 8의 Optional은 null 처리를 함수형 스타일로 깔끔하게 만들어줍니다.
LoginService 구현
@Service
public class LoginServiceImpl implements LoginService {
private final LoginDao loginDao;
public LoginServiceImpl(LoginDao loginDao) {
this.loginDao = loginDao;
}
@Override
public Optional<UserDto> login(UserDto userDto) {
// 1. 이메일로 사용자 조회
UserDto dto = loginDao.login(userDto.getUserEmail());
// 2. 비밀번호 검증
if(dto != null && userDto.getUserPassword().equals(dto.getUserPassword())) {
dto.setUserPassword(null); // 보안: 비밀번호 제거
return Optional.of(dto); // 성공
}
return Optional.empty(); // 실패
}
}
Optional 반환: null 대신 Optional로 감싸서 안전하게 처리
loginDao.login(): 이메일로 사용자 정보 조회 (비밀번호 포함)
equals(): 입력한 비밀번호와 DB 비밀번호 비교
dto.setUserPassword(null): 비밀번호는 세션에 저장하지 않음 (보안)
Optional.of(dto): 값이 있는 Optional 객체 생성
Optional.empty(): 빈 Optional 객체 생성 (로그인 실패)
LoginController - ifPresentOrElse 활용
@Controller
@ResponseBody
@RequestMapping("/auth")
public class LoginController {
private final LoginService loginService;
public LoginController(LoginService loginService) {
this.loginService = loginService;
}
@PostMapping("/login")
public Map<String, String> login(UserDto dto, HttpSession session) {
Map<String, String> map = new HashMap<>();
Optional<UserDto> optional = loginService.login(dto);
// Optional의 ifPresentOrElse로 간결하게 처리
optional.ifPresentOrElse(
userDto -> {
session.setAttribute("userDto", userDto);
map.put("result", "success");
},
() -> {
map.put("result", "fail");
}
);
return map;
}
}
HttpSession session: Spring이 자동으로 세션 객체 주입
Optional<UserDto>: null일 수도 있는 UserDto를 안전하게 감싼 컨테이너
ifPresentOrElse(): 함수형 프로그래밍 스타일
첫 번째 람다: 값이 있을 때 실행 (로그인 성공)
두 번째 람다: 값이 없을 때 실행 (로그인 실패)
session.setAttribute("userDto", userDto): 세션에 사용자 정보 저장
세션: 서버 메모리에 저장되며, 브라우저 종료 전까지 유지
람다식 ->: Java 8의 함수형 표현식
login-mapper.xml
https://mybatis.org/dtd/mybatis-3-mapper.dtd">
SELECT user_seq, user_name, user_password, user_email,
user_profile_image, user_register_date
FROM users
WHERE user_email = #{user_email}
📖 Optional이란? null 값을 안전하게 다루기 위한 컨테이너 클래스입니다. NullPointerException을 방지하고 함수형 프로그래밍 스타일로 코드를 작성할 수 있습니다!
로그인 성공 후 게시판 페이지
아직 board.jsp 부분은 완성을 못했지만 이렇게 로그인을 성공하면 board.jsp 화면으로 넘어가게 됩니다.
🎨 6단계: 페이지 라우팅 처리
PageController - 페이지 이동 관리
@Controller
public class PageController {
@GetMapping("/pages/register")
public String register() {
return "register"; // register.jsp로 이동
}
@GetMapping("/pages/login")
public String login() {
return "login"; // login.jsp로 이동
}
@GetMapping("/pages/board")
public String board() {
return "board"; // board.jsp로 이동
}
}
Spring MVC에서는 ViewResolver가 자동으로 .jsp 확장자를 붙여서 JSP 파일을 찾아줍니다!
🐛 트러블슈팅
문제 1: 404 Not Found 에러
증상
POST http://localhost:8080/users/register → 404
원인 @RequestMapping과 @PostMapping의 경로 조합 실수
해결
@RequestMapping("/users") // 기본 경로
@PostMapping("/register") // 실제: /users/register
오늘 배운 Spring MVC는 전통적인 방식으로, 설정이 복잡하지만 동작 원리를 이해하기에 더 좋아요!
2. 계층형 아키텍처의 장점
장점 설명
유지보수성
각 계층이 독립적이라 수정이 용이
테스트 용이성
계층별 단위 테스트 가능
재사용성
Service 로직을 여러 Controller에서 재사용
확장성
새로운 기능 추가 시 해당 계층만 수정
3. 프론트엔드 vs 백엔드 유효성 검사
🖥️ 프론트엔드 검사
✓ 사용자 경험(UX) 향상
✓ 즉각적인 피드백
✓ 불필요한 서버 요청 감소
🔒 백엔드 검사
✓ 보안의 최종 방어선
✓ 악의적인 요청 차단
✓ 데이터 무결성 보장
⚠️ 중요 프론트엔드 검사는 우회 가능합니다. 백엔드 검사는 필수입니다!
4. RESTful API 설계 원칙
POST /users/register ← 회원가입 (리소스 생성)
POST /auth/login ← 로그인 (인증)
GET /users/{id} ← 회원 정보 조회
PUT /users/{id} ← 회원 정보 수정
DELETE /users/{id} ← 회원 탈퇴
💪 오늘 나는 무엇을 잘했는가 (성취)
✅ 처음부터 끝까지 스스로 구현 완료 선생님 코드를 그대로 따라 치는 게 아니라, 흐름을 이해하고 나만의 방식으로 구현했습니다. 특히 에러가 발생했을 때 스스로 해결책을 찾으려고 노력했어요.
✅ Postman으로 API 검증 습관화 프론트엔드를 만들기 전에 Postman으로 백엔드가 제대로 동작하는지 먼저 확인하는 습관을 들였습니다. 이게 디버깅 시간을 엄청 단축시켜주더라고요!
✅ Optional 문법 완벽 이해 Java 8의 Optional이 왜 필요한지, 어떻게 사용하는지 제대로 이해했습니다. 특히 ifPresentOrElse()는 정말 우아한 문법이에요!
🔧 어떤 문제를 겪었고, 어떻게 개선할까 (개선점)
문제: 에러 발생 시 원인 파악이 느렸다
처음에는 에러가 나면 "뭐가 문제지?"하고 막막했어요. 특히 404 에러가 나면 URL 문제인지, 컨트롤러 문제인지 구분이 안 됐습니다.
개선 계획
에러 로그를 꼼꼼히 읽는 습관 들이기
브라우저 개발자 도구 Network 탭 적극 활용
계층별로 System.out.println() 찍어가며 디버깅
내일은 Eclipse의 디버거 사용법을 공부해볼 예정
📚 오늘 배운 것 (학습)
Spring의 DI (Dependency Injection)
public class UserController {
private final UserService userService;
// 생성자 주입 방식 (권장)
public UserController(UserService userService) {
this.userService = userService;
}
}
Spring이 자동으로 UserService 구현체를 찾아서 주입해줍니다. 이게 바로 **제어의 역전(IoC)**이에요!
MyBatis의 #{} vs ${}
구분 #{} ${}
처리 방식
PreparedStatement
String 치환
SQL Injection
안전 ✅
위험 ⚠️
사용 시점
값 바인딩
테이블명, 컬럼명
<!-- 안전한 방식 -->
WHERE user_email = #{userEmail}
<!-- 위험한 방식 (절대 값에 사용 금지!) -->
WHERE user_email = ${userEmail}
😊 좋았던 점 & 😅 아쉬웠던 점
좋았던 점
👍 실무 기술을 직접 체험 회원가입/로그인은 거의 모든 서비스의 기본이에요. 이걸 직접 구현하니 웹 개발의 큰 그림이 보이기 시작했습니다!
👍 계층 분리의 중요성 체감 처음엔 "왜 이렇게 복잡하게 나누지?"라고 생각했는데, 직접 해보니 코드가 오히려 깔끔하고 유지보수가 쉽더라고요.
👍 Modern Java 문법 습득 Optional, 람다식, Stream API 등 Java 8+ 문법을 실전에서 사용해보니 이해가 확 됐어요!
👍 Spring MVC의 동작 원리 이해 Spring Boot보다 설정이 복잡하지만, 덕분에 DispatcherServlet, ViewResolver 등의 동작 원리를 제대로 이해할 수 있었어요!
아쉬웠던 점
😢 비밀번호 평문 저장 실무에서는 절대 안 되는 방식인데, 학습용이라 일단 평문으로 저장했어요. 다음에는 Spring Security의 BCryptPasswordEncoder를 사용해보고 싶어요!
😢 예외 처리 부족 지금은 단순히 "success" / "fail"로만 구분하는데, 실무에서는 더 구체적인 에러 코드가 필요할 것 같아요.
😢 테스트 코드 미작성 JUnit으로 단위 테스트를 작성하면 훨씬 안정적인 코드가 될 텐데, 아직 배우지 못해서 아쉬워요!
🎓 나만의 학습 팁
1. 흐름도 그리기
코드 작성 전에 종이에 데이터 흐름을 그려보세요!
📱 사용자 입력
↓
🌐 Fetch API
↓
🎯 Controller
↓
💼 Service (비밀번호 검증)
↓
💾 DAO
↓
🗄️ DB 조회
이렇게 시각화하면 "지금 어디서 막혔지?"를 바로 파악할 수 있어요!
2. Postman 먼저, JSP는 나중에
프론트엔드를 만들기 전에 Postman으로 API부터 검증하세요. 문제가 프론트엔드에 있는지 백엔드에 있는지 명확하게 구분할 수 있습니다.
3. Git 커밋 전략
✅ feat: 회원가입 API 구현
✅ feat: 로그인 세션 처리 추가
✅ fix: MyBatis mapper 경로 수정
✅ refactor: Optional 적용하여 코드 개선
기능 단위로 커밋하면 나중에 문제가 생겼을 때 되돌리기도 쉽고, 내가 뭘 했는지 한눈에 보여요!
🚀 다음 학습 목표
📌 단기 목표 (이번 주)
✓ 게시판 CRUD 기능 구현
✓ 파일 업로드 기능 추가
✓ 페이징 처리 학습
📌 중기 목표 (이번 달)
✓ Spring Security 적용
✓ JWT 토큰 기반 인증 학습
✓ JUnit 단위 테스트 작성
📌 장기 목표 (부트캠프 기간)
✓ Spring Boot로 마이그레이션
✓ AWS 배포 경험
✓ 포트폴리오 프로젝트 3개 이상
🎬 마치며
처음에는 "이렇게 복잡한 걸 내가 할 수 있을까?" 걱정했지만, 한 단계씩 차근차근 따라가니 완성할 수 있었습니다.
특히 Postman에서 200 OK와 함께 {"result": "success"}가 떴을 때의 그 희열! 🎉 수없이 마주한 에러들, 그리고 그걸 하나하나 해결해나가는 과정이 정말 값진 경험이었어요.
"복잡해 보이는 것도 작은 단계로 쪼개면 해결할 수 있다!"
이게 오늘의 가장 큰 깨달음입니다.
Spring MVC는 Spring Boot보다 설정이 복잡하지만, 덕분에 Spring의 동작 원리를 정말 깊이 있게 이해할 수 있었어요. 기초가 탄탄해야 나중에 어떤 프레임워크를 사용하더라도 흔들리지 않는다는 걸 느꼈습니다.