Spring MVC 게시판 만들기 3편: 서비스 계층과 비즈니스 로직의 모든 것
💡 이 글에서 배울 것
Controller가 직접 DB에 접근하면 안 되는 이유와, Service 계층이 어떤 “진짜 일”을 하는지 파헤쳐봅니다.
🎯 한눈에 보기
| 주제 | 핵심 내용 |
|---|---|
| @Service | 비즈니스 로직을 처리하는 계층 |
| DI | Spring이 객체를 자동으로 주입 |
| 페이징 | skip = (page - 1) × size |
📌 이번 편에서 배울 것
이번 편에서는 “진짜 일”을 하는 Service 계층을 파헤쳐 봅니다:
@Service의 역할과 의미- 의존성 주입(DI)이란?
- 페이징 계산 로직
- 검색 조건 처리
🤔 왜 Service 계층이 필요할까?
Controller가 직접 DB에 접근하면 되지 않을까요? 안됩니다!
flowchart LR
subgraph "❌ 나쁜 구조"
C1[Controller] --> D1[(DB)]
end
subgraph "✅ 좋은 구조"
C2[Controller] --> S[Service] --> M[Mapper] --> D2[(DB)]
end
계층 분리의 장점:
| 장점 | 설명 |
|---|---|
| 재사용성 | 같은 로직을 여러 Controller에서 사용 가능 |
| 테스트 용이 | Service만 따로 테스트 가능 |
| 유지보수 | 로직 변경 시 Service만 수정 |
| 책임 분리 | 각 계층이 하나의 역할만 담당 |
📦 BoardService 구조
@Service // "나는 비즈니스 로직을 처리하는 클래스야!"
@Log4j2 // 로그 출력
@RequiredArgsConstructor // final 필드의 생성자 자동 생성
public class BoardService {
private final BoardMapper boardMapper; // DB 접근용 Mapper
// CRUD 메서드들...
}
💉 의존성 주입(DI) 이해하기

BoardService는 BoardMapper가 필요합니다. 이걸 “의존한다”고 표현해요.
전통적인 방식 (🚫 사용하면 안됨):
public class BoardService {
// 직접 객체 생성 → 문제 발생!
private BoardMapper boardMapper = new BoardMapperImpl();
}
Spring 방식 (✅ DI 사용):
@RequiredArgsConstructor
public class BoardService {
// Spring이 알아서 넣어줌!
private final BoardMapper boardMapper;
}
flowchart TB
Spring[🌱 Spring Container] --> |"BoardMapper를 넣어줄게!"| Service
Service[BoardService] --> |"감사합니다!"| Mapper[BoardMapper]
style Spring fill:#c8e6c9
📋 게시글 목록 조회 (핵심!)
getList() 메서드는 검색 + 페이징을 한 번에 처리합니다.
public BoardListPaginDTO getList(int page, int size,
String typeStr, String keyword) {
// 1️⃣ 페이지 검증 (1 미만이면 1로)
page = page <= 0 ? 1 : page;
// 2️⃣ 사이즈 검증 (10~100 사이만 허용)
size = (size <= 10 || page > 100) ? 10 : size;
// 3️⃣ skip 계산 (몇 개를 건너뛸지)
int skip = (page - 1) * size;
// 4️⃣ 검색 조건 분해 ("TC" → ["T", "C"])
String[] types = parseTypes(typeStr);
// 5️⃣ DB에서 데이터 조회
List<BoardDTO> list = boardMapper.listSearch(skip, size, types, keyword);
int total = boardMapper.listCountSearch(types, keyword);
// 6️⃣ 페이징 DTO 생성 후 반환
return new BoardListPaginDTO(list, total, page, size, typeStr, keyword);
}
🔢 Skip 계산 이해하기
100개의 게시글이 있고, 한 페이지에 10개씩 보여준다면:
📄 1페이지: 100 ~ 91번 (최신순) → skip = 0
📄 2페이지: 90 ~ 81번 → skip = 10
📄 3페이지: 80 ~ 71번 → skip = 20
📄 4페이지: 70 ~ 61번 → skip = 30
📄 5페이지: 60 ~ 51번 → skip = 40
공식: skip = (page - 1) × size
flowchart LR
subgraph "1페이지 요청"
P1["page=1, size=10"] --> S1["skip = (1-1)×10 = 0"]
end
subgraph "3페이지 요청"
P3["page=3, size=10"] --> S3["skip = (3-1)×10 = 20"]
end
🔍 검색 조건 분해
사용자가 “제목 + 내용”으로 검색하면 types = "TC"가 넘어옵니다. 이걸 배열로 변환해야 해요.
// typeStr = "TC" → ["T", "C"]로 변환
String[] types = null;
if (typeStr != null && !typeStr.trim().isEmpty()) {
types = new String[typeStr.length()];
for (int i = 0; i < typeStr.length(); i++) {
types[i] = String.valueOf(typeStr.charAt(i));
}
}
| 입력 | 변환 결과 | 의미 |
|---|---|---|
"T" | ["T"] | 제목만 검색 |
"C" | ["C"] | 내용만 검색 |
"W" | ["W"] | 작성자만 검색 |
"TC" | ["T", "C"] | 제목 OR 내용 |
"TCW" | ["T", "C", "W"] | 제목 OR 내용 OR 작성자 |
📝 게시글 등록
public Long register(BoardDTO dto) {
// 1. DB에 삽입
int insertCount = boardMapper.insert(dto);
log.info("insertCount : " + insertCount);
// 2. 삽입된 글 번호 반환 (MyBatis가 자동으로 dto.bno에 설정)
return dto.getBno();
}
💡 Tip:
insert(dto)실행 후dto.getBno()로 새 글 번호를 알 수 있는 이유는 MyBatis의selectKey때문입니다! (4편에서 자세히 다룹니다)
👀 게시글 조회 / ✏️ 수정 / 🗑️ 삭제
나머지 CRUD는 간단합니다:
// 조회
public BoardDTO read(Long bno) {
return boardMapper.selectOne(bno);
}
// 삭제 (실제로는 delflag = true로 변경)
public void remove(Long bno) {
boardMapper.remove(bno);
}
// 수정
public void modify(BoardDTO dto) {
boardMapper.update(dto);
}
📊 BoardListPaginDTO - 페이징 계산의 핵심
이 DTO는 목록 데이터 + 페이징 정보를 함께 담습니다.
@Data
public class BoardListPaginDTO {
private List<BoardDTO> boardDTOList; // 게시글 목록
private int totalCount; // 전체 게시글 수
private int page, size; // 현재 페이지, 페이지당 개수
private int start, end; // 페이지 버튼 시작~끝
private boolean prev, next; // 이전/다음 버튼 표시
private List<Integer> pageNums; // [1,2,3,4,5,6,7,8,9,10]
private String types, keyword; // 검색 조건
}
페이지 블록 계산:
flowchart TB
subgraph "페이지 블록 1 (1~10)"
P1[1] --- P2[2] --- P3[3] --- P4[4] --- P5[5] --- P6[6] --- P7[7] --- P8[8] --- P9[9] --- P10[10]
end
subgraph "페이지 블록 2 (11~20)"
P11[11] --- P12[12] --- P13[13] --- P14[...] --- P20[20]
end
// 현재 페이지가 7이면 → tempEnd = 10
int tempEnd = (int)(Math.ceil(page / 10.0)) * 10;
// start = 10 - 9 = 1
this.start = tempEnd - 9;
// 실제 마지막 페이지 계산
if ((tempEnd * size) > totalCount) {
this.end = (int)(Math.ceil(totalCount / (double)size));
} else {
this.end = tempEnd;
}
📝 정리
| 개념 | 설명 |
|---|---|
@Service | 비즈니스 로직 클래스 표시 |
| DI | Spring이 필요한 객체를 자동 주입 |
| skip 계산 | (page - 1) × size |
| types 분해 | "TC" → ["T", "C"] |
| BoardListPaginDTO | 목록 + 페이징 정보 통합 |
🚀 다음 편 예고
**4편: MyBatis와 데이터베이스 연동 완벽 가이드**에서는:
- Mapper 인터페이스와 XML 파일의 관계
selectKey로 자동 생성된 키 가져오기- 동적 SQL (
<if>,<foreach>)
을 알아볼 예정입니다!
📚 시리즈 목차
- [1편] 프로젝트 구조와 아키텍처 완벽 정리
- [2편] Spring MVC 컨트롤러와 요청 흐름 이해하기
- [3편] 서비스 계층과 비즈니스 로직 구현하기 ← 현재 글
- [4편] MyBatis와 데이터베이스 연동 완벽 가이드
- [5편] 페이징과 검색 기능 구현하기
- [6편] 댓글 기능과 예외 처리 마스터하기