풀스택 백엔드 2026.06.05

syyu21b·2026년 6월 5일

풀스택 - 백엔드

목록 보기
23/49
post-thumbnail

[SECTION22. JPA를 이용한 CRUD 구현]

주요 JPA 어노테이션 정리

어노테이션설명
@EntityJPA 엔티티 클래스 선언(테이블과 연결하는 역할)
@Table(name="...")매핑할 테이블 이름 지정
@Id기본 키(PK) 지정
@GeneratedValuePK 자동 생성 전략 설정
@Column컬럼 속성 지정 (nullable, length 등)
@CreatedDate / @LastModifiedDate생성일/수정일 자동 관리

프로젝트 세팅

  • 프로젝트 구조 요약

src/main/java/com/example/demo/
├── DemoApplication.java          # 메인 클래스 (@EnableJpaAuditing)
├── entity/
│   └── Post.java                 # JPA 엔티티
├── repository/
│   └── PostRepository.java       # Spring Data JPA 레포지토리
├── service/
│   └── PostService.java          # 비즈니스 로직
├── controller/
│   └── PostController.java       # REST API 엔드포인트
└── dto/
    └── PostDto.java              # 요청/응답 DTO

  • 의존성 추가 (build.gradle)

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    // DB 드라이버 (H2 인메모리 - 실습용)
    runtimeOnly 'com.h2database:h2'

    // 실제 운영 환경이라면 MySQL 사용
    // runtimeOnly 'com.mysql:mysql-connector-j'
}

  • 애플리케이션 설정 (application.yml)

spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password:

  jpa:
    hibernate:
      ddl-auto: create-drop    # 개발용: 앱 시작 시 테이블 생성, 종료 시 삭제
                               # 운영 환경: validate 또는 none 사용
    show-sql: true             # SQL 콘솔 출력 (개발용)
    properties:
      hibernate:
        format_sql: true       # SQL 포맷팅하여 출력

  # H2 콘솔 활성화 (개발용)
  h2:
    console:
      enabled: true
      path: /h2-console

server:
  port: 8080

  • Post 엔티티 (Entity)

// Post.java
package com.example.demo.entity;

import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Entity
@Table(name = "posts")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT
    private Long id;

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

    @Column(nullable = false, columnDefinition = "TEXT")
    private String content;

    @Column(nullable = false)
    private String author;

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    // 생성 메서드 (정적 팩토리 패턴)
    public static Post create(String title, String content, String author) {
        Post post = new Post();
        post.title = title;
        post.content = content;
        post.author = author;
        return post;
    }

    // 수정 메서드
    public void update(String title, String content) {
        this.title = title;
        this.content = content;
    }
}

  • Repository 생성

// PostRepository.java
package com.example.demo.repository;

import com.example.demo.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface PostRepository extends JpaRepository<Post, Long> {

    // 메서드 이름으로 쿼리 자동 생성
    List<Post> findByAuthor(String author);

    List<Post> findByTitleContaining(String keyword);

    // JPQL 직접 작성
    @Query("SELECT p FROM Post p WHERE p.title LIKE %:keyword% OR p.content LIKE %:keyword%")
    List<Post> searchByKeyword(@Param("keyword") String keyword);
}

JpaRepository<엔티티 타입, PK 타입>을 상속받으면 기본 CRUD 메서드가 자동 제공됩니다.

기본 제공 메서드

메서드설명
save(entity)저장 / 수정
findById(id)ID로 단건 조회 → Optional<T> 반환
findAll()전체 조회
deleteById(id)ID로 삭제
count()전체 개수
existsById(id)존재 여부 확인

  • DTO(Data Transfer Object) 정의

// PostDto.java
package com.example.demo.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;

public class PostDto {

    // 생성 요청 DTO
    @Getter
    @NoArgsConstructor
    public static class CreateRequest {
        @NotBlank(message = "제목은 필수입니다.")
        @Size(max = 100, message = "제목은 100자 이하여야 합니다.")
        private String title;

        @NotBlank(message = "내용은 필수입니다.")
        private String content;

        @NotBlank(message = "작성자는 필수입니다.")
        private String author;
    }

    // 수정 요청 DTO
    @Getter
    @NoArgsConstructor
    public static class UpdateRequest {
        @NotBlank(message = "제목은 필수입니다.")
        private String title;

        @NotBlank(message = "내용은 필수입니다.")
        private String content;
    }

    // 응답 DTO
    @Getter
    public static class Response {
        private Long id;
        private String title;
        private String content;
        private String author;
        private String createdAt;
        private String updatedAt;

        public static Response from(com.example.demo.entity.Post post) {
            Response response = new Response();
            response.id = post.getId();
            response.title = post.getTitle();
            response.content = post.getContent();
            response.author = post.getAuthor();
            response.createdAt = post.getCreatedAt() != null ? post.getCreatedAt().toString() : null;
            response.updatedAt = post.getUpdatedAt() != null ? post.getUpdatedAt().toString() : null;
            return response;
        }
    }
}

  • Service 구현

// PostService.java
package com.example.demo.service;

import com.example.demo.dto.PostDto;
import com.example.demo.entity.Post;
import com.example.demo.repository.PostRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true) // 기본적으로 읽기 전용 트랜잭션
public class PostService {

    private final PostRepository postRepository;

    // ✅ CREATE
    @Transactional  // 쓰기 작업은 @Transactional 별도 선언
    public PostDto.Response createPost(PostDto.CreateRequest request) {
        Post post = Post.create(request.getTitle(), request.getContent(), request.getAuthor());
        Post savedPost = postRepository.save(post);
        return PostDto.Response.from(savedPost);
    }

    // ✅ READ - 전체 조회
    public List<PostDto.Response> getAllPosts() {
        return postRepository.findAll()
                .stream()
                .map(PostDto.Response::from)
                .collect(Collectors.toList());
    }

    // ✅ READ - 단건 조회
    public PostDto.Response getPost(Long id) {
        Post post = postRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없습니다. id: " + id));
        return PostDto.Response.from(post);
    }

    // ✅ UPDATE
    @Transactional
    public PostDto.Response updatePost(Long id, PostDto.UpdateRequest request) {
        Post post = postRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없습니다. id: " + id));

        post.update(request.getTitle(), request.getContent()); // 더티 체킹(Dirty Checking)으로 자동 업데이트
        return PostDto.Response.from(post);
    }

    // ✅ DELETE
    @Transactional
    public void deletePost(Long id) {
        if (!postRepository.existsById(id)) {
            throw new RuntimeException("게시글을 찾을 수 없습니다. id: " + id);
        }
        postRepository.deleteById(id);
    }

    // ✅ SEARCH
    public List<PostDto.Response> searchPosts(String keyword) {
        return postRepository.searchByKeyword(keyword)
                .stream()
                .map(PostDto.Response::from)
                .collect(Collectors.toList());
    }
}

  • Controller 구현

// PostController.java
package com.example.demo.controller;

import com.example.demo.dto.PostDto;
import com.example.demo.service.PostService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/posts")
@RequiredArgsConstructor
public class PostController {

    private final PostService postService;

    // POST /api/posts - 게시글 생성
    @PostMapping
    public ResponseEntity<PostDto.Response> createPost(@Valid @RequestBody PostDto.CreateRequest request) {
        PostDto.Response response = postService.createPost(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }

    // GET /api/posts - 전체 조회
    @GetMapping
    public ResponseEntity<List<PostDto.Response>> getAllPosts() {
        return ResponseEntity.ok(postService.getAllPosts());
    }

    // GET /api/posts/{id} - 단건 조회
    @GetMapping("/{id}")
    public ResponseEntity<PostDto.Response> getPost(@PathVariable Long id) {
        return ResponseEntity.ok(postService.getPost(id));
    }

    // PUT /api/posts/{id} - 수정
    @PutMapping("/{id}")
    public ResponseEntity<PostDto.Response> updatePost(
            @PathVariable Long id,
            @Valid @RequestBody PostDto.UpdateRequest request) {
        return ResponseEntity.ok(postService.updatePost(id, request));
    }

    // DELETE /api/posts/{id} - 삭제
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deletePost(@PathVariable Long id) {
        postService.deletePost(id);
        return ResponseEntity.noContent().build();
    }

    // GET /api/posts/search?keyword=xxx - 검색
    @GetMapping("/search")
    public ResponseEntity<List<PostDto.Response>> searchPosts(@RequestParam String keyword) {
        return ResponseEntity.ok(postService.searchPosts(keyword));
    }
}

  • 핵심정리

0개의 댓글