Synchronous APIs#
동기식 API(Synchronous API)는 현대 소프트웨어 통합의 기본적인 패턴으로, 시스템 간 통신에서 가장 직관적이고 널리 사용되는 방식이다. 이 패턴은 클라이언트가 요청을 보내고 서버가 해당 요청을 처리한 후 즉시 응답을 반환하는 방식으로 작동한다. 이러한 동기식 통신의 본질은 “요청-응답” 주기가 완료될 때까지 클라이언트가 다른 작업으로 진행하지 않고 대기한다는 점이다.
동기식 API는 직관적인 요청-응답 모델, 즉각적인 피드백, 데이터 일관성 보장 등의 장점으로 인해 여전히 많은 애플리케이션에서 중요한 역할을 담당하고 있다. 특히 사용자 인터페이스와 직접 상호작용하는 API, 트랜잭션이 중요한 작업, 데이터 일관성이 중요한 조회 작업에 적합하다.
그러나 동기식 API는 확장성 제한, 장기 실행 작업에 대한 부적합성, 네트워크 지연의 직접적 영향 등의 단점도 가지고 있다. 이러한 단점을 완화하기 위해 연결 풀링, 캐싱, 배치 처리, 타임아웃 관리, 병렬 처리, 서킷 브레이커 패턴 등의 최적화 전략을 적용할 수 있다.
실제 애플리케이션에서는 동기식 API와 비동기식 API를 함께 사용하는 혼합 접근법이 가장 효과적일 수 있다. 각 패턴의 장점을 활용하고 단점을 보완하여 최적의 솔루션을 구현할 수 있다.
동기식 API의 기본 원리#
동기식 API의 핵심은 직접적이고 즉각적인 상호작용이다.
이 패턴은 다음과 같은 기본 흐름을 따른다:
- 클라이언트가 API 엔드포인트에 요청을 전송한다.
- 서버는 요청을 수신하고 처리를 시작한다.
- 클라이언트는 서버의 응답을 기다리는 동안 차단(blocking) 상태가 된다.
- 서버는 요청 처리를 완료하고 결과를 포함한 응답을 클라이언트에게 반환한다.
- 클라이언트는 응답을 받은 후에야 다음 작업을 진행할 수 있다.
이 프로세스는 마치 전화 통화와 유사하다. 질문을 하고 상대방의 대답을 기다리는 동안 다른 대화를 나누기 어렵다. 응답을 받은 후에야 대화를 계속할 수 있다.
동기식 API의 주요 특징#
차단(Blocking) 동작#
동기식 API의 가장 두드러진 특징은 차단 동작이다. 클라이언트는 서버로부터 응답을 받을 때까지 다른 작업을 수행할 수 없다. 이는 API 호출이 완료될 때까지 스레드를 차단하며, 이 기간 동안 시스템 리소스는 유휴 상태로 유지된다.
1
2
3
4
5
6
7
8
9
10
11
12
| // 동기식 API 호출의 차단 동작 예시
public CustomerData getCustomerInfo(String customerId) {
// 이 API 호출이 완료될 때까지 현재 스레드는 차단됩니다
Response response = customerApiClient.get("/customers/" + customerId);
// 응답을 받은 후에야 다음 코드가 실행됩니다
if (response.isSuccessful()) {
return response.body();
} else {
throw new ApiException("Failed to retrieve customer data");
}
}
|
즉각적인 응답#
동기식 API는 요청이 완료되면 즉시 결과를 반환한다. 이러한 즉각적인 피드백은 사용자 상호작용이 필요한 애플리케이션에서 중요하다. 사용자는 작업이 성공했는지 실패했는지 즉시 알 수 있다.
강력한 일관성#
동기식 API는 요청 시점의 데이터 상태를 정확히 반영하는 응답을 제공한다. 이 특성은 데이터 일관성이 중요한 금융 거래나 예약 시스템과 같은 애플리케이션에서 특히 유용하다.
단순한 오류 처리#
동기식 패턴에서는 오류 처리가 비교적 단순하다. 오류가 발생하면 즉시 클라이언트에 전파되므로, 적절한 오류 처리 로직을 구현하기 쉽다.
1
2
3
4
5
6
7
8
| try {
CustomerData customer = customerService.getCustomerInfo("12345");
processCustomerData(customer);
} catch (ApiException e) {
// 오류 처리 로직
logger.error("API error: {}", e.getMessage());
showErrorMessage("고객 정보를 불러오는 데 실패했습니다.");
}
|
직관적인 프로그래밍 모델#
동기식 API는 순차적인 코드 실행 흐름을 따르므로 이해하고 디버깅하기 쉽다. 코드가 위에서 아래로 진행되며, 각 단계가 완료된 후에만 다음 단계로 진행된다.
동기식 API 구현 패턴#
RESTful API#
REST(Representational State Transfer)는 동기식 API 구현을 위한 가장 일반적인 아키텍처 스타일이다. RESTful API는 HTTP 메서드(GET, POST, PUT, DELETE 등)를 사용하여 리소스에 대한 작업을 수행한다.
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
32
33
34
35
| @RestController
@RequestMapping("/api/customers")
public class CustomerController {
private final CustomerService customerService;
@Autowired
public CustomerController(CustomerService customerService) {
this.customerService = customerService;
}
@GetMapping("/{id}")
public ResponseEntity<Customer> getCustomer(@PathVariable String id) {
Customer customer = customerService.findById(id);
if (customer != null) {
return ResponseEntity.ok(customer);
} else {
return ResponseEntity.notFound().build();
}
}
@PostMapping
public ResponseEntity<Customer> createCustomer(@RequestBody @Valid CustomerRequest request) {
Customer customer = customerService.create(request);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(customer.getId())
.toUri();
return ResponseEntity.created(location).body(customer);
}
}
|
이 RESTful API는 고객 정보 조회 및 생성을 위한 동기식 엔드포인트를 제공한다. 클라이언트는 요청을 보내고 서버로부터 응답을 받을 때까지 대기한다.
RPC(Remote Procedure Call)#
RPC는 클라이언트가 원격 서버에서 함수나 프로시저를 직접 호출하는 것처럼 보이게 하는 프로토콜이다. gRPC와 같은 현대적인 RPC 프레임워크는 효율적인 동기식 통신을 제공한다.
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
| // gRPC 서비스 정의 예시 (Proto 파일)
syntax = "proto3";
package customer;
service CustomerService {
rpc GetCustomer (CustomerRequest) returns (CustomerResponse);
rpc CreateCustomer (CreateCustomerRequest) returns (CustomerResponse);
}
message CustomerRequest {
string customer_id = 1;
}
message CreateCustomerRequest {
string name = 1;
string email = 2;
string phone = 3;
}
message CustomerResponse {
string customer_id = 1;
string name = 2;
string email = 3;
string phone = 4;
string status = 5;
}
|
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
| // gRPC 서비스 구현
public class CustomerServiceImpl extends CustomerServiceGrpc.CustomerServiceImplBase {
private final CustomerRepository customerRepository;
@Override
public void getCustomer(CustomerRequest request, StreamObserver<CustomerResponse> responseObserver) {
Customer customer = customerRepository.findById(request.getCustomerId());
if (customer != null) {
CustomerResponse response = CustomerResponse.newBuilder()
.setCustomerId(customer.getId())
.setName(customer.getName())
.setEmail(customer.getEmail())
.setPhone(customer.getPhone())
.setStatus(customer.getStatus())
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
} else {
responseObserver.onError(
Status.NOT_FOUND
.withDescription("Customer not found")
.asRuntimeException()
);
}
}
// 기타 메서드 구현...
}
|
gRPC는 Protocol Buffers를 사용하여 데이터를 직렬화하고, HTTP/2를 사용하여 효율적인 통신을 제공한다. 그러나 기본적으로는 동기식 요청-응답 패턴을 따른다.
GraphQL#
GraphQL은 API를 위한 쿼리 언어로, 클라이언트가 필요한 데이터를 정확히 지정할 수 있게 해준다. 기본적으로 동기식이지만, 단일 요청으로 여러 리소스의 데이터를 가져올 수 있어 효율적이다.
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
32
33
34
| # GraphQL 스키마 예시
type Customer {
id: ID!
name: String!
email: String!
phone: String
orders: [Order!]
}
type Order {
id: ID!
orderNumber: String!
totalAmount: Float!
status: String!
items: [OrderItem!]!
}
type OrderItem {
id: ID!
productId: ID!
productName: String!
quantity: Int!
price: Float!
}
type Query {
customer(id: ID!): Customer
customers(status: String): [Customer!]!
}
type Mutation {
createCustomer(name: String!, email: String!, phone: String): Customer!
updateCustomer(id: ID!, name: String, email: String, phone: String): Customer!
}
|
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
32
33
34
35
36
37
38
39
40
| // GraphQL 리졸버 구현
@Component
public class CustomerResolver implements GraphQLQueryResolver {
private final CustomerService customerService;
@Autowired
public CustomerResolver(CustomerService customerService) {
this.customerService = customerService;
}
public Customer customer(String id) {
return customerService.findById(id);
}
public List<Customer> customers(String status) {
if (status != null) {
return customerService.findByStatus(status);
}
return customerService.findAll();
}
}
@Component
public class CustomerMutation implements GraphQLMutationResolver {
private final CustomerService customerService;
@Autowired
public CustomerMutation(CustomerService customerService) {
this.customerService = customerService;
}
public Customer createCustomer(String name, String email, String phone) {
CustomerRequest request = new CustomerRequest(name, email, phone);
return customerService.create(request);
}
// 기타 뮤테이션 메서드...
}
|
SOAP(Simple Object Access Protocol)#
SOAP는 XML 기반의 메시지 프로토콜로, 주로 엔터프라이즈 환경에서 사용된다. 보안 및 트랜잭션 지원과 같은 엔터프라이즈 기능을 제공하지만, REST에 비해 더 복잡하고 무겁다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| <!-- SOAP 요청 예시 -->
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Header>
<AuthHeader xmlns="http://example.com/auth">
<Username>user123</Username>
<Password>password123</Password>
</AuthHeader>
</soap:Header>
<soap:Body>
<GetCustomer xmlns="http://example.com/customer">
<CustomerId>12345</CustomerId>
</GetCustomer>
</soap:Body>
</soap:Envelope>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| <!-- SOAP 응답 예시 -->
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetCustomerResponse xmlns="http://example.com/customer">
<Customer>
<Id>12345</Id>
<Name>John Doe</Name>
<Email>john.doe@example.com</Email>
<Phone>+1-555-123-4567</Phone>
<Status>ACTIVE</Status>
</Customer>
</GetCustomerResponse>
</soap:Body>
</soap:Envelope>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // JAX-WS를 사용한 SOAP 웹 서비스 구현
@WebService(serviceName = "CustomerService")
public class CustomerServiceImpl {
private CustomerRepository customerRepository;
@WebMethod
public Customer getCustomer(@WebParam(name = "customerId") String customerId) {
return customerRepository.findById(customerId);
}
@WebMethod
public Customer createCustomer(
@WebParam(name = "name") String name,
@WebParam(name = "email") String email,
@WebParam(name = "phone") String phone) {
CustomerRequest request = new CustomerRequest(name, email, phone);
return customerRepository.save(request.toCustomer());
}
}
|
동기식 API의 장점#
단순성과 이해 용이성
동기식 API는 직관적인 요청-응답 모델을 따르기 때문에 이해하고 구현하기 쉽다. 코드의 흐름이 선형적이고 예측 가능하여 디버깅이 용이하다.
즉각적인 피드백
동기식 API는 요청에 대한 즉각적인 응답을 제공하므로, 클라이언트는 작업 결과를 즉시 알 수 있다. 이는 사용자 인터페이스와 직접 상호작용하는 애플리케이션에 적합하다.
강력한 일관성
동기식 API는 요청 시점의 최신 데이터를 반환하므로, 데이터 일관성이 중요한 애플리케이션에 적합하다. 예를 들어, 은행 계좌 잔액 조회나 항공권 예약 확인과 같은 기능에서는 최신 정보를 제공하는 것이 중요하다.
트랜잭션 처리 용이성
동기식 통신은 트랜잭션을 관리하기 쉽게 한다. 요청이 완료되면 트랜잭션의 결과(성공 또는 실패)를 즉시 알 수 있다.
간단한 오류 처리
오류가 발생하면 즉시 클라이언트에 전파되므로, 오류 처리 흐름이 단순하고 직관적이다. 또한 필요한 경우 즉시 재시도 로직을 구현할 수 있다.
동기식 API의 단점#
확장성 제한
동기식 API는 요청-응답 주기 동안 리소스를 차단하기 때문에, 대량의 동시 요청을 처리할 때 확장성 문제가 발생할 수 있다. 서버는 각 연결을 유지해야 하므로 동시 처리 가능한 요청 수가 제한된다.
성능 제약
응답을 기다리는 동안 클라이언트 리소스가 차단되므로, 여러 API 호출을 순차적으로 처리해야 하는 경우 전체 응답 시간이 길어질 수 있다. 이는 사용자 경험에 부정적인 영향을 미칠 수 있다. 예를 들어, 세 개의 별도 API 호출이 각각 1초씩 걸린다면, 총 처리 시간은 최소 3초가 된다:
1
2
3
4
5
6
| // 순차적 API 호출의 성능 영향
CustomerData customer = customerService.getCustomer(customerId); // 1초
List<Order> orders = orderService.getCustomerOrders(customerId); // 1초
CreditScore creditScore = creditService.getCustomerScore(customerId); // 1초
// 총 처리 시간: 3초
|
장기 실행 작업에 부적합
처리 시간이 긴 작업(예: 대용량 파일 처리, 복잡한 계산)에는 동기식 API가 적합하지 않다. 클라이언트는 전체 처리가 완료될 때까지 대기해야 하므로 타임아웃 문제가 발생할 수 있다.
시스템 취약성 증가
동기식 API 체인에서 하나의 서비스가 응답하지 않으면, 연쇄 반응으로 전체 시스템이 영향을 받을 수 있다. 이는 시스템 복원력이 저하되는 결과를 초래한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 서비스 체인에서의 취약성 예시
try {
// 이 서비스가 응답하지 않으면 전체 체인이 지연됨
CustomerData customer = customerService.getCustomer(customerId);
List<Order> orders = orderService.getCustomerOrders(customer.getId());
List<Payment> payments = paymentService.getOrderPayments(orders);
return new CustomerProfile(customer, orders, payments);
} catch (ApiTimeoutException e) {
// 타임아웃 처리
logger.error("Service timeout: {}", e.getMessage());
throw new ServiceUnavailableException("일시적인 서비스 지연이 발생했습니다.");
}
|
네트워크 지연의 직접적 영향
동기식 API에서는 네트워크 지연이 직접적으로 사용자 경험에 영향을 미친다. 특히 모바일 애플리케이션이나 불안정한 네트워크 환경에서는 문제가 될 수 있다.
동기식 API 최적화 전략#
동기식 API의 단점을 완화하기 위한 여러 최적화 전략이 있다.
연결 풀링(Connection Pooling)#
여러 요청에서 재사용할 수 있는 연결 풀을 유지하여 연결 설정 오버헤드를 줄인다.
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
| // 연결 풀 설정 예시 (Spring Boot)
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
// 연결 설정
factory.setConnectTimeout(3000); // 연결 타임아웃: 3초
factory.setReadTimeout(5000); // 읽기 타임아웃: 5초
// 연결 풀 구성
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(100); // 총 연결 수
connectionManager.setDefaultMaxPerRoute(20); // 호스트당 최대 연결 수
HttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.build();
factory.setHttpClient(httpClient);
return new RestTemplate(factory);
}
}
|
캐싱(Caching)#
자주 요청되는 데이터를 캐싱하여 반복적인 API 호출을 줄이고 응답 시간을 개선한다.
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
| // 캐싱을 사용한 API 클라이언트 예시
@Service
public class CachedCustomerService {
private final CustomerApiClient apiClient;
private final Cache<String, Customer> customerCache;
public CachedCustomerService(CustomerApiClient apiClient) {
this.apiClient = apiClient;
this.customerCache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES) // 10분 후 만료
.maximumSize(1000) // 최대 1000개 항목
.build();
}
public Customer getCustomer(String customerId) {
// 캐시에서 먼저 조회
Customer cachedCustomer = customerCache.getIfPresent(customerId);
if (cachedCustomer != null) {
return cachedCustomer;
}
// 캐시에 없으면 API 호출
Customer customer = apiClient.getCustomer(customerId);
// 결과를 캐시에 저장
customerCache.put(customerId, customer);
return customer;
}
}
|
배치 처리(Batching)#
여러 개별 요청을 하나의 배치 요청으로 통합하여 네트워크 오버헤드를 줄인다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // 배치 API 엔드포인트 예시
@RestController
@RequestMapping("/api/customers")
public class CustomerBatchController {
private final CustomerService customerService;
@PostMapping("/batch")
public ResponseEntity<List<Customer>> getCustomersBatch(@RequestBody List<String> customerIds) {
if (customerIds.size() > 100) {
return ResponseEntity.badRequest().build(); // 최대 배치 크기 제한
}
List<Customer> customers = customerService.findByIds(customerIds);
return ResponseEntity.ok(customers);
}
}
|
타임아웃 관리#
적절한 타임아웃을 설정하여 시스템이 무한정 기다리지 않도록 한다.
1
2
3
4
5
6
7
8
9
10
11
| // 타임아웃 설정 예시
HttpClientBuilder clientBuilder = HttpClients.custom();
// 연결 타임아웃 (서버에 연결하는 시간)
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(3000) // 3초
.setSocketTimeout(5000) // 5초 (데이터 읽기 타임아웃)
.setConnectionRequestTimeout(2000) // 2초 (연결 풀에서 연결을 가져오는 타임아웃)
.build();
clientBuilder.setDefaultRequestConfig(requestConfig);
|
병렬 처리(Parallel Processing)#
독립적인 API 호출을 병렬로 처리하여 전체 응답 시간을 단축한다.
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
| // CompletableFuture를 사용한 병렬 API 호출 예시
public CustomerProfile getCustomerProfile(String customerId) {
// 병렬로 여러 API 호출
CompletableFuture<Customer> customerFuture =
CompletableFuture.supplyAsync(() -> customerService.getCustomer(customerId));
CompletableFuture<List<Order>> ordersFuture =
CompletableFuture.supplyAsync(() -> orderService.getCustomerOrders(customerId));
CompletableFuture<CreditScore> creditScoreFuture =
CompletableFuture.supplyAsync(() -> creditService.getCustomerScore(customerId));
// 모든 비동기 작업 완료 대기
CompletableFuture<CustomerProfile> profileFuture = customerFuture
.thenCombine(ordersFuture, (customer, orders) -> new CustomerProfilePartial(customer, orders))
.thenCombine(creditScoreFuture, (profile, score) -> {
profile.setCreditScore(score);
return new CustomerProfile(profile);
});
try {
// 결과 반환 (타임아웃 설정)
return profileFuture.get(5, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new ServiceException("Failed to retrieve customer profile", e);
}
}
|
서킷 브레이커(Circuit Breaker) 패턴#
서비스 장애 시 빠르게 실패하고 기본 응답을 제공하여 시스템 복원력을 향상시킨다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // Spring Cloud Circuit Breaker 예시
@Service
public class CustomerServiceWithCircuitBreaker {
private final RestTemplate restTemplate;
private final CircuitBreakerFactory circuitBreakerFactory;
public Customer getCustomer(String customerId) {
CircuitBreaker circuitBreaker = circuitBreakerFactory.create("customerService");
return circuitBreaker.run(
() -> restTemplate.getForObject("/api/customers/{id}", Customer.class, customerId),
throwable -> getCustomerFallback(customerId, throwable)
);
}
private Customer getCustomerFallback(String customerId, Throwable t) {
logger.warn("Circuit breaker triggered for customer {}: {}", customerId, t.getMessage());
// 기본 응답 또는 캐시된 데이터 반환
return new Customer(customerId, "Unknown", "N/A", "N/A", "UNKNOWN");
}
}
|
동기식 API의 적합한 사용 사례#
동기식 API는 다음과 같은 상황에서 가장 적합하다:
사용자 인터랙션 기반 작업
사용자가 즉각적인 응답을 기대하는 인터랙티브 애플리케이션에 적합하다. 예를 들어, 로그인, 검색, 간단한 데이터 조회와 같은 기능이 여기에 해당한다.
트랜잭션 작업
데이터의 일관성이 중요한 금융 거래, 주문 처리, 결제와 같은 작업에 적합하다.
간단한 CRUD 작업
데이터 생성, 읽기, 업데이트, 삭제와 같은 기본 데이터베이스 작업에는 동기식 API가 매우 적합하다. 이러한 작업은 일반적으로 빠르게 처리되고 즉각적인 응답이 필요하기 때문이다.
검증 및 인증 작업
사용자 입력 검증, 인증, 권한 부여와 같은 작업에는 즉각적인 응답이 필요하므로 동기식 API가 적합하다.
데이터 일관성이 중요한 조회 작업
최신 데이터가 필요한 잔액 조회, 재고 확인, 예약 가능 여부 조회와 같은 작업에 적합하다.
동기식 API 구현 시 고려사항#
동기식 API를 구현할 때 다음 사항을 고려해야 한다:
적절한 타임아웃 설정
클라이언트와 서버 모두에 적절한 타임아웃을 설정하여 무한정 대기 상태를 방지한다. 타임아웃은 애플리케이션과 작업의 특성에 따라 다르지만, 일반적으로 사용자 경험을 해치지 않는 범위 내에서 설정해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // 클라이언트 측 타임아웃 설정
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(3, TimeUnit.SECONDS) // 연결 타임아웃
.readTimeout(5, TimeUnit.SECONDS) // 읽기 타임아웃
.writeTimeout(5, TimeUnit.SECONDS) // 쓰기 타임아웃
.build();
// 서버 측 타임아웃 설정 (Spring Boot)
@Configuration
public class ServerConfig {
@Bean
public TomcatServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.addConnectorCustomizers(connector -> {
connector.setConnectionTimeout(30000); // 30초
});
return factory;
}
}
|
오류 처리 및 재시도 메커니즘
네트워크 오류, 서버 오류 등 다양한 실패 상황에 대한 처리 로직을 구현한다. 일시적인 오류의 경우 자동 재시도 메커니즘을 구현하는 것이 좋다.
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
32
33
34
35
36
37
| // Resilience4j를 사용한 재시도 메커니즘 구현
@Service
public class CustomerServiceWithRetry {
private final RestTemplate restTemplate;
private final RetryRegistry retryRegistry;
public CustomerServiceWithRetry(RestTemplate restTemplate, RetryRegistry retryRegistry) {
this.restTemplate = restTemplate;
this.retryRegistry = retryRegistry;
}
public Customer getCustomer(String customerId) {
Retry retry = retryRegistry.retry("customerService");
return Retry.decorateSupplier(
retry,
() -> restTemplate.getForObject("/api/customers/{id}", Customer.class, customerId)
).get();
}
}
// 재시도 설정
@Configuration
public class RetryConfig {
@Bean
public RetryRegistry retryRegistry() {
RetryConfig config = RetryConfig.custom()
.maxAttempts(3) // 최대 3번 시도
.waitDuration(Duration.ofMillis(500)) // 500ms 간격으로 재시도
.retryExceptions(IOException.class, HttpServerErrorException.class) // 재시도할 예외 유형
.ignoreExceptions(HttpClientErrorException.class) // 재시도하지 않을 예외 유형
.build();
return RetryRegistry.of(config);
}
}
|
최적화#
동기식 API의 성능을 최적화하기 위해 다음과 같은 기법을 적용할 수 있다:
응답 압축#
큰 응답의 경우 GZIP이나 Deflate와 같은 압축 기술을 사용하여 전송 시간을 단축할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
| // Spring Boot에서 응답 압축 설정
@Configuration
public class WebServerConfig {
@Bean
public GzipFilter gzipFilter() {
return new GzipFilter();
}
}
// application.properties
server.compression.enabled=true
server.compression.mime-types=application/json,application/xml,text/html,text/plain
server.compression.min-response-size=2048
|
페이지네이션 구현#
대량의 데이터를 반환하는 API의 경우 페이지네이션을 구현하여 응답 크기를 제한한다.
1
2
3
4
5
6
7
8
9
10
11
| @GetMapping("/products")
public ResponseEntity<Page<Product>> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "name") String sort) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sort));
Page<Product> products = productService.findAll(pageable);
return ResponseEntity.ok(products);
}
|
필요한 필드만 반환#
클라이언트에서 필요한 필드만 반환하여 응답 크기를 줄인다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // DTO를 사용한 필드 선택적 반환
@GetMapping("/products/{id}")
public ResponseEntity<ProductDTO> getProduct(
@PathVariable Long id,
@RequestParam(required = false) List<String> fields) {
Product product = productService.findById(id);
// 필요한 필드만 매핑
ProductDTO dto;
if (fields != null && !fields.isEmpty()) {
dto = productMapper.toDto(product, fields);
} else {
dto = productMapper.toDto(product);
}
return ResponseEntity.ok(dto);
}
|
적절한 HTTP 상태 코드 사용#
RESTful API에서는 적절한 HTTP 상태 코드를 사용하여 클라이언트에게 명확한 피드백을 제공하는 것이 중요하다.
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| @RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
public ResponseEntity<?> createOrder(@RequestBody @Valid OrderRequest request) {
try {
// 재고 확인
if (!inventoryService.isAvailable(request.getItems())) {
return ResponseEntity
.status(HttpStatus.CONFLICT)
.body(new ErrorResponse("INVENTORY_UNAVAILABLE", "요청한 상품의 재고가 부족합니다."));
}
// 주문 생성
Order order = orderService.createOrder(request);
// 성공 응답
return ResponseEntity
.status(HttpStatus.CREATED)
.location(URI.create("/api/orders/" + order.getId()))
.body(order);
} catch (CustomerNotFoundException e) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("CUSTOMER_NOT_FOUND", e.getMessage()));
} catch (PaymentDeclinedException e) {
return ResponseEntity
.status(HttpStatus.PAYMENT_REQUIRED)
.body(new ErrorResponse("PAYMENT_DECLINED", e.getMessage()));
} catch (ValidationException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("VALIDATION_ERROR", e.getMessage()));
} catch (Exception e) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("INTERNAL_ERROR", "주문 처리 중 오류가 발생했습니다."));
}
}
}
|
적절한 API 버전 관리#
API 변경 시 기존 클라이언트와의 호환성을 유지하기 위해 버전 관리 전략을 수립한다.
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
32
33
34
| // URL 경로 기반 버전 관리
@RestController
@RequestMapping("/api/v1/customers") // v1 버전
public class CustomerControllerV1 {
// v1 구현
}
@RestController
@RequestMapping("/api/v2/customers") // v2 버전
public class CustomerControllerV2 {
// v2 구현
}
// 헤더 기반 버전 관리
@RestController
@RequestMapping("/api/customers")
public class CustomerController {
@GetMapping("/{id}")
public ResponseEntity<?> getCustomer(
@PathVariable String id,
@RequestHeader(value = "API-Version", defaultValue = "1") int version) {
if (version == 1) {
CustomerV1 customer = customerServiceV1.findById(id);
return ResponseEntity.ok(customer);
} else if (version == 2) {
CustomerV2 customer = customerServiceV2.findById(id);
return ResponseEntity.ok(customer);
} else {
return ResponseEntity.badRequest().body("Unsupported API version");
}
}
}
|
동기식 API와 비동기식 API의 혼합 접근법#
실제 애플리케이션에서는 완전히 동기식이거나 완전히 비동기식인 경우는 드물며, 두 패턴을 적절히 혼합하여 사용하는 것이 일반적이다. 특히 장시간 실행되는 작업에 대해서는 비동기 처리를 도입하고, 빠른 응답이 필요한 작업에는 동기식 접근법을 유지하는 것이 좋다.
비동기 작업을 시작하는 동기식 API#
장시간 실행되는 작업을 시작할 때는 동기식 API를 사용하여 작업 ID를 즉시 반환하고, 실제 처리는 비동기적으로 수행한다.
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
| @RestController
@RequestMapping("/api/reports")
public class ReportController {
private final ReportService reportService;
@PostMapping("/generate")
public ResponseEntity<ReportGenerationResponse> generateReport(@RequestBody ReportRequest request) {
// 보고서 생성 작업 시작 (비동기)
String reportId = reportService.startReportGeneration(request);
// 작업 ID와 상태 반환 (동기식)
return ResponseEntity
.accepted()
.body(new ReportGenerationResponse(
reportId,
ReportStatus.PROCESSING,
"/api/reports/status/" + reportId
));
}
@GetMapping("/status/{reportId}")
public ResponseEntity<ReportStatusResponse> getReportStatus(@PathVariable String reportId) {
// 보고서 상태 조회 (동기식)
ReportStatus status = reportService.getReportStatus(reportId);
ReportStatusResponse response = new ReportStatusResponse(reportId, status);
// 완료된 경우 결과 URL 포함
if (status == ReportStatus.COMPLETED) {
response.setResultUrl("/api/reports/download/" + reportId);
}
return ResponseEntity.ok(response);
}
@GetMapping("/download/{reportId}")
public ResponseEntity<Resource> downloadReport(@PathVariable String reportId) {
// 보고서 상태 확인
ReportStatus status = reportService.getReportStatus(reportId);
if (status != ReportStatus.COMPLETED) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
// 생성된 보고서 다운로드 (동기식)
Resource resource = reportService.getReportFile(reportId);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + reportId + ".pdf\"")
.contentType(MediaType.APPLICATION_PDF)
.body(resource);
}
}
|
웹훅을 사용한 비동기 알림#
장시간 실행되는 작업이 완료되면 웹훅을 통해 클라이언트에게 알리는 패턴.
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| @RestController
@RequestMapping("/api/data-processing")
public class DataProcessingController {
private final DataProcessingService processingService;
@PostMapping("/jobs")
public ResponseEntity<JobResponse> submitJob(
@RequestBody JobRequest request,
@RequestParam(required = false) String callbackUrl) {
// 작업 생성 및 비동기 처리 시작
String jobId = processingService.startProcessing(request, callbackUrl);
// 작업 ID와 상태 정보 반환
return ResponseEntity
.accepted()
.body(new JobResponse(
jobId,
JobStatus.PROCESSING,
"/api/data-processing/jobs/" + jobId
));
}
// 웹훅 알림을 위한 서비스 메서드
@Service
public class WebhookService {
private final RestTemplate restTemplate;
public void sendJobCompletionWebhook(String callbackUrl, JobResult result) {
WebhookPayload payload = new WebhookPayload(
result.getJobId(),
result.getStatus(),
result.getCompletedAt(),
result.getResultUrl()
);
try {
restTemplate.postForEntity(callbackUrl, payload, Void.class);
log.info("Webhook sent to {} for job {}", callbackUrl, result.getJobId());
} catch (Exception e) {
log.error("Failed to send webhook for job {}: {}", result.getJobId(), e.getMessage());
// 재시도 로직 구현 가능
}
}
}
}
|
동기식 API 향후 발전 동향#
동기식 API는 계속해서 발전하고 있으며, 다음과 같은 동향이 주목받고 있다:
HTTP/2 및 HTTP/3 지원#
최신 HTTP 프로토콜은 다중화, 서버 푸시, 헤더 압축 등의 기능을 제공하여 동기식 API의 성능을 향상시킨다.
1
2
3
| // Spring Boot 2.4+ HTTP/2 설정
# application.properties
server.http2.enabled=true
|
GraphQL 확산#
REST의 대안으로 GraphQL이 점점 더 많이 사용되고 있다. GraphQL은 클라이언트가 필요한 데이터를 정확히 지정할 수 있게 하여 오버페칭과 언더페칭 문제를 해결한다.
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
| // Spring Boot GraphQL 구현 예시
@Controller
public class ProductGraphQLController {
private final ProductService productService;
@QueryMapping
public List<Product> products() {
return productService.findAll();
}
@QueryMapping
public Product product(@Argument String id) {
return productService.findById(id);
}
@MutationMapping
public Product createProduct(@Argument CreateProductInput input) {
Product product = new Product();
product.setName(input.getName());
product.setPrice(input.getPrice());
product.setDescription(input.getDescription());
return productService.save(product);
}
}
|
서버리스 아키텍처 통합#
서버리스 아키텍처는 동기식 API에 확장성과 비용 효율성을 제공한다. AWS Lambda, Azure Functions, Google Cloud Functions와 같은 서비스를 활용하여 동기식 API를 구현할 수 있다.
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
32
33
| // AWS Lambda를 사용한 동기식 API 예시
exports.handler = async (event) => {
try {
const productId = event.pathParameters.id;
// DynamoDB에서 상품 조회
const params = {
TableName: 'Products',
Key: { id: productId }
};
const result = await dynamoDB.get(params).promise();
if (!result.Item) {
return {
statusCode: 404,
body: JSON.stringify({ error: "Product not found" })
};
}
return {
statusCode: 200,
body: JSON.stringify(result.Item)
};
} catch (error) {
console.error('Error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: "Internal server error" })
};
}
};
|
4API 게이트웨이 및 서비스 메시#
API 게이트웨이와 서비스 메시는 동기식 API 관리에 중요한 역할을 한다. 이들은 라우팅, 인증, 속도 제한, 캐싱 등의 기능을 제공하여 API 성능과 보안을 향상시킨다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| # Kong API 게이트웨이 설정 예시
apis:
- name: customer-api
upstream_url: http://customer-service:8080/api
uris: /customers
methods:
- GET
- POST
- PUT
- DELETE
plugins:
- name: key-auth
- name: rate-limiting
config:
minute: 60
- name: proxy-cache
config:
content_type:
- application/json
cache_ttl: 300
|
용어 정리#
참고 및 출처#