Java

[HTTP Client] 예제; HttpURLConnection, Apache HttpClient, RestTemplate, OpenFeign, WebClient, RestClient

ride-dev 2024. 6. 14. 18:48

[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 의존성을 사용했습니다.

참고한 이슈: https://github.com/flyway/flyway/issues/3774

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를 선택해야 하며,

추가적인 설정이 필요할 수 있습니다.

728x90