개발 공부/프로젝트

[사이드 프로젝트] 가상 주식 거래소 만들기 (13) - DTO 변환과 Stream으로 완성한 내 자산 대시보드

baby-t 2026. 1. 7. 10:43

1. 들어가며: 해결의 열쇠는 'DTO'

지난 포스팅에서 StockHolding 엔티티를 직접 반환하려다 무한 루프(StackOverflow)와 프록시 에러의 늪에 빠졌습니다. @JsonIgnore 같은 임시방편을 고민했지만, 결국 정공법을 택하기로 했습니다.

"엔티티는 DB를 위한 것이고, 뷰(View)를 위한 객체는 따로 있어야 한다."

그래서 StockHolding 엔티티를 대체할 **StockHoldingDto**를 새로 만들고, 데이터를 안전하게 옮겨 담는 작업을 시작했습니다.


2. DTO 설계: 안전한 데이터 박스 만들기

먼저 문제가 되었던 StockHolding 엔티티 대신, 화면에 보여줄 데이터만 담을 DTO를 정의했습니다. 여기엔 User 객체 같은 위험한 필드는 빼버리고, 종목명, 수량, 평단가 같이 딱 필요한 정보만 넣습니다.

Java
 
@Getter
@NoArgsConstructor
public class StockHoldingDto {
    private String stockName;   // 객체(Stock) 대신 문자열(이름)만!
    private String stockCode;
    private Long quantity;
    private Double avgPrice;
    private Double currentPrice;
    private Double profitRate;  // 수익률 (계산해서 넣을 예정)

    // Entity -> DTO 변환을 위한 생성자
    public StockHoldingDto(StockHolding entity) {
        this.stockName = entity.getStock().getName(); // ★ 여기서 실제 데이터를 꺼냄 (Lazy Loading 초기화)
        this.stockCode = entity.getStock().getCode();
        this.quantity = entity.getQuantity();
        this.avgPrice = entity.getAvgPrice();
        this.currentPrice = entity.getStock().getCurrentPrice();
        
        // 수익률 계산 로직 포함
        if (this.avgPrice > 0) {
            this.profitRate = (this.currentPrice - this.avgPrice) / this.avgPrice * 100;
        } else {
            this.profitRate = 0.0;
        }
    }
}

그리고 기존의 MyAssetDto도 수정하여, 엔티티 리스트 대신 방금 만든 DTO 리스트를 담도록 바꿨습니다.

Java
@Getter
@AllArgsConstructor
public class MyAssetDto {
    // ... (기타 자산 필드들) ...
    
    // [수정] List<StockHolding> -> List<StockHoldingDto>
    private List<StockHoldingDto> stockHoldings; 
}

3. Service 로직 수정: Stream으로 우아하게 변환하기

이제 서비스 계층(StockHoldingService)에서 엔티티 리스트를 DTO 리스트로 변환해줘야 합니다. for문을 돌려도 되지만, Java 8의 Stream API를 사용하면 이 과정을 아주 직관적이고 깔끔하게 처리할 수 있습니다.

Java
// StockHoldingService.java 수정

public MyAssetDto getMyAssetStatus(Long userId) {
    // 1. 엔티티 리스트 조회
    List<StockHolding> myStocks = stockHoldingRepository.findAllByUserId(userId);

    // ... (자산 합계 계산 로직은 기존과 동일) ...

    // 2. ★ 핵심 로직: Entity List -> DTO List 변환
    List<StockHoldingDto> holdingDtos = myStocks.stream()
            .map(StockHoldingDto::new) // 생성자 참조: 각 엔티티를 DTO 생성자에 넣음
            .toList(); // 변환된 객체들을 다시 리스트로 묶음

    // 3. 최종 반환 (DTO 리스트를 담아서)
    return new MyAssetDto(
            totalAssetAmount,
            balance,
            ...,
            holdingDtos // 안전한 DTO 리스트 전달
    );
}

[기술적 포인트]

  • map(StockHoldingDto::new)가 실행되는 시점에 entity.getStock().getName()이 호출되면서, 프록시(가짜 객체)가 아닌 진짜 DB 데이터가 조회됩니다.
  • 이 작업은 트랜잭션(@Transactional) 안에서 이루어지므로 안전하게 데이터를 가져올 수 있습니다.
  • 결과적으로 컨트롤러로 나가는 데이터는 **순수한 Java 객체(POJO)**들뿐이라, JSON 변환기가 에러를 낼 일이 사라집니다.

4. 프론트엔드 마무리: 빨강과 파랑의 조화

백엔드 에러가 사라졌으니, 이제 프론트엔드(my_asset.html)에서 데이터를 받아 예쁘게 보여줄 차례입니다. 수익률이 양수(+)면 빨간색, 음수(-)면 파랑색으로 보이도록 스타일을 적용했습니다.

JavaScript
// updateTableUI 함수 내부
stockHoldings.forEach(holding => {
    // DTO 필드명에 맞춰 변수 사용 (holding.stockName)
    const profitLoss = (holding.currentPrice - holding.avgPrice) * holding.quantity;
    
    // 색상 클래스 결정 (Bootstrap + Custom CSS)
    const colorClass = profitLoss > 0 ? 'positive' : (profitLoss < 0 ? 'negative' : '');
    
    const row = `
        <tr>
            <td class="fw-bold">${holding.stockName}</td>
            <td>${holding.quantity}주</td>
            <td class="${colorClass}">${holding.profitRate.toFixed(2)}%</td>
            <td><button class="btn btn-danger btn-sm">매도</button></td>
        </tr>
    `;
    tableBody.innerHTML += row;
});

5. 결과 확인: 드디어 완성된 대시보드

코드를 모두 적용하고 서버를 재시작했습니다. 떨리는 마음으로 접속한 결과...

  • 총 자산: 10억 4백만 원 (초기 자본금 10억 + 수익)
  • 평가 손익: +387만 원
  • 보유 주식: KRW-BTC(비트코인) 2주, 수익률 +1.46%

500 에러는 씻은 듯이 사라졌고, DB에 있는 데이터가 정확하게 계산되어 화면에 출력되었습니다.


6. 시리즈 중간 회고 (Refactoring)

이번 구현을 통해 **"돌아가기만 하는 코드"**와 **"좋은 구조를 가진 코드"**의 차이를 경험했습니다.

  • RESTful 리팩토링: API 주소를 직관적으로 통합.
  • DTO 패턴 도입: 순환 참조 해결 및 보안 강화.
  • Stream API 활용: 가독성 높은 데이터 변환 로직 구현.

이제 "내 돈이 얼마나 있는지"는 알 수 있게 되었습니다. 하지만 아직 **"내가 언제 이걸 샀지?"**에 대한 기록은 볼 수 없습니다. 다음 포스팅에서는 [거래 내역(History)] 페이지를 구현하고, JPA Auditing을 활용해 "언제(Created At)" 데이터를 자동으로 관리하는 방법을 다뤄보겠습니다.