1. 들어가며: 매수는 했는데, 내 돈은 어디에?
지난 포스팅까지 매수/매도 기능을 구현하여 DB에 데이터가 쌓이는 것까지 확인했습니다. 하지만 사용자 입장에서는 DB를 직접 열어볼 수 없으니, **"내 잔고가 얼마 남았고, 주식이 얼마나 있는지"**를 확인할 방법이 없습니다.
이번 포스팅에서는 사용자의 **'내 자산 현황'**을 조회하는 백엔드 로직을 구현하고, 그 과정에서 기존의 주문 API를 조금 더 RESTful하게 리팩토링한 과정을 정리해 봅니다.
2. 주문 API 리팩토링: RESTful하게 통합하기
자산 조회 기능을 만들기 전에, 기존에 작성했던 주문(Order) 관련 코드를 먼저 다듬기로 했습니다. 처음에는 단순하게 생각해서 **매수(POST /orders/buy)**와 **매도(POST /orders/sell)**를 별개의 API로 만들었습니다. 기능상으로는 잘 돌아갔지만, 곰곰이 생각해보니 아쉬운 점이 있었습니다.
🤔 고민: "주문(Order)이라는 행위는 하나인데, 굳이 URL을 나눠야 할까?"
RESTful API 설계 원칙에 따르면 URL은 **행위(Verb)**가 아닌 **자원(Resource)**을 나타내야 합니다. 따라서 /buy, /sell처럼 URL에 행위를 넣는 것보다는, /api/orders라는 하나의 자원으로 통합하고, **요청 본문(Body)**에 타입을 담아 보내는 것이 훨씬 깔끔하고 확장성이 좋다고 판단했습니다.
Refactoring: Controller는 "접수"만 받자
컨트롤러의 역할을 **"요청을 받아 서비스에 넘기는 접수처"**로 명확히 했습니다. 어떤 주문인지 판단하는 로직도 서비스로 내렸습니다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
@PostMapping
public ResponseEntity<String> createOrder(@RequestBody OrderRequestDto requestDto) {
// Controller는 if/else 로직을 몰라도 됩니다.
// 그냥 Service의 createOrder한테 "이거 처리해줘"라고 던지면 끝입니다.
orderService.createOrder(requestDto);
return ResponseEntity.ok("주문 접수 완료");
}
}
Refactoring: Service에서 분기 처리
이제 서비스 계층에서 DTO의 타입을 확인해 적절한 비즈니스 로직(매수/매도)을 수행합니다.
@Service
@RequiredArgsConstructor
@Transactional
public class OrderService {
// ... Repository 의존성 주입 ...
// [New] 외부에서 들어오는 유일한 창구
public void createOrder(OrderRequestDto dto) {
// 1. DTO에서 주문 타입 확인 ("BUY" or "SELL")
if ("BUY".equalsIgnoreCase(dto.getOrderType())) {
// 2. 매수 로직 호출
buy(dto.getUserId(), dto.getCode(), dto.getQuantity());
} else if ("SELL".equalsIgnoreCase(dto.getOrderType())) {
// 3. 매도 로직 호출
sell(dto.getUserId(), dto.getCode(), dto.getQuantity());
} else {
// 4. 예외 처리
throw new IllegalArgumentException("잘못된 주문 타입입니다.");
}
}
// 내부 로직 (외부에선 알 필요 없으니 private으로 변경)
private void buy(Long userId, String code, Long quantity) {
// ... (기존 매수 로직과 동일) ...
}
private void sell(Long userId, String code, Long quantity) {
// ... (기존 매도 로직과 동일) ...
}
}
이렇게 구조를 바꾸니 프론트엔드 입장에서도 API 주소를 하나만 관리하면 되어 훨씬 편해졌습니다.
3. Service 구현: 자산 현황 계산 로직 (StockHoldingService)
이제 본격적으로 '내 자산 조회' 기능을 구현합니다. 화면에 뿌려줄 정보는 단순한 DB 조회가 아니라, 계산된 파생 데이터들이 필요합니다.
- 총 자산: 예수금 + (보유 주식의 현재 가치 총합)
- 총 평가 손익: (현재 가치 총합) - (매수 금액 총합)
- 수익률: (평가 손익 / 매수 금액) * 100
이 로직을 StockHoldingService에 구현했습니다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class StockHoldingService {
private final StockHoldingRepository stockHoldingRepository;
private final AccountRepository accountRepository;
public MyAssetDto getMyAssetStatus(Long userId) {
// 1. [자산] 예수금 가져오기
Account account = accountRepository.findByUserId(userId)
.orElseThrow(() -> new IllegalArgumentException("계좌 없음"));
Long balance = account.getBalance();
// 2. [주식] 리스트 가져오기
List<StockHolding> myStocks = stockHoldingRepository.findAllByUserId(userId);
// 3. [계산] 합산 로직
long totalPurchaseAmount = 0; // 총 매수 금액
long totalEvaluationAmount = 0; // 총 평가 금액
// 미리 DTO 리스트를 준비합니다.
List<StockHoldingDto> holdingDtos = myStocks.stream()
.map(StockHoldingDto::new) // 생성자 참조를 통해 변환
.toList();
for (StockHolding holding : myStocks) {
// ★ 도메인 활용 포인트! holding 안에서 바로 Stock을 꺼내 가격 확인
Double currentPrice = holding.getStock().getCurrentPrice();
Double avgPrice = holding.getAvgPrice();
Long quantity = holding.getQuantity();
// 전체 자산 계산
totalPurchaseAmount += (long) (quantity * avgPrice);
totalEvaluationAmount += (long) (quantity * currentPrice);
}
// 4. [최종] 파생 데이터(수익률 등) 계산
long totalAssetAmount = balance + totalEvaluationAmount;
long totalProfitLoss = totalEvaluationAmount - totalPurchaseAmount;
double returnRate = 0.0;
if (totalPurchaseAmount > 0) {
returnRate = ((double) totalProfitLoss / totalPurchaseAmount) * 100;
}
// 5. 결과 반환 (DTO로 포장)
return new MyAssetDto(
totalAssetAmount,
balance,
totalPurchaseAmount,
totalEvaluationAmount,
totalProfitLoss,
returnRate,
holdingDtos
);
}
}
여기서 중요한 점은 Entity를 그대로 반환하지 않고, MyAssetDto와 StockHoldingDto로 변환해서 반환한다는 점입니다. (이게 왜 중요한지는 다음 포스팅에서 뼈저리게 느끼게 됩니다...😅)
4. API 테스트: 브라우저로 날것의 데이터 확인하기
Postman 같은 거창한 도구까지 쓸 필요도 없습니다. GET 요청은 브라우저 주소창이 최고의 테스트 도구니까요. 서버를 켜고 크롬 주소창에 http://localhost:8080/api/my-assets?userId=29를 입력해 보았습니다.
(확인 결과)

JSON 데이터가 아주 예쁘게(?) 잘 넘어오는 것을 확인했습니다. 백엔드 로직은 완벽하게 준비되었습니다.
5. 마치며
이제 백엔드 API는 준비되었습니다. 남은 건 이 JSON 데이터를 받아 HTML 화면에 예쁘게 뿌려주는 일뿐입니다. "그냥 HTML 만들어서 타임리프나 JS로 연결하면 끝나겠지?"라고 가볍게 생각했지만, 다음 단계에서 Spring Boot JPA 개발자들이 한 번쯤 겪는다는 '그 에러'를 마주하게 됩니다.
다음 포스팅에서는 my_asset.html 화면 구현 과정과, 그 과정에서 발생한 500 에러(순환 참조 문제) 해결기를 다뤄보겠습니다.
'개발 공부 > 프로젝트' 카테고리의 다른 글
| [사이드 프로젝트] 가상 주식 거래소 만들기 (13) - DTO 변환과 Stream으로 완성한 내 자산 대시보드 (1) | 2026.01.07 |
|---|---|
| [사이드 프로젝트] 가상 주식 거래소 만들기 (12) - 화면 연동과 500 에러의 습격 (JPA 순환 참조) (0) | 2026.01.07 |
| [사이드 프로젝트] 가상 주식 거래소 만들기 (10) - 마침내 첫 거래 성사! (HTML/JS 연동과 디버깅) (0) | 2026.01.03 |
| [사이드 프로젝트] 가상 주식 거래소 만들기 (9) - 주문을 받는 창구, OrderController와 REST API (0) | 2025.12.10 |
| [사이드 프로젝트] 가상 주식 거래소 만들기 (8) - 핵심 비즈니스 로직 구현 (매수/매도와 트랜잭션) (0) | 2025.12.09 |