Spring

Spring Legacy 게시판 만들기 2편 - MVC 패턴 기반 CRUD 구현 (Controller, Service, Mapper)


📚 Spring Legacy 게시판 만들기 시리즈

1편에서 세팅을 마쳤으니, 이제 진짜 게시판의 뼈대를 만들어볼 차례예요! Spring에서는 코드를 역할별로 나누어서 관리하는데, 이것을 MVC 패턴이라고 해요.


🎭 MVC 패턴이란?

MVC는 Model, View, Controller의 약자예요. 마치 레스토랑처럼 각자 맡은 역할이 있어요!

graph LR
    subgraph "🍽️ 레스토랑"
        A["👨‍🍳 주방<br/>(Model)"]
        B["🧑‍💻 웨이터<br/>(Controller)"]
        C["📋 메뉴판<br/>(View)"]
    end
    
    D["👤 손님"] --> B
    B --> A
    A --> B
    B --> C
    C --> D
    
    style A fill:#ffeb3b
    style B fill:#4caf50,color:#fff
    style C fill:#2196f3,color:#fff
역할Spring에서하는 일레스토랑 비유
Controller@Controller요청을 받고 응답을 보냄웨이터 🧑‍💻
Service@Service실제 로직 처리주방장 👨‍🍳
MapperInterface데이터베이스와 대화창고 담당자 📦
DTOClass데이터를 담아 나름트레이 🍽️
ViewJSP화면에 보여줌메뉴판/접시 📋

📦 Step 1: DTO 만들기 - 데이터를 담는 그릇

DTO (Data Transfer Object) 는 데이터를 담아서 전달하는 역할을 해요.

package org.zerock.dto;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import lombok.*;

@Getter     // 📖 값을 읽는 메서드 자동 생성
@Setter     // ✏️ 값을 쓰는 메서드 자동 생성
@ToString   // 📝 내용을 문자열로 출력
@AllArgsConstructor  // 🏗️ 모든 필드를 받는 생성자
@NoArgsConstructor   // 🆕 빈 생성자
@Builder    // 🧩 빌더 패턴 사용 가능
public class BoardDTO {

    private Long bno;           // 글 번호
    private String title;       // 제목
    private String content;     // 내용
    private String writer;      // 작성자
    private LocalDateTime regDate;      // 등록일
    private LocalDateTime updateDate;   // 수정일
    private boolean delFlag;    // 삭제 여부
    
    // 날짜를 문자열로 변환 (yyyy-MM-dd 형식)
    public String getCreatedDate() {
        if (regDate == null) {
            return "";
        }
        return regDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    }
}

💡 Lombok이란? @Getter, @Setter 같은 어노테이션을 붙이면 getter/setter 메서드를 자동으로 만들어줘요. 코드가 훨씬 짧아지죠!


🗺️ Step 2: Mapper 만들기 - 데이터베이스와 대화하기

Mapper는 데이터베이스와 Java 코드 사이에서 통역사 역할을 해요.

Mapper 인터페이스 (Java)

package org.zerock.mapper;

import org.zerock.dto.BoardDTO;
import java.util.List;

public interface BoardMapper {
    // 📝 글 등록
    int insert(BoardDTO dto);
    
    // 📖 글 한 개 조회
    BoardDTO selectOne(long bno);
    
    // 🗑️ 글 삭제 (실제로는 삭제 표시만)
    int remove(long bno);
    
    // ✏️ 글 수정
    int update(BoardDTO dto);
    
    // 📋 글 목록 조회
    List<BoardDTO> list();
}

Mapper XML (SQL 정의)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.zerock.mapper.BoardMapper">

    <!-- 📝 글 등록 -->
    <insert id="insert">
        <selectKey keyProperty="bno" resultType="long" order="AFTER">
            SELECT LAST_INSERT_ID()
        </selectKey>
        INSERT INTO tbl_board (title, content, writer)
        VALUES (#{title}, #{content}, #{writer})
    </insert>

    <!-- 📖 글 한 개 조회 -->
    <select id="selectOne" resultType="org.zerock.dto.BoardDTO">
        SELECT * FROM tbl_board WHERE bno = #{bno}
    </select>

    <!-- 🗑️ 글 삭제 (soft delete) -->
    <update id="remove">
        UPDATE tbl_board SET delflag = true WHERE bno = #{bno}
    </update>

    <!-- ✏️ 글 수정 -->
    <update id="update">
        UPDATE tbl_board 
        SET title = #{title}, content = #{content}, 
            updatedate = NOW(), delflag = #{delFlag}
        WHERE bno = #{bno}
    </update>

    <!-- 📋 글 목록 (삭제되지 않은 것만) -->
    <select id="list" resultType="org.zerock.dto.BoardDTO">
        SELECT bno, title, writer, content, regdate
        FROM tbl_board
        WHERE delflag = false
        ORDER BY bno DESC
    </select>
</mapper>

📝 참고: #{ } 안에 적은 이름은 DTO의 필드명과 일치해야 해요! MyBatis가 자동으로 값을 넣어줍니다.


🔧 Step 3: Service 만들기 - 비즈니스 로직 처리

Service는 실제로 일을 처리하는 핵심 일꾼이에요.

package org.zerock.service;

import java.util.List;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.zerock.dto.BoardDTO;
import org.zerock.mapper.BoardMapper;

@Service  // 🏷️ "나는 서비스야!" 라고 Spring에게 알림
@Log4j2   // 📜 로그 기능 사용
@RequiredArgsConstructor  // 🔗 생성자 자동 주입
public class BoardService {

    private final BoardMapper boardMapper;

    // 📋 글 목록 조회
    public List<BoardDTO> getList() {
        return boardMapper.list();
    }

    // 📝 글 등록
    public Long register(BoardDTO dto) {
        int insertCount = boardMapper.insert(dto);
        log.info("insertCount : " + insertCount);
        return dto.getBno();  // 새로 생성된 글 번호 반환
    }

    // 📖 글 한 개 읽기
    public BoardDTO read(Long bno) {
        return boardMapper.selectOne(bno);
    }

    // 🗑️ 글 삭제
    public void remove(Long bno) {
        boardMapper.remove(bno);
    }

    // ✏️ 글 수정
    public void modify(BoardDTO dto) {
        boardMapper.update(dto);
    }
}

🎮 Step 4: Controller 만들기 - 요청 받고 응답 보내기

Controller는 사용자의 요청을 받는 첫 번째 관문이에요!

package org.zerock.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.zerock.dto.BoardDTO;
import org.zerock.service.BoardService;
import java.util.List;

@Controller  // 🏷️ "나는 컨트롤러야!" 라고 Spring에게 알림
@Log4j2
@RequestMapping("/board")  // 📍 /board로 시작하는 모든 요청 담당
@RequiredArgsConstructor
public class BoardController {

    private final BoardService boardService;

    // 📋 글 목록 페이지
    // GET /board/list → list.jsp
    @GetMapping("/list")
    public void list(Model model) {
        log.info("board list");
        
        List<BoardDTO> boardDtoList = boardService.getList();
        model.addAttribute("list", boardDtoList);
    }

    // ✏️ 글 쓰기 폼 페이지
    // GET /board/register → register.jsp
    @GetMapping("/register")
    public void register() {
        log.info("board register");
    }

    // 📝 글 등록 처리
    // POST /board/register → redirect /board/list
    @PostMapping("/register")
    public String registerPost(BoardDTO dto, RedirectAttributes rttr) {
        log.info("board register Post");
        
        Long bno = boardService.register(dto);
        rttr.addFlashAttribute("result", bno);
        
        return "redirect:/board/list";
    }
    
    // 📖 글 상세 보기
    // GET /board/read/1 → read.jsp
    @GetMapping("/read/{bno}")
    public String read(@PathVariable("bno") Long bno, Model model) {
        log.info("board read");
        
        BoardDTO dto = boardService.read(bno);
        model.addAttribute("board", dto);
        
        return "/board/read";
    }

    // 🔧 글 수정 폼 페이지
    // GET /board/modify/1 → modify.jsp
    @GetMapping("/modify/{bno}")
    public String modify(@PathVariable("bno") Long bno, Model model) {
        log.info("board modify");
        
        BoardDTO dto = boardService.read(bno);
        model.addAttribute("board", dto);
        
        return "/board/modify";
    }

    // ✏️ 글 수정 처리
    // POST /board/modify → redirect /board/read/번호
    @PostMapping("/modify")
    public String modifyPost(BoardDTO dto, RedirectAttributes rttr) {
        log.info("board modify Post");
        
        boardService.modify(dto);
        rttr.addFlashAttribute("result", dto.getBno());
        
        return "redirect:/board/read/" + dto.getBno();
    }
    
    // 🗑️ 글 삭제 처리
    // POST /board/remove → redirect /board/list
    @PostMapping("/remove")
    public String remove(@RequestParam("bno") Long bno, RedirectAttributes rttr) {
        log.info("board remove");
        
        boardService.remove(bno);
        rttr.addFlashAttribute("result", bno);
        
        return "redirect:/board/list";
    }
}

🔄 전체 흐름 이해하기

사용자가 글 목록을 볼 때 어떤 일이 일어나는지 살펴볼까요?

sequenceDiagram
    participant 👤 as 사용자
    participant 🎮 as Controller
    participant 🔧 as Service
    participant 🗺️ as Mapper
    participant 💾 as Database
    participant 📄 as JSP

    👤->>🎮: /board/list 요청
    🎮->>🔧: getList() 호출
    🔧->>🗺️: list() 호출
    🗺️->>💾: SELECT 쿼리 실행
    💾-->>🗺️: 결과 데이터
    🗺️-->>🔧: List<BoardDTO>
    🔧-->>🎮: List<BoardDTO>
    🎮->>📄: model에 데이터 담아서 전달
    📄-->>👤: HTML 화면 응답

계층별 역할 정리

graph TB
    subgraph "🎮 Controller Layer"
        A["BoardController<br/>@Controller"]
    end
    
    subgraph "🔧 Service Layer"
        B["BoardService<br/>@Service"]
    end
    
    subgraph "🗺️ Data Access Layer"
        C["BoardMapper<br/>Interface"]
        D["boardMapper.xml<br/>SQL"]
    end
    
    subgraph "📦 Domain"
        E["BoardDTO<br/>Data Object"]
    end
    
    A -->|"호출"| B
    B -->|"호출"| C
    C -->|"매핑"| D
    
    E -.->|"사용"| A
    E -.->|"사용"| B
    E -.->|"사용"| C
    
    style A fill:#4caf50,color:#fff
    style B fill:#ff9800,color:#fff
    style C fill:#2196f3,color:#fff
    style D fill:#9c27b0,color:#fff
    style E fill:#607d8b,color:#fff

🎯 URL과 HTTP 메서드 정리

기능HTTP 메서드URLController 메서드View
목록 조회GET/board/listlist()list.jsp
등록 폼GET/board/registerregister()register.jsp
등록 처리POST/board/registerregisterPost()redirect
상세 보기GET/board/read/{bno}read()read.jsp
수정 폼GET/board/modify/{bno}modify()modify.jsp
수정 처리POST/board/modifymodifyPost()redirect
삭제 처리POST/board/removeremove()redirect

🏷️ 어노테이션 총정리

mindmap
  root((Spring 어노테이션))
    Controller
      @Controller
      @RequestMapping
      @GetMapping
      @PostMapping
      @PathVariable
      @RequestParam
    Service
      @Service
      @RequiredArgsConstructor
    Mapper
      @Mapper 스캔
    공통
      @Log4j2
      @Getter/@Setter
      @ToString
어노테이션위치의미
@Controller클래스웹 요청을 처리하는 컨트롤러
@Service클래스비즈니스 로직을 처리하는 서비스
@RequestMapping클래스/메서드URL 경로 매핑
@GetMapping메서드GET 요청 처리
@PostMapping메서드POST 요청 처리
@PathVariable파라미터URL 경로에서 값 추출
@RequestParam파라미터요청 파라미터 값 추출
@RequiredArgsConstructor클래스final 필드 생성자 자동 생성

✅ 2편 정리

오늘 우리가 한 일을 체크해볼까요?

  • MVC 패턴 이해하기
  • DTO 클래스 만들기 (데이터 담기)
  • Mapper 인터페이스와 XML 만들기 (DB 연동)
  • Service 클래스 만들기 (로직 처리)
  • Controller 클래스 만들기 (요청 처리)
  • 전체 흐름 이해하기

🔮 다음 편 예고

3편에서는 실제로 **눈에 보이는 화면(JSP)**을 만들어볼 거예요!

  • 📋 글 목록 화면 만들기
  • ✏️ 글 작성 폼 만들기
  • 📖 글 상세 보기 화면 만들기
  • 🔧 글 수정/삭제 기능 구현

Bootstrap을 사용해서 멋진 디자인의 게시판을 완성할 거예요! 🎨


⚠️ 다음 편을 위한 준비물

  1. 1편에서 세팅한 프로젝트가 정상 작동하는지 확인
  2. Tomcat 서버가 실행되는지 확인
  3. MySQL 데이터베이스에 테스트 데이터를 미리 넣어두면 좋아요!
INSERT INTO tbl_board (title, content, writer) 
VALUES ('첫 번째 글', '안녕하세요!', '홍길동');