<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>wo_ogie.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 08 Oct 2024 17:34:12 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>wo_ogie.log</title>
            <url>https://velog.velcdn.com/images/wo_ogie/profile/b8fb9af8-60a6-4f33-92e5-17479670066b/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. wo_ogie.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/wo_ogie" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[JUnit과 Mockito로 단위 테스트 작성해보며 이해하기]]></title>
            <link>https://velog.io/@wo_ogie/JUnit%EA%B3%BC-Mockito%EB%A1%9C-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%91%EC%84%B1%ED%95%B4%EB%B3%B4%EB%A9%B0-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@wo_ogie/JUnit%EA%B3%BC-Mockito%EB%A1%9C-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%91%EC%84%B1%ED%95%B4%EB%B3%B4%EB%A9%B0-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 08 Oct 2024 17:34:12 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@wo_ogie/%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4%EB%A5%BC-%EA%B2%80%EC%A6%9D%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8Unit-Test#%EC%A0%95%EB%A6%AC">소프트웨어를 검증하는 방법: 단위 테스트(Unit Test)</a>에서 단위 테스트가 무엇인지, 그리고 단위 테스트에 대한 기본적인 개념들에 대해 살펴보았습니다.</p>
<p>이번 글에서는 JUnit, Mockito를 활용해서 Java/Spring Boot 환경에서 Mocking 방식으로 단위 테스트를 작성하는 방법을 자세히 알아보고자 합니다. 또한, 외부 의존성 없이 테스트를 독립적으로 수행할 수 있는 Stubbing(classical testing) 방식의 테스트 작성법도 함께 살펴보고자 합니다.</p>
<h1 id="예제-코드-살펴보기">예제 코드 살펴보기</h1>
<p>본문에서는 controller-service-repository의 layered 아키텍처로 구성된 API 서버를 예제로 하여, 이에 대한 단위 테스트 작성법에 대해 알아볼 것이다. 우선 단위 테스트를 작성할 예제를 살펴보자.</p>
<p>이번 예제에서 살펴볼 기능은 게시판 서비스의 CRUD 기능들로서, 다음과 같다.</p>
<ul>
<li><code>CREATE</code> 신규 게시글 업로드</li>
<li><code>READ</code> 게시글 단건 조회</li>
<li><code>READ</code> 전체 게시글 목록 조회</li>
<li><code>UPDATE</code> 게시글 수정</li>
<li><code>DELETE</code> 게시글 삭제</li>
</ul>
<p>테스트 케이스를 먼저 설계 및 작성한 후 기능을 구현하는 TDD(Test Driven Development, 테스트 주도 개발) 방법론도 있다. 그러나 이번 글의 목적은 &#39;단위 테스트를 작성하는 방법에 대해 알아보는 것&#39;이므로 이에 집중하여 구현된 코드에 대한 테스트 코드를 작성해보려고 한다.</p>
<h2 id="domain-entity">Domain entity</h2>
<pre><code class="language-java">@AllArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode
@Getter
public class Post {
    private Long id;    // 게시글 id
    private Long writerId;    // 게시글 작성자 id
    private String title;    // 게시글 제목
    private String content;    // 게시글 내용

    public static Post withId(Long id, Long writerId, String title, String content) {
        return new Post(id, writerId, title, content);
    }

    public static Post withoutId(Long writerId, String title, String content) {
        return new Post(null, writerId, title, content);
    }

    public void updateTitle(String title) {
        if (StringUtils.hasText(title)) {
            this.title = title;
        }
    }

    public void updateContent(String content) {
        if (StringUtils.hasText(content)) {
            this.content = content;
        }
    }

    public void verifyUpdateOrDeletePermission(Long userId) {
        if (!this.writerId.equals(userId)) {
            throw new IllegalArgumentException(&quot;게시물을 수정/삭제할 수 있는 권한이 없습니다. 게시물은 작성자만 수정/삭제할 수 있습니다.&quot;);
        }
    }
}</code></pre>
<h2 id="repository">Repository</h2>
<pre><code class="language-java">public interface UserRepository {
    boolean existsById(Long id);
}</code></pre>
<pre><code class="language-java">public interface PostRepository {
    Post save(Post post);
    Optional&lt;Post&gt; findById(Long id);
    List&lt;Post&gt; findAll();
    void update(Post post);
    void delete(Post post);
}</code></pre>
<p>이번 글에서 repository는 테스트하지 않는다. Layered 아키텍처에서 repository 계층은 보통 외부 의존성을 갖지 않는 객체이기도 하고, 보통 repository 계층은 단위 테스트를 작성하기보다는 통합 테스트를 작성하여 실제로 DB에 값을 잘 저장하는지, 값을 잘 읽어오는지를 테스트하기 때문이다.</p>
<p>다만, service 계층에서 DB 접근 로직을 사용해야 하기 때문에 필요한 기능들을 interface에 정의만 해두었다.</p>
<h2 id="service">Service</h2>
<pre><code class="language-java">@RequiredArgsConstructor
@Service
public class PostService {

    private final UserRepository userRepository;
    private final PostRepository postRepository;

    @Transactional
    public Post upload(UploadPostCommand command) {
        if (!userRepository.existsById(uploadPostCommand.writerId())) {
            throw new IllegalArgumentException(&quot;유저 정보가 존재하지 않습니다.&quot;);
        }
        return postRepository.save(Post.withoutId(
            command.writerId(),
            command.title(),
            command.content()
        ));
    }

    @Transactional(readOnly = true)
    public Post getById(Long id) {
        return postRepository.findById(id).orElseThrow();
    }

    @Transactional(readOnly = true)
    public List&lt;Post&gt; findAll() {
        return postRepository.findAll();
    }

    @Transactional
    public void update(UpdatePostCommand command) {
        Post post = getById(command.postId());
        post.verifyUpdateOrDeletePermission(command.requestUserId());
        post.updateTitle(command.title());
        post.updateContent(command.content());
        postRepository.update(post);
    }

    @Transactional
    public void delete(DeletePostCommand command) {
        Post post = getById(command.postId());
        post.verifyUpdateOrDeletePermission(command.requestUserId());
        postRepository.delete(post);
    }
}
</code></pre>
<h2 id="controller">Controller</h2>
<pre><code class="language-java">@RequiredArgsConstructor
@RequestMapping(&quot;/api/posts&quot;)
@RestController
public class PostController {

    private final PostService postService;

    @PostMapping
    public ResponseEntity&lt;UploadPostResponse&gt; uploadPost(
        @RequestBody @Valid UploadPostRequest request
    ) {
        Post post = postService.upload(new UploadPostCommand(
            request.writerId(),
            request.title(),
            request.content()
        ));
        return ResponseEntity
            .created(URI.create(&quot;/api/posts/&quot; + post.getId()))
            .body(UploadPostResponse.from(post));
    }

    @GetMapping(&quot;/{postId}&quot;)
    public PostInfoResponse getPost(@PathVariable Long postId) {
        Post post = postService.getById(postId);
        return PostInfoResponse.from(post);
    }

    @GetMapping
    public List&lt;PostInfoResponse&gt; findAllPosts() {
        return postService.findAll().stream()
            .map(PostInfoResponse::from)
            .toList();
    }

    @PatchMapping(&quot;/{postId}&quot;)
    public ResponseEntity&lt;Void&gt; updatePost(
        @PathVariable Long postId,
        @RequestParam Long requestUserId,
        @RequestBody UpdatePostRequest request
    ) {
        postService.update(new UpdatePostCommand(
            requestUserId,
            postId,
            request.title(),
            request.content()
        ));
        return ResponseEntity.noContent().build();
    }

    @DeleteMapping(&quot;/{postId}&quot;)
    public ResponseEntity&lt;Void&gt; deletePost(
        @PathVariable Long postId,
        @RequestParam Long requestUserId
    ) {
        postService.delete(new DeletePostCommand(requestUserId, postId));
        return ResponseEntity.noContent().build();
    }
}</code></pre>
<h1 id="테스트-코드-작성하기">테스트 코드 작성하기</h1>
<p>이제 위에서 구현된 기능들에 대한 단위 테스트 코드를 작성해보자.</p>
<h2 id="given---when--then">Given - When -Then</h2>
<p>본문에서 작성하는 모든 테스트 코드는 given-when-then 구조로 작성할 것이다.</p>
<p>각각의 단계가 의미하는 바는 다음과 같다.</p>
<ul>
<li><strong>given</strong>: 테스트 시나리오를 수행하기 위해 필요한 것들을 정의하는 단계이다. 테스트의 전제 조건을 설정하는 부분.</li>
<li><strong>when</strong>: 테스트하려는 기능이 동작하는 단계이다.</li>
<li><strong>then</strong>: 동작한 기능의 결과를 검증하는 단계이다.</li>
</ul>
<p>Given-when-then 구조로 테스트를 작성하게 되면 테스트의 가독성과 명확성을 높이고, 유지보수하기 용이하다는 이점이 있다. 각 단계가 명확한 역할을 가지고 있기 때문에, 테스트가 무엇을 검증하려고 하는지 쉽게 이해할 수 있다.</p>
<h2 id="service-layer-단위-테스트-작성">Service Layer 단위 테스트 작성</h2>
<p>우선 <code>PostService</code>에 대한 테스트 코드를 먼저 작성해보자. <code>PostServiceTest</code>라는 class를 생성한 후, 다음과 같이 작성한다.</p>
<pre><code class="language-java">import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import example.unit_test.domain.post.repository.PostRepository;
import example.unit_test.domain.user.repository.UserRepository;

@ExtendWith(MockitoExtension.class)
class PostServiceTest {

    @InjectMocks
    private PostService sut;

    @Mock
    private UserRepository userRepository;
    @Mock
    private PostRepository postRepository;

    // ...
}</code></pre>
<ul>
<li><code>@ExtendWith(MockitoExtension.class)</code>: Mockito 라이브러리를 사용하기 위해 관련 설정들을 로드한다. JUnit4의 <code>MockitoJUnitRunner</code>와 동일한 역할을 한다.</li>
<li><code>@InjectMocks</code>: 특정 필드를 가짜 객체(여기서는 mock 객체)를 주입받을 대상으로 지정한다. 가짜 객체는 constructor injection, property injection, setter injection을 통해 주입받을 수 있다.</li>
<li><code>@Mock</code>: 특정 필드를 mock 객체로 지정한다.</li>
</ul>
<p>이제 mock 객체를 사용하여 외부 의존성들이 우리의 의도대로 동작하도록 행위를 정의(mocking)할 것이다.</p>
<p>Mock 객체와 mocking에 대해 잘 모른다면 <strong>이전글</strong>을 읽어보길 권한다.</p>
<h3 id="전체-게시글-목록-조회">전체 게시글 목록 조회</h3>
<pre><code class="language-java">import static org.assertj.core.api.Assertions.assertThatIterable;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;

@Test
void 전체_게시글_목록을_조회한다() {
    // given
    final Long WRITER_ID = 10L;
    List&lt;Post&gt; expectedResult = List.of(
        Post.withId(1L, WRITER_ID, &quot;Test1&quot;, &quot;This is...&quot;),
        Post.withId(2L, WRITER_ID, &quot;Test2&quot;, &quot;This is...&quot;)
    );
    given(postRepository.findAll())
        .willReturn(expectedResult);

    // when
    List&lt;Post&gt; actualResult = sut.findAll();

    // then
    then(postRepository).should().findAll();
    then(postRepository).shouldHaveNoMoreInteractions();
    then(userRepository).shouldHaveNoInteractions();
    assertThatIterable(actualResult).isEqualTo(expectedResult);
}</code></pre>
<p>전체 게시글 목록 조회(<code>PostService.findAll()</code>)의 테스트 코드이다. </p>
<p>Given에서 예상 결과와 mock 객체의 행동에 대한 결과를 정의하고, when에서 테스트하고자 하는 기능을 실행한 후, 마지막으로 테스트 결과를 검증한다.</p>
<ul>
<li><code>BDDMockito.given(METHOD_CALL)</code>: Mock 객체의 특정 메서드 호출을 인자로 받으며, 인자로 전달받은 메서드에 대해 stubbing(행위에 대한 결과를 설정하는 것)할 수 있도록 활성화한다. 이후 <code>willReturn(VALUE)</code>을 사용하여 행위에 대한 결과를 설정할 수 있다. 그렇게 한다면, 우리가 stubbing 한 행위가 동작할 때는 언제나 설정된 결과가 도출된다.</li>
<li><code>BDDMockito.then(MOCK_OBJ)</code>: 인자로 mock 객체를 전달받으며 <code>should()</code>, <code>shouldXxx()</code>와 함께 쓰이며 mock 객체에 대한 메서드 호출 여부를 검증한다.<ul>
<li><code>should()</code>: 특정 메서드가 실제로 호출되었는지 검증한다.</li>
<li><code>shouldHaveNoMoreInteractions()</code>: Mock 객체에 대해 <code>BDDMockito.given(...)</code>에 의해 정의된 메서드 호출 외에, 추가적인 메서드 호출이 일어나지 않았음을 검증한다.</li>
<li><code>shouldHaveNoInteractions()</code>: Mock 객체에 대해 메서드 호출이 전혀 발생하지 않았음을 검증한다.</li>
</ul>
</li>
</ul>
<p>Mock 객체의 행위를 설정하고 테스트 결과를 검증하는 데 <code>BDDMockito</code>의 기능들을 사용했다. Mockito 라이브러리 내에 존재하는 <code>Mockito.verify()</code>, <code>Mockito.doReturn()</code>, <code>Mockito.when()</code> 등의 기능을 사용해도 동일한 작업을 수행할 수 있다. 다만, 나는 given-when-then의 의미가 메서드 이름에서 드러나는 <code>BDDMockito</code>의 사용을 선호한다.</p>
<h3 id="게시글-단건-조회">게시글 단건 조회</h3>
<p>게시글 단건 조회 기능에 대한 테스트 코드를 작성하기 전에 구현 코드를 다시 살펴보자.</p>
<pre><code class="language-java">@Transactional(readOnly = true)
public Post getById(Long id) {
    return postRepository.findById(id).orElseThrow();
}</code></pre>
<p><code>getById()</code>에는 두 가지 테스트 시나리오가 존재해야 한다. 첫 번째는 DB에서 id와 일치하는 게시글 정보를 정상적으로 조회했을 때이고, 두 번째는 일치하는 게시글 정보를 찾지 못한 경우(주로 id 값이 잘못된 경우)이다.</p>
<p>이 두 케이스에 대한 테스트 코드를 모두 작성해보자.</p>
<pre><code class="language-java">@Test
void 주어진_id로_게시글을_단건_조회한다() {
    // given
    final Long POST_ID = 1L;
    Post expectedResult = Post.withId(POST_ID, 2L, &quot;Test&quot;, &quot;Contents...&quot;);
    given(postRepository.findById(POST_ID))
        .willReturn(Optional.of(expectedResult));

    // when
    Post actualResult = sut.getById(POST_ID);

    // then
    then(postRepository).should().findById(POST_ID);
    then(postRepository).shouldHaveNoMoreInteractions();
    then(userRepository).shouldHaveNoInteractions();
    assertThat(actualResult).isEqualTo(expectedResult);
}

@Test
void 주어진_id로_게시글을_단건_조회한다_만약_존재하지_않는_id라면_예외가_발생한다() {
    // given
    final Long POST_ID = 1L;
    given(postRepository.findById(POST_ID))
        .willReturn(Optional.empty());

    // when
    Throwable ex = catchThrowable(() -&gt; sut.getById(POST_ID));

    // then
    then(postRepository).should().findById(POST_ID);
    then(postRepository).shouldHaveNoMoreInteractions();
    then(userRepository).shouldHaveNoInteractions();
    assertThat(ex).isInstanceOf(NoSuchElementException.class);
}</code></pre>
<p><code>PostRepository.findById()</code>의 return type이 optional이기 때문에 조회된 데이터가 없는 경우 empty optional 객체가 반환된다. 이 경우, 예외가 발생하도록 비즈니스 로직을 구현했으므로 AssertJ의 <code>catchThrowable()</code> 메서드를 사용해 발생한 예외를 잡고, 예상한 예외가 맞는지 검증해줬다.</p>
<h3 id="신규-게시글-업로드">신규 게시글 업로드</h3>
<pre><code class="language-java">@Test
void 게시글을_업로드한다() {
    // given
    final Long WRITER_ID = 1L;
    final Long EXPECTED_POST_ID = 2L;
    final String TITLE = &quot;Test&quot;;
    final String CONTENT = &quot;This is test...&quot;;
    UploadPostCommand uploadPostCommand = new UploadPostCommand(WRITER_ID, TITLE, CONTENT);
    Post expectedResult = Post.withId(EXPECTED_POST_ID, WRITER_ID, TITLE, CONTENT);
    given(userRepository.existsById(uploadPostCommand.writerId()))
        .willReturn(true);
    given(postRepository.save(any(Post.class)))
        .willReturn(expectedResult);

    // when
    Post actualResult = sut.upload(uploadPostCommand);

    // then
    then(userRepository).should().existsById(uploadPostCommand.writerId());
    then(postRepository).should().save(any(Post.class));
    then(userRepository).shouldHaveNoMoreInteractions();
    then(postRepository).shouldHaveNoMoreInteractions();
    assertThat(actualResult).isEqualTo(expectedResult);
}

@Test
void 게시글을_업로드한다_만약_작성자_정보가_존재하지_않는다면_예외가_발생한다() {
    // given
    final Long WRITER_ID = 1L;
    UploadPostCommand uploadPostCommand = new UploadPostCommand(WRITER_ID, &quot;Test&quot;, &quot;Contents...&quot;);
    given(userRepository.existsById(WRITER_ID))
        .willReturn(false);

    // when
    Throwable ex = catchThrowable(() -&gt; sut.upload(uploadPostCommand));

    // then
    then(userRepository).should().existsById(WRITER_ID);
    then(userRepository).shouldHaveNoMoreInteractions();
    then(postRepository).shouldHaveNoInteractions();
    assertThat(ex).isInstanceOf(IllegalArgumentException.class);
}</code></pre>
<p>코드가 이전에 비해 조금 길긴 하지만 새롭게 추가된 내용은 많지 않다. <code>given()</code>을 통해 메서드 호출을 stubbing 하는 것에 있어 이번에는 <code>any()</code>라는 메서드를 사용하였다.</p>
<ul>
<li><code>ArgumentMatchers.any()</code>: <code>null</code>을 제외한 모든 객체를 일치(equal)한 것으로 판단하게끔 한다. 만약 메서드의 인자로 type이 주어진 경우, 해당 type의 객체에 대해서만 일치 한것으로 판단한다. </li>
</ul>
<p><code>given(userRepository.existsById(1L))</code>처럼 메서드 호출의 인자로 특정 값을 넣어줬을 때에는 실제로 stubbing 할 때와 동일한 값이 전달되어야 우리가 설정한 결과가 도출된다. 즉, <code>existsById()</code>의 인자에 <code>1L</code>을 넣어준 경우에만 우리가 설정한대로 동작한다.</p>
<pre><code class="language-java">public Post upload(UploadPostCommand uploadPostCommand) {
    if (!userRepository.existsById(uploadPostCommand.writerId())) {
        throw new IllegalArgumentException(&quot;유저 정보가 존재하지 않습니다.&quot;);
    }
    return postRepository.save(Post.withoutId(
        uploadPostCommand.writerId(),
        uploadPostCommand.title(),
        uploadPostCommand.content()
    ));
}</code></pre>
<p><code>upload()</code>의 코드를 보면, <code>postRepository.save()</code>에 전달할 객체를 <code>upload()</code> 메서드 내부에서 생성하고 있다. 그렇기에 고려해 볼 문제점은 두 가지가 있다.</p>
<ol>
<li><code>upload()</code> 메서드가 실행되는 시점에 생성되는 객체를 테스트 코드의 given 단계에서 정의하여 stubbing하는 것도 어색하다. 내용은 같더라도 분명 다른 객체(메모리 주소가 다르기 때문)인데 이것을 같다고 볼 수 있는가?</li>
<li>현재는 <code>Post</code> domain entity에 <code>@EqualsAndHashCode</code>를 사용하여 <code>equal()</code>과 <code>hashcode()</code>를 재정의했지만, 만약 그렇지 않았다면 <code>upload()</code>에서 생성된 <code>Post</code> 객체와 stubbing을 할 때 전달해준 <code>Post</code> 객체가 동일한지 확인하기 위해 메모리 주소를 비교할테고, 결국 일치하지 않아 매칭이 되지 않는다. 결국 우리가 stubbing한 결과가 도출되지 않을 것이다.</li>
</ol>
<p>이러한 이유들로 여기에서는 <code>any()</code>를 사용하여 특정 객체가 아닌, 특정 type의 객체라면 모두 일치하는 것으로 매칭하게끔 설정했다.</p>
<h3 id="게시글-수정">게시글 수정</h3>
<pre><code class="language-java">@Test
void 수정할_게시글_정보가_주어지고_게시글을_수정한다() {
    // given
    final Long WRITER_ID = 1L;
    final Long POST_ID = 2L;
    UpdatePostCommand updatePostCommand = new UpdatePostCommand(WRITER_ID, POST_ID, &quot;New title&quot;, &quot;New Contents...&quot;);
    Post oldPost = Post.withId(POST_ID, WRITER_ID, &quot;Old title&quot;, &quot;Old Contents...&quot;);
    given(postRepository.findById(POST_ID))
        .willReturn(Optional.of(oldPost));
    willDoNothing()
        .given(postRepository).update(any(Post.class));

    // when
    sut.update(updatePostCommand);

    // then
    then(postRepository).should().findById(POST_ID);
    then(postRepository).should().update(any(Post.class));
    then(postRepository).shouldHaveNoMoreInteractions();
    then(userRepository).shouldHaveNoInteractions();
}

@Test
void 게시글을_수정한다_만약_수정하려는_유저와_게시글_작성자가_다르다면_예외가_발생한다() {
    // given
    final Long WRITER_ID = 1L;
    final Long REQUEST_USER_ID = 2L;
    final Long POST_ID = 3L;
    UpdatePostCommand updatePostCommand = new UpdatePostCommand(REQUEST_USER_ID, POST_ID, &quot;New&quot;, &quot;New Content&quot;);
    Post oldPost = Post.withId(POST_ID, WRITER_ID, &quot;Old title&quot;, &quot;Old Contents...&quot;);
    given(postRepository.findById(POST_ID))
        .willReturn(Optional.of(oldPost));

    // when
    Throwable ex = catchThrowable(() -&gt; sut.update(updatePostCommand));

    // then
    then(postRepository).should().findById(POST_ID);
    then(postRepository).shouldHaveNoMoreInteractions();
    then(userRepository).shouldHaveNoInteractions();
    assertThat(ex).isInstanceOf(IllegalArgumentException.class);
}</code></pre>
<p>게시글 수정 기능을 검증하는 테스트 코드이다. 만약 stubbing 하려는 메서드 호출에 대해, 결과(반환 값)가 없다면 <code>willReturn()</code>이 아닌 <code>willDoNothing()</code>을 사용하면 된다.</p>
<h3 id="게시글-삭제">게시글 삭제</h3>
<pre><code class="language-java">@Test
void 게시글을_삭제한다() {
    // given
    final Long WRITER_ID = 1L;
    final Long POST_ID = 2L;
    DeletePostCommand deletePostCommand = new DeletePostCommand(WRITER_ID, POST_ID);
    Post post = Post.withId(POST_ID, WRITER_ID, &quot;Title&quot;, &quot;Contents...&quot;);
    given(postRepository.findById(POST_ID))
        .willReturn(Optional.of(post));
    willDoNothing()
        .given(postRepository).delete(post);

    // when
    sut.delete(deletePostCommand);

    // then
    then(postRepository).should().findById(POST_ID);
    then(postRepository).should().delete(post);
    then(postRepository).shouldHaveNoMoreInteractions();
    then(userRepository).shouldHaveNoInteractions();
}

@Test
void 게시글을_삭제한다_만약_삭제하려는_유저와_게시글_작성자가_다르다면_예외가_발생한다() {
    // given
    final Long WRITER_ID = 1L;
    final Long REQUEST_USER_ID = 2L;
    final Long POST_ID = 3L;
    DeletePostCommand deletePostCommand = new DeletePostCommand(REQUEST_USER_ID, POST_ID);
    Post post = Post.withId(POST_ID, WRITER_ID, &quot;Title&quot;, &quot;Contents...&quot;);
    given(postRepository.findById(POST_ID))
        .willReturn(Optional.of(post));

    // when
    Throwable ex = catchThrowable(() -&gt; sut.delete(deletePostCommand));

    // then
    then(postRepository).should().findById(POST_ID);
    then(postRepository).shouldHaveNoMoreInteractions();
    then(userRepository).shouldHaveNoInteractions();
    assertThat(ex).isInstanceOf(IllegalArgumentException.class);
}</code></pre>
<p>이렇게 service layer의 비즈니스 로직에 대한 단위 테스트를 작성하는 방법에 대해 살펴봤다.</p>
<p>이제 controller layer(API) 호출에 대한 단위 테스트 코드를 작성하고 기능을 검증해보자.</p>
<h2 id="controller-layer-단위-테스트-작성">Controller Layer 단위 테스트 작성</h2>
<pre><code class="language-java">import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

import com.fasterxml.jackson.databind.ObjectMapper;

@WebMvcTest(controllers = PostController.class)
class PostControllerTest {

    @MockBean
    private PostService postService;

    private final MockMvc mvc;
    private final ObjectMapper mapper;

    @Autowired
    public PostControllerTest(MockMvc mvc, ObjectMapper mapper) {
        this.mvc = mvc;
        this.mapper = mapper;
    }

    // ...
}</code></pre>
<p>Controller 테스트를 위해 테스트 클래스에 <code>@WebMvcTest</code>를 선언했다. <code>@WebMvcTest</code>를 사용하면 <code>@SpringBootTest</code>를 사용하는 것과는 다르게 MVC 테스트와 관련된 구성 요소들만 로드한다(<code>@Controller</code>, <code>@ControllerAdvice</code>, <code>@JsonComponent</code> 등). 즉, MVC 테스트와 관련 없는 component들은 Spring Bean으로 등록하지 않는다.</p>
<p>여기에서는 <code>@WebMvcTest</code>의 <code>controllers</code> 속성에 테스트 대상 정보를 넘겨주었는데, 이렇게 하면 다른 불필요한 controller조차 bean으로 등록하지 않고, 테스트 대상만을 Spring bean으로 등록하게 된다. 이렇게 하면 불필요한 component들을 bean으로 등록하지 않아도 되니 테스트 구동 시간이 빨라진다.</p>
<p>다음은 <code>WebMvcTest</code>의 실제 코드 중 일부이다.</p>
<pre><code class="language-java">@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(WebMvcTestContextBootstrapper.class)
@ExtendWith({SpringExtension.class})
@OverrideAutoConfiguration(
    enabled = false
)
@TypeExcludeFilters({WebMvcTypeExcludeFilter.class})
@AutoConfigureCache
@AutoConfigureWebMvc
@AutoConfigureMockMvc
@ImportAutoConfiguration
public @interface WebMvcTest {
    // ...
}</code></pre>
<p><code>@WebMvcTest</code>가 붙은 테스트에서는 Spring Security(있는 경우)와 <code>MockMvc</code>도 자동으로 구성한다. <code>@WebMvcTest</code>는 MVC 테스트 환경만을 로드하기 때문에 controller에서 필요한 의존성이 있다면 <code>@Import</code> 또는 <code>@MockBean</code>과 함께 사용해야 한다.</p>
<p>여기서 알아두어야 할 개념은 <code>MockMvc</code>와 <code>@MockBean</code>이다.</p>
<ul>
<li><code>MockMvc</code>: Web layer의 단위 테스트를 작성하기 위해 만들어진 class로서, API 요청을 보내듯이 controller의 메서드를 호출할 수 있다.</li>
<li><code>@MockBean</code>: Spring <code>ApplicationContext</code>에 mock 객체를 추가하기 위해 사용한다. 즉, mock 객체를 Spring bean으로 등록한다. 이렇게 하면 controller에서는 Spring bean에 등록된 mock 객체를 의존성 주입받아 사용하게 된다.</li>
</ul>
<h3 id="전체-게시글-목록-조회-api">전체 게시글 목록 조회 API</h3>
<pre><code class="language-java">import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@Test
void 전체_게시글_목록을_조회한다() throws Exception {
    // given
    final Long WRITER_ID = 10L;
    List&lt;Post&gt; expectedResult = List.of(
        Post.withId(1L, WRITER_ID, &quot;Test1&quot;, &quot;This is...&quot;),
        Post.withId(2L, WRITER_ID, &quot;Test2&quot;, &quot;This is...&quot;)
    );
    given(postService.findAll())
        .willReturn(expectedResult);

    // when &amp; then
    mvc.perform(get(&quot;/api/posts&quot;))
        .andExpect(status().isOk())
        .andExpect(jsonPath(&quot;$.size()&quot;).value(expectedResult.size()));
    then(postService).should().findAll();
    then(postService).shouldHaveNoMoreInteractions();
}</code></pre>
<p>기본적으로 mock 객체에 대해 행위와 그 결과를 정의하는 방법은 service layer를 테스트할 때와 동일하다.</p>
<p><code>MockMvc.perform()</code>을 통해 controller의 method를 호출할 수 있으며, 결과에 대한 검증도 가능하다. <code>MockMvc.perform()</code>의 인자로는 <code>web.servlet.RequestBuilder</code>의 하위 interface를 구현한 <code>MockMvcRequestBuilders</code>을 넣어주면 된다. <code>MockMvc.RequestBuilders</code>에는 <code>get()</code>, <code>post()</code>, <code>put()</code>, <code>patc()</code>, <code>delete()</code> 등 API 요청을 정의하기 위한 메서드들이 존재한다.</p>
<p>API 호출 결과를 검증하는 방법은 간단하다. <code>MockMvcResultMatchers.status()</code>를 사용하여 status code를 검증할 수 있고, <code>MockMvcResultMatchers.jsonPath</code>를 사용하여 response body의 내용을 검증할 수 있다.</p>
<p>이후에는 service layer의 테스트에서 했던 것과 동일하게, <code>then()</code>을 사용하여 우리가 의도한대로 함수가 동작했는지, 부수적인 함수 호출은 없었는지를 검증하면 된다.</p>
<h3 id="게시글-단건-조회-api">게시글 단건 조회 API</h3>
<pre><code class="language-java">@Test
void 주어진_id로_게시글을_단건_조회한다() throws Exception {
    // given
    final Long POST_ID = 1L;
    Post expectedResult = Post.withId(POST_ID, 2L, &quot;Test&quot;, &quot;Contents..&quot;);
    given(postService.getById(POST_ID))
        .willReturn(expectedResult);

    // when &amp; then
    mvc.perform(
            get(&quot;/api/posts/{postId}&quot;, POST_ID)
        ).andExpect(status().isOk())
        .andExpect(jsonPath(&quot;$.postId&quot;).value(expectedResult.getId()));
    then(postService).should().getById(POST_ID);
    then(postService).shouldHaveNoMoreInteractions();
}</code></pre>
<h3 id="신규-게시글-업로드-api">신규 게시글 업로드 API</h3>
<pre><code class="language-java">@Test
void 신규_게시글을_업로드한다() throws Exception {
    // given
    final Long WRITER_ID = 1L;
    final Long EXPECTED_POST_ID = 2L;
    final String TITLE = &quot;New Post&quot;;
    final String CONTENT = &quot;Contents...&quot;;
    UploadPostRequest uploadPostRequest = new UploadPostRequest(WRITER_ID, TITLE, CONTENT);
    Post expectedResult = Post.withId(EXPECTED_POST_ID, WRITER_ID, TITLE, CONTENT);
    given(postService.upload(any(UploadPostCommand.class)))
        .willReturn(expectedResult);

    // when &amp; then
    mvc.perform(
            post(&quot;/api/posts&quot;)
                .contentType(MediaType.APPLICATION_JSON)
                .content(mapper.writeValueAsString(uploadPostRequest))
        ).andExpect(status().isCreated())
        .andExpect(jsonPath(&quot;$.postId&quot;).value(expectedResult.getId()));
    then(postService).should().upload(any(UploadPostCommand.class));
    then(postService).shouldHaveNoMoreInteractions();
}</code></pre>
<p>API 요청 시 포함할 request body 내용은 <code>MockHttpServletRequestBuilder.content()</code>를 사용하여 설정할 수 있는데, 함수의 인자로는 <code>byte[]</code> or <code>String</code> type만을 허용하기 때문에 request 객체를 문자열로 변환해야 한다. 이를 위해 <code>ObjectMapper</code>의 <code>writeValueAsString()</code> 메서드를 사용했다.</p>
<h3 id="게시글-수정-api">게시글 수정 API</h3>
<pre><code class="language-java">@Test
void 수정할_게시글_정보가_주어지고_게시글을_수정한다() throws Exception {
    // given
    final Long WRITER_ID = 1L;
    final Long POST_ID = 2L;
    UpdatePostRequest updatePostRequest = new UpdatePostRequest(&quot;Old title&quot;, &quot;Old contents&quot;);
    willDoNothing()
        .given(postService).update(any(UpdatePostCommand.class));

    // when &amp; then
    mvc.perform(
        patch(&quot;/api/posts/{postId}&quot;, POST_ID)
            .param(&quot;requestUserId&quot;, String.valueOf(WRITER_ID))
            .contentType(MediaType.APPLICATION_JSON)
            .content(mapper.writeValueAsString(updatePostRequest))
    ).andExpect(status().isNoContent());
    then(postService).should().update(any(UpdatePostCommand.class));
    then(postService).shouldHaveNoMoreInteractions();
}</code></pre>
<h3 id="게시글-삭제-api">게시글 삭제 API</h3>
<pre><code class="language-java">@Test
void 게시글을_삭제한다() throws Exception {
    // given
    final Long WRITER_ID = 1L;
    final Long POST_ID = 2L;
    willDoNothing()
        .given(postService).delete(any(DeletePostCommand.class));

    // when &amp; then
    mvc.perform(
        delete(&quot;/api/posts/{postId}&quot;, POST_ID)
            .param(&quot;requestUserId&quot;, String.valueOf(WRITER_ID))
    ).andExpect(status().isNoContent());
    then(postService).should().delete(any(DeletePostCommand.class));
    then(postService).shouldHaveNoMoreInteractions();
}</code></pre>
<h1 id="classical-approach-stub-객체로-테스트하기">Classical Approach: Stub 객체로 테스트하기</h1>
<p>지금까지 mock 라이브러리를 사용해서, mocking 방식으로 단위 테스트를 작성해보았다. 이번에는 외부 의존성 없이, 단위 테스트 작성에 필요한 test doubles를 직접 구현하여 테스트하는 방법에 대해 알아보자.</p>
<p>Stubbing은 객체의 입력에 대해 기대되는 출력을 사전에 정의하고 그 결과를 검증하는 방법이다. 이는 classical testing이라고도 불리며, <strong>mocking과는 달리 실제로 동작하는 객체를 직접 구현</strong>하여 의존성과 결합도를 최소화한다. <strong>Stub 객체</strong>는 테스트에서 필요한 호출에 대해 미리 준비된 답을 제공하는 객체로, 외부 시스템과의 의존성을 제거하여 테스트를 독립적으로 수행할 수 있게 한다.</p>
<p>Mocking이 객체의 행동을 검증하는 것에 중점을 둔다면, Stubbing은 상태를 검증하는 것에 중점을 둔다. 즉, 어떤 입력에 대해 개발자가 기대하는 결과가 일관되게 나오는지 검증하는 것이 핵심이다. 이 과정에서 외부 의존성 없이 필요한 객체를 직접 구현함으로써, 테스트가 외부 시스템과 테스트 대상의 세부 구현에 의존하지 않게 된다.</p>
<p>이제 앞에서 살펴봤던 게시글 기능을 Stubbing 방식으로 테스트해보자.</p>
<pre><code class="language-java">public class PostRepositoryStub implements PostRepository {
    private Map&lt;Long, Post&gt; postStore = new HashMap&lt;&gt;();
    private Long currentId = 1L;

    @Override
    public Post save(Post post) {
        Post savedPost = Post.withId(currentId++, post.getWriterId(), post.getTitle(), post.getContent());
        postStore.put(savedPost.getId(), savedPost);
        return savedPost;
    }

    @Override
    public Optional&lt;Post&gt; findById(Long id) {
        return Optional.ofNullable(postStore.get(id));
    }

    @Override
    public List&lt;Post&gt; findAll() {
        return new ArrayList&lt;&gt;(postStore.values());
    }

    @Override
    public void update(Post post) {
        postStore.put(post.getId(), post);
    }

    @Override
    public void delete(Post post) {
        postStore.remove(post.getId());
    }
}

public class UserRepositoryStub implements UserRepository {
    // UserRepository는 예제의 주요 관심사가 아니므로 간략히 구현하였음
    @Override
    public boolean existsById(Long id) {
        return true;
    }
}</code></pre>
<p>Stubbing test를 위해 <code>PostService</code>에서 사용할 두 개의 Stub 객체를 구현했다. 이제 이 Stub 객체를 사용하여 <code>PostService</code>의 게시글 업로드 기능을 테스트해보자.</p>
<pre><code class="language-java">@Test
void 게시글을_업로드한다() {
    // Given
    UserRepositoryStub userRepositoryStub = new UserRepositoryStub();
    PostRepositoryStub postRepositoryStub = new PostRepositoryStub();
    PostService postService = new PostService(userRepositoryStub, postRepositoryStub);

    UploadPostCommand command = new UploadPostCommand(1L, &quot;Test Title&quot;, &quot;Test Content&quot;);

    // When
    Post result = postService.upload(command);

    // Then
    assertNotNull(result.getId());
    assertEquals(&quot;Test Title&quot;, result.getTitle());
    assertEquals(&quot;Test Content&quot;, result.getContent());
}</code></pre>
<p>이처럼 Stub 객체를 사용하여 테스트를 작성하고, 기능을 검증할 수 있다.</p>
<p>Stubbing 방식의 테스트는 <strong>외부 의존성을 제거</strong>하고 테스트를 독립적으로 수행할 수 있다는 장점이 있다. 또한, <strong>비즈니스 로직의 세부 구현에 의존하지 않으며, 필요한 부분만 간단하게 구현</strong>하여 변화에 유연하게 대응할 수 있다. 이로 인해 Stubbing 방식은 테스트 코드의 유지보수성을 높이고, 효율적이고 빠른 테스트 환경을 제공하는데 매우 유용한 방법이다.</p>
<p>Mocking과 Stubbing 두 가지 방식 모두 장단점이 존재한다. Mocking은 객체 간 상호작용을 검증할 때 유리하지만, 테스트 대상의 세부 구현이 변경될 때 테스트 코드 역시 변경될 가능성이 크다. 반면에 Stubbing은 외부 의존성을 최소화하고, 테스트 대상의 세부 구현에 종속되지 않으므로 변화에 유연하고 빠르게 대응할 수 있다. 당연하게도 silver bullet은 없으며, 개발자는 상황에 맞게 팀의 테스트 룰을 정할 필요가 있다.</p>
<h1 id="참고-자료">참고 자료</h1>
<p><a href="https://martinfowler.com/bliki/GivenWhenThen.html">Martin Fowler - Given When Then</a>
<a href="https://spring.io/guides/gs/testing-web">Spring 공식 문서 - Testing the Web Layer</a>
<a href="https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTest.html">WebMvcTest 명세</a>
<a href="https://docs.spring.io/spring-framework/reference/testing/spring-mvc-test-framework.html">Spring 공식 문서 - MockMvc</a>
<a href="https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/mock/mockito/MockBean.html">MockBean 명세</a>
<a href="https://medium.com/daangn/%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%EC%9C%84%ED%95%9C-stub-%EA%B0%9D%EC%B2%B4-%ED%99%9C%EC%9A%A9%EB%B2%95-5c52a447dfb7">당근 테크 블로그 - 효율적인 테스트를 위한 Stub 객체 사용법</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[소프트웨어를 검증하는 방법: 단위 테스트(Unit Test)]]></title>
            <link>https://velog.io/@wo_ogie/%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4%EB%A5%BC-%EA%B2%80%EC%A6%9D%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8Unit-Test</link>
            <guid>https://velog.io/@wo_ogie/%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4%EB%A5%BC-%EA%B2%80%EC%A6%9D%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8Unit-Test</guid>
            <pubDate>Fri, 04 Oct 2024 06:06:00 GMT</pubDate>
            <description><![CDATA[<p>개발 공부를 하는 주변 지인들을 보면, 종종 테스트 코드를 작성하지 않는 경우를 발견하곤 합니다. 명확한 이유는 모르더라도 주변에서 테스트 코드를 작성해야 한다는 것 쯤은 들어본 경우가 대부분이지만, 테스트 코드를 작성하는 것이 막연하게만 느껴지기도 하고, 테스트 코드를 작성하고 관련 내용을 공부하는데 시간을 쓰기 아까워하는 경우가 많은 것 같습니다.</p>
<p>종종 주위 사람들에게서 자주 듣는 말들이 있습니다. &quot;단위 테스트를 작성하는 게 의미가 있나요?&quot;, &quot;차라리 통합 테스트를 작성하는게 기능이 정상 동작하는 것을 검증하는데 더욱 의미있지 않나요?&quot;와 같은 이야기를 듣습니다. 이는 단위 테스트위 목적과 장점을 이해하지 못한 상태에서 테스트 코드를 작성하려고 했기 때문이라고 생각합니다.</p>
<p>이 글을 통해 소프트웨어 테스트와 단위 테스트가 무엇인지, 왜 단위 테스트를 작성해야 하는지, 그리고 이를 통해 기능이 요구사항을 충족하고 있는지를 검증할 수 있는 방법에 대해 알아보고자 합니다.</p>
<p>저 역시 아직 어려움이 많지만, 테스트 코드 작성을 어려워했던 경험이 있기에 이런 경험을 살려 어떤 내용들을 알아야 할지 생각해보며 이야기해보고자 합니다.</p>
<blockquote>
<p>💁🏻‍♂️ 이 글에서 다루는 예제 코드는 Java, Spring Boot, JUnit 환경에서의 백엔드 프로젝트 코드를 다루고 있습니다. 다만, 코드 구현이 주요한 내용은 아니므로 다른 언어를 사용하시는 분들도 문제 없이 읽을 수 있습니다.</p>
</blockquote>
<h1 id="테스트의-중요성">테스트의 중요성</h1>
<p><img src="https://blog.kakaocdn.net/dn/tVKaI/btrE6yYuVjM/1YjXx52H7qt2ewtczqSKTK/img.png" alt="it_works"></p>
<p>개발을 하다보면 주변에서 쉽게 들을 수 있는 말이 있다. “테스트 코드를 작성하는 것은 중요하다”는 것이다. 그럼 왜 테스트 코드를 작성해야 하고, 어떻게 작성하는 것일까?</p>
<p>소프트웨어를 개발하다보면 논리적/기술적 오류로 또는 단순히 개발자의 실수로 문제가 종종 발생하곤 한다. 그 때문에 애플리케이션의 안정성을 확인하고자 어떤 형태로든 테스트를 진행하게 된다. (테스트 코드 작성, 실제로 프로그램 동작시켜 확인해보기 등)</p>
<p>구현된 기능에 대한 안정성을 확인하지 않고 개발 프로세스를 진행하게 되는 경우 버그, 오류의 발생과 요구사항의 변경에 대응하기 위해 많은 노력과 시간이 들어가게 된다. 만약 테스트 코드를 작성해두어 프로그램의 안정성을 확인한다면 현재 구현된, 또는 추후에 변경될 기능들에 대해서 지속적으로 안정성을 확인할 수 있다. 결과적으로 테스트 코드를 잘 작성해두면 안정성 있는 기능을 개발할 수 있으며, 오류와 요구사항 변경 등의 이슈에도 유연하게 대응할 수 있다.</p>
<p>그렇다면 테스트 코드는 어떻게 작성하는 것일까? 단위 테스트, E2E 테스트, 통합 테스트 등 많은 테스트 방법들이 있지만 본문에서는 단위 테스트를 작성보고, 테스트 설계에 대한 개발 방법론인 TDD(Test Driven Development)에 대해 알아보고자 한다.</p>
<h1 id="테스트-피라미드">테스트 피라미드</h1>
<p><img src="https://velog.velcdn.com/images/wo_ogie/post/a3450841-0536-4ab9-a016-67972ff83b98/image.png" alt=""></p>
<p>테스트 피라미드는 소프트웨어 테스트 전략의 대표적인 개념으로, 상단으로 올라갈수록 더 복잡하고 시간이 많이 걸리는 테스트를, 하단으로 내려갈수록 더 작고 빠르게 실행되는 테스트를 의미한다. 위 사진의 테스트 피라미드에는 다음과 같은 세 개의 계층이 나타나고 있다.</p>
<ol>
<li><p><strong>단위 테스트(Unit Test)</strong>: 단위 테스트는 작은 코드 단위(ex. 메서드의 개별적인 동작 등)를 테스트하는 것으로, 빠르고 간단하게 작성할 수 있어 빈번하게 사용하기와 자동화하기 용이하다. 개별적이고 작은 코드 단위를 테스트하는 것이므로 빠르게 피드백을 제공하여 코드 작성과 동시에 버그를 잡을 수 있다.</p>
</li>
<li><p><strong>통합 테스트(Integration Test)</strong>: 통합 테스트는 여러 모듈이나 시스템 간의 커뮤니케이션을 테스트하는 단계이다. 예를 들어 외부 시스템이나 DB, API와 통신하는 부분을 테스트하며, 그렇기에 단위테스트보다 무겁고 복잡하다. 여러 모듈이 결합되어 실제로 올바르게 동작하는지 확인하고자 할 때 유용하며, 개별 단위는 잘 동작하더라도 여러 단위들을 통합할 시 발생할 수 있는 문제를 발견할 수 있다.</p>
</li>
<li><p><strong>E2E 테스트(End-to-End Test)</strong>: E2E 테스트는 실제 사용자가 애플리케이션을 사용하는 흐름을 그대로 시뮬레이션 해보는 단계이다. 로그인부터 결제까지의 전 과정을 검증하는 등 전체 시스템이 제대로 동작하는지 확인하는 것이 목표이다. E2E 테스트는 전체 시스템을 테스트해보는 것이므로 가장 많은 리소스가 필요하고, 무겁기 때문에 모든 시나리오를 테스트하기 어려울 수 있다.</p>
</li>
</ol>
<p>테스트 피라미드는 소프트웨어를 개발함에 있어, 효율적이고 체계적인 테스트 전략을 제공하는 모델이다. 개발자는 단위 테스트, 통합 테스트, E2E 테스트를 적절히 사용한다면 더 나은 품질의 소프트웨어를 사용자에게 제공할 수 있을 것이다.</p>
<h1 id="단위-테스트">단위 테스트</h1>
<p>단위 테스트(Unit Test)는 전체 시스템 및 소프트웨어와는 별개로, 특정 구성 요소나 코드 집합 등 작은 단위(unit)를 테스트하는 방법이다. 단위 테스트의 주요 목적은 각 단위가 예상대로 잘 작동하는지 확인하는 것이다.</p>
<p>단위 테스트에서 무언가를 테스트 할 때는 테스트하고자 하는 대상이 온전하게 담당하고 있는, 자기 자신만의 역할을 검증하는 것에 중점을 둔다. 그렇기에 외부 의존성이나 테스트 대상이 아닌 기능들에 대해서는 배제하고 테스트를 진행하게 된다.</p>
<pre><code class="language-java">public interface PointHistoryRepository {
    // 특정 유저에 대한 point history 정보를 전부 조회한다
    List&lt;PointHistory&gt; findAllByUser(Long userId);
}</code></pre>
<pre><code class="language-java">@RequiredArgsConstructor
public class PointService {
    private final PointHistoryRepository pointHistoryRepository;

    public int getTotalPoint(Long userId) {
        return pointHistoryRepository
            .findAllByUser(userId).stream()
            .map(PointHistory::getPoint)
            .sum();
    }
}</code></pre>
<p>위 코드에서 <code>getTotalPoint</code>의 역할은 DB에서 포인트 내역을 전부 조회하여 총 몇 포인트가 쌓였는지 확인하는 것이다(포인트 사용은 하지 않는다고 가정). <code>getTotalPoint</code>의 기능은 &quot;DB에서 포인트 내역을 전부 조회하기&quot;와 &quot;포인트 내역들을 활용해 총 포인트가 몇인지 구하기&quot;로 나누어 볼 수 있다. <code>getTotalPoint</code>가 예상대로 잘 동작하려면 DB 연결에 성공해야 하고, DB에서 포인트 내역들을 정상 조회해야 하며, 조회된 포인트 내역들을 빠짐없이 전부 더해야 한다. 이러한 과정들이 모두 성공적으로 진행됐을 때 <code>getTotalPoint</code>는 &quot;잘 동작한다&quot;고 할 수 있을 것이다. </p>
<p>하지만 단위 테스트에서는 이 과정들을 모두 검증하려고 하지 않는다. 단위 테스트에서는 검증 대상인 <code>PointService</code>와 <code>getTotalPoint</code>를 하나의 단위로 보고 이 단위에 대한 정상 동작만을 검증한다. DB에서 데이터를 조회하는 역할을 부여받은 <code>PointHistoryRepository</code>가 잘 동작할 것이라고 가정하고, 포인트 내역들을 활용해 총 포인트가 몇인지 구하는 <code>getTotalPoint</code>만이 담당하고 있는 역할을 검증하는 것에 집중하는 것이다.</p>
<h2 id="왜-단위-테스트인가">왜 단위 테스트인가?</h2>
<p>그렇다면 많고 많은 테스트 방법 중 왜 단위 테스트를 작성해야 하는 것일까? 적은 테스트 코드로 더 많은 기능을 검증할 수 있는 통합 테스트나 E2E(End-to-End) 테스트를 작성하는 편이 효율적이고 좋지 않을까? 이제 단위 테스트의 이점에 대해 알아보자.</p>
<ol>
<li><p><strong>다른 테스트 방식보다 테스트 코드 작성에 걸리는 시간이 적다</strong>. 단위 테스트는 테스트 대상에 대한 로직 검증만을 하므로 테스트 코드 작성에 시간이 적게 걸린다. 그러므로 테스트 대상의 많은 시나리오를 테스트하며 높은 테스트 커버리지를 가져갈 수 있다. E2E 또는통합 테스트로 예상되는 모든 시나리오를 테스트한다고 상상해보면 정말 아찔하지 않은가?</p>
</li>
<li><p><strong>버그를 조기에 발견하고 수정할 수 있다.</strong> 작은 구성요소에 대해 다양한 시나리오를 테스트하게 되므로 각 단위에 대한 요구사항을 제대로 만족하는지 빠르게 검증할 수 있다. 이는 기능이 커질수록, 팀의 규모가 커질수록 큰 이점으로 다가온다. 예를 들어, 장바구니에 담아둔 제품들을 주문하는 기능을 제공한다고 가정해보자. 장바구니에는 여러 제품들이 담겨있을 것이며, 최종 결제 금액을 계산하기까지 많은 정책들이 존재할 것이다. 개발자들은 각자 담당한 기능을 개발하고, 하나로 결합함으로써 사용자들에게 완성된 하나의 주문 기능을 제공할 수 있다. 그런데 막상 테스트를 진행해보니 총 결제액이 예상보다 적게 책정되고 있다. 어느 정책에서 버그가 있는 것일까? 아니면 계산 로직이 잘못된 것일까? 이제 개발자들은 버그를 잡기 위해 많은 시간을 들여야 할 것이다. 만약 기능 개발과 함께 단위 테스트를 작성했다면 이러한 문제를 조기에 발견하고 조치할 수 있었을 것이다.</p>
</li>
<li><p><strong>코드 리팩토링 및 유지보수를 더욱 쉽게 만들어준다</strong>. 단위 테스트를 꼼꼼히 잘 수행했다면, 추후 코드를 리팩토링 했을 때 기능의 정상 동작 여부를 쉽게 검증할 수 있다. 수정된 코드가 작성된 테스트 케이스를 통과하는지, 그로 인해 기능이 요구사항을 충족시키고 있는지만 확인하면 되기 때문이다. 이러한 편리함은 리팩토링 하는 것을 쉽게 만들어주고, 리팩토링은 곧 코드 품질 향상으로 이어질 수 있다.</p>
</li>
<li><p><strong>잘 작성된 테스트 코드는 그 자체로 문서로 활용할 수 있다</strong>. 테스트 코드를 살펴보면 테스트 대상이 어떤 요구사항들을 갖고 있고, 어떤 역할을 수행해야 하는지 알 수 있다. 더욱이 단위 테스트의 경우, 각 기능에 대한 요구사항을 살펴볼 수 있어 테스트 코드 자체만으로도 다른 개발자들이 살펴볼 수 있는 문서의 역할을 할 수 있다. </p>
</li>
</ol>
<h2 id="test-doubles">Test Doubles</h2>
<h3 id="test-double은-무엇인가">Test Double은 무엇인가?</h3>
<p>단위 테스트에서는 테스트 대상 외의 기능들에 대해서는 전혀 관심을 갖지 않는다. 그렇기에 테스트하려는 대상 객체와 연관된 객체는 <strong>실제 객체를 사용하면 안 된다</strong>.
앞서 잠깐 살펴봤던 예제 코드를 다시 보자.</p>
<pre><code class="language-java">public interface PointHistoryRepository {
    // 특정 유저에 대한 point history 정보를 전부 조회한다
    List&lt;PointHistory&gt; findByUser(Long userId);
}</code></pre>
<pre><code class="language-java">@RequiredArgsConstructor
public class PointService {
    private final PointHistoryRepository pointHistoryRepository;

    public int getTotalPoint(Long userId) {
        return pointHistoryRepository
            .findByUser(userId).stream()
            .map(PointHistory::getPoint)
            .sum();
    }
}</code></pre>
<p>여기서 우리가 테스트하고자 하는 기능은 <code>PointService.getTotalPoint</code>이다. 그러나 해당 기능은 <code>PointHistoryRepository</code>의 <code>findByUser</code>를 사용하고 있기 때문에 테스트하고자 하는 기능의 결과가 <code>findByUser</code> 메서드의 결과에 영향을 받는다. </p>
<p>예를 들어, <code>findByUser</code>의 결과로 조회된 값이 없다(empty list)거나, 심지어는 DB 연결/통신에 실패할 수도 있을 것이다. 그러나 이런 케이스들은 <code>PointService.getTotalPoint</code> 비즈니스 로직의 관심 대상이 아니다. <code>PointService</code>는 DB에 실제로 데이터가 얼마나 들어있는지, DB에서 값을 잘 불러올 수 있는지는 궁금해하지 않는다. 단지 <code>PointHistoryRepository.findByUser</code>가 넘겨준 결과를 받아 총 포인트 액수를 잘 취합하는지만 검증하면 되는 것이다.</p>
<p>이처럼 <strong>단위 테스트에서는 테스트 대상이 아닌, 연관된 요소들을 실제 객체를 사용하여 검증해서는 안 된다</strong>. 그렇기에 <strong>실제 객체 대신 테스트 목적으로 사용되는 가상 객체를 주입</strong>해주게 되는데, 이러한 가상 객체들을 <strong>test doubles</strong>라고 한다.</p>
<h3 id="test-double의-종류">Test Double의 종류</h3>
<p>Test double은 크게 <strong>Dummy</strong>, <strong>Fake</strong>, <strong>Stub</strong>, <strong>Spy</strong>, <strong>Mock</strong>으로 나눌 수 있다.</p>
<p><strong>1. Dummy</strong>
<strong>테스트를 위해 객체를 전달하긴 하지만, 실제로 사용하지는 않는 것</strong>을 의미한다. 테스트 대상을 구성하기 위해 값을 채우는 용도로만 사용한다.</p>
<pre><code class="language-java">@RequiredArgsConstructor
public class PointService {
    private final PointRepository pointRepository;
    private final PointHistoryRepository pointHistoryRepository;

    public int getTotalPoint(Long userId) {
        return pointHistoryRepository
            .findByUser(userId).stream()
            .map(PointHistory::getPoint)
            .sum();
    }
}</code></pre>
<p>위처럼 <code>PointService</code>가 여러 기능들을 구현하기 위해 두 개의 의존성(<code>PointRepository</code>, <code>PointHistoryRepository</code>)을 받고 있다고 가정해보자. <code>PointService.getTotalPoint</code>를 테스트하기 위해서는 <code>PointHistoryRepository</code>만 있으면 된다. 그러나 <code>PointService</code> 객체를 생성하고 구성하려면 <code>PointRepository</code> 객체가 필요하다. 사용은 하지 않지만 테스트 대상의 구성을 위해 필요한 경우, 이럴 때 다음과 같은 dummy 객체를 넣어줄 수 있다.</p>
<pre><code class="language-java">public class PointRepositoryDummy implements PointRepository {

    @Override
    public void example() {
        // 아무런 기능도 하지 않음
    }
}</code></pre>
<p><strong>2. Fake</strong></p>
<p><strong>실제로 동작하는 구현 로직을 갖고 있으나, 실제 프로덕션에서 사용하기에는 적합하지 않은 객체</strong>이다. 주로 실제 객체를 단순화하여 구현한 형태가 된다.</p>
<pre><code class="language-java">public class PointHistoryRepositoryFake implements PointHistoryRepository {

    List&lt;PointHistory&gt; pointHistoryTable = new ArrayList&lt;&gt;();

    @Override
    public void save(PointHistory pointHistory) {
        pointHistoryTable.add(pointHistory);
    }

    @Override
       public List&lt;PointHistory&gt; findByUser(Long userId) {
        return pointHistoryTable.stream()
                    .filter(pointHistory -&gt; pointHistory.getUserId().equals(userId))
                    .toList();
    }
}</code></pre>
<p><strong>3. Stub</strong></p>
<p><strong>테스트에서 호출하는 요청에 대해 미리 준비된 결과를 제공하는 객체</strong>이다. 일반적으로 테스트 케이스에서 예상되는 특정 응답을 정의하기 위해 사용된다.</p>
<pre><code class="language-java">public class PointHistoryRepositoryStub implements PointHistoryRepository {

    @Override
       public List&lt;PointHistory&gt; findByUser(Long userId) {
        return List.of(
            new PointHistory(1L, userId, CHARGE, 500),
            new PointHistory(2L, userId, CHARGE, 1000)
        );
    }
}</code></pre>
<p>이 코드에서 <code>findByUser</code>는 전달받은 <code>userId</code>의 값과는 관계 없이 항상 일정한 결과를 반환한다. </p>
<p><strong>4. Spy</strong></p>
<p>Spy는 <strong>Stub의 역할을 수행하면서, 호출에 대한 정보를 기록하는 객체</strong>이다. 호출에 대한 정보는 &#39;호출이 이루어졌는지 여부(<code>boolean</code>)&#39;, &#39;호출이 몇 번 이루어졌는지(<code>int</code>)&#39; 등이 될 수 있다.</p>
<pre><code class="language-java">public class PointHistoryRepositoryStub implements PointHistoryRepository {

    @Getter
    private boolean findByUserMethodWasCalled = false;

    @Override
       public List&lt;PointHistory&gt; findByUser(Long userId) {
        this.findByUserMethodCallCount = true;
        return List.of(
            new PointHistory(1L, userId, CHARGE, 500),
            new PointHistory(2L, userId, CHARGE, 1000)
        );
    }
}</code></pre>
<p>위 코드의 <code>findByUser</code>에서는 메서드가 호출될 때, 임의의 테스트 데이터를 제공할 뿐만 아니라 자신이 호출되었는지 기록한다.</p>
<p>이 정보를 활용하면 테스트 대상의 세부 기능들이 원하는대로 호출되었는지 검증할 수 있다.</p>
<pre><code class="language-java">@Test
void findByUserTest() {
    // given
    PointService sut = new PointService(new PointHistoryRepositoryStub());
    long userId = 1L;

    // when
    int result = sut.getTotalPoint(userId);

    // then
    assertTrue(sut.getFindByUserMethodWasCalled());
}</code></pre>
<p><strong>5. Mock</strong></p>
<p>Mock은 <strong>호출에 대한 예상 결과를 정의하고, 정의된 내용에 따라 동작하는 객체</strong>이다. 또한 <strong>mock 객체는 수신된 호출을 기록하고 분석하기도 한다</strong>.</p>
<p>다음은 mock 객체를 사용한 단위 테스트 작성을 돕는 Mockito 라이브러리를 활용한 테스트 코드이다. 구체적인 작성법보다는 mock 객체를 사용하는 방법을 살펴보자.</p>
<pre><code class="language-java">@ExtendWith(MockitoExtension.class)
public class PointServiceTest {

    @Mock
    private PointHistoryRepository pointHistoryRepository;

    @Test
    void findByUserTest() {
        // given
        long userId = 1L;
        List&lt;PointHistory&gt; expectedResult = List.of(
            new PointHistory(1L, userId, CHARGE, 500),
            new PointHistory(2L, userId, CHARGE, 1000)
        );
        given(pointHistoryRepository.findByUserId(userId))
            .willReturn(expectedResult)

        // when
        int actualResult = sut.getTotalPoint(userId);

        // then
        then(pointHistoryRepository).sholud().findByUserId(userId);
        assertThat(actualResult).isEqualTo(500 + 1000);
    }
}</code></pre>
<p><code>PointService</code>가 필요로 하는 외부 의존성인 <code>PointHistoryRepository</code>를 mock 객체로 설정했다. 그 후 <code>given(...).willReturn()</code>으로 mock 객체가 호출되었을 때 제공해야 하는 결과를 정의했다. 이후 검증 단계에서는 <code>PointHistoryRepository.findByUserId</code>가 실제로 호출되었는지, 테스트 대상의 실행 결과가 예상과 일치하는지를 검증한다.</p>
<h1 id="정리">정리</h1>
<p>지금까지 소프트웨어 테스트의 중요성과 다양한 테스트 방법에 대해 살펴보았습니다. 특히, 그중 단위 테스트가 어떻게 소프트웨어의 개별 기능을 빠르고 정확하게 검증하는데 유용한지를 알아보았습니다. 단위 테스트는 초기에 문제를 발견하고 수정할 수 있는 효과적인 방법임을 알 수 있었습니다.</p>
<p><strong>참고 자료</strong>
<a href="https://www.headspin.io/blog/the-testing-pyramid-simplified-for-one-and-all">What is Testing Pyramid?</a>
<a href="https://medium.com/simform-engineering/importance-of-unit-testing-in-software-development-3f47ef326be1">Importance of Unit Testing in Software Development</a>
<a href="https://www.lambdatest.com/blog/unit-testing-frameworks/">17 Best Unit Testing Frameworks In 2024</a>
<a href="https://medium.com/@matiasglessi/mock-stub-spy-and-other-test-doubles-a1869265ac47">Mock, Stub, Spy and other Test Doubles</a>
<a href="https://tecoble.techcourse.co.kr/post/2020-09-19-what-is-test-double/">Test Double을 알아보자</a>
<a href="https://medium.com/daangn/%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%EC%9C%84%ED%95%9C-stub-%EA%B0%9D%EC%B2%B4-%ED%99%9C%EC%9A%A9%EB%B2%95-5c52a447dfb7">효율적인 테스트를 위한 Stub 객체 사용법</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[실용적인 RESTful API 설계에 대해]]></title>
            <link>https://velog.io/@wo_ogie/restful-api-design</link>
            <guid>https://velog.io/@wo_ogie/restful-api-design</guid>
            <pubDate>Wed, 19 Jul 2023 19:36:46 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>API 서버를 개발하다 보면 API를 설계함에 있어 고민이 드는 순간들이 있습니다. API URL은 어떻게 정의해야 하는지, 버전 관리는 어떻게 해야 하는지, 어떤 기능들을 제공해야 하는지, 응답은 어떤 구조로 설계해야 하는지 등을 예시로 들 수 있을 것 같네요. 이번 글에서는 이러한 고민을 할 때 도움이 될 만한 자료가 있어 소개하려고 합니다. 
내용이 짧지 않으므로 목차를 보고 궁금한 내용만 살펴보아도 괜찮으나, 내용이 좋으니 전체적으로 한 번 훑어보는 것을 추천합니다. 😊</p>
<blockquote>
<p>이 글은 기본적으로 <a href="https://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api">Best Practices for Designing a Pragmatic RESTful API</a>를 읽고 정리한 내용이나, 일부 내용이 수정되거나 다른 내용이 추가되었을 수 있습니다.</p>
</blockquote>
<br>

<h1 id="목차">목차</h1>
<ul>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#api%EC%9D%98-%EC%A3%BC%EC%9A%94-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD">API의 주요 요구사항</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#restful%ED%95%9C-url%EB%93%A4%EA%B3%BC-action%EB%93%A4%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%9D%BC">RESTful한 URL들과 Action들을 사용하라</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#%EC%96%B8%EC%A0%9C-%EC%96%B4%EB%94%94%EC%84%9C%EB%82%98-%ED%95%AD%EC%83%81-ssl%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%9D%BC-https">언제, 어디서나 항상 SSL을 사용하라 (HTTPS)</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#api-%EB%AC%B8%EC%84%9C%ED%99%94">API 문서화</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#versioning">Versioning</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#result-filtering-sorting-searching">Result filtering, sorting, searching</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#api%EC%97%90%EC%84%9C-%EB%B0%98%ED%99%98%EB%90%98%EB%8A%94-fields%EB%A5%BC-%EC%A0%9C%ED%95%9C%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95%EC%9D%84-%EC%A0%9C%EA%B3%B5%ED%95%98%EB%9D%BC">API에서 반환되는 fields를 제한하는 방법을 제공하라</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#%EC%83%9D%EC%84%B1-%EC%88%98%EC%A0%95-%EC%8B%9C%EC%97%90%EB%8A%94-%EB%B0%98%EB%93%9C%EC%8B%9C-resource-representation%EC%9D%84-%EB%B0%98%ED%99%98%ED%95%B4%EC%95%BC-%ED%95%9C%EB%8B%A4">생성, 수정 시에는 반드시 resource representation을 반환해야 한다</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#hateoas%EB%A5%BC-%EB%B0%98%EB%93%9C%EC%8B%9C-%EC%A0%81%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94%EA%B0%80">HATEOAS를 반드시 적용해야 하는가?</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#%EC%9D%91%EB%8B%B5-%ED%98%95%ED%83%9C%EB%8A%94-json%EB%A7%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%9D%BC">응답 형태는 JSON만 사용하라</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#field-name%EC%9C%BC%EB%A1%9C-%EB%8D%94-%EC%A0%81%ED%95%A9%ED%95%9C-%EA%B2%83%EC%9D%80-snake_case-vs-camelcase">Field name으로 더 적합한 것은? snake_case vs camelCase</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#%EA%B8%B0%EB%B3%B8%EC%A0%81%EC%9C%BC%EB%A1%9C-pretty-print%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B3%A0-gzip%EC%9D%84-%EC%A7%80%EC%9B%90%ED%95%98%EB%9D%BC">기본적으로 pretty print를 사용하고 gzip을 지원하라</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#dont-use-an-envelope-by-default-but-make-it-possible-when-needed">Don’t use an envelope by default, but make it possible when needed</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#json-encoded-post-put--patch-bodies">JSON encoded POST, PUT &amp; PATCH bodies</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#pagination">Pagination</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#%EA%B4%80%EB%A0%A8%EB%90%9C-resource-representations%EB%A5%BC-%EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C-%EB%B6%88%EB%9F%AC%EC%98%A4%EB%8A%94-%EB%B0%A9%EB%B2%95%EC%9D%84-%EC%A0%9C%EA%B3%B5%ED%95%98%EB%9D%BC">관련된 resource representations를 자동으로 불러오는 방법을 제공하라</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#http-method%EB%A5%BC-override-%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95%EC%9D%84-%EC%A0%9C%EA%B3%B5%ED%95%98%EB%9D%BC">HTTP method를 override 하는 방법을 제공하라</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#rate-limiting%EC%97%90-%EC%9C%A0%EC%9A%A9%ED%95%9C-response-headers%EB%A5%BC-%EC%A0%9C%EA%B3%B5%ED%95%98%EB%9D%BC">Rate limiting에 유용한 response headers를 제공하라</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#authenticaton">Authenticaton</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#caching">Caching</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#errors">Errors</a></li>
<li><a href="https://velog.io/@wo_ogie/restful-api-design#%EC%9E%90%EC%A3%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-http-status-codes">자주 사용하는 HTTP status codes</a></li>
</ul>
<br>

<h1 id="api의-주요-요구사항">API의 주요 요구사항</h1>
<p>API는 개발자를 위한 유저 인터페이스이다. 그러므로 쾌적한 유저 인터페이스를 만들기 위해 우리는 노력해야 한다.</p>
<p>이 글의 목표는 오늘날의 웹 애플리케이션을 위해 설계된 실용적인 API의 모범 사례를 설명하는 것이다. 그렇기 때문에 옳지 않다고 생각되는 학술적인 표준을 만족시키려고 하지 않을 것이다. </p>
<p>우선, 의사 결정 과정을 돕기 위해 API가 추구해야 하는 몇 가지 요구사항을 살펴보자.</p>
<ul>
<li>적절한 웹 표준을 사용해야 한다.</li>
<li>개발자에게 친숙해야 하며, 브라우저 주소창을 통해 탐색할 수 있어야 한다.</li>
<li>간단하고, 직관적이며 일관성을 유지함으로써 선택이 쉬울 뿐만 아니라 즐거워야 한다.</li>
<li>Enchant UI의 대부분을 구동할 수 있는 충분한 유연성을 제공해야 한다.</li>
<li>다른 요구사항과 균형을 유지하면서 효율적이어야 한다.</li>
</ul>
<p>API는 개발자의 UI이다. 그렇기 때문에 여느 UI와 마찬가지로 UX를 신중하게 고려하는 것이 중요하다.</p>
<br>

<h1 id="restful한-url들과-action들을-사용하라">RESTful한 URL들과 Action들을 사용하라</h1>
<p>REST의 핵심 원칙은 API를 논리적인 resources로 분리하는 것이다. 이러한 resource들은 특정 의미를 갖는 method(<code>GET</code>, <code>POST</code>, <code>PUT</code>, <code>PATCH</code>, <code>DELETE</code>)들을 사용하여 다뤄진다.</p>
<h3 id="그렇다면-무엇을-resource로-만들-수-있는가">그렇다면, 무엇을 resource로 만들 수 있는가?</h3>
<p>Resources는 동사가 아닌, API consumer의 관점에서 의미 있는 명사여야 한다. </p>
<p><strong>주의</strong>: 내부 모델이 resources에 명확히 대응될 수 있지만, 일대일 대응은 아니다. 여기서 핵심은 API에 관련 없는 구현 세부 정보를 유출하지 않는 것이다. API resources는 API consumer의 관점에서 이해할 수 있어야 한다.</p>
<p>일단 resource를 정의한 후에는 resource에 적용되는 action과 해당 action이 API에 매핑되는 방식을 식별해야 한다. RESTful 원칙은 다음과 같이 매핑된 HTTP methods를 사용하여 CRUD actions를 처리하는 전략들을 제공한다.</p>
<ul>
<li><code>GET /tickets</code> - 티켓 리스트 조회</li>
<li><code>GET /tickets/12</code> - 특정 티켓 조회</li>
<li><code>POST /tickets</code> - 새로운 티켓 생성</li>
<li><code>PUT /tickets/12</code> - 12번 티켓 수정</li>
<li><code>PATCH /tickets/12</code> - 12번 티켓을 부분적으로 수정</li>
<li><code>DELETE /tickets/12</code> - 12번 티켓 삭제</li>
</ul>
<p>REST의 장점은 존재하는 HTTP methods를 활용하여 단 하나의 <code>/tickets</code> endpoint에서 중요한 기능을 구현한다는 것이다. 이 때문에 지켜야 할 method naming convention이 없으며, URL 구조가 깔끔하고 명확하다.</p>
<h3 id="endpoint-name은-단수여야-하는가-복수여야-하는가">Endpoint name은 단수여야 하는가 복수여야 하는가?</h3>
<p>실용적인 방식은 URL format을 일관되게 유지하고 항상 복수형을 사용하는 것이다. 그렇게 한다면 이상한 복수형(person/people, goose/geese 등)을 처리할 필요가 없기 때문에 API consumer의 삶이 더 좋아지고 API provider가 구현하기 더 쉬워진다.</p>
<h3 id="but-relatations는-어떻게-처리하는가">But, relatations는 어떻게 처리하는가?</h3>
<p>만약 relation(관련 있는 resource)이 다른 resource 내에만 존재할 수 있는 경우, RESTful 원칙은 유용한 지침을 제공한다. 
예를 들어보자. <a href="https://www.enchant.com/">Enchant</a>의 ticket은 여러 개의 messages로 구성된다. 이러한 메시지들은 다음과 같이 <code>/tickets</code> endpoint에 논리적으로 매핑될 수 있다.</p>
<ul>
<li><code>GET /tickets/12/messages</code> - 12번 티켓에 대한 메시지 리스트 조회</li>
<li><code>GET /tickets/12/messages/5</code> - 12번 티켓의 5번 메시지 조회</li>
<li><code>POST /tickets/12/messages</code> - 12번 티켓에 새로운 메시지 생성</li>
<li><code>PUT /tickets/12/messages/5</code> - 12번 티켓의 5번 메시지 수정</li>
<li><code>PATCH /tickets/12/messages/5</code> - 12번 티켓의 5번 메시지를 부분적으로 수정</li>
<li><code>DELETE /tickets/12/messages/5</code> - 12번 티켓의 5번 메시지 삭제</li>
</ul>
<p><strong>대안 1</strong>: 만약 relation이 resource와 독립적으로 존재할 수 있는 경우, resource의 output representation 내에 relation에 대한 식별자를 포함하는 것이 합리적이다. 그러면 API consumer는 관련 resource의 endpoint에 도달함으로써 해당 자원을 활용할 수 있게 된다.</p>
<p><strong>대안 2</strong>: 만약 독립적으로 존재하는 relation이 일반적으로 resource와 함께 요청되는 경우, API는 relation의 representation을 자동으로 포함하고 API에 대한 두번째 호출(second hit)을 방지하는 기능을 제공할 수 있다. <em>Clean API and one hit to the server</em>.</p>
<h3 id="crud-operations의-개념에-딱-맞지-않는-action은-어떻게-해야-하는가">CRUD operations의 개념에 딱 맞지 않는 action은 어떻게 해야 하는가?</h3>
<p>애매한 상황이 될 수 있다. 이 경우, 다음과 같은 여러가지 접근 방식이 있다.</p>
<ol>
<li>Action이 resource의 필드처럼 나타나도록 재구성한다. 예를 들어, activated action이라는 데이터는 <code>boolean activated</code> 필드에 매핑될 수 있고 <code>PATCH</code>를 통해 resource에 업데이트 될 수 있다.</li>
<li>RESTful 원칙을 사용하여 해당 action을 sub-resource처럼 취급한다. 예를 들어, GitHub의 API를 사용하면 <code>PUT /gists/:id/star</code>로 <a href="http://developer.github.com/v3/gists/#star-a-gist">gist에 star 할 수 있고</a> <code>DELETE /gists/:id/star</code>로 <a href="http://developer.github.com/v3/gists/#unstar-a-gist">star를 해제할 수 있다</a>.</li>
<li>때로는 정말로 action을 합당한 RESTful 구조에 매핑할 방법이 없을 수도 있다. 예를 들어, multi-resource 검색은 특정 resource의 endpoint에 적용되는 것이 적합하지 않을 수 있다. 이 경우, resource가 아닐지라도 <code>/search</code>가 좋은 선택이 된다. 이상해 보일 수 있지만 이것은 괜찮다. API consumer의 관점에서 옳은 것이고, 명확히 혼란을 피하기 위해 문서화 했는지에만 주의하자.</li>
</ol>
<br>

<h1 id="언제-어디서나-항상-ssl을-사용하라-https">언제, 어디서나 항상 SSL을 사용하라 (HTTPS)</h1>
<p>항상 SSL을 사용해라. 예외는 없다. 오늘날, 웹 상에서의 API는 인터넷이 있는 모든 곳(도서관, 카페, 공항 등)에서 접속될 수 있다. 그리고 이들 모두가 보안적으로 안전하지는 않다. 많은 사람들이 통신을 전혀 암호화하지 않으므로, 자격 증명이 탈취될 경우 쉽게 도용되거나 위조될 수 있다.</p>
<p>주의해야 할 점은 API URL에 대한 non-SSL 접근이다. <strong>Non-SSL 접근을 SSL에 대응되는 항목으로 redirect 해서는 안된다</strong>. 이러한 경우에는 반드시 hard error를 발생시켜야 한다! 자동적으로 redirect가 설정되면 제대로 구현되지 않은(또는 문제가 있는) client가 자신도 모르게 암호화되지 않은 endpoint를 통해 request parameter들을 유출할 수 있다. Hard error는 이 실수를 조기에 발견하고 client가 적절하게 구성될 것을 보장한다.</p>
<br>

<h1 id="api-문서화">API 문서화</h1>
<p>API 문서는 쉽게 찾을 수 있고, 공개적으로 접근할 수 있어야 한다. 대부분의 개발자는 작업을 시작하기 전에 문서를 확인한다. 문서가 PDF 파일 안에 숨겨져 있거나 로그인이 필요한 경우 원하는 내용을 찾기 힘들 것이다.</p>
<p>문서에서는 전체 요청과 응답의 사이클이 설명되어야 한다. 요청은 브라우저에 붙여넣을 수 있는 예시여야 한다(브라우저에 붙여넣을 수 있는 링크나 터미널에 붙여넣을 수 있는 curl 등). 이에 대해 <a href="https://docs.github.com/ko/rest/gists?apiVersion=2022-11-28#list-gists">GitHub</a>와 <a href="https://stripe.com/docs/api">Stripe</a>에서 좋은 예시를 살펴볼 수 있다.</p>
<p>만약 public API를 배포한다면, 예고 없이 API를 중지하지 않겠다고 약속한 것이나 마찬가지다. 문서에는 반드시 deprecation schedule과 외부에서 볼 수 있는 API 업데이트 관련 세부 정보가 포함되어야 한다. 업데이트 사항들은 블로그(i.e. a changelog) 또는 이메일을 통해 전달되어야 한다.</p>
<br>

<h1 id="versioning">Versioning</h1>
<p>서버 개발자는 항상 API의 버전 관리를 해야 한다. 버전 관리를 하면 반복 작업을 더 빠르게 수행하고, 유효하지 않은 요청들이 새롭게 업데이트된 endpoints에 도달하는 것을 방지할 수 있다. 또한 일정 기간 동안은 이전 API 버전을 계속 제공할 수 있으므로 주요한 API 버전 전환을 원활하게 처리하는 데 도움이 된다.</p>
<p>API 버전을 URL에 담아야 할지, header에 담아야 할지에 대해서는 고민해 볼 필요가 있다. Header에 포함하는 것이 좋다는 의견도 있지만, 브라우저에서 여러 버전에 대한 resource를 탐색할 수 있도록 하고, 개발자의 경험을 보다 편하게 하기 위해서는 URL에 버전 정보가 포함되어야 한다. 다만 각각의 장단점이 뚜렷하므로 API를 배포하고 사용하는 환경에 따라 적절히 선택하는 것을 권장한다. 심지어는 두 방식 모두 선택할 수도 있다. 이에 대한 예시 중 하나로 <a href="https://stripe.com/docs/api/versioning">Stripe에서 API versioning을 하는 방법</a>을 살펴보는 것을 추천한다. URL에는 major version number(v1)가 존재하고, custom request header에는 날짜 기반의 sub-versions가 존재하는 형태이다. 이 경우 major version은 API 전체의 구조적 안정성을 제공하고, sub-versions는 비교적 작은 변경사항(field deprecations, endpoint changes 등)을 식별한다. </p>
<p>API가 항상 완벽하게 안정적으로 제공될 수는 없다. 변화는 피할 수 없다. 중요한 것은 변화를 관리하는 방법이다. 잘 문서화되어 수개월에 걸쳐 발표되는 deprecation schedules는 많은 API와 API consumer들이 충분히 받아들일만 할 것이다.</p>
<br>

<h1 id="result-filtering-sorting-searching">Result filtering, sorting, searching</h1>
<p>Base resource URL은 최대한 간결하게 유지하는 것이 좋다. 복잡한 필터링, 정렬 요구사항과 검색 기능은 모두 base URL의 위에 query parameter로 쉽게 구현될 수 있다. 좀 더 자세히 살펴보자.</p>
<h3 id="filtering">Filtering</h3>
<p>필터링을 구현하는 각 필드에 대해 unique query parameter를 사용한다. 예를 들어, <code>/tickets</code> endpoint에서 티켓 목록을 요청할 때, open 상태의 티켓들로 제한할 수 있다. 이는 <code>GET /tickets?state=open</code> 같은 요청으로 수행될 수 있다. 여기서 <code>state</code>는 필터룰 구현하는 query parameter이다.</p>
<h3 id="sorting">Sorting</h3>
<p>필터링과 마찬가지로 <code>sort</code>를 사용하여 정렬 방식을 설명할 수 있다. Sort parameter들을 쉼표로 구분하여 복잡한 정렬 요구사항을 표현할 수 있다. 이때, <strong>각 필드는 descending sort order를 의미하는 단항 음수 기호가 포함될 수 있다</strong>. 몇가지 예시를 살펴보자.</p>
<ul>
<li><code>GET /tickets?sort=-priority</code>: Priority의 내림차순 정렬 기준으로 티켓 리스트를 검색한다.</li>
<li><code>GET /tickets?sort=-priority,created_at</code>: Priority의 내림차순 정렬 기준으로 티켓 리스트를 검색한다. 같은 priority에서는 오래된 티켓이 앞 순서로 정렬된다.</li>
</ul>
<h3 id="searching">Searching</h3>
<p>때때로 기본 필터만으로는 충분하지 않고 텍스트 기반 검색 기능이 필요할 수도 있다. 이미 <a href="http://www.elasticsearch.org/">ElasticSearch</a>나 <a href="http://lucene.apache.org/">Lucene</a>와 같은 검색 기술을 사용하고 있을 수도 있다. 특정 유형의 resource를 검색하는 메커니즘으로 텍스트 검색 방식을 사용하는 경우, 이는 API에서 resource의 endpoint에 query parameter로 노출될 수 있다. 검색 쿼리들은 검색 엔진으로 곧바로 전달되어야 하며 API의 결과는 일반적인 결과와 동일한 형식이어야 한다.</p>
<p>이러한 것들을 결합하여 다음과 같은 쿼리들을 작성할 수 있다.</p>
<ul>
<li><code>GET /tickets?sort=-updated_at</code>: 최근 수정된 된 티켓들을 검색</li>
<li><code>GET /tickets?state=closed&amp;sort=-updated_at</code>: 최근 closed 된 티켓들 검색</li>
<li><code>GET /tickets?q=return&amp;state=open&amp;sort=-priority,created_at</code>: open 상태이며 ‘return’이라는 단어가 포함되고 priority가 높은 순서로 티켓들을 검색.</li>
</ul>
<h3 id="common-queries에-대한-aliases">Common queries에 대한 aliases</h3>
<p>일반적인 consumer에게 더 즐거운 API 경험을 제공하려면 조건들을 쉽게 접근할 수 있는 RESTful한 경로로 패키징 하는 것을 고려해라. 예를 들어, 위에서 살펴본 ‘최근 closed된 티켓 검색’은 <code>GET /tickets/recently_closed</code>로 변경될 수 있다.</p>
<br>

<h1 id="api에서-반환되는-fields를-제한하는-방법을-제공하라">API에서 반환되는 fields를 제한하는 방법을 제공하라</h1>
<p>API consumer가 항상 resource의 전체 표현을 필요로 하는 것은 아니다. 반환된 필드들을 선택하는 기능은 API consumer가 네트워크 트래픽을 최소화하고 API 사용 속도를 높이는 데 큰 도움이 된다.</p>
<p>쉼표로 구분된, 포함할 필드들의 목록이라는 의미를 갖는 <code>fields</code> query parameter를 사용해라. 예를 들어, <code>GET /tickets?fields=id,subject,updated_at&amp;state=open&amp;sort=-updated_at</code>는 open 상태인 티켓들의 정렬된 목록을 표시하기에 필요한 정보만을 검색한다.</p>
<blockquote>
<p>참고로 이 접근 방식은 <a href="https://velog.io/@wo_ogie/%EC%8B%A4%EC%9A%A9%EC%A0%81%EC%9D%B8-RESTful-API-Design%EC%97%90-%EB%8C%80%ED%95%B4#%EA%B4%80%EB%A0%A8%EB%90%9C-resource-representations%EB%A5%BC-%EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C-%EB%B6%88%EB%9F%AC%EC%98%A4%EB%8A%94-%EB%B0%A9%EB%B2%95%EC%9D%84-%EC%A0%9C%EA%B3%B5%ED%95%98%EB%9D%BC">관련된 resource representations를 자동으로 불러오는 방법을 제공하라</a>와 결합될 수 있다.
ex) <code>GET /tickets?embed=customer&amp;fields=id,customer.id,customer.name</code></p>
</blockquote>
<br>

<h1 id="생성-수정-시에는-반드시-resource-representation을-반환해야-한다">생성, 수정 시에는 반드시 resource representation을 반환해야 한다</h1>
<p><code>PUT</code>, <code>POST</code> or <code>PATCH</code> 호출은 제공된 parameter의 일부가 아닌, resource의 여러 필드들을 수정할 수 있다 (ex. <code>created_at</code> or <code>updated_at</code> timestamps). 업데이트 된 정보를 확인하기 위해 API consumer가 API를 다시 호출하지 않도록 하려면, API가 응답의 일부로 수정된(또는 생성된) 정보를 반환하도록 해야 한다.</p>
<br>

<h1 id="hateoas를-반드시-적용해야-하는가">HATEOAS를 반드시 적용해야 하는가?</h1>
<blockquote>
<p>참고로 <a href="https://en.wikipedia.org/wiki/HATEOAS">HATEOAS</a>에 대해 잘 모른다면 <a href="https://www.youtube.com/watch?v=RP_f5dMoHFc&amp;t=905s">그런 REST API로 괜찮은가 - HATEOAS</a>에서 알기 쉽게 설명해주고 있으니 참고하시면 좋을 것 같습니다. 유명한 keyword이니 직접 검색해보아도 좋은 자료가 많이 있을 거에요.</p>
</blockquote>
<p>API consumer가 링크를 생성해야 하는지, 아니면 API를 통해 링크를 제공해야 하는지에 대해서는 의견이 분분하다. RESTful 설계 원칙에서는 HATEOAS에 대해 다음과 같이 명시하고 있다. HATEOAS란 endpoint 와의 상호작용이 out-of-band information을 기반으로 하지 않고 output representation과 함께 제공되는 metadata 내에서 정의되어야 한다고 대략적으로 명시하는 것이다.</p>
<p>Web은 일반적으로 웹 사이트의 첫 페이지로 이동하여 페이지에 표시되는 내용을 기반으로 링크를 따라가는 HATEOAS type principles에 따라 작동하지만, 아직은 API에서 HATEOAS를 적용할 준비가 되지 않은 것 같다. 웹 사이트를 이용할 때 어떤 링크를 클릭할지는 run time 시점에 결정된다. 하지만, API를 사용하면 어떤 요청을 보낼지에 대한 결정이 run time이 아닌, API 통합 코드가 작성될 때 결정된다. 이러한 결정을 run time으로 연기할 수 있을까? 물론, 그렇게 하면 코드가 중단되지 않고 중요한 API 변경 사항을 처리할 수 없기 때문에 얻을 수 있는 이점이 많지 않다. 즉, HATEOAS는 유망하지만 아직 prime time에서 사용하기에는 무리가 있다고 생각한다. HATEOAS의 잠재력을 완전히 실현하려면 이러한 원칙을 중심으로 표준과 tool을 정의하는 데 좀 더 많은 노력을 기울여야 할 것이다.</p>
<p>현재로서는 사용자가 문서에 접근할 수 있다고 가정하고 API consumer가 링크를 만들 때 사용할 resource identifier를 output representation에 포함시키는 것이 가장 좋다. Resource에 대한 identifier를 사용하면 network에서 주고받는 데이터가 최소화되고, API consumer가 저장하는 데이터도 최소화된다(identifier가 포함된 URL을 대신 매우 작은 identifier만 저장하면 되기 때문).</p>
<p>또한 앞서 살펴본 versioning 방식을 고려했을 때, API consumer는 URL이 아닌 resource identifier를 저장하는 것이 장기적으로 더 합리적일 것이다. 결국 resource를 식별하기 위한 identifier는 여러 버전에 걸쳐 안정적으로 사용할 수 있지만, 특정 URL은 그렇지 않다.</p>
<br>

<h1 id="응답-형태는-json만-사용하라">응답 형태는 JSON만 사용하라</h1>
<p>XML은 API에 적합하지 않다. XML은 장황하고, 파싱하기 어렵고, 읽기 어렵고, 데이터 모델이 대부분의 프로그래밍 언어에서의 데이터 모델과 호환되지 않으며, output representation의 주요 요구 사항이 internal representation에서 직렬화되는 경우 XML의 확장성이라는 이점이 무의미해진다. 이러한 것들 외에도 더 많은 단점들이 존재한다. 주목해야 할 점은 오늘날 여전히 XML을 지원하는 주요 API를 찾기 어려울 것이라는 점이다. 물론 당신도 그렇게 해서는 안된다.</p>
<p>만약 고객들이 많은 수의 기업 고객으로 구성된 경우, XML을 지원해야 할 수도 있다. 만약 이 작업을 수행해야 한다면 다음과 같은 새로운 질문에 직면하게 된다.</p>
<h3 id="media-type은-accept-header-또는-url을-기반으로-하여-변경해야-하는가">Media type은 Accept header 또는 URL을 기반으로 하여 변경해야 하는가?</h3>
<p>브라우저에서의 탐색 가능성을 보장하려면 URL에 있어야만 한다. 여기에서 가장 합리적인 선택지는 endpoint URL에 <code>.json</code> 또는 <code>.xml</code> 확장자를 추가하는 것이다.</p>
<p>이에 대한 예시로 <a href="https://developers.kakao.com/docs/latest/ko/local/dev-guide#search-by-keyword">Kakao REST API - 키워드로 장소 검색하기</a>를 살펴볼 수 있다. 해당 API는 응잡 데아터의 format으로 <code>JSON</code>과 <code>XML</code>을 모두 지원하고 있는데, <code>https://dapi.kakao.com/v2/local/search/keyword.${FORMAT}</code> URL의 <code>${FORMAT}</code> 값을 통해 어떠한 format으로 응답을 받을지 결정할 수 있다.</p>
<br>

<h1 id="field-name으로-더-적합한-것은-snake_case-vs-camelcase">Field name으로 더 적합한 것은? snake_case vs camelCase</h1>
<p>Primary representation format으로 JSON을 사용하는 경우, JavaScript naming conventions를 따르는 것이 “올바른” 방법이다. 즉, filed name으로 camelCase가 사용된다는 것을 의미한다. 그런 다음 다양한 languages로 client library를 구축하는 경로로 이동하는 경우 해당하는 관용적 naming convention을 사용하는 것이 가장 좋다. (camelCase for C# &amp; Java, snake_case for python &amp; ruby)</p>
<br>

<h1 id="기본적으로-pretty-print를-사용하고-gzip을-지원하라">기본적으로 pretty print를 사용하고 gzip을 지원하라</h1>
<p><strong>공백이 압축된 output</strong></p>
<pre><code class="language-json">{&quot;name&quot;:&quot;woogie&quot;,&quot;age&quot;:20,&quot;sex&quot;: &quot;male&quot;}</code></pre>
<p><strong>Pretty print</strong></p>
<pre><code class="language-json">{
  &quot;name&quot;:&quot;woogie&quot;,
  &quot;age&quot;:20,
  &quot;sex&quot;:&quot;male&quot;
}</code></pre>
<p>공백이 압축된 output을 제공하는 API는 browser에서 보기에 별로 재미있지 않다. Pretty(예쁜, 보기 편한) printing을 활성화하기 위해 query parameter(ex. <code>pretty=true</code>)를 제공할 수도 있지만, 기본적으로 pretty prints를 제공하는 API가 이용하기 더 쉽다. Pretty printing에 대한 추가적인 데이터 전송에 드는 비용은 무시할 수 있는 수준이며, 특히 gzip을 구현하지 않았을 때의 비용과 비교하면 더더욱 그렇다.</p>
<p>몇 가지 사례에 대해 생각해보자. API consumer가 디버깅 중일 때, 직접 작성한 코드에 의해 API에서 받은 데이터를 출력하도록 하면 기본적으로 쉽게 읽을 수 있다. 또는 consumer가 생성하는 URL을 가져와 browser에서 직접 입력하면 기본적으로 쉽게 읽을 수 있다. 이런 사소한 것들이 API를 사용하기 좋게 만든다.</p>
<h3 id="이러한-부가적인-데이터-전송을-하는-것이-정말-괜찮을까">이러한 부가적인 데이터 전송을 하는 것이 정말 괜찮을까?</h3>
<p>실제 사례를 살펴보며 생각해보자. 기본적으로 pretty print를 사용하는 <a href="https://api.github.com/users/veesahni">GitHub의 API</a>에서 일부 데이터를 살펴보고 gzip 비교도 함께 해보자.</p>
<pre><code class="language-bash">$ curl https://api.github.com/users/veesahni &gt; with-whitespace.txt
$ ruby -r json -e &#39;puts JSON.parse(STDIN.read)&#39; &lt; with-whitespace.txt &gt; without-whitespace.txt
$ gzip -c with-whitespace.txt &gt; with-whitespace.txt.gz
$ gzip -c without-whitespace.txt &gt; without-whitespace.txt.gz</code></pre>
<p>결과로 얻은 파일의 크기는 다음과 같다.</p>
<ul>
<li><code>without-whitespace.txt</code> - 1221 bytes</li>
<li><code>with-whitespace.txt</code> - 1290 bytes</li>
<li><code>without-whitespace.txt.gz</code> - 477 bytes</li>
<li><code>with-whitespace.txt.gz</code> - 480 bytes</li>
</ul>
<p>위 예시에서 공백은 gzip을 사용하지 않을 때 출력 크기를 5.7% 증가시켰고, gzip을 사용할 때는 0.6% 증가시켰다. 반면에 gzip을 사용하는 것만으로도 크기를 60% 이상 절약할 수 있었다. Pretty print에 드는 비용이 상대적으로 적기 때문에 기본적으로 pretty print를 사용하고 gzip 압축이 지원되는지 확인하는 것이 가장 좋은 선택이다!</p>
<br>

<h1 id="dont-use-an-envelope-by-default-but-make-it-possible-when-needed">Don’t use an envelope by default, but make it possible when needed</h1>
<p>많은 API는 다음과 같이 response를 envelope한다.</p>
<pre><code class="language-json">{
  &quot;data&quot; : {
    &quot;id&quot; : 123,
    &quot;name&quot; : &quot;John&quot;
  }
}</code></pre>
<p>이렇게 응답을 envelope 하는 데에는 그럴만한 몇 가지 이유가 있다. 추가적인 metadata 또는 pagination information을 쉽게 포함할 수 있고, 일부 REST client는 HTTP header에 쉽게 access 할 수 없으며 <a href="http://en.wikipedia.org/wiki/JSONP">JSONP</a> requests는 HTTP header에 access 할 수 없다. 그러나, <a href="http://www.w3.org/TR/cors/">CORS</a>와 <a href="http://tools.ietf.org/html/rfc5988#page-6">Link header from RFC 5988</a>과 같이 빠르게 채택되는 표준으로 인해 enveloping이 불필요해지기 시작했다.</p>
<p>기본적으로는 envelope를 사용하지 않고, 예외적인 case에만 enveloping을 사용함으로써 API의 미래(추후 적용 및 호환성)를 보장할 수 있다.</p>
<h3 id="예외적인-경우에-envelope는-어떻게-사용해야-하는가">예외적인 경우에 envelope는 어떻게 사용해야 하는가?</h3>
<p>Envelope가 실제로 필요한 두 가지 상황이 있다 - 이는 API가 JSONP를 통해 domain 간의 request를 지원해야 하거나 client가 HTTP header로 작업할 수 없는 경우이다.</p>
<ol>
<li><p>Cross-domain에서의 JSONP를 지원하려면: 이러한 requests에는 callback function의 이름을 나타내는 추가적인 query parameter(일반적으로 <code>callback</code> or <code>jsonp</code>로 명명된)가 함께 제공된다. 이 parameter가 있는 경우, API는 항상 200 HTTP status code로 응답하고 JSON payload에서 실제 status code를 전달하는 full envelope mode로 전환해야 한다. 응답와 함께 전달될 추가적인 HTTP headers는 다음과 같이 JSON fields에 mapping 되어야 한다.</p>
<pre><code class="language-jsx"> callback_function({
       status_code: 200,
       next_page: &quot;https://..&quot;,
       response: {
             ... actual JSON response body ... 
       }
 })</code></pre>
</li>
<li><p>제한된 HTTP clients를 지원하려면: JSONP callback function 없이 enveloping을 trigger하는 special query parameter <code>?envelope=true</code>를 허용한다.</p>
</li>
</ol>
<br>

<h1 id="json-encoded-post-put--patch-bodies">JSON encoded POST, PUT &amp; PATCH bodies</h1>
<p>API input에 대해 JSON의 고려 사항을 생각해보자.</p>
<p>많은 API가 API request body에서 URL encoding을 사용한다. URL encoding은 말 그대로 URL query parameter의 데이터를 encoding 하는 데 사용하는 것과 동일한 규칙을 사용하여 key-value 쌍이 encoding 된 request body이다. 이 방법은 간단하고 널리 지원되며 작업이 수행되도록 한다.</p>
<p>하지만 URL encoding에는 몇 가지 문제가 있다. 일단 data type에 대한 개념이 존재하지 않기 때문에 API는 문자열에서 정수와 boolean 값을 파싱해야 한다. 또한 계층 구조에 대한 실제 개념이 존재하지 않는다. Key-value쌍으로 일부 구조를 만들 수 있는 몇 가지 규칙(ex. 배열을 나타내기 위해 key에 []를 추가하는 등)이 있기는 하지만 이를 JSON의 기본 계층 구조와 비교할 수는 없다.</p>
<p>API가 간단하다면 URL encoding으로 충분할 수도 있다. 하지만, 출력 형식과 일치하지 않을 수도 있다.</p>
<p>JSON 기반 API의 경우, API input도 JSON을 고수해야 한다.</p>
<p>JSON으로 encoding 된 <code>POST</code>, <code>PUT</code>, <code>PATCH</code> 요청을 받는 API는 <code>Content-Type</code> header를 <code>application/json</code>으로 설정하거나 <code>415 Unsupported Media Type</code> HTTP status code를 던져야 한다. </p>
<br>

<h1 id="pagination">Pagination</h1>
<p>Envelope를 지원하는 API는 일반적으로 envelope 자체에 pagination data를 포함한다. 최근까지만 해도 더 나은 선택지가 많지 않았기 떄문이다. 하지만 오늘날 pagination 세부 정보를 포함하는 올바른 방법은 <a href="https://datatracker.ietf.org/doc/html/rfc8288">RFC 8288에서 도입한 <code>Link</code> header</a>를 사용하는 것이다.</p>
<p><code>Link</code> header를 사용하는 API는 미리 만들어진 링크 집합을 반환할 수 있으므로 API consumer는 링크를 직접 구성할 필요가 없다. 이는 pagination이 <a href="https://developers.facebook.com/docs/graph-api/overview#paging">cursor 기반</a>일 때 특히 중요하다. 다음은 <a href="https://docs.github.com/ko/rest?apiVersion=2022-11-28#pagination">GitHub 문서</a>에서 가져온 올바르게 사용된 <code>Link</code> header의 예시이다.</p>
<pre><code>Link: &lt;https://api.github.com/user/repos?page=3&amp;per_page=100&gt;; rel=&quot;next&quot;, &lt;https://api.github.com/user/repos?page=50&amp;per_page=100&gt;; rel=&quot;last&quot;</code></pre><p>이에 대한 내용은 <a href="https://docs.github.com/ko/rest/guides/using-pagination-in-the-rest-api?apiVersion=2022-11-28#using-link-headers">GitHub - Using pagination in the REST API</a>에서 더 자세히 살펴볼 수 있다.</p>
<p>그러나 많은 API가 사용 가능한 총 결과 수와 같은 추가적인 pagination information을 반환하기를 원하기 때문에 이것이 완전한 solution은 아니다. Count를 전달해야 하는 API는 <code>X-Total-Count</code>와 같은 custom HTTP header를 사용할 수도 있다.</p>
<br>

<h1 id="관련된-resource-representations를-자동으로-불러오는-방법을-제공하라">관련된 resource representations를 자동으로 불러오는 방법을 제공하라</h1>
<p>API consumer가 요청중인 resource와 관련된 (또는 resource에서 참조되는) data를 load 해야 하는 경우가 많다. Consumer가 이 information을 얻기 위해 반복적으로 API를 hit 하도록 요구하는 대신, 관련 data 요청 시 original resource와 함께 return 되도록 허용한다면 상당한 효율성이 있을 것이다.</p>
<p>그러나, 이는 일부 RESTful principles에 위배되므로 <code>embed</code> (or <code>expand</code>) query parameter 기반으로만 사용함으로써 편차를 최소화 할 수 있다.</p>
<p>이 경우, <code>embed</code>는 embed 되는 fields의 comma separated list이다. 또한 sub-fields를 참조하기 위해 dot-notation을 사용할 수 있다. 예를 들어 <code>GET /tickets/12?embed=customer.name,assigned_user</code>처럼 하면 다음과 같은 additional details가 포함된 ticket이 return된다.</p>
<pre><code class="language-json">{
  &quot;id&quot; : 12,
  &quot;subject&quot; : &quot;I have a question!&quot;,
  &quot;summary&quot; : &quot;Hi, ....&quot;,
  &quot;customer&quot; : {
    &quot;name&quot; : &quot;Bob&quot;
  },
  assigned_user: {
   &quot;id&quot; : 42,
   &quot;name&quot; : &quot;Jim&quot;,
  }
}</code></pre>
<p>물론 이러한 것들을 구현하는 능력은 실제로 내부 복잡성에 달려 있다. 이러한 종류의 embedding은 쉽게 <a href="http://stackoverflow.com/questions/97197/what-is-the-n1-selects-issue">N+1 select issue</a>(N+1 problem)로 이어질 수 있다.</p>
<br>

<h1 id="http-method를-override-하는-방법을-제공하라">HTTP method를 override 하는 방법을 제공하라</h1>
<p>일부 HTTP client는 간단한 <code>GET</code>, <code>POST</code> 요청만 처리할 수 있다. 제한된 clients에 대한 접근성을 높이려면 API에 HTTP method를 override 할 수 있는 방법이 필요하다. 이에 대해서는 명확한 표준이 없지만, 일반적으로 <code>PUT</code>, <code>PATCH</code> 또는 <code>DELETE</code> 중 하나가 포함된 문자열 값으로 <code>X-HTTP-Method-Override</code>를 받는 것이 일반적이다.</p>
<p>Overrider header는 반드시 <code>POST</code> 요청에서만 허용되어야 한다. <code>GET</code> 요청은 <a href="https://softwareengineering.stackexchange.com/questions/188860/why-shouldnt-a-get-request-change-data-on-the-server">절대 서버의 데이터를 변경해서는 안 된다</a>!</p>
<br>

<h1 id="rate-limiting에-유용한-response-headers를-제공하라">Rate limiting에 유용한 response headers를 제공하라</h1>
<p>Abuse를 방지하기 위해 API에 속도 제한을 추가하는 것이 표준 관행이다. <a href="https://datatracker.ietf.org/doc/html/rfc6585">RFC 6585</a>는 이를 수용하기 위해 HTTP status code <a href="https://datatracker.ietf.org/doc/html/rfc6585#section-4">429 Too Many Requests</a>를 도입했다.</p>
<p>하지만 실제로 limit에 도달하기 전에 소비자에게 limit을 알려주는 것은 매우 유용할 수 있다. 이 영역은 현재 표준이 부족하지만 HTTP response header를 사용하여 널리 사용되는 여러 규칙이 있다.</p>
<p>최소한 다음 header를 포함할 것을 권장한다.</p>
<ul>
<li><code>X-Rate-Limit-Limit</code>: 현재 period 동안 허용된 요청 수</li>
<li><code>X-Rate-Limit-Remaining</code>: 현재 peroid에서 남은 요청 수</li>
<li><code>X-Rate-Limit-Reset</code>: 현재 period에서 남은 시간(초)</li>
</ul>
<h3 id="x-rate-limit-reset에-time-stamp-대신-남은-시간초이-사용되는-이유는-무엇인가"><code>X-Rate-Limit-Reset</code>에 time stamp 대신 남은 시간(초)이 사용되는 이유는 무엇인가?</h3>
<p>Timestamp에는 날짜 및 시간대와 같이 유용하지만 불필요한 모든 종류의 정보가 포함되어 있다. APU consumer는 언제 다시 요청을 보낼 수 있는지만 알고 싶을 뿐이며, 초 단위의 남은 시간은 최소한의 추가 처리로 이 질문에 대한 답을 제공한다. 또한 <a href="https://en.wikipedia.org/wiki/Clock_skew">clock skew</a>와 관련된 문제도 방지할 수 있다.</p>
<p>일부 API는 <code>X-Rate-Limit-Reset</code>에 UNIX timestamp(seconds since epoch)를 사용하기도 하는데, 이는 좋지 않다. 이렇게 하지 말 것!</p>
<h3 id="x-rate-limit-reset에-unix-timestamp를-사용하는-것이-왜-나쁜-예시인가"><code>X-Rate-Limit-Reset</code>에 UNIX timestamp를 사용하는 것이 왜 나쁜 예시인가?</h3>
<p><a href="https://www.w3.org/Protocols/rfc2616/rfc2616.txt">HTTP spec</a>은 이미 <a href="https://www.ietf.org/rfc/rfc1123.txt">RFC 1123 날짜 형식</a>을 사용하도록 <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3">명시</a>하고 있다(현재 <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18">Date</a>, <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25">If-modified-Since</a>, <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.29">Last-Modified</a> HTTP headers). 만약 timestampe를 사용하는 새로운 HTTP header를 지정하려면 UNIX timestamp를 사용하는 대신 RFC 1123 규칙을 따라야 한다.</p>
<br>

<h1 id="authenticaton">Authenticaton</h1>
<p>RESTful API는 stateless 해야 한다. 이는 request authentication이 cookie나 session에 의존하지 않아야 함을 의미한다. 대신, 각 request에는 일종의 authentication credentials(자격 증명)가 제공되어야 한다.</p>
<p>항상 SSL을 사용하면 authentication credentials를 HTTP basic auth의 user name field에 전달되는, 무작위로 생성된 access token으로 간소화 할 수 있다. 이 방법의 가장 큰 장점은 브라우저에서 완벽하게 탐색 가능하다는 것이다. 즉, 브라우저는 서버에서 <code>401 Unauthorized</code> status code를 수신하면 credentials에 대해 묻는 prompt를 표시할 수 있다.</p>
<p>그러나 이런 token-over-basic-auth 인증 방법은 user가 관리자 인터페이스에서 API consumer 환경으로 token을 복사하는 것이 실용적인 경우에만 허용된다. 이것이 가능하지 않은 경우에는 <a href="https://oauth.net/2/">OAuth2</a>를 사용하여 제 3자에게 안전한 token 전송을 제공해야 한다. OAuth2는 <a href="https://datatracker.ietf.org/doc/html/rfc6750">Bearer tokens</a>를 사용하며 전송 암호화를 위해 SSL에 의존한다.</p>
<p>JSONP를 지원해야 하는 API는 JSONP 요청이 Http basic auth credentials나 Bearer tokens를 전송할 수 없으므로 세 번째 인증 방법이 필요하다. 이 경우 special query parameter <code>access_token</code>을 사용할 수 있다. 참고로 대부분의 웹 서버는 query parameter를 서버 로그에 저장하므로 token에 대해 query parameter를 사용하는 데는 보안 이슈가 있다.</p>
<p>위의 세 가지 방법은 모두 API boundary를 넘어 token을 전송하는 방법일 뿐이지, 실제 token 자체는 동일할 수 있다.</p>
<br>

<h1 id="caching">Caching</h1>
<p>HTTP는 built-in caching framework를 제공한다. 추가적인 outbound response headers를 추가하고 inbound request headers를 수신할 때 약간의 검사를 수행하기만 하면 된다.</p>
<p>여기에는 <a href="http://en.wikipedia.org/wiki/HTTP_ETag">ETag</a>와 <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.29">Last-Modified</a>라는 두 가지 접근 방식이 존재한다.</p>
<h3 id="etag">ETag</h3>
<p>Response를 생성할 때, representation의 hash 또는 checksum이 포함된 HTTP header <code>ETag</code>를 포함한다. 이 값은 output representation이 변경될 때마다 변경되어야 한다. 이제 inbound HTTP requests에 일치하는 <code>ETag</code> 값과 함께 <code>If-None-Match</code> header가 포함된 경우, API는 resource의 output representation 대신 <code>304 Not Modified</code> status code를 반환해야 한다.</p>
<h3 id="last-modified">Last-Modified</h3>
<p>Timestamp를 사용한다는 점을 제외하면 기본적으로 <code>ETag</code>와 비슷하게 작동한다. Response header <code>Last-Modified</code>에는 <a href="https://www.rfc-editor.org/rfc/rfc9110.html#name-date-time-formats">RFC 1123</a> 형식의 timestamp가 포함되며, 이 timestamp는 <code>If-Modified-Since</code>와 비교하여 유효성을 검사한다. HTTP spec에는 <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3">허용되는 세 가지 날짜 형식</a>이 있으며 서버는 이 중 하나를 사용할 수 있도록 해야 한다.</p>
<br>

<h1 id="errors">Errors</h1>
<p>HTML error page가 방문자에게 유용한 error message를 보여주는 것처럼 API는 유용한 에러 메시지를 known consumable format으로 제공해야 한다. Error의 representation은 고유한 fileds의 set이 있는 어떠한 resource의 representation과 다르지 않아야 한다.</p>
<p>API는 항상 적절한 HTTP status code를 return 해야 한다. API error는 일반적으로 두 가지 유형으로 나뉜다: client issues에 대한 400 series status codes &amp; server issues에 대한 500 series status codes. API는 최소한 모든 400 series error가 consumable JSON error representation과 함께 제공되도록 표준화 해야 한다. 또한 가능한 경우(i.e. load balancers &amp; reverse proxies가 custom error bodies를 생성할 수 있는 경우), 500 series status codes로 확장되어야 한다.</p>
<p>JSON error body는 개발자를 위해 몇가지 항목을 제공해야 한다 - 유용한 에러 메시지, unique한 에러 코드(문서에서 더 자세한 내용을 찾아볼 수 있는)와 가능한 한 상세한 설명. JSON output representation은 다음과 같다.</p>
<pre><code class="language-json">{
  &quot;code&quot; : 1234,
  &quot;message&quot; : &quot;Something bad happened :(&quot;,
  &quot;description&quot; : &quot;More details about the error here&quot;
}</code></pre>
<p><code>PUT</code>, <code>PATCH</code>, <code>POST</code> requests에 대한 validation errors는 field breakdown(분석, 명세)이 필요하다. 이는 validation failures에 대해 고정된 상위 에러 코드를 사용하고 다음과 같이 추가적인 <code>errors</code> field에 상세한 errors를 제공하여 설계된다.</p>
<pre><code class="language-json">{
  &quot;code&quot; : 1024,
  &quot;message&quot; : &quot;Validation Failed&quot;,
  &quot;errors&quot; : [
    {
      &quot;code&quot; : 5432,
      &quot;field&quot; : &quot;first_name&quot;,
      &quot;message&quot; : &quot;First name cannot have fancy characters&quot;
    },
    {
       &quot;code&quot; : 5622,
       &quot;field&quot; : &quot;password&quot;,
       &quot;message&quot; : &quot;Password cannot be blank&quot;
    }
  ]
}</code></pre>
<br>

<h1 id="자주-사용하는-http-status-codes">자주 사용하는 HTTP status codes</h1>
<p>HTTP는 API에서 응답할 수 있는 여러가지 <a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes">의미 있는 status code</a>를 정의한다. 이것들을 활용하여 API consumer는 응답을 적절하게 routing 할 수 있다. 다음은 자주 사용되는 HTTP status code 목록을 간략하게 정리한 것이다.</p>
<ul>
<li><code>200 OK</code> - 성공적인 GET, PUT, PATCH or DELETE에 대한 response. 또한 무언가를 생성하지 않은 POST에 대해서도 사용될 수 있다.</li>
<li><code>201 Created</code> - 무언가를 생성하는 POST에 대한 응답. 새로운 resource의 위치를 가리키는 <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.30">Location header</a>와 함께 사용되어야 한다.</li>
<li><code>204 No Content</code> - body를 반환하지 않는 성공적인 응답 (like a DELETE request).</li>
<li><code>304 Not Modified</code> - HTTP caching header가 동작되고 있을 때 사용된다.</li>
<li><code>400 Bad Request</code> - body가 파싱되지 않는 경우처럼 요청이 잘못된 경우.</li>
<li><code>401 Unauthorized</code> - 인증 관련 정보가 없거나 유효하지 않은 경우. 또한 API가 브라우저에서 사용되는 경우, 인증 관련 팝업 발생시키는 데에도 유용하다.</li>
<li><code>403 Forbidden</code> - 인증에는 성공했으나, 요청한 사용자가 resource에 접근할 수 없거나 접근 권한이 없는 경우.</li>
<li><code>404 Not Found</code> - 존재하지 않는 resource가 요청된 경우.</li>
<li><code>405 Method Not Allowed</code> - 인증되었으나, 요청한 사용자에게 허용되지 않는 HTTP method가 요청된 경우.</li>
<li><code>410 Gone</code> - 이 endpoint의 resource를 더 이상 사용할 수 없음을 나타낸다. 오래된 버전의 API들에 대한 포괄적인 응답으로 유용하다.</li>
<li><code>415 Unsupported Media Type</code> - 요청의 일부로 잘못된 content type이 제공된 경우.</li>
<li><code>422 Unprocessable Entity</code> - validation error들에 사용됨.</li>
<li><code>429 Too Many Requests</code> - rate limiting으로 인해 요청이 거부된 경우.</li>
</ul>
<br>

<h1 id="결론">결론</h1>
<p>이렇게 RESTful API를 설계할 때 고려할 점들에 대해 살펴보았습니다. 물론 이러한 사항들을 모두, 반드시 지켜야 할 필요는 없습니다. 결국 API를 설계하는 개발자가 고민하고 결정하는 것이지요. 하지만 RESTful API를 설계하고자 하는 개발자라면 이러한 사항들에 대해 살펴보고, 어떻게 설계해야 하는지에 대한 고민을 해보는 과정은 반드시 필요하지 않을까 싶습니다.</p>
<blockquote>
<p><strong>References</strong></p>
</blockquote>
<ul>
<li><a href="https://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api">Best Practices for Designing a Pragmatic RESTful API</a></li>
<li><a href="https://www.youtube.com/watch?v=RP_f5dMoHFc">그런 REST API로 괜찮은가</a></li>
<li><a href="https://docs.github.com/ko/rest/gists?apiVersion=2022-11-28#list-gists">GitHub API Docs</a></li>
<li><a href="https://stripe.com/docs/api">Stripe API Docs</a></li>
<li><a href="https://velog.io/@estell/%EC%A2%8B%EC%9D%80-REST-API%EB%A5%BC-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95">REST 성숙도 모델 4단계</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Vue] Vuex]]></title>
            <link>https://velog.io/@wo_ogie/Vue-Vuex-tmdapad3</link>
            <guid>https://velog.io/@wo_ogie/Vue-Vuex-tmdapad3</guid>
            <pubDate>Sat, 14 Aug 2021 14:47:15 GMT</pubDate>
            <description><![CDATA[<p><a href="https://vuex.vuejs.org">Vuex 공식문서</a>
<a href="https://vuex.vuejs.org/kr">Vuex 공식문서(KR)</a></p>
<h1 id="vuex란">Vuex란?</h1>
<hr>
<ul>
<li>Vuex는 Vue.js 애플리케이션 <strong>상태 관리 패턴 + 라이브러리</strong></li>
<li>애플리케이션에서 사용하는 모든 데이터를 중앙에서 관리하여, 규모가 크고 복잡한 애플리케이션의 컴포넌트들을 효율적으로 관리할 수 있다</li>
</ul>
<br>

<h1 id="vuex-등록">Vuex 등록</h1>
<hr>
<pre><code class="language-javascript">// store.js
import Vue from &#39;vue&#39;;
import Vuex from &#39;vuex&#39;;

Vue.use(Vuex);

export const store = new Vuex.Store({

});</code></pre>
<pre><code class="language-javascript">// main.js
import { store } from &#39;./store/store.js&#39;;

new Vue({
  ...,
  store,
  ...
});</code></pre>
<br>

<h1 id="vuex-컨셉">Vuex 컨셉</h1>
<hr>
<ul>
<li><strong>State</strong> : 컴포넌트 간에 공유하는 데이터 <code><strong>data()</strong></code></li>
<li><strong>View</strong> : 데이터를 표시하는 화면 <code><strong>template</strong></code></li>
<li><strong>Action</strong> : 사용자의 입력에 따라 데이터를 변경하는 <code><strong>methods</strong></code>
<img src="https://images.velog.io/images/wo_ogie/post/292ef43d-8dd4-4904-85b3-2e6d7134a5a8/image.png" alt=""></li>
<li><em>단방향 데이터 흐름*</em> 처리를 단순하게 도식화한 그림</li>
</ul>
<p><br><br></p>
<h1 id="vuex-구조">Vuex 구조</h1>
<hr>
<p>컴포넌트 -&gt; 비동기 로직 -&gt; 동기 로직 -&gt; 상태
<img src="https://images.velog.io/images/wo_ogie/post/158499be-1bc0-48c0-85a8-3045b8e7206b/image.png" alt=""></p>
<p>✅ <strong>단방향</strong>이라는 것이 중요!!</p>
<p><br><br></p>
<h1 id="vuex-기술-요소">Vuex 기술 요소</h1>
<hr>
<ul>
<li><strong>state</strong> : 여러 컴포넌트에 공유되는 데이터 <code><strong>data</strong></code></li>
<li><strong>getters</strong> : 연산된 state 값을 접근하는 속성 <code><strong>computed</strong></code></li>
<li><strong>mutations</strong> : state 값을 변경하는 이벤트 로직·메서드 <code><strong>methods</strong></code></li>
<li><strong>actions</strong> : 비동기 처리 로직을 선언하는 메서드 <code><strong>async methods</strong></code></li>
</ul>
<br>

<h2 id="1-state">1. state</h2>
<ul>
<li>여러 컴포넌트 간에 공유할 데이터 - <strong>상태(state)</strong></li>
</ul>
<pre><code class="language-javascript">// Vue
data: {
  message: &#39;Hello Vue.js!&#39;
}

// Vuex
state: {
  message: &#39;Hello Vue.js!&#39;
}</code></pre>
<pre><code class="language-html">&lt;!-- Vue --&gt;
&lt;p&gt;{{ message }}&lt;/p&gt;

&lt;!-- Vuex --&gt;
&lt;p&gt;{{ this.$store.state.message }}&lt;/p&gt;</code></pre>
<br>

<h2 id="2-getters">2. getters</h2>
<ul>
<li>state 값을 접근하는 속성이자 <code><strong>computed()</strong></code>처럼 미리 연산된 값을 접근하는 속성</li>
</ul>
<pre><code class="language-javascript">// store.js
state: {
  num: 10
},
getters: {
  getNum(state) {
    return state.num;
  },
  doubleNum(state) {
    return state.num * 2;
  }
}</code></pre>
<pre><code class="language-html">&lt;p&gt;{{ this.$store.getters.getNum }}&lt;/p&gt;
&lt;p&gt;{{ this.$store.getters.doubleNum }}&lt;/p&gt;</code></pre>
<br>

<h2 id="3-mutations">3. mutations</h2>
<ul>
<li>state의 값을 변경할 수 있는 <strong>유일한 방법</strong>이자 <strong>메서드(methods)</strong></li>
<li>mutations는 <code><strong>commit()</strong></code>으로 동작시킨다</li>
</ul>
<pre><code class="language-javascript">// store.js
state: { num: 10 },
mutations: {
  printNum(state) {
    return state.num;
  },
  sumNum(state, anotherNum) {
    return state.num + anotherNum;
  }
}

// App.vue
this.$store.commit(&#39;printNum&#39;);
this.$store.commit(&#39;sumNum&#39;, 20);</code></pre>
<h3 id="mutations의-commit-형식">mutations의 commit() 형식</h3>
<ul>
<li>state를 변경하기 위해 mutations를 동작시킬 때 <strong>인자(payload)</strong>를 전달할 수 있음. payload의 이름은 자유롭게 설정</li>
</ul>
<pre><code class="language-javascript">// store.js
state: { storeNum: 10 },
mutations: {
  modifyState(state, payload) {
    console.log(payload.str);
    return state.storeNum += payload.num;
  }
}

// App.vue
this.$store.commit(&#39;modifyState&#39;, {
  str: &#39;passed from payload!!&#39;,
  num: 20
});</code></pre>
<br>

<h3 id="왜-state는-직접-변경하지-않고-mutations로-변경할까">왜 state는 직접 변경하지 않고, mutations로 변경할까?</h3>
<p><img src="https://images.velog.io/images/wo_ogie/post/4be300d0-9c77-4871-9334-91388c37b132/vuex.jpg" alt=""></p>
<ul>
<li><p>여러 개의 컴포넌트에서 아래와 같이 state 값을 변경하는 경우 <strong>어느 컴포넌트에서 해당 state를 변경했는지 추적하기가 어렵다</strong></p>
<pre><code class="language-javascript">methods: {
increaseCounter() { this.$store.state.counter++; }
}</code></pre>
</li>
<li><p>특정 시점에 어떤 컴포넌트가 state를 접근하여 변경한 건지 확인하기 어렵기 때문</p>
</li>
<li><p>따라서, 뷰의 반응성을 거스르지 않게 명시적으로 상태 변화를 수행. </p>
</li>
<li><p><em>반응성, 디버깅, 테스팅 혜택*</em></p>
</li>
</ul>
<br>

<h2 id="4-actions">4. actions</h2>
<ul>
<li>비동기 처리 로직을 선언하는 메서드. 비동기 로직을 담당하는 mutations</li>
<li>데이터 요청, <a href="https://joshua1988.github.io/web-development/javascript/promise-for-beginners/">Promise</a>, ES6 async와 같은 <a href="https://joshua1988.github.io/web-development/javascript/javascript-asynchronous-operation/">비동기 처리</a>는 모두 actions에 선언</li>
<li>actions는 <code><strong>dispatch()</strong></code>로 동작시킨다</li>
</ul>
<p>아래는 actions에서 mutations를 사용할 때의 흐름을 확인해 볼 수 있는 코드이다</p>
<pre><code class="language-javascript">// store.js
state: {
  num: 10
},
mutations: {
  doubleNum(state) {    // 3️⃣
    return state.num * 2;
  }
},
actions: {
  delayDoubleNum(context) {    // 2️⃣ context로 store의 메서드와 속성에 접근
    context.commit(&#39;doubleNum&#39;);
  }
}

// App.vue
this.$store.dispatch(&#39;delayDoubleNum&#39;);    // 1️⃣</code></pre>
<br>

<h4 id="actions-비동기-코드-example1">actions 비동기 코드 example1</h4>
<pre><code class="language-javascript">// store.js
mutations: {
  addCounter(state) {
    state.counter++;
  }
},
actions: {
  delayedAddCounter(context) {
    setTimeout(() =&gt; context.commit(&#39;addCounter&#39;), 2000);
  }
}

// App.vue
methods: {
  incrementCounter() {
    this.$store.dispatch(&#39;delayedAddCounter&#39;);
  }
}</code></pre>
<h4 id="actions-비동기-코드-example2">actions 비동기 코드 example2</h4>
<pre><code class="language-javascript">// store.js
mutations: {
  setData(state, fetchedData) {
    state.product = fetched.Data;
  }
},
actions: {
  fetchProductData(context) {
    return axios.get(&#39;https://domain.com/products/1&#39;)
              .then(response =&gt; context.commit(&#39;setData&#39;, response));
  }
}

// App.vue
methods: {
  getProduct() {
    this.$store.dispatch(&#39;fetchProductData&#39;);
  }
}</code></pre>
<br>

<h3 id="왜-비동기-처리-로직은-actions에-선언해야-할까">왜 비동기 처리 로직은 actions에 선언해야 할까?</h3>
<ul>
<li>언제, 어느 컴포넌트에서 해당 state를 호출하고, 변경했는지 확인하기가 어려움</li>
<li>state 값의 변화를 추적하기가 어렵기 때문에 mutations 속성에는 동기 처리 로직만, actions 속성에는 비동기 처리 로직만 넣어야 한다.</li>
</ul>
<p><img src="https://images.velog.io/images/wo_ogie/post/2a9dded2-2e4f-44d2-8219-a32bd9592ea7/image.png" alt="image"></p>
<p>[그림] 여러 개의 컴포넌트에서 mutations로 시간 차를 두고 state를 변경하는 경우</p>
<br>

<h2 id="5modules">5.modules</h2>
<h3 id="store-속성-모듈화">store 속성 모듈화</h3>
<pre><code class="language-javascript">// store.js
export const store = new Vuex.store({
  state: { ... },
  getters: { ... },
  mutations: { ... },
  actions: { ... }
});</code></pre>
<p>위 코드를 모듈화 하면 아래처럼 작성할 수 있다</p>
<pre><code class="language-javascript">/// store.js
import * as getters from &#39;store/getters.js&#39;;
import * as mutations from &#39;store/mutations.js&#39;;
import * as actions from &#39;store/actions.js&#39;;

export const store = new Vuex.store({
  state: { ... },
  getters,    // getters: getters,
  mutations,    // mutations: mutations
  actions    // actions: actions
});</code></pre>
<br>

<h3 id="store-모듈화">store 모듈화</h3>
<ul>
<li>앱 규모가 커서 1개의 store로는 관리가 힘들 때 <code><strong>modules</strong></code> 속성 사용</li>
</ul>
<pre><code class="language-javascript">import Vue from &#39;vue&#39;;
import Vuex from &#39;vuex&#39;;
import todo from &#39;modules/todo.js&#39;;

export const store = new Vuex.Store({
  modules: {
    moduleA: todo,    // 모듈 명칭 : 모듈 파일 이름
  }
});

// todo.js
const state = { ... };
const getters = { ... };
const mutations = { ... };
const actions = { ... };</code></pre>
<p><br><br></p>
<h1 id="helper">Helper</h1>
<hr>
<h2 id="0-helper-사용법">0. Helper 사용법</h2>
<ul>
<li>Helper를 사용하고자 하는 vue 파일에서 아래와 같이 해당 Helper를 loading</li>
</ul>
<pre><code class="language-javascript">// App.vue
import { mapState } from &#39;vuex&#39;;
import { mapGetters } from &#39;vuex&#39;;
import { mapMutations } from &#39;vuex&#39;;
import { mapActions } from &#39;vuex&#39;;

export default {
  computed: { ...mapState([&#39;num&#39;]), ...mapGetters([&#39;countedNum&#39;]) },
  methods: { ...mapMutations([&#39;clickBtn&#39;]), ...mapActions([&#39;asyncClickBtn&#39;]) }
};</code></pre>
<br>

<h2 id="1-mapstate">1. mapState</h2>
<ul>
<li>Vuex에 선언한 state 속성을 뷰 컴포넌트에 더 쉽게 연결하는 Helper</li>
</ul>
<pre><code class="language-javascript">// store.js
state: {
  num: 10
}

// App.vue
import { mapState } from &#39;vuex&#39;;

computed: {
  ...mapState([&#39;num&#39;])
}</code></pre>
<pre><code class="language-html">&lt;!-- &lt;p&gt;{{ this.$store.state.num }}&lt;/p&gt; --&gt;
&lt;p&gt;{{ this.num }}&lt;/p&gt;</code></pre>
<br>

<h2 id="2-mapgetters">2. mapGetters</h2>
<ul>
<li>Vuex에 선언한 getters 속성을 뷰 컴포넌트에 더 쉽게 연결해주는 Helper</li>
</ul>
<pre><code class="language-javascript">// store.js
getters: {
  reverseMessage(state) {
    return state.msg.split(&#39;&#39;).reverse().join(&#39;&#39;);
  }
}

// App.vue
import { mapGetters } from &#39;vuex&#39;;

computed: { ...mapGetters([&#39;reverseMessage&#39;]) }</code></pre>
<pre><code class="language-html">&lt;!-- &lt;p&gt;{{ this.$store.getters.reverseMessage }}&lt;/p&gt; --&gt;
&lt;p&gt;{{ this.reverseMessage }}&lt;/p&gt;</code></pre>
<br>

<h2 id="3-mapmutations">3. mapMutations</h2>
<ul>
<li>Vuex에 선언한 mutations 속성을 뷰 컴포넌트에 더 쉽게 연결해주는 Helper</li>
<li>인자를 작성하지 않아도 호출할 때 인자를 암묵적으로 넘겨준다</li>
</ul>
<h4 id="example1">Example1</h4>
<pre><code class="language-javascript">// store.js
mutations: {
  clickBtn(state) {
    alert(state.msg);
  }
}

// App.vue
import { mapMutations } from &#39;vuex&#39;;

methods: {
  ...mapMutations([&#39;clickBtn&#39;]),
  authLogin() {},
  displayTable() {}
}</code></pre>
<pre><code class="language-html">&lt;button @click=&quot;clickBtn&quot;&gt;popup message&lt;/button&gt;</code></pre>
<h4 id="example2">Example2</h4>
<pre><code class="language-javascript">// store.js
state: { storeNum: 10 },
mutations: {
  modifyState(state, payload) {
    console.log(payload.str);
    return state.storeNum += payload.num;
  }
}

// App.vue
import { mapMutations } from &#39;vuex&#39;;

methods: {
  // 인자를 작성하지 않아도 호출할 때 인자가 있다면 암묵적으로 넘겨준다
  ...mapMutations([&#39;modifyState&#39;])
}</code></pre>
<pre><code class="language-html">&lt;button @click=&quot;modifyState({str, num})&quot;&gt;&lt;/button&gt;</code></pre>
<br>

<h2 id="4-mapactions">4. mapActions</h2>
<ul>
<li>Vuex에 선언한 actions 속성을 뷰 컴포넌트에 더 쉽게 연결해주는 Helper</li>
</ul>
<pre><code class="language-javascript">// store.js
actions: {
  delayClickBtn(context) {
    setTimeout(() =&gt; context.commit(&#39;clickBtn&#39;), 2000);
  }
}

// App.vue
import { mapActions } from &#39;vuex&#39;;

methods: {
  ...mapActions([&#39;delayClickBtn&#39;]),
}</code></pre>
<pre><code class="language-html">&lt;button @click=&quot;delayClickBtn&quot;&gt;delay popup message&lt;/button&gt;</code></pre>
<br>

<h2 id="5-helper의-유연한-문법">5. Helper의 유연한 문법</h2>
<ul>
<li>Vuex에 선언한 속성을 그대로 컴포넌트에 연결하는 문법</li>
</ul>
<pre><code class="language-javascript">// 배열 리터럴
...mapMutations([
  &#39;clickBtn&#39;
])</code></pre>
<ul>
<li>Vuex에 선언한 속성을 컴포넌트의 특정 메서드에 연결하는 문법</li>
</ul>
<pre><code class="language-javascript">// 객체 리터럴
...mapMutations({
  popupMsg: &#39;clickBtn&#39;    // 컴포넌트 methods 이름 : Store의 mutations 이름
})</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JavaScript] 스코프, 호이스팅]]></title>
            <link>https://velog.io/@wo_ogie/JavaScript-%EC%8A%A4%EC%BD%94%ED%94%84-%ED%98%B8%EC%9D%B4%EC%8A%A4%ED%8C%85</link>
            <guid>https://velog.io/@wo_ogie/JavaScript-%EC%8A%A4%EC%BD%94%ED%94%84-%ED%98%B8%EC%9D%B4%EC%8A%A4%ED%8C%85</guid>
            <pubDate>Sat, 14 Aug 2021 10:13:26 GMT</pubDate>
            <description><![CDATA[<h1 id="스코프scope">스코프(Scope)</h1>
<hr>
<ul>
<li>변수 이름, 함수 이름, 클래스 이름 등 모든 식별자는 자신이 선언된 위치에 의해 참조할 수 있는 유효 범위가 결정된다. 이러한 <strong>유효 범위를 스코프라 한다.</strong></li>
<li><strong>스코프</strong>는 전역(global)과 지역(local)으로 구분할 수 있으며, 변수는 자신이 선언된 위치에 의해 스코프가 결정된다.</li>
</ul>
<pre><code class="language-javascript">var x = &#39;global&#39;

function foo() {
  var x = &#39;local&#39;;
  console.log(x); // local
}

foo()
console.log(x);    // global</code></pre>
<p>위 예제에서 <strong>출력 결과가 서로 다른 이유는</strong> 변수명(<code>x</code>)은 같지만 <strong>스코프가 다른 별개의 변수를 호출하고 있기 때문</strong>이다.</p>
<p><br><br></p>
<h1 id="호이스팅hoisting">호이스팅(Hoisting)</h1>
<hr>
<ul>
<li><strong>호이스팅</strong>은 변수 선언이 <strong>스코프</strong>의 선두로 끌어 올려진 것처럼 동작하는 JavaScript 고유의 특징을 말한다.</li>
</ul>
<br>

<pre><code class="language-javascript">console.log(x);    // 1️⃣ 
var x = 10;
console.log(x);    // 2️⃣</code></pre>
<p>C, Java, Python과 같은 프로그래밍 언어를 공부해 본 사람이라면 이렇게 답할 것이다.</p>
<blockquote>
<p>🙋🏻‍♂️ 소스코드는 위에서부터 한 줄씩 순차적으로 실행됩니다. 따라서 <code>1️⃣</code>의 코드가 실행될 때에는 변수 <code>x</code>가 선언되어 있지 않으므로 에러가 발생할 것입니다</p>
</blockquote>
<p>하지만, JavaScript에서는 <code>1️⃣</code>의 결과로 <code>undefined</code>가, <code>2️⃣</code>의 결과로 10이 출력된다.</p>
<p>이런 결과가 나오는 이유에 대해서 이해하려면 JavaScript 엔진에서 변수 선언이 동작하는 방식에 대해 이해할 필요가 있다.</p>
<br>

<p>JavaScript에서 <strong>변수 선언은 런타임(소스코드가 순차적으로 실행되는 시점) 이전에 실행</strong>되지만, <strong>값의 할당은 런타임에 실행</strong>된다.</p>
<p>JavaScript 엔진은 소스코드를 한 줄씩 순차적으로 실행하기에 앞서, 먼저 소스코드의 평가 과정을 거치면서 소스코드를 실행하기 위한 준비를 한다. 이때 JavaScript 엔진은 변수 선언을 포함한 모든 선언문(변수 선언문, 함수 선언문 등)을 소스코드에서 찾아내 먼저 실행한다.  즉, JavaScript에서는 변수 선언 위치에 상관없이 어디에서든지 변수를 참조할 수 있다.</p>
<p><code><strong>var</strong></code>로 선언한 변수는 선언 즉시 <code>undefined</code>로 초기화 된다는 특징이 있다. 따라서 소스코드를 순차적으로 실행하는 런타임 이전 단계에서 <code><strong>var</strong></code>로 선언한 변수는 이미 선언되어 있으며 <code>undefined</code>로 초기화 된 상태이다.</p>
<p>이렇듯 <strong>변수 선언문이 코드의 선두로 끌어 올려진 것처럼 동작하는 JavaScript 고유의 특징을 호이스팅(hoisting)이라 한다</strong>.</p>
<br>

<p>마지막으로 예제 하나만 더 살펴보도록 하자.</p>
<pre><code class="language-javascript">var x = &#39;global&#39;;

function foo() {
  console.log(x); // 1️⃣ undefined
  var x = &#39;local&#39;;
}

foo();
console.log(x); // 2️⃣ global</code></pre>
<p><strong>호이스팅은 스코프를 단위로 동작한다.</strong></p>
<p>어떤 분들은 <code>1️⃣</code>에서 global이 출력될 것이라 예상하는 사람들도 있을 것이다. 하지만, 그렇지 않다.</p>
<p><code>foo()</code> 함수가 호출되면서 <code>1️⃣</code>을 실행하기 이전에는 이미 <code>var x = 'local';</code>에서 변수 <code>x</code>의 선언과 함께 <code>undefined</code>로 초기화가 진행된 상태이다. 그러므로 <code>1️⃣</code>에서는 <code>undefined</code>가 출력되는 것이다.</p>
<br>

<p>이로써 도입부에서 언급한 호이스팅의 의미를 모두 설명하였다. </p>
<p><strong>도대체 자바스크립트 엔진은 이러한 과정들을 어떻게 처리하는지</strong>에 대해 궁금하다면 <strong>실행 컨텍스트(Execution Context)</strong>와 <strong>렉시컬 환경(Lexical Environment)</strong>에 대해 공부하는 것을 추천한다.</p>
<p>마지막으로 앞서 언급한 호이스팅의 정의를 다시 한 번 읽어보고 다음 내용으로 넘어가도록 하자.</p>
<blockquote>
<p><strong>호이스팅</strong>은 변수 선언이 <strong>스코프</strong>의 선두로 끌어 올려진 것처럼 동작하는 JavaScript 고유의 특징을 말한다.</p>
</blockquote>
<p><br><br></p>
<h1 id="각-변수-선언-키워드의-특징">각 변수 선언 키워드의 특징</h1>
<hr>
<p>변수는 <code><strong>var</strong></code>, <code><strong>let</strong></code>, <code><strong>const</strong></code> 키워드를 사용하여 선언할 수 있다.
이제, 지금까지 살펴본 <strong>스코프</strong>, <strong>호이스팅</strong>을 참고하여 키워드 각각의 특징에 대해 알아보자.</p>
<h2 id="var">var</h2>
<h3 id="1-변수-중복-선언-허용">1. 변수 중복 선언 허용</h3>
<p><code><strong>var</strong></code>로 선언한 변수는 같은 스코프 내에서 중복 선언이 가능하다.</p>
<pre><code class="language-javascript">var x = 1;
console.log(x);    // 1

// var 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언을 허용한다
var x = 100;
console.log(x);    // 100</code></pre>
<br>

<h3 id="2-함수-레벨-스코프">2. 함수 레벨 스코프</h3>
<p><code><strong>var</strong></code> 키워드로 선언한 변수는 오직 함수의 코드 블록만을 지역 스코프로 인정한다. 즉, <code>if</code>나 <code>for</code>문 안에서 선언한 변수도 전역변수가 된다.</p>
<pre><code class="language-javascript">/* Example1 */
if (true) {
  var x = 10;
}

console.log(x);    // 10</code></pre>
<pre><code class="language-javascript">/* Example2 */
for (var i = 0; i &lt; 3; i++) {
  console.log(i); // 0 1 2
}

console.log(i);    // 3</code></pre>
<pre><code class="language-javascript">/* Example3 */
function foo() {
  // x는 foo 함수 내부에서만 사용할 수 있는 변수이다
  var x = 10;
  console.log(x); // 10
}

foo();
console.log(x);    // ReferenceError</code></pre>
<p>이러한 <code><strong>var</strong></code>의 함수 레벨 스코프는 의도치 않게 전역 변수를 선언/남발하게 되는 원인이 될 수 있다.</p>
<br>

<h3 id="3-변수-호이스팅">3. 변수 호이스팅</h3>
<p><code><strong>var</strong></code> 키워드로 선언한 변수는 <strong>호이스팅</strong>에 의해 변수 선언문이 스코프의 선두로 끌어 올려진 것처럼 동작한다. 단, 변수 할당문이 실행되기 이전에 변수를 참조하면 <code>undefined</code>를 반환한다.</p>
<pre><code class="language-javascript">// 런타임 이전에 호이스팅에 의해 변수 x가 이미 선언되어 있다
// 이때 변수 x는 undefined로 초기화 되어 있다

console.log(x);    // undefined

x = 10;

console.log(x); // 10

//  변수 선언은 JavaScript 엔진에 의해 런타임 이전에 실행된다
var x;</code></pre>
<p>지금까지 <code><strong>var</strong></code>의 특징에 대해 살펴보았다. 이제부터는 ES6에서 추가된 <code><strong>let</strong></code>, <code><strong>const</strong></code>의 특징에 대해 알아보자.</p>
<br>

<h2 id="let-const">let, const</h2>
<h3 id="1-변수-중복-선언-금지">1. 변수 중복 선언 금지</h3>
<p>앞서 살펴본 <code><strong>var</strong></code>과 달리 <code><strong>let</strong></code>, <code><strong>const</strong></code>는 같은 스코프 내에서 중복 선언을 금지한다.</p>
<pre><code class="language-javascript">var x = 1;
var x = 10; // 아무런 Error가 발생하지 않는다

let y = 2;
let y = 20; // SyntaxError: Identifier &#39;y&#39; has already been declared</code></pre>
<br>

<h3 id="2-블록-레벨-스코프">2. 블록 레벨 스코프</h3>
<p><code><strong>var</strong></code>키워드로 선언한 변수는 함수 레벨 스코프를 따른다.</p>
<p>하지만 <code><strong>let</strong></code>, <code><strong>const</strong></code> 키워드로 선언한 변수는 모든 코드 블록(함수, if문, for문, try/catch문 등)을 지역 스코프로 인정하는 블록 레벨 스코프를 따른다.</p>
<pre><code class="language-javascript">let x = 1;

if (true) {
  let x = 10;
  let y = 2;

  console.log(x); // 10
  console.log(y); // 2
}

console.log(x);    // 1
console.log(y);    // ReferenceError: y is not defined</code></pre>
<br>

<h3 id="3-변수-호이스팅-1">3. 변수 호이스팅</h3>
<pre><code class="language-javascript">console.log(x); // ReferenceError: x is not defined

let x = 1;

console.log(x); // 1</code></pre>
<p>위 예제를 보면 <code><strong>let</strong></code>, <code><strong>const</strong></code>키워드로 선언한 변수는 <strong>호이스팅이 발생하지 않는 것처럼 보인다</strong>. 하지만, 그렇지 않다. <code><strong>let</strong></code>, <code><strong>const</strong></code>키워드로 선언한 변수도 <strong>호이스팅이 발생한다</strong>.</p>
<pre><code class="language-javascript">let x = 1;

if (true) {
  console.log(x);
  let x = 10;
}</code></pre>
<p>위의 예제에서 <code>console.log(x);</code>의 실행 결과는?</p>
<blockquote>
<p>🙋🏻‍♀️ 전역 변수 <code>x=1</code>이 선언된 상태이고 <code>let x = 10;</code>는 아직 실행되지 않았으므로 <code>console.log(x)</code>에서는 1이 출력될 것입니다.</p>
</blockquote>
<p>틀렸다. 실행 결과 <strong><code>ReferenceError</code>가 발생한다</strong>. 이는 <code><strong>let</strong></code>, <code><strong>const</strong></code> 키워드로 선언한 변수도 <strong>여전히 호이스팅이 발생하기 때문</strong>이다. </p>
<p>JavaScript에서의 변수 선언과 초기화 과정을 살펴보면서 이해해보자.</p>
<br>

<h2 id="var-변수-선언-및-초기화-과정">var 변수 선언 및 초기화 과정</h2>
<pre><code class="language-javascript">var x;
x = 1;</code></pre>
<p><img src="https://images.velog.io/images/wo_ogie/post/40015f72-03d2-402c-9849-3268e2549769/image.png" alt="var 변수 선언 및 초기화 과정"></p>
<p><code><strong>var</strong></code> 키워드로 선언한 변수는 런타임 이전에 JavaScript 엔진에 의해 암묵적으로 <strong>선언 단계</strong>와 <strong>초기화 단계</strong>가 동시에 진행된다. 즉, 선언 단계에서 변수 선언을 하는 즉시 초기화 단계에서 변수를 <code>undefined</code>로 초기화한다.</p>
<br>

<h2 id="let-const-변수-선언-및-초기화-과정">let, const 변수 선언 및 초기화 과정</h2>
<pre><code class="language-javascript">let x;
x = 1;</code></pre>
<p><img src="https://images.velog.io/images/wo_ogie/post/9e83d28b-7507-4967-85e4-00bfee2b8b84/image.png" alt="let, const 변수 선언 및 초기화 과정"></p>
<p><code><strong>let</strong></code>, <code><strong>const</strong></code> 키워드로 선언한 변수는 <strong>선언 단계</strong>와 <strong>초기화 단계</strong>가 분리되어 진행된다. 즉, 런타임 이전에 JavaScript 엔진에 의해 암묵적으로 <strong>선언 단계</strong>가 진행되긴 하지만 <strong>초기화 단계</strong>는 변수 선언문에 도달했을 때 실행된다. 이때 스코프의 시작 지점부터 초기화 단계 전까지, 즉 변수를 참조할 수 없는 구간을 <strong>일시적 사각지대(TDZ)</strong>라고 한다.</p>
<br>

<p>위에서 보았던 예제를 다시 가져와봤다.</p>
<pre><code class="language-javascript">let x = 1;

if (true) {
  console.log(x); // ReferenceError: Cannot access &#39;x&#39; before initialization
  let x = 10;
}</code></pre>
<p><code><strong>let</strong></code> 키워드로 선언한 변수인 <code>x</code>가 호이스팅이 발생하지 않았다면 <code>console.log(x);</code>는 1을 출력해야 한다. 하지만 여전히 호이스팅이 발생했기 때문에 에러가 발생하는 것이다.</p>
<br>

<p>정리해보자</p>
<blockquote>
<p>JavaScript는 모든 선언(<code>var</code>, <code>let</code>, <code>const</code>, <code>function</code>, <code>class</code> 등)을 호이스팅 한다. 
이 중 ES6에서 도입된 <code>let</code>, <code>const</code>, <code>class</code>를 사용한 선언은 호이스팅이 <strong>발생하지 않는 것처럼</strong> 동작한다 </p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>