최원종의 개발 블로그

V3-2 게시글 상세보기 (연관관계 기본 활용) 본문

Spring boot 입문

V3-2 게시글 상세보기 (연관관계 기본 활용)

chl6698 2026. 5. 11. 17:44

게시글 상세보기 화면


Board 코드

   @ManyToOne(fetch = FetchType.EAGER)
    // @OneToMany
    // @OneToOne
    @JoinColumn(name = "user_id") // 외래키 컬럼명 표시 됨
    private User user;

 

 

코드 분석

① @ManyToOne(fetch = FetchType.EAGER)
연관된 데이터를 어느 시점에 가져올지 결정하는 로딩 전략.

EAGER (즉시 로딩): 게시글(Board)을 조회할 때, JPA가 Join 쿼리를 사용해서 작성자(User) 정보까지 한 번에 다 들고옴.

LAZY (지연 로딩): 게시글만 먼저 가져오고, 
나중에 board.getUser()를 호출해서 실제 데이터가 필요한 순간에 DB에 추가 쿼리를 날림.

② @JoinColumn(name = "user_id")
외래키(FK)를 관리하는 주인임을 나타냄.

name = "user_id"는 DB 테이블에 생성될 컬럼명을 지정하는 것. 
만약 이걸 생략하면 user_id가 기본값으로 들어가지만, 명시적으로 적어주는 것이 유지보수에 좋음.

게시글 상세 보기 요청 조회 코드

// 게시글 상세보기 요청 (조회) (필수값 기본키로 조회)
public Board findById(Integer id) {

    // 영속성 컨텍스트를 사용하기 위해
    // 1. 엔티티 매니저에서 제공하는 메서드를 활용 방법
    Board board = em.find(Board.class, id);

    // 2. JPQL 문법으로 Board 를 조회 하는 방법
//        String jpql = """
//                SELECT b FROM Board b WHERE b.id = :id
//                """;

//        return em.createQuery(jpql, Board.class)
//                .setParameter("id", id)
//                .getSingleResult();
    return board;
}

EAGER와 LAZY 차이

EAGER (즉시 로딩): "데이터를 조회할 때 연관된 객체까지 한꺼번에 다 가져온다."

LAZY (지연 로딩): "연관된 객체는 일단 비워두고(프록시), 실제로 사용할 때 DB에서 가져온다."

 

@ManyToOne(fetch = FetchType.EAGER)

Hibernate: 
    select
        b1_0.id,
        b1_0.content,
        b1_0.created_at,
        b1_0.title,
        u1_0.id,
        u1_0.created_at,
        u1_0.email,
        u1_0.password,
        u1_0.username 
    from
        board_tb b1_0 
    left join
        user_tb u1_0 
            on u1_0.id=b1_0.user_id 
    where
        b1_0.id=?

 

@ManyToOne(fetch = FetchType.LAZY)

Hibernate: 
    select
        b1_0.id,
        b1_0.content,
        b1_0.created_at,
        b1_0.title,
        b1_0.user_id 
    from
        board_tb b1_0 
    where
        b1_0.id=?

EAGER와 LAZY 상세 분석표

구분 EAGER (즉시 로딩) LAZY (지연 로딩)
개념 엔티티 조회 시 연관된 객체를 함께 조회 연관된 객체를 실제 사용할 때 조회
조회 방식 JOIN을 사용하여 한 번에 쿼리 실행 프록시(Proxy) 객체로 조회 후 필요시 쿼리 실행
쿼리 발생 시점 em.find() 호출 시점 obj.getRelation().getName() 호출 시점
연관관계 기본값 @ManyToOne, @OneToOne @OneToMany, @ManyToMany
장점 연관 데이터를 항상 함께 사용할 때 편리함 불필요한 조인을 방지하여 성능 최적화에 유리
단점 예상치 못한 복잡한 조인과 성능 저하 유발 연관 객체 사용 시마다 추가 쿼리 발생 (N+1 고려 필요)

 


detail-mustache 코드

{{> layout/header}}
<div class="container p-5">

    <!-- 수정삭제버튼 섹션 -->
    <!-- d-flex justify-content-end: 버튼들을 오른쪽 정렬 -->
    <div class="d-flex justify-content-end">
        <!--
        수정 버튼: GET 방식으로 수정 폼 페이지로 이동
        {{! board.id}}: 현재 게시글의 ID를 URL에 포함
        RESTful URL 패턴: /board/{id}/update-form
        -->
        <a href="/board/{{board.id}}/update-form" class="btn btn-warning me-1">수정</a>

        <!--
        삭제 버튼: POST 방식으로 삭제 요청
        form을 사용하는 이유: DELETE 동작은 GET이 아닌 POST/DELETE 방식 사용
        RESTful URL 패턴: /board/{id}/delete
        -->
        <form action="/board/{{board.id}}/delete" method="post">
            <button class="btn btn-danger">삭제</button>
        </form>
    </div>

    <!-- 작성자 정보 표시 -->
    <div class="d-flex justify-content-end">
        <!-- {{! board.username}}: Board 객체의 getUsername() 메서드 호출 -->
        <b>작성자</b> : {{board.user.username}}
    </div>

    <!-- 게시글 내용 섹션 -->
    <div>
        <!-- 게시글 제목 -->
        <h2><b>{{board.title}}</b></h2>
        <hr />
        <!-- 게시글 본문 -->
        <div class="m-4 p-2">
            <!--
            {{! board.content}}: 게시글 내용 출력
            HTML 태그가 포함된 내용은 {{! board.content}} 형태로 사용 (이스케이프 해제)
            보안상 사용자 입력 내용은 기본적으로 HTML 태그를 이스케이프 처리
            -->
            {{board.content}}
        </div>
    </div>

    <!-- 댓글 섹션 (향후 구현 예정) -->
    <div class="card mt-3">
        <!-- 댓글 등록 폼 -->
        <div class="card-body">
            <!--
            댓글 등록 폼: POST 방식으로 댓글 저장
            향후 댓글 기능 구현시 사용할 예정
            현재는 정적 HTML로만 구성
            -->
            <form action="/reply/save" method="post">
                <textarea class="form-control" rows="2" name="comment" placeholder="댓글을 입력하세요"></textarea>
                <div class="d-flex justify-content-end">
                    <button type="submit" class="btn btn-outline-primary mt-1">댓글등록</button>
                </div>
            </form>
        </div>

        <!-- 댓글 목록 헤더 -->
        <div class="card-footer">
            <b>댓글리스트</b>
        </div>

        <!-- 댓글 목록 (현재는 정적 더미 데이터) -->
        <div class="list-group">
            <!-- 댓글 아이템 1 -->
            <div class="list-group-item d-flex justify-content-between align-items-center">
                <div class="d-flex">
                    <!-- 댓글 작성자 배지 -->
                    <div class="px-1 me-1 bg-primary text-white rounded">cos</div>
                    <!-- 댓글 내용 -->
                    <div>댓글 내용입니다</div>
                </div>
                <!-- 댓글 삭제 버튼 -->
                <form action="/reply/1/delete" method="post">
                    <button class="btn">🗑</button>
                </form>
            </div>

            <!-- 댓글 아이템 2 -->
            <div class="list-group-item d-flex justify-content-between align-items-center">
                <div class="d-flex">
                    <div class="px-1 me-1 bg-primary text-white rounded">ssar</div>
                    <div>댓글 내용입니다</div>
                </div>
                <form action="/reply/1/delete" method="post">
                    <button class="btn">🗑</button>
                </form>
            </div>
        </div>
    </div>
</div>

{{> layout/footer}}