CAP Java Handler 자동 등록 모르면 큰일 #shorts #SAP #CAPforJava
Handler 자동 등록이란? 왜 중요한가
캡(CAP) for Java에서 가장 강력하지만 동시에 가장 헷갈리는 개념이 바로 Handler 자동 등록(auto-registration)입니다. 아밥(ABAP)에서 BAdI나 Enhancement를 SE19/SE80으로 명시적으로 등록하던 방식과 달리, 캡 자바는 Spring Boot의 컴포넌트 스캔과 EventHandler 인터페이스를 결합해 핸들러를 자동으로 발견하고 런타임에 연결합니다. 이 메커니즘을 모르면 "왜 내 핸들러가 호출되지 않지?"라는 미스터리에 시간을 허비하게 됩니다.
학습 목표 체크리스트:
- EventHandler 인터페이스 + @Component의 역할을 구분해 설명할 수 있다
- @ServiceName / @Before / @On / @After가 자동 등록 메타데이터로 어떻게 작동하는지 안다
- @HandlerOrder로 실행 순서를 제어할 수 있다
- 핸들러가 호출되지 않는 흔한 원인 3가지를 진단할 수 있다
- Maven 플러그인이 자동 생성한 타입세이프 인터페이스를 활용할 수 있다
선수 지식
본 문서는 다음 지식을 전제로 합니다.
- Spring Boot의 컴포넌트 스캔과 의존성 주입(@Autowired) 기본 개념
- CDS(Core Data Services) 엔티티 정의 문법과 service.cds 작성 경험
- Maven 빌드 라이프사이클(generate-sources, compile)
- OData V4 CRUD 요청 흐름에 대한 일반적인 이해
아밥 개발자라면 BAdI/Enhancement Spot을, 자바 백엔드 개발자라면 Spring AOP의 @Around 패턴을 떠올리면 직관적으로 접근할 수 있습니다.
환경 / 버전 / 준비물
본 튜토리얼은 다음 환경을 기준으로 작성되었습니다. 캡 자바는 비교적 빠르게 진화하므로 사용 중인 버전에 맞춰 일부 API가 다를 수 있다는 점을 일반적으로 염두에 두는 것이 권장됩니다.
- CAP Java SDK 2.x (cds-services-* 라이브러리)
- Spring Boot 3.x
- Java 17 이상 (LTS 권장)
- Maven 3.9+
- cds-maven-plugin (타입세이프 모델 생성용)
- SAP 비피티(BTP) Cloud Foundry 또는 Kyma 런타임 (배포 시)
- 로컬 개발: H2 In-Memory DB / SQLite, 운영: SAP HANA Cloud
프로젝트 골격은 cds init --add java 또는 mvn archetype:generate -DarchetypeArtifactId=cds-services-archetype로 만들면 자동 등록에 필요한 설정이 미리 구성됩니다.
핵심 개념: Spring 자동 발견 메커니즘
캡 자바의 런타임 철학은 한 문장으로 요약됩니다. "everything that happens at runtime is an event." CRUD 요청, 액션 호출, 드래프트 저장, 외부 시스템 연동 모두 이벤트로 추상화됩니다. 그리고 이 이벤트를 처리하는 코드 단위가 Event Handler입니다.
자동 등록은 두 개의 마커가 만났을 때만 작동합니다.
@Component— Spring이 클래스를 빈으로 등록하고 컨텍스트에 보관implements EventHandler— 캡 런타임이 "이 빈은 이벤트 핸들러다"라고 식별하는 마커 인터페이스
도식으로 보면 다음과 같습니다.
[Spring Boot 시작]
│
▼
[ComponentScan] ──검출──▶ @Component 클래스들
│
▼
[CAP Runtime] ──필터──▶ implements EventHandler 인 빈만 선별
│
▼
[메서드 스캔] ──@Before/@On/@After 어노테이션 메타데이터 수집
│
▼
[ServiceCatalog 바인딩] ──@ServiceName + entity 속성으로 라우팅 테이블 구축
│
▼
[요청 발생 시] EventContext → 매칭되는 핸들러 체인 호출
비유하자면 우체국 분류 시스템과 같습니다. @Component는 "이 사람을 직원으로 채용"하는 행위, implements EventHandler는 "분류 부서 소속"이라는 명찰, @ServiceName은 "담당 지역", @Before/@On/@After는 "처리 단계(접수/배달/사후처리)"에 해당합니다. 어느 하나라도 빠지면 우편물이 책상 위에 도착하지 않습니다.
실전 코드 3단계
1단계: 기본 예제 — 최소 핸들러
가장 단순한 형태로, Books 엔티티 생성 전에 가격을 검증하는 핸들러입니다. 이 한 클래스만 있어도 추가 등록 코드 없이 즉시 작동합니다.
package com.example.bookshop.handlers;
import org.springframework.stereotype.Component;
import com.sap.cds.services.cds.CqnService;
import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.handler.annotations.Before;
import com.sap.cds.services.handler.annotations.ServiceName;
import cds.gen.adminservice.Books;
import cds.gen.adminservice.Books_;
import java.util.List;
@Component
@ServiceName("AdminService")
public class AdminServiceHandler implements EventHandler {
@Before(event = CqnService.EVENT_CREATE, entity = Books_.CDS_NAME)
public void validateBooks(List<Books> books) {
for (Books b : books) {
if (b.getPrice() == null || b.getPrice().signum() < 0) {
throw new IllegalArgumentException("Price must be non-negative");
}
}
}
}
주목할 점: Books_와 Books는 손으로 작성한 클래스가 아니라 cds-maven-plugin이 CDS 모델로부터 자동 생성한 것입니다. 컴파일 타임에 엔티티 이름 오타가 잡히는 이유입니다.
2단계: 실무 시나리오 — @On 처리 + 로깅 + 에러 분기
외부 결제 서비스 호출을 시뮬레이션하는 @On 핸들러입니다. @Before에서 검증, @On에서 핵심 처리, @After에서 감사 로그를 남기는 3단계 패턴을 보여줍니다.
package com.example.bookshop.handlers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.sap.cds.services.ErrorStatuses;
import com.sap.cds.services.ServiceException;
import com.sap.cds.services.cds.CdsCreateEventContext;
import com.sap.cds.services.cds.CqnService;
import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.handler.annotations.After;
import com.sap.cds.services.handler.annotations.Before;
import com.sap.cds.services.handler.annotations.On;
import com.sap.cds.services.handler.annotations.ServiceName;
import com.sap.cds.services.persistence.PersistenceService;
import cds.gen.adminservice.Orders;
import cds.gen.adminservice.Orders_;
import java.util.List;
@Component
@ServiceName("AdminService")
public class OrderHandler implements EventHandler {
private static final Logger log = LoggerFactory.getLogger(OrderHandler.class);
@Autowired
private PersistenceService db;
@Before(event = CqnService.EVENT_CREATE, entity = Orders_.CDS_NAME)
public void validateOrders(List<Orders> orders) {
orders.forEach(o -> {
if (o.getQuantity() == null || o.getQuantity() <= 0) {
throw new ServiceException(ErrorStatuses.BAD_REQUEST,
"Quantity must be positive");
}
});
}
@On(event = CqnService.EVENT_CREATE, entity = Orders_.CDS_NAME)
public void handleOrderCreate(CdsCreateEventContext context, List<Orders> orders) {
log.info("Processing {} order(s)", orders.size());
// 핵심 처리: 재고 차감 등 도메인 로직
// context.setCompleted() 를 호출하지 않으면 다음 @On 으로 체인 진행
}
@After(event = CqnService.EVENT_CREATE, entity = Orders_.CDS_NAME)
public void auditOrders(List<Orders> orders) {
orders.forEach(o -> log.info("Order {} created", o.getId()));
}
}
여기서 ServiceException은 OData 에러 페이로드로 자동 변환되어 클라이언트에 적절한 HTTP 상태 코드와 함께 반환됩니다.
3단계: 프로덕션 — @HandlerOrder + 권한 + 단위 테스트
실행 순서를 강제하고, Spring Security 기반 역할 검사를 추가하며, 테스트 가능하도록 분리한 형태입니다.
package com.example.bookshop.handlers;
import com.sap.cds.services.handler.annotations.HandlerOrder;
import com.sap.cds.services.handler.annotations.Before;
import com.sap.cds.services.handler.annotations.ServiceName;
import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.cds.CqnService;
import com.sap.cds.services.request.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import cds.gen.adminservice.Orders_;
@Component
@ServiceName("AdminService")
public class SecurityHandler implements EventHandler {
@Autowired
private UserInfo userInfo;
@Before(event = CqnService.EVENT_CREATE, entity = Orders_.CDS_NAME)
@HandlerOrder(HandlerOrder.EARLY)
public void checkRole() {
if (!userInfo.hasRole("admin")) {
throw new SecurityException("admin role required");
}
}
}
JUnit 5 + Spring Boot Test로 검증하는 예시입니다.
@SpringBootTest
@AutoConfigureMockMvc
class OrderHandlerTest {
@Autowired
private MockMvc mvc;
@Test
@WithMockUser(roles = "admin")
void createOrder_negativeQuantity_returns400() throws Exception {
mvc.perform(post("/odata/v4/AdminService/Orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"quantity\": -1}"))
.andExpect(status().isBadRequest());
}
}
EARLY 순서 덕분에 권한 검사가 검증 로직보다 먼저 실행되어, 권한 없는 사용자에게는 입력값 정보조차 노출되지 않습니다.
흔한 실수 / 트러블슈팅 FAQ
Q1. 핸들러 메서드가 호출되지 않습니다. 90% 이상은 다음 중 하나입니다. (1) implements EventHandler를 빠뜨림 — Spring 빈으로는 등록되지만 캡 런타임이 인식하지 못합니다. (2) @Component가 누락 — 컴포넌트 스캔 대상이 아님. (3) @ServiceName 문자열 오타 — service.cds의 service 이름과 정확히 일치해야 합니다. 로그에서 Registered handler 메시지가 출력되는지 먼저 확인하세요.
Q2. entity 속성에 문자열 리터럴을 써도 되나요? 동작은 합니다만 권장되지 않습니다. Books_.CDS_NAME 같은 생성된 상수를 쓰면 CDS 모델 변경 시 컴파일 에러로 잡혀 안전합니다. Maven 플러그인이 만든 cds.gen.* 패키지를 일반적으로 활용하세요.
Q3. 같은 이벤트에 핸들러가 여러 개면 순서가 어떻게 되나요? 기본은 정의되지 않은 순서이며, @HandlerOrder(HandlerOrder.EARLY) 또는 HandlerOrder.LATE, 그리고 정수 값으로 명시할 수 있습니다. 같은 우선순위 내에서는 클래스 로딩 순서에 의존하므로, 순서가 중요한 로직은 반드시 명시적으로 지정하는 것이 권장됩니다.
Q4. @On 핸들러가 두 개면 둘 다 실행되나요? 첫 번째 @On 핸들러가 context.setCompleted()를 호출하면 체인이 종료됩니다. Application Service의 기본 CRUD 핸들러를 덮어쓰려면 명시적으로 setCompleted를 호출해야 한다는 점을 기억하세요.
Q5. Persistence Service를 주입했는데 NPE가 납니다. 생성자 주입이 아닌 필드 @Autowired를 쓸 때는 객체를 new로 직접 생성하지 말고 항상 Spring 컨테이너를 통해 가져와야 합니다. 테스트에서는 @SpringBootTest 또는 @MockBean을 활용합니다.
다음 단계 / 관련 주제
자동 등록을 이해했다면 다음 주제로 확장해보는 것이 자연스럽습니다.
- Remote Service: S/4HANA OData API를 캡 자바 핸들러에서 호출 — 외부 시스템 통합
- DraftService: Fiori Elements draft-enabled 엔티티의 자동 핸들러 패턴
- Multitenancy: 테넌트별 컨텍스트 분리와 핸들러 동작
- CAP CQN Query API: 핸들러 안에서 동적 쿼리 작성
- Custom Events: 비즈니스 이벤트를 직접 정의하고 emit/handle
참고 자료
- CAP Java — Event Handlers (cap.cloud.sap)
- CAP Java — CQN Services (cap.cloud.sap)
- CAP Java — Building Applications (cap.cloud.sap)
- SAP-samples/cloud-cap-samples-java (GitHub)
- SAP Cloud Application Programming Model (help.sap.com)
- CAP Documentation Hub (help.sap.com)
- CAP Best Practices on BTP (help.sap.com)
- Create a CAP Java Application (developers.sap.com)
- Develop a Business App with CAP for Java (developers.sap.com)
핵심 한 줄
@Component + implements EventHandler — 이 두 마커가 만나야 캡 자바 런타임이 핸들러를 자동으로 발견하고, @ServiceName과 @Before/@On/@After 메타데이터로 라우팅 테이블을 짠다. 둘 중 하나라도 빠지면 코드는 컴파일되지만 호출되지 않는다는 사실이, 캡 자바 핸들러 디버깅의 출발점입니다.