Spring에서 서버와 서버 간 HTTP 요청을 쉽게 처리할 수 있도록 제공하는 기능
기존에는 브라우저에서 서버로 요청을 보내는 구조였다면, RestTemplate을 사용하면 내 서버가 다른 서버에 요청을 보내는 Client 역할을 할 수 있음
Client
↓
내 서버
↓ RestTemplate
외부 서버 / API 서버
getForEntity()GET 방식으로 서버에 요청하고 응답을 ResponseEntity로 반환
restTemplate.getForEntity(uri, ItemDto.class)
UriComponentsBuilderURI를 편리하게 생성하기 위한 기능
UriComponentsBuilder
.fromUriString("http://localhost:7070")
.path("/api/server/get-call-obj")
.queryParam("query", query)
.build()
.toUri()
fromUriString() → 기본 URL 설정path() → 요청 경로 설정queryParam() → Query String 추가build() → URI 생성toUri() → URI 객체 변환응답 JSON을 Java 객체로 자동 변환할 수 있다는 점도 중요한 특징
postForEntity()POST 방식으로 서버에 데이터를 전달할 때 사용
restTemplate.postForEntity(
uri,
requestData,
ItemDto.class
)
Java 객체를 Body에 전달하면 JSON 형태로 자동 변환
exchange()GET, POST 등의 HTTP Method뿐만 아니라 Header, Body 등의 요청 정보를 직접 구성해야 할 때 사용
RequestEntity<User> requestEntity =
RequestEntity
.post(uri)
.header("X-Authorization", token)
.body(user);
restTemplate.exchange(requestEntity, String.class);
RequestEntityHTTP 요청에 필요한 정보를 하나의 객체로 구성
getForEntity(), postForEntity()보다 세부적인 HTTP 요청 설정이 필요한 경우 exchange() 사용
외부 서비스에서 제공하는 기능을 다른 개발자가 사용할 수 있도록 공개한 API
이번 실습에서는 Naver Shopping Open API를 사용해 상품 검색 기능 구현
API 사용 신청
↓
Client ID / Client Secret 발급
↓
API 요청
↓
JSON 응답
↓
Java 객체 변환
상품 검색 결과에서 사용한 주요 데이터
title → 상품명link → 상품 링크image → 상품 이미지lprice → 최저가Header에 인증 정보를 포함하여 요청
DB에서는 외래 키(FK)를 이용하여 테이블 간 관계를 표현
User
↑
│ FK
Food
DB 테이블은 JOIN을 사용하기 때문에 어느 테이블을 기준으로 조회하더라도 원하는 데이터를 조회 가능
반면 JPA Entity에서는 객체 간 참조 관계를 직접 설정해야 함
DB
Entity
JPA에서 연관관계를 설정할 때 가장 중요한 개념
외래 키를 실제로 관리하는 Entity
연관관계의 주인만 외래 키를 등록, 수정, 삭제할 수 있음
@JoinColumn외래 키의 주인이 사용하는 애너테이션
@JoinColumn(name = "user_id")
private User user;
mappedBy양방향 관계에서 연관관계의 주인이 아닌 쪽에 사용
@OneToMany(mappedBy = "user")
private List<Food> foodList;
mappedBy의 값은 연관관계의 주인이 가지고 있는 필드명
@JoinColumn
→ 연관관계의 주인
mappedBy
→ 연관관계의 주인이 아님
@OneToOneEntity 간 1 대 1 관계를 표현
@OneToOne
@JoinColumn(name = "user_id")
private User user;
1:1 관계에서는 N의 관계가 없기 때문에 외래 키의 주인을 직접 결정해야 함
@OneToOne
@JoinColumn(name = "user_id")
private User user;
@OneToOne(mappedBy = "user")
private Food food;
@ManyToOneEntity 간 N 대 1 관계를 표현
예를 들어 여러 음식이 하나의 고객에게 연결되는 경우
Food → User
Food 1 ─┐
Food 2 ─┼→ User
Food 3 ─┘
일반적으로 N 쪽 Entity가 외래 키의 주인
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
// Food
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
// User
@OneToMany(mappedBy = "user")
private List<Food> foodList;
실제 애플리케이션에서도 @ManyToOne을 자주 사용
@OneToManyEntity 간 1 대 N 관계를 표현
User
↓
Food
Food
Food
1:N 관계에서는 외래 키가 N 쪽 테이블에 존재하지만, 1 쪽 Entity가 연관관계의 주인이 될 수 있음
이 경우 추가적인 UPDATE SQL이 발생할 수 있기 때문에 일반적으로 @ManyToOne을 활용하는 구조를 선호
@ManyToManyEntity 간 N 대 M 관계를 표현
예를 들어
User N : M Food
한 명의 User가 여러 Food를 주문하고, 하나의 Food가 여러 User에게 주문될 수 있음
N:M 관계를 DB에서 처리하기 위해 중간 테이블 사용
User
↓
Order
↑
Food
@ManyToMany@ManyToMany
@JoinTable(...)
private List<User> userList;
간단하게 사용할 수 있지만 중간 테이블을 직접 관리하기 어렵다는 단점 존재
실무에서는 N:M 관계를 단순한 @ManyToMany로 처리하기보다 중간 Entity를 직접 만드는 방식을 사용할 수 있음
User 1 ─── N Order N ─── 1 Food
@Entity
public class Order {
@ManyToOne
private User user;
@ManyToOne
private Food food;
}
중간 테이블에 추가 정보 저장 가능
예:
즉, 단순한 연결 테이블이 아니라 하나의 의미 있는 Entity로 관리 가능
JPA는 연관된 Entity를 언제 조회할지 설정할 수 있음
지연 로딩
실제로 연관된 데이터가 필요한 시점에 조회
@ManyToOne(fetch = FetchType.LAZY)
즉시 로딩
Entity를 조회할 때 연관된 Entity까지 즉시 조회
@ManyToOne(fetch = FetchType.EAGER)
| 관계 | 기본값 |
|---|---|
@OneToMany | LAZY |
@ManyToMany | LAZY |
@ManyToOne | EAGER |
@OneToOne | EAGER |
연관된 데이터가 많을 수 있는 관계는 불필요한 조회를 줄이기 위해 지연 로딩 사용
지연 로딩은 실제 데이터가 필요한 시점에 조회하는 방식
User 조회
↓
User만 조회
↓
foodList 사용
↓
Food 조회
이때 지연 로딩된 Entity를 조회하려면 영속성 컨텍스트가 존재해야 함
즉, 일반적으로 트랜잭션이 유지되는 상태에서 지연 로딩된 데이터를 조회해야 함
Transaction
↓
Persistence Context
↓
Lazy Loading
Lazy Loading + Transaction
이 관계를 기억해둘 것
하나의 Entity에서 수행한 작업을 연관된 Entity에도 전파하는 기능
CascadeType.PERSIST부모 Entity를 저장할 때 연관된 Entity도 함께 저장
@OneToMany(
mappedBy = "user",
cascade = CascadeType.PERSIST
)
User save()
↓
Food도 함께 save()
CascadeType.REMOVE부모 Entity를 삭제할 때 연관된 Entity도 함께 삭제
cascade = CascadeType.REMOVE
User delete()
↓
Food도 함께 delete()
cascade = {
CascadeType.PERSIST,
CascadeType.REMOVE
}
orphanRemoval부모 Entity와의 연관관계가 끊어진 자식 Entity를 자동으로 삭제하는 기능
예를 들어
User
├─ Food 1
├─ Food 2
└─ Food 3
여기서 Food 2를 User의 컬렉션에서 제거하면
User
├─ Food 1
└─ Food 3
orphanRemoval = true 설정 시 DB에서도 Food 2가 삭제됨
@OneToMany(
mappedBy = "user",
orphanRemoval = true
)
Cascade REMOVE
부모 Entity 자체를 삭제할 때 자식 Entity도 삭제
orphanRemoval
부모와 자식의 연관관계가 끊어졌을 때 자식 Entity 삭제
REMOVE, orphanRemoval 사용 시 다른 Entity에서 해당 객체를 참조하고 있는지 확인 필요
이번 주에는 지금까지 학습한 내용을 활용하여 My Select Shop 프로젝트 구현
Naver Shopping Open API를 이용하여 상품 검색
검색어
↓
My Select Shop
↓ RestTemplate
Naver Shopping API
↓
상품 정보
↓
상품 목록 반환
POST /api/products
상품 정보를 DB에 저장
PUT /api/products/{id}
myprice만 수정
GET /api/products
회원이 저장한 관심상품 목록 조회
주요 필드
id
title
image
link
lprice
myprice
일정한 시간 간격으로 특정 작업을 자동 실행하는 기능
My Select Shop에서는 관심상품의 최저가를 주기적으로 확인하는 작업에 활용
@EnableScheduling
Scheduling 기능 활성화
@Scheduled(...)
특정 메서드를 주기적으로 실행
Scheduler
↓
Naver API 호출
↓
관심상품 최저가 조회
↓
DB 가격 업데이트
즉, 사용자가 직접 요청하지 않아도 정해진 시간에 서버에서 작업 수행
My Select Shop에 회원 기능 추가
username
password
↓
인증
↓
JWT 발급
↓
Client 저장
기존에 학습한 Spring Security + JWT 인증 방식을 프로젝트에 적용
관심상품은 특정 회원에게 귀속되어야 함
User
↓
Product
상품 Entity에 User와의 연관관계 설정
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
로그인한 사용자의 상품만 조회할 수 있도록 사용자 식별 정보와 데이터의 연관관계 설정
데이터가 많아질 경우 모든 데이터를 한 번에 조회하지 않고 페이지 단위로 조회
Spring Data JPA에서는 Pageable 사용
Pageable pageable =
PageRequest.of(page, size, sort);
page → 페이지 번호size → 페이지당 데이터 개수sort → 정렬 기준Page<Product>
상품 목록을 특정 기준으로 정렬
예:
ASC → 오름차순DESC → 내림차순Sort.Direction.ASC
Sort.Direction.DESC
Pageable
├─ page
├─ size
└─ sort
Spring Data JPA의 Repository 메서드에 Pageable을 전달하여 페이징과 정렬을 함께 처리
관심상품을 효율적으로 관리하기 위한 폴더 기능 추가
주요 정보
id
name
user
폴더와 회원은
Folder : User = N : 1
관계
회원 한 명이 여러 개의 폴더를 생성할 수 있음
상품 하나가 여러 폴더에 포함될 수 있고, 하나의 폴더에도 여러 상품이 포함될 수 있음
Product : Folder = N : M
ProductFolder Entity 생성
Product 1 ─── N ProductFolder N ─── 1 Folder
id
product
folder
N:M 관계를 중간 Entity를 통해 관리
POST /api/folders
회원이 원하는 폴더 생성
GET /api/user-folder
로그인한 회원이 생성한 폴더만 조회
회원 A가 생성한 폴더가 회원 B에게 노출되면 안 됨
따라서 로그인한 User와 Folder의 연관관계를 기준으로 조회
POST /api/products/{productId}/folder
상품과 폴더를 연결
상품 조회
↓
폴더 조회
↓
현재 로그인한 회원의 상품인지 확인
↓
현재 로그인한 회원의 폴더인지 확인
↓
중복 여부 확인
↓
ProductFolder 저장
단순히 ID만 받아 저장하는 것이 아니라 사용자 권한과 데이터 소유권까지 검증
회원은
을 구분하여 조회 가능
GET /api/folders/{folderId}/products
User
+
Folder ID
+
Pageable
회원과 폴더를 함께 조건으로 사용하여 현재 로그인한 회원의 특정 폴더에 포함된 상품만 조회
getForEntity()postForEntity()exchange()RequestEntityResponseEntityUriComponentsBuilder@JoinColumnmappedBy@OneToOne@ManyToOne@OneToMany@ManyToManyLAZYEAGERCascadeType.PERSISTCascadeType.REMOVEorphanRemovalRestTemplate
↓
외부 API 통신
↓
Naver Shopping API
↓
상품 데이터 조회
↓
JPA Entity 설계
↓
Entity 연관관계
↓
LAZY / EAGER
↓
Cascade / orphanRemoval
↓
My Select Shop 적용
↓
회원별 관심상품 관리
↓
페이징 / 정렬
↓
Folder / ProductFolder
단순히 JPA의 연관관계 애너테이션을 사용하는 것보다 왜 연관관계의 주인이 필요한지, 왜 중간 Entity를 사용하는지, 언제 지연 로딩을 사용하는지를 이해하는 것이 중요하다.
연관 관계 설정할 때 고려할 사항이 많아서 한 번 더 복습해야 할 것 같다.