카카오테크캠퍼스 STEP2 3주차 회고
이번 3주차의 주제는 기존 JDBCTemplate를 쓰던 코드를 JPA 로 변환하는 것이다.
JPA?
JPA는 자바 진영에서 ORM(Object-Relational-Mapping) 기술 표준으로 사용되는 인터페이스의 모음.
간략하게 어플리케이션의 객체를 RDB 테이블에 자동으로 영속화 해주는 것.
[Spring JPA] JPA 란?
이번 글에서는 JPA(Java Persistence API)가 무엇인지 알아보려고한다. JPA는 자바 진영에서 ORM(Object-Relational Mapping) 기술 표준으로 사용되는 인터페이스의 모음이다. 그 말은 즉, 실제적으로 구현된것이
dbjh.tistory.com
1단계 - 엔티티 매핑
지금까지 작성한 JdbcTemplate 기반 코드를 JPA로 리팩터링하고 실제 도메인 모델을 어떻게 구성하고 객체와 테이블을 어떻게 매핑해야 하는지 알아본다.
기존 JdbcTemplate를 이용하여 엔티티를 DB 테이블에 CRUD 할때에는 직접 SQL 쿼리문을 작성해줘야하는 번거로움이 있었다.
public class Gift {
private Long id;
private String name;
private int price;
private String imageUrl;
public Gift() {
}
public Gift(Long id, String name, int price, String imageUrl) {
if (!isValidName(name)) {
throw new IllegalArgumentException("카카오 문구는 MD와 협의 후 사용가능합니다.");
}
this.id = id;
this.name = name;
this.price = price;
this.imageUrl = imageUrl;
}
// setter/getter
private boolean isValidName(String name) {
return name != null && !name.contains("카카오");
}
}
@Repository
public class GiftDao {
private final JdbcTemplate jdbcTemplate;
public GiftDao(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void create(Gift gift) {
String sql = "INSERT INTO gift (name, price, imageUrl) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, gift.getName(), gift.getPrice(), gift.getImageUrl());
}
public Gift findById(Long id) {
String sql = "SELECT * FROM gift WHERE id = ?";
return jdbcTemplate.queryForObject(sql, (rs, rowNum) ->
new Gift(
rs.getLong("id"),
rs.getString("name"),
rs.getInt("price"),
rs.getString("imageUrl")
), id);
}
}
이렇게 INSERT ~ , SELCET ~ 와 같이 직접 DB에 저장시키는 쿼리문을 작성해줘야했는데, JPA를 쓰게되면 간단하게 ORM기술을 이용하여 저장시킬 수 있다.
@Entity
@Table(name = "gift")
public class Gift {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
private String name;
@NotNull
private int price;
@NotNull
@Column(name = "imageurl")
private String imageUrl;
@OneToMany(mappedBy = "gift", cascade = CascadeType.REMOVE)
protected List<Wish> wishes = new ArrayList<>();
public Gift() {
}
public Gift(String name, int price, String imageUrl) {
if (!isValidName(name)) {
throw new IllegalArgumentException("카카오 문구는 MD와 협의 후 사용가능합니다.");
}
this.name = name;
this.price = price;
this.imageUrl = imageUrl;
}
}
이렇게 애노테이션을 이용하여 db에 jpa가 자동으로 매핑하여 테이블을 생성할 수 있게 해줄 수 있다.
테이블 내부에 INSERT 나 SELECT 등의 CRUD 작업을 할려면 어떻게 해야할까?
JpaRespository를 사용하면된다.
@Repository
public interface GiftRepository extends JpaRepository<Gift, Long> {
}
JpaRespository를 상속받은 GiftRespository를 만들어주었다. 이를 이용하여 우리는 복잡한 Jdbc 코드를 사용하지않더라도 간단하게 DB에 데이터 접근 작업을 할 수 있게되었다.
@Service
public class GiftService {
private final GiftRepository giftRepository;
@Autowired
public GiftService(GiftRepository giftRepository) {
this.giftRepository = giftRepository;
}
public GiftResponse getGift(Long id) {
Gift gift = giftRepository.findById(id)
.orElseThrow(() -> new NoSuchElementException("Gift not found with id " + id));
return GiftResponse.from(gift);
}
이와 같이 서비스클래스에서 GiftRepository를 주입받아 giftRepository.findById(id) 메서드만으로 db 내의 데이터를 간단하게 가져올 수 있다.
@Service
public class GiftService {
private final GiftRepository giftRepository;
@Autowired
public GiftService(GiftRepository giftRepository) {
this.giftRepository = giftRepository;
}
public GiftResponse getGift(Long id) {
Gift gift = giftRepository.findById(id)
.orElseThrow(() -> new NoSuchElementException("Gift not found with id " + id));
return GiftResponse.from(gift);
}
public void addGift(GiftRequest giftRequest) {
Gift gift = giftRequest.toEntity();
giftRepository.save(gift);
}
@Transactional
public void updateGift(GiftRequest giftReq, Long id) {
Gift gift = giftRepository.findById(id)
.orElseThrow(() -> new NoSuchElementException("Gift not found with id " + id));
gift.modify(giftReq.getName(), giftReq.getPrice(), giftReq.getImageUrl());
giftRepository.save(gift);
}
public void deleteGift(Long id) {
giftRepository.deleteById(id);
}
}
이렇게 데이터 생성,읽기,수정,삭제의 CRUD 작업을 간단한 메서드를 통해 할 수 있게 바뀌었다.
@RestController
@RequestMapping("/api/gifts")
public class GiftController {
private GiftService giftService;
@Autowired
public GiftController(GiftService giftService) {
this.giftService = giftService;
}
@PostMapping
public ResponseEntity<String> addGift(@RequestBody GiftRequest giftReq) {
giftService.addGift(giftReq);
return ResponseEntity.status(HttpStatus.CREATED).body("Gift created");
}
@GetMapping("/{id}")
public ResponseEntity<GiftResponse> getGift(@PathVariable Long id) {
GiftResponse gift = giftService.getGift(id);
return ResponseEntity.ok(gift);
}
}
서비스클래스에서 작성한 메서드들을 HTTP API로 이용하기위해 컨트롤러에도 위와 같이 작성해주었다.
2단계 - 연관관계 매핑
지금까지 작성한 JdbcTemplate 기반 코드를 JPA로 리팩터링하고 실제 도메인 모델을 어떻게 구성하고 객체와 테이블을 어떻게 매핑해야 하는지 알아본다.
- 객체의 참조와 테이블의 외래 키를 매핑해서 객체에서는 참조를 사용하고 테이블에서는 외래 키를 사용할 수 있도록 한다.
먼저 연관관계란 무엇일까?
엔티티 간의 관계를 설정하고 관리하는 것. 엔티티 간의 관계를 통해 객체 지향 프로그래밍의 객체 관계와 데이터베이스의 테이블 관계를 매핑할 수 있다.
일대일,일대다,다대일,다대다 등의 연관관계가 존재한다.
https://jeong-pro.tistory.com/231
JPA 연관 관계 한방에 정리 (단방향/양방향, 연관 관계의 주인, 일대일, 다대일, 일대다, 다대다)
JPA에서 가장 중요한 것 JPA에서 가장 중요한 것을 뽑자면, "객체와 관계형 데이터베이스 테이블이 어떻게 매핑되는지를 이해하는 것"이라고 생각합니다. 🏅 왜냐하면 JPA의 목적인 "객체 지향 프
jeong-pro.tistory.com
유저(User),상품(Gift)과 위시리스트(Wish) 간의 연관관계를 생각해보자.
- 상품(Gift)은 위시리스트(Wish)와 일대다 관계
- 유저(User)는 위시리스트(Wish)와 일대다 관계
@Entity
@Table(name = "wish")
public class Wish {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
@NotNull
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "gift_id")
@NotNull
private Gift gift;
@Entity
@Table(name = "gift")
public class Gift {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
private String name;
@NotNull
private int price;
@NotNull
@Column(name = "imageurl")
private String imageUrl;
@OneToMany(mappedBy = "gift", cascade = CascadeType.ALL)
private List<Wish> wishes = new ArrayList<>();
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
@NotNull
private String email;
@NotNull
private String password;
Wish의 입장에서 Gift와 다대일이기 때문에 Wish가 Many Gift가 One이 된다.
Wish의 입장에서 User와 다대일이기 때문에 Wish가 Many User가 One이 된다.
현재는 유저id와 상품id의 참조를 통하여 위시리스트기능을 구현하였다.
(Gift의 wishes는 아직 쓸일이 없을 것같다)
create table gift
(
`id` BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
`name` VARCHAR(255) NOT NULL,
`price` INT NOT NULL,
`imageUrl` VARCHAR(255) NOT NULL
);
CREATE TABLE users
(
`id` BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
`email` VARCHAR(255) NOT NULL UNIQUE,
`password` VARCHAR(255) NOT NULL
);
CREATE TABLE wish
(
`id` BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
`user_id` BIGINT NOT NULL,
`gift_id` BIGINT NOT NULL,
`quantity` INT NOT NULL DEFAULT 1,
FOREIGN KEY (user_id) REFERENCES users (id),
FOREIGN KEY (gift_id) REFERENCES gift (id)
);
schema.sql 스크립트 파일에선 위와같이 테이블을 정의해줄 수 있다.
.
wish 테이블에서 유저id와 상품id를 외래키 참조해주고 있다.
아직 연관관계 매핑에 대해선 내용이 어렵기도하고 방대하여 앞으로 학습할 내용이 많을 것 같다.
3단계 - 페이지네이션
상품과 위시 리스트 보기에 페이지네이션을 구현한다.
- 대부분의 게시판은 모든 게시글을 한 번에 표시하지 않고 여러 페이지로 나누어 표시한다. 정렬 방법을 설정하여 보고 싶은 정보의 우선 순위를 정할 수도 있다.
- 페이지네이션은 원하는 정렬 방법, 페이지 크기 및 페이지에 따라 정보를 전달하는 방법이다.
페이지네이션이란?
페이지네이션은 대량의 데이터를 여러 페이지로 나누어 보여주는 기술이다.웹 애플리케이션에서 특히 중요한 기능으로, 많은 데이터를 한 번에 사용자에게 보여주는 대신 적절한 크기로 나누어 표시하여 사용자 경험을 개선하고 시스템 성능을 최적화할 수 있다. 예를 들어, 쇼핑몰에서 수천 개의 상품을 한 페이지에 모두 보여주기보다 20개씩 나누어 여러 페이지로 제공하는 방식.
우리는 JPA 의 Pageable 인터페이스를 통해 페이지네이션을 쉽게 구현할 수있다.
[JPA] Spring Data Jpa + Pageable로 Pagination 쉽게 구현하기
이번주는 독서리뷰를 남길 수 있는 미니 프로젝트를 진행하고 있다. 리뷰 글을 특정 조건으로 뽑아서 정렬 기준을 선택하여 조회하는 기능을 구현하고자 한다.전체 리뷰글을 조회하거나, 내가
velog.io
public interface WishRepository extends JpaRepository<Wish, Long> {
Page<Wish> findByUser(User user, Pageable pageable);
}
먼저 레포지토리에서 findByUser의 반환형을 Page<>로 받아준다.
public PagingResponse<Wish> getGiftsForUser(Long userId, int page, int size) {
PageRequest pageRequest = PageRequest.of(page - 1, size, Sort.by("id").ascending());
User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("Invalid user ID"));
Page<Wish> wishes = wishRepository.findByUser(user, pageRequest);
return new PagingResponse<>(page, wishes.getContent(), size, wishes.getTotalElements(), wishes.getTotalPages());
}
이후 서비스클래스에서 유저의 위시리스트를 받아오는 메서드를 Pageable의 구현체 중 하나인 PageRequest를 이용하여 페이지네이션을 사용하는 방식으로 변경해준다.
.
PageRequest.of(page - 1, size, Sort.by("id").ascending())
page -1 : 요청할 페이지의 번호. jpa에서의 페이지는 0부터 시작한다. 사용자가 1을 입력했을때 첫페이지가 보여지도록 설정한 것.
size : 한 페이지에 최대 몇개의 요소만큼 보여줄 것인지를 결정한다. 사용자가 입력한 숫자만큼 보여지도록 설정.
Sort.by("id").ascending() : id를 기준으로 오름차순으로 페이지 내에서 보여준다.
public class PagingRequest {
private int page = 1;
private int size = 5;
public PagingRequest(int page, int size) {
this.page = page;
this.size = size;
}
public PagingRequest() {
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
}
사용자에게 현재 페이지,페이지에 보여줄 항목의 수를 입력받을 PagingRequest (디폴트 값 page= 1 size = 5)
public class PagingResponse<T> {
private List<T> content;
private int page;
private int size;
private long totalElements;
private int totalPages;
public PagingResponse(int page, List<T> content, int size, long totalElements, int totalPages) {
this.page = page;
this.content = content;
this.size = size;
this.totalElements = totalElements;
this.totalPages = totalPages;
}
public List<T> getContent() {
return content;
}
public int getPage() {
return page;
}
public int getSize() {
return size;
}
public long getTotalElements() {
return totalElements;
}
public int getTotalPages() {
return totalPages;
}
}
pageable의 모든 정보를 보여주는것이 아닌 일부정보만 보여주기 위해 생성한 PagingResponse
@RestController
@RequestMapping("/api")
public class WishController {
private final WishService wishService;
@Autowired
public WishController(WishService wishService) {
this.wishService = wishService;
}
@GetMapping("/mywish")
public ResponseEntity<PagingResponse<WishResponse>> getUserGifts(@RequestAttribute("user") User user,
@ModelAttribute PagingRequest pagingRequest) {
if (user != null) {
PagingResponse<Wish> userWishes = wishService.getGiftsForUser(user.getId(), pagingRequest.getPage(), pagingRequest.getSize());
List<WishResponse> wishResponses =
userWishes.getContent()
.stream()
.map(wish -> new WishResponse(wish.getGift().getId(), wish.getGift().getName(), wish.getGift().getPrice(), wish.getQuantity()))
.collect(Collectors.toList());
PagingResponse<WishResponse> pagingResponse = new PagingResponse<>(pagingRequest.getPage(), wishResponses, pagingRequest.getSize(), userWishes.getTotalElements(), userWishes.getTotalPages());
return ResponseEntity.ok(pagingResponse);
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
컨트롤러에서 메서드를 만들어준뒤 Postman으로 요청을 보내보자.
이렇게 페이지네이션 구현이 완료되었다.
3주차 총평
기존 JdbcTemplate는 쿼리문이 코드에 섞여있어 가독성 부분에서 다소 난잡한 느낌이있었다. 처음 써본 ORM 기술인 JPA를 통해 코드를 훨씬 간결하고 가독성좋게 만들 수 있었고,신기한 기능들이 많았다.아직 데이터베이스에 대해서도 잘 모르는 단계라 이것저것 구글링하면서 알아보며 과제는 어찌저찌 진행했는데 앞으로 학습해야할 내용들이 너무나 많다는 걸 다시 깨닫게되었다. 특히 JPA를 학습하며 영속성 컨텍스트라는 개념이 등장하게되는데 이 부분도 처음 들어본 용어들이 많이 나와 제대로 이해하는 데에는 많은 시간이 필요할 것같다.이것저것 구글링으로 알아낸 정보를 통해 기능을 구현하는 것에 멈추지 않고, 그 기술에 대해 원리라던가 나오게된 배경 등을 깊게 알아보는 시간을 가져봐야겠다.
'KakaoTechCampus' 카테고리의 다른 글
[Spring] 페이징 처리 시 JPA N + 1 문제를 @EntityGraph로 해결해보자 (0) | 2024.11.09 |
---|---|
[Spring] 카카오페이 api 이용해서 테스트 결제 기능 구현하기 (1) | 2024.11.09 |
[카카오테크캠퍼스] STEP2 2주차 회고 (0) | 2024.07.08 |