<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>miso_.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Thu, 25 Apr 2024 00:48:53 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>miso_.log</title>
            <url>https://velog.velcdn.com/images/miso_/profile/ce52da0d-cd73-446a-9c63-da4de2815963/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. miso_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/miso_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[찌리릿 [Zziririt] 테스트 코드]]></title>
            <link>https://velog.io/@miso_/%EC%B0%8C%EB%A6%AC%EB%A6%BF-Zziririt-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C</link>
            <guid>https://velog.io/@miso_/%EC%B0%8C%EB%A6%AC%EB%A6%BF-Zziririt-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C</guid>
            <pubDate>Thu, 25 Apr 2024 00:48:53 GMT</pubDate>
            <description><![CDATA[<h2 id="repository-layer-단위-테스트">Repository Layer 단위 테스트</h2>
<p>직접 작성한 쿼리문을 테스트 하기 위해서 리포지토리 테스트를 작성했다.</p>
<pre><code class="language-kotlin">@Repository
class BoardQueryDslRepositoryImpl : QueryDslSupport(), BoardQueryDslRepository {
    private val board = QBoardEntity.boardEntity
    private val post = QPostEntity.postEntity
    private val category = QCategoryEntity.categoryEntity

    override fun findBoards(): List&lt;BoardRowDto&gt; {
        return queryFactory
            .select(
                QBoardRowDto(
                    board.id,
                    board.boardName
                )
            )
            .from(board)
            .orderBy(board.id.asc())
            .fetch()
    }


    override fun findStreamers(): List&lt;StreamerBoardRowDto&gt; {
        return queryFactory
            .select(
                QStreamerBoardRowDto(
                    board.id,
                    board.boardUrl,
                    board.boardName,
                )
            )
            .from(board)
            .where(board.boardType.eq(BoardType.STREAMER_BOARD))
            .orderBy(board.id.asc())
            .fetch()
    }

    override fun findBoardStatusToInactive(): List&lt;Long&gt; {
        val checkInactiveDate = LocalDateTime.now().minusDays(8)

        return queryFactory.select(board.id).distinct()
            .from(post)
            .leftJoin(board)
            .on(board.id.eq(post.board.id))
            .where(post.modifiedAt.loe(checkInactiveDate), board.boardType.eq(BoardType.STREAMER_BOARD))
            .fetch()
    }

    override fun updateBoardStatusToInactive(inactiveBoardIdList: List&lt;Long&gt;) {
        queryFactory
            .update(board)
            .set(board.boardActStatus, BoardActStatus.INACTIVE)
            .where(board.id.`in`(inactiveBoardIdList))
            .execute()
    }

    override fun findActiveStatusBoards(): List&lt;BoardRowDto&gt; {
        return queryFactory
            .select(
                QBoardRowDto(
                    board.id,
                    board.boardName
                )
            ).from(board)
            .where(board.boardActStatus.eq(BoardActStatus.ACTIVE))
            .orderBy(board.id.asc())
            .fetch()
    }
}</code></pre>
<p>먼저 리포지토리 테스트를 위한 데이터베이스를 설정했다.</p>
<pre><code class="language-kotlin">spring:
  config:
    activate:
      on-profile: test
  datasource:
    url: jdbc:h2:mem:test;MODE=MySQL;
    driver-class-name: org.h2.Driver
    username: sa
    password:
  jpa:
    show-sql: true
    hibernate:
      ddl-auto:  create-drop
    properties:
      hibernate:
        format-sql: true
        highlight_sql: true
        use_sql_comments: true
        default_batch_fetch_size: 1000
    open-in-view: false  </code></pre>
<p>보통 GIVEN 절에서 데이터를 설정할 때 모든 메서드에서 공동으로 사용하는 데이터가 있을 경우 BeforeEach를 활용해 중복을 제거할 수 있다. 하지만 현 상홯에서는 부적합하다는 것을 확인해 각 메서드마다 GIVEN 절에서 객체들을 따로 생성했다.</p>
<pre><code class="language-kotlin">@DataJpaTest
@ActiveProfiles(&quot;test&quot;)
@Import(value = [QueryDslConfig::class, BaseTimeEntityConfig::class, BaseEntityConfig::class])
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class BoardQueryDslRepositoryImplTest @Autowired constructor(
    private val boardRepository: CustomBoardRepository,
    private val testEntityManager: TestEntityManager
) {
    @Test
    fun sampleTest() {
        boardRepository.save(
            BoardEntity(
                socialMember = SocialMemberEntity(
                    email = &quot;test@gmail.com&quot;,
                    nickname = &quot;test&quot;,
                    provider = OAuth2Provider.TEST,
                    providerId = &quot;providerId&quot;,
                    memberRole = MemberRole.ADMIN
                ),
                boardName = &quot;test&quot;,
                boardUrl = &quot;boardUrl&quot;,
                boardType = BoardType.ZZIRIRIT_BOARD
            )
        )
        testEntityManager.flush()
    }

    @Test
    fun `게시판 전체가 조회되는지 확인`() {
        // GIVEN
        val socialMember = SocialMemberEntity(
            email = &quot;test@gmail1.com&quot;,
            nickname = &quot;test1&quot;,
            provider = OAuth2Provider.TEST,
            providerId = &quot;providerId&quot;,
            memberRole = MemberRole.ADMIN
        )

        val DEFAULT_BOARD_LIST = listOf(
            BoardEntity(
                socialMember = socialMember,
                boardName = &quot;test1&quot;,
                boardUrl = &quot;boardUrl1&quot;,
                boardType = BoardType.ZZIRIRIT_BOARD,
                boardActStatus = BoardActStatus.INACTIVE
            ),
            BoardEntity(
                socialMember = socialMember,
                boardName = &quot;test2&quot;,
                boardUrl = &quot;boardUrl2&quot;,
                boardType = BoardType.ZZIRIRIT_BOARD,
                boardActStatus = BoardActStatus.ACTIVE
            ),
            BoardEntity(
                socialMember = socialMember,
                boardName = &quot;test3&quot;,
                boardUrl = &quot;boardUrl3&quot;,
                boardType = BoardType.ZZIRIRIT_BOARD,
                boardActStatus = BoardActStatus.ACTIVE
            ),
            BoardEntity(
                socialMember = socialMember,
                boardName = &quot;test4&quot;,
                boardUrl = &quot;boardUrl4&quot;,
                boardType = BoardType.ZZIRIRIT_BOARD,
                boardActStatus = BoardActStatus.ACTIVE
            ),
            BoardEntity(
                socialMember = socialMember,
                boardName = &quot;test5&quot;,
                boardUrl = &quot;boardUrl5&quot;,
                boardType = BoardType.ZZIRIRIT_BOARD,
                boardActStatus = BoardActStatus.ACTIVE
            ),
            BoardEntity(
                socialMember = socialMember,
                boardName = &quot;test6&quot;,
                boardUrl = &quot;boardUrl6&quot;,
                boardType = BoardType.STREAMER_BOARD,
                boardActStatus = BoardActStatus.INACTIVE
            ),
            BoardEntity(
                socialMember = socialMember,
                boardName = &quot;test7&quot;,
                boardUrl = &quot;boardUrl7&quot;,
                boardType = BoardType.STREAMER_BOARD,
                boardActStatus = BoardActStatus.ACTIVE
            ),
            BoardEntity(
                socialMember = socialMember,
                boardName = &quot;test8&quot;,
                boardUrl = &quot;boardUrl8&quot;,
                boardType = BoardType.STREAMER_BOARD,
                boardActStatus = BoardActStatus.ACTIVE
            ),
            BoardEntity(
                socialMember = socialMember,
                boardName = &quot;test9&quot;,
                boardUrl = &quot;boardUrl9&quot;,
                boardType = BoardType.STREAMER_BOARD,
                boardActStatus = BoardActStatus.ACTIVE
            ),
            BoardEntity(
                socialMember = socialMember,
                boardName = &quot;test10&quot;,
                boardUrl = &quot;boardUrl10&quot;,
                boardType = BoardType.STREAMER_BOARD,
                boardActStatus = BoardActStatus.ACTIVE
            )
        )
        boardRepository.saveAllAndFlush(DEFAULT_BOARD_LIST)

        // WHEN
        val result = boardRepository.findBoards()

        // THEN
        result.size shouldBe 10
        result[0].boardName shouldBe &quot;test1&quot;
        result[9].boardName shouldBe &quot;test10&quot;
    }

    @Test
    fun `스트리머 게시판 전체가 조회되는지 확인`() {
        // GIVEN
        val socialMember = SocialMemberEntity(
            email = &quot;test@gmail1.com&quot;,
            nickname = &quot;test1&quot;,
            provider = OAuth2Provider.TEST,
            providerId = &quot;providerId&quot;,
            memberRole = MemberRole.ADMIN
        )

        val DEFAULT_BOARD_LIST = listOf(
            BoardEntity(
                socialMember = socialMember,
                boardName = &quot;test1&quot;,
                boardUrl = &quot;boardUrl1&quot;,
                boardType = BoardType.ZZIRIRIT_BOARD,
                boardActStatus = BoardActStatus.INACTIVE
            ),
            ...
        )
        boardRepository.saveAllAndFlush(DEFAULT_BOARD_LIST)

        // WHEN
        val result = boardRepository.findStreamers()

        // THEN
        result.size shouldBe 5
        result[4].streamerNickname shouldBe &quot;test10&quot;    // 스트리머의 닉네임이 게시판의 이름이도록 정책 세움
    }

    @Test
    fun `비활성화 상태로 변경시킬 게시판의 아이디들을 조회하는지 확인`() {
        // GIVEN &amp; WHEN
        val socialMember = SocialMemberEntity(
            email = &quot;test@gmail1.com&quot;,
            nickname = &quot;test1&quot;,
            provider = OAuth2Provider.TEST,
            providerId = &quot;providerId&quot;,
            memberRole = MemberRole.ADMIN
        )

        val DEFAULT_BOARD_LIST = listOf(
            BoardEntity(
                socialMember = socialMember,
                boardName = &quot;test1&quot;,
                boardUrl = &quot;boardUrl1&quot;,
                boardType = BoardType.ZZIRIRIT_BOARD,
                boardActStatus = BoardActStatus.INACTIVE
            ),
            ...
        )
        boardRepository.saveAllAndFlush(DEFAULT_BOARD_LIST)
        val result = boardRepository.findBoardStatusToInactive()

        // THEN
        result.size shouldNotBe null    // 비활성화 상태로 변경시킬 게시판의 아이디를 찾지 못하면 0이 size 가 0이라도 반환되기 때문에 null 이 아니면 메서드 동작 확인 성공.
    }

    @Test
    fun `게시판이 비활성화 상태로 업데이트 되는지 확인`() {
        // GIVEN
        val socialMember = SocialMemberEntity(
            email = &quot;test@gmail1.com&quot;,
            nickname = &quot;test1&quot;,
            provider = OAuth2Provider.TEST,
            providerId = &quot;providerId&quot;,
            memberRole = MemberRole.ADMIN
        )

        val DEFAULT_BOARD_LIST = listOf(
            BoardEntity(
                socialMember = socialMember,
                boardName = &quot;test1&quot;,
                boardUrl = &quot;boardUrl1&quot;,
                boardType = BoardType.ZZIRIRIT_BOARD,
                boardActStatus = BoardActStatus.INACTIVE
            ),
            ...
        )
        val boards = boardRepository.saveAllAndFlush(DEFAULT_BOARD_LIST)

        // WHEN
        val boardIds = boards.map { it.id!! }

        // THEN
        boardRepository.updateBoardStatusToInactive(boardIds)   // 반환 타입이 unit 이라서 행위를 테스트

    }

    @Test
    fun `활성화 상태의 게시판이 조회되는지 확인`() {
        // GIVEN
        val socialMember = SocialMemberEntity(
            email = &quot;test@gmail1.com&quot;,
            nickname = &quot;test1&quot;,
            provider = OAuth2Provider.TEST,
            providerId = &quot;providerId&quot;,
            memberRole = MemberRole.ADMIN
        )

        val DEFAULT_BOARD_LIST = listOf(
            BoardEntity(
                socialMember = socialMember,
                boardName = &quot;test1&quot;,
                boardUrl = &quot;boardUrl1&quot;,
                boardType = BoardType.ZZIRIRIT_BOARD,
                boardActStatus = BoardActStatus.INACTIVE
            ),
            ...
        )
        boardRepository.saveAllAndFlush(DEFAULT_BOARD_LIST)

        // WHEN
        val result = boardRepository.findActiveStatusBoards()

        // THEN
        result.size shouldBe 8
    }
}</code></pre>
<hr>
<h3 id="repository-layer-test-code-트러블-슈팅">Repository Layer Test Code 트러블 슈팅</h3>
<p>이전에 작성했던 코드에서 companion object 를 이용해 테스트 메서드 내에서 동일한 데이터를 모두 사용할 수 있게 작성했는데 오류를 만나게 됐다.</p>
<pre><code class="language-kotlin">@Test
fun `게시판 전체가 조회되는지 확인`() {
    // GIVEN
    boardRepository.saveAllAndFlush(DEFAULT_BOARD_LIST)

    // WHEN
    val result = boardRepository.findBoards()

    // THEN
    result.size shouldBe 10
    result[0].boardName shouldBe &quot;test1&quot;
    result[9].boardName shouldBe &quot;test10&quot;
}
...
 companion object {
        companion object {
        val socialMember = SocialMemberEntity(
            email = &quot;test@gmail1.com&quot;,
            nickname = &quot;test1&quot;,
            provider = OAuth2Provider.TEST,
            providerId = &quot;providerId&quot;,
            memberRole = MemberRole.ADMIN
        )

        val DEFAULT_BOARD_LIST = listOf(
            BoardEntity(
                socialMember = socialMember,
                boardName = &quot;test1&quot;,
                boardUrl = &quot;boardUrl1&quot;,
                boardType = BoardType.ZZIRIRIT_BOARD,
                boardActStatus = BoardActStatus.INACTIVE
            ),
            ...</code></pre>
<p><img src="https://velog.velcdn.com/images/miso_/post/25ee5c77-d2c0-42d8-8b26-2d34e4301585/image.png" alt=""></p>
<blockquote>
<p>org.springframework.dao.DataIntegrityViolationException: could not execute statement [Referential integrity constraint violation: &quot;FK3N752SW9EC3V9S2V4MMJ3710N: PUBLIC.BOARD FOREIGN KEY(SOCIAL_MEMBER_ID) REFERENCES PUBLIC.SOCIAL_MEMBER(ID) (CAST(1 AS BIGINT))&quot;; SQL statement:</p>
</blockquote>
<p>에러 메시지를 확인해보니 companion object 에서 만든 Board 객체 내 SocialMember 객체를 @Test 메서드 실행 시 Board 안에서 찾을 수 없기 때문이었다.</p>
<p>Board 테이블이 참조하는 SocialMember 의 컬럼 값과 참조되는 SocialMember 테이블의 컬럼 값이 동일하지 않아 발생한 문제였다.</p>
<p>참조 무결성 제약조건이 위배된 상황을 확인하고 난 후 테스트 메서드 내에서 모두 같은 데이터를 쓰지만 데이터를 격리해 픽스쳐를 공유하지 않기로 결정했다.</p>
<p>생성되는 데이터를 메서드 내에서 바로 확인할 수 있다는 이점도 생겼다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[찌리릿[Zziririt] 프로젝트 회고]]></title>
            <link>https://velog.io/@miso_/%EC%B0%8C%EB%A6%AC%EB%A6%BFZziririt-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@miso_/%EC%B0%8C%EB%A6%AC%EB%A6%BFZziririt-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Wed, 17 Apr 2024 00:36:37 GMT</pubDate>
            <description><![CDATA[<h3 id="keep지속할-것">KEEP(지속할 것)</h3>
<ol>
<li><a href="https://docs.google.com/spreadsheets/d/1R33YPytL6RtWXk16qvQTP4UOgSotSVUseI0vb9x34jU/edit#gid=1709744959">프로젝트 타임라인 설정</a> -&gt; 기획 단계에서 기본 틀을 만들어놔서 개발할 때 매끄럽게 계획들을 진행할 수 있었다.</li>
<li><a href="https://docs.google.com/spreadsheets/d/1R33YPytL6RtWXk16qvQTP4UOgSotSVUseI0vb9x34jU/edit#gid=1397693154">스프레드 시트로 테스트 코드 시나리오 공유</a> -&gt; 다른 팀원의 도메인 정책을 이해하는데 아주 좋았다.</li>
</ol>
<h3 id="problem문제가-된-것">PROBLEM(문제가 된 것)</h3>
<ol>
<li>추상적인 정책들을 코드로 구현하는 것이 어려웠다.</li>
<li>프론트를 그리며 구현하는 것 또한 어려웠다.</li>
</ol>
<h3 id="try다음에-시도할-것">TRY(다음에 시도할 것)</h3>
<ol>
<li>팀원들이 각자 개발에 집중할 수 있게 개인 개발 시간을 정해서 그 시간동안 혼자서 집중하는 시간을 가지는 것.</li>
<li>클라이밍 가면서 하자.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/miso_/post/06ac96eb-fba3-482a-b557-1698338a0c33/image.png" alt="">
열정 넘치는 팀원들과 함께한 덕분에 좋은 성과를 낼 수 있었던 것 같다.
찌리릿을 통해서 많은 것들을 배웠고 개발 외적으로도 개인적인 성장을 할 수 있었다.</p>
<hr>
<h3 id="기획">기획</h3>
<p>Figma 를 활용해 <a href="https://www.figma.com/file/nAbypv6fJE941WPlJIMj91/DBMJ---BrainStoming?type=whiteboard&amp;node-id=0-1&amp;t=uzU5ONfEKkd9ZIKT-0">브레인스토밍</a>을 하고 <a href="https://www.figma.com/file/UI5mkMJeOK2SYJTFR6Mw12/%EC%99%80%EC%9D%B4%EC%96%B4%ED%94%84%EB%A0%88%EC%9E%84?type=design&amp;node-id=0-1&amp;mode=design&amp;t=SAOvLa7AvS8H6QyD-0">와이어프레임</a>을 만들었다. </p>
<h3 id="프로젝트의-목표">프로젝트의 목표</h3>
<p>네이버 스트리밍 플랫폼 치지직의 커뮤니티 서비스를 제공하는 것
<img src="https://velog.velcdn.com/images/miso_/post/075c1299-87be-45d5-bb55-e8163c3a060a/image.png" alt=""></p>
<h3 id="찌리릿-아키텍쳐">찌리릿 아키텍쳐</h3>
<p><img src="https://velog.velcdn.com/images/miso_/post/8030867b-3f0e-4caf-b49c-d95109a13c2d/image.png" alt=""></p>
<h3 id="erd">ERD</h3>
<p><img src="https://velog.velcdn.com/images/miso_/post/b93c26d6-7df5-4fa0-9dc9-2eec8080a378/image.png" alt=""></p>
<hr>
<h2 id="개발한-도메인과-성능개선">개발한 도메인과 성능개선</h2>
<h3 id="querydsl-을-활용한-스케쥴러">QueryDsl 을 활용한 스케쥴러</h3>
<p>게시판의 정책으로 8일 이상 스트리머 게시판 내 업데이트 된 게시글이 없다면 게시판을 비활성화 상태값으로 변경시켜 프론트에서 보이지 않도록 구현해야했다.</p>
<p>일정 주기로 실행시켜야하므로 스프링 스케쥴러를 활용했고, 직관적으로 쿼리를 확인하고 복잡한 쿼리를 쉽게 작성할 수 있는 QueryDsl 을 활용했다.</p>
<pre><code class="language-kotlin"> @Transactional
    @Scheduled(cron = &quot;0 0 0 * * *&quot;)
    fun boardScheduler() {
        val inactiveBoardIdList = boardRepository.findInactiveBoardStatus()

        boardRepository.updateBoardStatusToInactive(inactiveBoardIdList)
    }
</code></pre>
<p>대량의 데이터를 효율적으로 일괄 업데이트 처리하기 위해 더티 체킹 방식이 아닌 QueryDsl update 방식을 사용했다.</p>
<pre><code class="language-kotlin">override fun findBoardStatusToInactive(): List&lt;Long&gt; {
        val checkInactiveDate = LocalDateTime.now().minusDays(8)

        return queryFactory.select(board.id).distinct()
            .from(post)
            .leftJoin(board)
            .on(board.id.eq(post.board.id))
            .where(post.modifiedAt.loe(checkInactiveDate), board.boardType.eq(BoardType.STREAMER_BOARD))
            .fetch()
    }

override fun updateBoardStatusToInactive(inactiveBoardIdList: List&lt;Long&gt;) {
        queryFactory
            .update(board)
            .set(board.boardActStatus, BoardActStatus.INACTIVE)
            .where(board.id.`in`(inactiveBoardIdList))
            .execute()
    }    </code></pre>
<h3 id="트러블-슈팅-스케쥴러-작업-중복-실행-가능성-문제">트러블 슈팅: 스케쥴러 작업 중복 실행 가능성 문제</h3>
<p>scale-out 한 다른 어플리케이션 서버에서 구동 중인 2번째 스케쥴러가 동시 작업 진행 시 스케쥴러 수행 자체의 중복이 발생할 수 있는 문제점을 발견했다.</p>
<p>중복 실행에 대한 해결을 위해 ShedLock 을 이용해 스케쥴링 Lock 을 설정했다.</p>
<p>Shedlock 정보 관리를 위한 테이블 생성</p>
<pre><code class="language-kotlin">CREATE TABLE shedlock(name VARCHAR(64) NOT NULL, lock_until TIMESTAMP NOT NULL,
    locked_at TIMESTAMP NOT NULL, locked_by VARCHAR(255) NOT NULL, PRIMARY KEY (name));</code></pre>
<p>Bean 설정</p>
<pre><code class="language-kotlin">@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = &quot;15m&quot;)
class ScheduleConfig {

    @Bean
    fun lockProvider(dataSource: DataSource): LockProvider {
        return JdbcTemplateLockProvider(
            JdbcTemplateLockProvider.Configuration.builder()
                .withJdbcTemplate(JdbcTemplate(dataSource))
                .usingDbTime()
                .build()
        )
    }
}</code></pre>
<p>@SchedulerLock 적용</p>
<pre><code class="language-kotlin">    @Transactional
    @Scheduled(cron = &quot;0 0 0 * * *&quot;)
    @SchedulerLock(name = &quot;boardStatus_lock&quot;, lockAtLeastFor = &quot;14m&quot;, lockAtMostFor = &quot;14m&quot;)
    fun boardScheduler() {
        LockAssert.assertLocked()

        val inactiveBoardIdList = boardRepository.findInactiveBoardStatus()
        boardRepository.updateBoardStatusToInactive(inactiveBoardIdList)

        logger.debug { &quot;scheduled task&quot; }
    }</code></pre>
<h3 id="s3-파일-업로드-구현">S3 파일 업로드 구현</h3>
<p>스트리머 게시판 신청 시 치지직 스트리머임을 인증하는 이미지를 제출해야하는 정책이 있어 AWS S3 를 이용했다. </p>
<p>AWS 비밀 키를 백엔드에서 안전하게 관리하고 클라이언트에 노출시키지 않게해 보안을 강화하기 위해서 백엔드에서 S3를 관리하도록 결정했다.</p>
<p>파일의 확장자를 확인해 이미지 파일만 받을 수 있게 구현했다.</p>
<pre><code class="language-kotlin">fun String.isImageFileOrThrow() {
    val fileExtension = this.split(&quot;.&quot;).let { it[it.lastIndex] }
    check(fileExtension.contains(&quot;png&quot;) or fileExtension.contains(&quot;jpeg&quot;) or fileExtension.contains(&quot;jpg&quot;)) {
        throw RestApiException(ErrorCode.NOT_IMAGE_FILE_EXTENSION)
    }
}</code></pre>
<p>다른 도메인에서도 사용할 수 있도록 메서드로 구현했다.</p>
<pre><code class="language-kotlin">@Service
class S3Service(
    private val amazonS3Client: AmazonS3Client,
) {

    @Value(&quot;\${cloud.aws.s3.bucket}&quot;)
    private lateinit var bucket: String

    fun uploadFiles(dir: String, files: List&lt;MultipartFile&gt;): List&lt;String&gt; {
        val imageUrls = ArrayList&lt;String&gt;()
        for (file in files) {
            val randomFileName = &quot;$dir/${UUID.randomUUID()}${LocalDateTime.now()}${file.originalFilename}&quot;

            file.originalFilename?.isImageFileOrThrow()

            val objectMetadata = ObjectMetadata()
            objectMetadata.contentLength = file.size
            objectMetadata.contentType = file.contentType

            try {
                val inputStream: InputStream = file.inputStream
                amazonS3Client.putObject(bucket, randomFileName, inputStream, objectMetadata)
                val uploadFileUrl = amazonS3Client.getUrl(bucket, randomFileName).toString()
                imageUrls.add(uploadFileUrl)
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
        return imageUrls
    }

}</code></pre>
<h3 id="트러블-슈팅-타임아웃-방지">트러블 슈팅: 타임아웃 방지</h3>
<p>다수의 사용자로부터 동시에 S3 서비스 요청이 들어올 경우 서버의 스레드 소진 위험 가능성이 존재하는 것을 발견했고 이로 인해 타임아웃이 발생될 수 있다. </p>
<p>이에 따라 스레드 풀 설정을 적절하게 하고 타임아웃 문제를 해결하기 위해 코루틴을 이용했다. </p>
<p>백그라운드 스레드로 별도의 스레드 풀을 사용함으로써 비동기 작업을 할 수 있게 withContext와 Dispatchers.IO 로 읽기/쓰기 작업에 스레드를 최적화시켰다.</p>
<p>await 함수를 사용해 비동기 작업이 완료될때까지 기다리고 결과를 반환받기위해 async를 활용했다.</p>
<pre><code class="language-kotlin">suspend fun imageUploadWithCoroutine(dir: String, files: List&lt;MultipartFile&gt;) = withContext(Dispatchers.IO) {
    val imageUrls = ArrayList&lt;String&gt;()
    val uploadImage = files.map {
        val randomFileName = &quot;$dir/${UUID.randomUUID()}${LocalDateTime.now()}${it.originalFilename}&quot;
        val uploadFileUrl = amazonS3Client.getUrl(bucket, randomFileName).toString()

        it.originalFilename?.isImageFileOrThrow()
        val objectMetadata = ObjectMetadata().apply {
            this.contentType = it.contentType
            this.contentLength = it.size
        }
        async {
            val putObjectRequest = PutObjectRequest(
                bucket,
                randomFileName,
                it.inputStream,
                objectMetadata,
            )
            amazonS3Client.putObject(putObjectRequest)
            imageUrls.add(uploadFileUrl)
        }
    }
    uploadImage.await()
    return@withContext imageUrls</code></pre>
<h3 id="성능개선-presigned-url">성능개선: presigned URL</h3>
<p>클라이언트에서 여러 장의 이미지 파일 자체를 백엔드로 데이터를 보내주면 aws에 저장되고 클라이언트에게 다시 링크를 넘겨주고 있는데 이 과정에서 서버의 부하가 생길 수 있는 문제가 있다. </p>
<p>그래서 서버를 경유하지 않고 프론트에서 직접 파일을 업로드 할 수 있는 aws S3 의 Presigned Url 정책을 통해 해결하기로 결정했다. </p>
<pre><code class="language-kotlin">fun getPreSignedUrl(fileNames: List&lt;MultipartFile&gt;): Map&lt;String, Serializable&gt;? {
        val encodedFileName = fileNames.let { &quot;${it}_${LocalDateTime.now()}&quot; }
        val expiration = Date()
        var expTimeMillis: Long = expiration.time

        expTimeMillis += (3 * 60 * 1000).toLong() // 3분
        expiration.time = expTimeMillis // url 만료 시간 설정

        val generatePresignedUrlRequest: GeneratePresignedUrlRequest =
            GeneratePresignedUrlRequest(bucket, &quot;test/${encodedFileName}&quot;)
                .withMethod(HttpMethod.PUT).withExpiration(expiration)

        return mapOf(
            &quot;preSignedUrl&quot; to amazonS3Client.generatePresignedUrl(generatePresignedUrlRequest),
            &quot;encodedFileName&quot; to encodedFileName
        )
    }</code></pre>
<h3 id="성능개선-응답속도-향상">성능개선: 응답속도 향상</h3>
<p>응답속도 비교를 위해 이미지 업로드 컨트롤러를 따로 만들어 포스트맨을 이용해 테스트를 진행했다.
1001 ms -&gt; 452 ms (54.8% 감소)
452 ms -&gt; 250 ms (44.6% 감소)
프론트에서 S3를 관리하면 업로드 속도가 제일 빠르다는 것을 알 수 있다.</p>
<pre><code class="language-kotlin">@PostMapping(&quot;/imageUpload&quot;)
fun uploadImage(@RequestParam multipartFile: List&lt;MultipartFile&gt;): String {
    return s3Service.uploadFiles(dir = &quot;test&quot;, multipartFile)
}

@PostMapping(&quot;/imageUploadWithCoroutine&quot;)
suspend fun imageUploadWithCotoutine(@RequestParam multipartFile: List&lt;MultipartFile&gt;): String {
    return s3Service.imageUploadWithCoroutine(dir = &quot;test&quot;, multipartFile)
}

@PostMapping(&quot;/imageUploadWithPreSignedUrl&quot;)
fun uploadImageWithPreSignedUrl(@RequestParam multipartFile: List&lt;MultipartFile&gt;): Map&lt;String, Serializable&gt;? {
    return s3Service.getPreSignedUrl(multipartFile)
}</code></pre>
<hr>
<h3 id="찌리릿-고도화-프로젝트">찌리릿 고도화 프로젝트</h3>
<p>앱 출시를 위해서 ios 개발자 분들과 협업을 하게 됐다
구현하고 싶었던 이벤트 도메인과 성능개선을 진행하고 싶고 앱 개발자들이 원하는 api 와 프론트-백 연결하는 법이 궁금하다 웹이랑 많이 다른가 싶고
재밌게 협업해보고 싶다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[리프레시 토큰]]></title>
            <link>https://velog.io/@miso_/%EB%A6%AC%ED%94%84%EB%A0%88%EC%8B%9C-%ED%86%A0%ED%81%B0</link>
            <guid>https://velog.io/@miso_/%EB%A6%AC%ED%94%84%EB%A0%88%EC%8B%9C-%ED%86%A0%ED%81%B0</guid>
            <pubDate>Sun, 04 Feb 2024 15:34:39 GMT</pubDate>
            <description><![CDATA[<p>로그인을 하면 리프레시 토큰이 저장되고(empty claims이다)</p>
<p><img src="https://velog.velcdn.com/images/miso_/post/440415b7-7809-4f78-a2b1-19fc37c73734/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/miso_/post/c1258495-a2b5-46e5-b6bb-6f55b3d35c2f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/miso_/post/efa9c01c-294b-461d-97d7-3dac9fd9a646/image.png" alt=""></p>
<p>로그아웃을 하면 널값으로 바뀐다. </p>
<hr>
<p>Member.kt</p>
<pre><code class="language-kotlin">fun removeRefreshToken() {
      this.refreshToken = null
    }</code></pre>
<p>MemberService.kt</p>
<pre><code class="language-kotlin">@Transactional
fun logout() {
    val authenticatedEmail = authenticationUtil.getUserEmail()
    val member = memberRepository.findMemberByEmail(authenticatedEmail) ?: throw Exception(&quot;등록된 멤버가 아닙니다&quot;)

    member.removeRefreshToken()

//  memberRepository.save(member)      @Transactional 안쓰면 따로 jpa 한테 시켜서 저장해야되고 어노테이션 쓰면 더티체킹으로 리프레시 토큰에 널값이 업데이트 된다
    }</code></pre>
<br>
<br>
<br>

<h3 id="리프레시-토큰을-사용하면-왜-좋을까">리프레시 토큰을 사용하면 왜 좋을까?</h3>
<hr>
<p>엑세스 토큰의 보안을 위해 엑세스 토큰의 시간을 짧게 지정하고 만료되기까지 더 긴 시간이 걸리는 리프레시 토큰으로 서비스 사용자가 잦은 로그아웃으로 불편하지 않게 하고 탈취로부터 안전하도록 엑세스 토큰을 자주 갱신할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로그인/로그아웃 오류]]></title>
            <link>https://velog.io/@miso_/%EB%A1%9C%EA%B7%B8%EC%9D%B8%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@miso_/%EB%A1%9C%EA%B7%B8%EC%9D%B8%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Sun, 04 Feb 2024 15:32:23 GMT</pubDate>
            <description><![CDATA[<h3 id="로그인-오류">로그인 오류</h3>
<hr>
<p>java.lang.ClassCastException: class java.lang.String cannot be cast to class com.teamsparta.member.global.auth.UserPrincipal (java.lang.String is in module java.base of loader &#39;bootstrap&#39;; com.teamsparta.member.global.auth.UserPrincipal is in unnamed module of loader &#39;app&#39;)</p>
<pre><code class="language-kotlin">    @Transactional
    fun login(request: LoginRequest): LoginResponse {
        val authenticatedEmail = AuthenticationUtil.getUserEmail()
        val member = memberRepository.findMemberByEmail(authenticatedEmail) ?: throw Exception(&quot;등록된 멤버가 아닙니다&quot;)//NosuchEntityExeption(&quot;MEMBER&quot;)
        val jwt = jwtPlugin.generateJwt(member)
</code></pre>
<p>로그인을 하는 단계가 토큰을 받는 단계인데 아직 컨텍스트 홀더에 담기지 않았는데 받지도 않은 비어있는 authentication 안에서 이메일을 꺼내오려고 했다.</p>
<br>
<br>
<br>


<h3 id="로그아웃-오류">로그아웃 오류</h3>
<hr>
<p>NullPointerException이 발생했는데 오류문을 읽어보니 subject(토큰제목)가 비어있으면 안된다고 적혀있었다.</p>
<p>이 오류는 실제 값이 아닌 null을 가지고 있는 객체/변수를 호출할 때 발생하는 예외라고 한다.</p>
<p>디버깅을 해서 payload를 보니 <code>sb</code>: 서브젝트가 없었다.</p>
<p>토큰을 생성할 때 서브젝트를 같이 빌드하지 않아서 <code>subject</code> 가 null 값이 나왔다. </p>
<pre><code class="language-kotlin">fun generateJwt(member: Member): LoginResponse {
        val accessToken = generateToken(member)
        val refreshToken = generateRefreshToken(member)

        return LoginResponse(accessToken, refreshToken)
    }

private fun generateToken(member: Member): String {
    val claims = mapOf(
        &quot;nickName&quot; to member.nickName,
        &quot;email&quot; to member.email,
        &quot;role&quot; to member.role
    )

    return Jwts.builder()
        .subject(member.id.toString())
        .issuer(issuer)
        .issuedAt(Date.from(Instant.now()))
        .claims(claims)
        .signWith(key)
        .expiration(Date.from(Instant.now().plus(Duration.ofHours(accessTokenExpirationHour))))
        .compact()
    }

private fun generateRefreshToken(member: Member): String {
    val emptyClaims: Map&lt;String, Any&gt; = emptyMap()

    return Jwts.builder()
        .subject(member.id.toString())
        .issuer(issuer)
        .issuedAt(Date.from(Instant.now()))
        .claims(emptyClaims)
        .signWith(key)
        .expiration(Date.from(Instant.now().plus(Duration.ofHours(refreshTokenExpirationHour))))
        .compact()
    }</code></pre>
<p>generateToken 메서드에 .subject(member.id.toString())를 추가하고</p>
<p>generateRefreshToken 메서드에도 member 인자를 추가하고 id값을 문자열로 바꿔 subject를 추가해 오류를 수정했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Data JPA Auditing BaseEntity ]]></title>
            <link>https://velog.io/@miso_/Spring-Data-JPA-Auditing-BaseEntity</link>
            <guid>https://velog.io/@miso_/Spring-Data-JPA-Auditing-BaseEntity</guid>
            <pubDate>Fri, 02 Feb 2024 15:16:26 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-kotlin">@Configuration
@EnableJpaAuditing
class JpaAuditingConfig</code></pre>
<pre><code class="language-kotlin">@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class BaseEntity {

    @CreatedDate
    @Column(columnDefinition = &quot;TIMESTAMP(6)&quot;, name = &quot;created_at&quot;, nullable = false, updatable = false)
    var createdAt: LocalDateTime = LocalDateTime.now()
        protected set

    @LastModifiedDate
    @Column(columnDefinition = &quot;TIMESTAMP(6)&quot;, name = &quot;updated_at&quot;, nullable = false)
    var updatedAt: LocalDateTime = LocalDateTime.now()
        protected set
}</code></pre>
<p>엔티티가 생성되고 변경되는 시점을 감지해 생성시각, 수정시각을 기록할 수 있다.</p>
<p>먼저 EnableJpaAuditing 어노테이션을 사용하여, Auditing을 활성화 해야한다. </p>
<pre><code class="language-kotlin">@Entity
@Table(name = &quot;posts&quot;)
class Post private constructor(
    _title: String,
    _content: String,
    _member: Member
) : BaseEntity() {
@Column(name = &quot;ended_at&quot;)
var endedAt: LocalDateTime = createdAt.plusDays(14)
    private set

    ...
fun from() = PostResponse(
        id = id,
        title = title,
        content = content,
        member = member.nickname,
        createdAt = createdAt,
        endedAt = endedAt
        )</code></pre>
<p>이후 엔티티 클래스에 상속받고 사용한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[연관관계 - 2]]></title>
            <link>https://velog.io/@miso_/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-2</link>
            <guid>https://velog.io/@miso_/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-2</guid>
            <pubDate>Thu, 01 Feb 2024 14:07:08 GMT</pubDate>
            <description><![CDATA[<p>양방향 매핑 문제는 순환 참조 문제가 생길 수 있다.</p>
<p>서로를 계속 호출하는 문제가 발생!</p>
<ul>
<li>순환 참조 문제<pre><code class="language-kotlin">@GetMapping(&quot;/test&quot;)
fun test(): Member {
  return Member()
}</code></pre>
객체.response를 쓰는 이유는 서로의 엔티티를 계속 참조하다가 발생하는 순환참조를 방지하기 위해서다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[연관관계 - 1]]></title>
            <link>https://velog.io/@miso_/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-ukzi0sd3</link>
            <guid>https://velog.io/@miso_/%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-ukzi0sd3</guid>
            <pubDate>Wed, 31 Jan 2024 14:54:50 GMT</pubDate>
            <description><![CDATA[<ol>
<li><p>FK만 집어 넣은 연관관계 매핑</p>
<pre><code class="language-kotlin">@GetMapping(&quot;/prac1&quot;)
fun practice1() {
 val emf = Persistence.createEntityManagerFactory(&quot;hello&quot;)
 val em = emf.createEntityManager()

 // transaction
 val transaction = em.transaction
 transaction.begin()

 var team = Team()
 team.teamName = &quot;테스트 팀네임&quot;

 val member = Member()
 member.memberName = &quot;테스트 멤버네임&quot;
 member.teamId = team.id
 em.persist(member)

 transaction.commit()

 em.close()
 emf.close()
}</code></pre>
<p>team id로 매핑은 되지만 DB와 DB 사이를 억지로 집어넣은 느낌이 든다. </p>
</li>
</ol>
<p>동작도 가능하지만 객체를 집어넣어서 매핑하는게 객체지향적으로 좋다.</p>
<p>이렇게 나온 개념이 ManyToOne, OneToMany라고 한다.</p>
<pre><code class="language-kotlin">@Entity
class Member {
    @Id @GeneratedValue
    var id: Long = 0

    @Column(name = &quot;title&quot;)
    var memberName: String = &quot;&quot;

    @Column(name = &quot;team_id&quot;)
    var teamId: Long = 0L

    var team: Team
}</code></pre>
<hr>
<p>객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다. </p>
<ul>
<li><p>테이블: 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.</p>
</li>
<li><p>객체 : 참조를 사용해서 연관된 객체를 찾는다.</p>
<p>==&gt; 테이블과 객체 사이에는 이런 큰 간격이 있다.</p>
</li>
</ul>
<hr>
<ol start="2">
<li>단방향 지정</li>
</ol>
<pre><code class="language-kotlin">@Entity
class Member {
    @Id @GeneratedValue
    var id: Long = 0

    @Column(name = &quot;title&quot;)
    var memberName: String = &quot;&quot;

    @Column(name = &quot;team_id&quot;)
    var teamId: Long = 0L

    @ManyToOne
    var team: Team? = null
}</code></pre>
<p>team id 를 외래키(FK)로 지정한다.</p>
<p><img src="https://velog.velcdn.com/images/miso_/post/df8e687b-0ad2-4784-af0f-9b42f85bd32c/image.png" alt=""></p>
<p>이렇게 단방향만으로도 모든 매핑이 가능!</p>
<p>그래서 설계할 때 단방향으로만 테이블 연관관계를 맺어놓고 만약, MEMBER가 TEAM을 참조해야 하는 경우에만 양방향으로 바꾸는 것! 처음부터 양방향 막 쓰지 않도록 주의!!</p>
<hr>
<ol start="3">
<li>단방향 활용</li>
</ol>
<pre><code class="language-kotlin">@GetMapping(&quot;/prac1&quot;)
fun practice1() {
    val emf = Persistence.createEntityManagerFactory(&quot;hello&quot;)
    val em = emf.createEntityManager()

    // transaction
    val transaction = em.transaction
    transaction.begin()

    var team = Team()
    team.teamName = &quot;테스트 팀네임&quot;
    em.persist(team)

    val member = Member()
    member.memberName = &quot;테스트 멤버네임&quot;
    member.team = team
    em.persist(member)

    transaction.commit()

    em.close()
    emf.close()
}</code></pre>
<pre><code class="language-kotlin">@Entity
class Member(
    @Id
    @GeneratedValue()
    @Column(name = &quot;MEMBER_ID&quot;)
    val id: Long = 0,

    @Column(name = &quot;USERNAME&quot;)
    var username: String,

    @ManyToOne
    @JoinColumn(name = &quot;TEAM_ID&quot;)
    val team: Team
)</code></pre>
<p>일대다 중 멤버가 FK의 주인이므로 멤버에 팀 객체를 담는다.</p>
<p>=&gt; FK의 주인에 One객체를 집어 넣는다.</p>
<p>객체지향적이고 DB문제도 해결(DB를 어거지로 묶는 위의 예시 해결)</p>
<p>=&gt; 이렇게 하면 join의 성질도 갖게 된다.</p>
<hr>
<ol start="4">
<li>양방향 연관관계</li>
</ol>
<br>

<p>만약 팀에 멤버 리스트가 존재한다면?</p>
<pre><code class="language-kotlin">@Entity
class Team {
    @Id @GenenratedValue
    var id: Long = 0

    @Column(name =&quot;title&quot;)
    var teamName: String = &quot;&quot;

      @OneToMany(mappedBy = &quot;team&quot;, fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true)
    val member: MutableList&lt;Member&gt; = mutableListOf()
}</code></pre>
<p>한 팀에 여러멤버가 들어오니 멤버 매핑할 때 리스트로 설정</p>
<p>이렇게 해도 테이블 연관관계는 단방향과 변함이 없다.</p>
<p>양방향으로 하든 안하든 단방향으로 하면 DB 연결이 다 끝난다.</p>
<p>그래서 처음에 작업할 때 단방향으로 다 연결하고 나중에 도메인 로직을 작성할 때 댓글을 통해서 게시글을 찾게 된다면 양방향 연결하기(서비스 로직)</p>
<p>양방향은 객체지향 문제이기에 DB와는 아무관계가 없다.</p>
<hr>
<ul>
<li>연관관계의 주인(mappedBy)
연관관계의 주인을 지정한다는 것은, 객체의 두 관계 중 제어의 권한(데이터 조회, 저장, 수정, 삭제)를 갖는 실질적인 관계가 누구인지 JPA에게 알리는 것이다.</li>
</ul>
<p>따라서 연관관계의 주인은 연관 관계를 갖는 두 객체 사이에서 조회, 저장, 수정, 삭제를 할 수 있지만, 주인이 아니면 조회만 가능하다.</p>
<p>주인은 외래키가 있는 곳으로 지정하면 된다.</p>
<p>Member엔티티가 외래 키를 가지고 있으믕로 연관관계의 주인이 된다.</p>
<p>연관관계의 주인이 아닌 객체는 mappedBy 속성을 사용한다.</p>
<ul>
<li>mappedBy == 나는 키의 주인이 아니다.</li>
<li>team이 member에 묶여 있다. team은 member 객체만 조회할 수 있다, Team 엔티티의 member리스트는 mappedBy로 읽기 전용</li>
</ul>
<blockquote>
<p>&lt;양방향 매핑 규칙&gt;</p>
</blockquote>
<ul>
<li>객체의 두 관계중 하나를 연관관계의 주인으로 지정</li>
<li>연관관계의 주인만이 외래 키를 관리(등록, 수정)</li>
<li>주인이 아닌 쪽은 읽기만 가능(team에 해당)</li>
</ul>
<pre><code class="language-kotlin">@GetMapping(&quot;/prac1&quot;)
fun practice1() {
    val emf = Persistence.createEntityManagerFactory(&quot;hello&quot;)
    val em = emf.createEntityManager()

    // transaction
    val transaction = em.transaction
    transaction.begin()

    val member = Member()
    member.memberName = &quot;새로운 멤버네임&quot;
    em.persist(member)


    val team = Team()
    team.teamName = 새로운 팀네임&quot;
    team.member.add(member)
    em.persist(team)


    transaction.commit()

    em.close()
    emf.close()
}</code></pre>
<p>만약 주인이 아닌 방향(역방향)만 연관관계 설정을 하면 Member 테이블의 TEAM_ID는 NULL 값이 들어간다.</p>
<br>

<p>올바르게 사용하려면,</p>
<pre><code class="language-kotlin">@GetMapping(&quot;/prac1&quot;)
fun practice1() {
    val emf = Persistence.createEntityManagerFactory(&quot;hello&quot;)
    val em = emf.createEntityManager()

    // transaction
    val transaction = em.transaction
    transaction.begin()

    val team = Team()
    team.teamName = &quot;Z새로운 팀네임11&quot;
    em.persist(team)

    val member = Member()
    member.memberName = &quot;새로운 멤버네임11&quot;
    member.team = team
    em.persist(member)


    transaction.commit()

    em.close()
    emf.close()
}</code></pre>
<p><img src="https://velog.velcdn.com/images/miso_/post/5640942a-e3c7-4fa9-ab1c-76b32f84faca/image.png" alt=""></p>
<p>멤버에다가 팀을 넣어야 팀 아이디가 DB에 들어간다.</p>
<p>mappedBy Member List가 읽기 전용이라 멤버 추가를 JPA가 무시해버린다.</p>
<hr>
<p>매번 양방향의 코드를 추가하는 것이 힘들기에 연관관계 편의 메서드 생성 권장</p>
<pre><code class="language-kotlin">@Entity
class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;MEMBER_ID&quot;)
    var id: Long

    @Column(name = &quot;USERNAME&quot;)
    val username: String

    @ManyToOne
    @JoinColumn(name = &quot;TEAM_ID&quot;)
    val team: Team

    // 연관관계 편의 메서드
    fun addTeam(team: Team) {
        this.team = team;
        team.getMembers().add(this);
    }

    ... 생략
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[로그인 1차 검증]]></title>
            <link>https://velog.io/@miso_/%EB%A1%9C%EA%B7%B8%EC%9D%B8-1%EC%B0%A8-%EA%B2%80%EC%A6%9D</link>
            <guid>https://velog.io/@miso_/%EB%A1%9C%EA%B7%B8%EC%9D%B8-1%EC%B0%A8-%EA%B2%80%EC%A6%9D</guid>
            <pubDate>Tue, 30 Jan 2024 13:49:57 GMT</pubDate>
            <description><![CDATA[<p>@Valid 를 사용하기 위해 <code>implementation &#39;org.springframework.boot:spring-boot-starter-validation&#39;</code> 추가 후, </p>
<hr>
<p>LoginRequest.kt</p>
<pre><code class="language-kotlin">data class LoginRequest(
    @field:Email(message = &quot;올바른 이메일 형식이 아닙니다.&quot;)
    @field:NotBlank(message = &quot;이메일을 입력해주세요&quot;)
    val email: String,
    @field:NotBlank(message = &quot;비밀번호를 입력해주세요&quot;)
    val password: String,
)
</code></pre>
<p>SignUpRequest.kt</p>
<pre><code class="language-kotlin">data class SignUpRequest(
    @field:Email(message = &quot;올바른 이메일 형식이 아닙니다.&quot;)
    @field:NotBlank(message = &quot;이메일을 입력해주세요&quot;)
    val email: String,
    @field:NotBlank(message = &quot;닉네임을 입력해주세요&quot;)
    val nickName: String,
    @field:NotBlank(message = &quot;비밀번호를 입력해주세요&quot;)
    val password: String,
    @field:NotBlank(message = &quot;&#39;MEMBER&#39;/&#39;ADMIN&#39; 중 역할을 입력해주세요&quot;)
    val role: String,
)
</code></pre>
<p> Validation 관련 annotation에 @field:를 prefix로 붙이고 message에 출력문을 적어준다.</p>
<hr>
<p>AppUserController.kt</p>
<pre><code class="language-kotlin">@PostMapping(&quot;/login&quot;)
fun login(@Valid @RequestBody loginRequest: LoginRequest): ResponseEntity&lt;LoginResponse&gt; {
    return ResponseEntity
        .status(HttpStatus.OK)
        .body(appUserService.login(loginRequest))
    }
}</code></pre>
<p>@RequestBody에 대한 validation 에러는 MethodArgumentNotValidException이다. </p>
<hr>
<p>GlobalExceptionHandler.kt</p>
<pre><code class="language-kotlin">@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleMethodArgumentNotValidException(e: MethodArgumentNotValidException): ResponseEntity&lt;ErrorResponse&gt; {
    return ResponseEntity.status(HttpStatus.BAD_REQUEST)
        .body(ErrorResponse(e.bindingResult.allErrors.map { it.defaultMessage }.reduce { acc, string -&gt; &quot;$acc, $string&quot;  }))
    }
}</code></pre>
<p>ErrorResponse로 반환할 때 </p>
<p>e.bindingResult -&gt; 유효성 검사에서 발생한 모든 오류를 가져온다.</p>
<p>.map { it.defaultMessage } -&gt; 각 오류의 기본 메시지를 추출한다.</p>
<p>.reduce { acc, cur -&gt; &quot;$acc, $cur&quot; } -&gt; 모든 메시지를 하나의 문자열로 결합한다. 각 메시지는 컴마로 구분되고 모든 메시지가 단일 문자열로 결합한다.</p>
<p>컨트롤러에서 실행되는 유효성 검사 실패에 대한 모든 메시지를 하나의 문자열로 합쳐서 클라이언트에세 전달할 에러 응답문을 생성한다.</p>
<hr>
<h3 id="reduce">reduce()</h3>
<p>reduce()를 실행하면 하나의 결과값을 반환한다.</p>
<p>줄이다라는 뜻 그 자체의 성질을 가지며 축적되며 줄여나간다.</p>
<ul>
<li>인자</li>
</ul>
<p><code>acc</code> accumulator : 누산기, 누적되는 값, 최종적으로 출력되는 값
     <code>cur</code> current : 현재 돌고 있는 요소</p>
<hr>
<p><img src="https://velog.velcdn.com/images/miso_/post/5c89579c-8cbc-48eb-a08f-e38edb87a40b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/miso_/post/e5823815-e9e0-4fde-9120-543cfd84e5a8/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/miso_/post/47cefe22-2b33-4137-ad53-866c832f6cc6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/miso_/post/bf790161-49a2-495e-9490-c2de3105d865/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[GiveHub 프로젝트 중 예외처리 오류]]></title>
            <link>https://velog.io/@miso_/GiveHub-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%A4%91-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@miso_/GiveHub-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%A4%91-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Sun, 28 Jan 2024 14:10:24 GMT</pubDate>
            <description><![CDATA[<p><code>NoSuchEntityException</code> 오류 메시지가 &quot;존재하지 않는 %s입니다.&quot; 인데,</p>
<p> 발생한 오류가<code>NoSuchEntityException(&quot;MEMBER&quot;)</code>였을때 오류 메시지는 &quot;존재하지 않는 MEMBER입니다.&quot;</p>
<p> <code>NoSuchEntityException(&quot;POST&quot;)</code> 의 오류가 발생했을때에도 &quot;존재하지 않는 MEMBER입니다.&quot; 와 같이 이전의  오류 메시지가  출력되는 버그가 있었다. </p>
<p><img src="https://velog.velcdn.com/images/miso_/post/ffba7966-0a1e-4c7b-a86a-681c25fabbad/image.png" alt=""></p>
<hr>
<p>&lt;수정 전&gt;</p>
<p><img src="https://velog.velcdn.com/images/miso_/post/a1474d13-b944-4e2a-a2a6-4148255353e7/image.png" alt=""></p>
<p>문제의 원인은<code>NoSuchEntityException</code>  entity 프로퍼티(”MEMBER” , ”POST”..)를 enum 클래스 메시지에 직접 넣어 변경했기 때문이다.</p>
<p><code>CommonErrorCode</code>enum 클래스 내에서 <code>message</code>프로퍼티가 mutable이어서 오류메시지가 변하지 않았다. </p>
<p>이로인해 다른 <code>NoSuchEntityException</code> 인스턴스를 사용해 다른 예외를 처리할 때 같은 메시지가 나오는 오류가 발생했다. </p>
<hr>
<p>&lt;수정 후&gt;</p>
<p><img src="https://velog.velcdn.com/images/miso_/post/7a3331c4-b5ef-47b2-9cd4-828722aab83d/image.png" alt=""></p>
<p>이 문제를 해결하기 위해 message 프로퍼티를 직접 변경하지 않고 매번 새로운 문자열을 담을 수 있는 변수 customMessage를 생성했다.</p>
<p>ErrorResponse 데이터 클래스에 customMessage를 인자로 받는 of 메서드를 새로 생성했다. </p>
<p>이넘 클래스의 메시지(errorCode.message)를 직접 변경하는 대신 새로운 문자열을 담을 수 있는 변수를 생성해 ErrorResponse에 전달했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로필 수정 - 비밀번호 변경 제한]]></title>
            <link>https://velog.io/@miso_/%ED%94%84%EB%A1%9C%ED%95%84-%EC%88%98%EC%A0%95-%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%EB%B3%80%EA%B2%BD-%EC%A0%9C%ED%95%9C</link>
            <guid>https://velog.io/@miso_/%ED%94%84%EB%A1%9C%ED%95%84-%EC%88%98%EC%A0%95-%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%EB%B3%80%EA%B2%BD-%EC%A0%9C%ED%95%9C</guid>
            <pubDate>Sat, 27 Jan 2024 14:45:50 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/miso_/post/b5a8281d-9867-41e1-928e-d0d3a9f5935e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/miso_/post/c02797ee-5d55-46a9-a80c-ddb48b00c28f/image.png" alt=""></p>
<ol>
<li><p>비밀번호만 저장하는 테이블 생성</p>
</li>
<li><p>PasswordHistory 엔티티 클래스 작성</p>
<pre><code class="language-kotlin">@Entity
@Table(name = &quot;passwords&quot;)
class PasswordHistory private constructor(
 _email: String,
 _password: String
): BaseEntity() {
 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 var id: Long? = null
     private set

 @Column(name = &quot;email&quot;)
 var email: String = _email
     private set

 @Column(name = &quot;password&quot;)
 var password: String = _password
     private set

 fun update(password: String) {
     this.password = password
 }

 companion object {
     fun of(email: String, password: String) = PasswordHistory(
         _email = email,
         _password = password
     )

 }
}
</code></pre>
</li>
</ol>
<pre><code>3. PasswordRepository 리포지토리
```kotlin
interface PasswordRepository: JpaRepository&lt;PasswordHistory, Long&gt; {
    fun findByEmailOrderByUpdatedAtDesc(email: String): List&lt;PasswordHistory&gt;
}</code></pre><ol start="4">
<li><p>최근 3번안에 사용한 비밀번호는 사용할 수 없도록 제한하는 로직</p>
<pre><code class="language-kotlin">@Transactional
fun updateProfile(request: ProfileRequest): ProfileResponse {
 val passwordHistories = passwordRepository.findByEmailOrderByUpdatedAtDesc(request.email)
 val isDuplicate = passwordHistories.any { passwordEncoder.matches(request.password, it.password) }

 if (isDuplicate) {
     throw InvalidPasswordException()
     }

 val authenticatedId = AuthenticationUtil.getAuthenticationUserId()
 val member = getByEmailOrNull(request.email)
 member.verify(authenticatedId)

 val updatedPassword = passwordEncoder.encode(request.password)
 member.update(request.introduce, updatedPassword)

 if (passwordHistories.size &lt; 3) {
     passwordRepository.save(PasswordHistory.of(request.email, updatedPassword))
 } else {
     passwordHistories[0].update(updatedPassword)
 }

 return member.from()
}</code></pre>
<p>저장된 비밀번호를 findByEmailOrderByUpdatedAtDesc 메서드로 내림차순 정렬을 적용한 상태로 리스트 형식으로 passwordHistories 변수에 담는다.</p>
</li>
</ol>
<p>any { } 를 사용해 리스트를 돌면서 변경을 요청한(request.password) 비밀번호와 it.password(PasswordHistory의 프로퍼티 password 하나)가 중복되는지 확인해 true/false로 반환된 반환값을 isDuplicate 변수에(Bolean Type) 담는다.</p>
<p>DB에 저장된 멤버의 아이디인지 확인하기 위해서 스프링시큐리티 authentication 안에있는 principal로 사용된 userdetails에서 스트링으로 저장했던 id를 다시 Long 타입으로 변환해 authenticatedId 변수에 담는다.</p>
<p>요청받은 이메일로 member 객체를 불러와 Member 엔티티 클래스에서 멤버의 id와 authentication의 principal이 같은지 검증을 한 뒤 변경한 비밀번호는 암호화해서 저장한다.</p>
<p>if문: passwordHistories 리스트에서 비밀번호 개수가 3개 미만일때 새로운 비밀번호가 DB에 들어간다.</p>
<hr>
<pre><code class="language-kotlin">// 스프링 시큐리티를 통해 인증을 받은 유저인지 확인하기 위해서 전역에 쓰일 수 있는 객체 생성
object AuthenticationUtil {
    private fun getAuthenticatedUser(): UserDetails =
        SecurityContextHolder.getContext().authentication.principal as UserDetails

    fun getAuthenticationUserId() = getAuthenticatedUser().username.toLong()

}</code></pre>
<p>이 유저가 스프링 시큐리티로 인증을 받은 유저인지 확인하기 위해서 스프링 시큐리티의 SecurityContextHolder 안에 있는 Authentication에 있는 principal을 가져와 UserDetails를 반환하는 getAuthenticatedUser private 메서드를 생성한다.</p>
<p>이후 UserDetails로 getAuthenticationUserId 메서드를 생성한다. 이 메서드는 서비스 전역에서 쓰인다.</p>
<p>principal로 이용했던 UserDetails에서 member.id.toString()이 username으로 사용됐다. </p>
<p>getAuthenticationUserId 메서드는 private으로 만든 getAuthenticatedUser를 호출해 String으로 저장했던 member 객체의 아이디를 Long 타입으로 바꿔 반환한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[더티체킹을 이용한 댓글 업데이트]]></title>
            <link>https://velog.io/@miso_/%EB%8D%94%ED%8B%B0%EC%B2%B4%ED%82%B9%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%8C%93%EA%B8%80-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8</link>
            <guid>https://velog.io/@miso_/%EB%8D%94%ED%8B%B0%EC%B2%B4%ED%82%B9%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%8C%93%EA%B8%80-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8</guid>
            <pubDate>Thu, 25 Jan 2024 12:11:01 GMT</pubDate>
            <description><![CDATA[<p>CommentRequest.kt</p>
<pre><code class="language-kotlin">data class CommentRequest(
    val memberId: Long,
    val content: String
)</code></pre>
<p>Comment.kt</p>
<pre><code class="language-kotlin">@Column(name = &quot;content&quot;)
var content: String = _content
    private set
        ...

fun update(newContent: String){
     content = newContent
    }</code></pre>
<p>CommentService.kt</p>
<pre><code class="language-kotlin">@Transactional
fun updateComment(postId: Long, commentId: Long, request: CommentRequest): CommentResponse{
    // TODO: 댓글을 작성한 사용자만 댓글 수정 가능해야 함
    val comment = getByIdOrNull(commentId)

    comment.update(request.content)

    return comment.toResponse()
    }</code></pre>
<hr>
<p>request에서 변경한 <code>newContent</code>를 <code>content</code>에 넣어주기만 해도 1차캐시(newContent)와 스냅샷(최초의 content)이 달라져서 flush() 되는 과정중에 업데이트 쿼리가 생성된다.</p>
<p><code>update</code> 메서드 안에 있는 <code>content</code>는 엔티티 컬럼의 <code>content</code>이다(<code>@field:Column(name = &quot;content&quot;) public final var content: String</code>)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL 날짜만 가져오기, 다중정렬(여러 개의 ORDER BY 조건)]]></title>
            <link>https://velog.io/@miso_/SQL-%EB%82%A0%EC%A7%9C%EB%A7%8C-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0-%EB%8B%A4%EC%A4%91%EC%A0%95%EB%A0%AC%EC%97%AC%EB%9F%AC-%EA%B0%9C%EC%9D%98-ORDER-BY-%EC%A1%B0%EA%B1%B4</link>
            <guid>https://velog.io/@miso_/SQL-%EB%82%A0%EC%A7%9C%EB%A7%8C-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0-%EB%8B%A4%EC%A4%91%EC%A0%95%EB%A0%AC%EC%97%AC%EB%9F%AC-%EA%B0%9C%EC%9D%98-ORDER-BY-%EC%A1%B0%EA%B1%B4</guid>
            <pubDate>Wed, 24 Jan 2024 13:28:46 GMT</pubDate>
            <description><![CDATA[<h3 id="날짜만-가져오기">날짜만 가져오기</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/miso_/post/f754ff62-7ec1-4e68-9ff1-2ec620f895c6/image.png" alt=""></p>
<p>오답1</p>
<pre><code class="language-sql">SELECT ANIMAL_ID, NAME, DATE_FORMAT(DATETIME, &#39;%Y/%M/%D&#39;) AS &#39;날짜&#39;
FROM ANIMAL_INS
ORDER BY ANIMAL_ID;</code></pre>
<p><img src="https://velog.velcdn.com/images/miso_/post/8bbe5e92-4796-4adc-8c9d-9857e358979b/image.png" alt=""></p>
<p>오답2</p>
<pre><code class="language-sql">SELECT ANIMAL_ID, NAME, DATE(DATETIME) AS &#39;날짜&#39;
FROM ANIMAL_INS
ORDER BY ANIMAL_ID;</code></pre>
<p><img src="https://velog.velcdn.com/images/miso_/post/77767b02-2021-4d02-a30c-643b58d0c247/image.png" alt=""></p>
<p>정답</p>
<pre><code class="language-sql">SELECT ANIMAL_ID, NAME, DATE_FORMAT(DATETIME, &#39;%Y-%m-%d&#39;) AS &#39;날짜&#39;
FROM ANIMAL_INS
ORDER BY ANIMAL_ID;</code></pre>
<p><img src="https://velog.velcdn.com/images/miso_/post/4e5add34-b9ba-4c0e-bab7-96ba893ef78b/image.png" alt=""></p>
<p>%Y    연도, 숫자, 네 자리</p>
<p>%m    월, 숫자 ( 00.. 12)</p>
<p>%d    월의 일 ( 00.. 31)</p>
<br>
<br>
<br>


<h3 id="다중정렬여러-개의-order-by-조건">다중정렬(여러 개의 ORDER BY 조건)</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/miso_/post/d5f2d5bd-48f4-40b7-b689-3c272a9e1ae2/image.png" alt=""></p>
<pre><code class="language-sql">SELECT DR_NAME, DR_ID, MCDP_CD, DATE_FORMAT(HIRE_YMD, &#39;%Y-%m-%d&#39;) AS &#39;HIRE_YMD&#39;
FROM DOCTOR
WHERE MCDP_CD = &#39;CS&#39; OR MCDP_CD = &#39;GS&#39;
ORDER BY HIRE_YMD DESC, DR_NAME ASC;</code></pre>
<p><img src="https://velog.velcdn.com/images/miso_/post/807221ab-9d8d-464c-a8de-c7d3eb4210bc/image.png" alt=""></p>
<p>ORDER BY 조건을 여러 개 넣어야 할 때, 왼쪽부터 순차적으로 정렬되기 때문에 순서를 고려해야 한다. == 우선순위가 높은 순서대로 나열하면 된다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring & Kotlin querydsl - Fetch Join ]]></title>
            <link>https://velog.io/@miso_/Spring-Kotlin-querydsl-Fetch-Join</link>
            <guid>https://velog.io/@miso_/Spring-Kotlin-querydsl-Fetch-Join</guid>
            <pubDate>Tue, 23 Jan 2024 12:42:02 GMT</pubDate>
            <description><![CDATA[<h3 id="fetch-join">Fetch Join</h3>
<hr>
<p>패치조인은 sql 에 존재하는 조인의 종류가 아닌 JPQL 의 성능을 최적화하기 위해 제공하는 조인의 한 형태이다.</p>
<p><img src="https://velog.velcdn.com/images/miso_/post/a2ecc981-053b-47ef-86c2-5aa51100b6eb/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/miso_/post/ddfab5f5-32c8-4f39-bfc6-c7bca68a64f2/image.png" alt=""></p>
<p>Qcomment 로 EntityPathBase 를 만들어 comment 변수로 comment 엔티티에 접근 가능하게 만들고,</p>
<p>.leftJoin 으로 todo.comments -&gt; todo 와 comment 로 이뤄진 ListPath 가 연결돼 comment 데이터 더미에 접근할 수 있게 됐다.</p>
<pre><code class="language-kotlin">/**
 * QTodo is a Querydsl query type for Todo
 */
@Generated(&quot;com.querydsl.codegen.DefaultEntitySerializer&quot;)
public class QTodo extends EntityPathBase&lt;Todo&gt; {

    private static final long serialVersionUID = 740783912L;

    public static final QTodo todo = new QTodo(&quot;todo&quot;);

    public final ListPath&lt;com.teamsparta.todo.domain.comment.model.Comment, com.teamsparta.todo.domain.comment.model.QComment&gt; comments = this.&lt;com.teamsparta.todo.domain.comment.model.Comment, com.teamsparta.todo.domain.comment.model.QComment&gt;createList(&quot;comments&quot;, com.teamsparta.todo.domain.comment.mode</code></pre>
<br>
<br>
<br>

<h4 id="fetchjoin-이전-쿼리문-개수">fetchJoin 이전 쿼리문 개수</h4>
<hr>
<p><img src="https://velog.velcdn.com/images/miso_/post/4d802dde-0d4b-4ce6-9dcf-eb3f94abe160/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/miso_/post/e024fbc3-43c4-432d-9f65-ef23989ba737/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/miso_/post/4d17acfe-ba42-4319-8cff-ced0a1f9a1f6/image.png" alt=""></p>
<p>3개의 데이터를 조회하는데 페이징에 썼던 count 쿼리문 1개를 제외하고, 4개의 쿼리문이 나갔다.</p>
<br>
<br>

<h3 id="fetchjoin-이후-쿼리문-개수">fetchJoin 이후 쿼리문 개수</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/miso_/post/1cbd20b3-6b0f-4d45-a6f7-a0464c6cb480/image.png" alt=""></p>
<p>두 테이블이 조인돼서 한방에 쿼리가 나갔다. N+1 문제 해결!!</p>
<pre><code>t1_0. todo 프로퍼티뿐만이 아니라
c1_0. comment 의 프로퍼티들을 잘 가져왔다</code></pre><p>만약 leftJoin 만 하고 fetchJoin 을 설정하지 않는다면, 똑같이 N+1 문제가 발생한다.</p>
<br>
<br>
<br>

<p>그 이유는 쿼리문에서 <code>selectFrom(todo)</code> todo 에서만 <code>select</code> 조회해오기 때문에  영속성 컨텍스트에 <code>comment</code> 가 없어서 계속 <code>select comment</code> 로 <code>comment</code> 를 가져오는 쿼리가 발동한 것. </p>
<pre><code class="language-kotlin">val comment = QComment.comment                         // comment EntityPathBase 정의
        val query = queryFactory.selectFrom(todo)
            .where(whereClause)
            .leftJoin(todo.comments, comment)         // left join == todo 테이블을 기준으로 comment 를 연결해준다는 뜻 , 두 테이블이 join 됨
            .fetchJoin()                              // 레프트 조인 이후 fetchJoin() 을 설정 
            .offset(pageable.offset)
            .limit(pageable.pageSize.toLong())</code></pre>
<p>근데 <code>org.hibernate.orm.query                  : HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory</code>  경고가 뜬다.</p>
<p>offset 쿼리와 limit 쿼리가 나가지 않았다. Fetch Join 을 하게 되면 offset 과 limit 이 null 로 나가게 된다.</p>
<p>그래서 데이터 리스트를 다 가져와서 어플리케이션 상에서 모든 데이터를 다 가져온 이후에 페이징(offset 과 limit)을 적용하는 것이다.</p>
<p>todo를 다 가져오고 나서 페이징 처리를 한건데 지금은 todo 할일 데이터가 몇개 없지만 만개의 데이터가 된다면 메모리에 만건을 올려두고 페이징 처리를 하게 된다면 큰 문제가 된다.(모든 데이터를 가져와 페이징 개수만큼 자른다)</p>
<p>그래서 todo - comment가 OneToMany 일대다 관계인데 이럴때 Fetch Join 을 하면서 Paging 까지 같이 쓰면 위험하다. </p>
<p>그래서 일대다 관계일 땐 Fetch Join을 지양해야 한다.</p>
<p>또 Fetch Join을 두번 쓴다면 Multiple Fetch Exception 오류가 뜬다.</p>
<p>만약 todo - todoApplication과 같이 또 다른 일대다 관계의 테이블을 left join 하고 성능 최적화를 위해서 fetch join을 쓰는 것은 불가능하다.</p>
<p>N개의 테이블에서 계속 데이터를 가져오게되면서 기하급수적으로 늘 수 있기 때문에  Hibernate 에서 허용하지 않는다. --&gt;  주의하기!!</p>
<p>ManyToOne 관계를 여러 개 하는건 상관 없다.</p>
<hr>
<p>일대다 관계에서 Pagination도 적용하면서 성능 최적화를 하고 싶다면, fetch Join 을 꼭 !! 설정하지 말고!!  yml 파일에 default_batch_fetch_size 를 적용하면 된다(보통 500개 정도만 씀)</p>
<p><img src="https://velog.velcdn.com/images/miso_/post/d5675336-ada6-40a2-ba46-02aad1f7ac6b/image.png" alt=""></p>
<p>연관된 엔티티를 500 개까지 가져올 수 있게 된다. default_batch_fetch_size 는 IN 쿼리를 사용한다.</p>
<p>IN 쿼리는 ROW를 체크만하는 EXISTS랑 다르게 ROW의 데이터를 모두 확인하기 때문에 쿼리가 여러번 나가지 않고 한방에 사용할 수 있는 것이다.</p>
<hr>
<p>다대일 관계는 Fetch Join을 여러개 써도 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin & Spring 트랜잭셔널]]></title>
            <link>https://velog.io/@miso_/Kotlin-Spring-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%94%EB%84%90</link>
            <guid>https://velog.io/@miso_/Kotlin-Spring-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%94%EB%84%90</guid>
            <pubDate>Mon, 22 Jan 2024 14:52:23 GMT</pubDate>
            <description><![CDATA[<p>DB가 제공하는 트랜잭션 기능을 사용하면 commit과 rollback으로 정상적인 작업이 가능하도록 할 수 있다.</p>
<p>계좌이체를 예시로 들면,</p>
<p>트랜잭션이 없다면 작업중 서버에 오류가 났을 때 중간에 이체된 돈은 그냥 사라지고 만다.</p>
<p>이런 서비스 참사를 막기 위해서 작업 중 하나라도 실패를 한다면 거래 이전으로 되돌리는데 이것을 롤백(rollback)이라고 한다.</p>
<p>롤백을 하게 된다면 결과적으로 오류가 나도 이체한 사람의 잔고가 감소하지 않는다.</p>
<p>그리고 모든 작업이 정상적으로 성공하는 경우 데이터베이스에 정상 반영하는 것을 커밋(commit)이라고 한다.</p>
<p>즉, 데이터 변경 쿼리가 DB에 반영되려면 commit을 호출해야 하고, 결과를 반영하고 싶지 않다면 트랜잭셔널을 이용해 rollback을 호출하면 된다.</p>
<br>
<br>
<br>
<br>
<br>

<h3 id="피드백으로-받았던-무분별한-transactional-사용">피드백으로 받았던 무분별한 @Transactional 사용</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/miso_/post/f1224a6a-7df3-4607-9149-d017b206407a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/miso_/post/48533582-3977-450d-91ee-2fb7778c897e/image.png" alt=""></p>
<p>DB가 제공하는 트랜잭셔널 어노테이션은 생성할 때 굳이 필요하지 않다. 외부 개입이 없기 때문이다. 오류가 난다면 DB에 반영이 되지 않는다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA Persistence Context(+EntityManager)]]></title>
            <link>https://velog.io/@miso_/Persistence-Context</link>
            <guid>https://velog.io/@miso_/Persistence-Context</guid>
            <pubDate>Sun, 21 Jan 2024 14:43:16 GMT</pubDate>
            <description><![CDATA[<h3 id="영속성-컨텍스트">영속성 컨텍스트</h3>
<hr>
<p>DB와 프로그램 사이에 공간이 있는데 이 공간이 영속성 컨텍스트이다.</p>
<br>


<ul>
<li>JPA 의 영속성 컨텍스트로 관리되는 엔티티들</li>
</ul>
<p><img src="https://velog.velcdn.com/images/miso_/post/0452e9bb-ad4b-4027-8b86-cc98922e80e3/image.png" alt=""></p>
<ul>
<li><p>엔티티의 생명주기</p>
<ul>
<li>비영속 -&gt; transient(new): 새로 작성된 엔티티가 영속성 컨텍스트에 담기기 전 상태</li>
<li>영속 -&gt; managed: 영속성 컨텍스트에 담겨 관리되는 상태</li>
<li>준영속 -&gt; detached: 영속성 컨텍스트에 저장되었다가 분리된 상태</li>
<li>삭제 -&gt; removed: 영속성 컨텍스트에 저장되었다가 삭제된 상태</li>
</ul>
</li>
</ul>
<br>
<br>


<h3 id="영속성-컨텍스트--emf-em">영속성 컨텍스트 (+ EMF, EM)</h3>
<hr>
<pre><code class="language-kotlin">@GetMapping(&quot;/practice1&quot;)
fun practice1() {
    val emf = Persistence.createEntityManager(&quot;hi&quot;)
    val em = emf.createEntityManager()

    // 객체를 생성한 상태 (비영속)
    val member = Member()
    member.id = 101L
    member.title = &quot;테스트&quot;

    val transaction = em.transaction
    transaction.brgin()

    println(&quot;BEFORE&quot;)
    em.persist(member)     // 객체를 저장한 상태(영속)
    println(&quot;AFTER&quot;)

    transaction.commit()

    em.close()
    emf.close()</code></pre>
<p>실행 순서: BEFORE -&gt; AFTER -&gt; insert member</p>
<p>println(&quot;BEFORE&quot;) 보다 먼저 트랜잭션이 시작되면서 DB 와의 커넥션이 열렸지만,</p>
<p>member를 1차 캐시에 올려놓은 이후지만, </p>
<p>println(&quot;AFTER&quot;) 프린트문 출력 이후에 커밋이 되면서 member 엔티티를 영속성 컨텍스트에 저장하는 순서가 제일 마지막이 되었다. </p>
<p>=&gt; member가 1차 캐시에 가만히 있다가 커밋을 만나면 DB로 들어가게 된다.</p>
<br>
<br>
<br>


<h3 id="1차-캐시">1차 캐시</h3>
<hr>
<pre><code class="language-kotlin">@GetMapping(&quot;/practice2&quot;)
    fun practice2(){

        // EntityManager 생성
        val emf = Persistence.createEntityManagerFactory(&quot;hello&quot;)
        val em = emf.createEntityManager()


        //객체를 생성한 상태(비영속)
        val member = Member()
        member.id = 1001L
        member.title = &quot;회원&quot;

        val transaction = em.transaction
        transaction.begin()

        //1차 캐시에 저장됨
        em.persist(member) // insert

        //3번 멤버 조회
        val member1 = em.find(Member().javaClass, 103L)  // select

        println(&quot;조회 결과 : &quot; + member1.title) // println

        transaction.commit()
        em.close()
        emf.close()
    }</code></pre>
<p>실행순서: select member1 -&gt; insert member</p>
<p>1001번의 회원을 1차 캐시에 저장했다.</p>
<p>em.find 로 103번의 멤버를 조회하게 되어 103번의 member1을 1차 캐시에 올려 먼저 찾은 다음, </p>
<p>100번의 member가 1차 캐시에 다시 올려놓아지고 커밋시 DB에 저장된다.(이 일을 모두 엔티티 매니저가 한다)</p>
<br>
<br>
<br>

<h3 id="영속성-엔티티의-동일성-보장">영속성 엔티티의 동일성 보장</h3>
<hr>
<pre><code class="language-kotlin">@GetMapping(&quot;/practice3&quot;)
    fun practice3(){

        // EntityManager 생성
        val emf = Persistence.createEntityManagerFactory(&quot;hello&quot;)
        val em = emf.createEntityManager()

        val transaction = em.transaction
        transaction.begin()

        //103번 멤버 조회
        val member1 = em.find(Member().javaClass, 103L)
        //103번 멤버 조회
        val member2 = em.find(Member().javaClass, 103L)

        println(&quot;비교 결과 : &quot; + (member1 === member2))

        transaction.commit()
        em.close()
        emf.close()

    }</code></pre>
<p>두개는 같은 것일까? 같은 것이 맞다. </p>
<p>1차 캐시에 있던 103번의 Member를 1차 캐시에서 또 조회하게 된다. 성능상 이점이 있다.</p>
<br>
<br>
<br>

<h3 id="트랜잭션이-지원하는-쓰기지연-transactional-write-behind">트랜잭션이 지원하는 쓰기지연 Transactional write-behind</h3>
<hr>
<pre><code class="language-kotlin"> @GetMapping(&quot;/practice4&quot;)
    fun practice4(){

        // EntityManager 생성
        val emf = Persistence.createEntityManagerFactory(&quot;hello&quot;)
        val em = emf.createEntityManager()

        val transaction = em.transaction
        transaction.begin()


        val member1 = Member()
        member1.id = 10000L
        member1.title = &quot;회원님1&quot;

        val member2 = Member()
        member2.id = 10001L
        member2.title = &quot;회원님2&quot;

        // 이 때 Insert 쿼리를 보내게 될까?
        em.persist(member1)
        em.persist(member2)


        println(&quot;=============================&quot;)

        transaction.commit()
        em.close()
        emf.close()
    }</code></pre>
<p>실행순서: println(&quot;=============================&quot;) -&gt; insert member1 -&gt; insert member2</p>
<p>영속성 컨텍스트 내 쓰기 지연 SQL 저장소가 있는데,</p>
<p>엔티티 매니저는 transaction을 commit하기 직전까지 내부 쿼리 저장소인 쓰기 지연 SQL 저장소에 쿼리문을 원기옥처럼 모아놓는다. </p>
<p>트랜잭션 커밋시 이 쿼리들이 DB로 한번에 뿌려진다. </p>
<p>트랜잭션이 커밋될때 한꺼번에 데이터베이스에 적용된다.</p>
<p>이것을 쓰기 지연 (Transactional write-behind)이라고 한다.</p>
<br>
<br>
<br>

<h3 id="변경-감지-dirty-checking">변경 감지 Dirty Checking</h3>
<hr>
<pre><code class="language-kotlin">@GetMapping(&quot;/practice5&quot;)
    fun practice5(){
        // EntityManager 생성
        val emf = Persistence.createEntityManagerFactory(&quot;hello&quot;)
        val em = emf.createEntityManager()

        val transaction = em.transaction
        transaction.begin()

        //103번 멤버 조회
        val member1 = em.find(Member().javaClass, 103L)
        member1.title = &quot;바뀔까&quot;

        transaction.commit()
        em.close()
        emf.close()
    }</code></pre>
<p>실행순서: 103번의 Member 조회 -&gt; 데이터 변경 -&gt; 커밋 시 적용됨</p>
<p>업데이트 쿼리문이 자동생성돼 member1의 title 이 &#39;바뀔까&#39;로 바뀐다.</p>
<p>이 때 Dirty Checking(변경감지)이 이용된다.</p>
<p>103번 Member 엔티티가 엔티티 매니저에 의해 1차 캐시에 올려지면서 영속성 컨텍스트에 들어올 때 JPA는 최초의 엔티티 상태를 복사해 스냅샷에 저장해놓는다.</p>
<p>트랜잭션 커밋시 엔티티 매니저가 내부에서 먼저 flush()를 호출한다.</p>
<blockquote>
<p>flush(): 영속성 컨텍스트의 변경내용을 DB와 동기화, 싱크를 맞추는 역할.</p>
</blockquote>
<p>엔티티와 최초의 스냅샷이 다르다면 변경내용(member1.title = &quot;바뀔까&quot;)을 Update 쿼리로 자동생성해 쓰기 지연 SQL 저장소에 저장한다.</p>
<p>쓰기 지연 저장소의 업데이트 쿼리가 발동돼 엔티티의 변경사항이 DB에 적용되고 트랜잭션이 끝나고 커밋된다. </p>
<p><img src="https://velog.velcdn.com/images/miso_/post/c6040655-15c0-49f9-bf90-c3b795e5492e/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA EntitiyManager(+EMF)]]></title>
            <link>https://velog.io/@miso_/entitiy-manager</link>
            <guid>https://velog.io/@miso_/entitiy-manager</guid>
            <pubDate>Sat, 20 Jan 2024 14:50:11 GMT</pubDate>
            <description><![CDATA[<h3 id="entitiymanager-와-entitiymanagerfactory-특징">EntitiyManager 와 EntitiyManagerFactory 특징</h3>
<hr>
<ul>
<li><p>엔티티 매니저 팩토리</p>
<ul>
<li><p>엔티티 매니저 팩토리에서 필요할 때마다 여러 개의 엔티티 매니저를 생성해준다.</p>
</li>
<li><p>셍성비용이 크다.</p>
</li>
<li><p>한개만 만들어 프로그램 전체에서 공유하도록 설계되어 있다.</p>
</li>
<li><p>여러 스레드가 동시에 접근해도 안전하게 설계되었다.</p>
</li>
</ul>
</li>
</ul>
<br>

<ul>
<li><p>엔티티 매니저</p>
<ul>
<li><p>생성 비용이 적다.</p>
</li>
<li><p>여러 스레드가 동시에 접근하면 동시성 문제가 발생한다(공유 불가)</p>
</li>
<li><p>DB 커넥션 풀에서 연결이 필요한 시점에 커넥션을 얻어 사용한다.</p>
</li>
</ul>
</li>
</ul>
<br>
<br>
<br>
<br>

<h3 id="엔티티-매니저뿐만이-아니라-엔티티-매니저-팩토리까지-쓰는-이유는-무엇일까">엔티티 매니저뿐만이 아니라 엔티티 매니저 팩토리까지 쓰는 이유는 무엇일까?</h3>
<hr>
<p>동시성 문제: 동일한 자원에 대해 여러 스레드가 동시에 접근하면서 발생하는 문제</p>
<p><img src="https://velog.velcdn.com/images/miso_/post/070323e9-1f3c-4720-a888-bcc6055e2851/image.png" alt=""></p>
<p>프로그램들이 성능을 위해서 여러 스레드들이 일을 같이 하게 되어 있는데, 하나의 큰 일을 동시에 처리하려다 보면 동시성 문제가 발생할 수 있다.</p>
<p>엔티티 매니저 팩토리는 Thread-safe 하기에 여러 스레드에서 접근해도 안전하다. </p>
<p>하지만 특징에 적힌 것처럼 생성하는데 많은 비용을 초래한다. </p>
<p>그래서 하나의 팩토리를 생성해서 공유하는 방식을 택한다. </p>
<p>비용이 거의 들지 않는 엔티티 매니저를 요청이 들어올 떄마다 1개씩 생성해준다. </p>
<p>하지만 엔티티 매니저 또한 여러 스레드가 동시에 접근하면 문제가 발생한다. </p>
<p>그렇기에 스레드 간에 공유할 수 없다.</p>
<p><img src="https://velog.velcdn.com/images/miso_/post/b9a00045-0230-4812-82b6-d7109feaf85d/image.png" alt=""></p>
<p>요청이 1개 들어올 떄마다 엔티티 매니저 팩토리에서 요청 1개당 매니저 1개를 붙여준다. </p>
<p>이로 인해 동시성 문제가 발생되지 않는다. </p>
<p>이 엔티티매니저는 내부적으로 DB 와 커넥션을 맺는다.</p>
<p>해당 그림에서 요청1 과 요청2 는 DB 연결이 필요하기 때문에 Connection Pool 과 연결되어 있지만, DB 연결이 필요하지 않다면 꼭 필요한 시점까지 DB 와 connection 을 하지 않는다. </p>
<p>이 connection 은 보통 트랜잭션을 시작할 때 획득한다. </p>
<br>
<br>
<br>
<br>

<h3 id="entitymanager">EntityManager</h3>
<hr>
<p>엔티티를 데이터 베이스에 CRUD 하는 역할이며 엔티티와 관련된 일을 수행하는 엔티티 관리자이다.</p>
<br>

<p> @PersistenceContext 어노테이션을 통해 주입</p>
<pre><code class="language-kotlin">abstract class QueryDslSupport {

    // EntityManager 필요할 때 @PersistenceContext 이용
    @PersistenceContext
    protected lateinit var entityManager: EntityManager


    // 다른 곳에서도 쓸 수 있게 쿼리 팩토리를 만들어야 한다.
    protected val queryFactory: JPAQueryFactory
        get() {
            return JPAQueryFactory(entityManager)
        }
}</code></pre>
<br>

<p>Todo Application 에서 infra 패키지 내에 QueryDslSupport 추상 클래스 내 EntityManager 가 생성돼서 담겨 있는데, </p>
<p><img src="https://velog.velcdn.com/images/miso_/post/93305348-36bf-439e-a263-7d45ebb4ac67/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/miso_/post/e24b5117-7b9a-4905-b9f5-ed7023002c88/image.png" alt=""></p>
<p>각각의 리포지토리 구현체에 담겨 클래스 내에 작성된 쿼리문에 맞게 EntityManager 가 엔티티를 CRUD 해준다.</p>
<p>이 EntityManager 는 싱글톤이기에 동시성 문제가 발생할 수도 있는데 스프링에서는 문제없이 잘 실행됐다.</p>
<p>그 이유가 무엇일까?</p>
<p>스프링 프레임워크는 여기에 실제 EntityManager 를 주입하는 것이 아니라 실제 EntityManager 를 연결해주는 가짜 프록시 EntityManager 를 주입해둔다. </p>
<p>그리고 이 EntityManager 를 호출하면 현재 데이터베이스 트랜잭션과 관련된 실제 EntityManager 를 호출해준다.</p>
<p>이 덕분에 Swagger에서 QueryDsl 을 이용한 api 가 잘 excute 된 것이었다.</p>
<br>
<br>

<ul>
<li>생성자 주입을 했을시에도 자동적으로 주입 가능</li>
</ul>
<br>
<br>
<br>
<br>
<br>
<br>


<p>참고:</p>
<p><a href="https://velog.io/@gksrywls97/EntityManager-EntityManagerFactory">https://velog.io/@gksrywls97/EntityManager-EntityManagerFactory</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring QueryDsl fetch(), fetchOne(),  fetchFirst() ]]></title>
            <link>https://velog.io/@miso_/Spring-QueryDsl-fetch-fetchOne-fetchFirst</link>
            <guid>https://velog.io/@miso_/Spring-QueryDsl-fetch-fetchOne-fetchFirst</guid>
            <pubDate>Fri, 19 Jan 2024 12:29:43 GMT</pubDate>
            <description><![CDATA[<ol>
<li>fetch() :  리스트로 결과를 반환하는 방법. (만약에 데이터가 없으면 빈 리스트를 반환)</li>
</ol>
<hr>
<ol start="2">
<li>fetchOne()</li>
</ol>
<ul>
<li>fetchOne()은 getSingleResult(query)를 반환한다.</li>
<li>getSingleResult()는 <strong>결과가 여러 건일 때 NonUniqueResultException을 throw한다.</strong></li>
<li>이로 인해, fetchOne()을 사용하게 되면 결과가 여러 건이면 <strong>NonUniqueResultException이 발생한다.</strong></li>
</ul>
<hr>
<ol start="3">
<li>fetchFirst() </li>
</ol>
<ul>
<li>fetchFirst()는 limit(1).fetchOne()을 반환한다.</li>
<li>즉, <strong>limit(1)을 통해 미리 결과를 단건으로 치환하고 fetchOne()을 수행한다.</strong></li>
<li>이로 인해, fetchFirst()을 사용하게 되면 <strong>결과가 여러 건이어도 내부적으로 미리 limit(1)을 수행하여 가장 위의 한 건만 조회된다.</strong></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[@Component, 
@Configuration 차이]]></title>
            <link>https://velog.io/@miso_/Component-Configuration-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@miso_/Component-Configuration-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Wed, 17 Jan 2024 13:39:04 GMT</pubDate>
            <description><![CDATA[<p>@Component </p>
<ul>
<li>개발자가 직접 작성한 클래스를 빈으로 등록하고 싶을 때 사용</li>
</ul>
<p>@Configuration</p>
<ul>
<li>개발자가 직접 제어가 불가능한 외부 라이브러리 또는 설정을 위한 클래스를 Bean으로 등록할 때 사용</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring & Kotlin AOP]]></title>
            <link>https://velog.io/@miso_/Spring-Kotlin-AOP</link>
            <guid>https://velog.io/@miso_/Spring-Kotlin-AOP</guid>
            <pubDate>Tue, 16 Jan 2024 12:36:21 GMT</pubDate>
            <description><![CDATA[<h3 id="위에서-보면-oop-옆에서-보면-aop">위에서 보면 OOP 옆에서 보면 AOP</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/miso_/post/9d69482c-57cc-4f44-8b2b-d5d05c080650/image.png" alt=""></p>
<h4 id="트랜잭셔널을-이용해서-서비스를-오가며-사용">트랜잭셔널을 이용해서 서비스를 오가며 사용</h4>
<br>
<br>
<br>
<br>


<h3 id="어플리케이션의-진행-단계">어플리케이션의 진행 단계</h3>
<hr>
<ul>
<li><strong>Aspect</strong><ul>
<li>횡단 관심사(부가기능)를 모듈화한 단위이다. Aspect는 부가기능을 정의하는 Advice와 적용 위치를 결정하는 PointCut으로 구성된다.</li>
</ul>
</li>
<li><strong>PointCut</strong><ul>
<li>Aspect가 적용될 프로그램상 실제 위치이다.</li>
</ul>
</li>
<li><strong>JoinPoint</strong><ul>
<li>PointCut의 후보군이다. Aspect가 적용될 수 있는 위치들을 말한다. method가 호출되는 시점, 특정 class의 생성자가 호출되는 시점, exception이 발생하는 시점 등이 될 수 있다.</li>
</ul>
</li>
<li><strong>Advice</strong><ul>
<li>실질적으로 부가 기능 로직이 정의되어있는 객체라고 볼 수 있다.</li>
</ul>
</li>
<li><strong>Weaving</strong><ul>
<li>Aspect를 실제 코드에 적용하는 과정을 나타낸다.</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/miso_/post/feac6ae1-2082-4f2a-94da-3c1108afe0b8/image.png" alt=""></p>
<p>point cut 과 adivice를 합쳐서 하나의 Aspect라고 한다.</p>
<br>
<br>

<h3 id="weaving--실제-코드에-적용하는-과정">Weaving : 실제 코드에 적용하는 과정</h3>
<h3 id="대표적인-두-가지-방식">&lt;대표적인 두 가지 방식&gt;</h3>
<hr>
<h4 id="☑️--spring-aop-vs-aspectj">☑️  Spring AOP vs AspectJ</h4>
<p>두 프레임워크의 큰 차이는 weaving 방법에 있다.</p>
<br>

<h4 id="1-aspectj">1. AspectJ</h4>
<ul>
<li>Compile-time Weaving : </li>
</ul>
<p>AOP가 적용된걸 확인하고 컴파일 시점에 우리가 작성한 클래스가 변경된다.</p>
<ul>
<li>Load-time Weaving : </li>
</ul>
<p>Java기반 프로그램은 실행되기 전 JVM에서 클래스를 로딩하는 시점이 있다. 자바 컴파일러(java Complier)가 자바 소스파일을 컴파일하고자바 바이트코드로 JVM (자바 가상머신)이 이해가능한 파일로 만든다. 이때, 변경된 바이트 코드를 사용하게 함으로써, 원본 클래스는 변경하지 않고, AOP를 적용하는 방식.</p>
<p>-&gt; 실행 시에 모든 클래스가 로딩되지 않고 필요한 시점에 클래스를 로딩하여 사용할 수 있다</p>
<br>
<br>

<h4 id="2-spring-aop-가-사용하는-run-time-weaving">2. Spring AOP 가 사용하는 Run-time Weaving</h4>
<p>객체에 직접 접근하는게 아닌 중간에 프록시를 두고 프록시를 통해 객체에 접근해 AOP가 적용된다.</p>
<p><img src="https://velog.velcdn.com/images/miso_/post/d683e9f5-3f13-4d1a-a1dd-34c0545e3a56/image.png" alt=""></p>
<p>슈퍼클래스 ClassB의 서브클래스 ClassB Proxy</p>
<p>AspectJ는 Compile-time Weaving, Load-time Weaving으로 AOP를 구현하고,</p>
<p>Spring AOP는 Run-time Weaving 을 사용해 AOP를 구현한다.</p>
<p>스프링 AOP가 프록시를 거쳐야하기에 오버헤드가 있고 AspectJ보다 느리다.</p>
<p>AspectJ는 모든 부분에 JoinPoint 적용이 가능하다. 하지만 스프링AOP 는 프록시 패턴을 이용하기 때문에 항상 프록시에 해당하는 sub class를 만들어야 한다. 이때 Aspect(PointCut + Advice)를 적용하려는 class가 final class(상속을 못받는 클래스) 일때는 적용이 되지 않는다. Java의 final or static method에도 적용이 되지 않는다. </p>
<br>
<br>
<br>
<br>
<br>

<h3 id="규칙-2개-존재">규칙 2개 존재</h3>
<hr>
<p>a. Spring AOP는 일반 method만을 JointPoint로 사용할 수 있다. </p>
<p>b. 또 스프링 프레임워크이니까 스프링에서 관리하는 Bean에서만 작동한다.</p>
<br>

<p>속도가 빨라 성능면에서는 AspectJ가 좋지만, weacing 방법 두가지 중 하나를 골라야 하고, 별도의 컴파일러나 weaver가 필요하기 때문에 Spring AOP에 비해 매우 복잡하다. Spring AOP는 간단하게 사용이 가능하다.</p>
<br>
<br>
<br>
<br>
<br>

<h3 id="적용해보기">&lt;적용해보기&gt;</h3>
<hr>
<p>어플리케이션 상단에 적기
@EnableAspectJAutoProxy</p>
<p>Spring AOP인데 AspectJ 이름이 붙은 이유는 AspectJ를 차용해서 만들었기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/miso_/post/4d2968db-2473-444b-bbae-79ac9b527e77/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/miso_/post/a81c7a88-5461-4dd3-ba85-3e511fb3d9be/image.png" alt=""></p>
<p>이 클래스 자체가 AOP가 되는 것이다. 포인트 컷과 어드바이스가 적혀있는 하나의 객체.</p>
<p>여기에 이제 부가기능을 모듈화 한다.</p>
<p><img src="https://velog.velcdn.com/images/miso_/post/610bce9a-e74d-4655-b28a-93828867d6a4/image.png" alt=""></p>
<p>Aspect니까 어노테이션 붙이고, 구성요소 빈 등록 어노테이션 붙인다.</p>
<ul>
<li><code>@Around</code> 는 Advice의 적용 시점 중 하나이다. JoinPoint를 기준으로, Advice가 언제 동작할지를 정의한다. 아래 5가지가 있다.<ul>
<li><code>@Around</code> : 메서드 실행 전후로 동작.</li>
<li><code>@Before</code> : 메서드가 호출되기 전에 Advice가 실행.</li>
<li><code>@After</code> : 메서드 결과와 관계없이, 메서드가 완료되면 Advice가 실행.</li>
<li><code>@AfterReturning</code> : 메서드가 정상적으로 반환 했을시에만 Advice가 실행.</li>
<li><code>@AfterThrowing</code> : 메서드가 예외를 발생시킬때 Advice가 실행.</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/miso_/post/cbb7e55e-519a-40ba-8982-765c926debde/image.png" alt=""></p>
<p>AOP가 잘 적용됨</p>
<hr>
<p>먼저 어노테이션 클래스를 만들고, </p>
<p><img src="https://velog.velcdn.com/images/miso_/post/d19e2461-0a81-479c-8e33-ec166d27e1fa/image.png" alt=""></p>
<pre><code class="language-kotlin">@Target(AnnotationTarget.FUNCTION)  // Target 은 어노테이션이 적용될 대상을 의미 CLASS : 어떤 클래스에 적용, PROPERTY : 어떤 프로퍼티에 적용, ANNOTATION_CLASS : 다른 어노테이션 클래스에 적용
// 지금은 메서드의 수행시간을 알아보는 것이기에 FUNCTION 지정
@Retention(AnnotationRetention.RUNTIME)
annotation class StopWatch()</code></pre>
<p><img src="https://velog.velcdn.com/images/miso_/post/b7cf5d8a-85da-4ec4-b1bf-eecb684fd855/image.png" alt=""></p>
<hr>
<p>AOP를 작성하고,</p>
<p><img src="https://velog.velcdn.com/images/miso_/post/0df4c2cd-38a6-4a0c-83bd-dc9869ac6ebf/image.png" alt=""></p>
<hr>
<p>시간을 측정할 api에 @StopWatch 어노테이션을 달고 swagger를 실행해 아이디를 조회하면,</p>
<p><img src="https://velog.velcdn.com/images/miso_/post/43abba72-66df-4afa-bb49-a60c5d843668/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/miso_/post/24e0491f-b9dd-4dff-9403-18fa1a91d17a/image.png" alt=""></p>
<p>스탑워치는 특정 api가 너무 오래걸려서 어디가 오버헤드가 있는지 측정할 수 있다. 
그래서 모든 api에 스탑워치를 달면 어디가 느리게 실행되는지 알 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[뉴스피드 과제 - 스프링 코틀린 페이지네이션]]></title>
            <link>https://velog.io/@miso_/%EB%89%B4%EC%8A%A4%ED%94%BC%EB%93%9C-%EA%B3%BC%EC%A0%9C-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BD%94%ED%8B%80%EB%A6%B0-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98</link>
            <guid>https://velog.io/@miso_/%EB%89%B4%EC%8A%A4%ED%94%BC%EB%93%9C-%EA%B3%BC%EC%A0%9C-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BD%94%ED%8B%80%EB%A6%B0-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98</guid>
            <pubDate>Mon, 15 Jan 2024 12:17:54 GMT</pubDate>
            <description><![CDATA[<h3 id="게시판-페이지네이션">게시판 (페이지네이션)</h3>
<p><img src="https://velog.velcdn.com/images/miso_/post/7dca5693-4f4d-4da9-a011-018e7dae7582/image.png" alt=""></p>
<p>컨트롤러에서 전체 데이터의 개수를 세는 count 쿼리가 포함된 Page 객체를 반환타입으로 받았다.</p>
<br>


<hr>
<br>

<p><img src="https://velog.velcdn.com/images/miso_/post/fb4ea7fa-d355-43bd-bc9b-413740ac03d4/image.png" alt=""></p>
<p>서비스 구현에서 Pageable 인터페이스를 상속받아 Pageable의 구현체 PageRequest 생성자의 파라미터에 현재페이지, 조회할 데이터 수, 정렬 정보를 넣었다.</p>
<p>Offset 방식을 사용해 limit 예약어를 통하여 현재 페이지에만 5개의 데이터가 조회 되도록 했다. 또 최근 게시글을 발행한 아이디 순서로 내림차순 정렬했다.</p>
<p>컨트롤러에서 반환값이 BoardListResponse 이기 때문에 map을 사용해서 페이징된 Board 객체들을 리스트화 했다.</p>
<br>


<hr>
<br>

<p><img src="https://velog.velcdn.com/images/miso_/post/bfc473ff-fbcc-4949-a79a-87a447770426/image.png" alt=""></p>
<p>레포지토리에 findAllByOrderByCreatedAtDesc() 메서드를 생성하고 Pageable 인터페이스를 상속받은 변수 pageable을 파라미터로 전달하고 요청한 페이지의 정보를 반환받을 수 있도록 Board를 Page객체로 만들어 반환했다.</p>
<br>
<br>

<hr>
<br>

<h4 id="pageable-파라미터-정보-없이-요청한-경우">pageable 파라미터 정보 없이 요청한 경우</h4>
<p><img src="https://velog.velcdn.com/images/miso_/post/7deedc65-7a23-4b47-bfac-3a2c116e4cb0/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>