<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>sang_hyeok_2.log</title>
        <link>https://velog.io/</link>
        <description>꾸준히!</description>
        <lastBuildDate>Mon, 18 Nov 2024 13:07:42 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. sang_hyeok_2.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/sang_hyeok_2" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Project] Facade 패턴으로 의존성 개선하기]]></title>
            <link>https://velog.io/@sang_hyeok_2/Project-Facade-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EC%9D%98%EC%A1%B4%EC%84%B1-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sang_hyeok_2/Project-Facade-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EC%9D%98%EC%A1%B4%EC%84%B1-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 18 Nov 2024 13:07:42 GMT</pubDate>
            <description><![CDATA[<h3 id="service에서-코드-리팩토링의-필요성">Service에서 코드 리팩토링의 필요성</h3>
<p>프로젝트를 진행을 하면서 event의 service단을 작업을 하고 있었습니다. event를 생성하고 수정을 하는 과정에서 필드의 다른 엔티티들이 필요로 하다보니 event의 service단에서 repository들을 의존을 하는 것이 굉장히 컸습니다. 의존을 많이 하다가 보니 유지 보수도 힘들어진다고 느끼게 되었습니다.</p>
<p>무엇보다도 객체지향적이지 않았습니다. 우리는 객체지향을 사용을 하고 있습니다. 하지만 repository의 의존이 많아지면서 event의 service단이 너무 많은 책임을 지면서 객체지향을 활용하지 못하고 있다고 생각이 들었습니다. 그래서 service단의 책임을 줄이고 하나의 책임을 가지게 하고 싶었습니다.</p>
<h3 id="그러면-왜-facade-패턴인가">그러면 왜 Facade 패턴인가?</h3>
<p>저는 이 문제를 해결을 하기 위해서 Facade 패턴을 적용을 하기로 했습니다. 왜 Facade 패턴인지를 말하기 전에 Facade 패턴이 무엇인지부터 간단하게 알아보겠습니다. </p>
<h4 id="facde-패턴">Facde 패턴</h4>
<p>Facade패턴은 구조적 디자인 패턴 중 하나입니다. Facade패턴은 복잡한 일을 쉽게 사용할 수 있도록 해주는 디자인 패턴입니다. 우리가 만약 컴퓨터를 실행을 시키려고 전원 버튼을 누르면 운영체제가 돌아가고 네트워크가 연결이 되고 여러 프로그램이 실행이 됩니다. 이렇게 하나의 동작으로 다양한 일들이 일어납니다. 이렇게 하나의 동작으로 많은 일과 복잡한 일을 해주는 것이 Facade 패턴입니다. </p>
<p>프로그래밍에서도 서브시스템 클래스들, 즉 실제 작업을 수행하는 클래스들을 Facade에서 클라이언트와 상호작용을 하는 인터페이스에서 서브시스템 클래스들이 상호작용을 할 수 있도록 해주는 역할을 합니다.</p>
<h4 id="왜-facade-패턴을-사용하는가">왜 Facade 패턴을 사용하는가?</h4>
<p>Facade를 사용하는 이유는 service의 책임을 낮추기 위해서 입니다. service에서 controller에게 결과를 넘겨 줄 때, 다른 repository가 필요한 경우가 있습니다. 그러다보니 service가 다른 repository를 책임지는 경우가 발생을 합니다. 결국 service의 책임을 낮춰주어야 하는데 이 책임을 낮출 수 있는 패턴이 Facade 패턴입니다. Facade를 만들어서 service가 해줘야 하는 책임을 대신 지게 해주기 위해서입니다. 이렇게 되면 service는 하나의 repository만을 책임을 지면 solid원칙 중 단일 책임의 원칙 또한 지킬 수 있습니다.</p>
<h3 id="facade-패턴-적용을-해보기">Facade 패턴 적용을 해보기</h3>
<p>이제 Facade 패턴 전에 코드를 보면서 문제를 살펴보기 Facade 패턴을 적용을 하면서 개선을 해보겠습니다.</p>
<h4 id="개선-전">개선 전</h4>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class EventService {
    private final EventRepository eventRepository;
    private final EventLocationRepository eventLocationRepository;
    private final DeletedEventRepository deletedEventRepository;

    @Transactional
    public EventEntity createEvent(EventRequestDTO dto) {
        EventLocationEntity eventLocationEntity = getEventLocationEntity(dto.getEventLocationId());
        EventEntity newEventEntity = new EventEntity(dto, eventLocationEntity);
        return eventRepository.save(newEventEntity);
    }

    @Transactional
    public EventEntity deletedEvent(Long eventId) {
        EventEntity eventEntity = getEventEntity(eventId);
        DeletedEventEntity deletedEventEntity = new DeletedEventEntity(eventEntity);
        deletedEventRepository.save(deletedEventEntity);
        eventRepository.delete(eventEntity);
        return eventEntity;
    }


    private EventLocationEntity getEventLocationEntity(Long eventLocationId) {
        return eventLocationRepository.findById(eventLocationId).orElseThrow(() -&gt;
                new GlobalCommonException(EventLocationErrorResponsive.NOT_FOUND_EVENT_LOCATION)
        );
    }

}</code></pre>
<p>지금 개선 전에 일부 코드를 보면 service 클래스에서 event를 생성, 삭제를 하면서 필요한 repository를 책임을 지면서 사용을 하고 있습니다. 그러면서 solid의 원칙 중 하나인 단일 책임 원칙을 위배하면서 객체지향적이지 않은 코드가 됩니다. 또 많은 책임으로 인해서 코드도 복잡해졌습니다. 이를 Facade 패턴을 사용을 해서 개선을 해보겠습니다.</p>
<h4 id="개선-후">개선 후</h4>
<p>먼저, service단을 개선을 해보겠습니다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class EventService {

    private final EventRepository eventRepository;

    public EventEntity createEvent(EventRequestDTO dto,
                                   EventLocationEntity eventLocationEntity) {
        EventEntity newEventEntity = new EventEntity(dto, eventLocationEntity);
        return eventRepository.save(newEventEntity);
    }

    public EventEntity deletedEvent(Long eventId) {
        EventEntity eventEntity = getEventById(eventId);
        eventRepository.delete(eventEntity);

        return eventEntity;
    }
}</code></pre>
<p>service에서는 단 하나의 repository만을 책임을 집니다. 여기서는 event repository를 책임을 집니다. 이전 개선이 되기 전에 코드보다 간단해졌고 보기도 편해졌습니다. </p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class EventFacade {

    private final EventService eventService;
    private final EventLocationService eventLocationService;
    private final DeletedEventService deletedEventService;

    @Transactional
    EventEntity createEvent(EventRequestDTO dto) {
        EventLocationEntity eventLocationEntity = eventLocationService.getEventLocation(dto.getEventLocationId());
        CategoryEntity categoryEntity =  categoryService.getCategory(dto.getCategoryId());
        EventEntity eventEntity = eventService.createEvent(dto, eventLocationEntity);

        categoryEventService.createCategoryEvent(categoryEntity, eventEntity);
        return eventEntity;
    }

    @Transactional
    EventEntity deletedEvent(Long eventId) {
        EventEntity eventEntity = eventService.deletedEvent(eventId);
        deletedEventService.createDeletedEvent(eventEntity);

        return eventEntity;
    }

}</code></pre>
<p>Facade를 만들어서 Facade가 service가 지고 있던 책임을 가지고 클래스 안에서 복잡한 작업을 대신 해주는 역할을 합니다. 이런한 방식의 service에 과중되어 있던 책임을 나누고 service에서 단일 책임 원칙을 지킬 수 있게 되었습니다. 이제 복잡한 것들은 Facade에서 조합을 통해서 처리를 할 수 있게 되었습니다.</p>
<h3 id="마무리">마무리</h3>
<p>이번 포스트에서는 가독성과 객체지향을 지키기 위해서 Facade 패턴을 적용을 해보았습니다. 사실 이전까지 디자인 패턴에 대한 중요성을 크게 느끼지 못 하고 있었는데 이번 계기를 통해서 디자인 패턴이 가독성과 더 나아가 객체지향을 위해서 사용이 될 수 있다는 것을 느낄 수 있었습니다. 그리고 새롭게 적용이 된 것을 보면서 개발의 즐거움을 느낄 수 있었습니다. 코드 또한 더 간단해지고 객체지향적이어지고 가독성이 좋아졌습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] 프로젝트에서 Test Fixture를 어떻게 사용할 지 고민해보기]]></title>
            <link>https://velog.io/@sang_hyeok_2/Project-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-Test-Fixture%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%A0-%EC%A7%80-%EA%B3%A0%EB%AF%BC%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@sang_hyeok_2/Project-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-Test-Fixture%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%A0-%EC%A7%80-%EA%B3%A0%EB%AF%BC%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 18 Nov 2024 06:37:04 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<p>프로젝트를 진행을 하면서 테스트 코드를 작성을 해왔습니다. 그러면 자연스럽게 테스트를 위한 Fixture를 쓰게 되었습니다. 사용을 하면서 아무 생각 없이 사용을 하고 있었습니다. 그런데 사용을 하면서 Fixture를 작성을 하면서 코드가 길어지고 가독성이 떨어지는 것을 느끼게 되었습니다. 그래서 Test Fixture를 어떻게 사용을 하면 좋은 지 고민을 하게 되면서 공부를 하게 되었습니다.</p>
<h3 id="test-fixture가-무언가요">Test Fixture가 무언가요?</h3>
<p>그러면 위에서 언급을 한 Test Fixture는 무엇일까요? 
Test Fixture는 테스트를 하기 전에 테스트에 필요한 환경이나 상태를 설정을 하는 것을 말합니다. 예를 들어 보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/2025de06-e4c2-40b5-ad58-44b8d4890b4b/image.png" alt=""></p>
<p>만약 제가 event를 생성을 하는 test를 한다고 한다면 category의 정보와 event location, 이벤트 장소의 정보를 필요로 합니다. 즉, event 생성 test 전에 미리 그 정보를 설정을 해주어야 한다는 것이지요. 이렇게 test를 하기 전에 필요한 정보나 환경을 설정을 해주는 것입니다. </p>
<p>여기서 한 가지 중요한 것은 테스트 환경을 일관성 있게 고정된 환경으로 제공을 해야 한다는 것입니다. 이 말 뜻은 모든 테스트가 동일한 환경에서 독립적으로 실행이 될 수 있어야 한다는 의미입니다. 만약에 계속 변동이 되는 환경이 주어지게 된다면 이는 테스트에 대한 신뢰성이 떨어지게 되고 다른 환경을 제공을 해야 하니 중복 되는 코드가 늘어나게 됩니다. 이는 좋은 테스트라고 볼 수 없습니다. </p>
<p>그러므로 Test Fixture를 만들어서 일관되고 고정된 환경을 모든 테스트에 적용을 한다면 코드 중복이 줄어들고 테스트의 신뢰성을 높힐 수 있습니다.</p>
<h3 id="test-fixture를-사용해보기">Test Fixture를 사용해보기</h3>
<p>먼저, 기존의 사용을 했던 Test Fixture를 살펴보겠습니다.
간단하게 상황을 설명을 하자면 event를 생성하는 테스트와 수정하는 테스트를 진행을 하고자 합니다. 이 때, 두 테스트에 모두 필요로 하는 것이 eventLocation입니다. 그리고 event 또한 필요합니다.</p>
<pre><code class="language-java">@DisplayName(&quot;이벤트를 생성하면, 생성이 된 이벤트의 정보가 나온다.&quot;)
@Test
void createEventTest() {
    // given
    EventRequestDTO dto = EventRequestDTO.builder()
        .eventLocationId(1L)
        .eventName(&quot;대한민축 vs 일본 축구 친선경기&quot;)
        .eventDescription(&quot;축구 경가&quot;)
        .date(LocalDate.of(2024, 10, 15))
        .startTime(LocalTime.of(8, 0))
        .build();

    EventLocationEntity eventLocationEntitySeoul = new EventLocationEntity(1L, &quot;서울 월드컵 경기장&quot;, 50000);
    EventEntity eventEntity = EventEntity.builder()
                .eventId(1L)
                .eventLocationEntity(eventLocationEntitySeoul)
                .eventName(&quot;대한민축 vs 일본 축구 친선경기&quot;)
                .eventDescription(&quot;축구 경기&quot;)
                .date(LocalDate.of(2024, 10, 15))
                .startTime(LocalTime.of(20, 30))
                .build();

    BDDMockito.given(eventLocationRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventLocationEntity));

    BDDMockito.given(eventRepository.save(any(EventEntity.class)))
                .willReturn(eventEntity);

    // when
    EventEntity result = eventService.createEvent(dto);

    // then
    assertThat(result).extracting(&quot;eventName&quot;, &quot;eventDescription&quot;, &quot;date&quot;, &quot;startTime&quot;)
        .contains(eventEntity.getEventName(), eventEntity.getEventDescription(), eventEntity.getDate(), eventEntity.getStartTime());
    assertThat(result).extracting(&quot;eventLocation&quot;)
        .isEqualTo(eventLocationEntitySeoul);
}


@DisplayName(&quot;수정하고 싶은 이벤트 정보를 수정하면, 수정된 정보가 나온다.&quot;)
@Test
void updateEventTest() {
    // given
    EventLocationEntity eventLocationEntitySeoul = new EventLocationEntity(1L, &quot;서울 월드컵 경기장&quot;, 50000);
    EventLocationEntity eventLocationEntitySuwon = new EventLocationEntity(2L, &quot;수원 빅버드 경기장&quot;, 50000);

    EventEntity eventEntity = EventEntity.builder()
                .eventId(1L)
                .eventLocationEntity(eventLocationEntitySeoul)
                .eventName(&quot;대한민축 vs 일본 축구 친선경기&quot;)
                .eventDescription(&quot;축구 경기&quot;)
                .date(LocalDate.of(2024, 10, 15))
                .startTime(LocalTime.of(20, 30))
                .build();
    EventUpdateRequestDTO dto = EventUpdateRequestDTO.builder()
                .eventLocationId(2L)
                .eventName(&quot;대한민축 vs 일본 축구 친선경기&quot;)
                .build();

    eventEntity.update(dto, eventLocationEntitySuwon);

    BDDMockito.given(eventLocationRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventLocationEntitySeoul));

    BDDMockito.given(eventRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventEntity));

    BDDMockito.given(eventRepository.save(any(EventEntity.class)))
                .willReturn(eventEntity);

    // when
    eventService.updateEvent(1L, dto);

    // then
    assertThat(eventEntity).extracting(&quot;eventName&quot;, &quot;eventDescription&quot;, &quot;date&quot;, &quot;startTime&quot;)
}
</code></pre>
<p>현재 코드는 evnet를 생성을 하는 테스트와 수정을 하는 테스트 두 가지 경우가 있습니다. 각각의 메소드에 동일한 eventLocation을 만들어 주었습니다. 하지만 메소드 레벨에서 만들어 주었기 때문에 독립적으로 동작이 가능한 것을 알 수 있습니다.</p>
<p>위 코드에서 동일한 값이 독립적으로 각각의 테스트에 영향을 주지 않지만 같은 코드가 반복이 되면서 중복이 되는 것을 알 수 있습니다. 이를 한 번 개선을 헤보겠습니다.</p>
<h3 id="중복되는-test-fixture-개선을-해보기">중복되는 Test Fixture 개선을 해보기</h3>
<h4 id="setup으로-중복-해결하기">setUp으로 중복 해결하기</h4>
<p>첫 번째 방법으로는 setUp 메소드를 사용을 하는 것입니다.</p>
<pre><code class="language-java">@BeforeEach
void setUp() {

     EventLocationEntity eventLocationEntitySeoul = new EventLocationEntity(1L, &quot;서울 월드컵 경기장&quot;, 50000);
    EventLocationEntity eventLocationEntitySuwon = new EventLocationEntity(2L, &quot;수원 빅버드 경기장&quot;, 50000);

    EventEntity eventEntity = EventEntity.builder()
                .eventId(1L)
                .eventLocationEntity(eventLocationEntity)
                .eventName(&quot;대한민축 vs 일본 축구 친선경기&quot;)
                .eventDescription(&quot;축구 경기&quot;)
                .date(LocalDate.of(2024, 10, 15))
                .startTime(LocalTime.of(20, 30))
                .build();

}

@DisplayName(&quot;이벤트를 생성하면, 생성이 된 이벤트의 정보가 나온다.&quot;)
@Test
void createEventTest() {
    // given
    EventRequestDTO dto = EventRequestDTO.builder()
        .eventLocationId(1L)
        .eventName(&quot;대한민축 vs 일본 축구 친선경기&quot;)
        .eventDescription(&quot;축구 경가&quot;)
        .date(LocalDate.of(2024, 10, 15))
        .startTime(LocalTime.of(8, 0))
        .build();

    BDDMockito.given(eventLocationRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventLocationEntitySeoul));

    BDDMockito.given(eventRepository.save(any(EventEntity.class)))
                .willReturn(eventEntity);

    // when
    EventEntity result = eventService.createEvent(dto);

    // then
    assertThat(result).extracting(&quot;eventName&quot;, &quot;eventDescription&quot;, &quot;date&quot;, &quot;startTime&quot;)
        .contains(eventEntity.getEventName(), eventEntity.getEventDescription(), eventEntity.getDate(), eventEntity.getStartTime());
    assertThat(result).extracting(&quot;eventLocation&quot;)
        .isEqualTo(eventLocationEntitySeoul);
}


@DisplayName(&quot;수정하고 싶은 이벤트 정보를 수정하면, 수정된 정보가 나온다.&quot;)
@Test
void updateEventTest() {
    // given
    EventUpdateRequestDTO dto = EventUpdateRequestDTO.builder()
                .eventLocationId(2L)
                .eventName(&quot;대한민축 vs 일본 축구 친선경기&quot;)
                .build();

    eventEntity.update(dto, eventLocationEntitySuwon);

    BDDMockito.given(eventLocationRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventLocationEntitySeoul));

    BDDMockito.given(eventRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventEntity));

    BDDMockito.given(eventRepository.save(any(EventEntity.class)))
                .willReturn(eventEntity);

    // when
    eventService.updateEvent(1L, dto);

    // then
    assertThat(eventEntity).extracting(&quot;eventName&quot;, &quot;eventDescription&quot;, &quot;date&quot;, &quot;startTime&quot;)
}
</code></pre>
<p>event와 eventLocation을 setUp 메소드로 빼주고 BeforeEach 어노테이션을 달아 주었습니다. BeforeEach 어노테이션을 달아주면 각각 테스트가 실행되기 전에 setUp 메소드를 실행을 시켜줍니다. 각 테스트 메소드는 독립적이기 때문에 setUp 메소드의 값들이 서로 영향을 주지 않고 독립적으로 주어집니다. 그리고 setUp에서 동일한 값들을 관리하기 때문에 중독도 줄어 들고 가독성도 좋아진 것을 확인을 할 수 있습니다.</p>
<h4 id="private-메소드를-사용해서-test-fixture를-만들기">private 메소드를 사용해서 Test Fixture를 만들기</h4>
<p>다른 방법은 클래스 내부에 private 메소드를 사용을 해서 필요한 값을 리턴을 하는 메소드를 만듭니다.</p>
<pre><code class="language-java">private EventEntity getEventEntity(EventLocationEntity eventLocationEntity, String eventName) {
    return EventEntity.builder()
                .eventId(1L)
                .eventLocationEntity(eventLocationEntity)
                .eventName(eventName)
                .eventDescription(&quot;축구 경기&quot;)
                .date(LocalDate.of(2024, 10, 15))
                .startTime(LocalTime.of(20, 30))
                .build();
}

private EventLocationEntity getEventLocationEntity(Long eventLocationId, String eventLocationName) {
    return new EventLocationEntity(eventLocationId, eventLocationName, 50000);
}

</code></pre>
<p>이렇게 클래스 내부에 event를 반환하는 메소드와 eventLocation을 반환을 하는 매소드를 만들어주는 것입니다.</p>
<pre><code class="language-java">@DisplayName(&quot;이벤트를 생성하면, 생성이 된 이벤트의 정보가 나온다.&quot;)
@Test
void createEventTest() {
    // given
    EventRequestDTO dto = EventRequestDTO.builder()
        .eventLocationId(1L)
        .eventName(&quot;대한민축 vs 일본 축구 친선경기&quot;)
        .eventDescription(&quot;축구 경가&quot;)
        .date(LocalDate.of(2024, 10, 15))
        .startTime(LocalTime.of(8, 0))
        .build();

    EventLocationEntity eventLocationEntitySeoul = getEventLocationEntity(1L, &quot;서울 월드컵 경기장&quot;);
    EventEntity eventEntity = getEventEntity(eventLocationEntity, dto.getEventName());


    BDDMockito.given(eventLocationRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventLocationEntity));

    BDDMockito.given(eventRepository.save(any(EventEntity.class)))
                .willReturn(eventEntity);

    // when
    EventEntity result = eventService.createEvent(dto);

    // then
    assertThat(result).extracting(&quot;eventName&quot;, &quot;eventDescription&quot;, &quot;date&quot;, &quot;startTime&quot;)
        .contains(eventEntity.getEventName(), eventEntity.getEventDescription(), eventEntity.getDate(), eventEntity.getStartTime());
    assertThat(result).extracting(&quot;eventLocation&quot;)
        .isEqualTo(eventLocationEntitySeoul);
}


@DisplayName(&quot;수정하고 싶은 이벤트 정보를 수정하면, 수정된 정보가 나온다.&quot;)
@Test
void updateEventTest() {
    // given
    EventLocationEntity eventLocationEntitySeoul = getEventLocationEntity(1L, &quot;서울 월드컵 경기장&quot;);
    EventLocationEntity eventLocationEntitySuwon = getEventLocationEntity(2L, &quot;수원 빅버드 경기장&quot;);


    EventEntity eventEntity = getEventEntity(eventLocationEntity, dto.getEventName());

    EventUpdateRequestDTO dto = EventUpdateRequestDTO.builder()
                .eventLocationId(2L)
                .eventName(&quot;대한민축 vs 일본 축구 친선경기&quot;)
                .build();

    eventEntity.update(dto, eventLocationEntitySuwon);

    BDDMockito.given(eventLocationRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventLocationEntitySeoul));

    BDDMockito.given(eventRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventEntity));

    BDDMockito.given(eventRepository.save(any(EventEntity.class)))
                .willReturn(eventEntity);

    // when
    eventService.updateEvent(1L, dto);

    // then
    assertThat(eventEntity).extracting(&quot;eventName&quot;, &quot;eventDescription&quot;, &quot;date&quot;, &quot;startTime&quot;)
}</code></pre>
<p>event와 eventLocation을 반환을 하는 메소드를 분리를 하고 이를 테스트 하는 메소드에서 사용을 합니다. 이로 인해서 가독성이 높아지고 각 테스트 메소드 별로 독립성을 유지를 할 수 있습니다.</p>
<h4 id="별도의-클래스를-만들어서-관리-해주기">별도의 클래스를 만들어서 관리 해주기</h4>
<p>이번 방법은 별도의 클래스를 만들어 주는 것입니다.</p>
<pre><code class="language-java">public class CommonTestFixture {

    public static EventEntity getEventEntity(EventLocationEntity eventLocationEntity, String eventName) {
        return EventEntity.builder()
                .eventId(1L)
                .eventLocationEntity(eventLocationEntity)
                .eventName(eventName)
                .eventDescription(&quot;축구 경기&quot;)
                .date(LocalDate.of(2024, 10, 15))
                .startTime(LocalTime.of(20, 30))
                .build();
    }

    public static EventLocationEntity getEventLocationEntity(Long eventLocationId, String eventLocationName) {
        return new EventLocationEntity(eventLocationId, eventLocationName, 50000);
    }
}</code></pre>
<p>자주 사용이 되는 Test Fixture 같은 경우에는 따로 클래스를 만들어 관리를 해줄 수 있습니다. 왜냐하면 이 Test Fixture들이 event를 만드는 곳에서만 사용을 하는 것이 아닐 수도 있습니다. 나중에 좌석과 관련된 테스트를 진행을 할 수도 있고 티켓을 만드는 테스트를 진행을 할 수 있습니다. 즉, 외부 Test Fixture를 만들어서 여러 곳에서 사용을 할 수 있도록 만들어 주는 것입니다. 이 때 메소드는 static으로 만들어서 CommonTestFixture의 인스턴스 없이 사용할 수 있도록 해줍니다.</p>
<pre><code class="language-java">@DisplayName(&quot;이벤트를 생성하면, 생성이 된 이벤트의 정보가 나온다.&quot;)
@Test
void createEventTest() {
    // given
    EventRequestDTO dto = EventRequestDTO.builder()
        .eventLocationId(1L)
        .eventName(&quot;대한민축 vs 일본 축구 친선경기&quot;)
        .eventDescription(&quot;축구 경가&quot;)
        .date(LocalDate.of(2024, 10, 15))
        .startTime(LocalTime.of(8, 0))
        .build();

    EventLocationEntity eventLocationEntitySeoul = CommonTestFixture.getEventLocationEntity(1L, &quot;서울 월드컵 경기장&quot;);
    EventEntity eventEntity = CommonTestFixture.getEventEntity(eventLocationEntity, dto.getEventName());


    BDDMockito.given(eventLocationRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventLocationEntity));

    BDDMockito.given(eventRepository.save(any(EventEntity.class)))
                .willReturn(eventEntity);

    // when
    EventEntity result = eventService.createEvent(dto);

    // then
    assertThat(result).extracting(&quot;eventName&quot;, &quot;eventDescription&quot;, &quot;date&quot;, &quot;startTime&quot;)
        .contains(eventEntity.getEventName(), eventEntity.getEventDescription(), eventEntity.getDate(), eventEntity.getStartTime());
    assertThat(result).extracting(&quot;eventLocation&quot;)
        .isEqualTo(eventLocationEntitySeoul);
}


@DisplayName(&quot;수정하고 싶은 이벤트 정보를 수정하면, 수정된 정보가 나온다.&quot;)
@Test
void updateEventTest() {
    // given
    EventLocationEntity eventLocationEntitySeoul = CommonTestFixture.getEventLocationEntity(1L, &quot;서울 월드컵 경기장&quot;);
    EventLocationEntity eventLocationEntitySuwon = CommonTestFixture.getEventLocationEntity(2L, &quot;수원 빅버드 경기장&quot;);


    EventEntity eventEntity = CommonTestFixture.getEventEntity(eventLocationEntity, dto.getEventName());

    EventUpdateRequestDTO dto = EventUpdateRequestDTO.builder()
                .eventLocationId(2L)
                .eventName(&quot;대한민축 vs 일본 축구 친선경기&quot;)
                .build();

    eventEntity.update(dto, eventLocationEntitySuwon);

    BDDMockito.given(eventLocationRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventLocationEntitySeoul));

    BDDMockito.given(eventRepository.findById(any(Long.class)))
                .willReturn(Optional.of(eventEntity));

    BDDMockito.given(eventRepository.save(any(EventEntity.class)))
                .willReturn(eventEntity);

    // when
    eventService.updateEvent(1L, dto);

    // then
    assertThat(eventEntity).extracting(&quot;eventName&quot;, &quot;eventDescription&quot;, &quot;date&quot;, &quot;startTime&quot;)
}</code></pre>
<p>이제 테스트 코드에서 바로 사용을 하기만 하면 됩니다. 외부 클래스를 이용을 하면 코드의 가독성이나 어느 곳에서 사용이 가능하도록 할 수 있습니다.</p>
<h3 id="어느-방법을-사용할까요">어느 방법을 사용할까요?</h3>
<p>위에 3가지의 방법에 대해서 알아 보았습니다. 이제 3가지 방법중 어느 방법을 쓰는 것이 좋을까요?
저의 생각을 말하자면 <strong>setUp으로 Test Fixture를 만드는 방법은 지양을 한다고 이야기하고 싶습니다.</strong>
이유를 말하기 전에 3가지 방법은 상황에 따라 적합한 방법이 있고 setUp방법을 지양한다고 해서 setUp방법이 나쁜 방법은 아니라고 말하고 싶습니다. </p>
<p>이유를 말하자면 setUp방식의 경우 가독성이 두 방법보다 떨어진다고 생각을 합니다. 그 이유는 setUp이라는 메소드를 만들어서 거기서 필요한 정보나 환경을 한번에 설정을 해주는데 각 테스트마다 어떤 정보가 일일히 확인을 해야 합니다. 이는 오히려 불편함을 초래합니다. 그리고 필요가 없는 정보를 생성을 하게 됩니다. setUp 메소드는 모든 테스트를 할 때 실행이 됩니다. 그런데 setUp메소드에서 만든 정보가 테스트에 필요없는 정보가 있게 되면 낭비가 되게 됩니다. 마지막으로 만약 setUp에서 하나의 정보를 변경을 하게 된다면 변경된 정보가 다른 테스트에도 영향을 미치게 됩니다. </p>
<pre><code class="language-java">EventLocationEntity eventLocationEntitySeoul = new EventLocationEntity(1L, &quot;서울 월드컵 경기장&quot;, 50000);</code></pre>
<p>setUp 메소드 예시에 있었던 event location입니다. 현재 서울 월드컵 경기장으로 만들어져 있습니다.</p>
<pre><code class="language-java">EventLocationEntity eventLocationEntitySeoul = new EventLocationEntity(1L, &quot;포항 스틸야드 경기장&quot;, 50000);</code></pre>
<p>그리고 이를 포항 스틸야드 경기장으로 변경을 했습니다. 그렇게 되면 모든 테스트가 서울 월드컵 경기장에서 포항 스틸야드 경기장으로 변경이 되는 것입니다. 물론 지금 테스트에서 스틸야드 경기장으로 변경을 한다고 해서 그게 문제가 될 것은 없지만 만약 영향을 주는 정보라면 오히려 다른 테스트에 영향을 줘서 테스트 간에 독립적인 테스트가 안 될 수 있습니다.</p>
<p>그래서 저는 setUp으로 Test Fixture를 하는 것을 지양을 합니다. 물론 아예 안 쓴다는 것이 아니라 가급적 사용을 안하겠다 라는 것입니다.</p>
<h3 id="마무리">마무리</h3>
<p>이번 포스트에서는 Test Fixture에 대해서 알아보고 어떤 사용을 할 수 있는지 알아보았습니다. 이번 포스트의 내용은 정답은 아니고 지극히 개인적으로 고민을 해봤을 때 난 이렇게 쓰는 게 좋을 것 갔다는 의견입니다. 그러니 가볍게 보시고 개인적인 생각인 것을 참고 주시면 감사하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] redis로 성능 개선 하기]]></title>
            <link>https://velog.io/@sang_hyeok_2/Project-redis%EB%A1%9C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sang_hyeok_2/Project-redis%EB%A1%9C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 14 Nov 2024 12:24:08 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<p>이번 포스트에서는 카테고리로 이벤트를 조회하는 api의 성능을 개선을 해보려고 합니다. 예매 프로젝트 특성 상 티켓 예매가 오픈이 되는 날 많은 사용자가 이벤트를 조회를 하는 경우가 발생을 하는데 조회가 느려지면 사용자에게 불편함을 줄 수 있다고 판단을 하였습니다. 성능을 체크를 하고 성능을 개선을 해보고자 합니다.</p>
<h3 id="성능-체크해보기">성능 체크해보기</h3>
<p>먼저, 기존의 카테고리로 이벤트를 조회를 하는 api의 코드와 성능을 알아보겠습니다.</p>
<pre><code class="language-java">public List&lt;EventResponseDTO&gt; getEventByCategoryId(Page&lt;CategoryEventEntity&gt; categoryEventEntities) {
    List&lt;EventResponseDTO&gt; eventEntities = new ArrayList&lt;&gt;();

    categoryEventEntities.forEach(categoryEventEntity -&gt; {
        eventEntities.add(new EventResponseDTO(categoryEventEntity.getEventEntity()));
    });

    return eventEntities;
}</code></pre>
<p>기존의 코드에서는 MySql에 직접 접근을 해서 데이터를 가지고 와서 return을 해주고 있습니다. 즉, getEventByCategoryId메소드가 호출이 될 때마다 DB와 직접 통신을 하면서 데이터를 가지고 오는 것입니다. 그러면 이 코드로 속도가 얼마나 걸리는 지 알아보겠습니다. 테스트는 두 개의 경우를 볼려고 합니다. 하나의 경우는 하나의 요청이 갔을 때와 또 다른 하나는 8000명이 요청을 보냈을 때 입니다.
먼저, 하나의 요청의 대한 속도입니다.
<img src="https://velog.velcdn.com/images/sang_hyeok_2/post/b1890d37-7d0e-4fc1-84df-ac164ed94acd/image.png" alt="">
데이터 베이스를 갔다오고 데이터를 넘겨 줬을 때 속도가 <strong>49ms</strong>가 나왔습니다.
그렇다면, 8000명이 요청을 했다면 얼머나 걸릴까요? 8000명이 점진적으로 요청을 했을 때를 테스트를 해보았습니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/d1b1d374-7394-4040-be18-838976549272/image.png" alt=""></p>
<p>평균 값이 <strong>3172ms</strong>가 나왔습니다.
이제 성능을 체크를 해보았으니 성능을 개선을 해보겠습니다.</p>
<h3 id="성능-개선을-위해서-redis-cache를-선택한-이유">성능 개선을 위해서 Redis Cache를 선택한 이유</h3>
<p>성능 개선을 위해서 Redis Cache를 선택한 이유는 먼저, 현재 프로젝트에서 Redis를 사용을 하고 있습니다. 분산 락을 구현하기 위해서 Redis를 사용을 하고 있습니다. 그래서 Redis Cache를 사용을 한다고 해서 추가적인 비용이나 관리가 필요하지 않습니다. 
다음으로는 빠른 속도입니다. Redis는 In-Memory-Database이기 때문에 데이터 베이스까지 접근을 하지 않고 메모리까지만 접근을 하기 때문에 빠르게 데이터를 가지고 올 수 있습니다. 그리고 같은 이유로 데이터 베이스의 부하를 줄여줄 수 있습니다. 
또한 카테코리의 의한 이벤트 조회는 자주 조회가 일어나기 때문에 Redis Cache의 저장을 하는 것이 성능 상 더 효율적이라고 판단을 했습니다.</p>
<h3 id="redis-cache로-성능-개선하기">Redis Cache로 성능 개선하기</h3>
<h4 id="redis-cache-설정">Redis Cache 설정</h4>
<pre><code class="language-java"> @Bean
 public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());

        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);

        RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofDays(7))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer));

        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(cacheConfig)
                .build();
}</code></pre>
<p>CacheManager는 Spring Boot에서 Redis Cache를 사용할 수 있도록 하는 코드입니다. 
ObjectMapper는 JSON의 직렬화와 역직렬화 기능을 제공을 하고 JavaTimeModule은 날짜 타입을 직렬화, 역직렬화를 제공을 하는 클래스입니다. JavaTimeModule의 경우 LocalDate 타입의 필드 값이 있기 때문에 넣어주었습니다. GenericJackson2JsonRedisSerializer는 Redis에 저장할 java 객체를 직렬화 해주는 역할을 합니다. 그래서 객체를 직렬화를 해서 Redis에 JSON 형태로 저장이 됩니다. 
RedisCacheConfiguration는 cache의 기본 설정을 설정하는 코드입니다. entryTtl으로 cache의 기간을 7일로 설정을 합니다. serializeKeysWith는 cache의 키의 값을 String 타입으로 직렬화를 하고 serializeValuesWith은 GenericJackson2JsonRedisSerializer를 사용해서 직렬화, 역직렬화를 해줍니다.
마지막으로 Redis 저장소를 연결을 해주고 RedisCacheManager를 생성을 해준다.</p>
<h4 id="redis-cache-메소드에-적용하기">Redis Cache 메소드에 적용하기</h4>
<pre><code class="language-java">@Cacheable(value = &quot;events&quot;, key = &quot;#categoryEventEntities.content[0].categoryEntity.categoryId&quot;)
public List&lt;EventResponseDTO&gt; getEventByCategoryId(Page&lt;CategoryEventEntity&gt; categoryEventEntities) {
    List&lt;EventResponseDTO&gt; eventEntities = new ArrayList&lt;&gt;();

    categoryEventEntities.forEach(categoryEventEntity -&gt; {
        eventEntities.add(new EventResponseDTO(categoryEventEntity.getEventEntity()));
    });

    return eventEntities;
}</code></pre>
<p>Cacheable 어노테이션을 통해서 결과를 Redis Cache에 저장을 할 수 있습니다. 그리고 동일한 메소드가 호출이 되면 Cache에 저장이 된 값이 반환이 됩니다. value는 cache에 저장이 될 이름으로 사용이 되고 key의 경우 cache에 저장이 될 때 key 값으로 사용이 됩니다.</p>
<p>이렇게 Redis Cache을 사용하는 방법에 대해서 알아보았습니다. 그리고 이제 이 Redis Cache를 사용을 했을 때 속도 더 빨라졌는지 확인을 해보도록 하겠습니다.</p>
<h3 id="redis-cache-성능-확인하기">Redis Cache 성능 확인하기</h3>
<p>마찬가지로 요청을 한 번 보낸 것과 여러 명이 요청을 했을 때 속도를 보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/3527efb6-239e-4a7b-bae0-423e81c100e7/image.png" alt=""></p>
<p>요청을 한 번 보냈을 때 결과입니다.
1<strong>2ms로 기존에 Redis Cache를 사용하지 않았을 때 49ms 보다 37ms 줄어 든 것</strong>을 볼 수 있습니다.
확실 더 빨라진 것을 알 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/7ba8ecca-1dee-4517-a4e4-36a5d440be7a/image.png" alt=""></p>
<p>다음은 8000명이 요청을 했을 때 결과입니다. 
<strong>평균적으로 878ms으로 Redis Cache를 적용하지 않았던 3172ms 보자 2294ms 줄어든 것</strong>을 볼 수 있습니다. Redis Cache를 적용을 한 것이 더 빠르다는 것을 확연하게 알 수 있었습니다.</p>
<h4 id="redis가-mysql보다-빠른-이유">Redis가 MySql보다 빠른 이유</h4>
<p>Redis가 MySql보다 빠른 이유는 In-Memory-Database이기 때문입니다. MySql은 디스크를 사용하기 때문에 Redis보다 더 멀리 있어서 갔다 오는 것이 오래 걸립니다. 또한 Redis의 경우 Key-Value 형태이기 때문에 데이터 구조가 단순해서 접근하기 쉽지만 RDB인 MySql의 경우 테이블이 연결되어 있거나 구조가 복잡하기 때문에 접근을 하려면 시간이 더 걸릴 수 밖에 없습니다. </p>
<h3 id="마무리">마무리 &amp;</h3>
<p>이번 포스트에서는 Redis Cache를 이용을 해서 경기 조회 api의 성능을 개선을 해보았습니다. 성능을 개선을 한 결과 <strong>하나의 요청을 보냈을 때는 37ms의 속도가, 8000명이 했을 때는 2294ms가 개선</strong>이 된 것을 볼 수 있었습니다. 1초라는 시간이 어떻게 보면 큰 시간이 아닐 수 있지만 사용자 입장에서는 큰 시간이라고 생각을 합니다. 그런 의미에서 2294ms가 개선이 된 것은 유의미한 개선이었습니다. 그래서 성능을 개선을 할 때 Cache을 잘 적용을 한 다면 유의미한 결과을 얻을 수 있다고 생각이 들었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Redisson 분산 락 AOP 적용]]></title>
            <link>https://velog.io/@sang_hyeok_2/Project-Redisson-%EB%B6%84%EC%82%B0-%EB%9D%BD-AOP-%EC%A0%81%EC%9A%A9</link>
            <guid>https://velog.io/@sang_hyeok_2/Project-Redisson-%EB%B6%84%EC%82%B0-%EB%9D%BD-AOP-%EC%A0%81%EC%9A%A9</guid>
            <pubDate>Tue, 12 Nov 2024 17:04:54 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<p>지난 시간 Redisson 분산락을 구현을 해보았습니다. 그리고 이 분산 락을 통해서 동시성 제어를 했습니다. 여기서 한 가지 더 발전을 시켜보고 싶었던 부분은 가독성과 재사용성이었습니다. 코드를 보면 좌석을 선택을 하는 비지니스 로직과 동시성을 제어를 하는 분산 락 로직이 같이 있는 것을 볼 수 있습니다. 두 로직이 같이 있다 보니 코드의 가독성도 떨어지고 만약 다른 곳에서 분산 락을 사용을 할 때, 다시 분삭 락 로직을 구현을 해야 합니다. 이에 이번 포스트에서는 가독성과 재사용성을 높혀보도록 하겠습니다.</p>
<h3 id="aop란">AOP란</h3>
<p>AOP에 대해서 간단하게 알아보고자 합니다.
AOP는 관심 지향 프로그래밍이라고 합니다. AOP는 비지니스 로직을 도와주는 부가 기능 즉, 핵심 로직하고는 관련이 없지만 이 핵심 로직을 도와주는 로직을 모듈화를 시켜서 중복을 줄이고 재사용성을 높이는 것을 말합니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/6dc786b6-8017-485f-8b74-b89e82170a03/image.png" alt=""></p>
<p>예를 들어보겠습니다. 대학교에는 다양한 학과가 있다. 이 학과는 고유한 값들이 존재를 할 수 있지만 회장, 부화장 등은 역할이 똑같고 공통적으로 존재한다는 것을 알 수 있습니다. 이렇게 역할이 똑같고 공통적인 부가 기능을 분리해서 모듈화를 해서 다른 곳에서도 똑같이 쓰일 수 있게 해주는 것입니다.</p>
<h3 id="분산-락-aop로-분리하기">분산 락 AOP로 분리하기</h3>
<p>분산 락 로직을 살펴보겠습니다.</p>
<pre><code class="language-java">     @Transactional
    public SeatEntity selectSeat(Long seatId) {
        String lockKey = &quot;seat-lock:&quot; + seatId;
        RLock lock = redissonClient.getLock(lockKey);

        boolean isLocked = false;

        try {
            isLocked = lock.tryLock(1, 300, TimeUnit.SECONDS);
            if (!isLocked) {
                throw new GlobalCommonException(SeatErrorResponsive.FILL_SEAT);
            }

            SeatEntity seatEntity = getSeatById(seatId);
            if(seatEntity.getSeatStatus() == SeatStatus.SELECT) {
                throw new GlobalCommonException(SeatErrorResponsive.FILL_SEAT);
            }
            seatEntity.updateSeatStatus(SeatStatus.SELECT);
            return seatRepository.save(seatEntity);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException(&quot;Thread was interrupted while waiting for lock&quot;, e);
        } finally {
            if (isLocked) {
                lock.unlock();
            }
        }
    }</code></pre>
<p>위 코드에는 좌석을 선택하는 로직과 동시성을 제어하는 분산 락이 한 번에 같이 있습니다. 보기에 복잡해 보이며 가독성이 떨어집니다. 또한 현재는 이 부분에서만 분산 락을 사용을 하고 있지만 추후에 분산 락을 다시 사용하게 되면 위에 분산 락 로직을 다시 작성을 해주어야 합니다. 그래서 분산 락을 분리를 해줄려고 합니다. </p>
<h4 id="distributedlock-어노테이션-만들기">DistributedLock 어노테이션 만들기</h4>
<pre><code class="language-java">@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {

    String key();

}</code></pre>
<p>DistributedLock 어노테이션을 만들어줍니다.
이렇게 어노테이션을 만들어주면 분산 락을 사용하고자 하는 곳에 선언적을 사용이 가능하고 이로 인해 가독성과 재사용성이 높아집니다.</p>
<h4 id="aop-분산-락-로직-구현">AOP 분산 락 로직 구현</h4>
<pre><code class="language-java">@Aspect
@Component
@RequiredArgsConstructor
public class DistributedLockAspect {

    private final RedissonClient redissonClient;

    @Around(&quot;@annotation(distributedLock)&quot;)
    public Object lock(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable {
        String lockKey = distributedLock.key();
        RLock lock = redissonClient.getLock(lockKey);

        boolean isLocked = false;

        try {
            isLocked = lock.tryLock(5, 20, TimeUnit.SECONDS);
            if (isLocked) {
                return joinPoint.proceed();
            } else {
                throw new IllegalStateException(&quot;Could not acquire lock for key: &quot; + lockKey);
            }
        } finally {
            if (isLocked) {
                lock.unlock();
            }
        }
    }
}
</code></pre>
<p>Aspect 어노테이션을 통해서  DistributedLockAspect 클래스가 AOP로 실행이 될 수 있도록 설정을 합니다. 그리고 Around 어노테이션을 통해서 DistributedLock 어노테이션을 적용한 메소드에 대해서 lock 메소드가 호출이 되도록 합니다. 이 lock 메소드가 실행이 되면서 분산 락을 사용을 할 수 있겠습니다.</p>
<h4 id="분산-락-메소드에-사용하기">분산 락 메소드에 사용하기</h4>
<pre><code class="language-java">@Transactional
@DistributedLock(key = &quot;seat-lock:#seatId&quot;)
public SeatEntity selectSeat(Long seatId) {
    SeatEntity seatEntity = getSeatById(seatId);
    if (seatEntity.getSeatStatus() == SeatStatus.SELECT) {
    throw new GlobalCommonException(SeatErrorResponsive.FILL_SEAT);
    }
    seatEntity.updateSeatStatus(SeatStatus.SELECT);
    return seatRepository.save(seatEntity);
}</code></pre>
<p>좌석을 선택하는 메소드에 DistributedLock 어노테이션을 붙혀줍니다. 그리고 key의 이름을 지정해줍니다. 이렇게 되면 이 메소드를 사용할 때 DistributedLockAspect 클래스의 Lock을 사용해서 분산 락을 사용을 합니다.</p>
<h3 id="마무리">마무리</h3>
<p>이번 포스트에서는 Redisson의 분산 락을 AOP를 통해서 분리를 했습니다. 그 결과로 DistributedLock 어노테이션 하나로 어디서든 Redisson의 분산 락을 사용을 할 수 있게 되었습니다. AOP를 통해서 코드의 가독성을 높였으며 분산 락 기능 코드의 재사용성을 높일 수 있었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Redisson을 이용한 좌석 선택 동시성 제어하기]]></title>
            <link>https://velog.io/@sang_hyeok_2/Project-Redisson%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%A2%8C%EC%84%9D-%EC%84%A0%ED%83%9D-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sang_hyeok_2/Project-Redisson%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%A2%8C%EC%84%9D-%EC%84%A0%ED%83%9D-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 08 Nov 2024 16:10:10 GMT</pubDate>
            <description><![CDATA[<h3 id="좌석을-선택을-할-때-동시성을-제어해야-하는-이유">좌석을 선택을 할 때 동시성을 제어해야 하는 이유</h3>
<p>보통 예매를 하는 애플리케이션을 보면 좌석을 선택하고 예매를 합니다. 좌석을 선택하게 되면 다른 이용자는 좌석을 선택을 하지 못하게 됩니다. 이번 프로젝트에서도 이 플로우를 따라 갑니다. 먼저, 이용자가 좌석을 선택을 하고 예매를 합니다. 근데 여기서 한 가지 주의를 해야 할 상황이 있습니다. 만약 두 명의 이용자가 같은 좌석을 예매를 하려고 한다면 어떻게 될까요? 한 좌석의 한 명만 예약이 가능한데 두 명이 한 좌석을 예매를 하는 경우가 생길 것입니다. 그래서 좌석을 선택을 할 때, 동시성을 제어를 해주어야 합니다.</p>
<h3 id="redisson을-사용하는-이유">Redisson을 사용하는 이유</h3>
<p>이전 포스트에서 낙관적 락, 비관적 락, Redis의 원자성 명령어 등 동시성을 제어하는 방법들에 대해서 알아보았습니다. 이번 좌석을 선택하는 기능에서는 Redis의 Redisson을 작용하고자 합니다. 그 이유는 먼저, 프로젝트가 Scale Out을 선택했습니다. 그래서 서버가 분산이 되어 있는 상황입니다. 그리고 티켓팅을 하는 날짜이먄 트래픽이 많아 집니다. 분산 환경에서는 Redisson이 최적화가 되어 있습니다. 물론 Redis를 설치하고 관리를 해야 하지만 성능과 적합성을 보았을 때 Redisson을 사용하는 것이 더 좋다고 판단을 했습니다.</p>
<h3 id="redisson의-분산-락-동작-원리">Redisson의 분산 락 동작 원리</h3>
<p>Redisson이 분산 락을 어떻게 구현을 하는 지 간단하게 알아보겠습니다. 
Redisson의 경우 pub-sub 기반으로 Lock을 구현을 합니다. Lock을 관리하기 위한 채널을 생성을 하고 Lock을 점유하고 있는 스레드가 작업을 마치고 Lock을 반환을 하면 채널을 통해서 Lock을 점유하기 위해서 대기하고 있는 스레드에게 전달을 합니다. 전달을 받은 스레드들은 Lock을 점유하기 위해서 시도를 합니다. </p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/fbc90831-59cc-4287-bb70-9fd94144c6a8/image.png" alt=""></p>
<h3 id="redisson-프로젝트에-적용하기">Redisson 프로젝트에 적용하기</h3>
<p>이제 Redisson을 프로젝트에 적용을 해서 분산 락을 구현을 하겠습니다.
먼저, Redisson의 의존성 추가와 Config로 bean 등록을 해줍니다.</p>
<pre><code class="language-java">implementation &#39;org.redisson:redisson-spring-boot-starter:3.20.0&#39;</code></pre>
<p>의존성을 추가를 해줍니다.</p>
<pre><code class="language-java">@Configuration
public class RedissonConfig {

    @Bean
    @Profile(&quot;!test&quot;)
    public RedissonClient redisson() {
        Config config = new Config();
        config.useSingleServer()
                .setAddress(&quot;redis://my-cache-server:6379&quot;)
                .setConnectionMinimumIdleSize(10)
                .setConnectionPoolSize(10);

        return Redisson.create(config);
    }

}</code></pre>
<p>Redisson을 Bean등록을 해줍니다. 이때 저는 docker를 사용하고 있기 때문에 docker의 redis이미지 이름으로 넣어주었습니다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class SeatGradeService {

    private final RedissonClient redissonClient;

    @Transactional
    public SeatEntity selectSeat(Long seatId) {
        String lockKey = &quot;seat-lock:&quot; + seatId;
        RLock lock = redissonClient.getLock(lockKey);

        boolean isLocked = false;

        try {
            isLocked = lock.tryLock(1, 300, TimeUnit.SECONDS);
            if (!isLocked) {
                throw new GlobalCommonException(SeatErrorResponsive.FILL_SEAT);
            }

            SeatEntity seatEntity = getSeatById(seatId);
            if (seatEntity.getSeatStatus() == SeatStatus.SELECT) {
                throw new GlobalCommonException(SeatErrorResponsive.FILL_SEAT);
            }
            seatEntity.updateSeatStatus(SeatStatus.SELECT);
            return seatRepository.save(seatEntity);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException(&quot;Thread was interrupted while waiting for lock&quot;, e);
        } finally {
            if (isLocked) {
                lock.unlock();
            }
        }

    }

}</code></pre>
<p>이제 Redisson을 통해서 동시성 제어를 해줍니다.</p>
<pre><code class="language-java">String lockKey = &quot;seat-lock:&quot; + seatId;
RLock lock = redissonClient.getLock(lockKey);</code></pre>
<p>먼저, lockKey를 통해서 하나의 좌석에 대한 고유한 lock을 만들어 줍니다. 이 lock을 RLock에 넘겨주어서 Redis 서버와 연결을 해주는 분산 락 객체를 만들어줍니다. </p>
<pre><code class="language-java">isLocked = lock.tryLock(1, 300, TimeUnit.SECONDS);</code></pre>
<p>lock의 tryLock을 통해서 lock을 얻는 시도를 합니다. 이 때, tryLock에는 3개의 파라미터가 들어가는데 첫 번째 파라미터는 lock을 얻기 위해서 몇초동안 시도할 지 초를 정해주는 것입니다. 두 번째는 lock을 획득을 하게 되면 300초동안 가지고 있다는 것을 의미합니다. 그리고 마지막 파라미터는 시간의 단위를 설정을 해주는 것입니다.</p>
<pre><code class="language-java">if (!isLocked) {
    throw new GlobalCommonException(SeatErrorResponsive.FILL_SEAT);
}</code></pre>
<p>만약 lock을 이미 다른 스레드가 lock을 얻어서 lock을 얻기 실패하기 때문에 좌석이 이미 선택되었다는 에러를 반환을 합니다.</p>
<pre><code class="language-java">SeatEntity seatEntity = getSeatById(seatId);
if (seatEntity.getSeatStatus() == SeatStatus.SELECT) {
                throw new GlobalCommonException(SeatErrorResponsive.FILL_SEAT);
}
seatEntity.updateSeatStatus(SeatStatus.SELECT);
return seatRepository.save(seatEntity);</code></pre>
<p>그리고 좌석을 찾아서 SeatStatus의 값을 변경을 해줘서 선택을 완료 할 수 있도록합니다.</p>
<pre><code class="language-java">if (isLocked) {
    lock.unlock();
}        </code></pre>
<p>그리고 lock을 해제를 해줍니다.</p>
<p>이제 테스트 코드를 작성을 해서 동시성이 잘 제어가 되는 지 살펴보겠습니다.</p>
<pre><code class="language-java">    @Test
    void testConcurrentSeatSelection() throws InterruptedException {
        RLock mockLock = mock(RLock.class);
        BDDMockito.given(redissonClient.getLock(any(String.class))).willReturn(mockLock);
        BDDMockito.given(mockLock.tryLock(any(Long.class), any(Long.class), any(TimeUnit.class)))
            .willReturn(true));
        BDDMockito.doNothing().when(mockLock).unlock();

        EventLocationEntity eventLocationEntity = getEventLocationEntity(1L, &quot;서울 월드컵 경기장&quot;);
        SeatGradeEntity seatGradeEntity = getSeatGradeEntity(1L, eventLocationEntity);
        SeatEntity seatEntity = CommonTestFixture.getSeatEntity(1L, seatGradeEntity);

        BDDMockito.given(seatRepository.findById(any(Long.class)))
                .willReturn(Optional.of(seatEntity));
        BDDMockito.given(seatRepository.save(any(SeatEntity.class)))
                .willAnswer(invocation -&gt; invocation.getArgument(0));

        int numberOfThreads = 10;
        ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);
        CountDownLatch countDownLatch = new CountDownLatch(numberOfThreads);

        AtomicInteger successCount = new AtomicInteger(0);
        AtomicInteger failureCount = new AtomicInteger(0);

        for (int i = 0; i &lt; numberOfThreads; i++) {
            executorService.submit(() -&gt; {
                try {
                    seatService.selectSeat(1L);
                    successCount.incrementAndGet();
                } catch (GlobalCommonException e) {
                    assertThat(e.getErrorCode()).isEqualTo(SeatErrorResponsive.FILL_SEAT);
                    failureCount.incrementAndGet();
                } finally {
                    countDownLatch.countDown();
                }
            });

        }

        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.MINUTES);

        System.out.println(&quot;Success Count: &quot; + successCount.get());
        System.out.println(&quot;Failure Count: &quot; + failureCount.get());

        assertThat(successCount.get()).isEqualTo(1);
        assertThat(failureCount.get()).isEqualTo(9);

    }</code></pre>
<p>먼저, 10개의 스레드를 생성을 하고 작업을 시작을 합니다. Lock을 얻어서 성공적으로 작업을 마치면 successCount가 올가고 lock을 얻기 못해서 작업을 하지 못한다면 failureCount가 올라갑니다. 그래서 예상되는 결과는 1개만 성공을 하고 나머지는 실패를 하니 successCount 1, failureCount는 9입니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/beba69b5-40c3-4071-b5a4-b211be649105/image.png" alt=""></p>
<p>결과를 보면 우리가 원하던 결과가 나온 것을 볼 수 있습니다.</p>
<h3 id="마치며">마치며</h3>
<p>Redisson을 이용을 해서 분산 락을 구현을 하고 실제 프로젝트에 적용을 해보았습니다. Redisson을 사용을 하면서 분산 서버 상황에서도 동시성을 잘 제어를 할 수 있게 되었습니다. 하지만 한 가지 아쉬운 것을 분산 락을 구현을 한 부분의 가독성이 조금 떨어진다는 것입니다. 그래서 다음에는 이 부분을 가독성을 높히는 작업을 해보자 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] In-Memory-Database와 JWT 토큰 - 대용량 트래픽 (3)]]></title>
            <link>https://velog.io/@sang_hyeok_2/Project-In-Memory-Database%EC%99%80-JWT-%ED%86%A0%ED%81%B0-%EB%8C%80%EC%9A%A9%EB%9F%89-%ED%8A%B8%EB%9E%98%ED%94%BD-3</link>
            <guid>https://velog.io/@sang_hyeok_2/Project-In-Memory-Database%EC%99%80-JWT-%ED%86%A0%ED%81%B0-%EB%8C%80%EC%9A%A9%EB%9F%89-%ED%8A%B8%EB%9E%98%ED%94%BD-3</guid>
            <pubDate>Tue, 29 Oct 2024 12:20:11 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<p>저번 포스트에서는 Scale Out의 세션 불일치 문제의 해결 방식으로 Sticky Session, Session-Clustering의 대해서 알아보았습니다. Sticky Session의 경우, 고정된 사용자와 서버를 사용하기 때문에 오히려 트래픽이 더 늘어나 Scale Out의 장점을 살리지 못 할 수 있었고 Session-Clustering의 경우에는 각 서버의 session의 데이터를 복사를 해야 하기 때문에 성능이 저하가 되는 단점이 있었습니다. 그래서 이번 포스트에서는 로그인을 할 때 세션 불일치 문제를 해결할 수 있는 다른 방법들을 알아보고 어떤 방법이 현재 프로젝트에 적합한 지를 알아보겠습니다.</p>
<h3 id="session-storage을-알아보자">Session Storage을 알아보자</h3>
<p>세션 불일치 문제를 해결할 수 있는 방법 중 하나가 Session Storage를 이용을 하는 것이다.
Session Storage란 기존의 서버의 session을 이용하는 것이 아니라 외부에 따로 session 저장소를 만들어서 서버들이 이 session 저장소를 공유하도록 하는 방법을 말한다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/2b6cc361-ee30-45ed-b558-cd7a8f68d6ad/image.png" alt=""></p>
<p>Session Storage를 이용을 하면 서버를 추가를 하더라도 추가한 서버에 Session Storage 주소만 추가를 하면 되기 때문에 기존 서버는 수정할 부분이 없다. 또한 같은 Session Storage를 공유를 하기 때문에 다른 곳에 복제를 해야 하는 과정을 거치지 않아서 성능적으로 좋다. </p>
<h4 id="session-storage의-종류">Session Storage의 종류</h4>
<p>먼저, DiskDatabase와 In-Memory-Database가 있다.</p>
<p>DiskDatabase는 디스크에 데이터를 저장하는 것을 말합니다. 즉, 세션을 disk에 저장을 합니다. DiskDatabase의 장점으로는 비휘발성으로 전원 공급이 안 되더라도 데이터가 저장이 계속 되어 있는 장점이 있습니다. 하지만 디스크에 저장이 되기 때문에 데이터를 CPU까지 가지고 올 때 시간이 많이 걸린다는 단점이 있습니다.</p>
<p>In-Memory-Database의 경우 메모리에 세션을 저장을 하는 것을 말합니다. 그렇기 때문에 데이터를 가지고 오는 속도가 굉장히 빠릅니다. 하지만 휘발성이기 때문에 서버가 종료가 되거나 전원의 공급이 되지 않는다면 데이터가 날아가 버리는 단점이 있습니다. </p>
<p>이 두가지 종류 중에 어떤 종류가 Session Storage로 사용하기에 적합할까요? 
In-Memory-Database를 사용하는 것 적합하다고 생각이 듭니다. 그 이유는 Session에 저장되는 데이터를 보면 영구적으로 저장이 필요없는 데이터입니다. 오히려 영구적으로 저장을 하면 안 되는 데이터들도 있습니다. 로그인을 예시로 들자면 session에 로그인 정보를 저장하고 판단을 하는데 일정 시간이나 기간이 지나면 데이터를 지우도록 합니다. 그렇기 때문에 In-Memory-Database를 사용하는 것이 적합하고 생각합니다.</p>
<h4 id="in-memory-database는-단점이-뭐지">In-Memory-Database는 단점이 뭐지?</h4>
<p>그렇다면 로그인을 구현을 할 때 In-Memory-Database의 단점은 뭘까요? 
In-Memory-Database는 결국 외부 저장소입니다. 그렇기 때문에 외부에서 데이터를 가지고 와야 하기 때문에 시간이 더 오래 걸릴 수 있습니다. Session의 경우는 서버 내부에 있기 때문에 Session 보다는 시간이 더 걸립니다. 또한 session의 경우 다른 서버의 session의 복제를 하지만 In-Memory-Database의 경우 하나이기 때문에 In-Memory-Database가 문제가 생기거나 다운이 되어 버린다면 모든 서버의 영향을 주는 경우가 발생을 합니다.</p>
<h3 id="jwt-토큰-방식-알아보기">JWT 토큰 방식 알아보기</h3>
<p>JWT 토큰 방식은 로그인을 할 때 사용자에게 JWT 토큰을 발급을 하고 이후에 요청을 할 때 발급한 토큰을 가지고 인증처리를 하는 방식을 말합니다. JWT 토큰은 클라이언트에서 요청을 보낼 때 같이 넣어서 보내면 토큰을 검증을 하고 인증을 해줍니다. JWT 토큰 방식의 장점은 무상태로 진행이 된다는 것입니다. 무상태란 요청을 처리를 할 때, 이전 요청의 상태나 다른 데이터를 참조하지 않고 독립작으로 처리를 한다는 장점이 있습니다. JWT 토큰을 이용하게 되면 로그인을 할 때 세션 불일치 문제를 걱정하거나 외부에 저장소를 만들 필요가 없어집니다. 저는 이러한 이유로 JWT 토큰을 이용하는 것이 프로젝트의 로그인을 구현하는 것에 있어서 적합하고 판단을 했습니다.</p>
<h3 id="jwt-토큰으로-구현하기">JWT 토큰으로 구현하기</h3>
<h4 id="loginfilter로-로그인-했을-때-token을-만들어서-넘겨주기">LoginFilter로 로그인 했을 때, Token을 만들어서 넘겨주기</h4>
<pre><code class="language-java">@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;
    private final JWTUtil jwtUtil;

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

        String email = request.getParameter(&quot;email&quot;);
        String password = request.getParameter(&quot;password&quot;);

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(email, password);
        return authenticationManager.authenticate(authenticationToken);

    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {

        ObjectMapper objectMapper = new ObjectMapper();
        CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal();

        String email = customUserDetails.getEmail();
        Long id = customUserDetails.getId();
        String token = &quot;Bearer &quot; + jwtUtil.createJwt(email, id);

        LoginResponseDTO loginResponseDTO = new LoginResponseDTO(id, email, token);
        String result = objectMapper.writeValueAsString(loginResponseDTO);

        response.addHeader(&quot;Authorization&quot;,  token);
        response.setContentType(&quot;application/json&quot;);
        response.setCharacterEncoding(&quot;utf-8&quot;);

        response.getWriter().write(result);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
    }

}</code></pre>
<p>먼저, LoginFilter을 만들어 줍니다. LoginFilter는 로그인을 했을 때 가입이 된 사용자가 맞는지 확인을 하고 토큰을 새로 만들어줍니다. 그리고 새로 만든 토큰을 응답 헤더에 Authorization 이라는 이름으로 담아서 클라이언트에게 보내줍니다.
successfulAuthenticationa메소드가 Token을 새로 만들어 주고 이를 요청 헤더에 담아서 보내주는 역할을 하는 메소드입니다.</p>
<h4 id="login-후-요청을-할-때-token-검증하기">Login 후 요청을 할 때, Token 검증하기</h4>
<pre><code class="language-java">@RequiredArgsConstructor
public class JWTFilter extends OncePerRequestFilter {

    private final JWTUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authorizationHeader = request.getHeader(&quot;Authorization&quot;);

        if (authorizationHeader == null || !authorizationHeader.startsWith(&quot;Bearer &quot;)) {
            throw new GlobalCommonException(AuthErrorResponsive.ABNORMAL_TOKEN);
        }

        String token = authorizationHeader.replace(&quot;Bearer &quot;, &quot;&quot;);

        if(!jwtUtil.isExpired(token)) {
            Authentication authentication = jwtUtil.getAuthentication(jwtUtil.getEmail(token));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }



        filterChain.doFilter(request, response);
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        return StringUtils.startsWithAny(request.getRequestURI(), &quot;/login&quot;);
    }

}</code></pre>
<p>doFilterInternal메소드를 보면 로그인 후 요청을 보낼 그 요청을 받아서 먼저, 토큰을 추출을 하고 검증을 하는 과정을 거칩니다. 그리고 검증이 완료가 되면 인증 정보를 secutiryContextHolder에 담아줍니다. 이후에 요청을 완료를 하고 응답을 넘겨줍니다.</p>
<h4 id="결론">결론</h4>
<p>Scale Out으로 인한 세션 불일치를 문제를 해결하는 방식으로 외부에 Session Storage를 두는 방법에 대해서 알아보았습니다. Session Storage에는 DiskDatabase와 In-Memory-Database가 있고 DiskDatabase는 영구적으로 저장이 가능하지만 속도가 느리고 In-Memory-Database는 속도는 빠르지만 영구적으로 저장이 불가능합니다. 그래서 세션 정보는 영구적으로 저장을 할 필요가 없기 때문에 Session Storage를 사용을 한다면 In-Memory-Database가 적합하다고 판단을 하였습니다. 하지만 Session Storage의 경우 외부 서버이기 때문에 데이터를 가지고 오는 시간이 걸리고 또 Session Storage의 문제가 생기면 모든 서버에 영향을 줄 수가 있는 단점이 있었습니다. 그래서 저는 무상태로 독립적인 요청을 할 수 있는 JWT 토큰으로 로그인을 구현하는 것이 더 적합하다고 판단을 하였습니다. JWT은 로그인 시 JWT 토큰을 발급을 하고 그 토큰으로 인증 처리 후 응답을 줍니다. 그렇기 때문에 로그인에 한해서 session이 필요가 없고 외부 Session Storage를 둘 필요가 없습니다. 그래서 JWT 토큰 방식이 더 적합하다고 판단을 했습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Redis로 동시성 제어하기 - Spin Lock]]></title>
            <link>https://velog.io/@sang_hyeok_2/Project-Redis%EB%A1%9C-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4%ED%95%98%EA%B8%B0-Spin-Lock</link>
            <guid>https://velog.io/@sang_hyeok_2/Project-Redis%EB%A1%9C-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4%ED%95%98%EA%B8%B0-Spin-Lock</guid>
            <pubDate>Sat, 26 Oct 2024 17:43:07 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<p>이전 포스트에서는 Redis에서 기본으로 제공을 하는 원자성 명령어를 통해서 동시성을 제어하는 법을 알아보고 프로젝트에 적용을 해보았습니다. 하지만 작업을 하려던 스레드가 Lock을 획득을 하지 못 했을 때, 작업을 중단이 되면서 인기 검색어의 count를 올리는 작업을 하지 못 하지 못 하면서 원하는 결과를 주지 못 하여서 적합하지 않다고 판단을 하였습니다. 그래서 이번 포스트에서는 <strong>Redis를 이용한 방식인 Spin Lock을 사용해서 분락 락을 간단히 구현</strong>을 해서 동시성을 제어를 해보도록 하겠습니다.</p>
<h3 id="spin-lock하고-분산-락이-뭔데">Spin Lock하고 분산 락이 뭔데??</h3>
<p><strong>Spin Lock은 임계구역에 Lock을 얻지 못 해서 대기를 할 때, Lock을 얻을 때까지 루프를 돌면서 Lock을 얻기 위해서 시도를 하는 방식</strong>을 말합니다.
<img src="https://velog.velcdn.com/images/sang_hyeok_2/post/26e251c5-5d9a-40ca-a95c-89a7c602e3ec/image.png" alt="">
위의 그림을 보면서 설명을 하면 스레드가 공유자원을 활용하거나 임계 구역에서 Lock을 걸고 작업을 하고 작업이 끝나면 Lock을 해제를 합니다. 스레드가 작업을 하는 동안 대기를 하고 있는 스레드는 공유자원이나 임계구역의 상태를 확인을 해야 하는데 확인을 하는 방법으로 루프를 활용을 해서 계속해서 확인을 하는 방법입니다.</p>
<p><strong>분산 락은 여러 서버 혹은 여러 프로세스가 동일한 자원에 동시에 접근하지 못 하도록하는 방법</strong>입니다. 다중 서버를 사용하고 있다면 각각에 서버에서 실행이 되고 있는 애플리케이션에서 동시에 같은 자원에 접근을 하려고 한다면 이를 마찬가지로 동시성 문제가 발생을 합니다. 이 분산 환경에서 동시성 제어를 해주는 방법이 분산 락입니다.</p>
<h3 id="spin-lock으로-분산-락을-구현해-적용해보자">Spin Lock으로 분산 락을 구현해 적용해보자</h3>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class PopularSearchService {

    private final StringRedisTemplate redisTemplate;

    private static final String LOCK_PREFIX = &quot;lock:search:&quot;;
    private static final String SEARCH_RANKING_KEY = &quot;popular:search:ranking&quot;;
    private static final int RETRY_DELAY = 100;
    private static final int LOCK_EXPIRY_TIME = 5;
    private static final int MAX_RETRIES = 20;


    public void incrementSearchCount(String keyword) {
        String lockKey = LOCK_PREFIX + keyword;
        int retries = 0;

        while (retries &lt; MAX_RETRIES) {
            Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, &quot;LOCKED&quot;, LOCK_EXPIRY_TIME, TimeUnit.SECONDS);

            if (Boolean.TRUE.equals(lockAcquired)) {
                try {
                    updateSearchCount(keyword);
                } finally {
                    releaseLock(lockKey);
                }
                return;
            } else {
                retries++;
                try {
                    Thread.sleep(RETRY_DELAY);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

        }
        System.out.println(&quot;다른 프로세스에서 이미 락을 획득했습니다. 요청을 무시합니다.&quot;);
    }

    private void releaseLock(String lockKey) {
        redisTemplate.delete(lockKey);
    }

    private void updateSearchCount(String keyword) {
        redisTemplate.opsForZSet().incrementScore(SEARCH_RANKING_KEY, keyword, 1);
    }
}</code></pre>
<p>이전 포스트에서 공유를 했던 원자성 명령어를 통해 구현한 동시성 제어 방법의 로직과 조금 차이가 있습니다. RETRY_DELAY의 경우 Lock을 얻지를 못 했을 때 얼마나 대기를 할지에 대한 값입니다. LOCK_EXPIRY_TIME는 Lock을 가지고 얼마나 유지할 수 있느지에 대한 시간입니다. MAX_RETRIES는 Lock울 갖기 위해서 최대 얼마까지 시도를 할 지에 대한 값입니다.</p>
<p>incrementSearchCount메소드를 살펴보면 검색어마다 고유한 키를 설정을 하고 while문을 통해서 MAX_RETRIES의 값만큼 반복을 하면서 Lock 획득을 위해서 setIfAbsent 메소드를 사용을 하고 있습니다. Lock 획득에 성공을 하면 updateSearchCount 메소드를 실행을 해서 Count를 올리고 Lock을 삭제를 합니다. 만약 Lock을 획득을 하지 못 하면 RETRY_DELAY만큼 대기를 하다가 다시 Lock을 얻기 위해서 시도를 합니다. 이러한 방식으로 Spin Lock을 통해서 구현을 했습니다.</p>
<h3 id="결과-살펴보기">결과 살펴보기</h3>
<p>이제 이 로직으로 동시성을 테스트를 해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/75e3af2f-5103-47c3-bbcb-2fc24b12651b/image.png" alt=""></p>
<p>결과가 예상했던 결과와 달랐습니다. 그 이유는 MAX_RETRIES 만큼 재시도를 했음에도 불구하고 Lock을 얻지 못 했기 때문에 결국 스레드가 작업을 하지 못 하고 중단이 되는 경우가 발생했기 때문입니다.</p>
<h3 id="프로젝트에-맞는-방법은">프로젝트에 맞는 방법은?</h3>
<p>제가 내린 결론은 S<strong>pin Lock은 현재 프로젝트에 맞지 않다는 판단</strong>입니다. 그 이유는 먼저 위에 결과 살펴보기에서 처럼 우리 원하는 결과를 확실하게 가져다준다는 보장이 없습니다. 큰 경기에 경우 제가 기준으로 잡아던 8000명 보다 더 많은 인원이 몰리는데 더 몰리게 되면 원하는 결과가 나온다는 보장이 휠씬 떨어집니다. </p>
<p>또 <strong>Spin Lock을 사용하게 되면 CPU 자체의 부담</strong>이 커집니다. Spin Lock은 Lock을 얻을 때까지 계속 반복을 하는 방법입니다. 만약 임계 구역이 여러 개라면, 락의 반환이 오래 걸린다면 부하가 많이 걸릴 가능성이 매우 큽니다.</p>
<p>또한 <strong>분산 락을 완벽하게는 구현을 하지 못한다</strong>는 점입니다. Redis는 단일 스레드입니다. 근데 만약 어떤 프로세스가 접근을 해서 작업을 하다가 서버가 다운이 됐다고 했을 때, 다른 프로세스에서 Lock 풀렸다고 판단을 하고 접근을 하게 됩니다. 이는 동시성 제어가 되지 않은 점입니다.</p>
<p>마지막으로 <strong>비용</strong>입니다. 회사나 같이 하는 분들이 있다면 모르겠지만 프로젝트는 현재 혼자 진행을 하고 있고 이에 대한 비용은 저 혼자 지불을 합니다. 그러다 보니 비용을 생각을 안 할 수 없습니다. 이를 고려를 했을 때 redis를 사용하는 비용이 방법을 쓰는 것보다 크다고 판단이 들었습니다.</p>
<p><strong>결론적으로 프로젝트에서 인기 검색어에 대한 동시성 문제는 Pessimitic Lock을 사용하는 것이 가장 적합</strong>다고 판단을 했습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Redis로 동시서 제어하기 - SETNX]]></title>
            <link>https://velog.io/@sang_hyeok_2/Project-Redis%EB%A1%9C-%EB%8F%99%EC%8B%9C%EC%84%9C-%EC%A0%9C%EC%96%B4%ED%95%98%EA%B8%B0-SETNX</link>
            <guid>https://velog.io/@sang_hyeok_2/Project-Redis%EB%A1%9C-%EB%8F%99%EC%8B%9C%EC%84%9C-%EC%A0%9C%EC%96%B4%ED%95%98%EA%B8%B0-SETNX</guid>
            <pubDate>Sat, 26 Oct 2024 08:33:22 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가며">들어가며</h3>
<p>저번 포스트에서는 인기검색어를 구현을 하는데 있어서 Synchronized 키워드와 Optimistic Lock, Pessimitic Lock을 사용하고 어느 방법이 현재 프로젝트에 적합한 지를 알아 보았습니다. 그리고 Pessimitic Lock을 사용하는 것이 현재 프로젝트에서 적합하다는 판단까지 내렸습니다.
그런데 Pessimitic Lock이 속도가 걸린다는 단점이 있었습니다. 속도가 느리다는 것은 성능의 문제와 관련이 있습니다. 그래서 이 속도를 높히는 방법으로 <strong>In-Memory-Database인 Redis를 통해서 동시성을 제어</strong>를 해보고자 합니다. 이 포스트에서 <strong>Redis의 SETNX를 이용한 방법</strong>에 대해서 알아 보겠습니다.</p>
<h3 id="redis와-setnx-간단하게-알아보자">Redis와 SETNX 간단하게 알아보자</h3>
<p>Redis는 In-Memory-Database입니다. In-Memory-Datadase는 무엇일까요?</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/e6beacf2-3c5b-4f63-a16a-51b6e171523a/image.png" alt=""></p>
<p>보통 <strong>데이터베이스는 디스크에 저장</strong>이 됩니다. 대표적으로 <strong>MySql, MariaDB, Postgresql</strong> 등이 있습니다. 이렇게 <strong>디스크에 저장이 되는 데이터 베이스는 비휘발성으로 영구적으로 저장이 가능</strong>합니다. 하지만 디스크에 저장이 되기 때문에 디스크에서 CPU까지 정보를 가지고 와야 합니다. 그러다 보면 <strong>시간이 오래 걸리는 단점</strong>이 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/897b5ff3-f9fa-441a-9b79-583701fdcd0a/image.png" alt=""></p>
<p><strong>In-Memory-Database는 메모리에 저장이 되는 데이터 베이스</strong> 입니다. 즉, 메모리에 데이터를 저장하고 읽는 것입니다. 메모리에 저장이 되기 때문에 디스크에 저장이 된 <strong>데이터보다 빠르게 가지고 올 수 있습니다.</strong> 하지만 메모리는 휘발성이기 때문에 서버가 다운이 되거나 <strong>컴퓨터가 종료가 되면 데이터가 유실이 되는 단점</strong>이 있습니다.</p>
<p><strong>Redis는 In-Memory_Database 중 하나</strong>입니다. <strong>SETNX은 Redis에서 기본적으로 제공을 하는 원자성 명령어</strong>입니다. 주로 <strong>분산 락을 구현을 하거나 동시성을 제어하기 위해서 사용</strong>이 됩니다. <strong>Redis는 단일 스레드이기 때문에 원자성을 유지해 주는데 이러한 특성 때문에 SETNX가 원자적으로 실행이 가능</strong>합니다. SETNX명령어는 키가 존재하지 않을 때만 값을 설정을 하고 키가 있을 때는 작업을 진행을 하지 않습니다. 이 명령어가 바로 아래의 나와 있는 명령어입니다.</p>
<pre><code>SETNX key value</code></pre><p>이러한 방식을 이용을 해서 동시성을 제어를 하는 Lock을 만들 수 있습니다.</p>
<h3 id="그렇다면-프로젝트에-어떻게-사용을-할-건데">그렇다면 프로젝트에 어떻게 사용을 할 건데?</h3>
<p>이제 redis와 SETNX을 이용해서 포로젝트의 인기 검색어에 적용을 해보겠습니다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class PopularSearchService {

    private final StringRedisTemplate redisTemplate;

    private static final String LOCK_PREFIX = &quot;lock:search:&quot;;
    private static final String SEARCH_RANKING_KEY = &quot;popular:search:ranking&quot;;



    public void incrementSearchCount(String keyword) {
        String lockKey = LOCK_PREFIX + keyword;

        Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, &quot;LOCKED&quot;, 5, TimeUnit.SECONDS);

        if (lockAcquired) {
            try {
                updateSearchCount(keyword);
            } finally {
                releaseLock(lockKey);
            }
        } else {
            System.out.println(&quot;다른 프로세스에서 이미 락을 획득했습니다. 요청을 무시합니다.&quot;);
        }

    }

    private void releaseLock(String lockKey) {
        redisTemplate.delete(lockKey);
    }

    private void updateSearchCount(String keyword) {
        redisTemplate.opsForZSet().incrementScore(SEARCH_RANKING_KEY, keyword, 1);
    }

}</code></pre>
<p>먼저, LOCK_PREFIX의 경우 Lock을 설정하기 위한 키의 접두사입니다. SEARCH_RANKING_KEY는 인기 검색어와 횟수가 저장이 되는 키의 접두사입니다.
incrementSearchCount메소드는 인기검색어의 count를 올려주는 메소드입니다. 이 메소드에서는 Redis의 setIfAbsent를 통해서 동시성을 제어를 하기 위한 Lock을 만들어 줍니다. lockKey를 만들어서 그 lock가 없으면 값을 &quot;LOCKED&quot;라는 값으로 설정을 하고 여기서 값을 얻는데 성공을 하면 True, 이미 값이 존재를 해서 실패를 하면 False를 반환을 합니다.
만약 True라면 updateSearchCount 메소드를 실행을 하는데 Sorted Set의 해당 검색어를 자동 연산을 시킵니다. 자동 연산이 되면 또 자동으로 Count 값에 의해서 자동 정렬이 됩니다. 그리고 finally 구문이 실행이 되면서 위에서 설정이 되었던 lockKey를 삭제를 해줍니다.</p>
<h3 id="이제-결과를-볼까">이제 결과를 볼까?</h3>
<p>이제 이 로직을 했을 때 동시성 테스트를 해보겠습니다. 조건은 저번 포스트와 마찬가지로 8000으로 하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/26d9c965-0f79-4507-b8d4-b4231ee9d01a/image.png" alt=""></p>
<p>결과를 보니 711이 나왔습니다. 제가 원하던 8000이라는 값은 나오지 않았습니다. 어떻게 된 것일까요?
이 방법은 Lock을 획득을 했을 때 작업을 처리하도록 했습니다. 즉, Lock을 획득을 하지 못 하면 작업을 할 수 없는 것입니다. 전에 사용을 했던 Pessimitic Lock의 경우 Lock을 얻지 못 하면 Lock을 얻을 때까지 대기를 합니다. 하지만 SETNX의 경우 Lock을 얻지 못 하면 대기를 하는 것이 아니라 바로 작업이 종료가 되게 됩니다. 이러한 이유로 8000의 동시성 테스트를 했을 때, 711만 카운트가 된 원인입니다.</p>
<p>이러한 방식을 보았을 때, 방법을 현재 프로젝트의 인기검색어 동시성 문제에 대해서 적합하지 않은 방법이라고 판단을 했습니다.</p>
<p>그래서 다음 포스트에서는 redis로 Spin Lock을 이용한 분산 락을 구현을 해서 동시성을 제어를 해보고 Pessimitic Lock과 비교을 어떤 방법이 더 적합한지 고민을 해보도록 하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] 인기 검색어 동시성 문제 해결하기]]></title>
            <link>https://velog.io/@sang_hyeok_2/Project-%EC%9D%B8%EA%B8%B0-%EA%B2%80%EC%83%89%EC%96%B4-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sang_hyeok_2/Project-%EC%9D%B8%EA%B8%B0-%EA%B2%80%EC%83%89%EC%96%B4-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 19 Oct 2024 14:25:51 GMT</pubDate>
            <description><![CDATA[<h3 id="내가-예상한-것과-db에-저장이-된-게-다르네">내가 예상한 것과 DB에 저장이 된 게 다르네,,,</h3>
<p>프로젝트를 진행을 하면서 인기 검색어를 구현을 하게 되었습니다. 인기 검색어는 사용자가 검색을 한 단어를 DB에 저장을 하고 그 단어가 검색이 될 때마다 카운트를 올려서 1씩 증가를 하도록 했습니다. 그런데 이 부분에서 동시성 테스트를 할 필요성을 느꼈습니다. <strong>티켓 판매가 오픈이 되는 날이 되면 사용자들이 몰리기 때문에 동시성 문제 발생을 할 수 있다고 판단</strong>을 하였습니다. 그래서 테스트를 진행을 해보았습니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/c16b16c1-3c33-4563-836f-8096c63a9dde/image.png" alt=""></p>
<p>위에 사진처럼 <strong>동시에 접근하는 수는 8000명</strong>으로 잡았습니다. 그 이유는 K리그 평균 관중 수가 8000명 정도이기 때문입니다. 수원이라는 단어로 검색을 하였고 <strong>제가 원하는 결과는 DB의 카운트에 8000이 들어가 있는 결과</strong>였습니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/8d504ae8-5d82-4c2a-b603-8f2cae7fbe58/image.png" alt=""></p>
<p>하지만 <strong>결과는 831의 카운트</strong>만이 올라갔습니다. 즉, <strong>동시성 문제가 발생</strong>을 한 것입니다.
그렇다면 동시성 문제는 무엇인데 8000이 동시에 요청을 했는데 831번만 카운트가 올라간 것일까요?
동시성 문제에 대해서 알아보겠습니다.</p>
<h3 id="동시성-문제가-뭔데">동시성 문제가 뭔데??</h3>
<p>동시성 문제에 대해서 알아보겠습니다.
<strong>동시성 문제는 여러 사용자가 같은 자원이나 데이터를 사용하기 위해서 동시에 접근을 하고 수정을 하려고 할 때 발생을 하는 문제</strong>입니다. 
<img src="https://velog.velcdn.com/images/sang_hyeok_2/post/ea092a19-f8c0-42fd-83c1-f86f9fe6fca3/image.png" alt=""></p>
<p>예시를 들어보겠습니다.</p>
<blockquote>
<p>모임 통장을 공유하는 사용자A와 사용자B가 있습니다. 모임 통장에는 10000원이 들어있습니다.
사용자A와 사용자B는 각각 회비로 2000원, 5000원을 냅니다. 두 사용자가 모두 회비를 내면 잔액는 17000원이 되어야 합니다. 회비를 내는 날이 되어서 둘이 동시에 내거나 사용자A가 진행을 하던 중 사용자 B가 돈을 낸다고 가정을 해봅시다.
사용자A가 먼저 현재 잔고를 조회를 합니다. 그러면 현재 잔고가 10000원이라는 것을 알게됩니다. 이때, 사용자B도 마찬가지로 회비를 내기 위해서 잔고를 조회를 합니다. 이 때, 사용자에는 자신이 내야 하는 2000원를 넣게 되고 잔고 12000원이 됩니다. 사용자B도 회비를 5000원을 내서 기존에 조회를 했던 10000원과 5000원이 합쳐저 총액이 15000원이 됩니다.
총 잔고 17000원이 15000원이 되는 문제가 발생을 합니다. 
두 사용자가 같은 데이터를 동시에 접근을 했고 각자 다르게 업데이트를 하면서 발생하게 된 문제입니다.
이를 동시성 문제라고 합니다.</p>
</blockquote>
<p>그렇다면 이 동시성 문제를 어떻게 해결할 수 있을까요?</p>
<h3 id="그래서-동시성을-해결할-수-있는-방법이-뭔데">그래서 동시성을 해결할 수 있는 방법이 뭔데?</h3>
<p>동시성을 해결하는 방법에는 <strong>Synchronized, Opyimistic Lock(낙관적 락), Pessimistic Lock(비관적 락)</strong>이 있습니다. 이 방법들의 동작하는 방식에 대해서 알아보겠습니다.</p>
<h4 id="synchronized">Synchronized</h4>
<p>첫 번째는 Sychronized라는 자바의 키워드를 사용하는 것입니다.</p>
<pre><code class="language-java">public class SearchKeywordService {

    private int searchCount;

    public synchronized void incrementSearchCount() {
        searchCount++;
    }

    public int getSearchCount() {
        return searchCount;
    }
}</code></pre>
<p>동작 방식을 설명을 하기 위해서 간단한  코드를 가지고 왔습니다.
incrementSearchCount 라는 메소드에 synchronized 키워드를 사용을 하고 있습니다.
이렇게 되면 synchronized 키워드가 공유 자원에 대해서 동시에 접근하는 것을 제어를 할 수 있습니다.
synchronized는 모니터 락을 사용을 해서 공유 자원을 보호를 합니다.
먼저, 작업을 수행을 하려는 스레드가 synchronized가 관리하는 임계 영역이나 메소드를 실행하려고 할 때, 락을 얻으려고 하고 얻게 되면 작업을 시작을 합니다. 작업을 시작을 하면서 다른 스레드들이 임계 영역에 접근을 하면 락이 이미 다른 스레드가 가지고 있기 때문에 대기를 하게 됩니다. 그리고 작업 중인 스레드가 작업을 마치면 락을 해제를 하고 대기 중인 다른 스레드가 작업 기회를 얻습니다.</p>
<h4 id="optimistic-lock낙관적-락">Optimistic Lock(낙관적 락)</h4>
<p><strong>Optimistic Lock은 트랜잭션 충돌이 발생이 되지 않은 것이라고 예상을 하고 사용하는 Lock</strong>입니다.
이 말은 접근하고자 하는 데이터에 Lock을 걸지 않고 진행을 하다가 트랜잭션 충돌이 발생을 했을 때 트랜잭션을 롤백을 시키는 방법입니다.</p>
<pre><code class="language-java">@Entity
@Getter
public class SearchKeyword {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String keyword;

    private int searchCount;

    @Version  // 낙관적 락을 위한 버전 필드
    private int version;

}</code></pre>
<p>Optimistic Lock을 사용한 간단한 예시를 가지고 왔습니다.
이 부분은 인기 검색어의 엔티티인데 필드 중에 version이 있고 어노테이션으로 @Vesion이 있다.
이 버전을 통해서 동시성을 제어합니다. 트랜잭션이 완료가 되면 이 버전을 증가를 시켜서 이 버전을 비교해서 트랜잭션을 롤백을 시키면서 동시성을 제어를 합니다.</p>
<p>예시를 들어 보겠습니다.</p>
<blockquote>
<p>만약 사용자A가 검색을 통해서 검색어의 카운트를 증가하려고 합니다. 이 때 버전은 1입니다.
마찬가지로 사용자B가 같은 검색어를 검색하면서 카운트를 증가를 시키려고 합니다. 이 때 버전도 1입니다.
이 때, 사용자A가 검색을 한 작업이 종료가 되면서 카운트가 올라가고 버전 또한 올라갑니다. 그래서 버전이 2가 됩니다.
사용자B가 검색을 한 작업 완료가 된 버전과 비교를 했을 때, 사용자B의 작업은 버전 1이고 현재 데이터의 버전은 2이기 때문에 버전 차이가 발생을 합니다.
이 차이로 인해서 사용자B의 작업은 롤백이 되게 됩니다.</p>
</blockquote>
<p>데이터의 Lock을 거는 것이 아니라 version을 통해서 트랜잭션 충돌이 일어났을 때 롤백을 시키면서 동시성을 제어를 합니다.</p>
<h4 id="pessimitic-lock비관적-락">Pessimitic Lock(비관적 락)</h4>
<p>Pessimitic Lock은 트랜잭션 충돌이 일어날 거라고 예상을 하고 사용을 하는 Lock입니다.
접근을 하고자 하는 데이터의 Lock을 걸어서 Lock 걸려 있다면 그 데이터에는 접근할 수 없게 만드는 방법입니다.</p>
<pre><code class="language-java">
@Repository
public interface SearchKeywordRepository extends JpaRepository&lt;SearchKeyword, Long&gt; {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Optional&lt;SearchKeyword&gt; findByKeyword(String keyword);
}</code></pre>
<p>Pessimitic Lock을 사용을 하기 위해서는 @Lock 어노테이션을 통해서 사용을 합니다.
이 어노테이션을 사용을 하면 이 메소드를 통해서 읽거나 수정을 하는 데이터에 Lock이 걸리고 동시성을 제어를 할 수 있게 됩니다.</p>
<p>예시를 들어보겠습니다.</p>
<blockquote>
<p>Optimistic Lock에서 사용을 했던 예시를 사용을 하겠습니다.
사용자A가 검색어를 검색을 하고 사용자B도 검색을 합니다.
사용자A가 접근을 하면 그 검색어 Lock이 걸리게 됩니다. Lock이 걸려 있기 때문에 사용자B는 이 데이터에 접근을 할 수가 없습니다.
사용자A가 작업을 마치고 Lock을 풀게 되면 그제서야 사용자B가 접근을 할 수 있게 됩니다.</p>
</blockquote>
<p>Pessimitic Lock의 경우는 데이터에 Lock을 걸어서 동시성을 제어를 합니다.</p>
<h3 id="그러면-프로젝트에-어떤-방법을-사용을-하지">그러면 프로젝트에 어떤 방법을 사용을 하지??</h3>
<p>프로젝트의 인기 검색어에서는 어떤 방법이 맞을까요? 한 번 각 방법들을 적용을 해보겠습니다!</p>
<h4 id="synchronized-적용해보기">Synchronized 적용해보기</h4>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class PopularSearchService {

    private final PopularSearchRepository popularSearchRepository;

    public synchronized void saveOrUpdatePopularSearch(String keyword) {

        PopularSearchEntity popularSearchEntity = popularSearchRepository.findByKeyword(keyword);

        if (popularSearchEntity != null) {
            popularSearchEntity.update(1);
            popularSearchRepository.save(popularSearchEntity);
        } else {
            PopularSearchEntity newPopularSearchEntity = new PopularSearchEntity(keyword, 1);
            popularSearchRepository.save(newPopularSearchEntity);
        }

    }

    public List&lt;PopularSearchEntity&gt; getPopularKeywords() {
        return popularSearchRepository.findTop10ByOrderBySearchCountDesc();
    }

}
</code></pre>
<p>동시성 문제가 일어나는 <strong>saveOrUpdatePopularSearch메소드에 synchronized 키워드</strong>를 붙혀줍니다.
이제 스레드가 <strong>saveOrUpdatePopularSearch메소드에 접근을 할 때는 모니터 락을 가지게 되고 동시성을 제어</strong>를 해주게 됩니다.</p>
<p>동시성 테스트를 하고 결과를 보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/af716917-3db5-48d1-b22b-1808919ad4ad/image.png" alt=""></p>
<p>결과가 8000으로 동시성이 제어가 된 것을 볼 수 있습니다!
하지만 <strong>Synchronized 키워드에는 단점</strong>이 존재합니다. <strong>바로 하나의 프로세스에서만 적용</strong>이 된다는 것입니다.
즉, <strong>다중 서버 서버가 2대 이상일 경우 2개의 프로세스가 존재를 하기 때문에 결국 동시성을 제어를 할 수 없습니다.</strong>
현재 프로젝트는 대용량 트랙픽을 감당을 해야하기 때문에 서버의 확장 방식을 Scale Out을 사용하고 있습니다. 트래픽이 많아지면 서버의 수가 들어나는 것입니다. 이는 여러 프로세스가 생성이 된다는 것이고 Synchronized 키워드로는 동시성을 제어하는데 한계가 있습니다.</p>
<p>그래서 <strong>Synchronized 키워드는 프로젝트에 적합하지 않은 방식</strong>입니다.</p>
<h4 id="optimistic-lock낙관적-락-사용해보기">Optimistic Lock(낙관적 락) 사용해보기</h4>
<p>이제 Optimistic Lock을 사용을 해보겠습니다.</p>
<pre><code class="language-java">@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = &quot;PopularSearch&quot;)
public class PopularSearchEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;popular_search_id&quot;)
    private Long PopularSearchId;

    @Column(name = &quot;keyword&quot;, nullable = false, unique = true)
    private String keyword;

    @Column(name = &quot;search_count&quot;)
    private int searchCount;

    @Version
    @Column(name = &quot;version&quot;)
    private int version;

    @CreatedDate
    @Column(name = &quot;created_at&quot;, updatable = false)
    private LocalDate createdAt;

    public PopularSearchEntity(String keyword, int searchCount) {
        this.keyword = keyword;
        this.searchCount = searchCount;
    }

    void update (int addCount) {
        this.searchCount = this.searchCount + addCount;
    }

}</code></pre>
<p>위에서 설명을 한 것처럼 <strong>엔티티에 version이라는 필드를 만들고 @Version을 붙여주면 됩니다.</strong>
그러면 말그대로 version이 생기고 그 version을 비교하면서 동시성을 제어를 합니다.
그러면 테스트를 해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/855b25b5-1910-448e-b88e-08c08184b743/image.png" alt=""></p>
<p><strong>테스트를 해보니 893이라는 결과</strong> 나왔습니다. 
Lock을 사용을 해서 동시성을 제어를 했는데 왜 동시성이 제어가 안 되었을까요?
<img src="https://velog.velcdn.com/images/sang_hyeok_2/post/963061a7-d7aa-4bf7-9c5e-1e6f6994afbf/image.png" alt=""></p>
<blockquote>
<p>만약 count라는 값에 Transaction1이 수정을 하려고 합니다. 이 때, 2, 3, 4도 같이 수정을 하려고 접근을 합니다. 이 때, Transactino1이 작업을 할 때 version이 같기 때문에 작업이 이루어집니다. 작업을 하면서 count만 변경을 하는 것이 아니라 version도 같이 변경이 됩니다. 그러면 version이 2가 됩니다. 
나머지 Transaction 2, 3, 4는 version이 1이기 때문에 version이 맞지 않기 때문에 작업을 진행을 하지 못 하게 됩니다.
즉, <strong>처음 작업에 성공을 하고 version을 바꾼 최초의 요청만을 커밋</strong>을 하기 때문입니다.
이러한 이유에서 8000명을 동시에 했을 때 841번 count가 됩니다.</p>
</blockquote>
<p>물론 에러가 발생을 했을 때 재시도를 하는 로직을 짜서 어느 정도 해결을 할 수 있지만 데이터를 수정을 해야 하는 경우 <strong>우리가 원하는 결과를 못 얻을 수 있습니다.</strong>
또한 재시도 로직의 경우 출돌이 많은 경우 재시도 로직이 많이 실행이 되면 DB의 성능이 저하가 될 수 있습니다.</p>
<p>이러한 이유로 데이터의 수정이 많은 인기 검색어에서는 이 <strong>Optimistic Lock도 적합하지 않다라고 판단</strong>을 하였습니다.</p>
<h4 id="pessimitic-lock비관적-lock-사용해보기">Pessimitic Lock(비관적 Lock) 사용해보기</h4>
<p>이제 Pessimitic Lock을 사용해보겠습니다.</p>
<pre><code class="language-java">public interface PopularSearchRepository extends JpaRepository&lt;PopularSearchEntity, Long&gt; {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    PopularSearchEntity findByKeyword(String keyword);

    List&lt;PopularSearchEntity&gt; findTop10ByOrderBySearchCountDesc();

}</code></pre>
<p>Pessimistic Lock은 Repository에 <strong>사용하고자하는 메소드에 @Lock 어노테이션을 붙혀주면 됩니다.</strong>
LockModeType으로 Lock의 타입을 정해줍니다.</p>
<p>그러면 테스트를 해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/2662cfc2-0d9c-4981-967d-d50905d839af/image.png" alt=""></p>
<p>테스트 결과 8000이 나온다는 것을 알 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/9a2633ff-c644-4eb5-9f3d-ad36edab41e2/image.png" alt=""></p>
<blockquote>
<p>Pessimistic Lock의 경우 메소드를 통해서 변경을 하고자 하는 데이터에 접근을 하면 아예 Lock을 걸어 버립니다. 그래서 다른 트랜잭션이 접근을 하려고 할 때, Lock으로 인해서 접근을 하지 못 하고 대기를 하게 되고 이미 작업을 수행 중이던 트랜잭션이 작업을 마치고 Lock을 풀면 대기하던 트랜잭션이 다시 Lock을 걸로 작업을 진행을 합니다.
이렇게 되면 작업을 하는 동안 <strong>다른 트랜잭션이 접근을 하지 못 하기 때문에 데이터의 일관성을 유지</strong>를 할 수 있습니다.</p>
</blockquote>
<p>그래서 <strong>현재 프로젝트의 인기 검색어에는 적합한 방법이라고 판단</strong>을 했습니다.</p>
<h3 id="결론">결론</h3>
<p>인기 검색어는 사용자들이 검색한 양에 따라서 달라집니다. 그래서 데이터가 잘 유지가 되는 것이 중요합니다.
하지만 현재 구현을 한 인기검색어는 많은 사용자들이 동시에 접근을 했을 때, 동시성이 제어가 되지 않아 데이터가 정확하게 유지가 되지 않습니다. 그래서 동시성을 제어하는 방법인 Synchronized, Optimistic Lock(낙관적 락), Pessimitic Lock(비관적 Lock)에 대해서 알아보았습니다.</p>
<blockquote>
<p>Synchronized는 동시성은 되지만 단일서버에서만 적용이 됩니다. 그래서 Scale Out으로 다중 서버가 될 수 있는 현재 프로젝트에서 적합한 방법은 아니라고 판단을 했습니다.</p>
</blockquote>
<blockquote>
<p>Optimistic Lock(낙관적 락)은 트랜잭션이 충돌이 일어났을 때 동시성을 제어를 하는데 이 판단을 version이라는 필드를 통해서 합니다. 그런데 이 version이 맞지 않은 경우 롤백을 시키기 때문에 동시성의 수 만큼 작업을 하지 못 하게 되어서 수정이 필요한 데이터에는 적합하지 않습니다. 그래서 이 Optimistic Lock도 현재의 프로젝트에는 맞지 않은 방법입니다.</p>
</blockquote>
<blockquote>
<p>Pessimitic Lock(비관적 Lock)은 트랜잭션 충돌이 일어날 것을 예상을 하고 변경을 하고자 하는 데이터에 Lock을 걸기 때문에 동시성의 수 만큼 작업을 진행을 할 수 있습니다. 또한 데이터 베이스에서 Lock을 걸기 때문에 다중 서버에서는 데이터의 동일성을 유지를 할 수 있습니다. 그래서 현재의 프로젝트의 인기 검색어에 적합한 방법이라고 판단을 하였습니다.</p>
</blockquote>
<p>그래서 이 프로젝트의 인기검색어에서는 <strong>Pessimitic Lock을 사용하기로 했습니다.</strong></p>
<h3 id="그런데-pessimitic-lock의-단점은-없나">그런데 Pessimitic Lock의 단점은 없나?</h3>
<p>그런데 Pessimitic Lock(비관적 Lock)의 단점은 없을까요?
테스트의 결과를 보면 단점을 하나 알 수 있습니다.</p>
<p>아래 사진은 동시성 제어하지 않았을 때 걸리는 시간입니다.
<img src="https://velog.velcdn.com/images/sang_hyeok_2/post/2641f3c6-b991-4b37-bd71-c1b2c5e0ed6b/image.png" alt=""></p>
<p>아래 사진은 Optimistic Lock으로 제어를 했을 때 걸리는 시간입니다.
<img src="https://velog.velcdn.com/images/sang_hyeok_2/post/cc1d14ba-2349-47ea-ab3d-46a1f696cd89/image.png" alt=""></p>
<p>아래는 사진은 Pessimitic Lock으로 제어를 했을 때 걸리는 사진입니다.
<img src="blob:https://velog.io/99975513-4246-4ecd-afcb-e69fc2806b71" alt="업로드중.."></p>
<p>사진의 시간을 보시면 Pessimitic Lock으로 테스트를 했을 때, 확실하게 시간이 더 걸린다는 것을 알 수 있습니다. 그 이유는 무엇일까요?</p>
<p>위에서 설명을 했듯이 Pessimitic Lock의 경우 데이터의 Lock을 걸어서 동시성을 제어를 합니다. 그래서 Lock이 풀릴 때까지 다른 트랜잭션은 대기를 해야 합니다. 그렇기 때문에 시간이 걸리고 심한 경우 병목 현상으로 성능저하가 일어날 수 있습니다. 지금은 8000명을 대상으로 했을 때 결과인데 더 많은 사용자 몰린다면 시간은 더 늘어날 것입니다. 시간이 늘어나면 사용자가 불편함을 느낄 것입니다. </p>
<p>그렇다면 Pessimitic Lock처럼 데이터의 일관성을 유지를 하면서 속도를 개선하는 방법은 무엇이 있을까요?
그 방법은 다음 포스트에서 알아보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[서버 분산 처리 과정에서 발생하는 데이터 Session 불일치 문제를 어떻게 해결할까? - 대용량 트래픽(2)]]></title>
            <link>https://velog.io/@sang_hyeok_2/%EC%84%9C%EB%B2%84-%EB%B6%84%EC%82%B0-%EC%B2%98%EB%A6%AC-%EA%B3%BC%EC%A0%95%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EB%8D%B0%EC%9D%B4%ED%84%B0-Session-%EB%B6%88%EC%9D%BC%EC%B9%98-%EB%AC%B8%EC%A0%9C%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EA%B2%B0%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@sang_hyeok_2/%EC%84%9C%EB%B2%84-%EB%B6%84%EC%82%B0-%EC%B2%98%EB%A6%AC-%EA%B3%BC%EC%A0%95%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EB%8D%B0%EC%9D%B4%ED%84%B0-Session-%EB%B6%88%EC%9D%BC%EC%B9%98-%EB%AC%B8%EC%A0%9C%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EA%B2%B0%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Thu, 03 Oct 2024 19:55:52 GMT</pubDate>
            <description><![CDATA[<h3 id="scale-out을-선택을-했을-때-발생하는-문제">Scale Out을 선택을 했을 때 발생하는 문제</h3>
<p>지난 포스트에서 서버 확장에 방법으로 Sacle Out이 더 적합하다는 판단을 내렸습니다.
하지만 Scale Out이라는 방식은 테이터 불일치라는 문제가 남아있습니다. 그렇다면 데이터 불일치 문제는 무엇일까요?
<strong>데이터 불일치</strong>은 <strong>서버가 여러대 일때, 서버 간의 session에 저장된 데이터가 일치하지 않은 문제</strong>를 말합니다.
한 가지 예시를 들어보겠습니다. 로그인을 구현을 할 때 여러 방식이 있는데 session에 인증 정보를 저장을 하는 방식이 있습니다. session에 저장이 된 인증 정보를 보고 사용자가 로그인이 된 상황임을 알 수 있게합니다. 하지만 서버가 여러 대인 경우 로그인 한 서버가 아닌 로그인을 하지 않은 서버에 요청을 보내게 되면 그 서버에는 사용자 인증 정보가 없기 때문에 로그인을 벗어난 상태에서 요청을 보내는 문제가 발생을 합니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/559544ee-cce1-4caa-a9ca-5f9c12ab13a5/image.png" alt=""></p>
<p>이러한 상황을 <strong>데이터 불일치</strong> 혹은 <strong>session 불일치</strong>라고 합니다.
그러면 이러한 문제에서 어떠한 해결 방식이 있는 지 한 번 알아보도록 하겠습니다.</p>
<h3 id="데이터-불일치-문제를-해결하기-위한-방법">데이터 불일치 문제를 해결하기 위한 방법</h3>
<p>데이터 불일치를 해결하기 위한 방법에는 Sticky Session(고정 세션)과 Session-Clustering(클라스터 기술)이 있습니다.</p>
<h4 id="sticky-session">Sticky Session</h4>
<p><strong>sticky Session</strong>은 <strong>클라이언트의 요청을 고정된 서버의 session으로만 보내는 것</strong>입니다.
예를 들어 사용자1이 서버3에서 요청을 했다면 모든 사용자1의 요청은 서버3으로 보내는 방법입니다.  </p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/644477e1-6f16-4915-a029-922116d6e998/image.png" alt=""></p>
<p>처음에 로그인을 할 때는 어느 서버로든 요청을 보낼 수 있습니다. 하지만 처음 로그인을 하고 요청이 보내진 서버가 정해지면 이제부터 어떤 요청을 보내든 그 서버로만 요청이 보내집니다.</p>
<p>처음 요청이 보내진 다음 사용자의 정보가 요청을 받은 서버의 session에 저장이 됩니다. 다음 요청에서는 로드 밸런스에 의해서 특정 사용자의 요청이 특정 서버와 연결이 되어서 그 서버만 사용을 하게 됩니다. 특정 서버를 찾는 방법은 요청을 보낼 때, 쿠키에 사용자의 정보를 담아서 보내고 로드밸런서는 요청과 같이 보낸 쿠키의 사용자 정보를 보고 요청을 보낼 특정 서버를 찾아서 요청을 보냅니다.</p>
<p>이러한 방식으로 세션의 데이터가 있으면 데이터의 불일치 문제를 해결하여 사용을 할 수 있습니다. 하지만 이러한 방식에도 문제점이 존재합니다.</p>
<h4 id="트래픽이-특정-서버에-몰리는-문제">트래픽이 특정 서버에 몰리는 문제</h4>
<p>sticky session은 <strong>트래픽이 특정 서버로만 몰리는 문제가 발생</strong>을 할 수 있습니다.
만약 서버1에 맵핑된 사용자들이 갑작스럽게 많은 요청을 보내면 이는 서버1이 다 감당을 해야 합니다.</p>
<p>예를 들어 봅시다.
우리가 회사에서 사용하는 공유기가 있습니다. 이 공유기는 외부IP로서 공유기를 사용을 하는 회사의 직원들은 외부IP 내의 사설IP, 가상의 내부 IP를 각각 가지고 있습니다. 이 사설IP만 보면 다른 IP를 쓰는 것 같지만 결국엔 같은 외부IP를 쓰게 되는 것입니다. 결국 이 공유기를 쓰는 모든 직원들이 이 공유기와 맵핑이 된 서버 session을 사용하게 됩니다. 이는 같은 서버에 트래픽이 집중이 된다는 것을 의미합니다. 그리고 트래픽이 집중이 되면 서버가 다운이 될 수 있는 가능성이 높아집니다.</p>
<p><strong>이는 Scale Out의 트래픽 분산 장점을 살리지 못 하는 방법</strong>입니다. 트래픽을 분산하기 위해서 Scale Out을 사용을 하는데 그 단점을 해결하기 위해서 장점을 잃어버리는 꼴이 되어 버립니다. </p>
<p>또한 <strong>효율성도 떨어집니다.</strong> 만약 트랙픽의 부하로 서버 장애가 발생을 했고 이를 복구를 했다고 가정을 해봅시다. 서버의 session에 사용자들의 정보가 남아 있고 서버는 다시 요청을 받을 준비를 합니다. 하지만 이미 사용자들은 로드밸랜서에 의해서 다른 서버를 사용을 하고 있고 이 서버는 요청을 받을 수 없습니다. 서버의 session에 사용하지 않은 데이터들이 남아 있고 이는 메모리 낭비로 이어지면서 효율성이 떨어지는 문제가 생깁니다.</p>
<h4 id="session-clustering">Session-Clustering</h4>
<p><strong>Session-Clustering</strong>은 <strong>서버를 하나의 클러스터로 묶어서 클러스터 내에서 세션을 공유하는 방법</strong>입니다. 클러스터는 여러 개의 서버를 하나의 시스템처럼 묶어서 동작을 하는 것을 말하는데 말 그대로 로드 밸랜서로 어느 서버로 요청을 보내든 세션을 동일하게 유지하는 방법입니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/df91e35d-3f5e-4510-95a5-36f07d0d5879/image.png" alt=""></p>
<p>이 session-clustering의 경우 WAS마다 방법이 다른데 Spring은 WAS로 Tomcat을 기준으로 알아보겠습니다.
Tomcat은 여러 방법 중에 Session Manager 중 <strong>DelatManager를 이용한 All-to-all Session Replication</strong>과 <strong>BackupManager를 이용한 Primary-secondary Session Replication</strong>을 알아보겠습니다.</p>
<h4 id="all-to-all-session-replication">All-to-all Session Replication</h4>
<p><strong>All-to-all Session Relication</strong> 방식은 말 그대로 <strong>모두 복사를 하는 것</strong>이다. 즉, 어느 한 서버 session의 데이터가 생기면 <strong>나머지 서버 세션에도 같은 데이터를 복사를 하는 것</strong>이다. 모든 서버가 같은 데이터를 가지고 있기 때문에 데이터 불일치 문제를 해결을 할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/9d61c5fd-60ec-412c-b871-b188901af340/image.png" alt=""></p>
<p>하지만 <strong>모든 서버의 session의 데이터를 복사하기 때문에 메모리가 많이 필요</strong>합니다. 또한 데이터가 생길 때마다 <strong>모든 서버 session의 데이터를 입력해야 하기 때문에 서버가 많은 수록 트래픽이 발생</strong>하기 때문에 성능이 크게 떨어질 수 있습니다. 그래서 공식문서에서도 4개 이상의 서버 session에서는 이 방식을 지양하는 방식이라고 합니다.</p>
<h4 id="primary-secondary-session-replication">Primary-Secondary Session Replication</h4>
<p><strong>Primary-Secondary Session Replication</strong>은 <strong>배포가 된 Primary 서버에 데이터가 저장이 되어 있다면 백업 Secondary 서버에만 데이터를 저장</strong>을 합니다. 그리고 나<strong>머지 서버에는 session의 id와 Primary 서버, Secondary 서버의 주소만 저장</strong>이 됩니다. 나머지 서버에 요청이 들어온다면 <strong>Primary 서버에 데이터를 요청을 해서 해당 session id의 해당하는 데이터를 가지고 옵니다.</strong> 최종적으로 데이터 불일치를 해결하게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/a8bb638e-6e33-4d16-b4c5-469666239bd1/image.png" alt=""></p>
<p>하지만 만약 로드 밸랜서가 Primary 서버가 아닌 다른 서버에 요청을 하게 되면 온전한 데이터를 받기 위해서 데이터를 요청을 하는 과정이 발생을 하게 됩니다. 이러한 과정이 <strong>대용량 트래픽에 의해서 수 많은 서버를 사용을 하게 된다면 빈번하게 일어나게 되고 이는 성능 저하 문제</strong>로 이어질 수 있습니다.</p>
<h3 id="sticky-session-session-clustering-다른-방식은-없을까">Sticky Session? Session-Clustering? 다른 방식은 없을까?</h3>
<p>Sticky Session과 Session-Clustering에 대해서 알아보았습니다.
두 방식 중 어떤 방식이 좋을지 고민을 해보았습니다. <strong>Sticky Session 방식은 Scale Out의 장점을 살릴 수 없는 단점을 가지고 있기 때문에 적절한 방법은 아니라고 생각</strong>이 들었습니다. <strong>Session-Clusting 방식</strong>의 경우는 새로운 서버가 생길 때마다 WAS에 새로 셋팅을 해줘야 하는 불편함이 있고 또 <strong>메모리 낭비와 계속 되는 복사로 인해서 성능 저하와 서버를 확장하는데 있어서 한계가 발생</strong>을 합니다. 이 또한 Scale Out의 장점을 살리지 못하는 방법이라고 생각이 들었습니다. 그래서 다른 방법에 대해서 찾아보게 되었습니다.
그 방법은 클러스터처럼 묶어서 관리를 하고 불필요한 복사가 일어나지 않은 <strong>In-Memory DataBase</strong>를 활용하는 방법입니다.</p>
<p>In-Memory DataBase를 활용하는 방법은 다음 포스트에서 알아보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[대용량 트래픽이 오면 서버를 어떻게 확장을 하는게 좋을까? - 대용량 트래픽(1)]]></title>
            <link>https://velog.io/@sang_hyeok_2/%EB%8C%80%EC%9A%A9%EB%9F%89-%ED%8A%B8%EB%9E%98%ED%94%BD%EC%9D%B4-%EC%98%A4%EB%A9%B4-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%99%95%EC%9E%A5%EC%9D%84-%ED%95%98%EB%8A%94%EA%B2%8C-%EC%A2%8B%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@sang_hyeok_2/%EB%8C%80%EC%9A%A9%EB%9F%89-%ED%8A%B8%EB%9E%98%ED%94%BD%EC%9D%B4-%EC%98%A4%EB%A9%B4-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%99%95%EC%9E%A5%EC%9D%84-%ED%95%98%EB%8A%94%EA%B2%8C-%EC%A2%8B%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Mon, 30 Sep 2024 16:24:13 GMT</pubDate>
            <description><![CDATA[<h3 id="대용량-트래픽에-대해서-고민을-하게-된-이유">대용량 트래픽에 대해서 고민을 하게 된 이유</h3>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/5e091f72-8aac-4250-ad12-a877a35b9f27/image.png" alt=""></p>
<p>제가 하고 있는 프로젝트는 스포츠나 뮤지컬 같은 공연을 애매를 하는 프로젝트입니다.
애매 프로젝트의 특징은 애매가 시작이 되면 많은 사용자가 애매를 하기 위해서 몰리기 때문에 그에 따라 트래픽이 증가를 합니다. 트래픽이 증가를 할 수록 이를 감당하는 서버는 버거워지기 시작합니다. 이게 계속 되면 서버가 과부화가 걸릴 수 있고 이는 서버 장애로 이어집니다.
서버 장애는 실제 사용하는 서비스에서 금전적인 손해, 사용자들의 신뢰를 잃을 수 있습니다.
그렇기 때문에 대용량 트래픽에 대한 대비를 해야 합니다.</p>
<h3 id="대용량-트래픽을-대처하는-방법은">대용량 트래픽을 대처하는 방법은?</h3>
<p>만약 대용량 트래픽이 들어온다면 어떻게 처리를 할 수 있을까요?
대용량 트래픽 처리 방법에는 2가지가 있습니다.</p>
<ul>
<li>서버의 성능을 높힌다.</li>
<li>서버를 여러 대 설치를 한다.</li>
</ul>
<p>2가지를 사용하면 대용량 트래픽을 대처를 할 수가 있습니다.
각각의 방법을 <strong>Scale Up</strong>과 <strong>Scale Out</strong>이라고 합니다.</p>
<p>그러면 <strong>Scale Up</strong>과 <strong>Scale Out</strong>에 대해서 자세히 알아보겠습니다.</p>
<h4 id="scale-up">Scale Up</h4>
<p><strong>Scale Up</strong>은 <strong>서버의 성능을 높히는 방법</strong>을 말합니다. 기존 서버의 사양, 예를 들면 CPU, 메모리 등을 추가하거나 더 높은 고사양으로 변경을 하는 것을 말합니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/cf3cf98c-aeee-419a-8c7f-e3ce80532b2d/image.png" alt=""></p>
<p>Scale Up은 <strong>수직적 확장</strong>이라고 합니다. 그 이유는 하나의 서버 성능을 위로 계속 높히기 때문입니다. 이러한 Scale Up에는 어떠한 장점이 있을까요?</p>
<p><strong>서버 하나에 장비를 추가하거나 교체를 하면 되기 때문에 구축이 간편</strong>합니다. 그렇기 때문에 자원 변경으로 인한 영향이 어플리케이션에 적은 편입니다. 그리고 하나의 서버만을 관리하기 때문에 여러 대의 서버를 관리하는 것보다 상대적으로 복잡성이 낮습니다.</p>
<p>하지만 <strong>서버에 붙힐 수 있는 장비가 제한이 되어 있고 이 자원의 한계를 초과를 하면 더 좋은 성능의 서버로 서버 자체를 변경</strong>을 해야 합니다. 서버 자체를 변경하는데 있어서 비용이 많이 든다는 문제가 있습니다. 또한 이러한 비용 문제로 지속적으로 Scale Up 하기에는 여러움이 있습니다. 
결국 서버가 하나이고 이 서버 하나가 모든 트래픽을 감당을 해야 하기 때문에 서버 장애가 발생할 가능성이 크고 이는 서비스를 아예 사용하지 못하는 경우가 발생을 합니다.</p>
<h4 id="scale-out">Scale Out</h4>
<p>Scale Out에 대해서 알아보고자 합니다.** Scale Out<strong>은 **새로운 서버를 추가라는 방식</strong>을 말합니다. 즉 트래픽을 받는 서버의 수를 늘리는 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/b2211ec4-a681-473b-98ab-9f466640ef11/image.png" alt=""></p>
<p>Scale Out은 <strong>수평적 확장</strong>이라고 합니다. 동일한 서버의 수를 늘리기 때문입니다. 그러면 Scale Out의 장점을 알아보겠습니다. </p>
<p>상황에 따라서 서버의 갯수를 늘리거나 줄일 수 있습니다. 상황에 따라 유연하게 대처가 가능하다는 것입니다. <strong>여러 대의 서버가 있기 때문에 분산 처리가 가능</strong>합니다. 그래서 하나의 서버의 트래픽이 집중되는 것을 여러 서버로 분산을 시킬 수 있다는 장점이 있습니다. 또한 <strong>하나의 서버에서 장애가 발생을 하더라도 다른 서버들이 존재를 하기 때문에 서비스가 중단이 되지 않고 돌아갈 수 있습니다.</strong></p>
<p>하지만 여러 대의 서버를 관리하고 설계를 해야하기 때문에 복잡성이 늘어나고 이에 따른 관리와 비용이 늘어나게 됩니다. 서버의 갯수가 늘어난만큼 문제 발생을 할 원인들이 늘어나고 각각의 Scale Out 서버가 Scale Up 서버보다 성능과 안전성면에서 떨어질 수 밖에 없습니다. 그리고 <strong>데이터 불일치 문제</strong>가 발생할 수 있습니다. 예를 들어 사용자가 서버a에서 로그인을 했을 때, 서버a에는 사용자의 정보가 있지만 서버b에서는 사용자가 로그인을 한 적이 없기 때문에 사용자의 데이터가 없는 문제가 생깁니다.</p>
<h3 id="scale-up과-scale-out-중-어떤-방법이-더-적합할까">Scale Up과 Scale Out 중 어떤 방법이 더 적합할까?</h3>
<p>둘 중 더 좋은 방법을 없습니다. 서비스의 용도의 따라 적합한 방법이 달라질 것입니다. 
그렇다면 <strong>애매 프로젝트에서는 어떤 방법이 적합할까 고민</strong>을 해보았습니다.</p>
<p>애매 프로젝트는 애매가 가능한 날짜와 시간대에 많은 <strong>트래픽이 발생</strong>을 합니다. 
또 애매는 여러 사용자가 <strong>동시에 시도를 하기 때문에 애매는 병렬적으로 처리</strong>를 해야 합니다.
그리고 애매를 하다가 중간에 많은 트래픽으로 서버 장애가 발생을 해서 애매를 하지 못하면 큰 문제가 생기기 때문에 <strong>어느 한 서버에서 장애가 발생을 하더라도 서비스는 계속 동작</strong>을 해야합니다. 이는 사용자가 서비스에 가지는 신뢰도도 중요하기 때문입니다.</p>
<p>이런 점을 고려했을 때, 제가 진행중인 애매 프로젝트에 적합한 서버 확장 방식은 트래픽을 분산을 시키고 장애가 발생하더라도 게속 서비스를 이어갈 수 있게 해서 <strong>신뢰성을 높히는 Scale Out 방식이 더 적합한 방식</strong>이라고 판단을 했습니다.</p>
<p>하지만 위에 언급을 했듯이 이 방식에는 데이터 불일치라는 문제가 있습니다. 
이 문제에 대해서는 다음 포스트에서 알아보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로젝트 개발 효율성을 위한 CI/CD 적용 과정]]></title>
            <link>https://velog.io/@sang_hyeok_2/Project-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B0%9C%EB%B0%9C-%ED%9A%A8%EC%9C%A8%EC%84%B1%EC%9D%84-%EC%9C%84%ED%95%9C-CICD-%EC%A0%81%EC%9A%A9-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@sang_hyeok_2/Project-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B0%9C%EB%B0%9C-%ED%9A%A8%EC%9C%A8%EC%84%B1%EC%9D%84-%EC%9C%84%ED%95%9C-CICD-%EC%A0%81%EC%9A%A9-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Wed, 25 Sep 2024 10:12:36 GMT</pubDate>
            <description><![CDATA[<h3 id="cicd가-필요하다고-생각한-이유">CI/CD가 필요하다고 생각한 이유</h3>
<p>이전에 개발을 하면서 수동으로 배포를 해왔습니다. 항상 EC2에 접속을 해서 git으로 프로젝트를 다운로드를 받고 build를 실행을 시키는 반복적인 과정을 진행을 해왔습니다. 이런 과정는 시간이 항상 많이 소요가 되었고 배포에 대한 피로도 높았습니다.
이번에는 낭비와 피로를 줄이기 위해서 프로젝트를 진행을 하면서 효율적인 관리를 위해서 지속적인 통합, 지속적인 배포 CI/CD를 적용을 하고 싶었습니다. 그래서 github actions를 통해서 CI/CD를 구축을 하고자 합니다.</p>
<p><strong>github actions를 선택한 이유</strong>는 github actions가 github 플랫폼에 내장이 되어 있기 때문에 git repository와 연동이 잘 되기 때문입니다. 또한 따로 설치를 할 필요가 없으며, 이미 다른 사람들이 만들어 놓른 Actions를 사용을 해서 편리하게 구현을 할 수 있어서 입니다.</p>
<p>이번 과정에서는 github actions를 이용해서 dev로 Pull Request를 할 때, CI가 일어나서 테스트 자동화가 되고 main으로 Pull Request가 되면 CD가 일어나도록 gitworkflow를 작성을 해보겠습니다.</p>
<h3 id="ci와-자동-테스트-구현">CI와 자동 테스트 구현</h3>
<p>branch 전략을 dev에서는 feature에서 딴 branch를 통합하는 과정을 거치기로 했습니다.
그래서 dev pull Request를 할 때, <strong>자동을 CI가 작동을 할 수 있도록 구성</strong>을 해야 했습니다.
이 때, <strong>고려를 해야하는 것이 docker를 이용하고 있다는 점</strong>이었습니다.
저는 공통적인 개발 환경을 만들기 위해서 docker를 사용하고 있었습니다. 그래서 CI를 구현을 할 때 docker를 사용하는 것을 고려 해야 했습니다.</p>
<p>먼저, 빌드와 테스트를 진행을 하기 위해서 소스코드를 받아야 하고 그 소스코드를 실행을 시키기 위한 환경을 제공을 해야 했습니다. 그래서 소스코드를 git repository에서 받고 Java 환경을 설치를 하고 그 안에서 테스트와 빌드가 진행이 되도록 구성을 했습니다. 또한 저는 지금 개발환경을 다른 개발자가 와도 맞출 수 있도록 하기 위해서 현재의 소스코드와 개발 환경을 docker 이미지로 빌드를 했습니다. 그리고 이미지를 다른 로컬 환경에 다운 받을 수 있도록 docker hub에 올리도록 구성을 했습니다.</p>
<p>이제 YMAL 코드를 보면서 하나씩 알아보겠습니다.</p>
<h4 id="전체-yaml-파일">전체 YAML 파일</h4>
<pre><code class="language-yml">name: CI

on:
  push:
    branches:
      - dev
  pull_request:
    branches:
      - dev

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Check out the repository
        uses: actions/checkout@v3

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: &#39;17&#39;
          distribution: &#39;temurin&#39;

      - name: Build and Test with Gradle
        run: ./gradlew clean build --no-daemon

      - name: Build Docker image
        run: docker build -t tickerflow-my-server .

      - name:  Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

      - name: Push Docker image to Docker Hub
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: dktmskf0813/tickerflow-java:latest</code></pre>
<h4 id="소스-코드-다운-받고-java-환경-설치하기">소스 코드 다운 받고 Java 환경 설치하기</h4>
<pre><code class="language-yml">- name: Check out the repository
  uses: actions/checkout@v3

- name: Set up JDK 17
  uses: actions/setup-java@v3
  with:
      java-version: &#39;17&#39;
    distribution: &#39;temurin&#39;</code></pre>
<p>먼저, actions/checkout@v3로 현재의 repository에서 소스코드를 가지고 옵니다.
프로젝트가 Java17버전으로 개발이 되었기 때문에 Java 17버전을 설치를 해서 소스코드가 빌드와 테스트 될 수 있는 환경을 만들어 줍니다.</p>
<h4 id="테스트-및-빌드-진행하기">테스트 및 빌드 진행하기</h4>
<pre><code class="language-yml">- name: Build and Test with Gradle
  run: ./gradlew clean build --no-daemon
</code></pre>
<p>프로젝트에 테스트를 자동으로 진행을 하고 빌드를 하기 위한 명령어를 실행을 해줍니다.
clean으로 이전 버전의 빌드는 삭제를 하고 새로 가져온 소스 코드를 build라는 명령어를 통해서 테스트하고 빌드를 진행을 합니다.</p>
<h4 id="docker-이미지로-만들기">Docker 이미지로 만들기</h4>
<pre><code class="language-yml">- name: Build Docker image
  run: docker build -t tickerflow-my-server .</code></pre>
<p>위에서 언급을 했듯이 Docker를 사용하고 있기 때문에 이미지를 만들어 줘야 합니다.
그래서 docker build 명령어를 통해서 현재의 소스 코드와 라이브러리들을 이미지로 만들어 줍니다.</p>
<h4 id="docker-hub에-이미지-올리기">Docker Hub에 이미지 올리기</h4>
<pre><code class="language-yml">  - name:  Login to Docker Hub
    uses: docker/login-action@v2
    with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

- name: Push Docker image to Docker Hub
  uses: docker/build-push-action@v4
  with:
    context: .
      push: true
    tags: dktmskf0813/tickerflow-java:latest</code></pre>
<p>만든 이미지는 다른 개발자들도 사용을 할 수 있도록 Docker Hub에 올리도록 합니다.
프로젝트 github Repository에 Secrets and variables에서 미리 저장해 둔 DOCKER_HUB_USERNAME, DOCKER_HUB_ACCESS_TOKEN을 이용해서 Docker hub에 로그인을 합니다.
그리고 우리가 만들어 놓은 이미지를 최신 태그를 붙혀서 push를 합니다.</p>
<p>그러면 branch dev로 push를 하거나 pull request를 보내게 되면 우리가 작성한 workflow가 동작을 하게 되면서 자동으로 CI에 대한 검증이 되게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/fe84e051-f3f7-411f-8396-7af5e40ff9f5/image.png" alt=""></p>
<p>검증이 성공을 하게 된다면 위 사진처럼 초록불이 들어오면서 성공했다고 뜨게 됩니다.</p>
<h3 id="cd로-자동-배포-구현">CD로 자동 배포 구현</h3>
<p>이제 main으로 push나 pull Request를 했을 때, workflow를 어떻데 구성을 고민을 해보았습니다.
일단 클라우드 서비스로 AWS와 네이버 클라우드를 고민을 했습니다.
제 선택은 AWS였습니다. 그 이유는 AWS는 방대한 기능들을 제공을 합니다. AWS안에서 Load Banlancing, DB, Docker등을 모두 해결을 할 수 있습니다. 또한 거대 기업인만큼 안전성도 보장이 되었기 때문에 AWS를 선택을 했습니다.</p>
<p>다음으로 고려를 해야 할 상황으로 Docker를 사용하는 것과 대용량 트래픽이 들어왔을 때 어떻게 할 것인지에 대한 부분이었습니다. 또 EC2에 소스 파일을 넘겨줘서 실행을 할지를 고려를 해서 workflow를 구성을 하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/94f7185c-93d3-49e8-82f9-00ec1415e8f4/image.png" alt=""></p>
<p>간단하게 설명을 하면 interillJ에서 github로 push 혹은 pull Request를 했을 때, github actions가 트리거가 되면서 workflow가 실행이 됩니다. 그리고 그 workflow 대로 AWS의 Docker hub의 역할을 하는 AWS ERC에 만들어 둔 이미지를 업로드하고 소스 파일 S3에 업로드를 합니다. 그리고 AWS CodeDeploy에 배포를 진행을 하라고 명령을 하면 AWS CodeDeploy가 EC2에게 S3에 배포에 필요한 파일들을 가지고 옵니다. 그리고 AWS ERC에서 이미지를 다운 받아서 컨테이너로 실행을 하면서 프로젝트를 실행을 합니다. 그리고 클라이언트가 접근을 할 때, 한 번에 많은 트래픽이 들어올 것을 고려를 해서 AWS LoadBalance를 사용을 해서 트래픽을 분산을 시킬 수 있도록 했습니다.</p>
<p>이제 전체 YMAL 코드를 보면서 하나씩 알아보겠습니다.</p>
<h4 id="전체-yaml-파일-1">전체 YAML 파일</h4>
<pre><code class="language-yml">name: CD

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  Deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Github Repository에 올린 파일을 불러온다.
        uses: actions/checkout@v4

      - name: JDK 17버전 설치
        uses: actions/setup-java@v4
        with:
            distribution: temurin
            java-version: 17

      - name: 테스트 및 빌드하기
        run: ./gradlew clean build

      - name: AWS Resource에 접근할 수 있게 AWS credentials 설정
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: ap-northeast-2
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: ECR에 로그인하기
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Docker 이미지 생성
        run: docker build -t ticket-flow .

      - name: Docker 이미지에 Tag 붙이기
        run: docker tag ticket-flow ${{ steps.login-ecr.outputs.registry }}/ticket-flow:latest

      - name: ECR에 Docker 이미지 Push하기
        run: docker push ${{ steps.login-ecr.outputs.registry }}/ticket-flow:latest

      - name: 압축하기
        run: tar -czvf $GITHUB_SHA.tar.gz appspec.yml scripts

      - name: S3에 프로젝트 폴더 업로드하기
        run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.tar.gz s3://ticket-flow/$GITHUB_SHA.tar.gz

      - name: Code Deploy를 활용해 EC2에 프로젝트 코드 배포
        run: aws deploy create-deployment
          --application-name ticket-flow
          --deployment-config-name CodeDeployDefault.AllAtOnce
          --deployment-group-name Production
          --s3-location bucket=ticket-flow,bundleType=tgz,key=$GITHUB_SHA.tar.gz</code></pre>
<h4 id="github-actions에서-소스-코드-다운-및-테스트-빌드하기">github actions에서 소스 코드 다운 및 테스트, 빌드하기</h4>
<pre><code class="language-yml">- name: Github Repository에 올린 파일을 불러온다.
  uses: actions/checkout@v4

- name: JDK 17버전 설치
  uses: actions/setup-java@v4
  with:
      distribution: temurin
    java-version: 17

- name: 테스트 및 빌드하기
  run: ./gradlew clean build</code></pre>
<p>github actions에서 먼저, repository의 소스 코드를 가지고 옵니다. 그리고 Java17을 설치를 해서 Java 환경을 만들어줍니다. 그리고 ./gradlew clean build 명령어를 통해서 테스트 및 빌드를 실행을 합니다.</p>
<h4 id="aws에-접근을-할-수-있도록-허용하기">AWS에 접근을 할 수 있도록 허용하기</h4>
<pre><code class="language-yml">- name: AWS Resource에 접근할 수 있게 AWS credentials 설정
  uses: aws-actions/configure-aws-credentials@v4
  with:
      aws-region: ap-northeast-2
      aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
      aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}</code></pre>
<p>AWS의 서비스들을 사용을 하고 있기 때문에 AWS에 접근을 할 수 있는 권한이 필요하다. 그래서 AWS에서 발급을 받은 AWS에 키를 넣어서 준다. 여기서 중요한 부분은 이 두 키는 공개가 되면 안 되기 때문에 github repository의 secrets에 담아서 사용하거나 가려서 사용을 해야 한다.</p>
<h4 id="aws-ecr에-로그인-후-이미지를-빌드하고-aws-ecr에-push하기">AWS ECR에 로그인 후 이미지를 빌드하고 AWS ECR에 push하기</h4>
<pre><code class="language-yml">- name: ECR에 로그인하기
  id: login-ecr
  uses: aws-actions/amazon-ecr-login@v2

- name: Docker 이미지 생성
  run: docker build -t ticket-flow .

- name: Docker 이미지에 Tag 붙이기
  run: docker tag ticket-flow ${{ steps.login-ecr.outputs.registry }}/ticket-flow:latest

- name: ECR에 Docker 이미지 Push하기
  run: docker push ${{ steps.login-ecr.outputs.registry }}/ticket-flow:latest</code></pre>
<p>지금의 프로젝트를 이미지로 빌드를 하고 AWS ECR에 올리기 위해서 로그인을 합니다. 그리고 이미지를 빌드를 하는 것을 AWS ECR에 push를 해줍니다.</p>
<h4 id="소스-파일과-필요한-파일을-압축하고-aws-s3에-올리기">소스 파일과 필요한 파일을 압축하고 AWS S3에 올리기</h4>
<pre><code class="language-yml">- name: 압축하기
  run: tar -czvf $GITHUB_SHA.tar.gz appspec.yml scripts

- name: S3에 프로젝트 폴더 업로드하기
  run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.tar.gz s3://ticket-flow/$GITHUB_SHA.tar.gz</code></pre>
<p>먼저, 프로젝트 실행에 필요한 파일들을 모아서 압축을 한다. appspec.yml은 AWS CodeDeploy를 사용하기 위해서 필수적인 파일이다. appspec.yml은 S3에서 EC2로 옮길 대상 파일과 CodeDeploy가 실행을 할 때, 실행을 시킬 파일을 지정할 수 있다. 그리고 그 실행할 파일이 scripts이다. script는 실행 중인 컨테이너를 정지하고 새로운 이미지를 받아서 새롭게 컨테이너를 실행을 하는 명령어가 적혀있다. 이 압축한 파일을 S3에 업로드를 해준다.</p>
<h4 id="aws-codedeploy에게-배포를-하라고-명령">AWS CodeDeploy에게 배포를 하라고 명령</h4>
<pre><code class="language-yml">- name: Code Deploy를 활용해 EC2에 프로젝트 코드 배포
  run: aws deploy create-deployment
    --application-name ticket-flow
    --deployment-config-name CodeDeployDefault.AllAtOnce
    --deployment-group-name Production
    --s3-location bucket=ticket-flow,bundleType=tgz,key=$GITHUB_SHA.tar.gz</code></pre>
<p>AWS CodeDeploy에게 EC2에 프로젝트를 가져와서 배포하라는 명령어를 내리면 배포를 자동화하는 yml이 끝이 난다.</p>
<h4 id="정리">정리</h4>
<p>이번 프로젝트를 진행을 하면서 CI/CD를 적용을 해보았습니다. 확실히 psuh나 pull Request를 통해서 자동 배포가 되니 따로 AWS에 들어갈 필요도 없고 수동으로 할 필요가 없어서 매우 편했고 효율성도 좋았습니다. 이번 포스트에서는 저는 어떻게 CI/CD를 적용을 했는지에 대해서 적어 보았습니다. github actions에서 빌드를 해서 이를 EC2에 넘겨서 실행하는 방법으로 시도를 해봤습니다. CI/CD를 구축할 때, 다양한 tool, 다양한 방법들이 있습니다. 각자가 좋다고 생각하는 방법으로 CI/CD를 구현을 해보셨으면 좋겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자료구조] Queue]]></title>
            <link>https://velog.io/@sang_hyeok_2/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-Queue</link>
            <guid>https://velog.io/@sang_hyeok_2/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-Queue</guid>
            <pubDate>Thu, 19 Sep 2024 12:08:01 GMT</pubDate>
            <description><![CDATA[<h3 id="overview">overview</h3>
<p>이번 포스트에서 공부할 자료구조는 Queue이다.</p>
<h3 id="queue">Queue</h3>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/2b4de5f1-1188-450b-882d-c519f3ab6e65/image.png" alt=""></p>
<p>Queue를 우리나라 말로 변역을 하면 대기줄, 열, 동사로는 차례를 기다리다 라는 뜻으로 사용이 되어진다.
예시로 들면 은행의 대기표와 같은 것이다. 은행에 먼저 온 사람이 먼저 대기표를 뽑고 먼저 은행업무를 본다. 그리고 들어온 순서대로 대기 순번을 갖고 대기 순번대로 은행일을 보고 은행을 나간다.</p>
<p>Queue는 구멍이 두 개인다. Stack과 다르게 들어오는 입구와 나가는 출구가 다르다. 그래서 먼저 들어온 데이터가 먼저 나가는 구조를 가지고 있다. 이를 선입선출(Frist In, Last Out)이라고 한다.</p>
<p>자바를 기준으로 Queue에서 많이 사용이 되는 메소드에 대해서 알아보자.
<strong>add,</strong> add는 Queue에 데이터를 넣을 때 사용이 된다.
<strong>remove,</strong> remove는 Queue에 데이터 삭제할 때 사용이 된다.
<strong>peek,</strong> peek은 가장 첫 번째에 있는 값을 조회하고 싶을 때 사용이 된다.</p>
<h3 id="자바에서-queue를-사용해보기">자바에서 Queue를 사용해보기</h3>
<h4 id="add">add</h4>
<pre><code class="language-java">public class QueueStudy {
    public static void main(String[] args) {
        Queue&lt;Integer&gt; queue = new LinkedList&lt;&gt;();

        queue.add(1);
        queue.add(2);
        queue.add(3);

        System.out.println(&quot;queue :&quot; + queue);
    }
}</code></pre>
<p>queue에 add메소드로 1,2,3을 넣고 출력을 해보았다.
<img src="https://velog.velcdn.com/images/sang_hyeok_2/post/ca78c01b-0c00-4471-add1-93ba14c0fc2f/image.png" alt="">
1, 2, 3이 잘 들어가 있는 것을 확인을 할 수 있다.</p>
<h4 id="remove">remove</h4>
<pre><code class="language-java">public class QueueStudy {
    public static void main(String[] args) {
        Queue&lt;Integer&gt; queue = new LinkedList&lt;&gt;();

        queue.add(1);
        queue.add(2);
        queue.add(3);

        Integer remove = queue.remove();

        System.out.println(&quot;remove :&quot; + remove);
    }
}</code></pre>
<p>1, 2, 3이 들어있는 Queue에 remove를 했을 때 반환을 하는 값을 출력을 해보았다.
<img src="https://velog.velcdn.com/images/sang_hyeok_2/post/4644001c-f2d6-4daf-82ee-000f3fe13e52/image.png" alt="">
값으로 1이 나왔다.
이 결과를 보면 Queue가 선입선출이라는 구조라는 것을 알 수 있다.
코드를 보면 1을 가장 먼저 넣어주었다. 그리고 remove을 했을 때 1이 나왔다라는 것은 가장 먼저 넣어주었던 1이 가장 앞에 있ㄷ기 때문에 나왔다라는 것이다. 이를 통해서 Queue가 선입선출이라는 구조를 가지고 있다는 것을 알 수 있다.</p>
<h4 id="peek">peek</h4>
<pre><code class="language-java">public class QueueStudy {
    public static void main(String[] args) {
        Queue&lt;Integer&gt; queue = new LinkedList&lt;&gt;();

        queue.add(1);
        queue.add(2);
        queue.add(3);

        Integer peek = queue.peek();

        System.out.println(&quot;peek :&quot; + peek);
    }
}</code></pre>
<p>peek은 가장 앞에 있는 데이터를 조회하는 메소드이다. 마찬가지로 1, 2, 3을 넣어주고 peek을 해보고 출력을 해보았다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/9ab36693-4505-4129-a4fc-e5d6fd54d6e7/image.png" alt=""></p>
<p>가장 먼저 넣어서 가장 앞에는 1이 나온 것을 알 수 있다. 이 또한 Queue의 선입선출의 구조를 의미하는 것이다.</p>
<h3 id="자바에서-queue를-구현해보기">자바에서 Queue를 구현해보기</h3>
<pre><code class="language-java">public class MakeQueue {

    private int maxSize;
    private int[] queueArray;
    private int front;
    private int rear;
    private int currentSize;

    public MakeQueue(int size) {
        this.maxSize = size;
        this.queueArray = new int[maxSize];
        this.front = 0;
        this.rear = -1;
        this.currentSize = 0;
    }

    public void add(int value) {
        if (isFull()) {
            System.out.println(&quot;가득 차있습니다.&quot;);
            return;
        }
        // 순환 큐 처리
        rear = (rear + 1) % maxSize;
        queueArray[rear] = value;
        currentSize++;
    }

    public int remove() {
        if (isEmpty()) {
            System.out.println(&quot;Queue is empty. Cannot dequeue&quot;);
            return -1; // 큐가 비어있으면 -1 반환
        }
        int temp = queueArray[front];
        front = (front + 1) % maxSize; // front 위치를 한 칸 앞으로 이동 (순환)
        currentSize--;
        return temp;
    }

    public int peek() {
        if (isEmpty()) {
            System.out.println(&quot;Queue is empty. Cannot peek&quot;);
            return -1;
        }
        return queueArray[front];
    }

    public boolean isEmpty() {
        return (currentSize == 0);
    }

    public boolean isFull() {
        return (currentSize == maxSize);
    }
}</code></pre>
<p>자바로 Queue를 구현을 해주었다.
Queue에 경우 입구와 출구가 따로 존재하기 때문에 이를 구분하기 위해서 front와 rear로 구분을 해주었다.
그래서 add에 경우 뒤로 들어가야 하기 때문에 rear를 이용을 했고 remove에 경우 앞에서 나가기 때문에 front를 활용을 해서 구현을 해주었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[자료구조] 스택]]></title>
            <link>https://velog.io/@sang_hyeok_2/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%8A%A4%ED%83%9D</link>
            <guid>https://velog.io/@sang_hyeok_2/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%8A%A4%ED%83%9D</guid>
            <pubDate>Wed, 18 Sep 2024 12:34:47 GMT</pubDate>
            <description><![CDATA[<h3 id="overview">overview</h3>
<p>이번 자료구조 포스트에서는 stack에 대해서 공부를 해볼 것이다.</p>
<h3 id="stack">stack</h3>
<p>stack을 우리나라 말로 하면 쌓다라는 의미이다.
stack이라는 자료구조는 데이터를 차곡차곡 쌓아올린 자료구조이다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/c367db81-3cb7-445c-9d5e-b22ee20f6859/image.png" alt=""></p>
<p>위 그림처럼 데이터가 들어오면 차곡차곡 쌓아올린다. 그런데 보면 데이터가 드나들 수 있는 출입구가 하나이다.
이 말은 데이터의 입출구가 같다는 것이다. 그래서 나타나는 특징이 stack은 먼저, 들어온 데이터가 나중에 나가는 <strong>선입후출(Frist In, Last Out)</strong>이라는 특징을 가진다.</p>
<p>자바를 기준으로 보았을 때, stack을 사용하면서 자주 사용을 하는 메소드들이 있다.
<strong>push,</strong> push는 stack에 top에 데이터를 추가하는 메소드이다.
<strong>pop,</strong> pop은 stack에 top에 있는 데이터를 제거하는 메소드이다.
<strong>isEmpty,</strong> isEmpty는 스택에 데이터가 없는 지를 판단을 하는 메소드이다.
<strong>peek,</strong> peek은 스택에 가장 위에 위치한 데이터를 반환을 하는 메소드이다.</p>
<h3 id="stack-사용해보기">stack 사용해보기</h3>
<p>이제 stack이 무엇인지, stack에 어떤 메소드들이 있는지 알아보았으니 이를 사용을 해보자.</p>
<h4 id="push">push</h4>
<pre><code class="language-java">public class StackStudy {
    public static void main(String[] args) {
        Stack&lt;Integer&gt; stackInt = new Stack&lt;&gt;();
        stackInt.push(1);
        stackInt.push(2);
        stackInt.push(3);

        System.out.println(&quot;stackInt = &quot; + stackInt);
    }
}</code></pre>
<p>stack이라는 객체를 만들어주고 push라는 메소드를 사용을 해서 1, 2, 3을 넣어주었다.
그리고 이를 출력을 해보았다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/69a49415-a1d4-41a0-8b49-a7dfe1615a27/image.png" alt=""></p>
<p>코드에서 넣어주었던 1,2,3이 나오는 것을 확인을 할 수 있었다.</p>
<h4 id="pop">pop</h4>
<pre><code class="language-java">public class StackStudy {
    public static void main(String[] args) {
        Stack&lt;Integer&gt; stackInt = new Stack&lt;&gt;();
        stackInt.push(1);
        stackInt.push(2);
        stackInt.push(3);

        Integer pop = stackInt.pop();


        System.out.println(&quot;stackInt = &quot; + stackInt);
        // 1, 2
        System.out.println(&quot;pop = &quot; + pop);
        // 3

    }
}</code></pre>
<p>이제 pop이라는 메소드를 사용을 해보자. 위에서 했던 코드에서 pop 메소드를 사용을 해보았다.
그리고 stack가 pop을 했을 때 반환하는 값을 출력을 해보았다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/6cfbb82a-b0a4-424d-bcb3-049c86982020/image.png" alt=""></p>
<p>stack은 3이 빠진 1, 2를 확인을 할 수 있는 것을 알 수 있다. 그리고 pop을 했을 때, 반환이 되는 값이 3인 것을 알 수 있다.
여기서 한 가지 알 수 있는 사실은 stack의 선입후출 특징이다.
코드를 보면 우리는 1을 먼저, 3을 나중에 넣었다. 근데 pop을 했을 때 반환이 되는 값이 3인 것을 알 수 있다. 이는 stack의 선입후출 특징이 적용이 된 것을 알 수 있다.</p>
<h4 id="isempty">isEmpty</h4>
<pre><code class="language-java">public class StackStudy {
    public static void main(String[] args) {
        Stack&lt;Integer&gt; stackIntOne = new Stack&lt;&gt;();
        Stack&lt;Integer&gt; stackIntTwo = new Stack&lt;&gt;();
        stackIntOne.push(1);
        stackIntOne.push(2);
        stackIntOne.push(3);

        System.out.println(&quot;stackIntOne = &quot; + stackIntOne.isEmpty());
        System.out.println(&quot;stackIntTwo = &quot; + stackIntTwo.isEmpty());
    }
}</code></pre>
<p>isEmpty는 값이 비어 있는지를 확인을 하는 메소드이다.
값이 들어있는 stackIntOne과 비어 있는 sackIntTwo를 만들고 isEmpty 메소드를 사용해서 출력을 해보았다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/016d65d7-d268-4711-a78b-5702003ef3ca/image.png" alt=""></p>
<p>값이 들어 있는 stackIntOne은 false가 나오고 비어 있는 sackIntTwo는 true가 나오는 것을 확인을 했다.</p>
<h4 id="peek">peek</h4>
<pre><code class="language-java">public class StackStudy {
    public static void main(String[] args) {
        Stack&lt;Integer&gt; stackInt = new Stack&lt;&gt;();
        stackInt.push(1);
        stackInt.push(2);
        stackInt.push(3);

        Integer peek = stackInt.peek();

        System.out.println(&quot;peek = &quot; + peek);
    }
}</code></pre>
<p>peek은 stack에서 가장 위에 있는 데이터를 반환을 한다.
stackInt에 peek을 한 것을 출력을 해보았다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/e63187d8-323d-42ce-a605-362d8ccea5b8/image.png" alt=""></p>
<p>현재, 나중에 들어온 3이 가장 위에 있다. 그렇기 때문에 peek을 했을 때, 가장 위에 있는 3이 출력이 되는 것을 확인을 할 수가 있다.</p>
<h3 id="java로-stack-구현하기">Java로 stack 구현하기</h3>
<pre><code class="language-java">public class makeStack {

    private int maxSize;
    private int[] stackArray;
    private int top;

    public makeStack(int size) {
        this.maxSize = size;
        this.stackArray = new int[this.maxSize];
        this.top = -1;
    }

    public void push(int data) {
        if (isFull()) {
            System.out.println(&quot;스택이 가득 찾습니다.&quot;);
        } else {
            stackArray[++top] = data;
        }
    }

    public int pop() {
        if (isEmpty()) {
            System.out.println(&quot;삭제할 데이터가 없습니다.&quot;);
            return -1; // 스택이 비어있으면 -1을 반환
        } else {
            return stackArray[top--];
        }
    }

    public int peek() {
        if (isEmpty()) {
            System.out.println(&quot;데이터가 없습니다.&quot;);
            return -1;
        } else {
            return stackArray[top];
        }
    }

    public boolean isEmpty() {
        return (top == -1);
    }

    public boolean isFull() {
        return (top == maxSize - 1);
    }
}</code></pre>
<p>array를 통해서 간단하게 stack을 구현을 해보았다.
array를 선택한 이유는 순서가 보장이 되는 자료구조이기 때문이다.</p>
<p>isEmpty와 isFull메소드를 통해서 stack에 데이터가 가득 찼는지 비어 있는지를 판단을 하였다. 그리고 push에 경우 가득 차있지 않다면 stackArray에 값을 추가를 해주고 pop은 비어 있지 않다면 가장 위에 있는 값, array를 기준으로 가장 뒤어 있는 값을 제거해주었다.
peek 또한 비어 있는지를 판단을 하고 비어 있지 않다면 가장 뒤에 있는 값을 반환을 해주도록 하였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Mock을 사용하는 테스트 알아보기]]></title>
            <link>https://velog.io/@sang_hyeok_2/Spring-Mock%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@sang_hyeok_2/Spring-Mock%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Wed, 18 Sep 2024 07:39:06 GMT</pubDate>
            <description><![CDATA[<h3 id="overview">overview</h3>
<p>Test는 실제 객체를 사용을 해서 Test를 하는 방법도 있지만 가짜 객체를 만들어서 Test를 하는 방법도 있다. 이번 포스트에서는 가짜 객체를 만들어서 Test를 하는 방법에 대해서 알아볼 것이다.</p>
<h3 id="mock에-대해서-알아보기">Mock에 대해서 알아보기</h3>
<p>보통 가짜 객체를 만들어서 테스트를 하는 방법을 Mock이라고 한다. 
코드를 살펴보기 전에 Mock이 무엇인지에 대해서 알아보자.</p>
<p>Mock을 우리나라 말로 변역을 해보면 명사로는 모조품, 동사로는 속이다 라는 의미가 있다.
이를 테스트에 적용을 시켜보면 모조품 객체를 만들어서 테스트를 한다 라고 생각을 할 수 있다.
모조품 객체를 <strong>Mock 객체 혹은 Mock Object</strong>라고 한다.
이 Mock 객체은 테스트를 하고자 하는 코드에서 사용이 되는 외부 의존성을 제거하기 위해서 사용이 된다.
의존성을 제거하면 테스트를 하고자하는 코드에만 집중을 할 수 있게 된다.
이는 테스트의 정확성과 효율성이 증가하는데 도움이 된다.</p>
<p>Spring Framwork에서 사용되는 Mock 라이브러리로 <strong>Mockito</strong>가 있다.
이 Mockito를 사용을 하면 간단한 어노테이션으로 외부의 의존성을 모킹을 할 수 있다.</p>
<p>테스트에도 여러가지 종류의 테스트가 있다.
단위 테스트, 통합 테스트 등 여럭 단계에서 테스트를 사용을 할 수 있는데 Mock은 단위 테스트를 작성을 할 때 사용이 된다.
단위 테스트는 작은 단위 코드를 테스트하기 때문에 빠르고 가겹게 진행이 되어야 한다.
실제 객체들을 사용하게 되면 Spring Application Context를 전부 올리게 되어서 속도가 늦는 경향이 있고 무거워진다.
그래서 Mock 객체를 사용을 하면 가볍고 빠르게 사용이 가능하기 때문에 MocK을 많이 사용을 하는 경향이 있다.</p>
<h3 id="mock-코드로-살펴보기">Mock 코드로 살펴보기</h3>
<pre><code class="language-java">@Mock
private CategoryRepository categoryRepository;

@InjectMocks
private CategoryService categoryService;

@DisplayName(&quot;카테고리의 level을 넣으면, level에 해당하는 데이터들의 리스트가 나온다.&quot;)
@Test
void getCategoriesByParentCategoryId() {
    // given
    CategoryEntity categoryEntity2 = CategoryEntity.builder()
        .categoryName(&quot;축구&quot;)
        .categoryLevel(2)
        .parentCategoryId(1L)
        .build();

    CategoryEntity categoryEntity3 = CategoryEntity.builder()
        .categoryName(&quot;야구&quot;)
        .categoryLevel(2)
        .parentCategoryId(1L)
        .build();

    BDDMockito.given(categoryRepository.findAllByParentCategoryId(1L))
        .willReturn(List.of(categoryEntity2, categoryEntity3));

    // when
    List&lt;CategoryResponseDTO&gt; categories = categoryService.getCategoryByParentCategoryId(1L);

    // then
    assertThat(categories).hasSize(2)
        .extracting(&quot;categoryName&quot;, &quot;categoryLevel&quot;, &quot;parentCategoryId&quot;)
        .containsExactlyInAnyOrder(
                        tuple(&quot;축구&quot;, 2, 1L),
                        tuple(&quot;야구&quot;, 2, 1L));

}</code></pre>
<p>Mock을 살펴보기 위해서 내가 진행 중인 카테고리의 level로 카테고리를 찾는 코드를 테스트하는 코드를 가지고 왔다.</p>
<p>코드를 살펴보면 given, when, then이 있다.
<strong>given</strong>은 주어진 상황을 의미한다. 테스트를 진행을 하기 전에 초기 상태나 조건을 설정하는 것이다.
<strong>when</strong>은 어떤 행동을 했을 때를 의미를 한다. 즉, categoryService에서 findAllByParentCategoryId라는 메소드를 실행하는 것이다.
<strong>then</strong>은 어떤 결과가 나와야 하는 지를 검증하는 단계이다. findAllByParentCategoryId라는 메소드가 실행이 되었을 때 어떤 결과가 나와야 하는데 그 결과가 나왔는 지를 확인하는 부분이다.</p>
<p>Mock을 사용할 때, 수동으로 넣어줄 수도 있지만 나는 어노테이션을 사용을 해서 자동으로 넣어주었다.
@Mock과 @InjectMocks 사용할 수 있는데 이는 CategoryService의 일부분 코드를 보면서 설명을 하겠다.</p>
<pre><code class="language-java">private final CategoryRepository categoryRepository;

public List&lt;CategoryResponseDTO&gt; getCategoryByCategoryLevel(int categoryLevel) {
    List&lt;CategoryResponseDTO&gt; categories = new ArrayList&lt;&gt;();
    categoryRepository.findAllByCategoryLevel(categoryLevel).forEach(categoryEntity -&gt; {
        categories.add(new CategoryResponseDTO(categoryEntity));
    });

    return categories;
}</code></pre>
<p>CategoryService에 보면 CategoryRepository를 주입을 받고 있다.
이렇게 주입을 해줘야 하는 객체에는 @Mock을 그리고 @Mock이 달린 객체를 주입 받아야 하는 객체에는 @InjectMocks를 달아주어야 한다.
그러면 자동으로 의존성 주입을 해준다.</p>
<p>그리고 CategoryRepository에서 findAllByCategoryLevel이라는 메소드를 통해서 카테고리들을 불러오고 있다. 
이 부분을 명시적으로 넣어주어야 한다. 즉, 해당 메소드의 반환 값을 임의로 지정을 해주어야 한다.</p>
<pre><code class="language-java">BDDMockito.given(categoryRepository.findAllByParentCategoryId(1L))
        .willReturn(List.of(categoryEntity2, categoryEntity3));</code></pre>
<p>이 부분이다. 
이러한 과정을 <strong>Stub</strong>라고 하는데, 이 Stub를 해주어야 하는 이유는 뭘까?</p>
<p>여기서 categoryRepository는 @Mock으로 만든 가짜 객체, Mock 객체이다.
즉, 가짜이기 때문에 findAllByParentCategoryId 메소드를 실제 실행을 할 수가 없다.
그래서 우리가 Mock 객체 categoryRepository는에서 findAllByParentCategoryId 메소드를 실행을 헸을 떄, 어떤 결과가 나올 것이라는 결과를 명시적으로 지정을 해주어야 한다.
이렇게 주어진 상황을 주고 when에서 우리가 테스트를 하고자 하는 메소드를 실제로 실행을 한다.
assertThat을 통해서 우리가 얻은 결과에서 어떤 부분을 검증을 하고 싶은지 체크를 하면 된다.</p>
<h3 id="정리">정리</h3>
<p>테스트를 하는 방법은 @SpringBootTest, @DataJpaTest, @Mock 등 다양한 방법이 있다.
방법들 중 어떤 것이 정답이라는 것은 없다.
단지 지금 내 상황에서 어떤 것이 더 효율적인지 판단을 해서 사용하는 것이 중요하다.
이번 포스트에서는 @Mock에 대해서 알아보았다.
Mock은 내가 집중적으로 테스트를 하고자하는 코드의 외부 주입을 분리 시키기 위해서 가짜 객체를 사용하는 것이다.
주로 작은 단위의 단위 테스트에서 사용하는 것을 권장 한다.</p>
<h3 id="참고-자료">참고 자료</h3>
<p><a href="https://velog.io/@wlsh44/symr7p68">[Test] Mockito를 이용한 단위 테스트</a>
<a href="https://f-lab.kr/insight/importance-of-writing-test-codes-and-using-mocks">테스트 코드 작성의 중요성과 모킹 활용하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Spring MVC의 패턴들]]></title>
            <link>https://velog.io/@sang_hyeok_2/Spring-Spring-MVC%EC%9D%98-%ED%8C%A8%ED%84%B4%EB%93%A4</link>
            <guid>https://velog.io/@sang_hyeok_2/Spring-Spring-MVC%EC%9D%98-%ED%8C%A8%ED%84%B4%EB%93%A4</guid>
            <pubDate>Mon, 05 Aug 2024 17:27:11 GMT</pubDate>
            <description><![CDATA[<h3 id="overview">Overview</h3>
<p>Spring은 내부적으로 FrontController 패턴과 Adapter 패턴을 적용하고 있다.
Spring이 FrontController 패턴과 Adapter 패턴을 왜 적용하고 있고 어떻게 사용을 하고 있는 지 알아보자.</p>
<h3 id="spring-mvc의-구조-알아보기">Spring MVC의 구조 알아보기</h3>
<p>먼저, 패턴들을 알기 전에 Spring MVC가 어떻게 적용이 되고 있는지 전체적인 구조와 흐름을 알아보도록 하자.
<img src="https://velog.velcdn.com/images/sang_hyeok_2/post/11d051b5-f237-4783-bf30-590e0edc9fd5/image.png" alt=""></p>
<p>클라이언트로 요창이 들어오면 Dispatcher Servlet이 받고 요청이 어떤 핸들러인지 정보를 조회를 한다.
그리고 핸들러 어댑터 목록에서 핸들러를 처리할 수 있는 어댑터를 찾아서 어댑터에서 핸들러를 실행을 한다.
그리고 그 결과를 Dispatcher Servlet이 viewResolver에 넘겨 주어서 View를 반환한 뒤 View를 랜더링을 해준다.</p>
<h4 id="dispatcher-servlet-내부-코드로-살펴보기">Dispatcher Servlet 내부 코드로 살펴보기</h4>
<p>흐름을 알아 보았으니 Spring MVC에서 중요한 역할을 하는 Dispatcher Servlet에 대해서 코드를 보면서 알아보겠다.</p>
<pre><code class="language-java">public class DispatcherServlet extends FrameworkServlet {
    // ...생략...

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // ...생략...
        mappedHandler = this.getHandler(processedRequest);
        // ...생략...
        HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
        // ...생략...
        mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
        // ...생략...
        this.applyDefaultViewName(processedRequest, mv);
        // ...생략...
        this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
        // ...생략...
    }

    // ...생략...
}</code></pre>
<p>DispatcherServlet는 매우 복잡하다. 여기서는 설명을 위해서 중요한 doDispatcher 부분 만을 뽑아서 설명을 하겠다.</p>
<p>먼저, DispatcherServlet를 보면 FrameworkServlet를 상속을 받는 것을 알 수 있다.
이 FramewrokServlet를 따라 올라가면 HttpServletBean을 상속을 받고 HttpServlet를 상속을 받고 있는 것을 알 수 있다.
즉, </p>
<blockquote>
<p>DispatcherServlet -&gt; FrameworkServlet -&gt; HttpServletBean -&gt; HttpServlet</p>
</blockquote>
<p>위와 같이 상속을 받고 있는 것이다. 이는 DispatcherServlet가 HttpServlet을 상속 받고 DispatcherServlet도 Servlet라는 것을 알 수 있다.</p>
<p>그러면 doDispatcher 메소드를 하나씩 살펴보자.
먼저, getHandler 메소드를 통해서 요청을 처리할 핸들러를 가지고 온다.</p>
<pre><code class="language-java">  protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        if (this.handlerMappings != null) {
            Iterator var2 = this.handlerMappings.iterator();

            while(var2.hasNext()) {
                HandlerMapping mapping = (HandlerMapping)var2.next();
                HandlerExecutionChain handler = mapping.getHandler(request);
                if (handler != null) {
                    return handler;
                }
            }
        }

        return null;
    }</code></pre>
<p>getHandler 메소드의 내부를 살펴보면 handlerMappings는 HandlerMapping 묶은 리스트이다.
이 HandlerMapping이 하는 역할은 요청을 받아서 그 요청을 처리해줄 핸들러를 찾는 것이다.
그래서 이 handlerMappings를 순회를 하면서 요청을 처리할 핸들러를 찾아서 return을 해준다.</p>
<p>그리고 getHandlerAdapter 메소드에 우리가 찾아온 핸들러를 넣어서 이 핸들러를 처리해 줄 adapter를 찾아서 가지고 온다.</p>
<pre><code class="language-java">protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
    if (this.handlerAdapters != null) {
        Iterator var2 = this.handlerAdapters.iterator();

            while(var2.hasNext()) {
                HandlerAdapter adapter = (HandlerAdapter)var2.next();
                if (adapter.supports(handler)) {
                    return adapter;
                   }
             }
      }

    throw new ServletException(&quot;No adapter for handler [&quot; + handler + &quot;]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler&quot;);
}</code></pre>
<p>getHandlerAdapter 메소드의 내부도 살펴보면, 마찬가지로 handlerAdapters라는 handlerAdapter 구현체들의 리스트이고 이 리스트를 순회를 하면서 핸들러를 호출할 수 있는 adapter를 찾는다.
그리고 adapter안에 supports 메소드가 있는데 이 안에 핸들러를 넣어서 호출을 할 수 있는 adapter를 검증을 해서 adapter를 리턴을 해준다.
이 부분이 handler에 맞는 adapter를 찾는 adapter 패턴이 적용된 부분이다.</p>
<pre><code class="language-java">public interface HandlerAdapter {
    boolean supports(Object handler);

    @Nullable
    ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;

    /** @deprecated */
    @Deprecated
    long getLastModified(HttpServletRequest request, Object handler);
}
</code></pre>
<p>그리고 handle 메소드를 보면 HandlerAdapter라는 인터페이스에 구현 메소드로 나와 있다.
아까 보았던 supports 메소드 있다.
adapter 구현체를 찾아서 그 구현체가 구현한 handler메소드를 통해서 handler를 호출을 한다.
그리고 handler 메소드는 ModelAndView를 반환을 한다.</p>
<pre><code class="language-java"> private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception {
        if (mv != null &amp;&amp; !mv.hasView()) {
            String defaultViewName = this.getDefaultViewName(request);
            if (defaultViewName != null) {
                mv.setViewName(defaultViewName);
            }
        }

    }</code></pre>
<p>다음은 applyDefaultViewName이라는 메소드는 ModelAndView객체 안에 viewName이 설정이 되어 있지 않은 경우 viewName을 설정을 해주는 메소드이다.
이름이 넣어주는 이유는 view를 찾을 때, viewName으로 하기 때문이다.</p>
<pre><code class="language-java"> private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { 
    // ...생략...
    this.render(mv, request, response);
    // ...생략...

}</code></pre>
<p>그리고 View를 랜더링해주는 processDispatchResult를 살펴보면 reder라는 메소드를 통해서 랜더링을 한다. 이 render 메소드도 살펴보자.</p>
<pre><code class="language-java">protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
    // ...생략...
    String viewName = mv.getViewName();
    View view;
    // ...생략...
    view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request);
    // ...생략...
    view.render(mv.getModelInternal(), request, response);


}</code></pre>
<p>render메소드를 살펴보면 아까 applyDefaultViewName 메소드를 통해서 설정한 viewName을 통해서 viewName을 가지고 오고 viewResolver에 viewName을 통해서 View를 찾고 View의 render 메소드를 통해서 view을 랜더링 해주고 있다.</p>
<p>이러한 과정을 거쳐서 요청이 들어왔을 때, 로직 처리와 뷰를 랜더링하는 과정을 내부 코드를 통해서 알아보았다.
여기서 getHandlerAdapter 메소드를 통해서 handler에 맞는 handlerAdapeter 찾고 가지고 오는 부분에서 adapeter 패턴이 적용이 된 것을 알 수 있었고 또 handler와 model, view를 통해서 MVC 패턴이 적용이 된 것을 알 수 있었다.</p>
<h3 id="mvc패턴과-adapter패턴을-구현해보기">MVC패턴과 Adapter패턴을 구현해보기</h3>
<p>내부 코드를 보면서 MVC패턴과 Adapter패턴이 어떤 식으로 동작을 하는지를 알아보았다.</p>
<p>이제 간단하게 Spring MVC를 구현을 해보면서 좀 더 확실하게 알아보자.</p>
<p>먼저, 거의 기능 처리가 없는 간단한 ControllerV1과 ControllerV2를 만들었다.</p>
<pre><code class="language-java">// handlerAdapter의 인터페이스
public interface MyHandlerAdapter {

    public boolean supports(Object handler);

    public ModelView handle(Object handler);

}
</code></pre>
<p>handler를 처리할 수 있는 Adapter인지를 확인하는 support 메소드, 그리고 handler를 실행하는 handler 메소드를 구현 메소드로 만들고 이를 구현한 ControllerV1Adapater와 ControllerV2Adapter를 만들었다.
handler 메소드의 경우, ModelView를 반환하도록 만들었다.</p>
<pre><code class="language-java">// dispacterServlet 역할을 하는 FrontController

@WebServlet(name = &quot;frontController&quot;, urlPatterns = &quot;/frontController/*&quot;)
public class FrontController extends HttpServlet {


    private final Map&lt;String, Object&gt; handlerMappingMap = new HashMap&lt;&gt;();
    private final List&lt;MyHandlerAdapter&gt; handlerAdapters = new ArrayList&lt;&gt;();

    public FrontController() {
        initFrontController();
        initControllerAdapter();
    }

    public void initFrontController() {
        handlerMappingMap.put(&quot;/frontController/v1&quot;, new ControllerV1());
        handlerMappingMap.put(&quot;/frontController/v2&quot;, new ControllerV2());
    }

    public void initControllerAdapter() {
        handlerAdapters.add(new ControllerV1Adapter());
        handlerAdapters.add(new ControllerV2Adapter());

    }


    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Object controller = getHandler(req);

        if (controller == null) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        MyHandlerAdapter adapter = getHandlerAdapter(controller);
        ModelView modelView = adapter.handle(controller);

        String viewName = modelView.getName();
        MyView view = viewResolver(viewName);

        view.render(req, resp);

    }

    private static MyView viewResolver(String viewName) {
        return new MyView(&quot;/WEB-INF&quot; + viewName + &quot;.jsp&quot;);
    }

    private MyHandlerAdapter getHandlerAdapter(Object controller) {
        for (MyHandlerAdapter handlerAdapter : handlerAdapters) {
            if (handlerAdapter.supports(controller)) {
                return handlerAdapter;
            }
        }
        return null;
    }

    private Object getHandler(HttpServletRequest req) {
        Object controller = handlerMappingMap.get(req.getRequestURI());
        return controller;
    }
}
</code></pre>
<p>먼저, 요청에 따른 controller들을 맵핑을 해서 handlerMappingMap에 담아두고 Adapter들의 리스트를 만들어 주는데 그 리스트가 handlerAdapters이다.</p>
<p>요청이 들어오면 그 요청의 url을 뽑아서 handlerMappingMap에서 요청에 해당하는 controller를 찾는다. 이 부분이 getHandler 메소드이다. 
그리고 handlerAdapters을 순회하면서 controller에 맞는 adapter를 찾아낸다. 이 때, support메소드를 통해서 해당되는 controller인지를 파악한다. 이 부분이 getHandlerAdapter 메소드이다.</p>
<p>찾아내면 adapter의 handler라는 메소드를 통해서 controller를 실행하고 modelView를 반환을 한다.
그리고 이 modelView의 viewName를 viewResolve에 넘겨주서 myView를 찾고 render 메소드를 통해서 view를 랜더링해준다.</p>
<p>간단하게 생각을 하면 FrontController라는 곳에서 위 로직을 다 짠 다음 역할에 따라 메소드로 빼주면 우리가 DispatcherServlet 내부에서 봤던 getHandler, getHandlerAdapter, viewResolve등이 된다.</p>
<p>이렇게 간단하게 구현을 해보면서 Spring MVC에서 MVC패턴과 adapter패턴을 사용하는 것을 알아보았다.</p>
<h3 id="참고-자료">참고 자료</h3>
<p><a href="https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard">스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Servlet에 이어서 JSP와 MVC 패턴을 알아보자 ]]></title>
            <link>https://velog.io/@sang_hyeok_2/Spring-Servlet%EC%97%90-%EC%9D%B4%EC%96%B4%EC%84%9C-JSP%EC%99%80-MVC-%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@sang_hyeok_2/Spring-Servlet%EC%97%90-%EC%9D%B4%EC%96%B4%EC%84%9C-JSP%EC%99%80-MVC-%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Tue, 30 Jul 2024 12:46:45 GMT</pubDate>
            <description><![CDATA[<h3 id="overview">Overview</h3>
<p>지난 포스트에서 Servlet에 대해서 알아보았다.
Servlet는 동적인 페이지를 응답할 수 있도록 해주는데 이를 응답을 반환을 해줄 때 view에 관한 로직이 보기가 별로 좋지 않은 즉, 가독성이 좋치 않은 문제가 있었다.
이를 해결하기 위해서 JSP라는 것이 나왔다. 
이번 포스트에서 JSP와 JSP가 나오면서 등장을 하게 된 MVC패턴에 대해서 알아보자.</p>
<h3 id="템플릿-엔진-jsp의-등장-그리고-mvc-패턴">템플릿 엔진, JSP의 등장 그리고 MVC 패턴</h3>
<p>Servlet에서 view를 응답을 해줄 때, 가독성이 떨어지는 문제가 있었다.</p>
<pre><code class="language-java">@WebServlet(name = &quot;responseHtmlServlet&quot;, urlPatterns = &quot;/response-html&quot;)
public class ResponseHtmlServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      response.setContentType(&quot;text/html&quot;);
      response.setCharacterEncoding(&quot;utf-8&quot;);

      PrintWriter writer = response.getWriter();
      writer.println(&quot;&lt;html&gt;&quot;);
      writer.println(&quot;&lt;body&gt;&quot;);
      writer.println(&quot; &lt;div&gt;안녕?&lt;/div&gt;&quot;);
      writer.println(&quot;&lt;/body&gt;&quot;);
      writer.println(&quot;&lt;/html&gt;&quot;);

    }
}</code></pre>
<p>위에 코드를 보면 response객체를 이용해서 HTML을 만들어서 응답을 만들어주고 있다.
위에 HTML코드는 하나 하나 코드를 추가해 주어야 한다.
지금은 짧은 코드이기 때문에 가능하지만 내용이 많아지면 더 복잡해지고 더 가독성이 떨어질 것이다.</p>
<p>그래서 이러한 문제를 해결하기 위해서 나온 것이 템플릿 엔진이다.
템플릿 엔진은 동적인 HTML을 처리하기 위한 도구이다. 서버로부터 데이터를 받아서 HTML로 변환을 하는 역할을 담당을 하고 있다.</p>
<p>JSP도 이 템플릿엔진의 종류 중 하나이다.</p>
<h4 id="jsp를-사용해보기">JSP를 사용해보기</h4>
<p>템플릿 엔진이 JSP를 사용해서 username을 사용해보자.
(여기서는 JSP의 문법을 다루기보다는 JSP와 MVC패턴이 무엇인지 이해하는 데 중점을 둔다. 그래서 JSP의 문법은 다루지 않는다.)</p>
<pre><code class="language-java">&lt;%@ page contentType=&quot;text/html; charset=UTF-8&quot; language=&quot;java&quot; %&gt;

&lt;html&gt;
&lt;head&gt;
    &lt;title&gt;제목&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;%
        String username = &quot;이상혁&quot;;
    %&gt;
    &lt;div&gt;
        Hello &lt;%= username %&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>JSP 문법을 사용을 해서 JSP내에서 변수를 선언과 초기화를 하고 HTML에서 사용하는 것을 볼 수 있다.
확실히 Servlet를 사용해서 HTML을 넘겨주는 것보다 덜 복잡하고 가독성이 좋다는 것을 느낄 수 있다.</p>
<p>그런데 한 가지 문제점이 생긴다.
JSP에서 view와 비지니스 로직 등 모든 부분이 들어가 있다가 보니 복잡해지고 복잡해지니 가독성이 떨어지고 결국에는 유지보수가 힘들어지게 되는 문제가 생겼다.</p>
<p>지금 위에 예시 코드에도 간단하지만 view를 나타내는 로직과 데이터를 관리하는 로직이 같이 있다.
지금은 간단하기 때문에 문제가 없지만 프로젝트가 커지게 되면 유지보수가 힘들어 질 것이다.</p>
<p>이러한 JSP를 해결하기 위해서 나온 것이 MVC패턴이다.</p>
<h3 id="mvc-패턴은-무엇일까">MVC 패턴은 무엇일까?</h3>
<p>MVC 패턴은 Model, View, Controller의 앞 글자를 따서 표현을 한 것이다.
MVC 패턴은 애플리케이션을 3가지 역할로 구분해서 개발하는 개발론이다.
3가지 역할이 Model, View, Controller이다.</p>
<p>Model, View, Controller가 어떤 역할을 담당을 하는 지 알아보자.</p>
<blockquote>
<p>Model : 데이터와 비지니스 로직을 처리하는 역할
View : UI, 보여지는 화면을 처리하는 역할
Controller : view와 model를 분리하고, view와 model을 연결하는 역할</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/4c062e30-f402-459a-b608-a4439e606337/image.png" alt=""></p>
<p>이제 위에 도식표를 보면서 알아보자.
요청을 받는 것은 controller가 받는다. 그리고 이 요청을 model에제 전달을 하고 model은 비지니스 로직을 수행을 해고 다시 controller에게 전달을 한다.
그리고 controller는 데이터를 view에 전달을 하고 view는 데이터를 화면으로 유저에게 보여준다.</p>
<h4 id="코드-mvc-패턴-공부하기">코드 MVC 패턴 공부하기</h4>
<p>도식화와 글도 좋은 자료이지만 개발자는 코드를 쳐보면 공부를 하면 좀 더 이해가 빨라진다.</p>
<pre><code class="language-java">// model

@Getter
@NoArgsConstructor
public class Student {

    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
</code></pre>
<p>Student라는 class로 model을 만들어 주었다.
model에서는 데이터를 저장하고 관리하는 역할을 한다.</p>
<pre><code class="language-java">package sample.servlet.study;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import sample.servlet.model.Student;

import java.io.IOException;

@WebServlet(name = &quot;studentServlet&quot;, urlPatterns = &quot;/student&quot;)
public class studentServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter(&quot;username&quot;);
        int age = req.getIntHeader(&quot;age&quot;);

        // 모델 샐성 및 데이터 전달
        Student student = new Student(username, age);

        // 뷰에 데이터 전달
        req.setAttribute(&quot;student&quot;, student);

        // view로 이동
        req.getRequestDispatcher(&quot;/student.jsp&quot;).forward(req, resp);
    }
}</code></pre>
<p>이전에 배운 servlet를 이용해서 controller를 만들었다.
파라미터를 받아서 student model을 만들어 주었고 이를 view에 전달을 하고 view를 랜더링을 시겼다.</p>
<pre><code class="language-java">&lt;jsp:useBean id=&quot;student&quot; scope=&quot;request&quot; type=&quot;javax.xml.stream.util.StreamReaderDelegate&quot;/&gt;
&lt;%--
  Created by IntelliJ IDEA.
  User: isanghyeog
  Date: 2024. 7. 30.
  Time: PM 9:13
  To change this template use File | Settings | File Templates.
--%&gt;
&lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&gt;
&lt;html&gt;
  &lt;head&gt;
      &lt;title&gt;Title&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
      &lt;h1&gt;Student Information&lt;/h1&gt;
      &lt;p&gt;Name: ${student.name}&lt;/p&gt;
      &lt;p&gt;Age: ${student.age}&lt;/p&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>JSP를 통해서 동적인 페이지를 만들어서 controller에서 넘겨준 데이터를 보여주고 있다.</p>
<p>이렇게 Model, controller, view를 통해서 역할을 구분하기 때문에 가독성이 높아지고 내가 원하는 부분을 쉽게 건드릴 수 있다. 즉, 유지보수가 쉬워진다는 것이다.</p>
<p>하지만 이러한 mvc패턴도 한계점이 존재한다.
바로 한 요청마다 하나의 servlet이 매핑이 된다는 단점이다.</p>
<p>만약, 하나씩 매핑이 되서 실행이 되면 공통적인 로직이 중복이 되는 한계가 생긴다.
즉, 공통처리가 어렵다는 것이다.</p>
<p>다음 블로그에서는 이 문제를 해결하기 위해서 Spring MVC에서는 이 Servlet를 어떻게 사용을 하고 있는지 알아볼 것이다.</p>
<h3 id="참고-자료">참고 자료</h3>
<p><a href="https://www.youtube.com/watch?v=h0rX720VWCg&amp;t=401s">[10분 테코톡] 루키의 Servlet &amp; Spring Web MVC</a>
<a href="https://binco.tistory.com/entry/Java-MVC%ED%8C%A8%ED%84%B4-%EB%B0%94%EB%A1%9C%EC%95%8C%EA%B8%B0">Java MVC 패턴 바로 알기</a>
<a href="https://www.youtube.com/watch?v=ogaXW6KPc8I">[10분 테코톡] 🧀 제리의 MVC 패턴</a>
<a href="https://www.youtube.com/watch?v=uoVNJkyXX0I">[10분 테코톡] 👩🏻‍💻👨🏻‍💻해리&amp;션의 MVC 패턴</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Servlet의 대해서 알아보자!]]></title>
            <link>https://velog.io/@sang_hyeok_2/Spring-Servlet%EC%9D%98-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@sang_hyeok_2/Spring-Servlet%EC%9D%98-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Mon, 29 Jul 2024 06:01:39 GMT</pubDate>
            <description><![CDATA[<h3 id="overview">Overview</h3>
<p>Spring에서 요청을 어떻게 처리하고 응답하는 과정에 대해서 이해하기 위해선 Servlet이라는 것에 대해서 알아야 한다.
그 이유는 동적인 요청에 대해서 처리하는 역할을 하는 것이 Servlet이기 때문이다.
이 포스트에서는 Servlet가 무엇인지에 대해서 알아보자.</p>
<h3 id="servlet이-생긴-이유는-뭘까">Servlet이 생긴 이유는 뭘까?</h3>
<h4 id="초기-web-server-통신">초기 Web Server 통신</h4>
<p>Servlet이 생긴 이유에 대해서 알기 위해서는 기존에 웹 통신이 어떻게 이루어졌는 지 알아보아야 한다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/06a125de-7120-493b-9e46-62d5446983b7/image.png" alt=""></p>
<p>기존의 웹 통신은 요청이 오면 index.html 같은 이미 만들어진 정적 파일들을 응답으로 보내주었다.
그래서 다른 사용자들이 요청을 해도 똑같은 index.html을 응답으로 보내주었다.
그런데 시간이 지날 수록 사용자마다 다르게 페이지를 보여주고 싶게 되고 페이지를 동적으로 처리하기를 바라게 된다.</p>
<h4 id="동적처리를-위한-cgi-등장">동적처리를 위한 CGI 등장</h4>
<p>CGI는 Common Gateway Interface의 약자로 요청을 동적으로 처리하기 위한 인터페이스이다.
CGI는 인터페이스이다. 인테페이스는 어떤 규약인데 즉, 웹 서버와 프로그램이 통신을 하기 위한 규약이다.</p>
<p>동적인 페이지를 만들기 위해선 웹 서버가 프로그램과 통신을 주고 받아야 하는데 언어가 다른 경우도 있고 환경이 다른 경우도 있으니 규약이 필요해진다. 이 규약을 인테페이스로 만든이 것이 CGI이다.
(인터페이스이므로 다양한 언어로 구현이 가능하다.)</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/5c5104b9-6835-48bc-84fd-dfd398da3a0c/image.png" alt=""></p>
<p>클라이언트에서 동적인 요청을 보내게 되면 웹 서버는 CGI에게 위임을 한다.
그러면 CGI가 동적인 처리를 하고 이를 html의 형태로 웹 서버에게 보내주면 웹 서버는 받아서 클라이언트에게 전달을 해준다.</p>
<p>CGI를 통해서 사용자들은 자신의 이름이 들어간 페이지라던가 오늘 방문한 방문자 수 같은 동적인 페이지를 받을 수 있게 되었다.</p>
<h4 id="cgi의-문제점">CGI의 문제점</h4>
<p>하지만 CGI에는 여러 가지 문제가 있었다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/be3ca5e0-4b95-4bb8-9fe1-fc2bc0016dd4/image.png" alt=""></p>
<p>첫 번째로 요청이 올 때마다 프로세스를 생성하는 것이다.
프로세스는 메모리를 많이 차지한다. 즉, 요청이 올 때마다 쓸데 없이 메모리를 많이 차지하게 되고 이는 메모리 낭비로 이어지는 것이다.</p>
<p>두 번째는 같은 객체를 사용하더라고 요청이 다르면 새로 객체를 생성이 되는 것이다.
이도 마찬가지로 객체가 차지하는 메모리가 많기 때문에 메모리 낭비로 이어지게 된다.</p>
<h4 id="servlet의-등장">Servlet의 등장</h4>
<p>위에 말한 CGI의 문제를 해결하기 위해서 나온 것이 Servlet이다.
서블릿은 CGI와 다르게 스레드를 사용해서 들어온 요청을 처리한다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/b55382a6-f77f-4fb4-bd1d-d7067120ce5a/image.png" alt=""></p>
<p>Servlet는 Java는 단일 인스턴스를 사용을 하고 있기 때문에 객체를 여러 개 생성할 필요가 없다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/2ce8e7dd-5624-4e1e-b244-5c2f2537143c/image.png" alt=""></p>
<p>Servlet은 개발자가 관리를 하지 않는다.
Servlet은 WAS, Web Application Server가 관리를 한다.
WAS에는 Servlet을 관리하는 Web Container(이하 웹 컨테이너)가 있고 이 Web Container 안에는 Servlet를 관리하는 Servlet Container(이하 서블릿 컨테이너)가 존재한다.
그래서 웹 서버에서 동적인 요청이 들어오면 WAS로 넘기면 웹 컨테이너에서 Servlet을 관리하면서 동적인 처리를 한다. </p>
<h3 id="servlet-파헤져보기">Servlet 파헤져보기</h3>
<p>이제 Servlet에 대해서 파헤져보자.</p>
<h4 id="servlet의-생명주기">Servlet의 생명주기</h4>
<p>먼저, Servlet의 생명 주기를 알아보고 동작원리에 대해서 알아보자.</p>
<p>Servlet의 생명주기에는 초기화, 작업 수행, 종료가 있다.</p>
<blockquote>
<p>init() - 초기화 : 서블릿 인스턴스를 생성되는 메소드
service() - 작업 수행 : 실제 기능이 실행이 되는 메소드
destory() - 종료 : 서블릿 인스턴스를 메모리에서 지우는 메소드</p>
</blockquote>
<h4 id="servlet의-동작-원리">Servlet의 동작 원리</h4>
<p>그러면 이제 요청이 들어왔을 때 서블릿이 어떻게 동작을 하는 지 알아보자.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/bf4f6585-9f76-4700-8652-7fb9324ed038/image.png" alt=""></p>
<p>브라우저에서 요청을 보내면 Http Request를 WAS에 보내고 WAS의 웹 컨테이너는 서블릿 컨테이너에 요청을 보내주면 서블릿 컨테이너는 Request 객체와 Response 객체를 만든다. 그리고 서블릿에게 넘겨준다.
서블릿 컨테이너는 이 요청이 어느 서블릿에 대한 요청인지를 판단을 한다. 만약 해당 서블릿이 존재하지 않는다면 서블릿의 인스턴스를 생성을 하고 init() 메소드를 실행한다.
다음으로 service()를 실행을 한다. service() 메소드를 통해서 doGet(), doPost() 같은 메소드로 요청을 처리한다.
요청 처리가 완료가 되면 Response를 통해서 응답을 작성을 하고 반환을 한다.</p>
<h4 id="servlet-코드-살펴보기">Servlet 코드 살펴보기</h4>
<p>이제 Servlet의 동작원리에 대해서 알아보았으니 Servlet의 코드에 대해서 알아보자.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/e9eecaba-92ec-4ea6-9264-c56d253ba8ad/image.png" alt=""></p>
<p>Servlet은 인터페이스 Servlet와 ServletConfig를 구현한 GenericServlet를 HttpServlet가 상속을 받고 있고 이를 사용해서 Servlet를 사용을 한다.</p>
<p>Servlet는 우리가 위에서 이야기 했던 init, service 등 메소들이 있고 ServletConfig는 servlet을 초기화할 때, 필요한 설정들을 관리하기 위해서 필요한 것이다.
이 두 인터페이스를 구현한 것이 GenericServlet이고 이 GenericServlet를 상속 받아서 HttpServlet을 사용을 하고 있다.</p>
<p>우리는 Servlet 인터페이스와 HttpServlet 클래스를 살펴볼 것이다.</p>
<pre><code class="language-java">
// Servlet 인터페이스 

package jakarta.servlet;

import java.io.IOException;

public interface Servlet {
    void init(ServletConfig var1) throws ServletException;

    ServletConfig getServletConfig();

    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

    String getServletInfo();

    void destroy();
}
</code></pre>
<p>Servlet 인터페이스를 보면 위에서 살펴 본 init(), service(), destory() 메소드들이 있다.
이제 이 후에 구현체에서 이 메소드들을 구현을 해서 servlet 구현체에서 사용을 할 수 있다.</p>
<pre><code class="language-java">public abstract class HttpServlet extends GenericServlet {
 // 생략
  public void init(ServletConfig config) throws ServletException {
        super.init(config);
        this.cachedUseLegacyDoHead = Boolean.parseBoolean(config.getInitParameter(&quot;jakarta.servlet.http.legacyDoHead&quot;));
  }

  protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String method = req.getMethod();
        long lastModified;
        if (method.equals(&quot;GET&quot;)) {
            lastModified = this.getLastModified(req);
            if (lastModified == -1L) {
                this.doGet(req, resp);
            } else {
                long ifModifiedSince;
                try {
                    ifModifiedSince = req.getDateHeader(&quot;If-Modified-Since&quot;);
                } catch (IllegalArgumentException var9) {
                    ifModifiedSince = -1L;
                }

                if (ifModifiedSince &lt; lastModified / 1000L * 1000L) {
                    this.maybeSetLastModified(resp, lastModified);
                    this.doGet(req, resp);
                } else {
                    resp.setStatus(304);
                }
            }
        } else if (method.equals(&quot;HEAD&quot;)) {
            lastModified = this.getLastModified(req);
            this.maybeSetLastModified(resp, lastModified);
            this.doHead(req, resp);
        } else if (method.equals(&quot;POST&quot;)) {
            this.doPost(req, resp);
        } else if (method.equals(&quot;PUT&quot;)) {
            this.doPut(req, resp);
        } else if (method.equals(&quot;DELETE&quot;)) {
            this.doDelete(req, resp);
        } else if (method.equals(&quot;OPTIONS&quot;)) {
            this.doOptions(req, resp);
        } else if (method.equals(&quot;TRACE&quot;)) {
            this.doTrace(req, resp);
        } else {
            String errMsg = lStrings.getString(&quot;http.method_not_implemented&quot;);
            Object[] errArgs = new Object[]{method};
            errMsg = MessageFormat.format(errMsg, errArgs);
            resp.sendError(501, errMsg);
        }

   }

   public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        HttpServletRequest request;
        HttpServletResponse response;
        try {
            request = (HttpServletRequest)req;
            response = (HttpServletResponse)res;
        } catch (ClassCastException var6) {
            throw new ServletException(lStrings.getString(&quot;http.non_http&quot;));
        }

        this.service(request, response);
    }

 // 생략

}</code></pre>
<p>Http Servlet에는 많은 메소들이 있지만 우리는 init(), service()를 살펴볼 것이다.
init() 메소드에 경우에는 말 그대로 부모 클래스의 init()를 불러와서 초기화를 진행을 하고 초기화 파라미터 값을 boolean 값으로 변경을 해서 변수 저장을 해줌으로서 요청이 다시 들어올 때 초기화 하는 과정을 생략을 해주는 역할을 해준다.</p>
<p>service()는 두 개가 있다.
먼저, 아래에 있는 service()를 보면 HttoServletRequest와 HttpServletResponse로 형 변환을 해준 뒤에 위에 있는 service()로 넘겨주면서 service()를 실행을 한다.
두 번쨰 service()에서는 request에서 method를 꺼내와서 메소드의 종류에 따라 그에 맞는 do 메소드들을 실행을 시켜준다.</p>
<h4 id="httpservlet를-이용해서-servlet-사용해-보기">HttpServlet를 이용해서 Servlet 사용해 보기</h4>
<p>우리는 Servlet이 어떻게 나왔고 어떻게 동작을 하고 내부는 어떻게 구성이 되어 있는 지 알아보았다.
이제 이 HttpServlet를 상속을 받아서 실제로 동작을 하는 원리 한 번 구현을 해보자.</p>
<p>먼저, 설정을 해주고 실행이 되는 지 확인을 해보자</p>
<pre><code class="language-java">
@SpringBootApplication
@ServletComponentScan
public class ServletApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServletApplication.class, args);
    }

}

</code></pre>
<p>Application class에 @ServletComponentScan이라는 어노테이션을 통해서 @WebServlet 어노테이션이 붙은 클래스를 스캔할 해서 등록을 할 수 있도록 해준다.</p>
<pre><code class="language-java">package sample.servlet.study;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

@WebServlet(name = &quot;helloServlet&quot;, urlPatterns = &quot;/hello&quot;)
public class HelloServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println(&quot;----- 실행 -----&quot;);
    }
}</code></pre>
<p>@WebServlet를 통해서 Servlet의 이름과 url을 맵핑을 시켜주고 요청이 들어왔을 때 잘 실행이 되는 지를 확인을 하기 위해서 실행이라는 글을 출력을 시켜보자</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/ea685eaf-99f3-4739-96a6-c7cf20d9eba3/image.png" alt=""></p>
<p>localhost:8080/hello로 요청을 보냈을 때 실행이 잘 나오는 것을 알 수 있다.</p>
<pre><code class="language-java">package sample.servlet.study;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

@WebServlet(name = &quot;helloServlet&quot;, urlPatterns = &quot;/hello&quot;)
public class HelloServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println(&quot;request = &quot; + req);
        System.out.println(&quot;response = &quot; + resp);
    }
}</code></pre>
<p>다음으로는 request와 response를 출력을 해보았다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/e8fa844d-fed2-4d52-a726-7f7ab334b161/image.png" alt=""></p>
<p>request와 response는 객체이다.
위에서 서블릿 컨테이너가 HttpRequest를 통해서 request, response 객체를 만들어서 servlet에 넘겨준다고 했는데 그 request, response가 바로 여기 사진에 나와 있는 객체이다.</p>
<p>이제 파라미터를 받아보자.</p>
<pre><code class="language-java">@WebServlet(name = &quot;helloServlet&quot;, urlPatterns = &quot;/hello&quot;)
public class HelloServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter(&quot;username&quot;);
        System.out.println(&quot;username = &quot; + username);
    }
}</code></pre>
<p>request에 getParameter() 메소드를 사용을 해서 값을 받아서 오면 </p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/54fa1d92-e373-4439-aefc-bd29711243ff/image.png" alt=""></p>
<p>이렇게 받을 수 있는 것을 확인을 할 수 있다.</p>
<p>그러면 이 username을 받아서 간단하게 동적으로 페이지를 반환을 해보자.</p>
<pre><code class="language-java">@WebServlet(name = &quot;helloServlet&quot;, urlPatterns = &quot;/hello&quot;)
public class HelloServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter(&quot;username&quot;);
        System.out.println(&quot;username = &quot; + username);

        resp.setContentType(&quot;text/plain&quot;);
        resp.setCharacterEncoding(&quot;utf-8&quot;);
        resp.getWriter().write(&quot;hello&quot; + username);

    }
}
</code></pre>
<p>response를 이용을 해서 반환을 해줄 내용을 작성을 하고 먼저, 이상혁이라는 이름으로 요청을 보내보자.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/a4af5d2a-dcc4-4028-934d-b9fa3032e60e/image.png" alt=""></p>
<p>이상혁이라는 이름으로 잘 나온다. 그러면 이번에는 김철수라는 이름을 파라미터로 넣어서 요청을 보내보자.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/bcff03ca-bdcc-411e-8947-d16d230f9feb/image.png" alt=""></p>
<p>파라미터를 김철수로 변경을 해서 보내보니 이름이 변경이 되어서 잘 나오는 것을 알 수 있었다.</p>
<h3 id="간단하게-정리하기">간단하게 정리하기</h3>
<p>우리가 username이라는 파라미터를 넣어서 우리가 원하는 이름이 나올 수 있도록 하였다.
이처럼 서블릿은 우리가 원하는 값을 통해서 사용자마다 화면이 다르게 혹은 화면에서 일부분이 다르게 보일 수 있도록 동적으로 페이지 처리를 도와주는 역할을 한다.
또 요청에 따라 서블릿을 다르게 해서 원하는 형태로 보여줄 수도 있다.
이러한 서블릿으로 인해서 JSP와 MVC 패턴으로 이어지기도 하게 된다.</p>
<h3 id="참고-자료">참고 자료</h3>
<p><a href="https://www.youtube.com/watch?v=3gmOuUWPZV4&amp;t=234s">[10분 테코톡] 리오, 밀리의 Servlet &amp; Spring MVC</a>
<a href="https://www.youtube.com/watch?v=cmwmamOQmPc&amp;t=449s">[10분 테코톡] 👨‍🎨규동의 Servlet &amp; Spring</a>
<a href="https://www.youtube.com/watch?v=2pBsXI01J6M&amp;t=370s">[10분 테코톡] 🌻타미의 Servlet vs Spring</a>
<a href="https://java-is-happy-things.tistory.com/23">Servlet (서블릿)이해하기 + 실습예제</a>
<a href="https://velog.io/@cocodori/Servlet">Servlet</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ [Java]배열에서 toString을 사용하는 법]]></title>
            <link>https://velog.io/@sang_hyeok_2/Java%EB%B0%B0%EC%97%B4%EC%97%90%EC%84%9C-toString%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B2%95</link>
            <guid>https://velog.io/@sang_hyeok_2/Java%EB%B0%B0%EC%97%B4%EC%97%90%EC%84%9C-toString%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B2%95</guid>
            <pubDate>Wed, 24 Jul 2024 12:28:08 GMT</pubDate>
            <description><![CDATA[<h3 id="overview">Overview</h3>
<p>배열에 toString을 하게 되면 배열의 메모리 주소가 나온다.
그런데 toString을 했을 때 배열 안에 있는 원소들이 나오게 하고 싶다.
하지만 배열은 toString을 재정의 할 수가 없다.
그렇다면 toString을 했을 때 배열 안에 원소들을 나타내는 방법는 무엇일까? </p>
<h3 id="참조형-타입-tostring-사용해보기">참조형 타입 toString 사용해보기</h3>
<p>먼저, 참조형 타입에 toString을 사용해보자.</p>
<pre><code class="language-java">@RequiredArgsConstructor
public class Person {

    private String name;
    private int age;

}
</code></pre>
<pre><code class="language-java">public class ToString {
    public static void main(String[] args) {

        Person person = new Person();
        System.out.println(&quot;person = &quot; + person.toString());

    }
}</code></pre>
<p>Person 클래스를 만들고 필드 값으로 name과 age를 넣어 주고 ToString 클래스에서 객체를 생성을 하고 toString을 해주었다.
이 때의 결과를 보자.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/99756909-2c32-4064-aff4-2ef5c7881c9a/image.png" alt=""></p>
<p>객체의 위치가 어디에 있는지에 대한 정보, 타입 그리고 객체의 메모리 주소가 나와 있다.
그러면 이제 toString을 재정의 해서 객체에 필드 값들의 정보를 알 수 있도록 해보자.</p>
<pre><code class="language-java">@RequiredArgsConstructor
public class Person {

    private String name;
    private int age;


    @Override
    public String toString() {
        return &quot;Person{&quot; +
                &quot;name=&#39;&quot; + name + &#39;\&#39;&#39; +
                &quot;, age=&quot; + age +
                &#39;}&#39;;
    }
</code></pre>
<p>toString을 재정의하고 main 메소드를 실행을 해본 결과를 보자.
<img src="https://velog.velcdn.com/images/sang_hyeok_2/post/494ff08c-4aa7-4f82-8908-45fbcdfcea26/image.png" alt="">
지금 생성자의 값을 넘겨주지 않아서 기본 값이 나오고 있는데, 보면 재정의를 하지 않은 경우와 다르게 객체 안에 필드 값들을 나올 수 있도록 하였다.</p>
<h3 id="배열에서-tostring-사용해보기">배열에서 toString 사용해보기</h3>
<p>그렇다면 배열을 만들고 toString 메소드를 사용해보자.</p>
<pre><code class="language-java">public class ToString {
    public static void main(String[] args) {

        int[] arr = {1, 2, 3, 4, 5};

        System.out.println(&quot;arr = &quot; + arr.toString());

    }
}</code></pre>
<p>기본형 자료인 int를 담은 배열을 만들었다.
그리고 이 배열을 toString을 어떤 값이 나오는 지 알아보자.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/a572d96e-2063-446f-9d2f-42a1c3edd9d8/image.png" alt=""></p>
<p>배열의 메모리 주소가 나온다.
즉, 배열 자체에 toString을 했을 때는 그 배열에 메모리 주소가 나오는 것을 알 수 있다.
그런데 내가 원하는 것은 배열에 요소들이 나오는 것을 원하다.
배열에서는 toString을 재정의 하지 못한다.
그렇다면 어떻게 해야 할까?</p>
<h3 id="arraystostring-사용하기">Arrays.toString 사용하기</h3>
<p>위에서 말 했듯이 배열에서는 toString을 재정의하지 못 한다.
그래서 사용하는 것이 Arrays의 toString 메소드이다.
이 메소드를 사용해보자.</p>
<pre><code class="language-java">public class ToString {
    public static void main(String[] args) {

        int[] arr = {1, 2, 3, 4, 5};

        System.out.println(&quot;arr = &quot; + Arrays.toString(arr));

    }
}
</code></pre>
<p>Arrays.toString을 사용을 했다.
결과를 보자.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/3340ba97-a66e-4656-8514-f06062ee0f20/image.png" alt="">
Arrays.toString 메소드를 사용하니 내가 원하는 형태의 값이 나왔다.</p>
<h4 id="arraystostring-내부-살펴보기">Arrays.toString 내부 살펴보기</h4>
<p>그렇다면 Arrays.toString 메소드의 내부는 어떻게 생겼을까?</p>
<pre><code class="language-java"> public static String toString(int[] a) {
        if (a == null)
            return &quot;null&quot;;
        int iMax = a.length - 1;
        if (iMax == -1)
            return &quot;[]&quot;;

        StringBuilder b = new StringBuilder();
        b.append(&#39;[&#39;);
        for (int i = 0; ; i++) {
            b.append(a[i]);
            if (i == iMax)
                return b.append(&#39;]&#39;).toString();
            b.append(&quot;, &quot;);
        }
    }</code></pre>
<p>코드 내부를 보면 먼저 배열을 매개변수로 담아서 null인지를 체크하고 배열의 길이에서 -1을 해준 값을 iMax에 담아준다.
그리고 만약 iMax가 -1이면 빈배열 표시는 반환한다.
이는 배열이 빈 배열일 때 길이가 0이라고 -1을 하면 iMax가 -1이기 때문이다.</p>
<p>그리고 StringBuilder를 통해서 b라는 변수에 배열 안에 요소들을 문자열로 하나씩 추가를 해주고 리턴을 시켜주면 내가 원하는 형태의 값이 나온다.</p>
<p>지금은 예시로 int 타입을 예시로 들었는데 다른 타입도 타입만 다를 뿐 내부 로직은 같다.</p>
<h3 id="참조형-타입-arraystostring-사용해보기">참조형 타입 Arrays.toString 사용해보기</h3>
<p>이제 참조형 타입을 배열에 넣고 Arrays.toString을 사용해보자.</p>
<h4 id="클래스에-tostring을-재정의하지-않은-경우">클래스에 toString을 재정의하지 않은 경우</h4>
<p>먼저, 참조형 타입에 toStrig을 재정의를 하지 않고 사용을 해보자.</p>
<pre><code class="language-java">@RequiredArgsConstructor
public class Person {

    private String name;
    private int age;

}</code></pre>
<pre><code class="language-java">public class ToString {
    public static void main(String[] args) {

        Person person1 = new Person();
        Person person2 = new Person();
        Person person3 = new Person();
        Person person4 = new Person();
        Person person5 = new Person();

        Person[] arr = {person1, person2, person3, person4, person5};

        System.out.println(&quot;arr = &quot; + Arrays.toString(arr));

    }
}</code></pre>
<p>Person의 재정의를 한 toString을 지우고 5개의 객체를 만들어서 배열에 담아 주었다.
이제 결과를 보자.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/93000905-4e20-4f12-9b64-da0b8bf593e7/image.png" alt=""></p>
<p>값이 길어서 사진에서 짤리긴 했는데 보면 배열은 잘 나오는 데 값이 객체의 위치 정보와 메모리 주소 값이 나온 것을 알 수 있다.
그 이유는 Person 클래스에서 toString 재정의가 되어 있지 않기 때문이다.</p>
<h4 id="클래스에-tostring을-재정의-하는-경우">클래스에 toString을 재정의 하는 경우</h4>
<pre><code class="language-java">@RequiredArgsConstructor
public class Person {

    private String name;
    private int age;


    @Override
    public String toString() {
        return &quot;Person{&quot; +
                &quot;name=&#39;&quot; + name + &#39;\&#39;&#39; +
                &quot;, age=&quot; + age +
                &#39;}&#39;;
    }
}
</code></pre>
<p>Person 클래스에 toString을 재정의하고 다시 main 메소드를 시작해보자.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/08320539-e150-4d96-9eaa-2d0f5c80ec5e/image.png" alt="">
마찬가지로 길어서 사진이 짤렸다.
toString을 했을 때는 객체의 내부 필드 정보들이 나온다.
이를 통해서 객체, 참조형 타입의 값을 담은 배열의 정보를 메모리 주소가 아닌 클래스 내부 정보로 나오게 하고 싶다면 클래스의 toString을 재정의 해주어야 한다.</p>
<h3 id="참고자료">참고자료</h3>
<p><a href="https://crmn.tistory.com/61">[자바]배열 내용 출력 하기(Arrays.toString())</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] Equals와 HashCode]]></title>
            <link>https://velog.io/@sang_hyeok_2/Java-Equals%EC%99%80-HashCode</link>
            <guid>https://velog.io/@sang_hyeok_2/Java-Equals%EC%99%80-HashCode</guid>
            <pubDate>Mon, 15 Jul 2024 12:46:59 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/d1439c3c-6003-4a44-aac4-b0e687a896f2/image.png" alt=""></p>
<h3 id="overview">overview</h3>
<p>참조형 타입을 비교하는 방법으로 ==와 Equals를 사용을 한다.
우리는 오늘 Equals에 대해서 알아볼 것이다.
그리고 Equals를 알게 되면 항상 같이 따라 나오는 HashCode가 있다.
이번 포스트에서는 Equals와 HashCode에 대해서 알아 볼 것이다.</p>
<h3 id="equals">Equals</h3>
<p>우리는 두 가지 방법 중에 Eqauls에 대해서 알아보고자 한다.
Equals를 잘 설명을 하기 위해서 Student 클래스를 통해서 예시를 사용하겠다.</p>
<pre><code class="language-java">@RequiredArgsConstructor
public class Student {

    private final String name;

}</code></pre>
<p>굉장히 단순한 객체를 만들었다.
Student라는 객체이고 name이라는 값을 갔는다.
이 Student 클래스로 객체 2개를 만들어서 equals를 재정의 하지 않았을 때와 재정의 했을 때를 비교를 해보자 </p>
<h4 id="equals를-재정의-하지-않았을-때">equals를 재정의 하지 않았을 때</h4>
<pre><code class="language-java">public class EqualsAndHashCode {

    public static void main(String[] args) {
**텍스트**
        Student studentA = new Student(&quot;student&quot;);
        Student studentB = new Student(&quot;student&quot;);

        boolean equals = studentA.equals(studentB);
        if (equals) {
            System.out.println(&quot;studentA와 student는 같다.&quot;);
            System.out.println(&quot;studentA : &quot; + studentA);
            System.out.println(&quot;studentB : &quot; + studentB);
        } else {
            System.out.println(&quot;studentA와 student는 다르다.&quot;);
            System.out.println(&quot;studentA : &quot; + studentA);
            System.out.println(&quot;studentB : &quot; + studentB);
        }

    }

}
</code></pre>
<p>EqualsAndHashCode라는 클래스의 main 메소드에 Student 클래스로 studentA와 studentB를 만들었다. 그리고 equals를 재정의를 하지 않은 상태에서 equals 메소드를 통해서 값을 비교해보자. 
main 메소드를 실행을 해보았을 때, 결과를 보자.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/f35e3531-b243-43f5-afc8-b2acb2b45644/image.png" alt=""></p>
<p>studentA와 studentB가 다르다라는 것을 볼 수 있다.
같은 클래스, 같은 name으로 객체를 생성하였는데 왜 다를까?
그 이유는 <strong>equals를 재정의하지 않고 비교를 하게 되면 각 객체의 주소 값을 보고 비교</strong>를 한다.
즉, 두 객체는 다른 메모리 주소에 저장이 되기 때문에 다른 객체라고 판단을 한다.
두 객체를 같은 객체로 판단을 하기 위해서는 equals의 재정의가 필요하다.</p>
<h4 id="equals를-재정의한-경우">equals를 재정의한 경우</h4>
<p>이제, equals를 재정의한 경우를 보자.</p>
<pre><code class="language-java">@RequiredArgsConstructor
public class Student {

    private final String name;

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }

        Student student = (Student) obj;

        return Objects.equals(name, student.name);
    }
}</code></pre>
<p>@Overrivde를 통해서 equals를 재정의를 해주었다.
그리고 나서 위에 main 메소드를 다시 시작을 해보자.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/6e63e19c-ac34-44eb-94ec-0478e11ad22f/image.png" alt=""></p>
<p>studentA와 studentB가 같다고 판단을 하고 있다.
이처럼 equals를 재정의를 통해서 논리적으로 같은 객체인지를 판단을 할 수 있다.</p>
<p>반대로 equals를 재정의하지 않으면 오직 자기 자신과만 같게 된다.</p>
<h3 id="hashcode">HashCode</h3>
<p>Equals를 알게 되면 항상 따라오는 녀석이 있다.
바로 HashCode이다.</p>
<p>HashCode는 생성한 객체를 식별하는 고유한 코드이다.
HashCode 메소드는 이 HashCode를 재정의 하는 메소드이다.</p>
<p>그러면 이 HashCode를 재정의 하는 것은 왜 필요할까?</p>
<p>아래의 코드를 보자</p>
<pre><code class="language-java">package org.example.study.study.equalsAndHashCode;

import java.util.HashSet;
import java.util.Set;

public class EqualsAndHashCode {

    public static void main(String[] args) {

        Student studentA = new Student(&quot;student&quot;);
        Student studentB = new Student(&quot;student&quot;);

        boolean equals = studentA.equals(studentB);
        if (equals) {
            System.out.println(&quot;studentA와 student는 같다.&quot;);
            System.out.println(&quot;studentA : &quot; + studentA);
            System.out.println(&quot;studentB : &quot; + studentB);
        } else {
            System.out.println(&quot;studentA와 student는 다르다.&quot;);
            System.out.println(&quot;studentA : &quot; + studentA);
            System.out.println(&quot;studentB : &quot; + studentB);
        }

        Set&lt;Student&gt; studentSet = new HashSet&lt;&gt;();
        studentSet.add
studentA);
        studentSet.add(studentB);
        System.out.println(&quot;studentSetSize : &quot; + studentSet.size());

    }

}
</code></pre>
<p>우리가 아까 보았던 EqualsAndHashCode에서 Student 클래스에 HashCode 메소드를 재정의하지 않은 상태에서 중복을 허용하지 않은 Collection의 Set을 사용해서 studentA와 studentB를 요소로 추가하고 그 크기를 출력하고 있다.</p>
<p>현재, studentA와 studentB는 논리적으로 같다고 판단을 하고 있다. 그래서 크기도 1이 될 것이라고 예상을 한다. main 메소드를 실행을 해보자.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/9134be5e-1379-4cc3-8c5b-c3be3c575bf2/image.png" alt=""></p>
<p>결과를 보면 예상과 다르게 2가 나오는 것을 볼 수 있다.
그 이유는 뭘까?</p>
<p>studentSetSize : 2의 위를 보면 객체의 정보가 나오고 @ 뒤에 임의 값이 나오는 것을 볼 수 있다.
이 임의 값이 해쉬코드이다. Set이 두 객체를 다를 게 판단을 한 것은 이 해쉬 값이 다르기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/347b12b9-e8d8-4cf6-924d-6bd6d66e2cf3/image.png" alt=""></p>
<p>Set의 경우 참조형 타입의 중복을 판단을 할 때는 먼저, HashCode의 값을 판단을 한다.
우리가 든 예시에 경우, 이 HashCode의 값이 다르기 때문에 다른 객체로 판단을 한 것이다.
만약, HashCode 값이 같으면 우리가 먼저, 재정의를 해주었던 Equals의 리턴 값으로 true이면 같은 객체, false이면 다른 객체라고 판단을 한다.</p>
<p>이 이유 때문에 Equals와 HashCode가 많이 묶여서 이야기가 나온다.
Set은 HashCode와 Equals 2가지를 보고 판단을 하기 때문이다.</p>
<p>그러면 HashCode를 재정의한 경우를 코드로 보자</p>
<pre><code class="language-java">@RequiredArgsConstructor
public class Student {

    private final String name;

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }

        Student student = (Student) obj;

        return Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}
</code></pre>
<p>@로 hashcode 메소드를 재정의 하였고 이제 main 메소드를 실행해보자</p>
<p><img src="https://velog.velcdn.com/images/sang_hyeok_2/post/de695dc2-a047-4062-aec2-d0abbd194fee/image.png" alt=""></p>
<p>결과를 보면 studentA와 studentB는 같고 정보 뒤에 HashCode도 같다.
그리고 studnetSetSize도 우리가 예상한 1이 나온다.</p>
<h3 id="참고-자료">참고 자료</h3>
<p><a href="https://tecoble.techcourse.co.kr/post/2020-07-29-equals-and-hashCode/">equals와 hashCode는 왜 같이 재정의해야 할까?</a>
<a href="https://velog.io/@sonypark/Java-equals-hascode-%EB%A9%94%EC%84%9C%EB%93%9C%EB%8A%94-%EC%96%B8%EC%A0%9C-%EC%9E%AC%EC%A0%95%EC%9D%98%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C">[Java] equals() &amp; hashcode() 메서드는 언제 재정의해야 할까?</a></p>
]]></description>
        </item>
    </channel>
</rss>