카테고리 없음

[유레카 / 백엔드] TIL - 9 (Spring (feat. MyBatis))

coding-quokka101 2025. 11. 7. 17:58

 

스프링 부트 3 핵심 가이드 ❘ 장정우 - 교보문고

 

📌 들어가며

안녕하세요! 오늘은 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)

< 프로젝트 패키지 구조 >

 

각 계층의 역할

계층 역할 예시

DispatcherServlet Front Controller 패턴 Spring MVC의 핵심
Controller HTTP 요청 수신 및 응답 @PostMapping("/users/register")
Service 비즈니스 로직 처리 비밀번호 검증, 세션 생성
DAO DB CRUD 작업 @Mapper 인터페이스
DTO 계층 간 데이터 전달 UserDto, UserResultDto

⚙️ 3단계: Spring 설정 파일 구성

mybatis-config.xml - MyBatis 설정

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

  <settings>
    <setting name="mapUnderscoreToCamelCase" value="true"/>
  </settings>

  <mappers>
    <mapper resource="mapper/user-mapper.xml"/>
    <mapper resource="mapper/login-mapper.xml"/>
    <mapper resource="mapper/board-mapper.xml"/>
  </mappers>

</configuration>

< indx.html 홈 화면 >

💡 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가 자동으로 구현체를 생성해줘요!

import


당연히 사진 처럼 import해줘야합니다!

③ user-mapper.xml - MyBatis SQL 매핑

<mapper namespace="com.mycom.myapp.user.dao.UserDao">

    <insert id="registerUser" parameterType="com.mycom.myapp.user.dto.UserDto">
        INSERT INTO users (user_name, user_password, user_email, user_register_date)
        VALUES (#{userName}, #{userPassword}, #{userEmail}, NOW())
    </insert>

</mapper>

 

  • 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

URL 매핑은 정확해야 합니다. 하나만 틀려도 404 지옥에 빠져요! 😱


문제 2: MyBatis BindingException

증상

Caused by: org.apache.ibatis.binding.BindingException: 
Invalid bound statement (not found)

원인
mybatis-config.xml에 mapper 파일 미등록

해결

<configuration>
    <mappers>
        <mapper resource="mapper/user-mapper.xml"/>
        <mapper resource="mapper/login-mapper.xml"/>
    </mappers>
</configuration>

MyBatis는 mapper XML을 자동으로 찾지 않습니다. 반드시 설정 파일에 등록하세요!


문제 3: Fetch 요청 시 파라미터 누락

증상
서버에서 userName, userPassword 등이 null로 수신됨

원인
Spring이 URLSearchParams 객체를 제대로 파싱하지 못함

해결
URLSearchParams는 자동으로 application/x-www-form-urlencoded 타입으로 전송되므로 별도 헤더 설정 불필요. 하지만 명시적으로 추가하는 것이 더 안전합니다.

let fetchOptions = {
    method: "post",
    headers: {
        "Content-Type": "application/x-www-form-urlencoded"
    },
    body: urlParams
}

 


📊 핵심 개념 정리

1. Spring MVC vs Spring Boot 차이점

구분 Spring MVC Spring Boot

설정 방식 XML 기반 수동 설정 자동 설정 (Auto Configuration)
서버 외부 Tomcat 필요 내장 Tomcat
의존성 관리 개별 라이브러리 추가 Starter 의존성
배포 WAR 파일 JAR 파일 (독립 실행)

오늘 배운 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 문제인지, 컨트롤러 문제인지 구분이 안 됐습니다.

개선 계획

  1. 에러 로그를 꼼꼼히 읽는 습관 들이기
  2. 브라우저 개발자 도구 Network 탭 적극 활용
  3. 계층별로 System.out.println() 찍어가며 디버깅
  4. 내일은 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"로만 구분하는데, 실무에서는 더 구체적인 에러 코드가 필요할 것 같아요.

// 현재
map.put("result", "fail");

// 개선안
map.put("result", "fail");
map.put("errorCode", "INVALID_PASSWORD");
map.put("message", "비밀번호가 일치하지 않습니다.");

😢 테스트 코드 미작성
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의 동작 원리를 정말 깊이 있게 이해할 수 있었어요. 기초가 탄탄해야 나중에 어떤 프레임워크를 사용하더라도 흔들리지 않는다는 걸 느꼈습니다.

백엔드 개발자로 성장하는 여정, 아직 갈 길이 멀지만 오늘 또 한 걸음 나아갔네요! 💪

다음 포스팅에서는 게시판 CRUD 구현기로 찾아뵙겠습니다!