일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- 디자인강의
- 백준
- 티스토리챌린지
- 내일배움카드
- 객체지향
- OPENPATH
- KDT
- 백엔드개발자
- 디자인챌린지
- 오픈패스
- 국비지원교육
- 환급챌린지
- 국비지원취업
- Be
- Java
- 국비지원
- baekjoon
- 오픈챌린지
- 부트캠프
- 디자인교육
- UXUI기초정복
- 내일배움캠프
- mysql
- 백엔드 부트캠프
- UXUI챌린지
- UXUIPrimary
- Spring
- 패스트캠퍼스
- 오블완
- 자바
- Today
- Total
군만두의 IT 공부 일지
[스터디7] 08. REST 엔드포인트 사용 본문
목차
14장. 스프링 데이터로 데이터 영속성 구현
이 장에서 다룰 내용
- 스프링 데이터의 작동 방식 이해하기
- 스프링 데이터의 리포지터리 정의하기
- 스프링 데이터 JDBC를 이용한 스프링 앱의 영속성 계층 구현하기
14.1 스프링 데이터란
- 스프링 데이터(Spring Data): 영속성 기술에 맞는 구현을 제공하는 스프링 생태계의 한 프로젝트
- 데이터 영속성을 구현하는 방법
- 드라이버 관리자를 이용하여 관계형 DBMS에 직접 연결하는 JDBC를 사용한다.
- JdbcTemplate을 사용하거나
- JDK 인터페이스(Statement, PreparedStatement, ResultSet 등)로 직접 작업할 수 있다.
- 하이버네이트(Hibernate) 같은 ORM 프레임워크를 사용한다.
- 다양한 NoSQL 기술 중 하나를 사용한다.
- 드라이버 관리자를 이용하여 관계형 DBMS에 직접 연결하는 JDBC를 사용한다.
스프링 데이터는 다음과 같이 영속성 계층 구현을 단순화한다.
- 다양한 영속성 기술에 대한 공통적인 추상화(인터페이스) 집합을 제공한다. 서로 다른 기술에 대한 영속성을 구현할 때 유사한 방식을 취할 수 있다.
- 스프링 데이터가 구현체를 제공하는 추상화만 사용하여 사용자가 영속성 연산 작업을 구현할 수 있다. 코드를 더 적게 작성할 수 있으므로 앱 기능을 더 빠르게 구현할 수 있고, 앱의 이해와 유지 관리가 쉽다.
14.2 스프링 데이터의 작동 방식
스프링 데이터 프로젝트는 한 기술 또는 다른 기술을 위한 다양한 모듈을 제공한다. 모듈은 서로 독립적이며, 다른 메이븐 의존성을 사용하여 프로젝트에 추가할 수 있다. 따라서 앱을 구현할 때 독립된 스프링 데이터 의존성은 제공되지 않는다.
- 스프링 데이터 프로젝트는 스프링 데이터가 지원하는 영속성 방식에 따라 하나의 메이븐 의존성을 제공한다. 스프링 데이터 JDBC 모듈을 사용하여 JDBC로 DBMS에 직접 연결하거나, 스프링 데이터 Mongo 모듈을 사용하여 MongoDB 데이터베이스에 연결할 수 있다.
- 앱에서 사용하는 영속성 기술이 어떤 것이든 스프링 데이터는 앱 영속성 기능을 정의하기 위해 확장하는 공통된 인터페이스(계약) 집합을 제공한다.
- Repository는 가장 추상적인 계약이다. 계약을 확장하면 앱은 사용자가 작성한 인터페이스를 특정 스프링 데이터 리포지토리로 인식한다. 하지만 새 레코드 추가, 모든 레코드 조회, 기본 키로 레코드 가져오기 같은 어떤 사전 정의된 연산 작업도 상속되지 않는다. Repository 인터페이스는 어떤 메서드도 선언하지 않는 마커 인터페이스(marker interface)다.
- CrudRepository는 일부 영속성 기능도 제공하는 가장 간단한 스프링 데이터 계약이다. 이 계약을 확장하여 앱 영속성 기능을 정의하면 레코드를 생성, 검색, 업데이트, 삭제하는 가장 간단한 연산 작업을 수행할 수 있다.
- PagingAndSortingRepository는 CrudRepository를 확장하여 레코드를 정렬하거나 특정 수(페이지) 단위로 조회하는 것과 관련된 연산 작업을 추가한다.
- 스프링 데이터는 서로 확장하는 여러 인터페이스를 제공함으로써 앱에 필요한 연산만 구현할 수 있게 해준다. 이런 접근 방식은 인터페이스 분리(interface segregation)라고 알려진 원칙이다.
- 예) 앱에서 CRUD 연산만 사용해야 할 때는 CrudRepository 계약을 확장한다. 앱은 레코드 정렬 및 페이징과 관련된 연산을 가져오지 않으므로 단순해진다. 앱에 단순한 CRUD 연산 외에 페이지네이션 및 정렬 기능이 필요하다면 보다 구체적인 계약 형태인 PagingAndSortingRepository 인터페이스를 확장해야 한다.
- 일부 스프링 데이터 모듈은 해당 모듈이 나타내는 기술에 대한 특정 계약을 제공할 수 있다.
- 예) 스프링 데이터 JPA를 사용하면 JpaRepository 인터페이스를 직접 확장 할 수도 있다. JpaRepository 인터페이스는 PagingAndSortingRepository보다 더 구체적인 계약이다. 이 계약에는 하이버네이트 같은 특정 기술을 사용할 때만 적용 가능한 연산(메서드)이 추가되며, 이 기술은 JPA(Jakarta Persistence API) 명세를 구현한다.
- 예) MongoDB 같은 NoSQL 기술을 사용하는 방법도 있다. MongoDB와 함께 스프링 데이터를 사용하려면 앱에 스프링 데이터 Mongo 모듈을 추가해야 하며, 이 모듈은 영속성 기술에 특정한 연산 을 추가하는 MongoRepository라는 특정 계약도 제공한다.
- 앱이 특정 기술을 사용할 때는 해당 기술에 특화된 연산을 제공하는 스프링 데이터 계약을 확장한다. 앱에 CRUD 연산이 필요하지 않을 때도 CrudRepository를 구현할 수 있지만, 일반적으로 특정 계약은 특정 기술을 사용하기에 편리한 솔루션을 제공한다.
14.3 스프링 데이터 JDBC 사용
스프링 데이터 JDBC로 스프링 앱의 영속성 계층을 구현한다. 일반 리포지터리를 구현하는 것 외에도 사용자 정의 리포지터리 연산 작업을 생성하고 사용한다.
- 사용자 계좌를 관리하는 전자 지갑을 구현할 것이다. 사용자는 자신의 계좌에서 다른 계좌로 돈을 이체할 수 있다. 사용자가 한 계좌에서 다른 계좌로 이체할 수 있는 이체 사용 사례를 구현한다.
- 이체 연산 단계
- 발신인 계좌에서 지정된 금액을 인출한다.
- 수취인 계좌에 해당 금액을 입금한다.
- 계정 테이블의 필드
- id: 기본 키(primary key). 자체 증가하는 INT 값으로 정의한다.
- name: 계좌 소유자 이름
- amount: 소유자가 계좌에 보유한 금액
// 1. pom.xml 의존성 추가
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>sq-ch14-ex1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>sq-ch14-ex1</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
// 2. 계좌 테이블의 레코드를 모델링하는 Account 클래스
package com.example.model;
import org.springframework.data.annotation.Id;
import java.math.BigDecimal;
public class Account {
@Id
private long id;
private String name;
private BigDecimal amount;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
}
모델 클래스가 생겼으므로 스프링 데이터 리포지터리를 구현할 수 있다. 이 애플리케이션은 CRUD 연산만 필요하므로 CrudRepository 인터페이스를 확장하는 인터페이스를 작성한다. 이때 스프링 데이터 인터페이스는 제공해야 하는 제너릭 타입이 두 개 있다.
- 리포지터리를 작성하는 모델 클래스(엔티티)
- 기본 키 필드 타입
메서드 이름을 쿼리로 변환하는 것은 몇 가지 단점 때문에 개발자가 스프링 데이터에 의존하여 메서드 이름을 쿼리로 변환하는 대신 쿼리를 명시적으로 작성할 것을 권장한다.
- 메서드 이름에 의존할 때 단점
- 연산 작업에 더 복잡한 쿼리가 필요할 때는 메서드 이름이 너무 길어 어려울 수 있다.
- 개발자가 실수로 메서드 이름을 리팩터링하면 자기도 모르게 앱 동작에 영향을 미칠 수 있다.
- 메서드 이름을 작성할 때 힌트를 제공하는 IDE가 없다면 스프링 데이터의 명명 규칙을 학습해야 한다.
- 스프링 데이터는 메서드 이름을 쿼리로 변환해야 하기 때문에 앱 초기화가 느려져 성능에 영향을 미친다.
이 문제를 피하는 간단한 방법은 @Query 애너테이션을 사용하여 해당 메서드를 호출할 때 실행할 SQL 쿼리를 지정하는 것이다. 스프링 데이터는 메서드 이름을 쿼리로 변환하는 대신 사용자가 제공한 쿼리를 사용한다.
// 3. 스프링 데이터 리포지터리 정의하기
package com.example.repositories;
import com.example.model.Account;
import org.springframework.data.jdbc.repository.query.Modifying;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import java.math.BigDecimal;
import java.util.List;
public interface AccountRepository extends CrudRepository<Account, Long> {
@Query("SELECT * FROM account WHERE name = :name")
List<Account> findAccountsByName(String name);
@Modifying
@Query("UPDATE account SET amount = :amount WHERE id = :id")
void changeAmount(long id, BigDecimal amount);
}
// 4. 서비스 클래스에 리포지터리 주입하기
package com.example.services;
import com.example.exceptions.AccountNotFoundException;
import com.example.model.Account;
import com.example.repositories.AccountRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
@Service
public class TransferService {
private final AccountRepository accountRepository;
public TransferService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
@Transactional
public void transferMoney(long idSender, long idReceiver, BigDecimal amount) {
Account sender = accountRepository.findById(idSender)
.orElseThrow(() -> new AccountNotFoundException());
Account receiver = accountRepository.findById(idReceiver)
.orElseThrow(() -> new AccountNotFoundException());
BigDecimal senderNewAmount = sender.getAmount().subtract(amount);
BigDecimal receiverNewAmount = receiver.getAmount().add(amount);
accountRepository.changeAmount(idSender, senderNewAmount);
accountRepository.changeAmount(idReceiver, receiverNewAmount);
}
public Iterable<Account> getAllAccounts() {
return accountRepository.findAll();
}
public List<Account> findAccountsByName(String name) {
return accountRepository.findAccountsByName(name);
}
}
// 5. REST 엔드포인트로 계좌 이체 사용 사례 노출하기
package com.example.controllers;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.dto.TransferRequest;
import com.example.model.Account;
import com.example.services.TransferService;
@RestController
public class AccountController {
private final TransferService transferService;
public AccountController(TransferService transferService) {
this.transferService = transferService;
}
@PostMapping("/transfer")
public void transferMoney(
@RequestBody TransferRequest request
) {
transferService.transferMoney(
request.getSenderAccountId(),
request.getReceiverAccountId(),
request.getAmount());
}
@GetMapping("/accounts")
public Iterable<Account> getAllAccounts(
@RequestParam(required = false) String name
) {
if (name == null) {
return transferService.getAllAccounts();
} else {
return transferService.findAccountsByName(name);
}
}
}
애플리케이션을 시작하고 데이터베이스의 모든 계좌를 반환하는 /accounts 엔드포인트를 호출하여 계좌 레코드를 확인한다.
curl http://localhost:8080/accounts
[
{
"id":1,
"name":"Jane Down",
"amount":1000.0
},
{
"id":2,
"name":"John Read",
"amount":1000.0
}
]
다음 명령으로 /transfer 엔드포인트 호출하여 제인이 존에게 100달러를 이체한다.
curl -XPOST -H "content-type:application/json" -d "{ \"senderAccountId\":1, \"receiverAccountId\":2, \"amount\":100}" http://localhost:8080/transfer
/accounts 엔드포인트를 다시 호출하여 차이를 확인한다. 제인은 900달러만 갖고 존은 1100달러를 갖는다.
curl http://localhost:8080/accounts
[
{
"id":1,
"name":"Jane Down",
"amount":900.0
},
{
"id":2,
"name":"John Read",
"amount":1100.0
}
]
이 글은 『스프링 교과서』 책을 학습한 내용을 정리한 것입니다.
'프로그래밍 > Java' 카테고리의 다른 글
[스터디10] 01. JPA 소개 (0) | 2025.06.28 |
---|---|
[스터디7] 09. 아키텍처 방식 및 XML, HTTP, JSON (0) | 2025.06.19 |
[스터디8] 06. 열거 타입과 애너테이션 (3) | 2025.06.07 |
[스터디7] 07. REST 엔드포인트 사용 (1) | 2025.06.04 |
[스터디7] 06. REST 서비스 구현 (0) | 2025.05.24 |