1. 들어가며: 해결의 열쇠는 'DTO'
지난 포스팅에서 StockHolding 엔티티를 직접 반환하려다 무한 루프(StackOverflow)와 프록시 에러의 늪에 빠졌습니다. @JsonIgnore 같은 임시방편을 고민했지만, 결국 정공법을 택하기로 했습니다.
"엔티티는 DB를 위한 것이고, 뷰(View)를 위한 객체는 따로 있어야 한다."
그래서 StockHolding 엔티티를 대체할 **StockHoldingDto**를 새로 만들고, 데이터를 안전하게 옮겨 담는 작업을 시작했습니다.
2. DTO 설계: 안전한 데이터 박스 만들기
먼저 문제가 되었던 StockHolding 엔티티 대신, 화면에 보여줄 데이터만 담을 DTO를 정의했습니다. 여기엔 User 객체 같은 위험한 필드는 빼버리고, 종목명, 수량, 평단가 같이 딱 필요한 정보만 넣습니다.
@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 리스트를 담도록 바꿨습니다.
@Getter
@AllArgsConstructor
public class MyAssetDto {
// ... (기타 자산 필드들) ...
// [수정] List<StockHolding> -> List<StockHoldingDto>
private List<StockHoldingDto> stockHoldings;
}
3. Service 로직 수정: Stream으로 우아하게 변환하기
이제 서비스 계층(StockHoldingService)에서 엔티티 리스트를 DTO 리스트로 변환해줘야 합니다. for문을 돌려도 되지만, Java 8의 Stream API를 사용하면 이 과정을 아주 직관적이고 깔끔하게 처리할 수 있습니다.
// 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)에서 데이터를 받아 예쁘게 보여줄 차례입니다. 수익률이 양수(+)면 빨간색, 음수(-)면 파랑색으로 보이도록 스타일을 적용했습니다.
// 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)" 데이터를 자동으로 관리하는 방법을 다뤄보겠습니다.