[코드로 배우는 스프링 부트 웹 프로젝트] 스프링 부트(Spring Boot) 02

hidihyeonee·2025년 2월 13일
0
post-thumbnail

2025.02.12 작성

OS : Window
개발환경: IntelliJ IDEA
개발언어: Java
프레임워크: Spring Boot


SpringBoot + MariaDB를 이용한 MVC 구조로 방명록 프로그램 구현

SpringBoot + JPA + thymeleaf

MVC(Model-View-Controller)란?

  • MVC는 웹 애플리케이션을 개발할 때 사용하는 소프트웨어 설계 패턴.
  • 이 패턴은 코드를 역할별로 나누어 유지보수와 확장성을 높이는 것이 목적임.

MVC의 역할

  • Model (모델) → 데이터, 비즈니스 로직을 담당 (DB와 연동되는 부분)
  • View (뷰) → 사용자에게 보여지는 화면 (HTML, Thymeleaf, React, Vue 등)
  • Controller (컨트롤러) → 사용자의 요청을 받고 응답을 반환 (데이터 처리 요청을 Service로 넘김)

컨트롤러 → 서비스 → 리포지토리 → 데이터베이스 흐름 분석

1️⃣ 컨트롤러 (Controller)

@RestController
@RequestMapping("/guestbook")
@RequiredArgsConstructor
public class GuestbookController {
    private final GuestbookService guestbookService;

    @PostMapping("/register")
    public ResponseEntity<Long> register(@RequestBody GuestbookDTO dto) {
        Long gno = guestbookService.register(dto);
        return ResponseEntity.ok(gno);
    }
}

✅ 컨트롤러의 역할

  • 사용자의 요청 (/register)을 받음.
  • GuestbookService에게 데이터를 처리하라고 요청함.
  • 처리 결과(등록된 글 번호)를 응답(ResponseEntity)으로 반환.

2️⃣ 서비스 (Service)

public interface GuestbookService {
    Long register(GuestbookDTO dto);
}

✅ 서비스의 역할

  • 비즈니스 로직을 수행하고, DB 처리 요청을 Repository에게 전달.
  • DTO ↔ Entity 변환을 수행하여 데이터 일관성 유지.

3️⃣ 서비스 구현체 (ServiceImpl)

@Service
@RequiredArgsConstructor
@Log4j2
public class GuestbookServiceImpl implements GuestbookService {

    private final GuestbookRepository guestbookRepository;

    @Override
    public Long register(GuestbookDTO dto) {
        log.info("등록 요청: " + dto);

        Guestbook entity = dtoToEntity(dto);
        entity = guestbookRepository.save(entity);

        return entity.getGno();
    }
}

✅ 서비스 구현체(ServiceImpl)의 역할

  • 컨트롤러에서 요청받은 데이터(DTO)를 엔티티로 변환.
  • Repository를 호출해서 DB에 저장.
  • 저장된 데이터의 ID(PK)를 반환.

❓ 근데 왜 ServiceImpl을 따로 만들어?

✔️ 유지보수성과 확장성을 위해 인터페이스(GuestbookService)와 구현체(GuestbookServiceImpl)를 분리
✔️ 나중에 GuestbookServiceImplV2 같은 새로운 버전이 필요할 때 쉽게 교체 가능
✔️ 인터페이스를 사용하면 Mock을 이용해 테스트하기 편리함

4️⃣ 리포지토리 (Repository)

@Repository
public interface GuestbookRepository extends JpaRepository<Guestbook, Long> {
}

✅ 리포지토리의 역할

  • DB와 직접 연결되어 데이터를 저장하고 조회하는 역할.
  • JpaRepository<Guestbook, Long>을 상속받아 기본적인 CRUD 기능을 자동으로 제공.

5️⃣ 데이터베이스 (MySQL, H2 등)

  • 리포지토리에서 JPA를 통해 SQL 실행 (INSERT, SELECT, UPDATE, DELETE)
  • DB에 방명록 데이터를 저장하고 조회할 수 있음.

1. BaseEntity (기본 엔티티 클래스)

등록일 및 수정일을 자동 관리하는 공통 부모 클래스.

package org.zerock.guestbook.entity;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@MappedSuperclass
@EntityListeners(value={AuditingEntityListener.class})
@Getter
abstract class BaseEntity {
    @CreatedDate
    @Column(name = "regdate", updatable = false)
    private LocalDateTime regdate;

    @LastModifiedDate
    @Column(name = "moddate")
    private LocalDateTime moddate;
}

🔹설명

  • @MappedSuperclass: 이 클래스를 다른 엔티티들이 상속받아 사용할 수 있도록 설정 (단독으로 테이블이 생성되지 않음).

  • @EntityListeners(AuditingEntityListener.class): 엔티티의 생성 및 수정 시간 기록을 자동으로 관리하기 위한 설정.

  • @CreatedDate: 엔티티가 생성될 때 자동으로 regdate(등록 날짜) 값을 설정.

  • @LastModifiedDate: 엔티티가 수정될 때 자동으로 moddate(수정 날짜) 값을 업데이트.

  • abstract class BaseEntity: 직접 객체를 생성할 일이 없으므로 추상 클래스로 선언.

🔹역할

  • 여러 엔티티에서 공통적으로 사용할 등록일(regdate)과 수정일(moddate)을 자동으로 관리하는 부모 클래스 역할을 함.


2. Guestbook (방명록 엔티티)

방명록 데이터를 관리하는 JPA 엔티티.

package org.zerock.guestbook.entity;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Guestbook extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long gno;

    @Column(length = 100, nullable = false)
    private String title;

    @Column(length = 1500, nullable = false)
    private String content;

    @Column(length = 50, nullable = false)
    private String writer;
}

🔹설명

  • @Entity: JPA에서 이 클래스를 데이터베이스 테이블과 매핑.

  • extends BaseEntity: BaseEntity를 상속하여 regdate 및 moddate를 자동으로 관리.

  • @Id: 기본 키(PK)로 gno 필드를 설정.

  • @GeneratedValue(strategy = GenerationType.IDENTITY): gno 값을 데이터베이스에서 자동 증가 방식(IDENTITY)으로 관리.

  • @Column:

    	- #### title(제목): 최대 100자, null 불가.
    	- #### content(내용): 최대 1500자, null 불가.
    	- #### writer(작성자): 최대 50자, null 불가.
  • @Builder: 객체 생성 시 빌더 패턴을 사용할 수 있도록 지원.

  • @AllArgsConstructor, @NoArgsConstructor: 모든 필드를 포함한 생성자와 기본 생성자 자동 생성.

  • @ToString: 객체 정보를 문자열로 변환할 때 모든 필드를 포함하도록 설정.

🔹역할

  • 방명록 데이터를 저장하는 guestbook 테이블과 매핑되는 JPA 엔티티.


3. GuestbookController (방명록 컨트롤러)

/guestbook 관련 요청을 처리하는 컨트롤러.

package org.zerock.guestbook.controller;

import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/guestbook")
@Log4j2
public class GuestbookController {

    @GetMapping({"/", "/list"})
    public String list() {
        log.info("list");

        return "/guestbook/list";
    }
}

🔹설명

  • @Controller: 스프링 MVC의 컨트롤러임을 나타냄.

  • @RequestMapping("/guestbook"): /guestbook URL과 매핑되는 컨트롤러.

  • @Log4j2: 로그 기능을 추가 (Log4j2 기반).

  • @GetMapping({"/", "/list"}):

    	- #### /guestbook/ 또는 /guestbook/list URL 요청을 처리.
    	- #### 로그에 "list" 메시지를 출력 (log.info("list")).
    	- #### /guestbook/list 뷰(HTML 페이지)를 반환.

🔹역할

  • 방명록 목록 페이지를 보여주는 역할을 하는 컨트롤러.

  • 현재는 단순히 /guestbook/list 뷰 페이지로 이동하는 기능만 구현되어 있음.


Q도메인(Q-Class)

Q도메인(Q-Class)란?

Querydsl을 사용할 때, 엔티티 클래스를 기반으로 자동 생성되는 타입 안전한(Query Type-Safe) Query 객체.
이 Q도메인을 활용하면 JPQL을 코드로 표현할 수 있어, SQL 인젝션을 방지하고 가독성을 높일 수 있음.

implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
    annotationProcessor(
            "jakarta.persistence:jakarta.persistence-api",
            "jakarta.annotation:jakarta.annotation-api",
            'com.querydsl:querydsl-apt:5.1.0:jakarta'
    )

📌 정리

Q도메인(Q-Class): 엔티티를 기반으로 Querydsl에서 사용하기 위한 타입 안전한 쿼리 객체.
자동 생성 방식: build/generated 폴더에 QGuestbook.java 같은 클래스를 만들어 줌.

이점

✅ 타입 안전 (컴파일 오류 방지)
✅ 가독성 향상 (SQL-like한 Java 코드)
✅ 동적 쿼리 생성이 쉬움 (필요한 조건만 추가)

테스트 코드를 작성해야 하는 이유

✅ 1. 서비스 로직이 정상적으로 동작하는지 검증 (DB 저장, 페이징 처리 등).
✅ 2. 개발 중 발생할 수 있는 버그를 조기에 발견 (잘못된 데이터 처리, 페이징 오류 등).
✅ 3. 코드 변경 시 기존 기능이 깨지지 않는지 확인 (테스트 자동화).
✅ 4. CI/CD 환경에서 자동 테스트 수행 가능 (배포 전 문제 발생 방지).

즉, 테스트 코드를 작성하면 버그를 방지하고 유지보수성을 높일 수 있음! 🚀


PageResultDTO

JPA의 Page 객체를 가공하여 DTO 리스트로 변환하는 역할.
페이지네이션(페이징 처리) 정보를 계산해서 클라이언트(UI)에서 사용할 수 있도록 제공.

페이지네이션 정보 계산 (makePageList())

private void makePageList(Pageable pageable) {
    this.page = pageable.getPageNumber() + 1; // 현재 페이지 (0-based → 1-based)
    this.size = pageable.getPageSize(); // 한 페이지당 항목 개수

    int tempEnd = (int) (Math.ceil((double) page / 10) * 10); // 10단위 페이지 계산
    start = Math.max(1, tempEnd - 9); // 시작 페이지는 최소 1
    end = Math.min(totalPages, tempEnd); // 끝 페이지는 totalPages보다 클 수 없음

    prev = start > 1; // 이전 페이지 존재 여부
    next = totalPages > tempEnd; // 다음 페이지 존재 여부

    // 페이지 리스트 생성 (예: 1~10, 11~20 ...)
    pageList = IntStream.rangeClosed(start, end).boxed().collect(Collectors.toList());
}

✅ 동작 원리

1. 현재 페이지 정보 저장

this.page = pageable.getPageNumber() + 1;
  • JPA의 Page 객체는 0부터 시작 (0-based index).
  • UI에서 사용하기 쉽게 1부터 시작하도록 변환.

2. 페이지 그룹 계산 (10개씩 묶어서 표시)

int tempEnd = (int) (Math.ceil((double) page / 10) * 10);
  • 현재 페이지 번호를 기준으로 10개 단위 페이지 묶음 생성
  • 예제
    - page = 1~10 → tempEnd = 10
    - page = 11~20 → tempEnd = 20
    - page = 21~30 → tempEnd = 30
  • 이 값을 사용하여 시작 번호와 끝 번호를 계산.

3. 시작 페이지 계산 (start)

start = Math.max(1, tempEnd - 9);
  • 페이지 그룹의 첫 번째 페이지를 계산.
  • Math.max(1, tempEnd - 9);
    - 만약 tempEnd = 10이면, start = 10 - 9 = 1
    - 만약 tempEnd = 20이면, start = 20 - 9 = 11
    - 즉, 항상 1 이상이 되도록 설정.

4. 끝 페이지 계산 (end)

  end = Math.min(totalPages, tempEnd);
  • 현재 totalPages보다 큰 경우를 방지.
  • 만약 totalPages = 7, tempEnd = 10이면, end = 7로 조정.

5. 이전/다음 페이지 존재 여부 (prev, next)

prev = start > 1; 
next = totalPages > tempEnd;
  • prev = true이면 이전 페이지가 존재함.
  • next = true이면 다음 페이지가 존재함.

6. 페이지 번호 리스트 생성 (pageList)

pageList = IntStream.rangeClosed(start, end).boxed().collect(Collectors.toList());
  • 예를 들어, start = 11, end = 20이면
    - pageList = [11, 12, 13, ..., 20]

BaseEntity

package org.zerock.guestbook.entity;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
abstract class BaseEntity {
    @CreatedDate
    @Column(name="regdate", updatable=false)
    private LocalDateTime regDate;

    @LastModifiedDate
    @Column(name="moddate")
    private LocalDateTime modDate;
}

🔹 BaseEntity는 왜 필요한가?

1️⃣ 모든 엔티티에서 공통으로 사용되는 필드를 관리

  • 데이터베이스의 모든 엔티티(테이블)에는 일반적으로 등록일(regDate) 및 수정일(modDate) 같은 공통 필드가 존재함.
  • 이런 필드를 각 엔티티마다 반복해서 정의하는 것은 비효율적이므로, BaseEntity라는 공통 부모 클래스를 만들어 상속받도록 설계함.

📌 예제: Guestbook 엔티티에서 BaseEntity를 상속받아 사용

@Entity
public class Guestbook extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long gno;

    @Column(length = 100, nullable = false)
    private String title;

    @Column(length = 1500, nullable = false)
    private String content;

    @Column(length = 50, nullable = false)
    private String writer;
}

✅ Guestbook 엔티티에는 regDate, modDate 필드가 없지만, BaseEntity를 상속하여 자동으로 포함됨.

2️⃣ @MappedSuperclass를 사용하여 테이블 생성 방지

  • @MappedSuperclass는 JPA에서 부모 클래스의 필드를 자식 엔티티에 포함시키되, 부모 클래스 자체는 테이블로 생성하지 않도록 하는 어노테이션임.
  • 만약 @MappedSuperclass 없이 @Entity를 사용하면 BaseEntity도 별도의 테이블로 생성됨.
  • 하지만 이 클래스는 단독으로 테이블이 필요하지 않으므로 @MappedSuperclass로 설정하여 자식 엔티티에서만 필드를 상속받도록 만듦.

3️⃣ @EntityListeners(AuditingEntityListener.class)로 자동 값 설정

  • 스프링 데이터 JPA의 Auditing 기능을 활성화하여, 엔티티가 생성되거나 변경될 때 자동으로 날짜를 기록할 수 있도록 함.
  • @CreatedDate와 @LastModifiedDate를 사용하면 엔티티가 생성될 때 자동으로 등록일이 저장되고, 수정될 때 자동으로 수정일이 업데이트됨.

GuestbookService

GuestbookService 인터페이스의 역할

  • Guestbook(방명록) 데이터를 처리하는 비즈니스 로직을 정의하는 역할.
  • 데이터베이스와 직접적인 연동을 담당하는 Repository를 감싸고, DTO와 Entity 변환을 관리하는 서비스 계층.

📌 즉, 컨트롤러 → 서비스 → 리포지토리 → 데이터베이스 순으로 동작하는데, 서비스 계층이 중간 역할을 수행.

DTO → Entity 변환 (dtoToEntity)

default Guestbook dtoToEntity(GuestbookDTO dto) {
    Guestbook entity = Guestbook.builder()
            .gno(dto.getGno())
            .title(dto.getTitle())
            .content(dto.getContent())
            .writer(dto.getWriter())
            .build();
    return entity;
}

✅ 기능:

  • GuestbookDTO를 Guestbook 엔티티로 변환하는 역할.
  • 기본적으로 DTO데이터 전송용 객체이므로, 데이터베이스에 저장하려면 반드시 Entity 객체로 변환해야 함.
profile
벨로그 쫌 재밌네?

0개의 댓글