Spring

Spring MVC 게시판 만들기 3편: 서비스 계층과 비즈니스 로직의 모든 것


💡 이 글에서 배울 것
Controller가 직접 DB에 접근하면 안 되는 이유와, Service 계층이 어떤 “진짜 일”을 하는지 파헤쳐봅니다.


🎯 한눈에 보기

주제핵심 내용
@Service비즈니스 로직을 처리하는 계층
DISpring이 객체를 자동으로 주입
페이징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) 이해하기

의존성 주입 개념

BoardServiceBoardMapper가 필요합니다. 이걸 “의존한다”고 표현해요.

전통적인 방식 (🚫 사용하면 안됨):

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비즈니스 로직 클래스 표시
DISpring이 필요한 객체를 자동 주입
skip 계산(page - 1) × size
types 분해"TC"["T", "C"]
BoardListPaginDTO목록 + 페이징 정보 통합

🚀 다음 편 예고

**4편: MyBatis와 데이터베이스 연동 완벽 가이드**에서는:

  • Mapper 인터페이스와 XML 파일의 관계
  • selectKey로 자동 생성된 키 가져오기
  • 동적 SQL (<if>, <foreach>)

을 알아볼 예정입니다!


📚 시리즈 목차