2025.02.12 작성
OS : Window
개발환경: IntelliJ IDEA
개발언어: Java
프레임워크: Spring Boot
SpringBoot + JPA + thymeleaf
@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)으로 반환.
public interface GuestbookService {
Long register(GuestbookDTO dto);
}
✅ 서비스의 역할
- 비즈니스 로직을 수행하고, DB 처리 요청을 Repository에게 전달.
- DTO ↔ Entity 변환을 수행하여 데이터 일관성 유지.
@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을 이용해 테스트하기 편리함
@Repository
public interface GuestbookRepository extends JpaRepository<Guestbook, Long> {
}
✅ 리포지토리의 역할
- DB와 직접 연결되어 데이터를 저장하고 조회하는 역할.
- JpaRepository<Guestbook, Long>을 상속받아 기본적인 CRUD 기능을 자동으로 제공.
등록일 및 수정일을 자동 관리하는 공통 부모 클래스.
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;
}
방명록 데이터를 관리하는 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;
}
- #### title(제목): 최대 100자, null 불가.
- #### content(내용): 최대 1500자, null 불가.
- #### writer(작성자): 최대 50자, null 불가.
/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";
}
}
- #### /guestbook/ 또는 /guestbook/list URL 요청을 처리.
- #### 로그에 "list" 메시지를 출력 (log.info("list")).
- #### /guestbook/list 뷰(HTML 페이지)를 반환.
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 환경에서 자동 테스트 수행 가능 (배포 전 문제 발생 방지).
즉, 테스트 코드를 작성하면 버그를 방지하고 유지보수성을 높일 수 있음! 🚀
JPA의 Page 객체를 가공하여 DTO 리스트로 변환하는 역할.
페이지네이션(페이징 처리) 정보를 계산해서 클라이언트(UI)에서 사용할 수 있도록 제공.
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());
}
this.page = pageable.getPageNumber() + 1;
int tempEnd = (int) (Math.ceil((double) page / 10) * 10);
start = Math.max(1, tempEnd - 9);
end = Math.min(totalPages, tempEnd);
prev = start > 1;
next = totalPages > tempEnd;
pageList = IntStream.rangeClosed(start, end).boxed().collect(Collectors.toList());
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;
}
@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를 상속하여 자동으로 포함됨.
📌 즉, 컨트롤러 → 서비스 → 리포지토리 → 데이터베이스 순으로 동작하는데, 서비스 계층이 중간 역할을 수행.
default Guestbook dtoToEntity(GuestbookDTO dto) {
Guestbook entity = Guestbook.builder()
.gno(dto.getGno())
.title(dto.getTitle())
.content(dto.getContent())
.writer(dto.getWriter())
.build();
return entity;
}