[Springboot] RestTemplate을 사용한 MSA 프로젝트 만들기 (서버간 통신, JPA, H2 DB, Scheduler, FileIO)
오늘은 RestTemplate을 사용하여 요즘 매우 핫하고 확장성이 뛰어난 MSA(Microservice Architecture) 프로젝트를 만들어 보았습니다.
WAS 2개를 사용하여 서버간 통신으로 데이터를 API로 전송하는 서비스를 구현하며 정리했습니다.
(실제로 요구사항이 들어왔을 때를 가정하고 프로젝트를 만들어 보았습니다. )
📝 목차
1. 데이터를 전송하는 localhost:8081 서버와 API 만들기
2. 데이터를 받는 localhost:8080 서버와 API 만들기
3. 테스트
👇 RestTemplate 과 동기식 요청이란
🙋♀️ RestTemplate 이란?
HTTP 통신을 위한 도구로 RESTful API 웹 서비스와의 상호작용을 쉽게 외부 도메인에서 데이터를 가져오거나 전송할 때 사용되는 스프링 프레임워크의 클래스를 의미합니다.
- 다양한 HTTP 메서드(GET, POST, PUT, DELETE 등)를 사용하며 원격 서버와 ‘동기식 방식’으로 JSON, XML 등의 다양한 데이터 형식으로 통신합니다.
- 동기식 방식으로 요청을 보내고 응답을 받을 때까지 블로킹되며, 요청과 응답이 완료되기 전까지 다음 코드로 진행되지 않습니다. 원격 서버와 통신할 때는 응답을 기다리는 동안 대기해야 합니다.
🙋♀️ 동기식 요청(Synchronous request) & 블로킹 요청(Blocking Request) 이란?
- RestTemplate은 기본적으로 ‘동기식 요청’ 처리를 수행하며 요청을 보내고 응답을 받을 때까지 블로킹됩니다.
- 클라이언트에서 요청을 보내면 결과가 반환될 때까지 대기를 하는 것을 의미합니다. unblocked 상태가 되면 다음 요청에 대해 처리를 수행합니다.
- 비동기식 요청 & 논 블로킹 요청도 있는데, 이 기능은 webflux에서 제공하는 Non-Blocking Request을 수행하면서 요청을 보내고 결과가 반환되지 않더라도 다른 작업을 수행할 수 있습니다.
💁♀️ 3가지 요구사항
1. 서버 1(localhost:8081)는 로컬 저장소에 있는 csv 파일을 FileIO InputStream을 사용하여 Dto로 변환할 것
2. 스케줄러로 읽은 데이터를 아래의 2개 로직을 구현하여 변환한 Dto를 10분마다 서버2 로 보낼 것
1) 금액의 총 합계 구하기
2) 코드를 기준으로 정렬 (Comparator 사용)하여 리스트에 담기
3. 서버2 는 JPA를 사용하여 H2 DB 에 insert 할 것
💻 사용한 버전 및 기술
- spring boot : 3.3.1
- java : 17
- Maven
- Spring RestTemplate
- JPA
- H2 DB
- Scheduler
- FileIO (java.nio)
1. 데이터를 전송하는 localhost:8081 서버 만들기
1) BankController
스케줄러를 사용하여 10분마다 전송하는 컨트롤러 입니다.
package com.example.demo.bankTransaction_240706;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
/**
* localhost:8081 서버 (데이터를 보내는 서버)
*/
@RequiredArgsConstructor
@RestController
@Slf4j
public class BankController {
private final BankService bankService;
// 10분마다 history.csv 파일 읽어 RestTemplate 으로 localhost:8080으로 쏘기
@Scheduled(fixedRate = 600000) // 10분마다 실행 (600,000ms)
public void sendFileData() throws IOException {
bankService.uploadDepositTotalToDB();
}
// 10분마다 history.csv 파일 읽어 RestTemplate 으로 localhost:8080으로 쏘기
@Scheduled(fixedRate = 600000) // 10분마다 실행 (600,000ms)
public void uploadSortedListToDB() throws IOException {
bankService.uploadSortedListToDB();
}
}
2) BankService
RestTemplate을 사용하는 핵심 로직입니다.
restTemplate.postForObject 를 사용하여 특정 URL로 DTO를 보내는 과정입니다.
원하는 URL은 static final로 명시적으로 상수 선언을 해주었습니다.
여기서 간단한 아래 2가지 로직을 구현했습니다.
1) 금액의 총 합계 2) 코드 기준 정렬 (Comparator 사용) 한 리스트
package com.example.demo.bankTransaction_240706;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* RestTemplate 을 사용하여 특정 URL로 원하는 데이터를 전송하는 로직
*/
@Service
@RequiredArgsConstructor
public class BankService {
private final RestTemplate restTemplate = new RestTemplate();
private static final String TOTAL_DEPOSIT_URL = "http://localhost:8080/bank/deposit/total/upload";
private static final String SORTED_USE_CODE_URL = "http://localhost:8080/bank/useCode/sort/upload";
// 1. 전체 입출금액의 total을 전송
public void uploadDepositTotalToDB() throws IOException {
List<BankTransaction> transactions = FileService.convertToBT();
long totalDeposit = 0;
for (BankTransaction transaction : transactions) {
totalDeposit += transaction.getDeposit();
}
// 특정 URL로 BankTransactionDto 전송 🎯
restTemplate.postForObject(TOTAL_DEPOSIT_URL, new BankTransactionDto(totalDeposit), BankTransactionDto.class);
System.out.println(new BankTransactionDto(totalDeposit));
}
// 2. 사용구분코드(A1, A2, A3) 순으로 정렬된 리스트 전송
public void uploadSortedListToDB() throws IOException {
List<BankTransaction> transactions = FileService.convertToBT();
Collections.sort(transactions, new Comparator<BankTransaction>() {
@Override
public int compare(BankTransaction o1, BankTransaction o2) {
return o1.getUseCode().compareTo(o2.getUseCode());
}
});
// 특정 URL로 BankTransactionDto 전송 🎯
restTemplate.postForObject(SORTED_USE_CODE_URL, new BankTransactionDto(transactions), BankTransactionDto.class);
System.out.println(new BankTransactionDto(transactions));
}
}
3) history.csv 파일
아래 파일은 로컬 저장소에 저장되는 파일입니다. 저는 스프링 프로젝트 resources 폴더에 저장하였습니다. 스프링 프로젝트가 아닌 아무 저장소 (바탕화면 등)에 해도 무관합니다.
30-01-2017,-100,Deliveroo,A1
30-01-2017,-50,Tesco,A2
01-02-2017,6000,Salary,A3
02-02-2017,2000,Royalties,A2
02-02-2017,-4000,Rent,A1
03-02-2017,3000,Tesco,A3
05-02-2017,-30,Cinema,A2
4) FileService
위 csv 파일을 읽어와서 List<BankTransaction>으로 변환하는 클래스입니다.
new File(경로)를 사용하여 파일을 읽어오고, file.getPath() 를 사용하여 편리하게 파일을 한 줄 한 줄 읽어올 수 있습니다.
package com.example.demo.bankTransaction_240706;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 로컬 경로에 있는 history.csv 파일을 List<BankTransaction>로 만들어서 반환
*/
@Service
@RequiredArgsConstructor
public class FileService {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy");
public static List<BankTransaction> convertToBT() throws IOException {
File file = new File("C:\\java\\hs_study\\bank_transaction_restTemplate\\src\\main\\resources\\history.csv");
Path filePath = Path.of(file.getPath()); // 1. Path객체로 filePath 읽고
List<String> lines = Files.readAllLines(filePath); // 2. Files를 직접 readAllLines
List<BankTransaction> bankTransactions = new ArrayList<>();
for (String line : lines) {
String[] values = line.split(",");
if (values.length == 4) {
try {
Date date = dateFormat.parse(values[0]);
long deposit = Long.parseLong(values[1]);
String description = values[2];
String useCode = values[3];
BankTransaction bankTransaction = new BankTransaction(date, deposit, description, useCode);
bankTransactions.add(bankTransaction);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
return bankTransactions;
}
}
5) BankTransactionDto
RestTemplate으로 보낼 최종 Dto 입니다.
저는 데이터의 총합인 depositTotal, 코드로 정렬된 리스트인 sortedBankTransaction 리스트를 필드로 잡았습니다.
package com.example.demo.bankTransaction_240706;
import lombok.*;
import java.util.List;
@Data
@NoArgsConstructor
public class BankTransactionDto {
private long depositTotal;
private List<BankTransaction> sortedBankTransaction;
public BankTransactionDto(long depositTotal) {
this.depositTotal = depositTotal;
}
public BankTransactionDto(List<BankTransaction> sortedBankTransaction) {
this.sortedBankTransaction = sortedBankTransaction;
}
}
6) BankTransaction
파일에서 읽어온 상세 데이터 Dto 입니다.
package com.example.demo.bankTransaction_240706;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Date;
@Data
@Service
@RequiredArgsConstructor
public class BankTransaction {
private Date date;
private long deposit; //입출금액
private String description; // 사용내역
private String useCode; // 사용구분코드
public BankTransaction(Date date, long deposit, String description, String useCode) {
this.date = date;
this.deposit = deposit;
this.description = description;
this.useCode = useCode;
}
}
이제 10분마다 데이터가 전송되는 서버 1개를 완성했습니다.
2. 데이터를 받는 localhost:8080 서버 만들기
이제 데이터를 받는 서버를 만들어보겠습니다.
먼저 pom.xml 파일 에 H2 DB 및 JPA 설정을 해줍니다.
<!-- H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Boot JDBC Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- JPA dependency -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
application.properties 파일에도 의존성을 추가해줍니다.
spring.application.name=java-test
# H2 Database settings
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.platform=h2
# H2 Console settings
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# JPA configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
1) BankController
8081에서 보내는 json 데이터를 받는 컨트롤러 입니다.
@ResponseBody 어노테이션으로 역직렬화시켜 Dto를 받아줍니다.
package com.hj.hs_study.bankTransaction_240706;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
/**
* localhost:8080 서버 (데이터를 받는 서버)
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/bank")
@Slf4j
public class BankController {
private final BankService bankService;
// 1. localhost:8081에서 10분마다 전송되는 [전체 입출금액의 total]를 H2 DB에 저장
@PostMapping("/deposit/total/upload")
public void saveDepositTotalUpload(@RequestBody BankTransactionDto bankTransactionDto) throws IOException {
log.info(String.valueOf(bankTransactionDto));
bankService.saveDepositTotalUpload(bankTransactionDto);
}
// 2. localhost:8081에서 10분마다 전송되는 [useCode 정렬된 리스트]를 H2 DB에 저장
@PostMapping("/useCode/sort/upload")
public void saveSortedListUpload(@RequestBody BankTransactionDto bankTransactionDto) throws IOException {
log.info(String.valueOf(bankTransactionDto));
bankService.saveSortedListUpload(bankTransactionDto);
}
}
2) BankService
컨트롤러로부터 받은 Dto 데이터를 H2 DB에 insert 하는 로직입니다.
package com.hj.hs_study.bankTransaction_240706;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@Service
@RequiredArgsConstructor
public class BankService {
private final BankRepository bankRepository;
// 1. localhost:8081에서 10분마다 전송되는 [전체 입출금액의 total]를 H2 DB에 저장
public void saveDepositTotalUpload(BankTransactionDto bankTransactionDto) {
bankRepository.save(bankTransactionDto);
}
// 2. localhost:8081에서 10분마다 전송되는 [useCode 정렬된 리스트]를 H2 DB에 저장
public void saveSortedListUpload(BankTransactionDto bankTransactionDto) {
bankRepository.save(bankTransactionDto);
}
}
3) BankRepository
JPA를 extends 하여 선언합니다. 따로 xml 쿼리를 작성하지 않아도 됩니다.
JpaRepository안의 제네릭으로는 @Entity를 붙인 Dto를 넣어주어야 합니다.
package com.hj.hs_study.bankTransaction_240706;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface BankRepository extends JpaRepository<BankTransactionDto, Long> {
}
4) BankTransaction
8081 서버와 거의 동일하지만, JPA를 사용하기 때문에 @Entity 어노테이션을 붙여 해당 클래스가 엔티티라는 것을 지정합니다.
식별자에는 @Id, 리스트를 만들 Dto가 있다면 @ManyToOne 을 지정을 해주어 JPA 가 인식할 수 있도록 해줍니다.
해주지 않으면 JPA 에러가 납니다.
package com.hj.hs_study.bankTransaction_240706;
import jakarta.persistence.*;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Date;
@Data
@Service
@RequiredArgsConstructor
@Entity
public class BankTransaction {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
//LocalDate date;
private Date date;
private long deposit; //입출금액
private String description; // 사용내역
private String useCode; // 사용구분코드
@ManyToOne
@JoinColumn(name = "bank_transaction_dto_id")
private BankTransactionDto bankTransactionDto;
public BankTransaction(Date date, long deposit, String description, String useCode) {
this.date = date;
this.deposit = deposit;
this.description = description;
this.useCode = useCode;
}
}
5) BankTransactionDto
최종 Dto입니다. 마찬가지로 @Entity, @Id, @OneToMany 를 선언하여 작성합니다.
package com.hj.hs_study.bankTransaction_240706;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@Entity
public class BankTransactionDto {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private long depositTotal;
@OneToMany(mappedBy = "bankTransactionDto", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<BankTransaction> sortedBankTransaction;
public BankTransactionDto(long depositTotal) {
this.depositTotal = depositTotal;
}
public BankTransactionDto(List<BankTransaction> sortedBankTransaction) {
this.sortedBankTransaction = sortedBankTransaction;
}}
3. 테스트
이제 모든 작업이 끝났습니다.
이제 10분마다 스케줄러가 파일의 데이터를 잘 읽어서 ,
아래의 로컬 저장소에 있는 csv 파일이 정말 총 합계 금액이 계산되어 DB에 저장되는지, 정렬된 리스트가 순서대로 DB에 들어가는지 확인해보겠습니다.
총 합계 금액인 6820 이 DB 에 잘 저장이 된 것을 볼 수 있습니다.
(처음에 0인 이유는 2가지 로직을 하나씩 전송해서 1가지 로직을 전송할 때에 다른 로직은 없기 때문입니다.)
정렬된 데이터도 순서대로 잘 insert가 된 것을 볼 수 있습니다.
여기까지 WAS 2개를 만들어 간단한 MSA 프로젝트를 만들어보았습니다.
오늘은 RestTemplate를 사용하여 동기식 요청으로 구현했지만,
다음에는 조금 더 진화한 방식인 Webflux의 비동기식 요청으로 구현한 MSA 프로젝트를 구현해 볼 예정입니다.