AI 질의응답-entity 구조
서론
ai에게 이것저것 질문하며 프로젝트를 어떤 식으로 구현할지 고민하던 중 entity 구조에 대한 질문이 떠올라 질문했더니 대답을 잘해줬다.
기록해볼만한 내용이라 생각해 포스팅하기로 했다.
질의응답
나의 말:
게시판 같은 경우는 목록에서 선택해서 상세를 보는데 이러면 board_title entity와 board_content entity를 따로 만들어야 해? 하나로 만들면 목록을 나열할 때에도 상세까지 모두 불러와야 하잖아
답변:
매우 통찰력 있는 질문이십니다! 데이터베이스 설계와 엔티티 구조에 관한 중요한 고려사항을 다루고 계십니다.
게시판 엔티티 설계 전략
1. 단일 엔티티 접근법 (현재 구조)
현재 있는 Board
엔티티 하나로 모든 정보를 관리하는 방식입니다:
public class Board {
@Id
private Integer id;
private String title;
private String content; // 여기에 큰 텍스트가 저장됨
private Integer hit;
// 기타 필드...
}
장점:
- 구조가 단순하고 이해하기 쉽습니다.
- 조회 시 JOIN이 필요 없어 쿼리가 간단합니다.
- 트랜잭션 관리가 쉽습니다.
단점:
- 목록 조회 시 불필요한 content 데이터까지 함께 로드됩니다.
- 게시글이 많고 content가 큰 경우 성능 저하가 발생할 수 있습니다.
2. 분리된 엔티티 접근법
내용이 큰 필드를 별도 엔티티로 분리하는 방식입니다:
public class BoardSummary {
@Id
private Integer id;
private String title;
private Integer hit;
// 목록에 필요한 간단한 필드들...
@OneToOne(mappedBy = "boardSummary", fetch = FetchType.LAZY)
private BoardContent content;
}
public class BoardContent {
@Id
private Integer id;
@OneToOne
@MapsId
@JoinColumn(name = "board_id")
private BoardSummary boardSummary;
@Lob
private String content; // 여기에 큰 텍스트가 저장됨
}
장점:
- 목록 조회 시 필요한 데이터만 가져와 성능이 향상됩니다.
- 큰 텍스트 필드를 분리하여 메모리 사용이 효율적입니다.
단점:
- 구조가 복잡해집니다.
- 상세 페이지 조회 시 추가 쿼리나 JOIN이 필요합니다.
- 트랜잭션 관리가 복잡할 수 있습니다.
실무에서의 최적 접근법
대부분의 실제 애플리케이션에서는 성능과 복잡성 사이의 균형을 잘 맞추어야 합니다. 아래는 몇 가지 효과적인 전략입니다:
1. 단일 엔티티 + JPQL 프로젝션 (추천)
@Entity
@Table(name = "board")
public class Board {
// 현재 구조 유지
@Id
private Integer id;
private String title;
@Lob
private String content;
private Integer hit;
// 기타 필드...
}
@Repository
public interface BoardRepository extends JpaRepository<Board, Integer> {
// 목록 조회용 - content 필드 제외
@Query("SELECT new com.example.demo.dto.BoardListDto(b.id, b.title, b.hit, b.user, b.createdAt) FROM Board b")
Page<BoardListDto> findAllForList(Pageable pageable);
// 상세 조회용 - 모든 필드 포함
Optional<Board> findById(Integer id);
}
// DTO 클래스
@Getter
@AllArgsConstructor
public class BoardListDto {
private Integer id;
private String title;
private Integer hit;
private User user;
private LocalDateTime createdAt;
// content 필드는 제외
}
장점:
- 엔티티 구조는 단순하게 유지합니다.
- 목록 조회 시 필요한 필드만 선택적으로 가져와 성능이 향상됩니다.
- 상세 페이지 조회 시 기존 findById 메서드를 사용합니다.
2. 지연 로딩(LAZY) 활용
@Entity
public class Board {
@Id
private Integer id;
private String title;
private Integer hit;
@Basic(fetch = FetchType.LAZY)
@Lob
private String content; // 지연 로딩으로 설정
}
참고: Hibernate 6.2 이상에서는 @Lob
필드의 기본 fetch 타입이 LAZY로 변경되었습니다.
결론
- 소규모 ~ 중규모 애플리케이션:
- 단일 엔티티 + DTO 프로젝션 방식이 가장 실용적입니다.
- 복잡성 대비 성능 이점이 큽니다.
- 대규모 애플리케이션:
- 데이터가 매우 크고 사용자가 많은 경우, 분리된 엔티티 접근법이 더 효율적일 수 있습니다.
- 또는 더 나아가 CQRS 패턴을 적용하여 조회와 명령을 완전히 분리하는 것도 고려할 수 있습니다.
첫 번째 접근법(단일 엔티티 + DTO 프로젝션)이 대부분의 경우에 좋은 균형을 제공합니다. 코드도 비교적 간단하면서도 성능상의 이점을 얻을 수 있기 때문입니다.
나의 말:
두 접근법 중 두번째를 추천하지 않는 이유는 뭐야? jpql을 작성하지 않아도 되고 비슷한 효과를 얻을 수 있을 것 같은데
답변:
매우 좋은 질문입니다! 지연 로딩(LAZY) 방식을 왜 첫 번째 방법보다 덜 추천하는지에 대한 중요한 이유들이 있습니다.
지연 로딩(LAZY) 방식이 덜 추천되는 이유
1. N+1 문제 발생 가능성
@Basic(fetch = FetchType.LAZY)
@Lob
private String content;
- 문제: 게시글 목록을 조회한 후, 루프를 돌면서 각 게시글의 일부 정보에 접근할 때 각 게시글마다 추가 쿼리가 발생합니다.
List<Board> boards = boardRepository.findAll(pageable).getContent();
for (Board board : boards) {
// 이 시점에 content에 접근하지 않으면 추가 쿼리 없음
String title = board.getTitle(); // 문제 없음
// 하지만 실수로 content에 접근하면?
String contentPreview = board.getContent().substring(0, 10); // 추가 쿼리 발생!
}
- 개발자가 실수로 지연 로딩 필드에 접근하면 성능 문제가 발생합니다.
- 특히 여러 개발자가 협업하는 프로젝트에서는 이런 실수가 발생하기 쉽습니다.
2. Lazy 로딩의 프록시 메커니즘 제약
- JPA의 지연 로딩은 프록시 객체를 통해 구현됩니다.
@Basic(fetch = FetchType.LAZY)
는 Hibernate 구현에 의존적이며, JPA 표준에서 완벽하게 지원되지 않을 수 있습니다.- 일부 DB에서는 큰 필드(CLOB, TEXT 등)의 지연 로딩이 제대로 동작하지 않을 수 있습니다.
3. 영속성 컨텍스트 관리의 복잡성
- 지연 로딩은 영속성 컨텍스트가 살아있을 때만 동작합니다.
- 트랜잭션이 끝난 후 지연 로딩 필드에 접근하면
LazyInitializationException
이 발생합니다. - 이를 방지하기 위한 추가 코드(OpenEntityManagerInViewFilter 등)가 필요할 수 있습니다.
4. 명시적 vs 암묵적 쿼리
- DTO 프로젝션은 명시적으로 어떤 필드가 조회되는지 코드에서 바로 확인할 수 있습니다.
- 지연 로딩은 암묵적으로 필요할 때 쿼리가 발생하므로 코드만 봐서는 어떤 쿼리가 실행될지 예측하기 어렵습니다.
5. 테스트의 어려움
- 지연 로딩 방식은 테스트 환경에서 재현하기 어려운 문제를 일으킬 수 있습니다.
- DTO 방식은 명확한 데이터 구조를 가지므로 테스트가 더 쉬워집니다.
실무 관점에서의 추천
1. 명확성과 안전성
// DTO 프로젝션 방식
@Query("SELECT new com.example.demo.dto.BoardListDto(b.id, b.title, b.hit) FROM Board b")
Page<BoardListDto> findAllForList(Pageable pageable);
- 이 코드는 어떤 필드가 조회되는지 명확하게 보여줍니다.
- 개발자가 실수로 콘텐츠 필드에 접근할 수 없어 안전합니다.
2. 성능 예측 가능성
- DTO 프로젝션은 항상 하나의 쿼리만 실행하여 성능이 예측 가능합니다.
- 지연 로딩은 코드 흐름에 따라 추가 쿼리가 발생할 수 있어 성능 예측이 어렵습니다.
3. 코드 유지보수성
- DTO 클래스는 API 응답과 직접 연결되므로 프론트엔드와의 계약을 명확히 합니다.
- 화면에 필요한 데이터만 정확히 주고받을 수 있습니다.
실제 개발자들의 선택
실무에서 많은 경험 있는 개발자들은 다음의 이유로 DTO 프로젝션 방식을 선호합니다:
- 명시적인 쿼리와 데이터 구조로 인한 코드 가독성
- 필요한 데이터만 조회하여 얻는 성능 이점
- N+1 문제를 원천 차단하는 안전성
- API 응답과 데이터베이스 엔티티를 분리하여 얻는 유연성
지연 로딩은 단순한 애플리케이션이나 프로토타입에서는 편리할 수 있지만, 복잡하고 성능이 중요한 실제 서비스에서는 DTO 프로젝션이 더 안정적인 선택입니다.
정리
내 생각대로 내용까지 포함한 게시판의 entity는 약간의 오버헤드를 포함한다는 사실을 알 수 있었고, 그에 대한 해결책도 얻을 수 있었다.
나는 두번째 접근법도 괜찮다고 생각했는데 확실히 다른 사람과 같이 프로젝트를 할 때에는 이런 부분도 생각하면서 구현해야 하는 것 같다.
내가 개념으로 구현할 줄 아는 것과 많은 노하우가 적용된 실무에서의 방법은 큰 차이가 있는데 AI는 설명과 함께 실무에서의 접근법을 추천해주니 배울 것이 많고 좋은 것 같다. AI의 지식을 내 것으로 흡수할 수 있도록 잘 정리하고 되새겨야겠다.