Skip to content

Conversation

@oOccasio
Copy link
Collaborator

@oOccasio oOccasio commented Dec 26, 2025

๐Ÿ”ฅPull requests

๐Ÿ‘ท ๊ณผ์ œ ๊ตฌํ˜„

ํ•„์ˆ˜๊ณผ์ œ

  • ์•ฑ์Ÿ ์‹ ์ฒญํ•˜๊ธฐ
  • ๊ธฐ๊ฒฝ, ๋ฐ‹์—… ์—ด์ •์ ์œผ๋กœ ์ฐธ์—ฌํ•˜๊ธฐ
  • ์•„ํ‹ฐํด์— ๋Œ“๊ธ€ ์ถ”๊ฐ€ํžˆ๊ธฐ

์„ ํƒ๊ณผ์ œ

  • ์บ์‹ฑ์œผ๋กœ ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๋ฐ ์ •๋ ฌ ์ตœ์ ํ™”
  • ์ธํ”„๋ผ ์„ค๊ณ„ ์‹œ๊ฐํ™”
  • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋„์ž…(์ผ๋ถ€)
  • ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ…(ํ•˜๋‚˜์˜ ํฐ ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ…๋ฐฉ ๊ตฌํ˜„)
  • N+1 ๊ฐœ์„  ๋ฐ ํ”„๋กœ์ ์…˜

๊ตฌํ˜„ํ•œ ๋‚ด์šฉ์— ๋Œ€ํ•ด์„œ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”๊ณผ ๊ณ ๋ฏผํ•œ ์ 

Comment + Member - N+1 ๋ฌธ์ œ

Comment์˜ ์ž‘์„ฑ์ž์ธ Member๋ฅผ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ์„œ ๋‚˜์˜ค๋Š” N+1 ๋ฌธ์ œ

2025-12-25T21:09:52.510+09:00  INFO 9120 --- [assignment] [nio-8080-exec-2] o.s.a.g.s.f.JwtAuthenticationFilter      : Authentication Successful: JwtAuthenticationToken [Principal=org.sopt.assignment.global.security.info.UserPrincipal@61afdbf5, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER]]
Hibernate: 
    select
        count(*) 
    from
        articles a1_0 
    where
        a1_0.id=?
Hibernate: 
    select
        c1_0.id,
        c1_0.article_id,
        c1_0.content,
        c1_0.created_at,
        c1_0.is_update,
        c1_0.member_id,
        c1_0.modified_at 
    from
        comments c1_0 
    left join
        articles a1_0 
            on a1_0.id=c1_0.article_id 
    where
        a1_0.id=? 
    limit
        ?
Hibernate: 
    select
        m1_0.id,
        m1_0.birthday,
        m1_0.created_at,
        m1_0.email,
        m1_0.gender,
        m1_0.modified_at,
        m1_0.name,
        m1_0.password,
        m1_0.role 
    from
        members m1_0 
    where
        m1_0.id=?
  • ๋กœ๊ทธ๋ฅผ ๋ณด๋ฉด select ๋ฅผ 3๊ฐœ๋ฅผ ๋‚ ๋ฆฌ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Œ
@Transactional(readOnly = true)
    public PageBaseDto<GetCommentResponseDto> getComments(Long articleId, Pageable pageable) {

        articleService.validateArticleExists(articleId);

        return PageBaseDto.from(commentRepository
                .findByArticleId(articleId, pageable).map(GetCommentResponseDto::from));
    }
  • Article์ด ์กด์žฌํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ํŒ๋‹จ์„ ํ•˜๋Š” articleId์ธ ๋ถ€๋ถ„ ํ•œ๊ฐœ
  • ๊ทธ๋ฆฌ๊ณ  commentRepository์—์„œ comment์™€ member๋ฅผ ๋ถ€๋ถ„์—์„œ 2๊ฐ€์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ชจ์Šต์„ ๋ณด์—ฌ์คŒ

โ†’ ์ด๋ฅผ fetch Join์œผ๋กœ ํ•ด๊ฒฐ

@Query("SELECT c FROM Comment c JOIN FETCH c.member WHERE c.article.id = :articleId")
    Page<Comment> findByArticleId(Long articleId, Pageable pageable);

ํ•˜์ง€๋งŒ Pagination๊ณผ Fetch Join์€ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ๊ฒƒ์œผ๋กœ ์•Œ๊ณ  ์žˆ์Œ

  • LIMIT์€ ํ˜ธ์ถœํ•œ ๊ฐ์ฒด ๊ธฐ์ค€์œผ๋กœ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์ด ์•„๋‹Œ row๊ธฐ์ค€์œผ๋กœ SELECTํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ•œ ๊ฐ์ฒด์— ์—ฌ๋ ค๊ฐ€์ง€์˜ ๊ฐ์ฒด๊ฐ€ Join๋˜์–ด ์žˆ๋Š” ๊ฒฝ์šฐ๋ฉด ๊ทธ ํ–‰์˜ ์ˆ˜๋งŒํผ ๋‹ค๋ฅธ ๊ฐ์ฒด์˜ ๊ฒฐ๊ณผ๊ฐ€ ์•ˆ๋ณด์ž„
  • ์ฆ‰ ํŽ˜์ด์ง•์ด ๊ผฌ์—ฌ๋ฒ„๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์— Hibernate๋Š” DB ํŽ˜์ด์ง• ๋ฐฉ์‹์ด์•„๋‹ˆ๋ผ ๋ฉ”๋ชจ๋ฆฌ ํŽ˜์ด์ง• ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๊ณ 
  • ๋ฉ”๋ชจ๋ฆฌ ํŽ˜์ด์ง•์€ ๋‹น์—ฐํžˆ ๋ชจ๋“  ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์˜จ ํ›„ ํŽ˜์ด์ง•ํ•˜๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ฉ”๋ชจ๋ฆฌ ์ดˆ๊ณผ๊ฐ€ ์ผ์–ด๋‚˜๊ณ  ์„ฑ๋Šฅ์ด ๋งค์šฐ ์•ˆ์ข‹์•„์ง

โ†’ @OneToMany ์˜ ๊ฒฝ์šฐ๋Š” ์ด๋Ÿฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€๋งŒ @ManyToOne ์˜ ๊ฒฝ์šฐ์—์„  ์ด๋Ÿฐ๋ฌธ์ œ๊ฐ€ ์ผ์–ด๋‚˜์ง€ ์•Š์Œ!

โ†’ ๊ทธ ์ด์œ ๋Š” ํ•œ๊ฐ์ฒด๋‹น ํ•˜๋‚˜์˜ Member๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ๋•Œ๋ฌธ์— ์—ฌ๋Ÿฌ๊ฐ€์ง€ ๊ฐ์ฒด๊ฐ€ Join๋  ๊ฐ€๋Šฅ์„ฑ์ด ์—†์Œ!

2025-12-25T21:42:48.477+09:00  INFO 10018 --- [assignment] [nio-8080-exec-1] o.s.a.g.s.f.JwtAuthenticationFilter      : Authentication Successful: JwtAuthenticationToken [Principal=org.sopt.assignment.global.security.info.UserPrincipal@56a340a8, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER]]
Hibernate: 
    select
        count(*) 
    from
        articles a1_0 
    where
        a1_0.id=?
Hibernate: 
    select
        c1_0.id,
        c1_0.article_id,
        c1_0.content,
        c1_0.created_at,
        c1_0.is_update,
        c1_0.member_id,
        m1_0.id,
        m1_0.birthday,
        m1_0.created_at,
        m1_0.email,
        m1_0.gender,
        m1_0.modified_at,
        m1_0.name,
        m1_0.password,
        m1_0.role,
        c1_0.modified_at 
    from
        comments c1_0 
    join
        members m1_0 
            on m1_0.id=c1_0.member_id 
    where
        c1_0.article_id=? 
    limit
        ?

ํ›„์—, ๋กœ๊ทธ๋ฅผ ๋‹ค์‹œ๋ณด๊ฒŒ ๋˜๋ฉด ํ•˜๋‚˜์˜ select๋ฌธ์œผ๋กœ Comment์™€ Member๋ฅผ ์กฐํšŒ์ค‘์ธ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Œ

โ†’ ํ•˜์ง€๋งŒ ๋‚ด๊ฐ€ ํ•„์š”ํ•œ๊ฑด ๋ฉค๋ฒ„ ์ด๋ฆ„์ธ๋ฐ Member์˜ ๋ชจ๋“  ํ•„๋“œ๋ฅผ ์กฐํšŒํ•˜๋Š” ๋ชจ์Šตโ€ฆ

์ตœ์ ํ™”ํ•˜๊ธฐ - Projection๊ณผ ์ฟผ๋ฆฌ

Option1 - DTO ๋ฅผ ์ง์ ‘ ์กฐํšŒํ•˜๊ธฐ (JPQL Projection)

// CommentRepository
@Query("""
    SELECT new org.sopt.assignment.comment.dto.response.GetCommentResponseDto(
        c.id,
        c.content,
        c.member.id,
        c.createdAt
    )
    FROM Comment c
    WHERE c.article.id = :articleId
    """)
Page<GetCommentResponseDto> findCommentDtosByArticleId(
    @Param("articleId") Long articleId, 
    Pageable pageable
);
  • ์žฅ์ 

    • Member ํ…Œ์ด๋ธ” ๊ฑด๋“œ๋ฆฌ๊ธฐ X
    • ํ•„์š”ํ•œ ์ปฌ๋Ÿผ๋งŒ SELECT ๊ฐ€๋Šฅ
    • ํƒ€์ž… ์•ˆ์ „ํ•จ
  • ๋‹จ์ 

    • DTO ์ƒ์„ฑ์ž ํ•„์š”(Record๋ฉด ใ„ฑใ…Š!)
    • ํŒจํ‚ค์ง€ ๊ฒฝ๋กœ ํ•˜๋“œ์ฝ”๋”ฉ

Option2: Interface Projection

// DTO ๋Œ€์‹  interface
public interface CommentSummary {
    Long getId();
    String getContent();
    Long getMemberId();  // c.member.id
    LocalDateTime getCreatedAt();
}

// Repository
@Query("SELECT c.id as id, c.content as content, c.member.id as memberId, c.createdAt as createdAt " +
       "FROM Comment c WHERE c.article.id = :articleId")
Page<CommentSummary> findCommentSummariesByArticleId(@Param("articleId") Long articleId, Pageable pageable);
  • ์žฅ์ 

    • ํŒจํ‚ค์ง€ ๊ฒฝ๋กœ ํ•˜๋“œ์ฝ”๋”ฉ ์—†์Œ
    • DTO ํด๋ž˜์Šค ๋ถˆํ•„์š”
    • ๊ฐ„๋‹จํ•จ
  • ๋‹จ์ 

    • Interface๋ผ์„œ ๋ถˆํŽธํ•จ(์ƒ์„ฑ์ž, ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ ๋ถˆ๊ฐ€)
    • ํ…Œ์ŠคํŠธ ์–ด๋ ค์›€(Mock ํ•„์š”) โ† ์ƒ์„ฑ์ž๊ฐ€ ์—†์Œ
    • JSON ์ง๋ ฌํ™” ์‹œ Getter์˜์กด โ† ํ•„๋“œ๊ฐ€ ์—†์Œ
    • ๋ถˆ๋ณ€์„ฑ ๋ณด์žฅ ์•ˆ๋จ
  • ์“ฐ๋Š” ์‹œ๊ธฐ

    • ๊ฐ„๋‹จํ•œ ์กฐํšŒ์šฉ
    • Controller์—์„œ ๋ฐ”๋กœ ๋ฐ˜ํ™˜
    • ๋ณ€ํ™˜ ๋กœ์ง ๋ถˆํ•„์š”

Option3: ๋„ค์ดํ‹ฐ๋ธŒ ์ฟผ๋ฆฌ

@Query(value = """
    SELECT 
        c.id,
        c.content,
        c.member_id,
        c.created_at
    FROM comments c
    WHERE c.article_id = :articleId
    """, nativeQuery = true)
Page<Object[]> findCommentsByArticleIdNative(@Param("articleId") Long articleId, Pageable pageable);
  • ์žฅ์ 

    • DB ํŠนํ™” ๊ธฐ๋Šฅ ์‚ฌ์šฉ ๊ฐ€๋Šฅ(MySQL ํ•จ์ˆ˜ ๋“ฑ)
    • ๋ณต์žกํ•œ ์ฟผ๋ฆฌ ์ž‘์„ฑ ์‰ฌ์›€
    • ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ทน๋Œ€ํ™”(์ธ๋ฑ์Šค ํžŒํŠธ)
  • ๋‹จ์ 

    • DB ์ข…์†์ (MySQL โ†’ PostgreSQL ์ „ํ™˜ ์‹œ ์ˆ˜์ •)
    • ํƒ€์ž… ์•ˆ์ „์„ฑ ์—†์Œ(Object[] )
    • ์ˆ˜๋™ ๋งคํ•‘ ํ•„์š”
    • ์˜คํƒ€ ์ปดํŒŒ์ผ์‹œ ๋ชป์žก์Œ

EntityGraph๋Š” ํžŒํŠธ์ด๊ณ  Fetch Join์€ ๋ช…๋ น์ด๋‹ค

  • Fetch Join (INNER JOIN) - ๋ช…์‹œ์ ์œผ๋กœ ์กฐ์ธํ•˜๋ผ๊ณ  ํ•˜๋Š” ๊ฒƒ, JPQL ํ‘œ์ค€ ํ‚ค์›Œ๋“œ, ๋ช…์‹œ์  ํŽ˜์น˜
  • EntityGraph (LEFT JOIN) - Hint ์ œ๊ณต, ์„ ํƒ์  ์ตœ์ ํ™”, ๋А์Šจํ•œ ์ฒ˜๋ฆฌ

Command(๋ช…๋ น) vs Hint

  • ๋ช…๋ น

    • ๋ฐ˜๋“œ์‹œ ์ด๋ ‡๊ฒŒ ํ•ด์•ผํ•˜๋Š” ๊ฒƒ
    • JPA ๊ตฌํ˜„์ฒด๋Š” ๋ฌด์กฐ๊ฑด ๋”ฐ๋ฅด๊ฒŒ ๋จ
  • ํžŒํŠธ

    • ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ข‹์„๊ฒƒ ๊ฐ™์€๋ฐ๋ผ๋Š” ๋А๋‚Œ
    • JPA ๊ตฌํ˜„์ฒด๊ฐ€ ๋ฌด์‹œํ•  ์ˆ˜๋„ ์žˆ์Œ(๋Œ€๋ถ€๋ถ„ ์‹คํ–‰ํ•จ)

JPA Hint ์ข…๋ฅ˜

  • Query Hint
@QueryHints({
    @QueryHint(name = "org.hibernate.readOnly", value = "true"),
    @QueryHint(name = "org.hibernate.fetchSize", value = "50")
})
Page<Comment> findAll(Pageable pageable);
  • EntityGraph(ํŠน๋ณ„ํ•œ ํžŒํŠธ)
@EntityGraph(attributePaths = {"member", "article"})
Page<Comment> findAll(Pageable pageable);

// "member๋ž‘ article ๊ฐ™์ด ๊ฐ€์ ธ์˜ค๋ฉด ์ข‹๊ฒ ์–ด~"
  • Fetch Mode Hint
@Fetch(FetchMode.JOIN)
private Member member;

// "๊ฐ€๋Šฅํ•˜๋ฉด JOIN์œผ๋กœ ๊ฐ€์ ธ์™€~"

ํžŒํŠธ์˜ ํŠน์ง•

  • ์„ ํƒ์  ์ ์šฉ
// Hibernate๋Š” ์ด ํžŒํŠธ ์ ์šฉ
@QueryHint(name = "org.hibernate.readOnly", value = "true")

// ๋‹ค๋ฅธ JPA ๊ตฌํ˜„์ฒด(EclipseLink)๋Š” ๋ฌด์‹œํ•  ์ˆ˜ ์žˆ์Œ
  • ๋ณด์žฅ์•ˆ๋จ
@EntityGraph(attributePaths = {"member"})
// "member๋ฅผ ์กฐ์ธํ•ด์ค˜"๋ผ๊ณ  ์ œ์•ˆ
// Hibernate๊ฐ€ ์ƒํ™ฉ์— ๋”ฐ๋ผ ์•ˆ ํ•  ์ˆ˜๋„ ์žˆ์Œ (์‹ค์ œ๋ก  ๊ฑฐ์˜ ์ ์šฉ)
  • ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ชฉ์ 
// ๊ธฐ๋Šฅ ๋ณ€๊ฒฝ X, ์„ฑ๋Šฅ๋งŒ ๊ฐœ์„ 
@QueryHint(name = "org.hibernate.cacheable", value = "true")

Test Code ์ž‘์„ฑ

  • V1 โ†’ N+1 ๋ฌธ์ œ ํ•ด๊ฒฐ Fetch Join ์ ์šฉ
@Transactional(readOnly = true)
    public PageBaseDto<GetCommentResponseDto> getCommentsV1(Long articleId, Pageable pageable) {

        articleService.validateArticleExists(articleId);

        return PageBaseDto.from(commentRepository
                .findByArticleId(articleId, pageable).map(GetCommentResponseDto::from));
    }
  • V2 โ†’ DTO Projection
@Transactional(readOnly = true)
    public PageBaseDto<GetCommentResponseDto> getCommentsV2(Long articleId, Pageable pageable) {

        articleService.validateArticleExists(articleId);

        return PageBaseDto.from(commentRepository
                .findCommentDtoByArticleId(articleId, pageable)
                .map(GetCommentResponseDto::from));
    }
  • V3 โ†’ Interface Projection
@Transactional(readOnly = true)
    public PageBaseDto<GetCommentResponseDto> getCommentsV3(Long articleId, Pageable pageable) {

        articleService.validateArticleExists(articleId);

        return PageBaseDto.from(commentRepository
                .findCommentSummariesByArticleId(articleId, pageable)
                .map(GetCommentResponseDto::from));
    }
  • V4 โ†’ Native Query
@Transactional(readOnly = true)
    public PageBaseDto<GetCommentResponseDto> getCommentsV4(Long articleId, Pageable pageable) {

        articleService.validateArticleExists(articleId);

        return PageBaseDto.from(commentRepository
                .findCommentsByArticleIdNative(articleId, pageable)
                .map(GetCommentResponseDto::from));
    }
  • V5 โ†’ N+1 ๋ฌธ์ œ ํ•ด๊ฒฐ ์ „
@Transactional(readOnly = true)
    public PageBaseDto<GetCommentResponseDto> getCommentsV5(Long articleId, Pageable pageable) {

        articleService.validateArticleExists(articleId);

        return PageBaseDto.from(commentRepository
                .findCommentByArticleId(articleId, pageable).map(GetCommentResponseDto::from));
    }

ํ…Œ์ŠคํŠธ์ฝ”๋“œ

@Test
    @DisplayName("Fetch Join ๋งŒ")
    void V1_์„ฑ๋Šฅ_์ธก์ •() {
        Pageable pageable = PageRequest.of(0, 10);

        long startTime = System.currentTimeMillis();

        PageBaseDto<GetCommentResponseDto> result =
                commentService.getCommentsV1(1L, pageable);

        long endTime = System.currentTimeMillis();

        System.out.println("V1 ์‹คํ–‰ ์‹œ๊ฐ„: " + (endTime - startTime) + "ms");
    }

    @Test
    @DisplayName("DTO Projection")
    void V2_์„ฑ๋Šฅ_์ธก์ •() {
        Pageable pageable = PageRequest.of(0, 10);

        long startTime = System.currentTimeMillis();

        PageBaseDto<GetCommentResponseDto> result =
                commentService.getCommentsV2(1L, pageable);

        long endTime = System.currentTimeMillis();

        System.out.println("V2 ์‹คํ–‰ ์‹œ๊ฐ„: " + (endTime - startTime) + "ms");
    }

    @Test
    @DisplayName("Interface projection")
    void V3_์„ฑ๋Šฅ_์ธก์ •() {
        Pageable pageable = PageRequest.of(0, 10);

        long startTime = System.currentTimeMillis();

        PageBaseDto<GetCommentResponseDto> result =
                commentService.getCommentsV3(1L, pageable);

        long endTime = System.currentTimeMillis();

        System.out.println("V3 ์‹คํ–‰ ์‹œ๊ฐ„: " + (endTime - startTime) + "ms");
    }

    @Test
    @DisplayName("Native Query")
    void V4_์„ฑ๋Šฅ_์ธก์ •() {
        Pageable pageable = PageRequest.of(0, 10);

        long startTime = System.currentTimeMillis();

        PageBaseDto<GetCommentResponseDto> result =
                commentService.getCommentsV4(1L, pageable);

        long endTime = System.currentTimeMillis();

        System.out.println("V4 ์‹คํ–‰ ์‹œ๊ฐ„: " + (endTime - startTime) + "ms");
    }

    @Test
    @DisplayName("N+1 ๋ฌธ์ œ")
    void V5_์„ฑ๋Šฅ_์ธก์ •() {
        Pageable pageable = PageRequest.of(0, 10);

        long startTime = System.currentTimeMillis();

        PageBaseDto<GetCommentResponseDto> result =
                commentService.getCommentsV5(1L, pageable);

        long endTime = System.currentTimeMillis();

        System.out.println("V5 ์‹คํ–‰ ์‹œ๊ฐ„: " + (endTime - startTime) + "ms");
    }

    @Test
    void ์ „์ฒด_์„ฑ๋Šฅ_๋น„๊ต() {
        Pageable pageable = PageRequest.of(0, 10);

        long v1 = measure(() -> commentService.getCommentsV1(1L, pageable));
        long v2 = measure(() -> commentService.getCommentsV2(1L, pageable));
        long v3 = measure(() -> commentService.getCommentsV3(1L, pageable));
        long v4 = measure(() -> commentService.getCommentsV4(1L, pageable));
        long v5 = measure(() -> commentService.getCommentsV5(1L, pageable));

        System.out.println("\n========== ์„ฑ๋Šฅ ๋น„๊ต ==========");
        System.out.println("V1: " + v1 + "ms");
        System.out.println("V2: " + v2 + "ms");
        System.out.println("V3: " + v3 + "ms");
        System.out.println("V4: " + v4 + "ms");
        System.out.println("V5: " + v5 + "ms");
        System.out.println("==============================\n");
    }

์ฒซ๋ฒˆ์งธ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ

  • V1 โ†’ N+1๋งŒ ํ•ด๊ฒฐํ•œ๊ฒฝ์šฐ

    V1 ์‹คํ–‰ ์‹œ๊ฐ„: 36ms

  • V2 โ†’ DTO Projection

    V2 ์‹คํ–‰ ์‹œ๊ฐ„: 31ms

  • V3 โ†’ Interface Projection

    V3 ์‹คํ–‰ ์‹œ๊ฐ„: 29ms

  • V4 โ†’ Native Query

    V4 ์‹คํ–‰์‹œ๊ฐ„: 32ms

  • V5 โ†’ N+1 ํ•ด๊ฒฐ X

    V5 ์‹คํ–‰์‹œ๊ฐ„: 38ms

์ƒ๊ฐ๋ณด๋‹ค ์„ฑ๋Šฅ์ฐจ์ด๊ฐ€ ์•ˆ๋‚˜์„œ ๋ฉค๋ฒ„ 1000๊ฐœ ๋Œ“๊ธ€ 10000๊ฐœ๋กœ ๊ฐ๊ฐ 10๊ฐœ์”ฉ ๋Œ“๊ธ€ ์ž‘์„ฑํ•ด์„œ ์กฐํšŒํ•˜๋Š” ์‹ฑํ™ฉ๊ฐ€์ •

๋‘๋ฒˆ์งธ ํ…Œ์ŠคํŠธ

  • V1 โ†’ N+1 Fetch Join ํ•ด๊ฒฐ - 37ms

  • V2 โ†’ Dto Projection - 35ms

  • V3 โ†’ Interface Projection - 29ms

  • V4 โ†’ 37 ms

  • V5 โ†’ 76ms

โ†’ ์—ฌ๋Ÿฌ๋ฒˆ ์‹คํ–‰ํ•ด๋ณธ ๊ฒฐ๊ณผ N+1 ํ•ด๊ฒฐ ์•ˆํ–ˆ์„ ๊ฒฝ์šฐ๊ฐ€ ํ™•์‹คํžˆ ๋А๋ฆฌ๊ณ  Interface Projection์ด ํ™•์‹คํžˆ ๋น ๋ฅด๊ณ  ๋‚˜๋จธ์ง€๋Š” ๋น„์Šท๋น„์Šท

  • DTO Projection์€ ๊ฐ์ฒด ์ƒ์„ฑ ๋น„์šฉ ์กด์žฌ

    1. ์ฟผ๋ฆฌ ์‹คํ–‰
    2. ๊ฐ Row ๋งˆ๋‹ค CommentQueryDto ์ƒ์„ฑ โ†’ ์ƒ์„ฑ์ž ํ˜ธ์ถœ
    3. ResponseDto ๋กœ ๋ณ€ํ™˜
  • Native Query

    Objectp[] -> DTO ๋ณ€ํ™˜ ์ˆ˜๋™ , ํƒ€์ž…์บ์ŠคํŒ… ๊ณผ์ •

  • Interface

    1. ์ฟผ๋ฆฌ ์‹คํ–‰
    2. ํ”„๋ก์‹œ ๊ฐ์ฒด ์ƒ์„ฑ(๊ฐ€๋ฒผ์›€)
    3. ResponseDto ๋ณ€ํ™˜ ์‹œ์—๋งŒ ์‹ค์ œ ์ ‘๊ทผ

Test Code ์‹คํ–‰์‹œ Interface Projection ์˜ค๋ฅ˜

1๋ฒˆ์งธ ์˜ค๋ฅ˜

Null return value from advice does not match primitive return type for: public abstract boolean org.sopt.assignment.comment.repository.CommentSummary.isUpdate()
org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type for: public abstract boolean org.sopt.assignment.comment.repository.CommentSummary.isUpdate()
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:237)
    at jdk.proxy3/jdk.proxy3.$Proxy213.isUpdate(Unknown Source)
    at org.sopt.assignment.comment.dto.response.GetCommentResponseDto.from(GetCommentResponseDto.java:47)
    at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
    at java.base/java.util.ArrayList$Itr.forEachRemaining(ArrayList.java:1085)
    at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1939)
    at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
    at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
    at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
    at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
    at org.springframework.data.domain.Chunk.getConvertedContent(Chunk.java:131)
    at org.springframework.data.domain.PageImpl.map(PageImpl.java:86)
    at org.sopt.assignment.comment.service.CommentService.getCommentsV3(CommentService.java:65)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:360)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:380)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728)
    at org.sopt.assignment.comment.service.CommentService$$SpringCGLIB$$0.getCommentsV3(<generated>)
    at org.sopt.assignment.comment.CommentPerformanceTest.V3_์„ฑ๋Šฅ_์ธก์ •(CommentPerformanceTest.java:93)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
  • JPA Interface Projection์€ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ๊ทธ๋Œ€๋กœ ๋งคํ•‘ํ•จ

    • ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋กœ๋”ฉํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹Œ
    • SELECT ์ ˆ์— ํฌํ•จ๋œ ์ปฌ๋Ÿผ ๊ฐ’์œผ๋กœ ํ”„๋ก์‹œ๋ฅผ ๋งŒ๋“ค์–ด์„œ getter์— ๋งคํ•‘์‹œ์ผœ์คŒ

    โ†’ ๊ทผ๋ฐ isUpdate ์นผ๋Ÿผ ๊ฐ’์ด null๋กœ ๋„˜์–ด์˜ด

  • ์—”ํ‹ฐํ‹ฐ์—์„  boolean๊ฐ’์˜ ๊ธฐ๋ณธ๊ฐ’์„ false๋กœ ํ•ด์คฌ์Œ

    โ†’ ๊ทธ๋ ‡๋‹ค๋ฉด boolean ๊ฐ’์— ์‹ค์ œ null์ด ๋“ค์–ด๊ฐ€์„œ ์ผ์–ด๋‚˜๋Š” ๋ฌธ์ œ๊ฐ€ ์•„๋‹ˆ๋ผ ๋งคํ•‘์„ ์ž˜๋ชป ์‹œํ‚ค๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ

  • JavaBeans getter ๊ทœ์น™์„ ๋ณด๊ฒŒ๋˜๋ฉด

  • isUpdate ์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋‚œ์ด์œ ?

    ํ•„๋“œ๋ช… = isUpdate

    property name = isUpdate

    getter = getIsUpdate

public interface CommentSummary {
    Long getId();
    String getContent();
    String getMemberName();
    LocalDateTime getCreatedAt();
    boolean getIsUpdate();
}

โ†’ getIsUpdate ๋กœ ๋ฐ”๊พธ๋ฉด ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋จ

  • Projection vs ์—”ํ‹ฐํ‹ฐ/POJO
    • boolean์ผ ๊ฒฝ์šฐ getter๋Š” isXXX๋Š” ์—”ํ‹ฐํ‹ฐ/POJO์—์„  ๋งž๋Š” ๋ง
    • ํ•˜์ง€๋งŒ Interface Projection์—์„  ๋ฉ”์„œ๋“œ ์ด๋ฆ„ ๊ธฐ๋ฐ˜์ด๊ธฐ ๋•Œ๋ฌธ์— getIsUpdate๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•จ

#์ธ๋ฑ์Šค

๊ธฐ์กด

@Table(name = "articles", indexes = {
        @Index(name = "idx_member_id", columnList = "member_id"),
        @Index(name = "idx_title", columnList = "title"),
        @Index(name = "idx_created_at", columnList = "created_at")
})

idx_title

  • ์ œ๋ชฉ ์™„์ „ ์ผ์น˜ ๊ฒ€์ƒ‰์—๋งŒ ํšจ๊ณผ์ 
  • ์ œ๋ชฉ ๋ถ€๋ถ„๊ฒ€์ƒ‰ LIKE '%keyword%'์—๋Š” ์ธ๋ฑ์Šค ์•ˆํƒ
  • title์— ์ด๋ฏธ UNIQUE์ œ์•ฝ์กฐ๊ฑด์ด ์žˆ์–ด์„œ ์ž๋™์œผ๋กœ ์ธ๋ฑ์Šค ์ƒ์„ฑ

idx_created_at

  • ๋‹จ์ผ ์ธ๋ฑ์Šค๋กœ๋Š” ๋น„ํšจ์œจ์ ์ž„
  • ์‹ค์ œ ์ฟผ๋ฆฌ๋Š” ๋ณดํ†ต WHERE member_id = ? ORDER BY created_at DESC
  • ๋‹จ์ผ ์นผ๋Ÿผ ์ธ๋ฑ์Šค๋Š” ์ •๋ ฌ์—๋งŒ ๋„์™€์คŒ
  • WHERE + ORDER BY์ปค๋ฒ„ ์‹คํŒจ
@Table(name = "articles", indexes = {
    // 1. ๋ณตํ•ฉ ์ธ๋ฑ์Šค: ํšŒ์›๋ณ„ ์ตœ์‹  ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ
    @Index(name = "idx_member_created", columnList = "member_id, created_at"),
    
    // 2. ํƒœ๊ทธ๋ณ„ ์ตœ์‹  ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ (ํ•„์š”์‹œ)
    @Index(name = "idx_tag_created", columnList = "tag, created_at")
})

idx_member_created

  • WHERE ์กฐ๊ฑด๊ณผ ORDER BY๋ฅผ ํ•˜๋‚˜์˜ ์ธ๋ฑ์Šค๋กœ ์ฒ˜๋ฆฌ
  • ํŽ˜์ด์ง• ์ฟผ๋ฆฌ์— ์ตœ์ 

idx_tag_created

  • TAG๊ฐ€ ์žˆ๋Š” ์ด์œ ๊ฐ€ TAG ๊ฒ€์ƒ‰์ด ์žˆ์„๋•Œ๋ผ๊ณ  ์ƒ๊ฐํ•˜๊ธฐ ๋•Œ๋ฌธ์—
  • ๊ทธ๋ฅผ ๋Œ€๋น„ํ•˜์—ฌ created ์™€ TAG๋ฅผ ๋ณตํ•ฉ์ธ๋ฑ์Šค

-> ์ด๋ ‡๊ฒŒ ์ธ๋ฑ์Šค๋ฅผ ๋ฐ”๊พธ๋ฉด ๊ฒ€์ƒ‰ ์กฐ๊ฑด์ด๋‚˜ ์ •๋ ฌ์กฐ๊ฑด์„ ์ถ”๊ฐ€ํ•ด์•ผํ•จ

#์ธํ”„๋ผ๊ตฌ์กฐ๋„

ECS๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค
ECS์˜ ํŠน์ง•์œผ๋กœ๋Š” ์ž๋™ ์žฅ์•  ๋ณต๊ตฌ, ๋ฌด์ค‘๋‹จ ๋ฐฐํฌ, ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋กœ๊น… ํ†ตํ•ฉ, ํ™•์žฅ์„ฑ์—์„œ ์œ ๋ฆฌํ•˜๊ณ 
์šด์˜ ์•ˆ์ •์„ฑ๊ณผ ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ, ํ™•์žฅ์„ฑ ์ค€๋น„์—์„œ ์ด์ ์„ ์–ป์–ด ๊ฐˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
EC2 t3.small ๊ธฐ์ค€ 17๋‹ฌ๋Ÿฌ๋งŒ ๋‚ด๊ฒŒ ๋œ๋‹ค๋ฉด Task ์ •์˜๋งŒ ์ž˜ํ•œ๋‹ค๋ฉด ๊ทธ๋ƒฅ EC2 t3.small์„ ์“ฐ๋Š” ๊ฒƒ๋ณด๋‹ค ECS ๊ด€๋ฆฌํ•ด์ฃผ๋Š” t3.small์„ ์‚ฌ์šฉํ•˜๋Š”๊ฒŒ ์œ ๋ฆฌํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค

ECS๋Š” ์šด์˜ํƒ€์ž…์ด Fargate์™€ EC2๊ฐ€ ์žˆ๋Š”๋ฐ, Fargate๋Š” ์„œ๋ฒ„๋ฆฌ์Šค๋กœ EC2๋ฅผ ์ง์ ‘ ๊ด€๋ฆฌํ•˜์ง€ ์•Š์•„๋„ ๋˜๋Š” ๊ฒƒ์ด๊ณ  EC2๋Š” ECS์—์„œ ์ž๋™ ์ƒ์„ฑํ•ด์ฃผ๋Š” ํ”ํžˆ ์•„๋Š” EC2 ํƒ€์ž…์œผ๋กœ ์•Œ๊ณ  ์žˆ๋Š”๋ฐ, ์‚ฌ์šฉ ๊ฒฝํ—˜์ด EC2 ๋ฐ–์— ์—†์–ด์„œ ๊ตฌ์กฐ๋„๋ฅผ ์ €๋ ‡๊ฒŒ ๊ทธ๋ ค๋ดค์Šต๋‹ˆ๋‹ค

ECS์˜ ๊ตฌ์„ฑ์š”์†Œ

1. ํด๋Ÿฌ์Šคํ„ฐ Cluster

์—ญํ• : ์ปจํ…Œ์ด๋„ˆ๋“ค์ด ์‹คํ–‰๋  ์ปดํ“จํŒ… ๋ฆฌ์†Œ์Šค์˜ ๋…ผ๋ฆฌ์  ๊ทธ๋ฃน
์˜ˆ์‹œ: "์›น์„œ๋ฒ„ ํด๋Ÿฌ์Šคํ„ฐ", "๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํด๋Ÿฌ์Šคํ„ฐ"

๊ตฌ์„ฑ ์š”์†Œ:
- EC2 ์ธ์Šคํ„ด์Šค๋“ค์˜ ๊ทธ๋ฃน (EC2 Launch Type)
- ๋˜๋Š” Fargate์˜ ์„œ๋ฒ„๋ฆฌ์Šค ์ปดํ“จํŒ… (Fargate Launch Type)

2. Task Definition (์ž‘์—… ์ •์˜)

์—ญํ• : ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰์„ ์œ„ํ•œ ์ฒญ์‚ฌ์ง„ (Blueprint)
Docker Compose์˜ yml ํŒŒ์ผ๊ณผ ์œ ์‚ฌํ•œ ์—ญํ• 

ํฌํ•จ ๋‚ด์šฉ:
- ์‚ฌ์šฉํ•  Docker ์ด๋ฏธ์ง€
- CPU/๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น๋Ÿ‰
- ๋„คํŠธ์›Œํ‚น ์„ค์ •
- ํ™˜๊ฒฝ ๋ณ€์ˆ˜
- ๋ณผ๋ฅจ ๋งˆ์šดํŠธ

3. ์ž‘์—…(Task)

์—ญํ• : Task Definition์— ๊ธฐ๋ฐ˜ํ•ด ์‹ค์ œ ์‹คํ–‰๋˜๊ณ  ์žˆ๋Š” ์ปจํ…Œ์ด๋„ˆ ์ธ์Šคํ„ด์Šค
๋น„์œ : ๋ ˆ์‹œํ”ผ(Task Definition)๋กœ ๋งŒ๋“  ์‹ค์ œ ์š”๋ฆฌ(Task)

ํŠน์ง•:
- ํ•˜๋‚˜์˜ Task = ํ•˜๋‚˜ ์ด์ƒ์˜ ์ปจํ…Œ์ด๋„ˆ
- ๊ฐ™์€ Task ์•ˆ์˜ ์ปจํ…Œ์ด๋„ˆ๋“ค์€ ๊ฐ™์€ ์„œ๋ฒ„์—์„œ ์‹คํ–‰
- localhost๋กœ ์„œ๋กœ ํ†ต์‹  ๊ฐ€๋Šฅ

4. Service(์„œ๋น„์Šค)

์—ญํ• : Task๋“ค์„ ์ง€์†์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ์›ํ•˜๋Š” ์ƒํƒœ๋ฅผ ์œ ์ง€

์ฃผ์š” ๊ธฐ๋Šฅ:
- ์›ํ•˜๋Š” ๊ฐœ์ˆ˜์˜ Task ์œ ์ง€ (์˜ˆ: ํ•ญ์ƒ 3๊ฐœ ์‹คํ–‰)
- Task๊ฐ€ ์ฃฝ์œผ๋ฉด ์ž๋™์œผ๋กœ ์ƒˆ Task ์‹œ์ž‘
- ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ์™€ ์—ฐ๊ฒฐ
- Rolling ์—…๋ฐ์ดํŠธ ์ˆ˜ํ–‰
- Auto Scaling ์ง€์›

์ธํ”„๋ผ๊ตฌ์กฐ๋„

Public Subnet์— ์™ธ๋ถ€์™€์˜ ํ†ต์‹ ์„ ์œ„ํ•œ NAT Gateway๋ฅผ ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค (ํ”„๋ผ์ด๋น— ์„œ๋ธŒ๋„ท -> NAT Gateway -> ์ธํ„ฐ๋„ท)
์ธ๋ฐ”์šด๋“œ ํŠธ๋ž˜ํ”ฝ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ALB๋„ public subnet์— ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค(์ธํ„ฐ๋„ท -> ALB -> ํ”„๋ผ์ด๋น— ์„œ๋ธŒ๋„ท์˜ ECS

Private Subnet์•ˆ์—๋Š” ECS ํด๋Ÿฌ์Šคํ„ฐ๋ฅผ ๋‘๊ณ  ๊ฐ€์šฉ์„ฑ ๋†’๊ฒŒํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ๊ฐ์˜ ๊ฐ€์šฉ์˜์—ญ์— EC2๋ฅผ 1๊ฐœ์”ฉ ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค EC2์•ˆ์—๋Š” ์Šคํ”„๋ง ๋ถ€ํŠธ ์ปจํ…Œ์ด๋„ˆ, Redis ์ปจํ…Œ์ด๋„ˆ, Nginx ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๊ฐ๊ฐ ํƒœ์Šคํฌ ์ •์˜๋ฅผ ํ†ตํ•ด ๋ฐฐ์น˜ํ•˜๊ณ  ํ”„๋ผ์ด๋น— ์„œ๋ธŒ๋„ท ๊ทธ๋ฃน์— RDS๋ฅผ ๋ฐฐ์น˜ํ•ด์ค๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  CI/CD ๋ฐ ์ด๋ฏธ์ง€ ์ €์žฅ์†Œ๋ฅผ ์œ„ํ•œ ECR์„ VPC ์™ธ๋ถ€์— ๋ฐฐ์น˜ํ•ด์ค๋‹ˆ๋‹ค

ECS๋Š” ํŠน์ดํ•˜๊ฒŒ ENI์™€ PAUSE ์ปจํ…Œ์ด๋„ˆ๋กœ ๋„คํŠธ์›Œํฌ ๋ฐ ํ†ต์‹ ์„ ๊ด€๋ฆฌํ•˜๋Š”๋ฐ ์ด ๋‚ด์šฉ์€ ๋„ˆ๋ฌด ๊ธธ์–ด์งˆ๊ฒƒ ๊ฐ™์•„์„œ ์ƒ๋žตํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค!



๐Ÿšจ ์ฐธ๊ณ  ์‚ฌํ•ญ

์›น์†Œ์ผ“๊ด€๋ จํ•ด์„œ๋Š” ์กฐ๊ธˆ์ด๋”ฐ๊ฐ€ ์ •๋ฆฌํ•ด์„œ์˜ฌ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค... ์•„์ง ์ •๋ฆฌ๊ฐ€ ๋œ๋œ๋ถ€๋ถ„์ด ์žˆ์–ด๊ฐ€์ง€๊ตฌ...

@oOccasio oOccasio self-assigned this Dec 26, 2025
@oOccasio oOccasio added the โœจ assignment New Assignment label Dec 26, 2025
@oOccasio oOccasio linked an issue Dec 26, 2025 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

โœจ assignment New Assignment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[SEMINAR] 7์ฃผ์ฐจ ์„ธ๋ฏธ๋‚˜

2 participants