Backend
home

2025-6-24 (화)

생성일
2025/06/23 23:03
태그
JPA

JPA

엔티티 연관관계 정리

Auth 파트

AuthService
package org.example.backendproject.Auth.service; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.example.backendproject.Auth.dto.LoginRequestDTO; import org.example.backendproject.Auth.dto.SignUpRequestDTO; import org.example.backendproject.user.dto.UserDTO; import org.example.backendproject.user.dto.UserProfileDTO; import org.example.backendproject.user.entity.User; import org.example.backendproject.user.entity.UserProfile; import org.example.backendproject.user.repository.UserRepository; import org.springframework.stereotype.Service; @RequiredArgsConstructor @Service public class AuthService { private final UserRepository userRepository; @Transactional // 해당 어노테이션 선언해야 저장이 된다. public void signUp(SignUpRequestDTO dto) { // 사용자 조회 여부 확인, null값 체크 if (userRepository.findByUserid(dto.getUserid()).isPresent()) { throw new RuntimeException("사용자가 이미 존재합니다."); } User user = new User(); user.setUserid(dto.getUserid()); user.setPassword(dto.getPassword()); UserProfile profile = new UserProfile(); profile.setUsername(dto.getUsername()); profile.setEmail(dto.getEmail()); profile.setPhone(dto.getPhone()); profile.setAddress(dto.getAddress()); /** 연관관계 설정 **/ profile.setUser(user); user.setUserProfile(profile); userRepository.save(user); } public UserDTO login(LoginRequestDTO loginRequestDTO) { User user = userRepository.findByUserid(loginRequestDTO.getUserid()) .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); if (!loginRequestDTO.getPassword().equals(user.getPassword())) { throw new RuntimeException("비밀번호를 찾을 수 없습니다."); } UserDTO userDTO = new UserDTO(); userDTO.setId(user.getId()); userDTO.setUserid(user.getUserid()); // 유저 프로필 UserProfileDTO profileDTO = new UserProfileDTO(); profileDTO.setUsername(user.getUserProfile().getUsername()); profileDTO.setEmail(user.getUserProfile().getEmail()); profileDTO.setPhone(user.getUserProfile().getPhone()); profileDTO.setAddress(user.getUserProfile().getAddress()); return userDTO; } }
Java
복사

User 파트

UserProfileDTO 생성
package org.example.backendproject.user.dto; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class UserProfileDTO { private String username; private String email; private String phone; private String address; }
Java
복사
User 엔티티에 해당 내용을 추가
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<Board> boards = new ArrayList<>();
Java
복사
BaseTime 엔티티 생성 - 공통 적용
package org.example.backendproject.user.entity; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; import java.time.LocalDateTime; import lombok.Getter; import lombok.Setter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Getter @Setter // JPA 엔티티의 생성/수정 시점을 자동으로 기록하도록 이벤트 리스너로 동작하도록 하는 어노테이션 @EntityListeners(AuditingEntityListener.class) // 이 클래스를 상속받는 엔티티들은 이 클래스의 필드를 컬럼으로 포함시키라는 어노테이션 @MappedSuperclass public abstract class BaseTime { // 엔티티가 저장될 때 자동으로 시간을 기록 @CreatedDate private LocalDateTime created_date; // 엔티티가 수정될 때 자동으로 시간 기록 @LastModifiedDate private LocalDateTime updated_date; }
Java
복사
@EnableJpaAuditing 적용 - BaseTime에 맞춰서
package org.example.backendproject; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication @EnableJpaAuditing public class BackendProjectApplication { public static void main(String[] args) { SpringApplication.run(BackendProjectApplication.class, args); } }
Java
복사
UserDTO에 profile 추가
package org.example.backendproject.user.dto; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @NoArgsConstructor @AllArgsConstructor @Getter @Setter public class UserDTO { private Long id; private String userid; private UserProfileDTO profile; // 추가 }
Java
복사
UserProfilerRepository 추가
package org.example.backendproject.user.repository; import org.example.backendproject.user.entity.UserProfile; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface UserProfilerRepository extends JpaRepository<UserProfile, Long> { }
Java
복사
UserService
package org.example.backendproject.user.service; import jakarta.persistence.EntityManager; import java.util.List; import lombok.RequiredArgsConstructor; import org.example.backendproject.user.dto.UserDTO; import org.example.backendproject.user.dto.UserProfileDTO; import org.example.backendproject.user.entity.User; import org.example.backendproject.user.entity.UserProfile; import org.example.backendproject.user.repository.UserProfilerRepository; import org.example.backendproject.user.repository.UserRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service public class UserService { private final UserRepository userRepository; private final UserProfilerRepository userProfilerRepository; private final EntityManager em; /** 내 정보 조회 **/ @Transactional(readOnly = true) public UserDTO getMyInfo(Long id) { User user = userRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("회원을 찾을 수 없습니다.")); UserDTO dto = new UserDTO(); dto.setId(user.getId()); dto.setUserid(user.getUserid()); UserProfile profile = user.getUserProfile(); UserProfileDTO profileDTO = new UserProfileDTO(); profileDTO.setUsername(profile.getUsername()); profileDTO.setEmail(profile.getEmail()); profileDTO.setPhone(profileDTO.getPhone()); profileDTO.setAddress(profileDTO.getAddress()); dto.setProfile(profileDTO); return dto; } /** 유저 정보 수정 **/ @Transactional public UserDTO updateUser(Long id, UserDTO userDTO) { // 유저 레포지토리를 통해서 유저를 가져옴 User user = userRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("토큰이 존재하지 않습니다.")); // 프로필 객체를 만들어서 기존에 변경되기 전 프로필을 넣어줌 UserProfile profile = user.getUserProfile(); // 수정하려는 프로필이 있는지 체크, 수정하려는 프로필 정보가 있는지 체크 if (profile != null && userDTO.getProfile() != null) { UserProfileDTO dtoProfile = userDTO.getProfile(); // 프로필을 수정하기 위해 전달받은 데이터로 변강한다. if (dtoProfile.getUsername() != null) profile.setUsername(dtoProfile.getUsername()); if (dtoProfile.getEmail() != null) profile.setEmail(dtoProfile.getEmail()); if (dtoProfile.getPhone() != null) profile.setPhone(dtoProfile.getPhone()); if (dtoProfile.getAddress() != null) profile.setAddress(dtoProfile.getAddress()); } /** * JPA에서 findById()로 가져온 엔티티는 영속 상태임. * 필드 값을 바꾸면 JPA가 트랜잭션 커밋할 때 자동으로 update 쿼리를 날림 */ // 아래는 변경된 내용을 프론트에 던져주기 위해 생성한다. UserDTO dto = new UserDTO(); dto.setId(user.getId()); dto.setUserid(user.getUserid()); UserProfileDTO profileDTO = new UserProfileDTO(); profileDTO.setUsername(profile.getUsername()); profileDTO.setEmail(profile.getEmail()); profileDTO.setPhone(profile.getPhone()); profileDTO.setAddress(profile.getAddress()); dto.setProfile(profileDTO); return dto; } //아래는 순환참조가 되는 예제 public User getProfile2(Long profileId) { UserProfile profile = userProfilerRepository.findById(profileId) .orElseThrow(()->new RuntimeException("프로필 없음")); return profile.getUser(); } //dto로 순환참조 방지 public UserDTO getProfile(Long profileId) { UserProfile profile = userProfilerRepository.findById(profileId) .orElseThrow(()->new RuntimeException("프로필 없음")); User user =profile.getUser(); if (user==null) throw new RuntimeException("연결된 유저 없음"); UserProfileDTO profileDTO = new UserProfileDTO( profile.getUsername(), profile.getEmail(), profile.getPhone(), profile.getAddress() ); UserDTO userDTO = new UserDTO( user.getId(), user.getUserid(), profileDTO ); return userDTO; } @Transactional public void saveAllUsers(List<User> users) { long start = System.currentTimeMillis(); for (int i = 0; i < users.size(); i++) { em.persist(users.get(i)); if (i % 1000 == 0){ em.flush(); em.clear(); } } long end = System.currentTimeMillis(); System.out.println("JPA saveAll 저장 소요 시간(ms): " + (end - start)); } }
Java
복사
UserController 수정
package org.example.backendproject.user.controller; import java.util.List; import lombok.RequiredArgsConstructor; import org.example.backendproject.user.dto.UserDTO; import org.example.backendproject.user.entity.User; import org.example.backendproject.user.service.UserService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/user") @RequiredArgsConstructor public class UserController { // @Value("${PROJECT_NAME:web Server}") // private String instanceName; // // @GetMapping // public String test() { // return instanceName; // } private final UserService userService; /** 내 정보 보기 **/ @GetMapping("/me/{id}") public ResponseEntity<UserDTO> getMyInfo(@PathVariable("id") Long userId){ return ResponseEntity.ok(userService.getMyInfo(userId)); } /** 유저 정보 수정 **/ @PutMapping("/me/{id}") public ResponseEntity<UserDTO> updateUser(@PathVariable("id") Long userId, @RequestBody UserDTO dto) { UserDTO updated = userService.updateUser(userId, dto); return ResponseEntity.ok(updated); } // dto로 순환참조 방지 @GetMapping("/profile/{profileId}") public UserDTO getProfile(@PathVariable Long profileId) { return userService.getProfile(profileId); } @PostMapping("/jpaSaveAll") public String saveAll(@RequestBody List<User> users) { userService.saveAllUsers(users); return "ok"; } }
Java
복사
회원 정보 확인
Postman에서 수정
localhost:8080 에 접속하여 수정
DB에서 수정 내용 확인

Board 파트

Board 엔티티
package org.example.backendproject.board.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import java.util.ArrayList; import java.util.List; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.example.backendproject.comment.entity.Comment; import org.example.backendproject.user.entity.BaseTime; import org.example.backendproject.user.entity.User; @NoArgsConstructor @AllArgsConstructor @Getter @Setter @Entity public class Board extends BaseTime { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String title; @Column(nullable = false) private String content; private String batchKey; // 아래는 글을 작성한 유저 정보 // 연관관계 매핑 // 다대일 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private User user; // 일대다 @OneToMany(mappedBy = "board") private List<Comment> comment = new ArrayList<>(); }
Java
복사
dto 생성
package org.example.backendproject.board.dto; import java.time.LocalDateTime; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class BoardDTO { private Long id; private String title; private String content; private String username; private Long user_id; private LocalDateTime created_date; private LocalDateTime updated_date; private String batchKey; public BoardDTO(Long id, String title, String content, String username, Long user_id, LocalDateTime created_date, LocalDateTime updated_date) { this.id = id; this.title = title; this.content = content; this.username = username; this.user_id = user_id; this.created_date = created_date; this.updated_date = updated_date; } }
Java
복사
BoardRepository
package org.example.backendproject.board.repository; import java.util.List; import org.example.backendproject.board.dto.BoardDTO; import org.example.backendproject.board.entity.Board; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; public interface BoardRepository extends JpaRepository<Board, Long> { // 특정 User의 게시글만 조회 //@Query(value = "SELECT * FROM board WHERE user_id = :userId", nativeQuery = true) //이거 사용안해도 되는데 예시로 네이티브쿼라로 작성한거임 List<Board> findByUserId(Long userId); //보드 엔티티를 기준으로 조회하되 //Board 엔티티 전체를 반환하는게 아니라 원하는 값만 보드dto 생성자에 넣어서 리스트로 반환합니다 //대소문자 구분없이 검색하는 옵션 //title에 해당 키워드가 포함되어 있거나 content에 키워드가 포함되어잇는거 출력 /** 페이징 적용 전 **/ /** 검색기능 **/ // 제목 또는 내용에 키워드가 포함된 글 검색 (대소문자 구분 없음) @Query("SELECT new org.example.backendproject.board.dto.BoardDTO(" + "b.id, b.title, b.content,b.user.userProfile.username, b.user.id, b.created_date, b.updated_date" + ") " + "FROM Board b " + "WHERE LOWER(b.title) LIKE LOWER(CONCAT('%', :keyword, '%')) " + "OR LOWER(b.content) LIKE LOWER(CONCAT('%', :keyword, '%'))") List<BoardDTO> searchKeyword(@Param("keyword") String keyword); /** 페이징 적용 후 **/ //페이징 전체 목록 @Query("SELECT new org.example.backendproject.board.dto.BoardDTO(" + "b.id, b.title, b.content,b.user.userProfile.username, b.user.id,b.created_date, b.updated_date) " + "FROM Board b ORDER BY b.title DESC") // + "ORDER BY b.title DESC") //쿼리로 정렬 Page<BoardDTO> findAllPaging(Pageable pageable); //페이징 처리 결과를 담는 페이징 객체입니다. //전체 페이지수, 현재 페이지 번호,전체 아이템 겟수 등 페이징 관련 모든 정보들을 반환합니다. //Pageable은 jpa에서 제공하는 페이징 정보를 담은 객체입니다. //page번호, 한페이지당 데이터 갯수 ,정렬 기준 등 파라미터를 받아 원하는 조건으로 페이징 및 정렬 쿼리를 생성할 수 있습니다. //페이징 검색 목록 @Query("SELECT new org.example.backendproject.board.dto.BoardDTO(" + "b.id, b.title, b.content,b.user.userProfile.username, b.user.id, b.created_date, b.updated_date) " + "FROM Board b " + "WHERE LOWER(b.title) LIKE LOWER(CONCAT('%', :keyword, '%')) " + "OR LOWER(b.content) LIKE LOWER(CONCAT('%', :keyword, '%')) ORDER BY b.title DESC") // + "ORDER BY b.title DESC")// 쿼리로 정렬 Page<BoardDTO> searchKeywordPaging(@Param("keyword") String keyword, Pageable pageable); }
Java
복사
BatchRepository - 일괄 등록 (배치 작업)
package org.example.backendproject.board.repository; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; import lombok.RequiredArgsConstructor; import org.example.backendproject.board.dto.BoardDTO; import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @Repository @RequiredArgsConstructor public class BatchRepository { private final JdbcTemplate jdbcTemplate; public void batchInsert(List<BoardDTO> boardDTO) { // 각각의 변수들은 Database에 있는 변수의 명칭대로 입력해줘야 정상적으로 동작됨 String sql = "INSERT INTO board (title, content, user_id, created_date, updated_date, batch_key) " + "VALUES (?, ?, ?, ?, ?, ?) "; jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { BoardDTO dto = boardDTO.get(i); ps.setString(1, dto.getTitle()); ps.setString(2, dto.getContent()); ps.setLong(3, dto.getUser_id()); ps.setString(4, String.valueOf(dto.getCreated_date())); ps.setString(5, String.valueOf(dto.getUpdated_date())); ps.setString(6, dto.getBatchKey()); } @Override public int getBatchSize() { return boardDTO.size(); } }); } }
Java
복사
BoardService
package org.example.backendproject.board.service; import jakarta.persistence.EntityManager; import java.util.List; import java.util.UUID; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.example.backendproject.board.dto.BoardDTO; import org.example.backendproject.board.entity.Board; import org.example.backendproject.board.repository.BatchRepository; import org.example.backendproject.board.repository.BoardRepository; import org.example.backendproject.user.entity.User; import org.example.backendproject.user.repository.UserRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Slf4j @Service @RequiredArgsConstructor public class BoardService { private final BoardRepository boardRepository; private final UserRepository userRepository; private final BatchRepository batchRepository; private final EntityManager em; /** 글 등록 **/ @Transactional public BoardDTO createBoard(BoardDTO boardDTO) { // userId(PK)를 이용해서 User 조회 if (boardDTO.getUser_id() == null) { throw new IllegalArgumentException("userId(PK)가 필요합니다!"); } // 연관관계 매핑! // 작성자 User 엔티티 조회 (userId 필요) User user = userRepository.findById(boardDTO.getUser_id()) .orElseThrow(() -> new IllegalArgumentException("작성자 정보가 올바르지 않습니다.")); // mysql 저장 Board board = new Board(); board.setTitle(boardDTO.getTitle()); board.setContent(boardDTO.getContent()); // 연관관계 매핑 board.setUser(user); Board saved = boardRepository.save(board); return toDTO(saved); } /** 게시글 상세 조회 **/ @Transactional(readOnly = true) public BoardDTO getBoardDetail(Long boardId) { Board board = boardRepository.findById(boardId) .orElseThrow(() -> new IllegalArgumentException("게시글 없음: " + boardId)); return toDTO(board); } /** 게시글 수정 **/ @Transactional public BoardDTO updateBoard(Long boardId, BoardDTO dto) { Board board = boardRepository.findById(boardId) .orElseThrow(() -> new IllegalArgumentException("게시글 없음: " + boardId)); board.setTitle(dto.getTitle()); board.setContent(dto.getContent()); boardRepository.save(board); return toDTO(board); } /** 게시글 삭제 **/ @Transactional public void deleteBoard(Long boardId) { if (!boardRepository.existsById(boardId)) { throw new IllegalArgumentException("게시글 없음: " + boardId); } boardRepository.deleteById(boardId); } /** 페이지 적용 전 **/ // 게시글 전체 목록 @Transactional(readOnly = true) public List<BoardDTO> getBoardList() { return boardRepository.findAll().stream() .map(this::toDTO) .collect(Collectors.toList()); } // 게시글 검색 페이징 아님 public List<BoardDTO> searchBoards(String keyword) { return boardRepository.searchKeyword(keyword); } /** 페이징 적용 후 **/ //페이징 전체 목록 public Page<BoardDTO> getBoards(int page, int size) { return boardRepository.findAllPaging(PageRequest.of(page, size)); //페이저블에 페이징에대한 정보를 담아서 레포지토리에 전달하는 역할 // return boardRepository.findAllWithDto(PageRequest.of(page, size, Sort.by("id").ascending())); //함수로 정렬 } //페이징 검색 목록 public Page<BoardDTO> searchBoardsPage(String keyword, int page, int size) { return boardRepository.searchKeywordPaging(keyword, PageRequest.of(page, size)); } /** 배치 작업 (JdbcTemplate) **/ @Transactional public void batchSaveBoard(List<BoardDTO> boardDTOList) { Long start = System.currentTimeMillis(); int batchsize = 1000; // 한번에 처리할 배치 크기 for (int i = 0; i < boardDTOList.size(); i += batchsize) { // i는 1000씩 증가 // 전체 데이터를 1000개씩 잘라서 배치리스트에 담는다. int end = Math.min(boardDTOList.size(), i + batchsize); // 두 개의 숫자 중 작은 수를 반환 List<BoardDTO> batchList = boardDTOList.subList(i, end); // 전체 데이터에서 1000씩 작업을 하는데 마지막 데이터가 1000개가 안 될수도 있으니 // Math.min()으로 전체 크기를 넘지 않게 마지막 인덱스를 계산해서 작업한다. // 내가 넣은 데이터만 엘라스틱서치에 동기화하기 위해 uuid 생성 String batchKey = UUID.randomUUID().toString(); for (BoardDTO dto : batchList) { dto.setBatchKey(batchKey); } // 1. MySQL로 INSERT batchRepository.batchInsert(batchList); } Long end = System.currentTimeMillis(); log.info("[BOARD][BATCH] 전체 저장 소요 시간(ms): {}", (end - start)); } // Entity → DTO 변환 private BoardDTO toDTO(Board board) { BoardDTO dto = new BoardDTO(); dto.setId(board.getId()); dto.setTitle(board.getTitle()); dto.setContent(board.getContent()); dto.setUser_id(board.getUser().getId()); dto.setUsername(board.getUser() != null ? board.getUser().getUserProfile().getUsername() : null); // ★ username! dto.setCreated_date(board.getCreated_date()); dto.setUpdated_date(board.getUpdated_date()); return dto; } @Transactional public void boardSaveAll(List<Board> boardList) { long start = System.currentTimeMillis(); for (int i = 0; i < boardList.size(); i++) { em.persist(boardList.get(i)); if (i % 1000 == 0) { em.flush(); em.clear(); } } long end = System.currentTimeMillis(); System.out.println("JPA Board SaveAll 저장 소요 시간(ms): " + (end - start)); } }
Java
복사

대량 배치 삽입 - 소요 시간 비교

JPA (1000개 기준)
소요 시간: 788ms
JdbcTemplate (1000개 기준)
소요 시간: 79ms
JPA (10000개 기준)
소요 시간 - 6.81s
JdbcTemplate 대량 배치 삽입
소요 시간: 432ms

오늘 푸시한 커밋 리스트

날짜
커밋 메시지
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24
2025-06-24