[HTTP Client] 예제 HttpURLConnection, Apache HttpClient, RestTemplate, OpenFeign, WebClient, RestClient
Spring Cloud를 활용한 Http Client 예제를 이어서 작성하겠습니다.
student-service와 school-service에 공통적으로 적용할 yml 파일을 config-server에 작성합니다.
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/students
username: ride
password: password
jpa:
hibernate:
ddl-auto: validate
database: postgresql
database-platform: org.hibernate.dialect.PostgreSQLDialect
flyway:
baseline-on-migrate: true
enabled: true
baseline-description: "init"
baseline-version: 0
user: ${spring.datasource.username}
password: ${spring.datasource.password}
management:
tracing:
sampling:
probability: 1.0
ddl-auto를 validate로 설정하여 스키마 간 불일치가 발생해도 이를 수정하지 않습니다.
update로 설정하면, 스키마와 엔티티 불일치 시 DB가 변경되며 데이터에 손실이 발생할 수 있지만,
validate로 설정 시 스키마와 엔티티가 불일치하면, 애플리케이션이 실행되지 않을 수 있습니다.
이때 flyway와 같은 마이그레이션 도구를 활용하여 DB 스키마에 대한 버전 관리를 하는 것으로,
불일치를 대처합니다.
(개발 편의상 update를 적용할 수도 있습니다)
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/schools
username: ride
password: password
jpa:
hibernate:
ddl-auto: validate
database: postgresql
database-platform: org.hibernate.dialect.PostgreSQLDialect
flyway:
baseline-on-migrate: true
enabled: true
baseline-description: "init"
baseline-version: 0
user: ${spring.datasource.username}
password: ${spring.datasource.password}
application:
config:
student-url: http://localhost:8222/api/v1/students/school
management:
tracing:
sampling:
probability: 1.0
application.config.student-url을 설정하여,
Http Client 통신에 사용합니다.
0.1. Student
student 프로젝트를 생성하고 빌드합니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'io.micrometer:micrometer-tracing-bridge-brave'
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.flywaydb:flyway-database-postgresql'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
분산추적을 위한 zipkin, 스키마와 DB 관리를 명시적으로 하기 위한 flyway 가 주요 의존성입니다.
flyway의존성이 특정 DB를 지원하지 않는 오류가 발생하여 flyway-database 의존성을 사용했습니다.
1.application.yml
application.yml을 작성합니다.
server:
port: 8090
spring:
application:
name: student-service
config:
import: optional:configserver:http://localhost:8888
server.port 를 통해 수신 포트를 설정합니다.
student 서비스를 8090 포트에서 수신할 것이며,
spring.application.name을 student-service로 식별할 수 있도록 합니다.
(config-server에 작성했던 student 의 yml 파일이름과 동일하게 명명합니다)
spring.config.import 설정을 통해 외부 서버(config-server)로부터 설정을 가져옵니다.
optional: 을 통해 configserver에 문제가 있어도 애플리케이션이 동작되도록 합니다.
configserver 를 통해 Spring Cloud Config 서버에서 설정을 가져도록 합니다.
8888은 config-server의 port입니다.
이는 student의 설정파일을 config-server로부터 가져오겠다는 것을 의미합니다.
2. Entity
package com.ride.http_client;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.*;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
@Entity
public class Student {
@Id
@GeneratedValue
private Integer id;
private String firstname;
private String lastname;
private String email;
private Integer schoolId;
}
School 과 연관된 필드를 만들어서 schoolId로 student를 조회할 수 있도록 합니다.
3. Repository
package com.ride.http_client;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface StudentRepository extends JpaRepository<String, Integer> {
List<Student> findAllBySchoolId(Integer schoolId);
}
Entity와 마찬가지로 SchoolId로 Student를 조회할 수 있도록 메서드를 생성합니다.
4. Service
package com.ride.http_client;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class StudentService {
private final StudentRepository repository;
public Integer saveStudent(Student student) {
return repository.save(student).getId();
}
public List<Student> findAllStudents() {
return repository.findAll();
}
public List<Student> findAllStudentBySchoolId(Integer schoolId) {
return repository.findAllBySchoolId(schoolId);
}
}
5. Controller
package com.ride.http_client;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/students")
public class StudentController {
private final StudentService service;
@GetMapping
public ResponseEntity<List<Student>> findAllStudents() {
return ResponseEntity.ok(service.findAllStudents());
}
@PostMapping
public ResponseEntity<Integer> saveStudent(
@RequestBody Student student
) {
return ResponseEntity.ok(service.saveStudent(student));
}
@GetMapping("/school/{school-id}")
public ResponseEntity<List<Student>> findAllStudentsBySchoolId(
@PathVariable("school-id") Integer schoolId
) {
return ResponseEntity.ok(service.findAllStudentBySchoolId(schoolId));
}
}
6. flyway
flyway로 DB 스키마를 관리합니다.
-- init_database.sql
create table if not exists student
(
id integer not null primary key,
firstname varchar(255),
lastname varchar(255),
email varchar(255),
school_id integer
);
create sequence if not exists student_seq increment by 50;
-- insert_student_data.sql
INSERT INTO student (id, firstname, lastname, email, school_id) VALUES (nextval('student_seq'), 'John', 'Doe', 'john.doe@example.com', 1);
INSERT INTO student (id, firstname, lastname, email, school_id) VALUES (nextval('student_seq'), 'Jane', 'Doe', 'jane.doe@example.com', 1);
INSERT INTO student (id, firstname, lastname, email, school_id) VALUES (nextval('student_seq'), 'Alice', 'Smith', 'alice.smith@example.com', 2);
INSERT INTO student (id, firstname, lastname, email, school_id) VALUES (nextval('student_seq'), 'Bob', 'Brown', 'bob.brown@example.com', 2);
INSERT INTO student (id, firstname, lastname, email, school_id) VALUES (nextval('student_seq'), 'Charlie', 'Johnson', 'charlie.johnson@example.com', 3);
INSERT INTO student (id, firstname, lastname, email, school_id) VALUES (nextval('student_seq'), 'Diana', 'Williams', 'diana.williams@example.com', 3);
INSERT INTO student (id, firstname, lastname, email, school_id) VALUES (nextval('student_seq'), 'Edward', 'Jones', 'edward.jones@example.com', 4);
INSERT INTO student (id, firstname, lastname, email, school_id) VALUES (nextval('student_seq'), 'Fiona', 'Taylor', 'fiona.taylor@example.com', 4);
HttpURLConnection, Apache HttpClient, RestTemplate, OpenFeign, WebClient, RestClient
위 모든 클라이언트에 공통으로 사용할 School 코드를 작성하겠습니다.
0.2. School
1. Entity
package com.ride.http_client;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.*;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
@Entity
public class School {
@Id
@GeneratedValue
private Integer id;
private String name;
private String email;
}
2. ResponseType
package com.ride.http_client;
import lombok.*;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
public class Student {
private String firstname;
private String lastname;
private String email;
}
3. DTO
package com.ride.http_client;
import lombok.*;
import java.util.List;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
public class FullSchoolResponse {
private String name;
private String email;
List<Student> students;
}
4. Repository
package com.ride.http_client;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface SchoolRepository extends JpaRepository<School, Integer> {
}
5. Controller
package com.ride.http_client;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/schools")
public class SchoolController {
private final SchoolService service;
@GetMapping
public ResponseEntity<List<School>> findAllSchools() {
return ResponseEntity.ok(service.findAllSchools());
}
@GetMapping("/with-students/{school-id}")
public ResponseEntity<FullSchoolResponse> findAllStudents(
@PathVariable("school-id") Integer schoolId
) {
return ResponseEntity.ok(service.findSchoolsWithStudents(schoolId));
}
@PostMapping
public ResponseEntity<Integer> saveSchool(
@RequestBody School school
) {
return ResponseEntity.ok(service.saveSchool(school));
}
}
6. Service
클라이언트만 따로 두고, Service는 최대한 원형을 유지하도록 하겠습니다.
package com.ride.http_client;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class SchoolService {
private final SchoolRepository repository;
private final 클라이언트 client;
public Integer saveSchool(School school) {
return repository.save(school).getId();
}
public List<School> findAllSchools() {
return repository.findAll();
}
public FullSchoolResponse findSchoolsWithStudents(Integer schoolId) {
var school = repository.findById(schoolId)
.orElse(School.builder()
.name("NOT_FOUND")
.email("NOT_FOUND")
.build());
var students = client.findAllStudentsBySchool(schoolId);
return FullSchoolResponse.builder()
.name(school.getName())
.email(school.getEmail())
.students(students)
.build();
}
}
이제 각 클라이언트 별로 프로젝트를 생성하고 로직을 작성하겠습니다.
7. flyway
DB 관리를 명시적으로 하기 위해 flyway를 사용했습니다.
파일명은 <버전>__<설명>.sql 형식으로(언더버 2개입니다),
설명의 공백도 언더바를 사용합니다.
-- init_database.sql
create table if not exists school
(
id integer not null primary key,
name varchar(255),
email varchar(255)
);
create sequence if not exists school_seq increment by 50;
-- insert_school_data.sql
INSERT INTO schools.public.school (id, name, email) VALUES (nextval('schools.public.school_seq'), 'School A', 'schoola@example.com');
INSERT INTO schools.public.school (id, name, email) VALUES (nextval('schools.public.school_seq'), 'School B', 'schoolb@example.com');
INSERT INTO schools.public.school (id, name, email) VALUES (nextval('schools.public.school_seq'), 'School C', 'schoolc@example.com');
INSERT INTO schools.public.school (id, name, email) VALUES (nextval('schools.public.school_seq'), 'School D', 'schoold@example.com');
1. HttpURLConnection
school 프로젝트를 생성하고 빌드합니다.
School, FullSchoolResponse, Student, StudentRepository, Controller는 위에 작성한 것과 동일합니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'io.micrometer:micrometer-tracing-bridge-brave'
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.flywaydb:flyway-database-postgresql'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
1. application.yml
server:
port: 8070
spring:
application:
name: school-service
config:
import: optional:configserver:http://localhost:8888
server.port 를 통해 수신 포트를 설정합니다.
HttpURLConnection school 서비스를 8070 포트에서 수신할 것이며,
spring.application.name을 school-service로 식별할 수 있도록 합니다.
(config-server에 작성했던 student 의 yml 파일이름과 동일하게 명명합니다)
spring.config.import 설정을 통해 외부 서버(config-server)로부터 설정을 가져옵니다.
optional: 을 통해 configserver에 문제가 있어도 애플리케이션이 동작되도록 합니다.
configserver 를 통해 Spring Cloud Config 서버에서 설정을 가져도록 합니다.
8888은 config-server의 port입니다.
이는 school의 설정파일을 config-server로부터 가져오겠다는 것을 의미합니다.
2. Service
package com.ride.http_client;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class SchoolService {
private final SchoolRepository repository;
private final HttpUrlConnectionClient client;
public Integer saveSchool(School school) {
return repository.save(school).getId();
}
public List<School> findAllSchools() {
return repository.findAll();
}
public FullSchoolResponse findSchoolsWithStudents(Integer schoolId) {
var school = repository.findById(schoolId)
.orElse(School.builder()
.name("NOT_FOUND")
.email("NOT_FOUND")
.build());
var students = client.findAllStudentBySchool(schoolId);
return FullSchoolResponse.builder()
.name(school.getName())
.email(school.getEmail())
.students(students)
.build();
}
}
3. HttpUrlConnectionClient
이제 HttpUrlConnectionClient의 세부 코드를 작성합니다.
package com.ride.http_client;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
@Component
@RequiredArgsConstructor
public class HttpUrlConnectionClient {
@Value("${application.config.student-url}")
private String studentUrl;
private final ObjectMapper objectMapper;
public List<Student> findAllStudentBySchool(Integer schoolId) {
try {
URL url = new URL(studentUrl + "/" + schoolId);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
BufferedReader br = new BufferedReader(new InputStreamReader((conn.getInputStream())));
StringBuilder response = new StringBuilder();
String output;
while ((output = br.readLine()) != null) {
response.append(output);
}
conn.disconnect();
return objectMapper
.readValue(
response.toString(),
objectMapper.getTypeFactory()
.constructCollectionType(
List.class,
Student.class
)
);
} catch (Exception e) {
throw new RuntimeException("Exception in findAllStudentBySchool", e);
}
}
}
HttpURLConnection 객체에 메서드와 헤더를 설정합니다.
이후 반환된 값(output)을 ObjectMapper를 통해 List<Student>로 변환합니다.
2. Apache HttpClient
프로젝트 의존성입니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'io.micrometer:micrometer-tracing-bridge-brave'
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.flywaydb:flyway-database-postgresql'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
School, FullSchoolResponse, Student, StudentRepository는 위에 작성한 것과 동일합니다.
1. application.yml
server:
port: 8071
spring:
application:
name: school-service
config:
import: optional:configserver:http://localhost:8888
2. Service
private final ApacheHttpClient client;
ApacheHttpClient 의존성을 주입합니다.
3. Client
package com.ride.http_client;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.List;
@Component
@RequiredArgsConstructor
public class ApacheHttpClient {
@Value("${application.config.student-url}")
private String studentUrl;
private final CloseableHttpClient client;
private final ObjectMapper objectMapper;
public List<Student> findAllStudentsBySchool(Integer schoolId) {
HttpGet request = new HttpGet(studentUrl + "/" + schoolId);
request.addHeader("Accept", "application/json");
try (CloseableHttpResponse response = client.execute(request)) {
String responseBody = EntityUtils.toString(response.getEntity());
return objectMapper.readValue(
responseBody,
objectMapper
.getTypeFactory()
.constructCollectionType(
List.class,
Student.class
)
);
} catch (IOException | ParseException e) {
throw new RuntimeException("Failed to fetch students", e);
}
}
}
apache client5 라이브러리의 메서드 객체를 생성하고, uri와 헤더를 설정합니다.
응답받은 값을 ObjectMapper로 변환합니다.
3. OpenFeign
school 프로젝트를 생성하고 빌드합니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'io.micrometer:micrometer-tracing-bridge-brave'
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.flywaydb:flyway-database-postgresql'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
openfeign 의존성을 필요로 합니다.
package com.ride.http_client;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
public class OpenFeignSchoolApplication {
public static void main(String[] args) {
SpringApplication.run(OpenFeignSchoolApplication.class, args);
}
}
의존성을 설치한 프로젝트의 메인클래스에 @EnableFeignClients 어노테이션을 추가합니다.
School, FullSchoolResponse, Student, StudentRepository는 위에 작성한 것과 동일합니다.
1. application.yml
server:
port: 8073
spring:
application:
name: school-service
config:
import: optional:configserver:http://localhost:8888
다른 port 를 설정합니다.
2. Service
private final OpenFeignClient client;
헤더 설정을 매개변수로 받았습니다.
var students = client.findAllStudentsBySchool(schoolId, "application/json");
3. Client
package com.ride.http_client;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import java.util.List;
@FeignClient(name = "student-service", url = "${application.config.student-url}")
public interface OpenFeignClient {
@GetMapping("/{school-id}")
List<Student> findAllStudentsBySchool(
@PathVariable("school-id") Integer schoolId,
@RequestHeader("Accept") String acceptHeader
);
}
OpenFeign은 코드가 상대적으로 간단한 편입니다.
4. RestTemplate
school 프로젝트를 생성하고 빌드합니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'io.micrometer:micrometer-tracing-bridge-brave'
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.flywaydb:flyway-database-postgresql'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
School, FullSchoolResponse, Student, StudentRepository는 위에 작성한 것과 동일합니다.
1. application.yml
server:
port: 8072
spring:
application:
name: school-service
config:
import: optional:configserver:http://localhost:8888
2. Service
private final RestTemplateClient client;
3. Client
RestTemplate 을 Spring Bean 으로 가동하고,
package com.ride.http_client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
의존성을 주입합니다.
package com.ride.http_client;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@Component
@RequiredArgsConstructor
public class RestTemplateClient {
@Value("${application.config.student-url}")
private String studentUrl;
private final RestTemplate restTemplate;
public List<Student> findAllStudentsBySchool(Integer schoolId) {
HttpHeaders headers = new HttpHeaders();
headers.set("Accept", "application/json");
HttpEntity<String> requestEntity = new HttpEntity<>(headers);
try {
ResponseEntity<List> response = restTemplate.exchange(
studentUrl + "/" + schoolId,
HttpMethod.GET,
requestEntity,
List.class
);
return response.getBody();
} catch (Exception e) {
throw new RuntimeException("Failed to fetch students", e);
}
}
}
5. WebClient
school 프로젝트를 생성하고 빌드합니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'io.micrometer:micrometer-tracing-bridge-brave'
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.flywaydb:flyway-database-postgresql'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
추가된 의존성은 아래와 같습니다.
implementation 'org.springframework.boot:spring-boot-starter-webflux'
School, FullSchoolResponse, Student, StudentRepository는 위에 작성한 것과 동일합니다.
1. application.yml
server:
port: 8074
spring:
application:
name: school-service
config:
import: optional:configserver:http://localhost:8888
2. Service
private final WebClientClient client;
3. Client
Spring Bean으로 관리하고 의존성을 주입받습니다.
package com.ride.http_client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}
package com.ride.http_client;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@RequiredArgsConstructor
public class WebClientClient {
@Value("${application.config.student-url}")
private String studentUrl;
private final WebClient.Builder webClientBuilder;
public List<Student> findAllStudentsBySchool(Integer schoolId) {
WebClient webClient = webClientBuilder.build();
try {
return webClient.get()
.uri(studentUrl + "/" + schoolId)
.header("Accept", "application/json")
.retrieve()
.bodyToFlux(Student.class)
.collectList()
.block();
} catch (Exception e) {
throw new RuntimeException("Failed to fetch students", e);
}
}
}
6. RestClient
school 프로젝트를 생성하고 빌드합니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'io.micrometer:micrometer-tracing-bridge-brave'
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.flywaydb:flyway-database-postgresql'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
School, FullSchoolResponse, Student, StudentRepository는 위에 작성한 것과 동일합니다.
1. application.yml
server:
port: 8075
spring:
application:
name: school-service
config:
import: optional:configserver:http://localhost:8888
2. Service
private final RestClientClient client;
3. Client
package com.ride.http_client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
@Configuration
public class RestClientConfig {
@Bean
public RestClient restClient() {
return RestClient.builder().build();
}
}
package com.ride.http_client;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import java.util.List;
@Component
@RequiredArgsConstructor
public class RestClientClient {
@Value("${application.config.student-url}")
private String studentUrl;
private final RestClient.Builder restClientBuilder;
public List<Student> findAllStudentsBySchool(Integer schoolId) {
RestClient restClient = restClientBuilder.build();
try {
return restClient.get()
.uri(studentUrl + "/" + schoolId)
.header("Accept", "application/json")
.retrieve()
.body(new ParameterizedTypeReference<List<Student>>() {});
} catch (Exception e) {
throw new RuntimeException("Failed to fetch students", e);
}
}
}
각기 다른 Http Client를 사용하여 기본적인 Http 통신을 구현했고,
Http Client의 Config클래스를 통해 더 다양한 설정을 할 수 있습니다.
예컨대, RestClient 또한 HttpInterface를 통해 선언형으로 활용할 수 있습니다.
각 프로젝트의 요구사항에 따라 적절한 HTTP Client를 선택해야 하며,
추가적인 설정이 필요할 수 있습니다.
'Java' 카테고리의 다른 글
[MSA] e-commerce API; 구현1 - 설정(docker, config-server, eureka-server, gateway) (0) | 2024.06.18 |
---|---|
[MSA] e-commerce API; 프로젝트 흐름 (0) | 2024.06.18 |
[HTTP Client] 개요; HttpURLConnection, Apache HttpClient, RestTemplate, OpenFeign, WebClient, RestClient (0) | 2024.06.14 |
[TDD] Test Driven Development (0) | 2024.05.23 |
[Spring AOP] 스프링 관점 지향 프로그래밍(Spring Aspect Oriented Programming) (0) | 2024.05.23 |