이전 글: 1편 — 왜 만들었고, 어떻게 설계했나
다음 글: 3편 — 매일 새벽 자동으로 돌아가는 파이프라인 설계
백엔드 설계에서 가장 먼저 결정할 것
백엔드를 시작할 때 가장 먼저 결정한 건 패키지 구조다.
선택지는 두 가지였다.
- 기능별 구조:
controller/,service/,repository/폴더 안에 모든 도메인 파일을 모아두는 방식 - 도메인 중심 구조:
domain/user/,domain/trade/,domain/news/처럼 도메인별로 독립시키는 방식
이 프로젝트는 도메인 중심을 선택했다.
이유는 단순하다. user와 관련된 코드(Controller, Service, Mapper, DTO)는 항상 함께 수정된다.
trade를 바꿀 때 user 폴더를 뒤질 일이 없다.
도메인이 독립성을 가질수록 코드를 찾는 시간이 줄고, 나중에 한 도메인만 잘라내서 분리하기도 쉬워진다.
com.stock.account/
├── domain/
│ ├── user/
│ │ ├── controller/
│ │ ├── service/
│ │ ├── mapper/
│ │ └── dto/
│ ├── trade/
│ ├── news/
│ └── blog/
├── common/
│ ├── exception/
│ └── response/
└── security/
각 도메인은 controller → service → mapper → DB 계층을 독립적으로 소유한다.
다른 도메인의 Mapper를 직접 참조하는 일은 없고, 필요하면 Service 레이어끼리만 호출한다.

ORM 대신 MyBatis를 선택한 이유
스프링 프로젝트라면 JPA를 먼저 떠올리는 게 일반적이다.
이 프로젝트에서는 처음부터 MyBatis를 선택했다.
이유는 데이터 접근 패턴에 있다.
금융 데이터는 단순 CRUD보다 복잡한 집계 쿼리가 훨씬 많다.
- 보유 종목별 평균 단가 계산 → 매매 내역 전체 집계
- 손익 계산 → 현재가 + 평균단가 + 환율 + 배당금 복합 연산
- 날짜 범위 + 계좌별 필터 + 카테고리 구분 복합 조건
JPA + JPQL로 이런 쿼리를 쓰면 가독성이 떨어지고, N+1 문제를 의식하면서 설계해야 하는 피로가 생긴다.
MyBatis XML에 SQL을 직접 쓰면 EXPLAIN으로 성능을 바로 확인할 수 있다.

실제 쿼리 예시를 보면 왜 MyBatis가 편한지 바로 알 수 있다.
<!-- mapper/balance/BalanceMapper.xml -->
<select id="calculateBalance" resultType="BalanceDto">
SELECT
s.ticker,
s.stock_name,
SUM(t.quantity) AS total_quantity,
SUM(t.quantity * t.price) / SUM(t.quantity) AS avg_price,
sp.close_price AS current_price,
(sp.close_price - SUM(t.quantity * t.price) / SUM(t.quantity))
* SUM(t.quantity) AS unrealized_pnl
FROM trades t
JOIN stocks s ON t.stock_id = s.id
JOIN stock_prices sp ON sp.stock_id = s.id
AND sp.price_date = #{lastTradingDate}
WHERE t.account_no = #{accountNo}
AND t.trade_type = 'BUY'
GROUP BY s.ticker, s.stock_name, sp.close_price
</select>SQL이 XML에 명시적으로 드러나 있어서 디버깅이 훨씬 쉽다.
실행 중 슬로우 쿼리가 생기면 해당 XML만 꺼내서 EXPLAIN을 돌리면 된다.
JWT Stateless 인증 구조
인증은 JWT Stateless 방식으로 구현했다. 서버에 세션을 저장하지 않는다.
선택 이유는 단순하다. REST API 서버이고, 사용자가 나 혼자이며, 토큰 검증만 하면 되는 구조다.
토큰 구조
| 토큰 | 유효 시간 | 저장 위치 |
|---|---|---|
| Access Token | 1시간 | 서버 미저장 |
| Refresh Token | 24시간 | DB (refresh_token 테이블) |
모든 요청은 JwtAuthFilter를 통과한다.Authorization: Bearer {token} 헤더를 읽고 유효성을 검증한 뒤
SecurityContextHolder에 인증 정보를 등록한다.

// JwtAuthFilter.java
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) {
String token = resolveToken(request);
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication auth = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}인증 제외 경로는 /api/auth/**, /swagger-ui/**, /api-docs/** 세 가지다.
CSRF는 REST API이므로 비활성화, 세션은 STATELESS 정책을 명시했다.
공통 응답 구조와 예외 처리
초반에 귀찮아서 건너뛰고 싶었던 부분이다. 결과적으로 가장 잘 한 결정이 됐다.
모든 REST 응답은 동일한 구조를 사용한다.
{
"code": 200,
"message": "success",
"data": { ... }
}ApiResponse<T>를 제네릭으로 만들어두면 Controller는 ApiResponse.success(data) 한 줄로 응답을 만든다.
프론트엔드도 response.data.code === 200 하나로 성공/실패를 판별한다.
예외 처리는 @RestControllerAdvice 하나에 집중했다.
// 사용 예시 — Service 레이어
if (trade.getQuantity() <= 0) {
throw new CustomException(HttpStatus.BAD_REQUEST, "수량은 0보다 커야 합니다");
}덕분에 Controller에 try-catch가 없다.

외부 API 호출 — 추가 의존성 없이
외부 API 호출에 RestTemplate이나 WebClient 대신 Java 11+ 내장 HttpClient를 직접 사용했다.
이유는 세 가지다.
- 추가 의존성이 없다.
build.gradle에 항목이 하나 줄어든다 - 동기/비동기 모두 지원한다. 나중에 비동기 전환 시 API가 동일하다
- 개인 프로젝트 수준에서 HTTP 클라이언트 추상화 레이어를 하나 더 만드는 건 오버엔지니어링이다
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Content-Type", "application/json")
.GET()
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
RSS 파싱에는 com.rometools:rome 라이브러리를 사용했다.
XML을 직접 파싱하는 것보다 훨씬 깔끔하게 피드 항목을 순회할 수 있다.
마무리
백엔드 설계에서 선택한 것들을 정리하면 다음과 같다.
| 패키지 구조 | 도메인 중심 | 수정 범위가 도메인 안에서 끝남 |
| ORM | MyBatis | 집계 쿼리 비중이 높고 SQL 직접 제어 필요 |
| 인증 | JWT Stateless | REST API + 단일 사용자 구조 |
| 응답 구조 | ApiResponse 공통화 | 프론트엔드 처리 단순화 |
| HTTP 클라이언트 | Java 내장 HttpClient | 추가 의존성 없이 충분 |
다음 편에서는 이 백엔드 위에서 매일 새벽 돌아가는 파이프라인을 어떻게 설계했는지 다룬다.@Scheduled, Quartz, Spring Batch 세 가지 스케줄링 전략을 왜 함께 쓰는지,
9단계 파이프라인의 시간 배치 기준도 함께 정리한다.