github blog를 검색포털에 검색되도록 설정하기

148 minute read

개요

github 블로그는 기본적으로 구글과 같은 검색포털에서 검색이 되지 않는다는 말을 들었다. 그래서 검색포털에 검색이 되려면 몇가지 작업을 해줘야한다. 오늘은 그 작업들을 해보려고 한다.

sitemap 생성

블로그 레퍼지토리의 최상단 경로에 sitemap.xml 파일을 생성한 뒤 아래의 내용을 작성해준다.

---
layout: null
---
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  
    <url>
      <loc>https://sunghwan7330.github.io/cleancode/clean_code_04/</loc>
      
        <lastmod>2023-11-20T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/cleancode/clean_code_03/</loc>
      
        <lastmod>2023-09-25T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/cleancode/clean_code_02/</loc>
      
        <lastmod>2023-09-01T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/cleancode/clean_code_01/</loc>
      
        <lastmod>2023-08-22T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/refactoring_2/refactoring_2_3_refactoring/</loc>
      
        <lastmod>2023-05-29T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/agile/kanban_board/</loc>
      
        <lastmod>2022-11-29T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/agile/agile_scrum/</loc>
      
        <lastmod>2022-11-27T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/infomation/info_javajigi_tdd/</loc>
      
        <lastmod>2022-10-17T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/refactoring_2/refactoring_2_2_refactoring_base/</loc>
      
        <lastmod>2022-09-20T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/refactoring_2/refactoring_2_1_example/</loc>
      
        <lastmod>2022-09-06T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/infomation/info_drdos/</loc>
      
        <lastmod>2022-08-15T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/embedded_c_tdd/embedded_c_tdd_13/</loc>
      
        <lastmod>2022-08-09T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/embedded_c_tdd/embedded_c_tdd_06/</loc>
      
        <lastmod>2022-08-02T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/infomation/atdd_instruction/</loc>
      
        <lastmod>2022-07-07T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/embedded_c_tdd/embedded_c_tdd_04/</loc>
      
        <lastmod>2022-06-13T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/embedded_c_tdd/embedded_c_tdd_03/</loc>
      
        <lastmod>2022-05-22T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/embedded_c_tdd/embedded_c_tdd_01/</loc>
      
        <lastmod>2022-04-29T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/make_computing_system/make_computing_system_12_operating_system/</loc>
      
        <lastmod>2022-04-15T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/make_computing_system/make_computing_system_11_compiler/</loc>
      
        <lastmod>2022-04-03T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/make_computing_system/make_computing_system_10_construction_analysis/</loc>
      
        <lastmod>2022-03-31T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/make_computing_system/make_computing_system_09_high_level_language/</loc>
      
        <lastmod>2022-03-23T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/make_computing_system/make_computing_system_08_program_control/</loc>
      
        <lastmod>2022-03-12T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/make_computing_system/make_computing_system_07_vm_operation/</loc>
      
        <lastmod>2022-02-24T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/make_computing_system/make_computing_system_06_assambler/</loc>
      
        <lastmod>2022-02-22T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/make_computing_system/make_computing_system_pracice_04/</loc>
      
        <lastmod>2022-02-04T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/make_computing_system/make_computing_system_05_computer_achitecture/</loc>
      
        <lastmod>2022-01-29T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/make_computing_system/make_computing_system_04_machine_code/</loc>
      
        <lastmod>2021-12-28T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/make_computing_system/make_computing_system_pracice_03/</loc>
      
        <lastmod>2021-12-22T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/make_computing_system/make_computing_system_03_seq/</loc>
      
        <lastmod>2021-12-01T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/make_computing_system/make_computing_system_bool_oper/</loc>
      
        <lastmod>2021-11-10T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/make_computing_system/make_computing_system_pracice_02/</loc>
      
        <lastmod>2021-11-06T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/make_computing_system/make_computing_system_pracice_01/</loc>
      
        <lastmod>2021-11-06T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/make_computing_system/make_computing_system_practice_intro/</loc>
      
        <lastmod>2021-11-06T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/make_computing_system/make_computing_system_bool/</loc>
      
        <lastmod>2021-10-27T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/infomation/codereview/</loc>
      
        <lastmod>2021-10-08T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/infomation/info_password_security/</loc>
      
        <lastmod>2021-10-08T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/2021_kakao_coding_test/2021kakao_05_ad_insert/</loc>
      
        <lastmod>2021-09-03T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/2021_kakao_coding_test/2021kakao_04_taxi_fare/</loc>
      
        <lastmod>2021-08-28T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/2021_kakao_coding_test/2021kakao_03_search_rank/</loc>
      
        <lastmod>2021-08-22T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/2021_kakao_coding_test/2021kakao_02_menu_renewal/</loc>
      
        <lastmod>2021-08-12T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/2021_kakao_coding_test/2021kakao_01_id_recommend/</loc>
      
        <lastmod>2021-08-08T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/infomation/info_packet_control/</loc>
      
        <lastmod>2021-07-31T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/blog/blog_category_count/</loc>
      
        <lastmod>2021-07-24T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/infomation/info_checksum_simd/</loc>
      
        <lastmod>2021-07-20T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/gdb/gdb_define/</loc>
      
        <lastmod>2021-07-16T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/raspberry_pi/raspberry_pi_samba/</loc>
      
        <lastmod>2021-07-11T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/infomation/info_FIDO2/</loc>
      
        <lastmod>2021-07-08T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/infomation/info_digital_signature/</loc>
      
        <lastmod>2021-06-28T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/testing/testing_google_assistant_api/</loc>
      
        <lastmod>2021-06-26T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/embedded_c_optimization/c_optimization_optimization_express/</loc>
      
        <lastmod>2021-06-20T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/embedded_c_optimization/c_optimization_loop_optimization/</loc>
      
        <lastmod>2021-06-20T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/embedded_c_optimization/c_optimization_if_switch/</loc>
      
        <lastmod>2021-06-19T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/raspberry_pi/raspberrypi_hangul/</loc>
      
        <lastmod>2021-06-15T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/embedded_c_optimization/c_optimization_array_struct/</loc>
      
        <lastmod>2021-06-15T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/blog/blog_search_agree/</loc>
      
        <lastmod>2021-06-13T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/embedded_c_optimization/c_optimization_data_type/</loc>
      
        <lastmod>2021-06-12T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/embedded_c_optimization/c_optimization_info/</loc>
      
        <lastmod>2021-06-11T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/blog/blog_memu/</loc>
      
        <lastmod>2021-06-11T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/blog/blog_sidebar/</loc>
      
        <lastmod>2021-06-10T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/gdb/gdb_tip/</loc>
      
        <lastmod>2021-06-08T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/embedded_c_optimization/c_optimization_pointer_use/</loc>
      
        <lastmod>2021-06-06T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/raspberry_pi/rasberrypi_init/</loc>
      
        <lastmod>2021-05-31T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/embedded_c_optimization/c_optimization_pointer/</loc>
      
        <lastmod>2021-05-31T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/personal_project/telegram_menu_bot/</loc>
      
        <lastmod>2019-11-01T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
    <url>
      <loc>https://sunghwan7330.github.io/personal_project/arduino_auto_light/</loc>
      
        <lastmod>2017-04-25T00:00:00+00:00</lastmod>
      

      
        <changefreq>weekly</changefreq>
      

      
          <priority>0.5</priority>
      

    </url>
  
</urlset>

해당 페이지를 등록하면 blog주소/sitemap.xml에 접속시 등록한 xml 화면이 나타나게 된다.

feed 생성

블로그 레퍼지토리의 최상단에 feed.xml 파일을 생성한 뒤 아래의 내용을 작성해준다.

---
layout: null
---
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Sunghwan's blog</title>
    <description></description>
    <link>https://sunghwan7330.github.io/</link>
    <atom:link href="https://sunghwan7330.github.io/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Sat, 20 Jul 2024 10:29:52 +0000</pubDate>
    <lastBuildDate>Sat, 20 Jul 2024 10:29:52 +0000</lastBuildDate>
    <generator>Jekyll v3.9.5</generator>
    
      <item>
        <title>4장 주석</title>
        <description>```
나쁜 코드에 주석을 달지 마라. 새로 짜라 
- 브라이언 W. 커니핸, P. J. 플라우거

개요

  • 주석은 오래될수록 코드에서 멀어짐
    • 프로그래머들이 주석을 유지하고 보수하기는 현실적으로 어려움
  • 부정확한 주석은 아예 없는 것보다 안좋음

주석은 나쁜 코드는 보완하지 못한다

  • 코드에 주석을 추가하는 이유는 코드 품질이 나쁘기 때문
  • 표현력이 풍부하고 깔끔하며 주석이 없는 코드가, 복잡하고 어수선한데 주석이 많이 달린 코드보다 훨씬 좋음
  • 주석으로 설명할 시간에 난장판이 된 코드를 정리하는 것이 좋음

코드로 의도를 표현하라

  • 아래의 두 예시를 볼 것
// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if ((employee.flags &amp; HOURLY_FLAG) &amp;&amp;
(employee.age &gt; 65))
if (employee.isEligibleForFullBenefits())
  • 주석으로 달려고 하는 내용을 함수로 만드는 것도 방법임

좋은 주석

법적인 주석

  • 정립된 구현 표준에 맞춰 넣는 주석
    • 예시
      // Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved.
      // GNU General Public License 버전 2 이상을 따르는 조건으로 배포한다.
    

정보를 제공하는 주석

  • 기본적인 정보를 주석으로 제공하면 편리함
// 테스트 중인 Responder 인스턴스를 반환한다.
protected abstract Responder responderInstance();
// kk:mm:ss EEE, MMM dd, yyyy 형식이다.
Pattern timeMatcher = Pattern.compile(
&quot;\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*&quot;);

의도를 설명하는 주석

  • 때때로 주석은 구현을 이해하게 도와줄 뿐만 아니라 결정에 깔린 의도까지 설명함
public int compareTo(Object o) {
  if(o instanceof WikiPagePath) {
    WikiPagePath p = (WikiPagePath) o;
    String compressedName = StringUtil.join(names, &quot;&quot;);
    String compressedArgumentName = StringUtil.join(p.names, &quot;&quot;);
    return compressedName.compareTo(compressedArgumentName);
  }
  return 1; // 오른쪽 유형이므로 정렬 순위가 더 높다.
}
public void testConcurrentAddWidgets() throws Exception {
  WidgetBuilder widgetBuilder = new WidgetBuilder(new Class[]{BoldWidget.class});
  String text = &quot;'''bold text'''&quot;;
  ParentWidget parent = new BoldWidget(new MockWidgetRoot(), &quot;'''bold text'''&quot;);
  AtomicBoolean failFlag = new AtomicBoolean();
  failFlag.set(false);

  // 스레드를 대량 생성하는 방법으로 어떻게든 경쟁 조건을 만들려 시도한다.
  for (int i = 0; i &lt; 25000; i++) {
    WidgetBuilderThread widgetBuilderThread =
    new WidgetBuilderThread(widgetBuilder, text, parent, failFlag);
    Thread thread = new Thread(widgetBuilderThread);
    thread.start();
  }
  assertEquals(false, failFlag.get());
}

의미를 명료하게 밝히는 주석

  • 인수나 반환값 자체를 명확하게 만들면 좋지만, 그렇지 못한 경우에 명료하게 밝히는 주석이 유용함
public void testCompareTo() throws Exception {
  WikiPagePath a = PathParser.parse(&quot;PageA&quot;);
  WikiPagePath ab = PathParser.parse(&quot;PageA.PageB&quot;);
  WikiPagePath b = PathParser.parse(&quot;PageB&quot;);
  WikiPagePath aa = PathParser.parse(&quot;PageA.PageA&quot;);
  WikiPagePath bb = PathParser.parse(&quot;PageB.PageB&quot;);
  WikiPagePath ba = PathParser.parse(&quot;PageB.PageA&quot;);

  assertTrue(a.compareTo(a) == 0); // a == a
  assertTrue(a.compareTo(b) != 0); // a != b
  assertTrue(ab.compareTo(ab) == 0); // ab == ab
  assertTrue(a.compareTo(b) == -1); // a &lt; b
  assertTrue(aa.compareTo(ab) == -1); // aa &lt; ab
  assertTrue(ba.compareTo(bb) == -1); // ba &lt; bb
  assertTrue(b.compareTo(a) == 1); // b &gt; a
  assertTrue(ab.compareTo(aa) == 1); // ab &gt; aa
  assertTrue(bb.compareTo(ba) == 1); // bb &gt; ba
}
  • 단, 그릇된 주석을 달아놓을 경우 상당히 위험

결과를 경고하는 주석

  • 다른 프로그래머에게 결과를 경고할 목적으로 사용하는 주석
// 여유 시간이 충분하지 않다면 실행하지 마십시오.
public void _testWithReallyBigFile() {
  writeLinesToFile(10000000);

  response.setBody(testFile);
  response.readyToSend(this);
  String responseString = output.toString();
  assertSubString(&quot;Content-Length: 1000000000&quot;, responseString);
  assertTrue(bytesSent &gt; 1000000000);
}
public static SimpleDateFormat makeStandardHttpDateFormat() {
  // SimpleDateFormat은 스레드에 안전하지 못하다.
  // 따라서 각 인스턴스를 독립적으로 생성해야 한다.
  SimpleDateFormat df = new SimpleDateFormat(&quot;EEE, dd MMM yyyy HH:mm:ss z&quot;);
  df.setTimeZone(TimeZone.getTimeZone(&quot;GMT&quot;));
  return df;
}

TODO 주석

  • 앞으로 할 일을 TODO 주석으로 남겨두면 편함
// TODO-MdM 현재 필요하지 않다.
// 체크아웃 모델을 도입하면 함수가 필요 없다.
protected VersionInfo makeVersion() throws Exception {
  return null;
}
  • 그렇다고 TODO를 난발하는 코드는 좋지 않음
  • 주기적으로 TODO를 체크하고 없애주도록 해야함

중요성을 강조하는 주석

  • 대수롭지 않다고 생각하는데 중요한 부분에 주석으로 사용하기도 함
String listItemContent = match.group(3).trim();
// 여기서 trim은 정말 중요하다. trim 함수는 문자열에서 시작 공백을 제거한다.
// 문자열에 시작 공백이 있으면 다른 문자열로 인식되기 때문이다.
new ListItemWidget(this, listItemContent, this.level + 1);
return buildList(text.substring(match.end()));

공개 API에서 Javadocs

  • 설명이 잘 된 공개 API는 참으로 유용하고 만족스러음
  • 공개 API를 구현한다면 반드시 훌륭한 docs 를 작성하는 것이 좋음

</description> Mon, 20 Nov 2023 00:00:00 +0000 https://sunghwan7330.github.io/cleancode/clean_code_04/</link> https://sunghwan7330.github.io/cleancode/clean_code_04/

    <category>cleancode</category>
    
    
    <category>cleancode</category>
    
  </item>

  <item>
    <title>3장 함수</title>
    <description>### 개요
  • 아래의 두 코드를 비교해 볼 것

      // 리펙터링 전
      public static String testableHtml(PageData pageData, boolean includeSuiteSetup) throws Exception {
        WikiPage wikiPage = pageData.getWikiPage();
        StringBuffer buffer = new StringBuffer();
        
        if (pageData.hasAttribute(&quot;Test&quot;)) {
          if (includeSuiteSetup) {
            WikiPage suiteSetup = PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_SETUP_NAME, wikiPage);
            if (suiteSetup != null) {
              WikiPagePath pagePath = suiteSetup.getPageCrawler().getFullPath(suiteSetup);
              String pagePathName = PathParser.render(pagePath);
              buffer.append(&quot;!include -setup .&quot;)
                .append(pagePathName)
                .append(&quot;\n&quot;);
            }
          }
          WikiPage setup = PageCrawlerImpl.getInheritedPage(&quot;SetUp&quot;, wikiPage);
          if (setup != null) {
            WikiPagePath setupPath = wikiPage.getPageCrawler().getFullPath(setup);
            String setupPathName = PathParser.render(setupPath);
            buffer.append(&quot;!include -setup .&quot;)
              .append(setupPathName)
              .append(&quot;\n&quot;);
          }
        }
        buffer.append(pageData.getContent());
        if (pageData.hasAttribute(&quot;Test&quot;)) {
          WikiPage teardown = PageCrawlerImpl.getInheritedPage(&quot;TearDown&quot;, wikiPage);
          if (teardown != null) {
            WikiPagePath tearDownPath = wikiPage.getPageCrawler().getFullPath(teardown);
            String tearDownPathName = PathParser.render(tearDownPath);
            buffer.append(&quot;\n&quot;)
              .append(&quot;!include -teardown .&quot;)
              .append(tearDownPathName)
              .append(&quot;\n&quot;);
          }
          if (includeSuiteSetup) {
            WikiPage suiteTeardown = PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_TEARDOWN_NAME, wikiPage);
            if (suiteTeardown != null) {
              WikiPagePath pagePath = suiteTeardown.getPageCrawler().getFullPath (suiteTeardown);
              String pagePathName = PathParser.render(pagePath);
              buffer.append(&quot;!include -teardown .&quot;)
                .append(pagePathName)
                .append(&quot;\n&quot;);
            }
          }
        }
        pageData.setContent(buffer.toString());
        return pageData.getHtml();
      }
    
      // 리펙터링 후
      public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
        boolean isTestPage = pageData.hasAttribute(&quot;Test&quot;);
        if (isTestPage) {
          WikiPage testPage = pageData.getWikiPage();
          StringBuffer newPageContent = new StringBuffer();
          includeSetupPages(testPage, newPageContent, isSuite);
          newPageContent.append(pageData.getContent());
          includeTeardownPages(testPage, newPageContent, isSuite);
          pageData.setContent(newPageContent.toString());
        }
        return pageData.getHtml();
      }
    
  • 리펙터링한 코드는 코드를 이해하기 쉬워짐

작게 만들어라

  • 함수는 작으면 작을수록 좋음 (저자의 경험에 바탕)
  • 들여쓰기는 2단을 넘지 말 것

한가지만 해라!

  • 개요에서 제시한 리팩터링 전 코드는 한 함수에서 여러가지 역할을 수행함
    • 버퍼 생성, 페이지 로드, 페이지 검색, 경로 랜더링 등등….
  • 함수는 한가지만 수행하고, 그 한가지만을 잘 해야함

함수 당 추상화 수준은 하나로!

  • 함수가 확실히 한가지 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 함
  • 내려가기 규칙
    • 코드는 위에서 아래로 이야기처럼 읽혀야 좋음
    • 한 함수의 다음에는 추상화 수준이 한 단계 낮은 함수가 옴
    • 즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한번에 한 단계씩 낮아짐

switch 문

  • Switch 문은 작게 만들기 어렵고, 한 가지 작업만 하는 switch 문을 만들기도 어려
  • Switch 문을 완전히 피할 수는 없지만, 저차원 클래스에 숨기는 방법이 있음

    public Money calculatePay(Employee e) throws InvalidEmployeeType {
      switch (e.type) {
        case COMMISSIONED:
          return calculateCommissionedPay(e);
        case HOURLY:
          return calculateHourlyPay(e);
        case SALARIED:
          return calculateSalariedPay(e);
        default:
          throw new InvalidEmployeeType(e.type);
      }
    }
    
  • 위 함수의 문제점
    • 함수가 길다. 새 직원 유형이 추가되면 더 길어짐
    • 한가지 작업만 수행하지 않음
    • SRP(Single Responsibility Principle)를 위반함
    • OCP(Open Closed Principle)를 위반함. 새 직원 유형을 추가할 때 마다 코드를 변경하기 때문
  • 이 문제를 해결한 코드
    • Switch 문은 추상 팩토리에 숨김
    • 팩토리는 Switch 문을 사용해 적절한 Empolyee 파생 클래스의 인스턴스를 생성
public abstract class Employee {
  public abstract boolean isPayday();
  public abstract Money calculatePay();
  public abstract void deliverPay(Money pay);
}
-----------------
public interface EmployeeFactory {
  public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
-----------------
public class EmployeeFactoryImpl implements EmployeeFactory {
  public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
    switch (r.type) {
      case COMMISSIONED:
        return new CommissionedEmployee(r) ;
      case HOURLY:
        return new HourlyEmployee(r);
      case SALARIED:
        return new SalariedEmployee(r);
      default:
        throw new InvalidEmployeeType(r.type);
    }
  }
}

서술적인 이름을 사용하라!

  • 함수가 작고 단순할 수록 서술적인 이름을 고르기 쉬워짐
  • 이름이 길더라도 서술적인게 긴 주석보다 좋음
  • 서술적인 이름을 사용하면 설계가 뚜렷해지므로 코드를 개선하기 쉬워짐
  • 모듈 내에서 함수 이름은 같은 문구, 명사, 동사 사용이 좋음

함수 인수

  • 함수에서 이상적인 인수 개수는 0 (무항) 임
  • 그 다음은 1개, 그 다음은 2개이며, 3항 이상은 피하는게 좋음. 4항은 무슨 이유라도 안됨
  • 코드를 읽는 사람에게는 includeSetupPageInto(new PageContent)보다 includeSetupPage()가 이해하기 더 쉬
  • 많이 쓰는 단항 형식
    • boolean fileExists(“MyFile”) 과 같이 질문을 던지는 형식
    • InputStream fileOpen(“MyFile”) 과 같이 특정 대상에 대한 결과를 받는 형식
    • passwordAttemptFailedNtimes(int attempts) 와 같이 return 없이 이벤트를 처리하는 형식
    • 위와 같은 형식이 아니면 단항 함수는 가급적 피할 것
  • 플래그 인수
    • 특히 bool 형식을 받는 함수는 좋지 않음
      • 함수 안에서 두가지 역할을 한다는 뜻이므로.
      • 함수를 각각 나누는 것이 더 좋음
  • 이항 함수
    • 인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어려움.
      • 예시로 writeField(name)writeField(outputStream, name) 보다 이해하기 쉬움
    • 이항 함수가 적절한 경우도 있음
      • Point p = new Point(0, 0) 와 같은 좌표 입력
      • assertEquals(expected, actual) 과 같은 테스트 검증 함
    • 이항 함수가 나쁜 것은 아니지만, 필요한 상황이 아니면 단항으로 바꾸려는 노력을 해봐야 함
  • 삼항 함수
    • 인수가 3개인 함수는 인수가 2개인 함수보다 훨씬 이해하기 어려움
    • 삼항 함수 만들때는 신중히 고려할 필요가 있음
  • 인수 객체
    • 인수가 2~3개 필요하다면 독자적인 클래스 변수로 선언할 수 있을지 검토해 볼 것

부수 효과를 일으키지 말 것

  • 한 함수에서 한가지를 하기로 하고 다른 행위를 하는 것은 좋지 않음
    • 함수로 넘어온 인수나 전역 변수를 수정하는 행위
  • 아래의 코드에서 부수 효과를 찾아보자.
public class UserValidator {
  private Cryptographer cryptographer;

  public boolean checkPassword(String userName, String password) {
    User user = UserGateway.findByName(userName);
    if (user != User.NULL) {
      String codedPhrase = user.getPhraseEncodedByPassword();
      String phrase = cryptographer.decrypt(codedPhrase, password);
      if (&quot;Valid Password&quot;.equals(phrase)) {
        Session.initialize();
        return true;
      }
    }
    return false;
  }
}
  • 위 코드에서 부수 효과는 Session.initialize(); 의 호출임
    • checkPassword 함수는 패스워드 체크만 해야 함
    • 함수 이름만 보고 패스워드 체크를 위해 위 함수를 사용하다가 세션이 초기화 될 수 있
  • 출력 인수
    • 일반적으로 인수는 입력으로 해석함
    • appendFooter(s) 는 s를 바닥글로 넣을까? 아니면 s 에 바닥글을 넣어 줄까?
    • public void appendFooter(StringBuffer report) 라는 선언부를 봐야 분명해짐
    • 일반적으로는 출력 인수는 피해야 함

명령과 조회를 분리하라

  • 함수는 뭔가를 수행하거나, 뭔가에 답하거나 둘 중에 하나만 해야 함
  • public boolean set(String attribute, String value); 이 함수는 attribute의 속성을 찾아 value로 바꾸고 성공 여부를 반환함
  • if (set(&quot;username&quot;, &quot;unclebob&quot;))... 은 어떻게 해석해야 하는가?
    • usernameunclebob 인지 확인하는 코드?
    • 또는 usernameunclebob 으로 설정하는 코드?
    • 이는 set 이 동사인지 형용사인지 분간하기 어려운 탓
    • set 을 동사로 의도했지만, if 문에 들어가면 형용사처럼 느껴지게 됨
  • 아래와 같이 명확하게 바꿔주는 것이 좋음
if (attributeExists(&quot;username&quot;)) {
  setAttribute(&quot;username&quot;, &quot;unclebob&quot;);
  ...
}

오류코드보다는 예외를 사용할 것

  • 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반함
if (deletePage(page) == E_OK) {
  if (registry.deleteReference(page.name) == E_OK) {
    if (configKeys.deleteKey(page.name.makeKey()) == E_OK){
      logger.log(&quot;page deleted&quot;);
    } else {
      logger.log(&quot;configKey not deleted&quot;);
    }
  } else {
    logger.log(&quot;deleteReference from registry failed&quot;);
  }
} else {
  logger.log(&quot;delete failed&quot;);
  return E_ERROR;
}
  • 반면 예외를 사용하면 오류처리 코드가 분리되어짐
try {
  deletePage(page);
  registry.deleteReference(page.name);
  configKeys.deleteKey(page.name.makeKey());
}
catch (Exception e) {
  logger.log(e.getMessage());
}

반복하지 마라!

  • 중복 코드가 늘어나면 해당 부분 수정시 범위가 늘어나게 됨
  • 중복 코드는 최대한 제거하려는 노력을 해야 함
  • 구조적 프로그래밍
    • 에츠허르 데이크스트(Edsger Dijkstra) 의 구조적 프로그래밍 원칙
    • 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 함
    • 즉, 함수는 return 문이 하나여야 하며, breakcontiune 를 사용하면 안되고, goto는 절대 사용하지 않음
    • 이는 함수가 작을 때 보다 함수가 클 때 상당한 이익을 제공함
    • 함수가 작을때는 return, break, continue 를 써도 좋음
  • 함수를 어떻게 짜죠?
    • 처음에는 길고 복잡하더라도 동작하도록 작성함
    • 이후 작성된 코드를 빠짐없이 테스트하는 단위 테스트 케이스를 작성함
    • 이후 코드를 다듬고, 함수를 쪼개고, 이름을 바꾸고, 중복을 제거하는 등의 리팩터링을 진행한 뒤 단위 테스트를 통과하도록 함

    </description> Mon, 25 Sep 2023 00:00:00 +0000 https://sunghwan7330.github.io/cleancode/clean_code_03/</link> https://sunghwan7330.github.io/cleancode/clean_code_03/

      <category>cleancode</category>
        
        
      <category>cleancode</category>
        
    </item>
    
    <item>
      <title>2장 의미있는 이름</title>
      <description>## 의도를 분명히 밝혀라
    
  • 함수명, 변수명은 의도를 분명하게 들어나야 함
public List&lt;int[]&gt; getThem() {
  List&lt;int[]&gt; list1 = new ArrayList&lt;int[]&gt;();
  for (int[] x : theList)
    if (x[0] == 4)

  list1.add(x);
  return list1;
}
  • 위 코드를 보았을 때 생기는 의문
  1. theList에 무엇이 들었는가?
  2. theList에서 0번째 값이 어째서 중요한가?
  3. 값 4는 무슨 의미인가?
  4. 함수가 반환하는 리스트 list1을 어떻게 사용하는가?
  • 아래와 같이 변경시 의도가 들어남
public List&lt;int[]&gt; getFlaggedCells() {
  List&lt;int[]&gt; flaggedCells = new ArrayList&lt;int[]&gt;();
  for (int[] cell : gameBoard)
    if (cell[STATUS_VALUE] == FLAGGED)

  flaggedCells.add(cell);
  return flaggedCells;
}

그릇된 정보를 피하라

  • 나름대로 널리 쓰이는 의미가 있는 단어를 다른 의미로 사용하지 말 것
    • accountList 의 형태가 List 가 아니라면 그릇된 정보를 제공할 수 있음
    • List가 아니라면 acountGroup 와 같은 이름 또는 account 로 명명하는 것이 좋음
  • 소문자 L과 대문자 O 사용시 주의할 것
int a = l;
if ( O == l )
  a = O1;
else
  l = 01;

검색하기 쉬운 이름을 사용하라

  • MAX_CLASSES_PER_STUDENT는 grep으로 찾기가 쉽지만, 숫자 7은 검색이 어려움
  • 변수나 상수를 여러곳에서 사용한다면 검색하기 쉬운 이름을 써야 함
for (int j=0; j&lt;34; j++) {
  s += (t[j]*4)/5;
}
int realDaysPerIdealDay = 4;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j=0; j &lt; NUMBER_OF_TASKS; j++) {
  int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
  int realTaskWeeks = (realTaskDays / WORK_DAYS_PER_WEEK);
  sum += realTaskWeeks;
}
  • 위 두 코드는 같은 동작을 함
    • 코드는 위가 간결할 수 있으나, 코드의 의도를 파악하는 것은 아래의 코드가 더 좋음
    • 또한 특정 변수나 상수를 검색하기 용이함

클래스 이름

  • 클래스 이름과 객체 이름은 명사나 명사구가 적합함
    • Customer, WikiPage, Account, AddressParser 등이 좋은 예
    • Manager, Processor, Data, Info 등과 같은 단어는 피하고, 동사는 사용하지 않도록 해야함

메서드 이름

  • 메서드 이름은 동사나 동사구가 적합함
    • postPayment, deletePage, save 등이 좋은 예
    • 접근자, 변경자, 조건자는 get, set, is 를 붙이는 것이 좋음

한 개념에 한 단어를 사용하라

  • 추상적인 개념 하나는 단어 하나만을 사용
    • 예시로 같은 메서드를 클래스마다 fetch, retrieve, get 등으로 각각 부르면 혼란스러움
    • 마찬가지로, 동일 코드 기반에 controller, manager, driver를 섞어 쓰면 혼란스러움

의미 있는 맥락을 추가하라

  • 클래스, 함수, 이름 공간에 의미를 넣어 맥락을 부여할 것
    • 예시 : firstName, lastName, street, houseNumber, city, state, zipcode
    • 같이 볼때는 주소라고 이해할 수 있지만, state 혼자만 있을떄는 의미를 알기 어려움
    • addr라는 접두어를 추가해 addrFirstName, addrLastName, addrState라 쓰면 맥락이 좀 더 분명해짐
    • Address 라는 클래스를 생성하는 방법도 있음</description> Fri, 01 Sep 2023 00:00:00 +0000 https://sunghwan7330.github.io/cleancode/clean_code_02/</link> https://sunghwan7330.github.io/cleancode/clean_code_02/

      <category>cleancode</category>
          
          
      <category>cleancode</category>
      

      </item>

      1장 깨끗한 코드 ## 나쁜 코드로 치르는 대가
  • 나쁜 코드는 개발 코드를 크게 떨어트림
  • 프로젝트 초반에는 번개처럼 나가다가 1~2년만에 굼뱅이처럼 기어가기도 합

p5 그림 1.1

  • 원대한 재설계의 꿈이 있지만, 생산성이 바닥이 되면 재설계를 하게 됨
  • 결국 재설계를 진행하는 팀과 기존의 유지보수 팀이 존재하게 됨
  • 재설계를 하는 팀은 기존의 제품과 동일한 모든 기능을 만들때 까지 제품을 출시할 수 없게 됨 (많은 시간 소요)
  • 재설계한 제품이 출시할때 쯤 되면 다시 만들자는 의견이 나옴 (재설계한 제품이 엉망이라서)

원초적 난제

  • 프로그래머는 나쁜 코드가 업무 속도를 늦춘다는 것을 알지만, 기한을 맞추려면 나쁜 코드를 양산할 수 밖에 없다고 생각함
  • 언제나 깨끗한 코드를 유지하는 습관이 중요함

깨끗한 코드란?

  • 비야네 스트롭스트룹
나는 우아하고 효율적인 코드를 좋아한다.
논리가 간단해야 버그가 숨어들지 못한다.
의존성을 최대한 줄여야 유지보수가 쉬워진다.
오류는 명백한 전략에 의거해 철저히 처리한다.
성능을 최적으로 유지해야 사람들이 원칙 없는 최적화 로코드를 망치려는 유혹에 빠지지 않는다.
깨끗한 코드는 한 가지를 제대로 한다.
  • 그래디 부치
깨끗한 코드는 단순하고 직접적이다.
깨끗한 코드는 잘 쓴 문장처럼 읽힌다.
깨끗한 코드는 결코 설계자의 의도를 숨기지 않는다.
오히려 명쾌한 추상화와 단순한 제어문으로 가득하다.
  • 데이브 토마스
깨끗한 코드는 작성자가 아닌 사람도 읽기 쉽고 고치기 쉽다.
단위 테스트 케이스와 인수 테스트 케이스가 존재한다.
깨끗한 코드에는 의미 있는 이름이 붙는다.
특정 목적을 달성하는 방법은 (여러 가지가 아니라) 하나만 제공한다.
의존성은 최소이며 각 의존성을 명확히 정의한다.
API는 명확하며 최소로 줄였다.
언어에 따라 필요한 모든 정보를 코드만으로 명확히 표현할 수 없기에 코드는 문학적으로 표현해야 마땅하다.
  • 마이클 페더스
깨끗한 코드의 특징은 많지만 그 중에서도 모두를 아우르는 특징이 하나 있다.
깨끗한 코드는 언제나 누군가 주의 깊게 짰다는 느낌을 준다.
고치려고 살펴봐도 딱히 손 댈 곳이없다.
작성자가 이미 모든 사항을 고려했으므로.
고칠 궁리를 하다보면 언제나 제자리로 돌아온다.
그리고는 누군가 남겨준 코드, 누군가 주의 깊게 짜놓은 작품에 감사를 느낀다.
  • 워드 커닝햄
코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다.
코드가 그 문제를 풀기 위한 언어처럼 보인다면 아름다운 코드라 불러도 되겠다.

보이스카우트 규칙

캠프장은 처음 왔을 때보다 더 깨끗하게 해놓고 떠나라.

  • 체크아웃할 때보다 좀 더 깨끗한 코드를 체크인 한다면 코드는 절대 나빠지지 않음
  • 변수 이름 변경, 함수 분할, 중복제거 등의 간단한 작업들을 조금씩 해나가는 것으로 충분함</description> Tue, 22 Aug 2023 00:00:00 +0000 https://sunghwan7330.github.io/cleancode/clean_code_01/</link> https://sunghwan7330.github.io/cleancode/clean_code_01/

      <category>cleancode</category>
        
        
      <category>cleancode</category>
        
    </item>
    
    <item>
      <title>7장 캡슐화</title>
      <description># 개요 
    

모듈을 분리하는 가장 중요한 기준은 시스템에서 각 모듈이 자신을 제외한 다른 부분에 드러내지 않아야 할 비밀을 얼마나 잘 숨기는데 있습니다.

클래스는 본래 정보를 숨기기 좋게 설계할 수 있습니다.

본 장에서는 클래스에서 본래의 정보를 숨기기 위한 캡슐화 방법에 대해 알아보려 합니다.

레코드 캡슐화하기

  • 리팩터링 전
organization = {name: 애크미 구스베리, country: GB}
  • 리팩터링 후
class Orgnization:

    def __init__(self, data):
        self.__name = data.name
        self.__country = data.country

    def getName(self):
        return self.__name

    def setName(self, name):
        self.__name = name

    def getCountry(self):
        return self.__country

    def setCountry(self, country):
        self.__country = country

배경

대부분의 프로그래밍 언어는 데이터 레코드를 표현하는 구조를 제공합니다. 레코드는 연관된 여러 데이터를 직관적인 방식으로 묶을 수 있어 각각을 취급할 때 보다 훨씬 의미있는 단위로 전달할 수 있게 됩니다.

절차

  1. 레코드를 담은 변수를 캡슐화한다.
  2. 레코드를 감싼 단순한 클래스로 해당 변수의 내용을 교체한다.
  3. 테스트한다.
  4. 원래 레코드 대신 새로 정의한 한 클래스 타입의 객체를 반환하는 함수들을 새로 만든다.
  5. 레코드를 반환하는 예전 함수를 사용하는 코드를 4.에서 만든 새 함수를 사용하도록 바꾼다.
  6. 클래스에서 원본 데이터를 반환하는 접근자와 원본 레코드를 반환하는 함수들을 제거한다.
  7. 테스트한다.

컬렉션 캡슐화하기

  • 리팩터링 전
class Person :
    def getCourses(self)
        return self.__courses

    def setCourses(self, aList):
        self.__courses = aList
  • 리팩터링 후
class Person :
    def getCourses(self)
        return self.__courses.slice()

    deff addCourse(self, aCourse):
        ……

    def removeCourse(self, aCourse):
        ……

배경

컬렉션 변수의 접근을 캡슐화하면서 getter가 컬렉션 자체를 반환하도록 한다면, 그 컬렉션을 감싼 클레스가 모르게 컬렉션의 원소들이 변경될 수 있습니다.

이러한 문제를 방지하기 위해 흔히 add() 와 remove()라는 이름의 컬렉션 변경자 메서드를 사용합니다. 이렇게 컬렉션을 소유한 클래스를 통해서만 원소를 변경하도록 하면 프로그램을 개선하면서 컬렉션 변경 방식도 원하는 대로 수정할 수 있습니다.

절차

  1. 컬렉션 변수를 캡슐화한다.
  2. 컬렉션에 원소를 추가 / 제거하는 함수를 추가한다.
  3. 정적 검사를 수행한다.
  4. 컬렉션을 참조하는 부분을 모두 찾는다. 컬렉션의 변경자를 호출하는 코드가 모두 앞에서 추가 / 제거 함수를 호출하도록 수정한다.
  5. 컬렉션 getter를 수정해서 원본 내용을 수정할 수 없는 읽기 전용 프락시나 복제본을 반환하게 한다.
  6. 테스트한다.

클래스 추출하기

  • 리팩터링 전
class Person:
    getOfficeAreaCode(self):
        return self.__officeAreaCode

    getOfficeNumber(self):
        return self.__officeNumber
  • 리팩터링 후
class Person:
    getOfficeAreaCode(self):
        return self.__telphoneNumber.getAreaCode()

    getOfficeNumber(self):
        return self.__telphoneNumber.getNumber()

class TelephoneNumber():
    getAreaCode(self):
        return self.__areaCode

    getNumber(self):
        return self.__number
        

배경

메서드와 데이터가 너무 많은 클래스는 이해하기 쉽지 않으니 잘 살펴보고 적절히 분리하는것이 좋습니다. 만약 일부 데이터와 메서드를 따로 묶을 수 있다면 분리하는것이 좋습니다. 제거해도 다른 필드나 메서드들이 논리적으로 문제가 없다면 분리할 수 있다는 뜻이 됩니다.

절차

  1. 클래스의 역할을 분리할 방법을 정한다
  2. 분리될 역할을 담당할 클래스를 새로 만든다
  3. 원래 클래스의 생성자에서 새로운 클래스의 인스턴스를 생성하여 필드에 저장해둔다.
  4. 분리될 역할에 필요한 필드들을 새 클래스로 옮긴다
  5. 매서드들도 새 클래스로 옮긴다
  6. 양쪽 클래스의 인터페이스를 살펴보면서 불필요한 메서드를 제거하고, 이름도 새로운 환경에 맞게 바꾼다
  7. 새 클래스를 외부로 노출할지 결정한다

클래스 인라인하기

  • 리팩터링 전
class Person:
    getOfficeAreaCode(self):
        return self.__telphoneNumber.getAreaCode()

    getOfficeNumber(self):
        return self.__telphoneNumber.getNumber()

class TelephoneNumber():
    getAreaCode(self):
        return self.__areaCode

    getNumber(self):
        return self.__number    
  • 리팩터링 후
class Person:
    getOfficeAreaCode(self):
        return self.__officeAreaCode

    getOfficeNumber(self):
        return self.__officeNumber

배경

클래스 인라인하기는 클래스 추출하기를 거꾸로 돌리는 리팩터링입니다. 더 이상 제 역할을 못해서 그대로 두면 안되는 클래스를 인라인 하는게 좋습니다. 역할을 옮기는 리팩터링을 하고 난 뒤 특정 클래스에 남는 역할이 거의 없을 때 이러한 현상이 자주 생기는데, 이때 클래스를 인라인하는게 보기 좋습니다.

절차

  1. 소스 클래스의 각 public 메서드에 대응하는 메서드들을 타깃 클래스에 생성한다. 이 메서드들은 단 순히 작업을 소스 클래스로 위임해야 한다.
  2. 소스 클래스의 메서드를 사용하는 코드를 모두 타깃 클래스의 위임 메서드를 사용하도록 바꾼다.
  3. 소스 클래스의 메서드와 필드를 모두 타깃 클래스로 옮긴다.

알고리즘 교체하기

  • 리팩터링 전
def foundPerson(self, people):
    for i in range(len(people)):
        if people[i] == Don:
            return Don
        if people[i] == John:
            return John
        if people[i] == Kent:
            return Kent
    reurn ‘’
  • 리팩터링 후
def foundPerson(self, people):
    candidates = [Don, John, Kent]
    return people if people in candidates else ‘’

배경

어떤 목적을 달성하는 방법에는 여러가지가 있고, 그 중에서는 다른 것보다 더 쉬운 방법이 있기 마련입니다. 리팩터링을 하면 복잡한 대상을 단순한 단위로 나눌 수 있지만, 떄로는 알고리즘 전체를 걷어내고 훨씬 간단한 알고리즘으로 바꿔야 할 때가 있습니다.

이러한 작업을 진행하기 전에 메서드가 가능한 잘게 나눴는지 확인할 필요가 있습니다. 거대하고 복잡한 알고리즘을 교체하기란 상당히 어려우니 알고리즘을 간소화하는 작업부터 해야 교체가 쉬워집니다.

절차

  1. 교체할 코드를 함수 하나에 모은다 .
  2. 이 함수만을 이용하여 동작을 검증하는 테스트를 마련한다.
  3. 대채할 알고리즘을 준비한다.
  4. 정적 검사를 수행한다.
  5. 기존 알고리즘과 새 알고리즘의 결과를 비교하는 테스트를 수행하한다.

</description> Mon, 29 May 2023 00:00:00 +0000 https://sunghwan7330.github.io/refactoring_2/refactoring_2_3_refactoring/</link> https://sunghwan7330.github.io/refactoring_2/refactoring_2_3_refactoring/

    <category>refactoring</category>
    
    
    <category>refactoring_2</category>
    
  </item>

  <item>
    <title>칸반 보드</title>
    <description># 개요 

오늘은 애자일에서 많이 사용하는 할일 관리 기법인 칸반 보드에 대해 간단하게 알아보도록 하겠습니다.

칸반 보드

칸반은 시각적 신호 를 나타내는 일본 단어입니다.

칸반 보드는 눈에 잘 보이지 않는 업무를 한눈에 파악할 수 있도록 하기 위해 만들어진 보드입니다.

칸반 보드는 슈퍼마켓의 매대에서 발견한 관리 기법이라고 합니다. 슈퍼마켓의 매대에 물건을 진열해 놓고 사람들은 물건을 사갑니다. 그리고 물건이 비워지면 다시 물건을 체웁니다.

칸반보드도 이와 마찬가지로 업무를 체워놓으면 구성원들이 가져가 업무를 진행하고 업무가 줄어들면 다시 업무를 체우는 방식으로 진행하게 됩니다.

눈에 보이는 보드를 이용하기에 업무 진행상황을 한눈에 볼 수 있다는 특징이 있습니다.

image

위 그림이 일반적인 칸반보드의 형태입니다. 경우에 따라 더 세분하게 나누기도 하지만 일반적으로는 세가지로 나눠집니다.

  • Todo : 진행해야 할 업무의 목록들을 놓는 곳
  • In Progress : 진행중인 업무를 놓는 곳
  • Done : 완료된 업무를 놓는 곳

그림을 보면 대충 어떤 느낌인지 아실껍니다.

업무를 진행하는 구성원은 Todo 에서 진행할 업무를 골라 In Progress 로 옮긴 뒤 업무를 진행합니다. 업무를 다 진행하였으면 카드를 Done로 옮깁니다. 이후 Todo 에서 다른 업무를 골라 다시 In Progress 로 옮긴 뒤 업무를 진행합니다.

이러한 식으로 진행하였을 때 업무관리와 진행상황을 한눈에 볼 수 있기 때문에 프로젝트 관리가 쉬워진다는 특징이 있습니다.

칸반 보드의 사용

칸반 보드는 사무실의 벽이나 화이트보드와 같은 보드에 마스킹 테이프 또는 포스트잇 등을 이용하여 만들 수 있습니다.

image

또는 다양한 SW를 활용하여 사용할 수도 있습니다.

칸반 보드를 지원하는 SW에는 Notion, Asana, Jira 등이 있습니다.

마무리

오늘은 칸반보드에 대해서 작성해보았습니다. 현업에서 업무 관리시에 Redmine 일감에 필터를 적용하여 관리했었는데, 다음에 또 기회가 된다면 칸반보드를 활용해볼까 생각중입니다.

참고

  • https://youtu.be/RIQSlXqXfbw

</description> Tue, 29 Nov 2022 00:00:00 +0000 https://sunghwan7330.github.io/agile/kanban_board/</link> https://sunghwan7330.github.io/agile/kanban_board/

    <category>agile</category>
    
    <category>kanban</category>
    
    
    <category>agile</category>
    
  </item>

  <item>
    <title>스크럼에 대하여</title>
    <description># 개요 

오늘은 스크럼에 대해 작성해보려 합니다.

저는 애자일하게 일하고 싶다고 말하면서도, 애자일에 대해서 자세히 모르는 것 같습니다. 그래서 앞으로는 애자일에 대해 조금씩 공부해보려 합니다.

오늘은 그 중에서 스크럼에 대해 알아보려 합니다.

아래의 내용은 위키피디아의 내용에 제 경험을 조금 추가하여 작성하였습니다.

스크럼이란?

스크럼은 프로젝트 관리를 위한 상호, 점진적 개발방법론이고, 애자일 개발 방법 중 하나입니다.

스크럼의 전체적인 흐름은 아래 그림과 같습니다.

스크럼_그림

위 그림에 각 단계에 대해서 하나씩 살펴보도록 하겠습니다.

예시로 우리는 테트리스 게임 만들기를 목표로 해보겠습니다.

스크럼 목표

스크럼은 아래의 가치에 중점을 두어 진행합니다.

  • 확약 : 약속한 것을 확실히 실혈하는 것
  • 전념 : 확약한 것의 실현에 전념하는 것
  • 정직 : 어떤 것이 자신에게 불리해도 숨기지 않는 것
  • 존중 : 자신과 다른 사람에게 경의를 표하는 것
  • 용기 : 팀 구성원은 자신이 옳은 일을 할 수 있도록 팀원간 갈등과 도전을 통해 작업할 수 있는 용기

스크럼 진행

스크럼은 제품 백로그를 정한 뒤 반복적은 스플린트를 진행하여 제품을 개발하게 됩니다. 백로그? 스플린트? 이러한 용어에 대해서는 앞으로 설명해보겠습니다.

제품 백로그 (product backlog)

제품 백로그는 제품에 대한 요구사항 목록입니다.

우리는 테트리스 게임을 만들기로 했으니 아래와 같은 요구사항이 있을 것입니다.

  • 게임판 만들기
  • 블록 모양 만들기
  • 조작 입력 기능 만들기
  • 점수 기능 만들기
  • 개발한 게임 패키징

스플린트 (splint)

스플린트는 일정 주기를 반복하여 기능을 개발하는 단계입니다.

스플린트 반복 주기에 대해서는 정하기 나름입니다. 제가 있던 직장에서는 2주 단위로 스플린트를 진행하였습니다.

스플린트를 직역하면 전력질주 인데, 스플린트 기간동안 집중하여 목표한 기능을 개발하는 것을 의미합니다.

스크럼을 통한 개발 진행시 이 스플린트를 반복하여 개발을 진행합니다.

스플린트 계획 회의 (scrume planning metting)

스플린트 계획 회의는 스플린트의 목표와 스플린트의 백로그를 계획하는 회의힙니다.

스플린트 백로그 (splint backlog)

이번 스플린트의 목표에 도달하기 위한 작업 목록을 의미합니다.

이번 프를린트의 목표가 게임판 만들기 라면, 게임판을 만들기 위한 작업 목록들을 작성하여 정리합니다.

일일 스크럼 회의 (daily scrum metting)

일일 스크럼 미팅은 하루단위로 진행하는 미팅(회의)입니다.

하루 단위로 진행사항, 앞으로 할일 또는 특이사항에 대해서 공유합니다.

이 회의는 최대한 간단하게 진행합니다.

회의가 길어지면 업무시간이 그 만큼 줄어들꺼니까요.

실행 가능한 제품 개발 (shippable product)

스플린트의 결과로써 나오는 실행 가능한 제품을 의미합니다.

제품 백로그 단계에서 기록한 목표를 스플린트를 반복적으로 진행하여 달성하게 된다면 실행 가능한 제품이 완성되게 됩니다.

마무리

열심히 작성한다고 작성했는데, 생각보다 내용이 부실한 것 같기도 하고…. ㅠ.ㅠ 혹시나 더 생각나는 부분이 있다면 더 추가하도록 하겠습니다.

참고

  • https://ko.wikipedia.org/wiki/%EC%8A%A4%ED%81%AC%EB%9F%BC_%28%EC%95%A0%EC%9E%90%EC%9D%BC_%EA%B0%9C%EB%B0%9C_%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%29

</description> Sun, 27 Nov 2022 00:00:00 +0000 https://sunghwan7330.github.io/agile/agile_scrum/</link> https://sunghwan7330.github.io/agile/agile_scrum/

    <category>agile</category>
    
    <category>scurm</category>
    
    
    <category>agile</category>
    
  </item>

  <item>
    <title>[우아한테크세미나] TDD 리팩토링 by 자바지기 박재성님 내용 정리</title>
    <description># 개요 

최근 TDD에 대한 관심이 생겨 자료를 찾던 중 유튜브 우아한Tech 체널에 공개된 세미나 중 박재성님의 TDD 강의 내용을 정리하였습니다.

1. 의식적인 연습이란?

TDD 리팩터링을 잘하려면 연습을 많이 할 것

무조건 연습을 많이 한다고 잘 할수는 없습니다. 다만 많은 노력을 통해 숙련되어야 자연스럽고 능숙하게 TDD를 수행할 수 있습니다.

강사님은 TDD를 5~6년을 도전한 후에야 테스트하기 쉬운 코드와 테스트하기 어려운 코드를 보는 눈이 생기고, 테스트하기 어려운 코드를 테스트하기 쉬운 코드로 설계하는 감이 생긴다고 말합니다.

TDD 리팩터링은 멋져보이지만 생각만큼 연습하기 어려운 점이 있습니다.

강사님이 교육자로써의 좀 더 효과적으로 쉽게 연습할 수 있는 방법을 고민하고 계신다고 합니다.

TDD를 포함하여 무언가에 노력하기 위한 서적으로 아래의 서적을 추천하셨습니다.

image

위의 책에 소개된 의식적인 연습의 7가지 원칙은 아래와 같습니다.

  1. 효과적인 훈련 기법이 수립되어 있는 기술 연마
  2. 개인의 컴포트 존을 벗어난 지점에서 진행, 자신의 현재 능력을 살짝 넘어가는 작업을 지속적으로 시도
  3. 명확하고 구체적인 목표를 가지고 진행
  4. 신중하고 계획적. 즉, 개인이 온전히 집중하고 ‘의식적’으로 행동할 것을 요구
  5. 피드백과 피드백에 따른 행동 변경을 수반
  6. 효과적인 심적 표상을 만들어내는 한편 심적 표상에 의존
  7. 기존에 습득한 기술의 특정 부분을 집중적으로 개선함으로써 발전시키고 수정하는 과정을 수반

의식적인 연습 예시 - 우테코 프리코스

강사님은 우아한 테크캠프 라는 자바 강의를 하고 계십니다. 해당 강의는 인원을 선발하여 진행하는데, 프리코스를 통해 인원을 선발합니다.

  • 3주동안 진행 매주 해결해야 할 미션을 부여
  • 미션을 완료한 후 github의 pull request로 제출
  • 각 PR 평가 후 공통 피드백 제공

선발 과정이지만 3주 동안 배움이 있는 과정을 만드는 것을 목표로 하고 있습니다. 단순 평가가 아닌 참여간 효과적인 피드백이 갈 수 있도록 합니다.

1주마다 새로운 제약사항을 추가하여 개발을 진행하도록 하는데, 주어지는 제약사항은 아래와 같습니다.

1주차 - 프로그래밍 제약사항

  • 자바 코드 컨벤션을 지키면서 코딩함
  • 인덴트 뎁스 3이 넘지 않도록 구현
  • 함수 또는 메서드는 한가지 일만 하도록 작게 구현

2주차 - 프로그래밍 제약

  • 함수 또는 메서드의 길이가 15라인을 넘어가지 않도록 구현
    • 함수는 한가지 일만 잘 하도록 구현
  • else 예약어를 쓰지 않는다

3주차

  • 함수의 길이가 10라인을 넘어기자 않도록 구현
  • 인덴트 뎁스 2 넘지 않도록 구현
  • 함수의 인자 수를 3개까지만 허용함

이와 같은 훈련을 하였을 때 코드는 더욱 간결해지고 가독성이 좋아집니다.

image

2. 의식적인 연습으로 TDD, 리팩토링 적용 - 개인

TDD, 리팩터링은 꾸준한 운동과 같다고 합니다. 평소에 30분 ~ 1시간씩 꾸준히 운동하는 것 처럼 평생동안 연습하겠다는 마음가짐으로 연습하는 것이 좋다고 합니다.

이를 위해서는 꾸준히 연습할 시간을 확보하는 것이 중요할 것입니다.

연습할 시간을 확보하였으면 단계적으로 연습을 하도록 합니다.

1단계 - 단위테스트 연습

내가 사용하는 API 사용법을 익히기 위한 학습 테스트에서 시작하는 것이 좋습니다.

  • 자바 String 클래스의 다양한 메소드 사용법
  • 자바 ArrayList 에 데이터를 추가, 수정, 삭제하는 방법

image

연습의 효과는 다음과 같습니다.

  • 단위 테스트 방법을 학습할 수 있음
  • 단위테스트 도구 학습법을 익힐 수 있음
  • 사용하는 API 에 대한 학습 효과가 있음

2단계 - TDD 연습

어려운 문제를 해결하는 것이 목적이 아니라 TDD를 연습하는 것을 목적으로 가벼운 프로젝트를 진행하는 것이 좋습니다.

난이도가 낮거나 자신에게 익숙한 문제로 시작하는 것을 추천합니다. 웹, 모바일 UI, DB 의존관계를 가지지 않는 요구사항으로 연습하는 것이 좋습니다.

예시 : 문자열 덧셈 계산기 요구사항

  • 요구사항 : 쉼표(,) 또는 콜론(:) 을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환
입력 출력
null 또는 “” 0
“1” 1
“1, 2” 3
“1,2:3” 6
  • TDD 사이클

image

  • 실패하는 테스트 생성 -&gt; 테스트 통과하는 코드 작성 -&gt; 코드 리팩터링 -&gt; 실패하는 테스트 작성 -&gt; …..
  • 실패하는 테스트 생성 -&gt; 테스트 통과하는 코드 작성 만 반복하는 것은 TDD가 아님
  • TDD의 핵심은 리팩터링임

  • 테스트 코드 작성
    • 원래는 테스트도 하나씩 추가하고 구현해야 함 (강의 편의상 한번에 진행)
public class StringCalculatorTest {
    @Test
    public void null_또는_빈값() {
        assertThat(StringCalculator.splitAndSum(null)).isEqualTo(0);
        assertThat(StringCalculator.splitAndSum(&quot;&quot;)).isEqualTo(0);
    }

    @Test
    public void 값_하나() {
        assertThat(StringCalculator.splitAndSum(&quot;1&quot;)).isEqualTo(1);
    }

    @Test
    public void 쉼표_구분자() {
        assertThat(StringCalculator.splitAndSum(&quot;1,2&quot;)).isEqualTo(3);
    }

    @Test
    public void 쉼표_콜론_구분자() {
        assertThat(StringCalculator.splitAndSum(&quot;1,2:3&quot;)).isEqualTo(6);
    }
}

메소드 분리 리팩터링

  • 리팩터링 할 것들
    • else 제거, 인덴트 줄이기 등등…
  • TDD 가 처음에는 막막하고 답답할 수 있지만, 꾸준한 연습을 통해 코드를 보는 눈을 기르는 것이 중요함

  • 테스트 코드를 건들이지 않고 위 함수에 대해서만 리팩터링 진행
public class StringCalculator {
    public static int splitAddSum (String text) {
        int result = 0;
        if (text == null || text.isEmpty()) {
            result = 0;
        }
        else {
            String[] values = text.split(&quot;,|:&quot;);
            for (String value : values) {
            result += Integer.ParseInt(value);
            }
        }
        return result;
    }
}
  • 메서드 분리를 통한 인덴트 줄이기
public class StringCalculator {
    public static int splitAddSum (String text) {
        int result = 0;
        if (text == null || text.isEmpty()) {
            result = 0;
        }
        else {
            String[] values = text.split(&quot;,|:&quot;);
            result = sum(values);
        }
        return result;
    }

    private static int sum(String[] vaules) {
        int result = 0;
        for (String value : values) {
            result += Integer.ParseInt(value);
        }
        return result;
    }
}
  • else 예약어 제거하기
public class StringCalculator {
    public static int splitAddSum (String text) {
        int result = 0;
        if (text == null || text.isEmpty()) {
            return 0;
        }

        String[] values = text.split(&quot;,|:&quot;);
        return sum(values);
    }

    private static int sum(String[] vaules) {
        int result = 0;
        for (String value : values) {
            result += Integer.ParseInt(value);
        }
        return result;
    }
}
  • 메서드가 한 가지 일만 하도록 구현하기
    • sum 함수가 string을 int로 변경과 덧셈 역할을 하고 있음
    • 이 역할을 각각의 메서드로 분리
    • 반복문을 두번 돌지만 값이 크지 않다면 성능적인 영향은 적음
    • 메서드를 분리함으로써 메서드의 재사용성이 증가함
public class StringCalculator {
    public static int splitAddSum (String text) {
        int result = 0;
        if (text == null || text.isEmpty()) {
            return 0;
        }

        String[] values = text.split(&quot;,|:&quot;);
        return sum(toInts(values));
    }

    private static int[] toInts(Strings[] values) {
        int[] numbers = new Int[values.length];
        for (int i=0; i&lt;values.length; i++) {
            numbers[i] = Integer.parseInt(values[i]);
        }
        return numbers;
    }

    private static int sum(String[] numbers) {
        int result = 0;
        for (int number : numbers) {
            result += number;
        }
        return result;
    }
}
  • 로컬 변수가 필요 없다면 합치기
public class StringCalculator {
    public static int splitAddSum (String text) {
        int result = 0;
        if (text == null || text.isEmpty()) {
            return 0;
        }

        return sum(toInts(text.split(&quot;,|:&quot;)));
    }

    private static int[] toInts(Strings[] values) {
        [...]
    }
	
    private static int sum(String[] numbers) {
        [...]
    }
}
  • compose method 패턴 적용
    • 어떤 메서드의 내부 로직이 한 눈에 이해하기 어렵다면, 그 로직을 의도가 잘 드러나며 동등한 수준의 작업을 하는 여러 단계로 나눔
public class StringCalculator {
    public static int splitAddSum (String text) {
        if (isBlank(text) 
            return 0;
		
        return sum(toInts(split()));
    }

    private static boolean isBlank (String text) {
        return (text == null || text.isEmpty())
    }
	
    private static String[] split(String text) {
        return text.split(&quot;,|:&quot;)
    }
	
    private static int[] toInts(Strings[] values) {
        [...]
    }
    
    private static int sum(String[] numbers) {
        [...]
    }
}
  • splitAddSum 함수를 처음 읽는 사람의 경우 어떤 코드가 더 읽기 좋을지 생각해볼 것

리팩토링 연습 - 클래스 분리

  • 문자열 덧셈 계산기의 요구사항 추가
    • 숫자 이외의 값 또는 음수를 전달하는 경우 RuntimeEeption 예외를 throw 한다.
입력 출력
null 또는 “” 0
“1” 1
“1, 2” 3
“1,2:3” 6
"-1,2:3" RuntimeEeption
  • 요구사항 추가에 따른 테스트 추가
public class StringCalculatorTest {
    @Test
    public void null_또는_빈값() { ... }
	
    @Test
    public void 값_하나() { ... }
	
    @Test
    public void 쉼표_구분자() { ... }
	
    @Test
    public void 쉼표_콜론_구분자() { ... }
	
    @Test(expected = RuntimeException.Class)
    public void 음수값() {
        StringCalculator.splitAndSum(&quot;-1,2:3&quot;);
    }
}
  • toInt 함수 추가 및 예외처리 추가
public class StringCalculator {
    public static int splitAddSum (String text) {
        if (isBlank(text) 
            return 0;
		
        return sum(toInts(split()));
    }

    private static boolean isBlank (String text) { [...] }
	
    private static String[] split(String text) { [...] }
	
    private static int[] toInts(Strings[] values) {
        int[] numbers = new Int[values.length];
        for (int i=0; i&lt;values.length; i++) {
            numbers[i] = toInt(values[i])
        }
        return numbers;
    }
	
	private static int toInt(String value) {
        int number = Integer.parseInt(value);
        if (number &lt; 0) {
            throw new RuntimeException();
        }
        return number;
    }
    
    private static int sum(String[] numbers) { [...] }
}
  • 원시값과 문자열 포장을 위한 클래스 분리
    • 양수만 입력받을 수 있는 클래스로 분리함
      public class Positive {
      private int number;
      
      private Positive(String value) {
          int number = Integer.parseInt(value);
          if (number &lt; 0) {
              throw new RuntimeException();
          }
          this.number = number;
      }
      	
      public Positive add(Positive other) {
          return new Positive(this.number + other.number);
      }
      
      public int getNumber() {
          return number;
      }
      }
      
public class StringCalculator {
    public static int splitAddSum (String text) {
        if (isBlank(text) 
            return 0;
		
        return sum(toInts(split()));
    }

    private static boolean isBlank (String text) { [...] }
	
    private static String[] split(String text) { [...] }

    private static Positive[] toPositives(String[] value) {
        Positive[] numbers = new Positive[values.length];
        for (int i=0; i&lt;values.length; i++) {
            number[i] = new Positive(values[i]);
        }
        return numbers;
    }
    
    private static int sum(String[] numbers) {
        Positive result = new Positive(0);
        for (Positive number : numbers) {
            result = result.add(number);
        }
        return result.getNumber();
    }
}
  • 클래스 분리 연습을 위해 활용할 수 있는 원칙
    • 일급 콜렉션을 쓴다
    • 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  • 일급 콜렉션 예시
    • Set 콜렉션 하나만 인스턴스로 가지는 클래스로 분리
    • 해당 콜랙션을 사용하는 메서드를 클래스 안으로 유입함 ```java import java.util.Set;

public class Lotto { private static final int LOTTO_SIZE = 6; private final Set<LottoNumber> lotto;

private Lotto(Set&lt;LottoNumber&gt; lotto) {
    if (lotto.size() != LOTTO_SIZE) {
        throw new IllegalArgumentException();
    }
    this.lotto = lotto;
} } ```

4단계 - 장난감 프로제그 난이도 높이기

  • 점진적으로 요구사항이 복잡한 프로그램 구현
  • 앞에서 지켰던 기준을 지키면서 프로그래밍 연습

  • TDD, 리펙토링 연습하기 좋은 프로그램 요구사항
  • 게임과 같이 요구사항이 명확한 프로그램으로 연습
  • 의존 관계 (모바일 UI, 웹 UI, 데이터베이스, 외부 API 와 같은 의존관계) 가 없이 연습
  • 약간은 복잡한 로직이 있는 프로그램

  • 연습하기 좋은 예 (단 UI는 콘솔)
    • 로또
    • 사다리 타기
    • 볼링 게임 점수판
    • 체스 게임
    • 지뢰찾기 게임

5단계 - 의존관계 추가를 통한 난이도 높이기

  • 웹 UI, 모바일 UI, 데이터베이스와 같은 의존관계를 추가

  • 이떄 필요한 역량
    • 테스트하기 쉬운 코드와 테스트하기 어려운 코드를 보는 눈
    • 테스트하기 어려운 코드를 테스트하기 쉬운 코드로 설계하는 감(sense)
  • 위 단계를 걸쳐 트레이닝 했다면 테스트하기 쉬운 코드와 어려운 코드를 분리하는 역량이 쌓였을 것

한 단계 더 나아가기

  • 한 단계 더 나아간 연습을 하고 싶다면
    • 컴파일 에러를 최소화하면서 리팩터링 하기
    • ATDD 기반으로 응용 어플리케이션 개발
    • 레거시 애플리케이션에 테스트 코드 추가해 리팩터링하기
  • 우리가 TDD, 리펙터링 적용이 실패하는 이유
    • TDD, 리팩터링 연습이 충분하지 않은 상태에서 레거시 앱에 테스트 코드 추가해 리팩터링 하기 와 같은 난이도에 도전

구체적인 연습 목표 찾기

image

위의 서적에 작성돤 객체지향 생활체조 원칙은 아래와 같습니다.

  1. 한 메서드에 오직 한 단계의 들여쓰기만 한다.
  2. elas 예약어를 쓰지 않는다.
  3. 모든 원시값과 문자열을 포장한다.
  4. 한 줄에 점을 하나만 찍는다.
  5. 줄여쓰지 않는다 (축약 금지)
  6. 모든 엔티티를 작게 유지한다.
  7. 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  8. 일급 콜렉션을 쓴다.
  9. 게터 / 세터 / 프로퍼티를 쓰지 않는다.

자세한 내용은 서적을 통해 직접 보는 것이 좋아보입니다.

클린 코드 서적에는 아래의 내용이 기록되어 있습니다.

메서드(함수)에서 가장 이상적인 인자 개수는 0개이다. 다음은 1개이고 그다음은 2개이다.
3개는 가능한 피하는 편이 좋다. 
4개 이상은 특별한 이유가 있어도 사용하면 안된다. 

TDD 는 여유를 가지고 꾸준히 연습을 하는 것이 좋습니다. 나만의 장난감 프로젝트를 통해 같은 과제를 반복적으로 구현할 수 있는 인내력, 꾸준함, 성실함을 쌓는 것이 중요합니다.

3. 리팩터링 적용 - 개인(주니어) -> 팀

TDD를 팀에 전파하기 전에 아래의 대한 사항을 고려하는 것이 좋습니다.

  • 사람은 기본적으로 변화를 거부하는 성향
  • 팀은 변화를 거부하는 성향이 더 강함
  • 대부분의 사람들은 변화에 실패한 경험을 가지고 있음

이때는 내가 맡은 구현에 TDD, 리팩터링을 적용하는 것이 좋습니다. 남에게 강요하지 전에 혼자서 묵묵히 진행하도록 해 줍니다.

이후 관심이 있는 사람이 생기면 TDD, 리팩터링을 전파해줍니다. 내가 구현한 코드 또는 동료의 관심에서 작은 성공(small success)를 맛보게 될 것입니다.

리더가 만약 하지 말라고 한다면 그만 해도 됩니다. 다만 이미 자신은 많이 성장했기 때문에 갈 곳은 많을 것입니다. 절대 손해보는 것이 아니기 때문에 걱정하지 않아도 됩니다.

이후 경력이 쌓이면 내가 리더가 될 수 있습니다.

4. TDD 리팩터링 적용 - 내가 리더

리더가 되었을 때도 위에 언급한 점을 고려해야 합니다.

  • 사람은 기본적으로 변화를 거부하는 성향
  • 팀은 변화를 거부하는 성향이 더 강함
  • 대부분의 사람들은 변화에 실패한 경험을 가지고 있음

리더가 되었을 때는 추가적으로 생각할 부분이 더 있습니다.

  • 1:1로 변화를 공략
  • 팀원이 개선할 부분을 말하고, 해결책을 제안하도록 유도

리더가 되었을 때는 팀원들과의 신뢰를 형성하는 것이 가장 중요합니다. 1:1 면담을 통해 개선할 부분을 찾는 것이 좋습니다.

1:1 면담을 하게 된다면 아래의 사항을 고려하는 것이 좋습니다.

  • 문제점에 대한 해답을 제시하려 하지 말것
  • ‘어떻게 하면 될까?’ , ‘너라면 어떻게 할 것 같아??’ 라는 반문을 제시

문제점에 대한 답이 리더가 아닌 팀원에게 나오도록 유도하는 것이 포인트입니다. 리더가 먼저 강요한다면 반감이 있을 수 있습니다. 대신에 팀원에게 답을 듣는다면 흥미를 가지게 될 수 있습니다.

1:1 면담, 팀 회고를 통해 우선 순위가 높으면서, 작은 변화를 통해 가장 높은 효과가 있을 것으로 생각되는 practice를 선택합니다. 선택한 practice가 개인, 팀 모두 익숙해 질 때 까지 한가지에 집중합니다. 선택한 practice 로 변화를 완료함으로써 작은 성공(small success)를 맛보면 성취감을 느낄 것입니다.

image

위와 같은 사이클을 반복하여 팀을 점차 발전시킬 수 있을 것입니다.

중요한 점은 어떤 practice를 하느냐가 아니고, 현재 조금씩 나아지고 있다는 방향성입니다. </description> Mon, 17 Oct 2022 00:00:00 +0000 https://sunghwan7330.github.io/infomation/info_javajigi_tdd/</link> https://sunghwan7330.github.io/infomation/info_javajigi_tdd/

    <category>TDD</category>
    
    <category>Refactoring</category>
    
    
    <category>infomation</category>
    
  </item>

  <item>
    <title>6장 기본적인 리팩터링</title>
    <description># 개요 

1장에서 중간을 생략하고 6장에서 시작하는 이유는 본격적인 리팩터링 기법을 정리하기 위함입니다.

2 ~ 5 장 까지는 리팩터링에 대한 유래, 예시 등이 담겨져 있으니 서적을 구매하여 보시는 것도 추천드립니다.

함수 추출하기

  • 리팩터링 전 ```python def printOwing(invoice): printBanner() outstanding = caculateOutstanding()

    # 세부 사항 출력 print(‘고객명 : {}’.format(invoice.customer)) print(‘채무액 : {}’.format(outstanding))


* 수정 후 

```python 

def printDetails(outstanding):
   # 세부 사항 출력 
    print('고객명 : {}'.format(invoice.customer))
    print('채무액 : {}'.format(outstanding))

def printOwing(invoice):
    printBanner()
    outstanding = caculateOutstanding()

     printDetails(outstanding)

}

배경

함수 추출하기는 코드 조각을 찾아 무슨 일을 하는지 파악한 다음, 독립된 함수로 추출하고 목적에 맞는 이름을 붙입니다.

함수를 짧게 만들면 함수 호출이 많아지게 됩니다. 이로 인해 성능이 느려질까 걱정하는 사람도 있습니다. 하지만 함수가 짧으면 캐싱하기가 쉽기에 컴파일러가 최적화하는데 더 유리하다고 합니다.

이러한 짧은 함수는 이름을 잘 지어야 효과가 좋기 때문에 함수 이름에 신경을 써야 합니다.

절차

  1. 함수를 새로 만들고 목적을 잘 드러내는 이름을 붙임
  2. 추출할 코드를 원본 함수에서 복사하여 새 함수를 붙여넣음
  3. 추출한 코드 중 원본 함수의 지역변수를 참조하거나 추출한 함수의 유효 볌위를 벗어나는 변수는 없는지 검사함
  4. 변수를 다 처리했다면 컴파일함
  5. 원본 함수에서 추출한 코드 부분을 새로 만든 함수를 호출하는 문장으로 바꿈
  6. 테스트 진행
  7. 이와 같은 코드가 또 있는지 확인하고 함수를 추철할지 검토

함수 인라인하기

  • 리팩터링 전
def getRating(driver):
    return moreThenFiveLateDeliveries(driver) ? 2 : 1


def moreThenFiveLateDeliveries(driver):
    return driver.numberOfLateDeliveries &gt; 5
  • 리팩터링 후 ```python def getRating(driver): return (driver.numberOfLateDeliveries > 5) ? 2 : 1

def moreThenFiveLateDeliveries(driver): return driver.numberOfLateDeliveries > 5


## 배경 

해당 서적에서는 목적이 분명히 드러나는 이름의 짤막한 함수를 이용하기를 권합니다. 
그래야 코드가 명료해지고 이해하기 쉬워지기 때문입니다. 

리팩터링 과정에서 잘못 추출된 함수들을 다시 인라인해줍니다. 
잘못 추출된 함수들을 원래 함수로 합친다음 필요하면 원하는 형태로 다시 추출하는 것입니다. 

## 절차 

1. 다형 메서드인지 확인
  * 서브 클래스에서 오버라이드하는 메서드는 인라인하면 안됨 
2. 인라인할 함수를 호출하는 곳으로 모두 찾음 
3. 각 호출문을 함수 본문으로 교체 
4. 하나씩 교체할 때마다 테스트함 
5. 함수 정의를 삭제 


# 변수 추출하기 

* 리팩터링 전 

```python
return order.quantity * order.itemPrice - max(0, order.quantity-500) * order.itemPrice * 0.05 + min(order.quantity * order.itemPrice * 0.1, 100)
  • 리팩터링 후
basePrice = order.quantity * order.itemPrice 
quantityDiscount = max(0, order.quantity-500) * order.itemPrice * 0.05
shipping = min(order.quantity * order.itemPrice * 0.1, 100)
return basePrice - quantityDiscount  + shipping 

배경

표현식이 너무 복잡하면 이해하기가 어렵습니다. 이럴때는 지역변수를 활용하여 표현식을 쪼개면 관리하기 더 편하게 됩니다. 복잡한 로직을 구성하는 단계마다 이름을 붙여 코드의 목적을 더 명확하게 드러낼 수 있습니다.

또한 이렇게 추가된 변수들은 디버깅 과정에서 도움이 됩니다. breaking point를 지정할 수 있기 때문입니다.

절차

  1. 추출하려는 표현식에 부작용이 없는지 확인
  2. 불변 변수를 하나 선언하고 이름을 붙일 표현식의 복제본을 대입함
  3. 원본 표현식을 새로 만든 변수로 교체
  4. 테스트
  5. 표현식을 여러 곳에서 사용한다면 각각을 새로 만든 변수로 교체함

함수 선언 바꾸기

  • 리팩터링 전
    def circum(radius):
        ……
        return
  • 리팩터링 후
    def circumference(radius):
        ……
        return

배경

함수는 프로그램을 작은 부분으로 나누는 주된 수단입니다. 그렇기 때문에 함수의 이름은 매우 중요한 요소 중 하나입니다.

함수의 이름이 좋으면 함수의 구현 코드를 보지 않고 호출 부분만 봐도 무슨 일을 하는지 유추가 가능합니다.

함수의 매개변수도 마찬가지입니다. 매개변수는 함수를 사용하는 문맥을 설정하기 때문에 이름을 잘 정해주는 것이 좋습니다.

절차

함수 선언을 바꾸는 간단한 절차는 다음과 같습니다.

  1. 매개변수를 제거하려든 먼저 함수 본문에서 제거 대상 매개변수를 참조하는 곳이 없는지 확인한다.
  2. 매서드 선언을 원하는 형태로 바꾼다
  3. 기존 메서드 선언을 참조하는 부분을 모두 찾아서 바뀐 형태로 수정한다.
  4. 테스트한다.

변경할 게 둘 이상이면 나눠서 처리하는 편이 나을 때가 많습니다. 이의 경우 이름 변경과 매개변수 추가에 대해서 각각 독립적으로 처리하는 것이 좋습니다.

아래는 마이그레이션 절차입니다.

  1. 이어지는 추출 단계를 수월하게 만들어야 한다면 함수의 본문을 적절히 리팩터링한다.
  2. 함수 본문을 새로운 함수로 추출한다.
    • 새로 만들 함수 이름이 기존 함수와 같다면 일단 검색하기 쉬운 이름을 임시로 붙여둔다.
  3. 추출한 함수에 매개변수를 추가해야 한다면 ‘간단한 절차’를 따라 추가한다.
  4. 테스트한다.
  5. 기존 함수를 인라인한다.
  6. 이름을 임시로 붙여뒀다면 함수 선언 바꾸기를 한 번 더 적용해서 원래 이름으로 되돌린다.
  7. 테스트한다.

변수 캡슐화 하기

  • 리팩터링 전
defaultOwner = {firstName: &quot;마틴&quot;, lastName: &quot;파울러&quot;}
  • 리팩터링 후
defaultOwnerData = {firstName: &quot;마틴&quot;, lastName: &quot;파울러&quot;}

def defaultOwner():
    return defaultOwnerData

def setDefaultOwner(arg):
    global defaultOwnerData
    defaultOwnerData = arg

배경

데이터 캡슐화는 데이터를 변경하고 사용하는 코드를 감시할 수 있는 확실한 통로가 되어주기 때문에 데이터 변경 전 검증이나 변경 후 추가 로직을 쉽게 넣을 수 있습니다. 객체 지향에서 객체의 데이터를 항상 private로 유지하는 이유가 여기에 있습니다. public를 private로 변경하고 캡슐화하여 가시 범위를 제한하는 것이 좋습니다.

절차

  1. 변수로의 접근과 갱신을 전담하는 캡슐화 함수들을 만든다.
  2. 정적 검사를 수행한다.
  3. 변수를 직접 참조하던 부분을 모두 적절한 캡슐화 함수 호출로 바꾼다.
  4. 변수의 접근 범위를 제한한다.
  5. 테스트한다.

매개변수 객체 만들기

  • 리팩터링 전

def amountInvoiced(startDate, endDate) :
    return

def amountInvoiced(startDate, endDate) :
    return

def amountInvoiced(startDate, endDate) :
    return
  • 리팩터링 후
def amountInvoiced(aDateRange) :
    return

def amountInvoiced(aDateRange) :
    return

def amountInvoiced(aDateRange) :
    return

배경

데이터 항목 여러개가 이 함수 저 함수에 함께 사용되는 경우를 종종 보게됩니다. 이런 경우에 데이터 구조 하나로 모아주는 것이 보기 좋습니다. 데이터 뭉치를 데이터 구조로 묶으면 데이터 사이의 관계가 명확해진다는 이점이 있습니다.

절차

  1. 적당한 데이터 구조가 아직 없다면 새로 만든다
  2. 테스트한다.
  3. 함수 선언 바꾸기 로 새 데이터 구조를 매개변수로 추가한다.
  4. 테스트한다.
  5. 함수 호출 시 새로운 인스턴스를 넘기도록 수정한다.
  6. 기존 매개변수를 사용하던 코드를 새 데이터 구조의 원소를 사용하도록 바꾼다.
  7. 다 바꿨다면 기존 매개변수를 제거하고 테스트한다.

</description> Tue, 20 Sep 2022 00:00:00 +0000 https://sunghwan7330.github.io/refactoring_2/refactoring_2_2_refactoring_base/</link> https://sunghwan7330.github.io/refactoring_2/refactoring_2_2_refactoring_base/

    <category>refactoring</category>
    
    
    <category>refactoring_2</category>
    
  </item>

  <item>
    <title>1장 리팩터링 첫 예시</title>
    <description># 개요 

해당 장에서는 예시 코드를 리팩터링 하면서 리팩터링의 중요성을 파악해보려 합니다.

해당 서적에서는 자바스크립트를 예시로 설명합니다.

저는 자바스크립트를 사용할 일이 없기 때문에 예제 코드를 파이썬으로 변환하여 설명드리려 합니다.

자바스크립트를 사용하시는 분들은 책을 직접 보시는 것도 추천드립니다. (유명한 서적기에)

예시는 아래의 레퍼지토리에 업로드하였습니다.

  • https://github.com/Sunghwan7330/my_study/tree/master/refactoring_example/python

예시 코드

예시 코드는 다양한 연극을 외주로 받아 공연하는 극단에서 공연 요청이 들어왔을 때 공연 청구서를 출력하는 예시입니다. 현재 이 극단은 비극과 희극만 공연하고, 공연료와 별개로 포인트를 지급하여 다음번 의뢰시 공연 할인을 받을 수 있도록 하려 합니다.

서적에서는 json 파일로 데이터를 입력하였는데, 저는 파이썬 코드로 입력하였습니다. palys에는 극단 정보, invoices.json 에는 공연료 청구서 내용을 입력하였습니다. 이후 statement 함수에서 청구서 내용을 return 받도록 하였습니다.

def main():
    invoice = {}
    invoice = {
        &quot;customer&quot;: &quot;BigCo&quot;,
        &quot;performances&quot;: [
            {
                &quot;playID&quot;: &quot;hamlet&quot;,
                &quot;audience&quot;: 55
            },
            {
                &quot;playID&quot;: &quot;as-like&quot;,
                &quot;audience&quot;: 35
            },
            {
                &quot;playID&quot;: &quot;othello&quot;,
                &quot;audience&quot;: 40
            }
        ]
    }
    plays = {
            &quot;hamlet&quot;: {&quot;name&quot;: &quot;Hamlet&quot;, &quot;type&quot;: &quot;tragedy&quot;},
            &quot;as-like&quot;: {&quot;name&quot;: &quot;As You Like It&quot;, &quot;type&quot;: &quot;comedy&quot;},
            &quot;othello&quot;: {&quot;name&quot;: &quot;Othello&quot;, &quot;type&quot;: &quot;tragedy&quot;}
    }
    print(statement(invoice, plays))
    return

공연료를 계산하여 반환하는 statement 함수는 아래와 같습니다.

def statement(invoice, plays):
    totalAmount = 0
    volumeCredits = 0
    result = '청구 내역 (고객명: {})\n'.format(invoice['customer'])
    creditFormat = ',.2f'
    for perf in invoice['performances']:
        play = plays[perf['playID']]
        thisAmount = 0
        if play['type'] == 'tragedy': #비극
            thisAmount = 40000;
            if perf['audience'] &gt; 30:
                thisAmount += 1000 * (perf['audience'] - 30)
        elif play['type'] == &quot;comedy&quot;: #비극
            thisAmount = 30000
            if perf['audience'] &gt; 20:
                thisAmount += 10000 + 500 * (perf['audience'] - 20)
            thisAmount += 300 * perf['audience'];
        else:
            return '알 수 없는 장르: %s' % play['type']

        # 포인트를 적립한다.
        volumeCredits += max(perf['audience'] - 30, 0);

        # 희극 관객 5명마다 추가 포인트를 제공한다.
        if &quot;comedy&quot; == play['type']:
            volumeCredits += perf['audience'] // 5
        # 청구 내역을 출력한다.
        result += ' {}: ${} ({}석)\n'.format(play['name'], format(thisAmount//100, creditFormat), perf['audience'])
        totalAmount += thisAmount;

    result += '총액: ${}\n'.format(format(totalAmount//100, creditFormat));
    result += '적립 포인트: {}점\n'.format(volumeCredits);
    return result;

이 코드의 실행 결과는 아래와 같습니다.

청구 내역 (고객명: BigCo)
 Hamlet: $650.00 (55석)
 As You Like It: $580.00 (35석)
 Othello: $500.00 (40석)
총액: $1,730.00
적립 포인트: 47점

statement 함수 쪼개기

amountFor 함수로 분리

위 코드에서 장르별로 나눈 if문이 눈에 띕니다. (원 코드에서는 switch 코드)

이러한 점은 코드를 분석하여 얻은 정보입니다. 그렇기 때문에 다음에 다시 볼때 내용을 잊고 다시 분석하게 될 가능성이 높습니다. 이러한 부분을 함수로 따로 추출해줍니다.

def amountFor(aPerformance, play): # 값이 바뀌지 않는 변수는 파라메타로 전달
    result = 0  # 명확한 이름으로 변경
 
    if play['type'] == 'tragedy': #비극
        result = 40000
        if aPerformance['audience'] &gt; 30:
            result += 1000 * (aPerformance['audience'] - 30)
    elif play['type'] == &quot;comedy&quot;: #비극
        result = 30000
        if aPerformance['audience'] &gt; 20:
            result += 10000 + 500 * (aPerformance['audience'] - 20)
        result += 300 * aPerformance['audience']
    else:
        return -1
   
    return result  # 함수 안에서 값이 바뀌는 변수 반환
 

기존함수의 내용을 복사하여 amountFor 이라는 새로운 함수를 생성하였습니다.

함수를 조금 더 직관적으로 보이게 하기 위해 아래의 수정을 반영하였습니다.

  • thisAmount 변수를 reslut 로 변경
  • perf를 aPerformance 로 변경

play 변수 제거

다음으로는 play 변수를 제거하려 합니다. playFor 함수를 생성해줍니다. 해당 함수는 aPerformance 를 입력받아 해당 play를 반환하는 함수입니다.

def playFor(aPerformance):
    plays = {
    &quot;hamlet&quot;: {&quot;name&quot;: &quot;Hamlet&quot;, &quot;type&quot;: &quot;tragedy&quot;},
    &quot;as-like&quot;: {&quot;name&quot;: &quot;As You Like It&quot;, &quot;type&quot;: &quot;comedy&quot;},
    &quot;othello&quot;: {&quot;name&quot;: &quot;Othello&quot;, &quot;type&quot;: &quot;tragedy&quot;}
    }
    return plays[aPerformance['playID']]

위 함수가 있다면 statement와 amonutFor 에서는 play가 필요없습니다. 따라서 해당 함수에서 play를 playFor 함수로 변경해줍니다.

def amountFor(aPerformance): # 값이 바뀌지 않는 변수는 파라메타로 전달
    result = 0  # 명확한 이름으로 변경
 
    type = playFor(aPerformance)['type']
    if type == 'tragedy': #비극
        result = 40000
        if aPerformance['audience'] &gt; 30:
            result += 1000 * (aPerformance['audience'] - 30)
    elif type == &quot;comedy&quot;: #비극
        result = 30000
        if aPerformance['audience'] &gt; 20:
            result += 10000 + 500 * (aPerformance['audience'] - 20)
        result += 300 * aPerformance['audience']
    else:
        return -1
   
    return result  # 함수 안에서 값이 바뀌는 변수 반환
 
 
def statement(invoice):
    totalAmount = 0
    volumeCredits = 0
    result = '청구 내역 (고객명: {})\n'.format(invoice['customer'])
    creditFormat = ',.2f'
    for perf in invoice['performances']:
        thisAmount = amountFor(perf)
 
        # 포인트를 적립한다.
        volumeCredits += max(perf['audience'] - 30, 0)
 
        # 희극 관객 5명마다 추가 포인트를 제공한다.
        if &quot;comedy&quot; == playFor(perf)['type']:
            volumeCredits += perf['audience'] // 5
        # 청구 내역을 출력한다.
        result += ' {}: ${} ({}석)\n'.format(playFor(perf)['name'], format(thisAmount//100, creditFormat), perf['audience'])
        totalAmount += thisAmount
 
    result += '총액: ${}\n'.format(format(totalAmount//100, creditFormat))
    result += '적립 포인트: {}점\n'.format(volumeCredits)
    return result

적립 포인트 계산 코드 추출하기

def volumeCreditsFor(perf):
    result = 0
 
    result += max(perf['audience'] - 30, 0)
    # 희극 관객 5명마다 추가 포인트를 제공한다.
    if &quot;comedy&quot; == playFor(perf)['type']:
        result += perf['audience'] // 5
 
    return result

포인트 계산하는 부분을 함수로 추출해줍니다.

format 함수로 추출하기

def mFormat(n):
    return format(n, ',.2f')

금액을 출력하는 부분에 사용된는 format 를 함수로 추출해줍니다.

statement 함수에서 volumeCredits 제거하기

def volumeCreditsFor(perf):
    result = 0
 
    result += max(perf['audience'] - 30, 0)
    # 희극 관객 5명마다 추가 포인트를 제공한다.
    if &quot;comedy&quot; == playFor(perf)['type']:
        result += perf['audience'] // 5
 
    return result

총 포인트를 계산하는 함수를 따로 추출해줍니다.

totalAmount 함수 추출

def totalAmount(invoice):
    result = 0
    for perf in invoice['performances']:
        result += amountFor(perf)
 
    return result

totalAmount 부분도 함수로 추출해줍니다.

지금까지의 전체 코드

# 리팩터링 2판 1장 예시 7
# totalAmount 함수 분리
# amountFor 인라이닝하기
 
def playFor(aPerformance):
    plays = {
    &quot;hamlet&quot;: {&quot;name&quot;: &quot;Hamlet&quot;, &quot;type&quot;: &quot;tragedy&quot;},
    &quot;as-like&quot;: {&quot;name&quot;: &quot;As You Like It&quot;, &quot;type&quot;: &quot;comedy&quot;},
    &quot;othello&quot;: {&quot;name&quot;: &quot;Othello&quot;, &quot;type&quot;: &quot;tragedy&quot;}
    }
    return plays[aPerformance['playID']]
 
def mFormat(n):
    return format(n, ',.2f')
 
def amountFor(aPerformance): # 값이 바뀌지 않는 변수는 파라메타로 전달
    result = 0  # 명확한 이름으로 변경
 
    type = playFor(aPerformance)['type']
    if type == 'tragedy': #비극
        result = 40000
        if aPerformance['audience'] &gt; 30:
            result += 1000 * (aPerformance['audience'] - 30)
    elif type == &quot;comedy&quot;: #희극
        result = 30000
        if aPerformance['audience'] &gt; 20:
            result += 10000 + 500 * (aPerformance['audience'] - 20)
        result += 300 * aPerformance['audience']
    else:
        return -1
   
    return result  # 함수 안에서 값이 바뀌는 변수 반환
 
 
def volumeCreditsFor(perf):
    result = 0
 
    result += max(perf['audience'] - 30, 0)
    # 희극 관객 5명마다 추가 포인트를 제공한다.
    if &quot;comedy&quot; == playFor(perf)['type']:
        result += perf['audience'] // 5
 
    return result
 
 
def toalVolumCrdits(invoice):
    result = 0
 
    for perf in invoice['performances']:
        result += volumeCreditsFor(perf)
 
    return result
 
 
def getTotalAmount(invoice):
    result = 0
    for perf in invoice['performances']:
        result += amountFor(perf)
 
    return result
 
def statement(invoice):
   
    result = '청구 내역 (고객명: {})\n'.format(invoice['customer'])
    for perf in invoice['performances']:    
        # 청구 내역을 출력한다.
        result += ' {}: ${} ({}석)\n'.format(playFor(perf)['name'], mFormat(amountFor(perf)//100), perf['audience'])
    totalAmount = getTotalAmount(invoice)
    volumeCredits = toalVolumCrdits(invoice)
    result += '총액: ${}\n'.format(mFormat(totalAmount//100))
    result += '적립 포인트: {}점\n'.format(volumeCredits)
    return result
 
def main():
    invoice = {
        &quot;customer&quot;: &quot;BigCo&quot;,
        &quot;performances&quot;: [
            {
                &quot;playID&quot;: &quot;hamlet&quot;,
                &quot;audience&quot;: 55
            },
            {
                &quot;playID&quot;: &quot;as-like&quot;,
                &quot;audience&quot;: 35
            },
            {
                &quot;playID&quot;: &quot;othello&quot;,
                &quot;audience&quot;: 40
            }
        ]
    }
 
    print(statement(invoice))
    return
 
if __name__ == '__main__':
    main()
 

마무리

현재까지의 리팩터링으로 처음보다 훨씬 보기좋게 변하였습니다.

참고한 서적에서는 위 상태에 조금 더 리펙터링을 진행합니다. 좀 더 보고싶으신 분들은 나중에 서점에서 한번 확인해보시길 바랍니다. :) </description> Tue, 06 Sep 2022 00:00:00 +0000 https://sunghwan7330.github.io/refactoring_2/refactoring_2_1_example/</link> https://sunghwan7330.github.io/refactoring_2/refactoring_2_1_example/

    <category>refactoring</category>
    
    
    <category>refactoring_2</category>
    
  </item>

  <item>
    <title>DRDoS 살펴보기</title>
    <description># 개요 

직장에서 업무 중 DRDoS 에 대한 조사가 필요하여 자료를 검색하게 되었습니다. 검색한 내용을 정리하여 포스팅을 진행해보려 합니다.

DRDoS

DRDoS는 Distributed Reflection Denial of Service 의 약자로 분산 반사 서비스 거부 공격이라는 의미입니다.

대역폭을 점유하는 서비스 거부공격이라는 점은 DDoS와 동일합니다.

다만 DRDoS는 반사체를 이용하여 서비스 거부 공격을 수행한다는 특징이 있습니다.

DDoS와 DRDoS 의 차이

DDoS

DDoS 의 경우 일반적으로 대량의 공격 PC 를 이용하여 대상자에게 서비스 거부 공격을 수행합니다. 공격을 하는 PC가 대상자를 향해 직접 트래픽을 전송하게 됩니다

image

대상자에게 많은 트래픽을 전송하기 위해 많은 공격 PC를 필요로 합니다. 일반적으로는 봇넷, 좀비PC를 이용하여 공격을 수행합니다.

DRDoS

DRDoS는 외부의 정상적인 서버(반사체)를 이용하여 공격을 수행합니다.

image

DRDoS의 공격은 실제 서비스되고 있는 서버를 이용하기 때문에 공격의 추적이 어렵습니다.

반사체를 이용한 DRDoS 원리

해당 장에서는 편의상 DNS 서비스로 예시를 진행하겠습니다.

DNS 요청 / 응답

DNS 는 Domain Name Server 의 약자입니다. DNS 서버는 인터넷 주소를 IP로 변환해주는 역할을 하게됩니다.

예를 들어 인터넷 브라우저 주소창에 인터넷 주소를 입력하게 되면 DNS 를 통해 IP 주소로 바꿔주게 됩니다.

image

IP spoofing

IP spoofing 은 IP를 속이는 공격방법입니다.

우리가 인터넷을 사용할 때 일반적으로는 IP 기반의 통신을 하게 됩니다. 이때 주고받는 네트워크 패킷에는 IP header를 포함하고 있습니다.

image

위에 보이는 이미지가 IPv4 헤더의 구조입니다. 여기서 Source IP는 보내는 대상의 IP 가 입력되고, Destination IP는 받는 대상의 IP가 입력됩니다.

IP spoofing 은 source IP를 변조하여 보내는 대상의 IP를 숨기는 공격방법입니다. 이렇게 하면 받는 대상자에게 보낸 대상자를 속일 수 있게 됩니다.

반사체를 이용한 서비스거부 공격

위에서 DNS 서버의 특징과 IP spoofing 에 대해 간단히 알아보았습니다. 이제 DRDoS가 반사체를 이용하여 어떻게 공격하는지 감이 오실껍니다.

DNS 서버는 요청에 대한 응답을 보내주게 됩니다. IP spoofing 을 통해 source IP를 변경하게 된다면….?? DNS 서버는 응답을 변경된 IP로 전달하게 됩니다.

여러 DNS 서버에 IP spoofing 을 사용하여 요청을 보내게 된다면, DNS 서버를 이용한 DoS 공격이 가능해집니다. 이떄 응답을 보내는 DNS 서버를 반사체라고 이해하시면 됩니다.

image

DRDoS 증폭 공격

앞에서 DRDoS 의 공격 방법에 대해서 알아보았니다.

DRDoS의 경우 반사체를 이용해 트래픽을 발생시키는 공격입니다. 이때 공격을 효율적으로 하기 위해서는 한번의 요청으로 많은 응답을 받도록 해야합니다. 즉, 공격자는 조금의 트래픽만을 사용해서 많은 양의 트래픽을 유도하게 해야합니다.

이와 같이 프로토콜의 특징을 이용하여 최대한의 서비스 응답을 받도록 하여 트래픽을 집중시키는 것을 증폭공격이라고 합니다.

TCP는 handshake를 통한 연결 과정이 있기 때문에 일반적으로는 UDP 서비스를 이용하여 공격합니다. DRDoS 증폭공격에 주로 사용되는 프로토콜은 DNS, NTP, CLDAP, SNMP 등이 있습니다.

아래는 각 프로토콜별로 요청에 대하여 응답이 얼마나 많이 증폭되는지에 대한 비율을 나타냅니다.

image

DRDoS 방어 기법

해당 절 부터는 저의 주관적인 의견이 있음을 알려드립니다.

UDP fragment 패킷 탐지

DRDoS 공격자는 대부분 Amplification attack 을 사용합니다. 응답 패킷을 최대한 증폭시켜 보내개 됩니다.

그렇기 때문에 대부분의 패킷은 fragment 형태로 전송되게 됩니다.

하나의 대상으로 많은 fragment 패킷이 유입된다면 DRDoS 공격을 의심할 수 있습니다.

하나의 서버에서만 응답한다면 일반적인 사용으로 판단할 수 있겠지만, 다수의 서버에서 큰 사이즈의 응답이 지속된다면 공격으로 볼 수 있습니다. 이때 해당 IP를 잠시 차단하여 공격을 막을 수 있습니다.

UDP 세션 관리를 이용한 탐지

일반적은 서버와 클라이언트의 통신은 요청과 응답으로 이뤄집니다. 하지만 DRDoS 공격의 경우 응답만 전송되게 됩니다.

UDP 세션 관리를 통해 요청이 없이 응답만 오는 경우 공격으로 탐지할 수 있습니다.

요청이 없는데 서버로부터 응답이 지속된다면 해당 서버의 IP를 일시적으로 차단하여 공격을 방어할 수 있습니다.

프로토콜 특징을 이용한 탐지 방법

해당 방법은 각 프로토콜의 응답을 분석하여 공격을 탐지하는 방법입니다.

이 방법은 구글에 검색하면 많은 논문들이 있습니다.

주로 DNS 또는 NTP를 이용한 Amplication attack 에 대한 탐지 방법이 있으며 방법도 다양합니다. 관심 있다면 한번 찾아보시는 것을 추천드립니다. </description> Mon, 15 Aug 2022 00:00:00 +0000 https://sunghwan7330.github.io/infomation/info_drdos/</link> https://sunghwan7330.github.io/infomation/info_drdos/

    <category>DoS</category>
    
    <category>DRDoS</category>
    
    <category>UDP reflection attack</category>
    
    <category>UDP Amplification attack</category>
    
    
    <category>infomation</category>
    
  </item>

  <item>
    <title>[임베디드 C를 윈한 TDD] 13장 레거시 코드에 테스트 추가하기</title>
    <description># 개요 

6장을 포스팅한 뒤 갑자기 13장을 포스팅하게 되었습니다.

앞부분에서 TDD 가 무엇인지를 파악한 뒤 필요한 부분 부터 보려고 하다 보니까 13장을 우선적으로 보게 되었습니다.

레거시 코드 변경 정책

  • 새로운 코드는 테스트 주도로 개발한다
  • 레거시 코드를 수정하기 전에 테스트를 추가한다
  • 레거시 코드에서 변경되는 것들은 테스트 주도로 개발한다

보이스카우트 원칙

보이스카우트는 캠프를 떠날 때는 처음 왔을 때보다 더 깨끗해야 한다 라는 단순한 원칙을 따릅니다. 즉, 수정한 부분에 대해서는 최대한 깔끔하게 코드를 깔끔하게 작성해야 한다는 의미입니다.

긴 함수에 추가하기

긴 함수는 따로 뽑아내어 이름을 붙일만한 내용이 많을 것입니다. 예시로 긴 함수에 새로 세줄을 추가하고자 한다면 다섯줄을 추출하도록 해보겠습니다. 결과적으로 이 함수는 두줄이 짧아집니다.

단, 기존 동작을 보존하기 위한 테스트는 먼저 추가되어야 합니다.

복잡한 조건식에 추가하기

조건식을 추출하여 이름을 잘 붙인 도움 함수로 만드는 방법입니다.

새로 추가한 코드의 조건식이 다른 모듈에도 포함되는 경우가 많은데 이는 함수로 이동해야 마땅합니다.

암호 같은 지역변수 이름

일단 변수의 목적을 이해했다면, 다음에 이 코드를 보게 될 여러분과 팀원들을 위해 이름을 변경하는 것이 좋습니다.

과도한 중첩

단계 깊이의 중첩을 도움 함수로 변경하는 것이 좋습니다. 조건문이 중첩되어 있으면 보호절 형태로 바꾸어 중첩의 깊이를 낮추는 것이 좋습니다.

레거시 코드 변경 알고리즘

마이클은 레거시 코드 변경 알고리즘을 아래와 같이 정의한다.

  1. 변경점을 식별한다.
  2. 테스트 포인트를 찾는다.
  3. 의존성을 깨뜨린다.
  4. 테스트를 작성한다.
  5. 변경하고 리팩터링한다.

1. 변경점 식별

첫 단계는 현재 코드에서 변경이 필요하다고 생각하는 부분을 찾아야 합니다.

2. 테스트 포인트 찾기

변경점이 식별되었으면, 이것을 어떻게 테스트할지 고민해야 합니다. 해당 코드가 어디서 입력을 받는지, 상황을 감시하기 위한 지점이 어디인지 살펴봐야 합니다.

3. 의존성 깨뜨리기 (혹은 그러지 않기)

레거시 코드를 테스트 하니스에 붙이거나 일부 테스트 포인트에 접근하기 위해서는 의존성을 깨트려야 합니다.

테스트 대역을 사용하려면 먼저 안전한 함수 추출 리팩터링을 적용할 필요가 있습니다. 레거시 코드 수정시 이는 좀 더 주의할 필요가 있습니다. 동료와 함께 작업하는 것이 실수를 방지하는 데에 도움이 될 것입니다.

전역 데이터에 대한 의존성을 깨트리기 위해서는 문제의 전역 데이터를 접근할 때 접근 함수를 통하도록 캡슐화 할 수 있습니다. 그런다음 테스트를 하는 동안 접근자를 오버라이드 하는 벙법으로 전역 데이터에 대한 통제권을 확보할 수 있습니다.

4. 테스트 작성

테스트 포인트를 찾았다면 레거시 코드의 현재 동작을 보존하면서 그 특징이 드러나는 테스트를 작성해야 합니다. 이는 매우 까다로우며, 특히 테스트 하니스를 처음 연결한다면 더더욱 그럴 것입니다. 이 부분은 아래 ‘부딪혀가며 통과하기’ 에서 다시 살펴보겠습니다.

5. 변경하고 리팩터링

기존 동작을 보존해주는 테스트가 마련되었으면, 리팩터링을 적용할 수 있는 안전이 확보되었습니다. 리팩터링을 통해 변경하기 쉽도록 코드를 재구성합니다. 래거시 동작이 테스트를 통해 안정적으로 유지되면 새로운 동작을 TDD로 작성하기 수월해집니다.

테스트 포인트

코드가 하는 일을 우리가 제대로 이해하고 있는지 확인하기 위해서는 테스트 포인 트가 필요합니다. 테스트 포인트는 종류에 따라서 비교적 쉽게 확보할 수 있는 것들도 있습니다.

봉합

함수 호출은 코드의 다른 부분들 사이에 봉합(seam)을 형성합니다. 이런 봉합이 테스트 포인트로는 좋습니다. 봉합을 통해서 우리는 테스트 대상 코드가 하는 일을 살펴 보고 동작에 영향을 미칠 수 있습니다.

봉합 지점에 테스트 대역을 투입하여 다른 모듈로 전달되는 데이터를 감시할 수 있습니다. 함수 호출 봉합에서 테스트 대역이 값을 반환하는 방식으로 테스트 대상 코드에 간접 입력들 제공할 수도 있습니다.

감지 변수

감지 변수는 접근이 어려운 데이터나 길이가 긴 함수에서 중간 결과를 확인하는데 유용합니다.

리팩터링이 안된 매우 긴 함수가 있다고 가정해보겠습니다. 여러분은 어디를 변경해야 할지 알고 있지만 구조적인 변경부터 시작하기에는 위험 부담이 있습니다. 이런 경우 전역변수 형태의 감지 변수를 한두 개 추가하여 위험 부담을 줄일 수 있습니다. 감지 변수는 길게 이어지는 계산 과정의 중간 값이나 상태 변수의 값을 조사하는데 사용하거나 반복문의 반복 횟수를 셀 때도 사용할 수 있습니다.

테스트 목적으로 전역변수를 추가하는 아이디어에 대해 부정적일 수 있습니다. 하지만 이러한 감지변수를 도입하는 것은 긴 함수를 풀어헤치는 중간 과정으로 생각하면 됩니다. 만약 TDD로 코드를 개발하였다면, 감지 변수 대신 함수 봉합을 이용할 수 있을 것입니다.

##디버그 출력 감지 포인트

개발 후 디버깅을 하다보면 코드 여기저기에서 디버그를 출력 함수를 호출하는 경우가 많습니다. 디버그 출력은 보통 옵션을 통해 켜고 끌 수 있습니다. 이러한 디버그 출력 장치를 ‘디버그 출력 감지 포인트’ 로 이용하여 접근이 쉽지 않은 코드의 동작에 대한 정보를 얻을 수 있습니다. 이러한 방법으로 디버그 출력 대신 테스트를 통해 모니터링을 할 수 있습니다.

테스트 주도로 버그 수정하기

버그를 수정할 때에도 테스트가 필요합니다.

만약 버그를 수정하면서 단위테스트를 추가할 수 있다면 추가하는 것이 좋습니다. 버그를 수정하면서 다른 버그를 만들지 않고 싶을 것입니다. 그렇기 떄문에 올바른 동작을 고정시키기 위해 테스트를 작성해야 하는 것을 의미합니다.

버그를 잡으면서 테스트를 추가하는데 드는 비용이 늘엇을 때 앞으로 생기는 이슈에 대한 비용이 감소한다고 생각하면 좋습니다. 결함을 제대로 수정하였을 때 추가되는 비용은 많지 않습니다. </description> Tue, 09 Aug 2022 00:00:00 +0000 https://sunghwan7330.github.io/embedded_c_tdd/embedded_c_tdd_13/</link> https://sunghwan7330.github.io/embedded_c_tdd/embedded_c_tdd_13/

    <category>임베디드</category>
    
    <category>TDD</category>
    
    
    <category>embedded_c_tdd</category>
    
  </item>

  <item>
    <title>[임베디드 C를 윈한 TDD] 6장 좋아 하지만...</title>
    <description># 개요 

본 장에서는 서적의 6장 내용을 정리하였습니다.

TDD를 처음 하면 몇몇 걱정이 생기게 됩니다. 이번 장에서는 TDD를 하면서 드는 의문에 대해 설명해주고 있습니다.

우린 시간이 없어요

개발을 하다보면 시간에 시간이 촉박하기 마련입니다. Led 드라이버를 보면 제품 코드보다 테스트 코드가 더 깁니다.

TDD를 실천하는 많은 사람들은 TDD가 업무 속도를 향상시킨다고 합니다. 속도 향상은 미래의 디버그 시간이 줄어들고, 실행 가능한 문서 역할을 하는 테스트 코드를 통해 더 깔끔하게 유지되기 때문입니다.

만약 TDD 가 시간이 더 걸린다면 어떨까요? 하지만 개발 소요시간 외에도 여러가지 비용이 있습니다. 예를 들면 고객 불만족, 판매 손실, 보증 수리, 결함 관리, 고객 서비스 등등… 이 있겠습니다. 시간을 조금 더 쓰더라도 더 적은 결함을 가진 제품을 내놓는 것이 더 가치가 있을 것입니다.

TDD를 하기 위해 시간을 찾으려면 먼저 현재 일하는 방식을 들여다봐야 할 것입니다.

수동 테스트

만약 현재 수동으로 단위 테스트를 하고 있다면, 이 시간의 일부를 TDD에 활용하도록 하는 것이 좋습니다. 레거시 환경에서 작업하고 있다면 수동 테스트를 완전 배제할 수는 없지만, 새로운 코드를 TDD로 개발하거나, 테스트되지 않은 레거시 코드의 일부에 대해 테스트를 작성할 수도 있을 것입니다.

수동 테스트의 초기 투자 테스트는 자동화보다 적을지 모르지만, 지속적이지 않습니다. 미래로 가면 거의 제로에 가까울 수 있습니다. 필요한 테스트를 재수행하지 않으면 테스트가 빨리 끝나서 좋겠지만, 향후 버그에 대한 비용은 감수해야 할 것입니다.

지속적인 테스트 하니스

우리는 가끔 새로 작성하는 코드에 대해서 테스트 main() 과 테스트 스텁을 몇몇 작성해 보았을 것입니다. 이는 자체적인 테스트 하니스를 만든 것으로 볼 수 있습니다.

이러한 테스트들은 매우 유익합니다. 코드의 품질이 향상되며, 더욱 더 잘 동작하는 코드를 제품에 통합시킬 수 있습니다.

하지만 코드가 통합되었을 때 기존의 테스트들이 방치되는 경우가 많은데, 이를 테스트 하니스에 넣어 지속적으로 유지하는 것이 좋습니다.

한 스텝씩 실행하는 단위 테스트

또 다른 수동 단위 테스트 방법으로 디버거를 사용해서 대상 코드를 한 스텝씩 실행하는 것입니다. 이 방법은 느리기도 하며, 반복하기 힘든 작업입니다.

이러한 테스트의 유효 수명은 위에서 언급한 main() 보다 짧습니다. 일부가 변경되면 이전의 테스트는 무용지물이 될 수 있습니다. 그러면 결국 처음부터 다시 테스트를 하게 됩니다. 수동 테스트에 드는 노력은 시간이 지나면 점점 증가하게 됩니다.

만약 시간이 없어서 모든 테스트를 할 수 없다면 버그가 발생하고, 향후에 드는 노력도 증가합니다.

단위 테스트에 들인 비용은 어디로 가는가?

TDD를 하지 않았다면 대부분 수동 테스트를 하거나 main() 함수 로 테스트를 할 것입니다. 이러한 활동들은 실행 비용은 엄청나지만, 돌려받는 것은 극히 한정적입니다.

이러한 방법들은 단위테스트에 직접 비용을 들이고, 디버깅을 하면서 간접 비용이 듭니다. 현재 단위 테스트에 들이는 비용 중 일부를 TDD에 들이는 것을 고려하는 것이 좋습니다. TDD에서는 변경이 있을 때 마다 테스트를 실행하고, 코드와 함께 테스트도 발전하기 때문에 투자에 대한 이득을 훨씬 많이 얻게 됩니다.

코드 작성 후에 테스트를 작성하면 왜 안되나?

일반적으로는 개발 후에 테스트를 작성합니다. 이를 개발 후 테스트 (Test-After Development) 라고 합니다. 테스트를 나중에 작성하는 것도 효과가 있긴 하지만, 테스트를 주도로 코드를 작성하는 것에는 미치지 못합니다.

다음은 TAD로는 얻을 수 없는 TDD 의 이득에 대한 몇가지 예입니다.

  • TDD는 설계에 영향을 미치게 된다. 테스트를 나중에 작성하는 경우에는 TDD가 설계에 미치는 긍정적 효과를 얻을 수 없다. TDD는 더 나은 API와 낮은 결합도, 높은 응집도를 가져온다.
  • TDD는 결함을 방지한다. 작은 실수를 하면 TDD는 바로 그것을 찾아낸다. 테스트를 나중에 작성할 때도 많은 실수를 찾아낼 수 있겠지만, TDD라면 발견할 수 있는 것들의 일부는 놓칠 수 있다. 이런 실수들은 결국 여러분의 버그 데이터베이스를 채울 것이다.
  • 테스트를 나중에 작성하면 테스트 실패의 근본 원인을 찾는 데 소중한 시간을 낭비하게 된다. 하지만 TDD에서는 보통 근본 원인이 명백하게 드러난다.
  • TDD가 더 엄격하고 테스트 커버리지도 높다. 테스트 커버리지가 TDD의 목적 은 아니지만 테스트를 나중에 작성하면 테스트 커버리지가 낮아진다.

테스트를 유지보수해야 할 것이다

테스트가 없다면 유지보수할 필요도 없겠지만, 대신 지루한 수동 테스트를 반복해야만 할 것입니다. 테스트로 인해 얻게 되는 가치는 유지 보수에 들어가는 노력을 보상할 것입니다.

TDD 와 테스트 케이스 설계 기술을 익히고 나면 유지보수 하는 일은 어렵지 않을 것입니다.

단위 테스트가 모든 버그를 찾아낼 수는 없다

사실이긴 하지만, 이것이 TDD를 하지 않을 이유는 아닙니다. TDD는 코드 한줄한줄이 원하는대로 동작한다는 것을 보장할 수 있도록 도와줍니다.

여전히 통합 테스트, 인수 테스트, 부하 테스트 등이 필요합니다.

TDD로 인해 문제들이 많이 제거되며, 상위 수준의 테스트에 걸맞는 문제를 찾게 됩니다. 단위 테스트는 변경이 일어날 때에 효과가 드러나게 됩니다. TDD 단위 테스트를 통해 변경이 정확하게 의도한대로 동작하는지를 검증할 수 있습니다.

사소한 실수가 크나큰 버그로 다가올 수 있습니다. 실수가 작든 크든 실수 방지를 위해서 TDD는 아주 효과적입니다.

빌드가 오래 걸린다

빌드 시간이 오래 걸린다면 증분 빌드 시간을 줄이기 위해 단위 테스트 빌드를 여러 개로 나눌 필요가 있습니다. 각 테스트를 필요한 모듈로 이동한 뒤 병렬처리를 통해 빌드를 수행하면 빌드 시간을 단축할 수 있습니다.

기존 코드는 어떻게?

아마 대부분의 개발자들은 현업에서 개발하고 있는 기존 코드들이 있을 것입니다. 또한 대부분은 자동화 테스트가 적거나 없을것입니다. 아마 이 상황에서 TDD를 바로 적용하기는 쉽지 않습니다. 이 경우 개발을 멈추면서 모든 테스트를 작성하는 것은 실용적이지 않을 것입니다.

기존 제품에 테스트를 추가하면서 TDD를 적용해나가는 기법들은 아래와 같습니다.

  • 새로운 함수에 모듈과 TDD 사용하기
  • 기존 코드를 변경할 때 테스트 추가하기
  • 버그를 수정할 때 테스트 추가하기
  • 미래를 위한 전략적 테스트들에 투자하기

이 주제는 13장인 레거시 코드에 테스트 추가하기 에서 상세하게 다룰 예정입니다. </description> Tue, 02 Aug 2022 00:00:00 +0000 https://sunghwan7330.github.io/embedded_c_tdd/embedded_c_tdd_06/</link> https://sunghwan7330.github.io/embedded_c_tdd/embedded_c_tdd_06/

    <category>임베디드</category>
    
    <category>TDD</category>
    
    
    <category>embedded_c_tdd</category>
    
  </item>

  <item>
    <title>우아한 ATDD</title>
    <description># 개요 

이번 포스팅은 우아한 Tech 채널에서 강의한 우아한 ATDD 에 대한 내용을 정리하려 합니다.

  • https://youtu.be/ITVpmjM4mUE

이번 포스팅은 개조식으로 정리하겠습니다.

대부분은 강의자료를 가져왔습니다.

인수테스트 첫 만남

  • 해당 절에서는 인수테스트를 접했을 떄의 느낌을 정리함

새로운 팀 새로운 문화

페어 프로그래밍

  • 즉각적인 피드백 받을 수 있었음

테스트

  • 테스트 코드로 부터 받는 피드백
  • 심리적인 안정감을 제공함

인수 테스트

  • 사용자 스토리(시나리오) 기반으로 기능 테스트

인수 테스트의 도움

  • 배표 없이 받는 빠른 피드백
  • 새로운 팀의 도메인과 서비스 흐름 파악에 큰 도움이 됨
  • 도메인을 이해하는데 예상보다 짧은 시간이 소요
  • 인수 테스트로 스펙 표현 가능

빠른 피드백

  • 새로운 문화들의 공통점은 빠른 피드백을 받는 방법
  • 든든한 지원군과 함께 코딩을 하는 느낌을 받음
  • 자신감을 주는 자동화된 테스트

피드백을 받는 방법

  • 페어 프로그래밍
  • 테스트 / 인수 테스트
  • 코드 리뷰
  • 배포
  • 출시

  • 이 중 즉각적인 피드백을 줄 수 있는 테스트가 효과적이라고 생각함

테스트 주도 개발

  • 테스트를 설계 활동으로 바꾸는 효과도 있음
  • 이로 인해 설계 품질에 관한 피드백도 빠르게 받게 도와줌
  • 기존에는 검증의 목적으로 테스트를 했다면, TDD는 좋은 설계를 할 수 있도록 도움을 줌

인수 테스트 주도 개발

  • 개발 전 인수 테스트 작성
  • 인수 테스트 통과를 위한 개발

인수 테스트를 기반으로 개발을 할 경우

  • 기존 인수 테스트 장점
    • 빠른 피드백을 받을 수 있음
    • 회귀 오류를 잡아줄 꾸준한 테스트를 만들 수 있음
    • 기존 기능을 망가뜨리지 않고 새 기능을 추가할 수 있음
  • 인수 테스트를 작성하면서 구현할 대상에 대한 이해도 증가
  • 작업의 시작과 끝이 명확해져서 심리적인 안정감에 도움을 줌

ATDD란

테스트 vs TDD

image

  • 테스트는 기존의 기능을 검증하는 용도
  • TDD는 구현의 요구사항을 명세하기 위해 테스트를 사용함

TDD vs BDD

image

  • BDD 는 행위를 기반으로 테스트를 작성해 나가는 방식

TDD vs BDD vs ATDD

image

  • ATDD 는 요구사항의 명세를 인수테스트로 명세함

다 같이 인수 조건을 정의하는 이슈

  • 각자의 생각만으로 개발하면 기획자, 개발자, QA 전부 다른 기능을 생각할 수 있음
  • 개발 전 다 같이 인수 조건을 정의한 뒤 개발을 진행하는 것이 중요함

ATDD 프로세스

image

  • TDD에 비해 복잡한 프로세스를 가짐
  • 본 강의에서는 개발 관련된 내용만 다룸

ATDD 개발 프로세스 예시

  • 인수 조건 정의
  • 인수 테스트 작성
  • 기능 구현

ATDD 개발 과정 예시 - 강의 수강 대기 신청

  • 수강 신청 서비스에서 수강 정원이 다 찼경우 신청자가 강의 포기시 대기자가 수강신청되는 방식

인수 조건

  • 인수 테스트가 충족해야 하는 조건
given
  강사는 강의를 생성했다.
  강사는 강의를 신청 가능 상태로 변경하였다.
  강의 모집인원 만큼 신청을 받았다.

when
  회원이 수강 대기 신청을 요청한다.

then
  회원은 강의의 수강 대기자로 등록 되었다.

인수 테스트

  • 인수 조건을 검증하는 테스트
  • 실제 요청/응답하는 환경과 유사하게 테스트 환경을 구성

  • 테스트 예시 image

기능 구현

  • 인수테스트를 충족하기 위한 코드 구현
  • 기능 구현은 TDD로 진행할 수 있음

ATDD 관련 경험한 고민들

인수 테스트 유래

  • 인수 : 물건을 받는다
  • SW 개발을 의뢰하고 결과물을 인수받을 때 요구사항에 만족하는지 확인하는 테스트
  • 개발 전 인수 조건을 확인하고 테스트를 만듦

인수 조건 만들기

요구사항 (사용자 스토리)

  • 예시
    • 누가 - 왜 - 무엇을 순서로 작성
      강사는 
      강의료를 환불해주기 위해 
      수강생의 수강을 취소 할 수 있다.
      

인수 조건 표현

  • given : 사전 조건
  • when : 검증 대상
  • then : 기대 결과

인수 조건 작성

  • 검증 하고자 하는 when 구문 먼저 작성
  • 기대 결과를 의미하는 then 구문 작성
  • when과 then에서 필요한 정보를 given을 통해 마련

인수 조건 예시

given 
  수강생이 수강 신청을 하였다. 
  과정의 남은 기간이 절반 이상이다. 
when 
  강사는 특정 수강생의 수강 상태를 취소 요청을 한다.
then 
  특정 수강생의 수강 상태가 취소 된다. 
  특정 수강생의 결제 내역이 환불 된다.

인수 테스트 코드 작성하기

인수 테스트 특징

  • Black Box 테스트
    • 내부 구조나 작동과 연관이 없는 테스트

테스트 도구 (웹)

  • 테스트 서버(환경)
    • @SpringBootTest
  • 테스트 클라이언트
    • MockMVC, WebTestClient, RestAssured
  • 도구에 대한 내용은 생략

인수 테스트 예시

image

테스트 초기화

  • 몇몇 예시들이 소개되지만 웹에서 사용할 수 있을만한 내용들임.
  • Application 래벨에서 테스트 구축시에는 테스트 하니스를 사용하는것이 좋을듯 함

repository 활용하여 데이터 초기화?

  • 손쉽게 데이터 초기화가 가능
  • 구현이 달라지면 테스트 영향을 받음
  • 유효성 검사 로직 없음
  • 깨지기 쉬운 테스트가 될 가능성이 높음

요청을 통한 데이터 초기화

  • 테스트 객체를 이용하여 직접 호출 후 초기화

image

중복 처리 & 가독성 개선 필요

  • 테스트 코드 가독성의 중요성
    • 가독성이 좋지 않으면 방치될 가능성이 높음
    • 테스트는 간결하게 작성해야 함
  • 메서드 분리
    • 반복되는 코드는 메서드로 분리
  • 다른 인수 테스트에서 재사용
    • 재사용을 위한 클래스 또는 함수로 분리

인수 테스트 작성 팁

  • 간단한 성공 케이스 우선 작성
    • 동작 가능한 가장 간단한 성공 케이스로 시작
    • 테스트가 동작하면 더 좋은 생각이 떠오를 수 있음
  • 인수 테스트 클래스
    • 같은 테스트 픽쳐스를 공유하는 인수 테스트를 클래스 단위로 묶음

인수 테스트 다음 TDD

  • 인수 테스트 작성 후 기능 개발을 TDD로 진행
    • TDD로 하지 않을 경우 개발 후 피드백을 덜 받게 됨
  • 도메인 부터 TDD
    • 인수 테스트를 통해 시나리오 및 전반적인 기능에 대한 기능을 이해
    • 이후 핵심 기능에 대한 도메인 설계를 한 후 도메인 객체에 대한 TDD를 수행

추천하는 흐름

  • Top-Down으로 방향을 잡고, Bottom-Up으로 구현하기
    • 인수 테스트 작성을 시작으로 내부 구현을 TDD로 구현
  • 인수 테스트 작성을 통해 요구사항과 기능 전반에 대한 이해를 선행
  • 내부 구현에 대한 설계 흐름을 구상
  • 설계가 끝나면 도메인부터 차근차근 TDD로 기능 구현
  • 만약 도메인이 복잡하거나 설계가 어려울 경우 이해하고 있는 부분 먼저 기능 구현
  • 인수 테스트의 요청을 처리하는 부분부터 진행할 수 있음

ATDD 도입해보기

나혼자 ATDD

  • 토이 프로젝트로 경험 쌓기
  • 간단한 기능부터 적용해보기
  • 경험해보면서 상황에 맞는 방법 찾기

다함께 ATDD (개발팀)

  • ATDD 개발 프로세스 룰 정하기(인수 조건, 포맷 등)
  • 팀 회의와 회고를 통한 피드백
  • 살아있는 규칙 정하기

성공적인 ATDD 적용을 위해

  • 아주 쉽게 시작할 수 있는 부분부터 도입
  • 조금씩 상황에 맞게 조정
  • 가벼운 프로세스부터 시작, 문제점 발견 시 민첩하게 대응

레거시 기반 인수 테스트 작성하기

  • 먼저 인수 테스트를 작성하여 기존에 구현된 기능을 보호하기

인수 테스트 작성 가이드 작성

image

지속적인 피드백

image

기획 & QA와 함께하는 ATDD

기획, QA 설득하기(장점 소개)

  • 개발 친화적인 용어는 제외하고 설명하기
  • 기존 방식과 비교하여 장점 이야기 하기

다같이 만드는 요구 사항

  • 기획/개발/QA 함께 인수 조건 회의 참여
  • 화면 기반으로 작성할 경우 이해도가 높음
  • 모든 인수 조건을 다같이 만드는건 비효율적

ATDD 적용 전과 후, 장점과 단점 확인

  • Common Understaning
  • 다른 포지션의 관점은 물론 업무 프로세스도 간접적으로 익힐 수 있음
  • 다른 포지션의 진행 상황에 대한 인지와 이해도가 높아짐
  • 인수 조건 정의의 어려움
    • 문서를 어떻게 관리해야 할 지에 대한 고민이 필요
  • 리소스
    • (기획/개발) 두번 작업하는 느낌을 받음

Q&A

ATDD는 TDD의 레드 그린 사이클을 따라가되 테스트를 인수테스트로 하는가?

  • 개발을 해 나갈때 인수 테스트 작성 후 개발 해 나가는 방식
  • 기능 구현시 TDD의 레드 그린 사이클을 따라 개발함

어떤 기능을 개발시 인수 테스트부터 작성하려 하면 TDD의 빠른 피드백이 희석될까?

  • 인수 테스트가 작성 비용이 크고 피드백이 느리는건 사실
  • 하지만 ATDD 안에 TDD 가 포함됨
    • TDD 개발시 레드 그린 사이클 에서 많은 유닛 테스트가 생성될 것
  • TDD 부터 시작하면 어디서부터 어떻게 해야되는지 막막할 수 있음
  • ATDD로 시작시 조금 더 시작하기 쉬울 것

통합 테스트와 인수 테스트의 차이

  • 인수 테스트는 사용자 스토리 기반으로 기능 테스트를 진행
  • 통합 테스트는 각각 단위들이 모였을 때 잘 동작하는지 확인
  • 비슷하게 보이지만, 인수 테스트는 사용자 스토리 기반의 테스트라는 차이가 있음

TDD 도입도 어려운데 ATDD를 도입할 수 있을지?

  • TDD보다 ATDD를 도입하는 것이 더 쉬울 것이라 생각됨
  • 적용하기 쉬운 부분부터 서서히 도입하는게 좋음
  • 이러한 관점에서 보았을 때는 TDD 보다는 ATDD가 도입하기 더 좋다는 의견

관심 없는 구성원들에게 어떻게 기술을 전파할 수 있을지?

  • 구성원의 마음을 변화하는 일은 쉽지 않음
    • 실패하더라도 좌절하지 말 것
  • 토이프로젝트 등을 통해 자신의 실력을 키울 것
    • 구성원을 바꾸기 위함도 있지만 나의 성장을 위해서도 좋음
  • 본인이 잘 알고있을 때 전파하는 것이 좋음
    • 남을 알려줄 때 신뢰감을 줄 수 있음

</description> Thu, 07 Jul 2022 00:00:00 +0000 https://sunghwan7330.github.io/infomation/atdd_instruction/</link> https://sunghwan7330.github.io/infomation/atdd_instruction/

    <category>atdd</category>
    
    <category>tdd</category>
    
    <category>agile</category>
    
    
    <category>infomation</category>
    
  </item>

  <item>
    <title>[임베디드 C를 윈한 TDD] 4장 완료까지 테스트하기</title>
    <description># 개요 

본 장에서는 첫 예제인 LED 드라이버를 제작하면서 TDD를 시작하려 합니다. 3장에 이어서 LED 드라이버를 완성하는 것을 목표로 합니다.

단순하게 시작해서 솔루션 키워가기

이전장의 단순무식한 구현은 점진적으로 더 견고해질 것입니다.

다음으로 추가할 테스트는 두개의 LED를 켜는 것입니다.

이전에 설명한바와 같이 테스트 먼저 추가해보겟습니다.

void TurnOnMultipleLeds (void ** state) {
  LedDriver_TurnOn(9);
  LedDriver_TurnOn(8);
  assert_int_equal(0x180, virtualLeds);
}

테스트는 쉬워야 합니다. 위와 같이 테스트를 작성하였습니다. 예상하는 결과는 8번 LED 와 9번 LED 가 켜져야 됩니다. 이때 virtualLeds 의 값은 00000001 10000000 입니다. 즉 0x0180 입니다.

이제 테스트를 실행해보겠습니다.

[==========] Running 4 test(s).
CMocka setup
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[       OK ] TurnOnLedOne
[ RUN      ] TurnOnLedOff
[       OK ] TurnOnLedOff
[ RUN      ] TurnOnMultipleLeds
[  ERROR   ] --- 0x180 != 0x1
[   LINE   ] --- led_driver_test.c:45: error: Failure!
[  FAILED  ] TurnOnMultipleLeds
CMocka teardown
[==========] 4 test(s) run.
[  PASSED  ] 3 test(s).
[  FAILED  ] 1 test(s), listed below:
[  FAILED  ] TurnOnMultipleLeds

 1 FAILED TEST(S)

테스트는 예상대로 실패합니다. LedDriver_TurnOn 함수는 현재 1번 LED를 켜는 기능만 하기 때문입니다. 이제 LedDriver_TurnOn 함수를 수정해보겠습니다.

void LedDriver_TurnOn(int ledNumber){
  *ledsAddress |= (1 &lt;&lt; ledNumber);
}

위 코드는 ledNumber 의 위치의 비트를 or 연산을 이용해 1로 변경시킵니다. 이제 다시 테스트를 실행해보겠습니다.

[==========] Running 4 test(s).
CMocka setup
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[  ERROR   ] --- 0x1 != 0x2
[   LINE   ] --- led_driver_test.c:33: error: Failure!
[  FAILED  ] TurnOnLedOne
[ RUN      ] TurnOnLedOff
[       OK ] TurnOnLedOff
[ RUN      ] TurnOnMultipleLeds
[  ERROR   ] --- 0x180 != 0x300
[   LINE   ] --- led_driver_test.c:45: error: Failure!
[  FAILED  ] TurnOnMultipleLeds
CMocka teardown
[==========] 4 test(s) run.
[  PASSED  ] 2 test(s).
[  FAILED  ] 2 test(s), listed below:
[  FAILED  ] TurnOnLedOne
[  FAILED  ] TurnOnMultipleLeds

 2 FAILED TEST(S)

예상과는 다르게 테스트가 두개나 실패합니다. TurnOnLedOne 테스트와 TurnOnMultipleLeds 테스트에서 실패하였습니다. 코드는 단지 한줄밖에 수정하지 않았습니다. 하지만 원치 않은 곳에서 에러가 발생합니다. 분명 작은 실수지만 이러한 사이드이펙트는 생각보다 자주 발생합니다. TDD는 이처럼 문제가 아직 작은 상태일 때 발견할 수 있다는 점입니다.

이제 문제를 해결해보겠습니다.

먼저 TurnOnLedOne 의 예상결과는 0x0001이지만 결과는 0x0002 가 되었습니다. 비트가 하나 더 이동했다는 것을 알 수 있습니다.

LedDriver_TurnOn 함수를 다시 수정하겠습니다.

void LedDriver_TurnOn(int ledNumber){
  *ledsAddress |= 1 &lt;&lt; (ledNumber - 1);
}
[==========] Running 4 test(s).
CMocka setup
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[       OK ] TurnOnLedOne
[ RUN      ] TurnOnLedOff
[       OK ] TurnOnLedOff
[ RUN      ] TurnOnMultipleLeds
[       OK ] TurnOnMultipleLeds
CMocka teardown
[==========] 4 test(s) run.
[  PASSED  ] 4 test(s).

테스트가 모두 통과하였습니다.

테스트가 모두 통과한 이 시점에서 코드를 개선할 부분이 있는지 확인합니다. 보기 좋지 않은 코드가 있다면 바로 리팩터링을 수행해줍니다.

아래와 같이 비트 처리 부분을 도움 함수로 리팩터링해줍니다.

static uint16_t convertLedNumberToBit(int ledNumber) {
  return 1 &lt;&lt; (ledNumber - 1);
}

void LedDriver_TurnOn(int ledNumber){
  *ledsAddress |= convertLedNumberToBit(ledNumber);
}

이 작업은 의도를 드러낼 수 있는 이름으로 포장하여 가독성을 올렸습니다. 전역 네임스페이스에 추가되는 것을 피해기 위해 static 키워드를 사용했습니다. convertLedNumberToBit 함수를 인라인이나 메크로로 처리하여 성능을 올릴 수 있지만, 컴파일러의 최적화라면 불필요한 작업입니다.

꾸준한 진행

하나하나의 테스트가 구현을 점차 완성시킵니다.

다양한 테스트를 추가하여 프로그램의 안정성을 올릴 수 있습니다. 다른 테스트 하나를 더 추가해보겠습니다 .

void TurnOffAnyLed (void ** state) {
  LedDriver_TurnOn(9);
  LedDriver_TurnOn(8);
  LedDriver_TurnOff(8);
  assert_int_equal(0x100, virtualLeds);
}

이제 테스트를 돌려봅니다.

[==========] Running 5 test(s).
CMocka setup
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[       OK ] TurnOnLedOne
[ RUN      ] TurnOnLedOff
[       OK ] TurnOnLedOff
[ RUN      ] TurnOnMultipleLeds
[       OK ] TurnOnMultipleLeds
[ RUN      ] TurnOffAnyLed
[  ERROR   ] --- 0x100 != 0
[   LINE   ] --- led_driver_test.c:52: error: Failure!
[  FAILED  ] TurnOffAnyLed
CMocka teardown
[==========] 5 test(s) run.
[  PASSED  ] 4 test(s).
[  FAILED  ] 1 test(s), listed below:
[  FAILED  ] TurnOffAnyLed

 1 FAILED TEST(S)

예상했던대로 테스트는 실패합니다. 앞에서 작성한 TurnOnLedOff 함수는 모든 LED를 끄기 때문입니다. TurnOnLedOff를 비트 마스크 처리하는 코드로 작성 후 테스트 하려면 다른 LED를 미리 ON 상태로 만들어줘야 합니다.

이를 위해 모두켜는 함수를 만들려 합니다. 먼저 테스트를 추가하겠습니다.

void AllOn (void ** state) {
  LedDriver_TurnAllOn();
  assert_int_equal(0xffff, virtualLeds);
}
enum {ALL_LEDS_ON = ~0, ALL_LEDS_OFF = ~ALL_LEDS_ON};

void LedDriver_TurnAllOn(void) {
  *ledsAddress = ALL_LEDS_ON;
}

위와 같이 모든 LED를 켤 수 있는 기능이 추가되었습니다. 이제 특정 LED를 끄는 테스트를 수정합니다.

void TurnOffAnyLed (void ** state) {
  LedDriver_TurnAllOn();
  LedDriver_TurnOff(8);
  assert_int_equal(0xff7f, virtualLeds);
}
void LedDriver_TurnOff(int ledNumber) {
  *ledsAddress &amp;= ~(convertLedNumberToBit(ledNumber));
}

이제 다시 테스트를 수행하면 통과하는 것을 확인할 수 있습니다.

[==========] Running 6 test(s).
CMocka setup
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[       OK ] TurnOnLedOne
[ RUN      ] TurnOnLedOff
[       OK ] TurnOnLedOff
[ RUN      ] TurnOnMultipleLeds
[       OK ] TurnOnMultipleLeds
[ RUN      ] TurnOffAnyLed
[       OK ] TurnOffAnyLed
[ RUN      ] AllOn
[       OK ] AllOn
CMocka teardown
[==========] 6 test(s) run.
[  PASSED  ] 6 test(s).

다음으로 소프트웨어에서 LED의 ON/OFF 상태를 읽을 수 있는지에 대한 테스트를 추가하려 합니다. 하드웨어에서 값을 읽지 않았다는 것을 확인하기 위해 아래와 같은 테스트를 추가합니다. 새로 추가하는 테스트에서 virtualLeds를 0xffff로 먼저 설정하면 드라이버가 LED의 현재 상태를 하드웨어로 부터 읽어오지 않는다는 것을 확인할 수 있습니다.

void LedMemoryIsNotReadable (void ** state) {
  virtualLeds = 0xffff;
  LedDriver_TurnOn(8);
  assert_int_equal(0x80, virtualLeds);
}
[==========] Running 7 test(s).
CMocka setup
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[       OK ] TurnOnLedOne
[ RUN      ] TurnOnLedOff
[       OK ] TurnOnLedOff
[ RUN      ] TurnOnMultipleLeds
[       OK ] TurnOnMultipleLeds
[ RUN      ] TurnOffAnyLed
[       OK ] TurnOffAnyLed
[ RUN      ] AllOn
[       OK ] AllOn
[ RUN      ] LedMemoryIsNotReadable
[  ERROR   ] --- 0x80 != 0xffff
[   LINE   ] --- led_driver_test.c:62: error: Failure!
[  FAILED  ] LedMemoryIsNotReadable
CMocka teardown
[==========] 7 test(s) run.
[  PASSED  ] 6 test(s).
[  FAILED  ] 1 test(s), listed below:
[  FAILED  ] LedMemoryIsNotReadable

 1 FAILED TEST(S)

테스트가 실패하는것을 보았을 때 LED의 현재 상태를 하드웨어로 부터 읽어오지 않는다는 것을 확인할 수 있습니다.

테스트를 통과시키기 위해, LED의 상태 를 ledsImage라는 파일 범위 비공개 변수에 기록하도록 합니다. LedDriver_Create()에서 이 변수를 초기화합니다.

uint16_t * ledsAddress;
static uint16_t ledsImage;

void LedDriver_Create(uint16_t * address) {
  ledsAddress = address;
  ledsImage = ALL_LEDS_OFF;
  *ledsAddress = ledsImage;
}

LedDriver_TurnOn(), LedDriver_TurnOff(), LedDriver_TurnAllOn() 함수에서 현 재 LED 상태를 알기 위해 ledsImage 변수를 이용합니다.

void LedDriver_TurnOn(int ledNumber){
  ledsImage |= convertLedNumberToBit(ledNumber);
  *ledsAddress = ledsImage;
}

void LedDriver_TurnOff(int ledNumber) {
  ledsImage &amp;= ~(convertLedNumberToBit(ledNumber));
  *ledsAddress = ledsImage;
}

void LedDriver_TurnAllOn(void) {
  ledsImage = ALL_LEDS_ON;
  *ledsAddress = ledsImage;
}

이제 모든 테스트가 통과하는 것을 확인하였습니다.

[==========] Running 7 test(s).
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[       OK ] TurnOnLedOne
[ RUN      ] TurnOnLedOff
[       OK ] TurnOnLedOff
[ RUN      ] TurnOnMultipleLeds
[       OK ] TurnOnMultipleLeds
[ RUN      ] TurnOffAnyLed
[       OK ] TurnOffAnyLed
[ RUN      ] AllOn
[       OK ] AllOn
[ RUN      ] LedMemoryIsNotReadable
[       OK ] LedMemoryIsNotReadable
[==========] 7 test(s) run.
[  PASSED  ] 7 test(s).

이후 코드 여러 곳에 중복된 *ledsAddress = ledsImage; 를 추출하여 도움 함수로 만들 고 향후 코드를 읽는 사람들이 코드를 쉽게 이해하도록 변경해줍니다.

static void updateHardware(void) {
  *ledsAddress = ledsImage;
}

void LedDriver_TurnAllOn(void) {
  ledsImage = ALL_LEDS_ON;
  updateHardware();
}

경계 조건 테스트

다음 테스트는 LED 번호의 정상값 범위를 상한값과 하한값으로 확인합니다. 이 테스트는 상세 요구사항 역할을 합니다.

void UpperAndLowerBounds (void** state) {
  LedDriver_TurnOn(1);
  LedDriver_TurnOn(16);
  assert_int_equal(0x80, virtualLeds);
}

위의 테스트는 LED의 유효 범위 내의 값을 제어하기 때문에 테스트 수행시 통과하게 됩니다.

[==========] Running 8 test(s).
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[       OK ] TurnOnLedOne
[ RUN      ] TurnOnLedOff
[       OK ] TurnOnLedOff
[ RUN      ] TurnOnMultipleLeds
[       OK ] TurnOnMultipleLeds
[ RUN      ] TurnOffAnyLed
[       OK ] TurnOffAnyLed
[ RUN      ] AllOn
[       OK ] AllOn
[ RUN      ] LedMemoryIsNotReadable
[       OK ] LedMemoryIsNotReadable
[ RUN      ] UpperAndLowerBounds
[       OK ] UpperAndLowerBounds
[==========] 8 test(s) run.
[  PASSED  ] 8 test(s).

LED 범위가 정상 범위를 벗어나면 어떤 동작을 해야될까요?

  1. 드라이버가 인접한 메모리 값을 쓴다?
  2. 잘못된 값이므로 무시한다?

2번이 더 좋은 방법으로 판단됩니다.

우선은 경계를 벗어나는 값이 입력되었을 때 어떤 결과가 나오는지 확인해봅니다.

void OutOfBoundsChangesNothing (void** state) {
  LedDriver_TurnOn(-1);
  LedDriver_TurnOn(0);
  LedDriver_TurnOn(1);
  LedDriver_TurnOn(2);
  LedDriver_TurnOn(17);
  LedDriver_TurnOn(33);
  LedDriver_TurnOn(3141);
  assert_int_equal(0, virtualLeds);
}

위의 테스트를 추가하고 테스트를 수행해보겠습니다.

[==========] Running 9 test(s).
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[       OK ] TurnOnLedOne
[ RUN      ] TurnOnLedOff
[       OK ] TurnOnLedOff
[ RUN      ] TurnOnMultipleLeds
[       OK ] TurnOnMultipleLeds
[ RUN      ] TurnOffAnyLed
[       OK ] TurnOffAnyLed
[ RUN      ] AllOn
[       OK ] AllOn
[ RUN      ] LedMemoryIsNotReadable
[       OK ] LedMemoryIsNotReadable
[ RUN      ] UpperAndLowerBounds
[       OK ] UpperAndLowerBounds
[ RUN      ] OutOfBoundsChangesNothing
[  ERROR   ] --- 0 != 0x3
[   LINE   ] --- led_driver_test.c:76: error: Failure!
[  FAILED  ] OutOfBoundsChangesNothing
[==========] 9 test(s) run.
[  PASSED  ] 8 test(s).
[  FAILED  ] 1 test(s), listed below:
[  FAILED  ] OutOfBoundsChangesNothing

 1 FAILED TEST(S)

유효범위 외의 값은 무시된 듯 해보입니다. (서적에서는 시프트 연산이 범위를 벗어나는 경우 로테이트 되었음)

하지만 경계조건을 처리하지 않은 코드를 테스트하다보면 테스트가 실행되다가 크래시가 발생하는 경우가 있습니다. 스택상의 배열의 경계를 벗어나 스택이 깨질 수도 있습니다. 우리가 작성한 테스트에서는 이러한 손상이 발생하지는 않았지만, 경계처리는 꼭 해주어야 합니다.

아래와 같이 LedDriver_TurnOn()와 LedDriver_TurnOff()에 보호절을 추가합니다.

void LedDriver_TurnOn(int ledNumber){
  if (ledNumber &lt;= 0 || ledNumber &gt; 16)
    return;

  ledsImage |= convertLedNumberToBit(ledNumber);
  updateHardware();
}

void LedDriver_TurnOff(int ledNumber) {
  if (ledNumber &lt;= 0 || ledNumber &gt; 16)
    return;

  ledsImage &amp;= ~(convertLedNumberToBit(ledNumber));
  updateHardware();
}

이후 테스트의 결과값을 수정하여 테스트가 통과하도록 해줍니다.

[==========] Running 9 test(s).
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[       OK ] TurnOnLedOne
[ RUN      ] TurnOnLedOff
[       OK ] TurnOnLedOff
[ RUN      ] TurnOnMultipleLeds
[       OK ] TurnOnMultipleLeds
[ RUN      ] TurnOffAnyLed
[       OK ] TurnOffAnyLed
[ RUN      ] AllOn
[       OK ] AllOn
[ RUN      ] LedMemoryIsNotReadable
[       OK ] LedMemoryIsNotReadable
[ RUN      ] UpperAndLowerBounds
[       OK ] UpperAndLowerBounds
[ RUN      ] OutOfBoundsChangesNothing
[       OK ] OutOfBoundsChangesNothing
[==========] 9 test(s) run.
[  PASSED  ] 9 test(s).

이 수정에서 실수한 부분이 있습니다. LedDriver_TurnOff 에 대한 테스트를 작성하지 않고 수정을 먼저 진행했습니다.

물론 테스트된 코드를 복사하였으므로 안전하다고 생각할 수 있지만, 이는 조심해야 합니다.

이제 빠진 LedDriver_TurnOff 에 대해서 경계를 벗어날때에 대한 테스트를 추가해줍니다.

void OutOfBoundsTurnOffDoesNoHarm (void** state) {
  LedDriver_TurnOff(-1);
  LedDriver_TurnOff(0);
  LedDriver_TurnOff(17);
  LedDriver_TurnOff(3141);
  assert_int_equal(0xffff, virtualLeds);
}

이제 테스트를 실행해보겠습니다.

[==========] Running 10 test(s).
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[       OK ] TurnOnLedOne
[ RUN      ] TurnOnLedOff
[       OK ] TurnOnLedOff
[ RUN      ] TurnOnMultipleLeds
[       OK ] TurnOnMultipleLeds
[ RUN      ] TurnOffAnyLed
[       OK ] TurnOffAnyLed
[ RUN      ] AllOn
[       OK ] AllOn
[ RUN      ] LedMemoryIsNotReadable
[       OK ] LedMemoryIsNotReadable
[ RUN      ] UpperAndLowerBounds
[       OK ] UpperAndLowerBounds
[ RUN      ] OutOfBoundsChangesNothing
[       OK ] OutOfBoundsChangesNothing
[ RUN      ] OutOfBoundsTurnOffDoesNoHarm
[       OK ] OutOfBoundsTurnOffDoesNoHarm
[==========] 10 test(s) run.
[  PASSED  ] 10 test(s).

예상과 다르게 테스트가 성공했습니다. 테스트에 성공한 이유는 시작시 모든 LED가 켜져있기 때문입니다. 따라서 테스트 시작시에 모든 LED를 켜고 시작해야 합니다.

아래와 같이 테스트를 수정하고 다시 진행해보겠습니다.

void OutOfBoundsTurnOffDoesNoHarm (void** state) {
  LedDriver_TurnAllOn();
  LedDriver_TurnOff(-1);
  LedDriver_TurnOff(0);
  LedDriver_TurnOff(17);
  LedDriver_TurnOff(3141);
  assert_int_equal(0xffff, virtualLeds);
}
[==========] Running 10 test(s).
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[       OK ] TurnOnLedOne
[ RUN      ] TurnOnLedOff
[       OK ] TurnOnLedOff
[ RUN      ] TurnOnMultipleLeds
[       OK ] TurnOnMultipleLeds
[ RUN      ] TurnOffAnyLed
[       OK ] TurnOffAnyLed
[ RUN      ] AllOn
[       OK ] AllOn
[ RUN      ] LedMemoryIsNotReadable
[       OK ] LedMemoryIsNotReadable
[ RUN      ] UpperAndLowerBounds
[       OK ] UpperAndLowerBounds
[ RUN      ] OutOfBoundsChangesNothing
[       OK ] OutOfBoundsChangesNothing
[ RUN      ] OutOfBoundsTurnOffDoesNoHarm
[       OK ] OutOfBoundsTurnOffDoesNoHarm
[==========] 10 test(s) run.
[  PASSED  ] 10 test(s).

테스트가 통과하는 것을 확인하였습니다.

이제 드라이버의 오류에 대해서 알람을 주는 기능을 추가해야 합니다.

RuntimeError.h 헤더 파일에 아래의 코드를 추가합니다.

void RuntimeError(const char * message, int parameter, const char * file, int line);

#define RUNTIME_ERROR(description, parameter)\
 RuntimeError(description, parameter, __FILE__, __LINE__)

RuntimeError는 이벤트 로그에 오류 메시지를 추가합니다. 테스트 중 에는 RuntimeError()를 스텁으로 만들어서 마지막으로 발생한 오류를 저장했다가 확인할 수 있게 합니다. RuntimeErrorStub.h 생성 후 아래와 같이 작성해줍니다.

void RuntimeErrorStub_Reset(void); 
const char * RuntimeErrorStub_GetLastError(void); 
int RuntimeErrorStub_GetLastParameter(void);
void RuntimeError(const char * m, int p, const char * f, int l);

스텁의 구현은 아래와 같습니다.

#include &quot;RuntimeErrorStub.h&quot;

static const char * message = &quot;No Error&quot;;
static int parameter = -1;
static const char * file = 0;
static int line = -1;

void RuntimeErrorStub_Reset(void) {
  message = &quot;No Error&quot;;
  parameter = -1;
}

const char * RuntimeErrorStub_GetLastError(void) {
  return message;
}

void RuntimeError(const char * m, int p, const char * f, int l) {
  message = m;
  parameter = p;
  file = f;
  line = l;
}
int RuntimeErrorStub_GetLastParameter(void) {
  return parameter;
}

RuntimeError()의 스텁 버전은 오류 정보를 저장하기만 합니다.

테스트 중에는 RuntimeError()의 스텁 버전이 링크됩니다. 이로써 테스트 케이스가 경계를 벗어나는 경우에 RuntimeError()가 호출되는지 여부를 확인할 수 있습니다.

아래의 테스트를 추가해줍니다.

void OutOfBoundsProducesRuntimeError (void** state) {
  LedDriver_TurnOn(-1);
  assert_string_equal(&quot;LED Driver: out-of-bounds LED&quot;, RuntimeErrorStub_GetLastError());
  assert_int_equal(-1, RuntimeErrorStub_GetLastParameter());
}

이제 테스트를 수행해줍니다.

[==========] Running 11 test(s).
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[       OK ] TurnOnLedOne
[ RUN      ] TurnOnLedOff
[       OK ] TurnOnLedOff
[ RUN      ] TurnOnMultipleLeds
[       OK ] TurnOnMultipleLeds
[ RUN      ] TurnOffAnyLed
[       OK ] TurnOffAnyLed
[ RUN      ] AllOn
[       OK ] AllOn
[ RUN      ] LedMemoryIsNotReadable
[       OK ] LedMemoryIsNotReadable
[ RUN      ] UpperAndLowerBounds
[       OK ] UpperAndLowerBounds
[ RUN      ] OutOfBoundsChangesNothing
[       OK ] OutOfBoundsChangesNothing
[ RUN      ] OutOfBoundsTurnOffDoesNoHarm
[       OK ] OutOfBoundsTurnOffDoesNoHarm
[ RUN      ] OutOfBoundsProducesRuntimeError
[  ERROR   ] --- &quot;LED Driver: out-of-bounds LED&quot; != &quot;No Error&quot;
[   LINE   ] --- led_driver_test.c:92: error: Failure!
[  FAILED  ] OutOfBoundsProducesRuntimeError
[==========] 11 test(s) run.
[  PASSED  ] 10 test(s).
[  FAILED  ] 1 test(s), listed below:
[  FAILED  ] OutOfBoundsProducesRuntimeError

 1 FAILED TEST(S)

이제 RUNTIME_ERROR( ) 호출을 추가해 테스트가 통과하도록 해줍니다.

void LedDriver_TurnOn(int ledNumber){
  if (ledNumber &lt;= 0 || ledNumber &gt; 16){
	RUNTIME_ERROR(&quot;LED Driver: out-of-bounds LED&quot;, -1);
    return;
  }

  ledsImage |= convertLedNumberToBit(ledNumber);
  updateHardware();
}

테스트를 실행해줍니다.

[==========] Running 11 test(s).
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[       OK ] TurnOnLedOne
[ RUN      ] TurnOnLedOff
[       OK ] TurnOnLedOff
[ RUN      ] TurnOnMultipleLeds
[       OK ] TurnOnMultipleLeds
[ RUN      ] TurnOffAnyLed
[       OK ] TurnOffAnyLed
[ RUN      ] AllOn
[       OK ] AllOn
[ RUN      ] LedMemoryIsNotReadable
[       OK ] LedMemoryIsNotReadable
[ RUN      ] UpperAndLowerBounds
[       OK ] UpperAndLowerBounds
[ RUN      ] OutOfBoundsChangesNothing
[       OK ] OutOfBoundsChangesNothing
[ RUN      ] OutOfBoundsTurnOffDoesNoHarm
[       OK ] OutOfBoundsTurnOffDoesNoHarm
[ RUN      ] OutOfBoundsProducesRuntimeError
[       OK ] OutOfBoundsProducesRuntimeError
[==========] 11 test(s) run.
[  PASSED  ] 11 test(s).

의도대로 통과하는 것을 확인하였습니다.

코드를 깔끔하게 유지하기 - 자주 리팩터링 하기

예제코드를 작성하면서 일부 작은 문제들을 제거하기 위해 리팩터링을 했습니다. 리팩터링을 할 것이 보이게 된다면 바로 진행하여 더 큰 문제가 자라날 기회를 없애는 것이 좋습니다. 단, 리팩터링은 모든 테스트가 통화하는 경우에만 진행하여야 합니다.

지금은 중복 코드를 추출해서 도움함수로 만들고, 매직 넘버 대신 상수 정의를 도입해서 두가지의 냄새를 제거해보려 합니다.

잘라내기 대신 복사하기

새로운 함수를 추출할 때, 중복코드 잘라내기(cut) 대신 복사하기(copy)를 사용해야 합니다. 새로 만들 함수에 뼈대만 추가하고 복사한 코드를 함수에 넣습니다. 새 함수에 인자나 반환값이 필요하면 이를 추가한 후 컴파일을 합니다.

테스트가 통과하면 중복 코드가 사용되고 있는 다른곳들도 새로 만든 도움함수로 치환합니다.

새 도움함수가 적용되고 테스트가 모두 통과한 다음 매직 넘버를 상수로 치환합니다.

enum {FIRST_LED = 1, LAST_LED = 16};
static BOOL IsLedOutOfBounds(int ledNumber) {
  return (ledNumber &lt; FIRST_LED) || (ledNumber &gt; LAST_LED);
}

IsLedOutOfBounds 함수는 외부에서 호출할 필요가 없으므로 헤더에 추가하지 않고 static으로 선언합니다. 리팩터링된 LedDriver_TurnOnLedDriver_TurnOff는 아래와 같다.

void LedDriver_TurnOn(int ledNumber){
  if (IsLedOutOfBounds(ledNumber)) {
    RUNTIME_ERROR(&quot;LED Driver: out-of-bounds LED&quot;, -1);
    return;
  }

  ledsImage |= convertLedNumberToBit(ledNumber);
  updateHardware();
}

void LedDriver_TurnOff(int ledNumber) {
  if (IsLedOutOfBounds(ledNumber)) {
    RUNTIME_ERROR(&quot;LED Driver: out-of-bounds LED&quot;, -1);
    return;
  }

  ledsImage &amp;= ~(convertLedNumberToBit(ledNumber));
  updateHardware();
}

비트 조작 코드는 아래와 같이 함수를 추출해주겠습니다.

static void setLedImageBit(int ledNumber) {
  ledsImage |= convertLedNumberToBit(ledNumber);
}

static void clearLedImageBit(int ledNumber) {
  ledsImage &amp;= ~convertLedNumberToBit(ledNumber);
}

void LedDriver_TurnOn(int ledNumber){
  if (IsLedOutOfBounds(ledNumber)) {
    RUNTIME_ERROR(&quot;LED Driver: out-of-bounds LED&quot;, -1);
    return;
  }

  setLedImageBit(ledNumber);
  updateHardware();
}

void LedDriver_TurnOff(int ledNumber) {
  if (IsLedOutOfBounds(ledNumber)) {
    RUNTIME_ERROR(&quot;LED Driver: out-of-bounds LED&quot;, -1);
    return;
  }

  clearLedImageBit(ledNumber);
  updateHardware();
}

한번에 하나씩 문제 해결하기

작은 단계를 밟아가는 것은 여러분이 한 번에 하나씩 문제를 해결하는 데 집중하 도록 도와줍니다. 사람은 한 번에 하나의 문제만 해결할 때 일을 더 잘 할 수 있습니다.

리팩터링 결과로 이전에 동작하던 테스트가 실패하면 디버깅을 하지 말라 합니다. 되돌리기(undo) 한 다음 여러분이 작업한 내용을 꼼꼼하게 살펴 보기를 바랍니다. 문제가 정말 명확하다면 바로 고쳐보는 것도 좋지만, 다시 녹색 상태로 돌아가려면 되돌리기를 얼마나 해야 하는지 의식하고 있어야 합니다. 만약 한두 군데 고쳐 봐서 테스트가 통과하지 않는다면 스스로의 구멍을 파고 있는 상황이 됩니다. 파는 것을 멈추고 되돌려서 다시 생각하는 것이 좋습니다.

완료될 때 까지 반복하기

드라이버의 핵심 기능은 갖춰졌습니다. 이제 뼈대에 살을 붙일때 까지 테스트와 제품 코드를 계속 추가할 수 있습니다.

이제 LED 상태를 가져오는 기능을 추가하겠습니다. 먼저 테스트부터 추가해줍니다.

void IsOn(void** state) {
  assert_int_equal(0, (LedDriver_IsOn(11)));
  LedDriver_TurnOn(11);
  assert_int_equal(1, (LedDriver_IsOn(11)));
}

컴파일 실패하는 것을 확인하고 IsOn 함수를 헤더에 추가해줍니다. 컴파일 성공하는것을 확인해고 이제 함수를 구현합니다.

bool LedDriver_IsOn(int ledNumber) {
  return false;
}

이 처럼 하드코딩된 코드를 추가하면 테스트에 실패하는 것을 확인할 수 있습니다.

이제 테스트가 통과하도록 코드를 수정해줍니다.

bool LedDriver_IsOn(int ledNumber) {
  return ledsImage &amp; (convertLedNumberToBit(ledNumber));
}
[==========] Running 12 test(s).
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[       OK ] TurnOnLedOne
[ RUN      ] TurnOnLedOff
[       OK ] TurnOnLedOff
[ RUN      ] TurnOnMultipleLeds
[       OK ] TurnOnMultipleLeds
[ RUN      ] TurnOffAnyLed
[       OK ] TurnOffAnyLed
[ RUN      ] AllOn
[       OK ] AllOn
[ RUN      ] LedMemoryIsNotReadable
[       OK ] LedMemoryIsNotReadable
[ RUN      ] UpperAndLowerBounds
[       OK ] UpperAndLowerBounds
[ RUN      ] OutOfBoundsChangesNothing
[       OK ] OutOfBoundsChangesNothing
[ RUN      ] OutOfBoundsTurnOffDoesNoHarm
[       OK ] OutOfBoundsTurnOffDoesNoHarm
[ RUN      ] OutOfBoundsProducesRuntimeError
[       OK ] OutOfBoundsProducesRuntimeError
[ RUN      ] IsOn
[       OK ] IsOn
[==========] 12 test(s) run.
[  PASSED  ] 12 test(s).

LED 제어 함수들처럼 LedDriver_IsOn()에서도 유효하지 않은 LED 번호가 들어 오는 경우를 처리해야 합니다. 다만 사양을 결정해야 합니다.

  • 유효하지 않은 LED는 On 상태로 보아야 하나,
  • 아니면 Off 상태로 보아야 하나?
  • 또는 On도 Off도 아니어야 하나?

우선은 범위를 벗어난 LED를 off 상태로 보겠습니다. 아래의 테스트를 추가합니다.

void OutOfBoundsLedsAreAlwaysOff(void**state) {
  assert_int_equal(0, LedDriver_IsOn(0));
  assert_int_equal(0, LedDriver_IsOn(17));
}

보호절을 추가하지 않았는데도 테스트가 통과하게 됩니다. 하지만 이는 장치마다 다를 수 있기 떄문에 보호절은 추가해야 합니다.

먼저 IsOn 함수를 하드코딩하여 true를 반환하도록 합니다.

bool LedDriver_IsOn(int ledNumber) {
  return true;
  //return ledsImage &amp; (convertLedNumberToBit(ledNumber));
}
[==========] Running 13 test(s).
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[       OK ] TurnOnLedOne
[ RUN      ] TurnOnLedOff
[       OK ] TurnOnLedOff
[ RUN      ] TurnOnMultipleLeds
[       OK ] TurnOnMultipleLeds
[ RUN      ] TurnOffAnyLed
[       OK ] TurnOffAnyLed
[ RUN      ] AllOn
[       OK ] AllOn
[ RUN      ] LedMemoryIsNotReadable
[       OK ] LedMemoryIsNotReadable
[ RUN      ] UpperAndLowerBounds
[       OK ] UpperAndLowerBounds
[ RUN      ] OutOfBoundsChangesNothing
[       OK ] OutOfBoundsChangesNothing
[ RUN      ] OutOfBoundsTurnOffDoesNoHarm
[       OK ] OutOfBoundsTurnOffDoesNoHarm
[ RUN      ] OutOfBoundsProducesRuntimeError
[       OK ] OutOfBoundsProducesRuntimeError
[ RUN      ] IsOn
[  ERROR   ] --- 0x1 == 0x1
[   LINE   ] --- led_driver_test.c:97: error: Failure!
[  FAILED  ] IsOn
[ RUN      ] OutOfBoundsLedsAreAlwaysOff
[  ERROR   ] --- 0 != 0x1
[   LINE   ] --- led_driver_test.c:103: error: Failure!
[  FAILED  ] OutOfBoundsLedsAreAlwaysOff
[==========] 13 test(s) run.
[  PASSED  ] 11 test(s).
[  FAILED  ] 2 test(s), listed below:
[  FAILED  ] IsOn
[  FAILED  ] OutOfBoundsLedsAreAlwaysOff

 2 FAILED TEST(S)

테스트가 실패하는 것을 확인하였습니다.

이제 올바른 코드를 추가하여 테스트가 통과하도록 합니다.

bool LedDriver_IsOn(int ledNumber) {
  if (IsLedOutOfBounds(ledNumber))  return false;
  
  return ledsImage &amp; (convertLedNumberToBit(ledNumber));
}

이제 테스트가 모두 통과하는것을 확인할 수 있습니다.

이제 LED가 꺼져있는지 확인하는 기능을 추가하겠습니다.

void IsOff (void** state) {
  assert_int_equal(1, LedDriver_IsOff(12));
  LedDriver_TurnOn(12);
  assert_int_equal(0, LedDriver_IsOff(12));
}
bool LedDriver_IsOff(int ledNumber) {
  return !LedDriver_IsOn(ledNumber);
}

이 역시 테스트 추가 후 기능 구현을 해줍니다. LedDriver_IsOff()를 마무리 짓기 위해 경계를 벗어난 LED 번호들은 항상 Off 상태라는 것만 확실히 해두면 됩니다.

OutOfBoundsLedsAreAlwaysOff 함수에서 LedDriver_IsOff 함수도 테스트할 수 있도록 수정해줍니다.

void OutOfBoundsLedsAreAlwaysOff (void** state) {
	assert_int_equal(1, LedDriver_IsOff(0));
	assert_int_equal(1, LedDriver_IsOff(17));
	assert_int_equal(0, LedDriver_IsOn(0));
	assert_int_equal(0, LedDriver_IsOn(17));
}

이제 여러 LED 를 끄는것과 LED를 모두 끄는 테스트를 추가해줍니다.

void TurnOffMultipleLeds (void** state) {
  LedDriver_TurnAllOn();
  LedDriver_TurnOff(9);
  LedDriver_TurnOff(8);
  assert_int_equal((~0x180)&amp;0xffff, virtualLeds);
}

마지막으로 LED를 모두 끄는 함수를 추가하겠습니다.

먼저 테스트부터 추가하겠습니다.

void AllOff (void** staate) {
  LedDriver_TurnAllOn();
  LedDriver_TurnAllOff();
  assert_int_equal(0, virtualLeds);
}

함수의 원형만 추가하여 테스트에 실패하는 것을 확인한 후에 함수를 작성해줍니다.

void LedDriver_TurnAllOff(void) {
  ledsImage = ALL_LEDS_OFF;
  updateHardware();
}

이제 모든 테스트가 성공하는 것을 확인합니다.

[==========] Running 16 test(s).
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[       OK ] TurnOnLedOne
[ RUN      ] TurnOnLedOff
[       OK ] TurnOnLedOff
[ RUN      ] TurnOnMultipleLeds
[       OK ] TurnOnMultipleLeds
[ RUN      ] TurnOffAnyLed
[       OK ] TurnOffAnyLed
[ RUN      ] AllOn
[       OK ] AllOn
[ RUN      ] LedMemoryIsNotReadable
[       OK ] LedMemoryIsNotReadable
[ RUN      ] UpperAndLowerBounds
[       OK ] UpperAndLowerBounds
[ RUN      ] OutOfBoundsChangesNothing
[       OK ] OutOfBoundsChangesNothing
[ RUN      ] OutOfBoundsTurnOffDoesNoHarm
[       OK ] OutOfBoundsTurnOffDoesNoHarm
[ RUN      ] OutOfBoundsProducesRuntimeError
[       OK ] OutOfBoundsProducesRuntimeError
[ RUN      ] IsOn
[       OK ] IsOn
[ RUN      ] OutOfBoundsLedsAreAlwaysOff
[       OK ] OutOfBoundsLedsAreAlwaysOff
[ RUN      ] IsOff
[       OK ] IsOff
[ RUN      ] TurnOffMultipleLeds
[       OK ] TurnOffMultipleLeds
[ RUN      ] AllOff
[       OK ] AllOff
[==========] 16 test(s) run.
[  PASSED  ] 16 test(s).

정리

지금까지 예제를 통해 TDD 를 실습하였습니다.

추가한 테스트를 모두 포함하면 아래와 같이 테스트 목록에 대한 테스트를 모두 수행하였을 것입니다.

image

TDD를 처음할때는 낯설것으로 생각됩니다. TDD는 코드를 작성하기 전에 테스트가 있어야 한다는 규칙을 내세웁니다. 만약 TDD에 대한 경험이 늘어난다면 TDD로 부터 얻게되는 피드백 그 자체가 보상이 될 것입니다. </description> Mon, 13 Jun 2022 00:00:00 +0000 https://sunghwan7330.github.io/embedded_c_tdd/embedded_c_tdd_04/</link> https://sunghwan7330.github.io/embedded_c_tdd/embedded_c_tdd_04/

    <category>임베디드</category>
    
    <category>TDD</category>
    
    
    <category>embedded_c_tdd</category>
    
  </item>

  <item>
    <title>[임베디드 C를 윈한 TDD] 3장 모듈 시작하기</title>
    <description># 개요 

본 장에서는 첫 예제인 LED 드라이버를 제작하면서 TDD를 시작하려 합니다.

LED 드라이버가 하는 일

LED 드라이버의 요구사항은 아래와 같습니다.

  1. LED 드라이버는 2가지 상태를 가지는 16개의 LED를 제어한다.
  2. 드라이버는 다른 LED에 영향을 주지 않고 각 LED를 On/Off 시킬 수 있다.
  3. 드라이버는 한 번의 인터페이스 함수 호출로 모든 LED를 On/Off 시킬 수 있다.
  4. 드라이버 사용자는 임의의 LED 상태를 조회할 수 있다.
  5. 전원이 공급되면 하드웨어는 기본적으로 LED를 On 상태로 만든다. 소프트웨 어에서 Off 시켜야만 한다.
  6. LED는 단일 16비트 워드(메모리 주소는 나중에 정해짐)로 메모리에 매핑된다.
  7. 비트가 ‘1’이 되면 해당 LED가 켜지고 ‘0’이 되면 해당 LED가 꺼진다.
  8. 최하위 비트가 1번 LED에, 최상위 비트가 16번 LED에 해당한다.

1~4 까지는 LED 드라이버가 하는 일에 관한 것입니다. 5~8 까지는 드라이버와 하드웨어가 어떻게 상호작용하는지를 설명합니닫.

설계를 시작하기 전 어떠한 테스트가 필요할지를 미리 생각합니다.

  • 드라이버가 초기화된 후에 모든 LED는 off 상태다.
  • LED 하나를 켤 수 있다.
  • LED 하나를 끌 수 있다.
  • 여러 개의 LED를 켜거나 끌 수 있다.
  • LED 모두 켜기
  • LED 모두 끄기
  • LED 상태 얻어오기
  • 경계 값 확인
  • 유효 범위를 벗어난 값 확인

첫 테스트 작성

참고하는 서적에서는 unity 라는 테스트 하니스를 사용하지만, 저는 cmocka를 이용하여 진행하겠습니다.

#include &lt;stdarg.h&gt;
#include &lt;stddef.h&gt;
#include &lt;setjmp.h&gt;
#include &lt;cmocka.h&gt;

#include &lt;stdint.h&gt;

#include &quot;led_driver.h&quot;

/* These functions will be used to initialize
   and clean resources up after each test run */
int setup (void ** state) {
  print_message(&quot;CMocka setup\n&quot;);
  return 0;
}

int teardown (void ** state) {
  print_message(&quot;CMocka teardown\n&quot;);
  return 0;
}

void LedsOffAfterCreate(void ** state) {
  uint16_t virtualLeds = 0xffff;
  LedDriver_Create(&amp;virtualLeds);
  assert_int_equal(0, virtualLeds);
}


int main (void) {
    const struct CMUnitTest tests [] =
    {
        cmocka_unit_test_setup_teardown(LedsOffAfterCreate, setup, teardown),
    };

    /* If setup and teardown functions are not
       needed, then NULL may be passed instead */

    int count_fail_tests =
        cmocka_run_group_tests (tests, setup, teardown);

    return count_fail_tests;
}

먼저 setup, teardown 함수를 정의합니다. setup는 테스트 시작 전 호출되는 함수이며, teardown 은 테스트 종료 후 호출되는 함수입니다. tests 에서는 필요한 테스트 목록을 작성하기 위한 배열입니다.

가장 먼저 LedsOffAfterCreate 라는 함수를 테스트하려 합니다. LedsOffAfterCreate 함수는 led가 초기화되서 전부 꺼지는지를 테스트하는 함수입니다. 함수에서는 가상의 LED 값을 설정하고 LedDriver_Create에 해당 주소를 넣었을 때 가상의 LED 가 모두 꺼지는지를 확인합니다. 이때 가상 LED의 값은 0이여야 합니다. assert_int_equal(0, virtualLeds); 는 virtualLeds의 값이 0이여야 테스트가 통과함을 의미합니다.

이후 실제 LED 드라이버를 구현하는 코드를 작성해줍니다.

#include &quot;led_driver.h&quot;

void LedDriver_Create(uint16_t * address) {
}
void LedDriver_Destroy(void) {
}

위와 같이 작성 후 컴파일하여 테스트를 실행해보겠습니다.

[==========] Running 1 test(s).
CMocka setup
[ RUN      ] LedsOffAfterCreate
[  ERROR   ] --- 0 != 0xffff
[   LINE   ] --- led_driver_test.c:25: error: Failure!
[  FAILED  ] LedsOffAfterCreate
CMocka teardown
[==========] 1 test(s) run.
[  PASSED  ] 0 test(s).
[  FAILED  ] 1 test(s), listed below:
[  FAILED  ] LedsOffAfterCreate

 1 FAILED TEST(S)

예상대로 테스트에 실패합니다. 이제 테스트가 실패하지 않도록 LedDriver_Create 함수를 작성해줍니다.

void LedDriver_Create(uint16_t * address) {
  *address = 0;
}

다시 컴파일한 뒤 테스트를 실행해보겠습니다.

[==========] Running 1 test(s).
CMocka setup
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
CMocka teardown
[==========] 1 test(s) run.
[  PASSED  ] 1 test(s).

테스트가 통과하였습니다. LedDriver_Create 가 의도대로 동작하고 있음을 의미합니다.

의존성 주입

LedDriver_Create 함수를 호출할 때 virtualLeds 를 전달하는 것은 ‘의존성 주입’ 을 사용한 것입니다. 컴파일 시 LED 주소를 고정하여 의존관계를 맺게 하는 대신 주소를 전달합니다. 이렇게 하면 타깃 시스템의 초기화 함수만 물리적인 LED 주소에 대해 컴파일 시점의 의존성을 갖게 됩니다.

의존성 주입 사용에 따른 부가적인 이익은 LED Driver의 재사용성이 높아진다는 점입니다. 드라이버를 라이브러리에 두고 LED 주소를 가지는 시스템에서 사용할 수 있습니다. 이는 TDD가 자연스럽고 유연하게 설계를 가져온다는 것을 보여줍니다.

테스트에 앞서 코드를 작성하지 말것

아래는 밥 마틴의 TDD의 3원칙입니다.

  • 실패하는 단위 테스트를 통과시키기 위한 경우에만 제품 코드를 작성하라.
  • 실패하는 단 하나의 단위 테스트만 작성하라. 빌드 실패도 실패다.
  • 실패하는 단 하나의 단위 테스트를 통과시킬 만큼만 제품 코드를 작성하라.
  • http://ArticleS.UncleBob.TheThreeRulesOfTdd (butunclebob.com) 참고

테스트 주도 개발은 테스트를 성공하기 위해 개발을 하는 방법론입니다. 테스트를 먼저 작성합니다. 이후 이 실패한 테스트를 성공하기 위한 코드를 작성하는 것입니다.

먼저 인터페이스를 테스트 주도로 개발하기

잘 설계된 모듈은 훌륭한 인터페이스가 필수적입니다. 처음 몇 개의 테스트는 인터페이스 설계를 이끌어냅니다. 인터페이스에 집중한다는 것은 개발하는 코드를 바깥에서 안으로 만들어간다는 것을 의미합니다. 이는 사용자 관점에서 더 편리한 인터페이스를 얻을 수 있습니다.

인터페이스를 구현할 때는 우선 하드코딩된 값을 반환하는 것부터 시작합니다. 여기서 핵심은 테스트가 아니라 인터페이스 설계를 유도하고 간단한 경계값 테스트를 얻는 것입니다.

드라이버의 주요 목적은 LED 를 On / Off 하는 것입니다. LED 마다 01부터 16까지 숫자가 붙어있습니다. 만약 1번 LED를 켜려면 드라이버가 0x0001을 LED의 메모리 매핑 주소에 쓰면 됩니다.

이제 LED를 켜는 테스트 코드를 작성하겠습니다.

void TurnOnLedOne(void ** state) {
  uint16_t virtualLeds = 0xffff;
  LedDriver_Create(&amp;virtualLeds);
  LedDriver_TurnOn(1);
  assert_int_equal(1, virtualLeds);
}

이와 같이 테스트를 작성하고 컴파일을 하면 컴파일 오류가 발생합니다. LED 드라이버의 헤더에 함수 프로토타입을 작성하고 c파일에 뼈대만 입력해줍니다.

void LedDriver_TurnOn(int ledNumber) {
}

이제 컴파일에 성공하게 될것입니다. 컴파일 후 테스트를 수행하면 결과가 실패하는 것을 알 수 있습니다.

[==========] Running 2 test(s).
CMocka setup
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[  ERROR   ] --- 0x1 != 0
[   LINE   ] --- led_driver_test.c:32: error: Failure!
[  FAILED  ] TurnOnLedOne
CMocka teardown
[==========] 2 test(s) run.
[  PASSED  ] 1 test(s).
[  FAILED  ] 1 test(s), listed below:
[  FAILED  ] TurnOnLedOne

 1 FAILED TEST(S)

이 함수를 통과하기 위해서는 LedDriver_TurnOn 함수에서 LED 메모리의 주소를 1로 변경해야 합니다. 그러기 위해서는 먼저 LedDriver_Create 함수에서 입력되는 LED 메모리 주소의 값을 보관해야합니다.

uint16_t * ledsAddress;
void LedDriver_Create(uint16_t * address) {
  ledsAddress = address
  *address = 0;
}

이와 같이 전역변수로 LED 메모리 주소를 가지고 있으면 LedDriver_TurnOn 함수에서 LED 메모리주소의 값을 변경할 수 있습니다. 위의 테스트에서 가장 간단하게 해결할 수 있는 방법은 LedDriver_TurnOn 함수에서 LED 메모리의 값을 1로 변경하는 것입니다.

void LedDriver_TurnOn(int ledNumber){
  *ledsAddress = 1;
}

이제 테스트를 진행해보면 모두 통과하는 것을 볼 수 있습니다.

[==========] Running 2 test(s).
CMocka setup
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[       OK ] TurnOnLedOne
CMocka teardown
[==========] 2 test(s) run.
[  PASSED  ] 2 test(s).

구현이 잘못 되었다!

많은 개발자들이 하드코딩, 그것도 명확히 문제가 있는 코드를 보면 마음이 편치 않습니다. 최종 구현은 최하위 비트만 설정해야 합니다. 지금 테스트만 보았을 때는 구현이 의도대로는 되었습니다. 만약 TDD를 하고 있지 않다면 이 코드는 그대로 남겨둘 수 있습니다. 그렇게 되면 나중에 버그가 발견될 수 있습니다.

테스트 목록을 진행하면서 이러한 구현은 남겨지지 않게 될것읍니다. 만약 하드코딩을 했는데 해당 부분에 대한 테스트가 목록에 없다면 당장 추가해야 합니다.

테스트가 정답!

위에서 작성한 테스트는 1번 LED를 켜는 테스트를 하였습니다. 현 시점에서 테스트는 통과하였습니다. 하지만 다른 LED를 켜게 되면 해당 테스트는 실패하게 됩니다. 그러면 이전에 작성한 하드코딩한 코드는 바로 수정하게 될 것입니다.

테스트에서 필요하기 전에 코드를 추가하면 복잡성이 높아집니다. TDD는 개발 전에 올바른 테스트를 먼저 작성해야 합니다. 올바른 테스트가 만든 뒤에야 코드를 작성해야 합니다.

다음 테스트 선택하기

현재 개발중인 LED 드라이버에서 큰 그림을 얻을 수 있도록 인터페이스를 발전해야 합니다. 먼저 이전 테스트에서 LED를 끄는 테스트를 추가해보겠습니다. 켜기와 끄는 기능은 상호보완적 관계이며, 추후 LED 조작이 서로 간섭되지 않는것을 검증할 수 있습니다.

void TurnOnLedOne(void ** state) {
  uint16_t virtualLeds = 0xffff;
  LedDriver_Create(&amp;virtualLeds);
  LedDriver_TurnOn(1);
  LedDriver_TurnOff(1);
  assert_int_equal(0, virtualLeds);
}

이후 테스트가 통과할 수 있는 코드를 작성합니다.

void LedDriver_TurnOff(int ledNumber)
{
  *ledsAddress = 0;
}

이후 테스트를 실행하면 모든 테스트가 통과됩니다.

[==========] Running 2 test(s).
CMocka setup
[ RUN      ] LedsOffAfterCreate
[       OK ] LedsOffAfterCreate
[ RUN      ] TurnOnLedOne
[       OK ] TurnOnLedOne
CMocka teardown
[==========] 2 test(s) run.
[  PASSED  ] 2 test(s).

점진적 진행

속인 다음 제대로 만들기

LED 드라이버는 LED 주소에 하드코딩된 값을 써서 테스트를 속일 수 있었습니다. 테스트가 더 많아지면 속이기는 쉽지 않습니다. 이렇게 되면 제대로 구현하는 편이 더 간단할 것입니다.

만약 실제 구현을 하는 것 보다 속이는것이 더 어려워지면 실제 구현으로 들어가는 것입니다. 이는 추후 이해할 수 있게 됩니다.

테스트는 작고 초점이 맞도록 유지!

1번 LED 를 off 시키는 것을 테스트하기 가장 쉬운 방법은 위와 같이 이전의 LED on 테스트 밑에 함수를 추가하는 것입니다. 이렇게 되면 테스트의 초점을 잃을 수 있습니다. 이렇게 되면 테스트가 실패할 수 있는 원인이 두가지 있습니다.

  1. LedDriver_TurnOn 이 잘못되어서 실패하거나 LedDriver_TurnOff 이 잘못되어서 실패할 수 있습니다.
  2. LedDriver_TurnOn 의 동작이 제대로 테스트되지 않습니다.

보통 TDD 초보들은 테스트 하나에 너무 많은 내용을 넣으려고 합니다. 하지만 이는 가독성을 떨어뜨리고 초점을 잃게 됩니다. 테스트는 읽기 쉽고, 크기가 작고, 초점을 맞춘 상태를 유지해야 합니다.

완전한 상태에서 리팩터링 하기

리팩터링을 안심하고 할 수 있는 유일한 때는 테스트가 모두 통과하는 때입니다. 테스트가 하나라도 통과하지 않으면 리팩터링을 하지 않는게 좋습니다. 테스트가 실패할 때는 코드의 동작을 고정시킬 수 없기 때문입니다. (리팩터링은 12장에서 깊게 다룬다고 합니다)

우리가 작성한 테스트 코드에도 냄새가 나기 시작했습니다. 테스트 케이스마다 vurtualLeds를 만들고 LedDriver_Create 를 호출합니다. 또한 LedsOffAfterCreate 는 특수한 경우를 다루므로 그대로 두어야 하며, 중복 테스트를 아래와 같이 테스트케이스 밖으로 꺼내야 합니다.

uint16_t virtualLeds;

int setup (void ** state) {
  LedDriver_Create(&amp;virtualLeds);
  print_message(&quot;CMocka setup\n&quot;);
  return 0;
}

int teardown (void ** state) {
  print_message(&quot;CMocka teardown\n&quot;);
  return 0;
}

void LedsOffAfterCreate(void ** state) {
  virtualLeds = 0xffff;
  LedDriver_Create(&amp;virtualLeds);
  assert_int_equal(0, virtualLeds);
}

void TurnOnLedOne(void ** state) {
  LedDriver_TurnOn(1);
  assert_int_equal(1, virtualLeds);
}

void TurnOnLedOff(void ** state) {
  LedDriver_TurnOn(1);
  LedDriver_TurnOff(1);
  assert_int_equal(0, virtualLeds);
}

테스트 주도 개발의 상태 기계

앞으로 할 일은 새로 추가할 기능을 결정하고, 원하는 결과를 테스트로 표현하는 것입니다.

인터페이스가 테스트와 잘 맞추지고 나면 링크 오류가 발생합니다. 그러면 뼈대 구현을 의도적으로 틀리게 추가합니다. 만약 실패를 기대했는데 통과한다면 테스트 내부에 문제가 있음을 나타냅니다.

테스트 실패를 확인하면 이제 테스트를 통과하도록 코드를 작성합니다. 테스트가 통과했다면 리팩터링을 통해 코드를 깔끔하게 만들어줍니다.

image

테스트FIRST

『Agile in a Flash』[OL11]에서 팀 오팅거(Tim Ottinger)와 제프 랭거(Jeff Langr)는 단위 테스트의 5가지 중요한 속성을 정의하였습니다. 테스트는 FIRST일 때 가장 효과적입니다.

  • Fast: 빠르다. 아주 빨라서 조금씩 수정할 때마다 테스트를 실행시켜도 결과를 기다 리느라 흐름이 깨지지 않는다.
  • Isolated: 격리되어 있다. 다른 테스트보다 먼저 실행되어야 하는 테스트가 없다. 테 스트의 실패도 서로 격리되어 있다.
  • Repeatable: 반복 가능하다. 반복 가능하다는 것은 자동화 되었음을 의미한다. 테 스트를 반복해서 테스트해도 항상 같은 결과가 나온다.
  • Self-verifying: 자신의 실행 결과를 자체적으로 확인한다. 모든 테스트가 통과하는 경우에는 단순히 “OK”를 보고하고 실패하는 경우에는 간결하게 세부 내용을 제공한다.
  • Timely: 시기적으로 적절하다. 프로그래머가 제품 코드에 딱 맞춰 (직전에) 테스트 를 작성하여 버그를 방지한다

</description> Sun, 22 May 2022 00:00:00 +0000 https://sunghwan7330.github.io/embedded_c_tdd/embedded_c_tdd_03/</link> https://sunghwan7330.github.io/embedded_c_tdd/embedded_c_tdd_03/

    <category>임베디드</category>
    
    <category>TDD</category>
    
    
    <category>embedded_c_tdd</category>
    
  </item>

  <item>
    <title>[임베디드 C를 윈한 TDD] 1장 테스트 주도 개발</title>
    <description># 1. 테스트 주도 개발이란?

테스트 주도 개발은 점진적으로 소프트웨어를 개발하는 기법입니다. 개발 코드를 바로 작성하지 않습니다. 기대에 대한 테스트를 작성한 뒤 해당 테스트가 실패하는 것을 확인한 후 테스트가 성공하도록 코드를 작성하는 개발 방법입니다.

테스트 자동화는 TDD에서 매우 중요합니다. TDD를 진행할 때는 각 단계마다 자동화된 단위 테스트를 새로 만들고, 그 테스트를 만족시키는 코드를 작성하게 됩니다. 제품 코드가 늘어나면서 단위테스트도 같이 늘어납니다. 코드를 고칠때마다 테스트 모음을 실행시키면서 새로 작성한 코드의 기능이 재대로 동작하는지 확인할뿐만 아니라 기존의 코드도 여전히 잘 동작하는지 확인할 수 있습니다.

소프트웨어는 깨지기 쉽습니다. 어떠한 변경을 하더라도 의도하지 않은 결과를 가져올 수 있습니다. 만약 테스트를 수작업으로 해야된다면 모든 테스트를 수행하기 어렵습니다. 테스트를 반복하는 비용이 너무 높기 때문에 우리는 일반적으로 개발한 부분에 대해서만 테스트를 진행합니다. 이로 인해 결함이 발생했는지 모르고 제품이 릴리즈되는 경우가 생깁니다. TDD는 자동화된 테스트 적분에 의도하지 않은 결과의 검출을 쉽게 도출할 수 있습니다.

TDD 마이크로 사이클

테스트 코드를 왕창 작성하고 나서 제품코드를 작성하는 것은 TDD가 아닙니다.

TDD는 작은 테스트 하나를 작성하고, 기존에 작성해놓은 테스트를 모두 통과하면서 추가한 테스트 역시 통과하도록 코드를 작성하는 것입니다.

아래 목록은 켄트 벡(Kent Beck)의 책, 『테스트 주도 개발』[Bec02]에 설명된 TDD 를 근거로 하여 정리한 TDD 사이클의 단계입니다.

  1. 작은 테스트를 하나 추가한다.
  2. 모든 테스트를 실행하여 새로 추가한 테스트가 실패하는 것을 눈으로 확인한다. 컴파일이 안 되는 것조차도 실패는 실패다
  3. 실패한 테스트를 통과시키기 위해 필요한 만큼만 조금 수정한다.
  4. 모든 테스트를 실행하여 새로 추가한 테스트가 통과하는지 확인한다.
  5. 중복을 제거하고 의도가 잘 표현되도록 리팩터링한다

TDD 사이클을 한번 마치는데에 걸리는 시간은 길어야 몇분 정도여야 합니다. 테스트와 코드는 점진적으로 추가됩니다. 코드가 새로 추가될 때 마다 작성되어있던 테스트들이 즉각적인 피드백을 주게 됩니다.

무언가를 수정할 때 마다 테스트를 실행해야 합니다. 테스트는 새로 작성한 코드가 동작하는지를 알려주면서, 수정으로 인한 의도하지 않은 결과가 생겼을 때 경고를 해줍니다.

TDD의 이득

  1. 버그 감소
  • 나중에 심각한 문제를 야기할 수 있는 크고 작은 논리적 오류들이 TDD를 진행하는 중에는 금방 발견할 수 있음
  1. 디버깅 시간 단축
  • 버그가 줄어든다는 것은 그 만큼 디버깅 시간도 줄어드는것을 의미함
  1. side-effect 결함 감소
  • 새로 작성하는 코드가 기존의 사양에 위배가 되면 테스트 실패로 확인이 가능해짐
  1. 마음의 평온
  • 철저히 테스트한 코드를 가지고 있다는 것은 자신감을 주게 됨
  1. 더 나은 설계
  • 좋은 설계는 테스트하기 쉬운 설계임
  • 긴함수, 서로 얽혀있는 코드, 복잡한 조건식들은 모두 테스트하기 어려운 코드임
  • 코드를 수정하기 위해 테스트 작성시 쉽게 작성할 수 없다면 설계적 문제가 드러난것임
  1. 진척 모니터
  • 어디까지 동작하고 얼마나 완료했는지를 테스트가 정확히 추척할 수 있음

임베디드 환경에서의 이득

  • 하드웨어가 준비되기 전에, 혹은 하드웨어 비용이 높아 여유분이 없을 때에 하드웨어에 독립적으로 제품 코드를 검증함으로써 위험을 줄인다.
  • 개발 시스템상에서 버그를 해결함으로써 타깃 컴파일, 링크, 업로드로 이어지는 시간이 오래 걸리는 작업의 횟수를 줄인다.
  • 문제를 찾고 고치기가 더 어려운 타깃 하드웨어상의 디버그 시간을 줄인다
  • 하드웨어와 소프트웨어 간의 상호작용을 테스트에서 모델링함으로써 상호작용 문제를 분리시킨다.
  • 모듈 간, 하드웨어와 소프트웨어 간의 의존성을 낮춤으로써 소프트웨어 설계를 개선한다. 테스트하기 쉬운 코드는 모듈화가 잘 된 코드다.

</description> Fri, 29 Apr 2022 00:00:00 +0000 https://sunghwan7330.github.io/embedded_c_tdd/embedded_c_tdd_01/</link> https://sunghwan7330.github.io/embedded_c_tdd/embedded_c_tdd_01/

    <category>임베디드</category>
    
    <category>TDD</category>
    
    
    <category>embedded_c_tdd</category>
    
  </item>

  <item>
    <title>[밑바닥부터 만드는 컴퓨팅] 12장 운영체제</title>
    <description># 1. 배경 

1.1 수학 연산

컴퓨터 시스템은 덧셈, 곱셈, 나눗셈같은 수학 연산을 지원합니다. 덧셈의 경우 ALU 수준에서 구현되며, 이는 3장에서 구현했었습니다. 곱셈 및 나눗셈같은 연산들은 하드웨어나 소프트웨어에서 처리되며, 비용 / 성능 요구사항에 따라 구현 방식이 달라집니다. 해당 절에서는 곱셈, 나눗셈 및 제곱근 연산을 OS 수준에서 구현합니다.

1.1.1 효율성 우선

수학 알고리즘은 n비트 2진수로 연산되며, 전형적인 컴퓨터 아키텍쳐에서는 32비트 또는 64비트를 사용합니다.

예를 들어 x * y 를 for문을 이용해 덧셈으로 구현한다고 해보겠습니다. 이때 y의 값이 최대값이라면 현존하는 가장 빠른 컴퓨터라고 해도 몇년이 걸릴수도 있습니다. 반면 곱셈 알고리즘을 이용하여 수행한다면 빠르게 수행이 가능합니다.

보통 알고리즘의 실행시간을 표기할때 보통 ‘빅 오’ 표기법인 O(n)을 사용합니다.

1.1.2 곱셈

image

위 그림의 알고리즘은 n 비트 숫자에 O(n) 덧셈 연산을 수행합니다. 비트 표현을 왼쪽으로 한 칸 이동시키거나 shiftedX 를 자기 자신에 더하면 shifted * 2 를 효율적으로 얻을 수 있습니다. 두 연산 모두 ALU 연산으로 쉽게 수행할 수 있습니다.

1.1.3 나눗셈

divide (x, y):
  // x&gt;=0이고 y &gt; 0일 때 x/y의 정수부
  if y&gt;x return 0
    q = divide(x, 2-y)
  if (x - 2 * q * y) &lt; y
    return 2 * q
  else
    return 2 * q + 1

이 알고리즘의 실행 시간은 재귀의 깊이에 좌우됩니다. 재귀의 각 단계에서 y의 값에 2가 곱해지고, y>x이면 중단하므로 재귀의 깊이는 x의 비트수인 n으로 제한됩니다. 그리고 재귀의 단계마다 상수 횟수만큼 덧셈, 뺄셈 및 곱셈 연산이 수행되므로 전체 실행시간은 O(n)이 됩니다.

1.1.4 제곱근

제곱근은 뉴턴 랩슨(newton-Raphson) 방법이나 테일러 급수 전개(Taylor series expansion) 과 같은 효율적인 계산 방법이 있습니다. 하지만 여기서는 더 간단한 방법으로 충분합니다. 제곱근 함수 y = √(x) 에는 다음과 같은 속성이 있습니다.

  • 단조 증가함수
  • 역함수 x = y^2 를 계산하는 방법을 알고 있음
sqrt(x):
  // y =vx의 정수부를 계산한다. 전략:
  // y≤x&lt; (y+1)인 정수 y를 찾는다. (0≤x&lt;2일 때)
  // 0...22-1의 범위에서 2진 탐색을 수행한다.
  y = 0
  for j = n/2 - 1 … 0 do
    …..
  return y

1.2 숫자의 문자열 표현

컴퓨터는 숫자를 내부적으로는 2진 코드로 표현합니다. OS는 특정 2진 코드를 문자로 변경하여 제공하는 루틴이 포함되있습니다. 간단한 문자들은 ASCII 코드로 정리되어 있습니다.

문자 ‘0’ 은 0x30이며 문자’9’는 0x39 입니다. 아래의 예시는 문자를 숫자로, 숫자를 문자로 변환하는 방법입니다.

// 음수가 아닌 숫자를 문자열로 변환한다.
int2String(n):
  lastDigit=n96 10
  c = lastDigit을 나타내는 문자
  if n &lt; 10
    return c (문자열 타입으로 반환)
  else
    return int2String(n/10).append(c)
// 문자열을 숫자로 변환한다.
string2Int(s);
  v = 0
  for i=1 ... (s의 length) do
    d = s[i]의 int 값
    v = v * 10 + d
  return v
  // s[1]이 s의 가장 윗자리 숫자를
  // 나타내는 문자라 가정한다.

1.3 메모리 관리

1.3.1 동적 메모리 할당

고수준 언어의 장점 중 하나는 프로그래머가 변수들에 RAM을 할당하고, 그 변수가 필요없을 때 메모리 공간을 재활용하는 세부 작업에 대해서 신경쓰지 않아도 된다는 점입니다. 모든 메모리 관리 작업들은 컴파일러, 운영체제, 가상머신이 처리해줍니다.

서로 다른 변수들은 프로그램의 생명 주기 내에서 서로 다른 시점에 메모리를 할당받습니다. 예로 정적 변수는 컴파일 시간에 컴파일러가 할당하는 반면, 지역변수는 서브루틴이 실행될 때 마다 스택상에 할당됩니다. 그 외의 메모리는 프로그램 실행 중 동적으로 할당되며, 이때 OS가 개입합니다. C 의 malloc 함수나 C++, 자바의 new 인스턴스를 사용할때마다 메모리 블록이 할당됩니다. 이후 더이상 쓸모없어졌을 때는 RAM 공간이 재활용됩니다. C, C++과 같은 언어는 메모리 공간 해제를 프로그래머가 직접 하지만, 자바와 같은 언어에서는 가비지컬렉션이 자동으로 처리합니다.

1.3.2 기초 메모리 할당 알고리즘

이 알고리즘이 다루는 데이터 구조는 단일 포인터로, free라 불리며 아직 할당되지 않은 힙 시작부분을 가리킵니다.


Initialization: free = heapBase
// size개의 단어들에 해당하는 메모리 블록을 할당한다.
alloc(size):
  pointer = free
  free = free + size
  return pointer

// 주어진 object의 메모리 공간을 해제한다.
deAlloc(object):
  do nothing

이 알고리즘의 경우 더이상 사용하지 않는 객체 공간을 회수하지 않아 공간낭비가 심합니다.

1.3.3 향상된 메모리 할당 알고리즘

이 알고리즘은 사용 가능한 메모리 세그먼트들의 연결 리스트인 freeList를 관리합니다. 각 세그먼트에는 두 가지 관리용 필드가 있습니다.

  • 세그먼트의 길이
  • 리스트에서 다음 세그먼트를 가리키는 포인터

image


Initialization:
    freeList=heapBase
    freeList length=heapLength
    freelist. next=null

// size 개수의 단어만큼 메모리 공간을 할당한다.
alloc(size):
    최적적합이나 최초적합 발견법으로 freeList를 탐색해서
      segment.length &gt; size인 세그먼트를 구한다.
    만약 그런 세그먼트를 못 찾으면, 실패값을 반환한다
      (또는 조각 모음을 시도한다).
    block=찾은 세그먼트에서 필요한 부분
      (또는 세그먼트에 남은 부분이 너무 작다면 전부)
    이 할당을 반영하도록 freeList를 업데이트한다.
    block-1]=size+1 // 메모리 해제를 위해 블록 크기를 기억한다.
    Retum block

// 객체를 해제한다.
dealloc(object):
    segment=object-1
    segment:length=object(-1)
    segment를 freeList에 삽입한다.

어떤 크기의 메모리 블록 할당을 요청 받으면 freeList 에서 적당한 세그먼트를 찾아야 합니다. 적당한 세그먼트가 발견되면, 그 세그먼트에서 요청된 메모리 블록을 가져옵니다. 다음으로 이 세그먼트에서 할당 후 나머지 부분이 freeList에 업데이트됩니다. 블록에 메모리가 없거나 나머지 부분이 실제로 너무 작다면 전체 세그먼트가 freeList에서 제거됩니다.

이와 같은 알고리즘은 시간이 지나면 블록 파편화 문제를 일으킬 수 있습니다. 따라서 분리된 세그먼트를 합쳐주는 일종의 조각모음 연산을 고려해야 합니다.

1.4 가변 길이 배열과 문자열

고수준 언어에서는 String 클래스와 같은 문자열 객체를 사용하여 간편하게 문자열을 사용합니다. 이 문자열 객체는 실제로는 배열로 구현이 가능합니다. 일반적으로 문자열이 생성되면 가능한 긴 길이의 배열이 할당됩니다. 그리고 문자열의 실제 길이는 이 최대길이보다 짧으며, 문자열 생애주기동안 그 길이가 저장됩니다. 보통 현재 문자열 길이를 넘어서는 배열 위치는 문자열에 포함되지 않는다고 간주합니다.

대부분의 프로그래밍 언어는 문자열타입 외에도 가변 길이 데이터타입을 지원합니다. 자바의 StringBuffer, C의 strXXX 함수들이 그 예시입니다. </description> Fri, 15 Apr 2022 00:00:00 +0000 https://sunghwan7330.github.io/make_computing_system/make_computing_system_12_operating_system/</link> https://sunghwan7330.github.io/make_computing_system/make_computing_system_12_operating_system/

    <category>밑바닥부터 만드는 컴퓨팅</category>
    
    <category>운영체제</category>
    
    
    <category>make_computing_system</category>
    
  </item>

  <item>
    <title>[밑바닥부터 만드는 컴퓨팅] 11장 컴파일러 1 : 코드 생성</title>
    <description># 1. 배경 

프로그램은 기본적으로 데이터를 조작하는 연산들의 나열입니다. 그러므로 고수준 프로그램을 저수준 언어로 컴파일할 때는 데이터 번역과 명령 번역이라는 두 가지 주요 문제가 있습니다.

1.1 데이터 번역

프로그램은 정수나 불(boolean)같은 단순한 타입에서, 배열이나 객체 같은 복잡한 타입까지 여러 종류의 변수 타입ㄷ들을 조작합니다. 변수의 생명주기와 범위의 종류도 중요한 요소입니다. 즉, 변수가 지역변수인지 전역변수인지, 인수인지에 ㄸ따라 다르게 처리하는 것이 중요합니다.

컴파일러는 프로그램 안에서 만나는 변수들마다 대상 플랫폼에서 변수 타입이 적절하게 수용되도록 매핑해야 합니다. 추가로 컴파일러는 변수의 종류에 따라 생명 주기와 범위를 관리해야 합니다.

1.1.1 기호 테이블

고수준 프로그램은 다양한 식별자를 도입합니다. 컴파일러는 식별자를 만나면 해당 식별자가 무엇을 나타내는지 알아야 합니다. 해당 식별자가 변수명인지, 글래스명인지, 함수명인지 알아야 하고, 변수라면 객체 필드인지 함수 필드인지, 변수 타입은 무엇인지를 알아야 합니다.

대부분의 컴파일러는 이러한 식별자를 매핑할 수 있는 테이블을 이용하여 관리합니다. 컴파일러는 소스코드에서 처음으로 새로운 식별자가 나타날 때 마다 그 상세 내용을 테이블에 추가합니다. 그리고 식별자가 다른 곳에서 나타나면 테이블을 참조해서 필요한 정보들을 모두 가져옵니다. 아래는 식별자를 저장하는 테이블의 예시입니다.

이름 타입 종류 #
nAccounts int static 0
id int field 0
name String field 1
balance int field 2
sum int argument 0
status Boolean local 0

예시로 balance=balancersum 라는 명령문이 있다면 valance가 필드번호 2 이고, sum이 필드번호가 0이라는 정보를 알아내서 계산하게 됩니다.

식별자들은 식별자가 인식되는 영역을 뜻하는 범위(scpoe)를 가집니다. 일반적으로는 바깥쪽 범위에서 안쪽 범위의 정의는 접근할 수 없습니다.

int a = 1
int main() {
  int b = 2;
  {
    int c = 3;
  }
  printf(a : %d\n, a);
  printf(b : %d\n, b); 
  printf(c : %d\n, c);  //error
  return 0;
}

위의 예제 코드는 main 함수에서 안쪽에서 생성된 변수인 c에 접근시 에러가 발생함을 확인할 수 있는 예제입니다. 컴파일러에서는 식별자를 찾을 때 자신의 지역에서 찾고, 없으면 바깥쪽으로 이동하여 찾습니다. 따라서 식별자의 볌위도 테이블에 기록해야 합니다.

1.1.2 변수 처리

컴파일러가 해결해야 할 역할 중 소스코드에 선언된 다양한 타입의 변수를 메모리에 매핑하는 일이 있습니다. 이 작업은 간단하지는 않습니다.

  • 변수 타입에 따라 메모리 크기가 다르므로 1:1 매핑이 아님
  • 변수 종류에 따라 생애주기가 다름
    • 정적 변수는 프로그램이 종료될때까지 유지됨
    • 객체 인스턴스들은 객체가 소멸될 때 메모리 반환이 되야 함

2단계 컴파일러 아키텍쳐에서는 변수들의 메모리 할당을 VM의 백엔드에서 처리하도록 하였습니다.

1.1.3 배열 처리

배열은 대부분 연속된 메모리 위치에 저장됩니다. 배열 이름은 보통 메모리에 할당된 RAM 블록의 시작 주소를 가리킵니다. 보통 OS에서는 size 만큼의 크기를 갖는 가용 메모리 블록을 찾아서 그 시작 주소를 반환하는 malloc 간은 함수가 있습니다. 만약 arr = new int[10] 과 같은 명령문은 컴파일러에서 arr = alloc(10) 과 같은 저수준 코드로 번역하게 됩니다.

image

위의 그림은 bar[k] = 19 를 실횅한 직후의 샘플입니다. bar = new int [10]; 를 하여 bar에는 int 배열의 시작주소가 저장됩니다. bar[k] = 19; 를 수행하면 bar 주소로부터 k 만큼 떨어진 주소에 19를 저장하게 됩니다.

1.1.4 객체 처리

어떠한 클래스 Employee 의 객체 인스턴스는 name, salary 와 같은 데이터 항목들과 이를 조작하는 메서드들이 캡슐화되어 있습니다. 컴파일러는 데이터와 연산을 상당히 다르게 처리합니다.

먼저 데이터 처리부터 보겠습니다. 데이터 처리는 배열 처리와 유사하여 객체 인스턴스의 필드들이 연속된 메모리 위치에 저장됩니다. 대부분의 객체 지향 언어에서는 클래스 타입 변수가 선언될 때 컴파일러는 포인터 변수만 할당합니다. 이후 생성자가 호출되어 실제 객체가 생성될 때 저장할 메모리 공간이 할당됩니다. 따라서 컴파일러가 클래스의 생성자를 컴파일할때는 먼저 클래스 필드의 개수와 타입을 보고, 객체 인스턴스를 메모리에 올리기 위한 크기를 계산하여야 합니다.

객체들은 시작 주소를 가리키는 포인터 변수로 표현되며, 객체에 캡슐화된 데이터는 시작 주소에서 시작하는 인덱스를 통해 접근이 가능합니다.

image

위 그림은 Complex 클래스와 이를 사용하는 함수가 메모리상에 어떻게 나타나는지에 대한 예시입니다.

1.2 명령 번역

해당 절에서는 고수준 명령이 어떻게 대상 언어로 변역되는지에 대해 살펴보려 합니다.

1.2.1 표현식 평가하기

image

위 그림은 x+g(2, y, -z) * 5 라는 구문을 분석하는 방법입니다. 이와 같은 트리를 파스 트리라 합니다. 코드 생성 알고리즘은 스택기반 플랫폼에서는 열폴란드 표기법으로 알려진 접미사 표기 형식으로 트리를 출력하면 됩니다.

표현식을 스택기반 코드로 번역하는 방법은 파스트리를 재귀적인 후위순회하는 방식으로 구성하면 됩니다.

1.2.2 흐름 제어하기

고수준 프로그래밍 언어는 if, while, for, switch 와 같은 다양한 흐름 제어 구조를 갖추고 있습니다. 반면 저수준 언어는 일반적으로 조건에 따른 goto만을 제공합니다.

image </description> Sun, 03 Apr 2022 00:00:00 +0000 https://sunghwan7330.github.io/make_computing_system/make_computing_system_11_compiler/</link> https://sunghwan7330.github.io/make_computing_system/make_computing_system_11_compiler/

    <category>밑바닥부터 만드는 컴퓨팅</category>
    
    <category>컴파일러</category>
    
    
    <category>make_computing_system</category>
    
  </item>

  <item>
    <title>[밑바닥부터 만드는 컴퓨팅] 10장 컴파일러 1 : 구문 분석</title>
    <description># 1. 배경 

일반적인 컴파일러는 구문 분석과 코드 생성의 두 모듈로 구성된다.

구문 분석 작업은 대개 두 모듈로 더 나눠집니다. 하나는 토큰화 모듈로, 입력 문자들을 언어 기본 요소들로 분류하는 모듈입니다. 또 하나는 파싱 모듈로 토큰화 결과로 나온 언어 기본 요소를 언어의 구문 규칙에 맞추는 모듈입니다.

image

1.1 어휘 분석

프로그램의 가장 단순한 구문 요소는 텍스트 파일에 저장된 문자열입니다.

프로그램 구문 분석의 첫 단계는 주석이나 공백을 무시하고 문자들을 토큰으로 분류하는 것입니다. 프로그램이 토큰화되면, 토큰들은 프로그램의 기본 원소가 됩니다.

image

1.2 문법

프로그램을 어휘 분석해서 토큰 스트림으로 만들고 나면, 이제 토큰 스트림을 파싱하여 형식 구조로 만들어야 합니다. 즉, 토큰들이 변수 선언, 명령문, 표현식 등과 같이 언어 구조 중에 어디에 해당하는지를 알아야 합니다.

image

위 그림은 C언어의 규칙 일부와 이 문법에 맞는 코드 예시입니다.

1.3 구문 분석

문법에 따라 입력 텍스트가 유효한지 확인해보는 행위를 구문분석이라고 합니다. 문법 규칙이 계층적이기 때문에 parser가 생성하는 출력은 parse tree나 derivation tree 라고 불리는 트리 기반 데이터 구조로 기술됩니다.

image

위 그림은 문법 조각에 따른 프로그램 조각의 파스 트리입니다. 삼각형은 더 아래 단계의 파스트리를 뜻합니다. </description> Thu, 31 Mar 2022 00:00:00 +0000 https://sunghwan7330.github.io/make_computing_system/make_computing_system_10_construction_analysis/</link> https://sunghwan7330.github.io/make_computing_system/make_computing_system_10_construction_analysis/

    <category>밑바닥부터 만드는 컴퓨팅</category>
    
    <category>컴파일러</category>
    
    <category>구문 분석</category>
    
    
    <category>make_computing_system</category>
    
  </item>

  <item>
    <title>[밑바닥부터 만드는 컴퓨팅] 9장 고수준 언어</title>
    <description>해당 장에서는 고수준 프로그램을 작성할 수 있는 잭(jack)이라는 언어를 소개합니다.  잭은 간단한 객체지향 언어입니다.  자바나 C# 같은 언어와 비슷하지만 문법이 더 단순하며, 상속을 지원하지 않습니다. 

10장과 11장에서는 잭 프로그램을 VM 코드로 변역하는 컴파일러를 만들고, 12장에서는 잭/핵 플랫폼의 간단한 운영체제를 작성할 예정입니다.

1. 배경

1.1 예제 1: Hello world

잭 언어는 Main.main 함수에서 시작됩니다. Main이라는 클래스 안에 main이라는 메서드가 있어야 합니다.

/* Hello World 프로그램 */ 
class Main { 
  function void main() { 
    /* 표준 라이브러리를 이용해서 텍스트를 출력한다. */ 
    do Output.printString(&quot;Hello World&quot;); 
    do Output.println(); // 새 라인 
    return; 
  } 
}

1.2 절차적 프로그래밍과 배열 처리

/* 정수열의 평균을 계산한다. */ 
class Main { 
  function void main() { 
    var Array a; 
    var int length; 
    var int i, sum; 

    let length = Keyboard. readInt(&quot;How many numbers? &quot;); 
    let a = Array.new(length); // 배열을 구성한다. 
    let i = 0; 

    while (i &lt; length) { 
      let a[i] = Keyboard. readInt(&quot;Enter the next number: &quot;); 
      let sum = sum + a[i]; 
      let i = i + 1; 
    } 

    do Output.printString(&quot;The average is: &quot;); 
    do Output.printInt(sum / length); 
    do Output.println(); 
    return; 
  } 
}

잭 프로그램은 표준 라이브러리에 내장된 Array 클래스를 통해 배열을 선언합니다. 잭 언어의 배열에는 타입이 없습니다. 정수, 객체 등등 어떤 것이라도 넣을 수 있습니다.

1.3 예제 3: 추상 데이터 타입

프로그래밍 언어는 기본 데이터 타입이 정해져 있는데, 잭은 int, char, boolean 세가지를 지원합니다. 추가로 필요한 경우 직접 클래스를 만들어 사용할 수 있습니다.

1.3.1 클래스 인터페이스 정의하기

이번 예시는 분수를 추상화하려 합니다.

// Fraction은 n/m을 표현하는 객체다. (n, m은 정수) 
field int numerator, denominator // Fraction 객체 속성 
constructor Fraction new(int a, int b) // 새로운 Fraction 객체를 반환한다. 
method int getNumerator() // 이 분수의 분자를 반환한다. 

method int getDenominator() // 이 분수의 분모를 반환한다. 
method Fraction plus (Fraction other) // 이 분수와 또 다른 분수의 합을 분수 객체로 반환한다. 
method void print() // 이 분수를 &quot;분자/분모&quot;  형식으로 출력한다.  필요한 분수 관련 기능을 여기에 추가한다

잭에서 현재 객체 수준의 연산은 메서드로 표현되고, 클래스 수준의 연산은 함수로 표현됩니다.

1.3.2 클래스 사용하기

// 2/3와 1/5를 더한다. 
class Main { 
  function void main() { 
    var Fraction a, b, c; 
    let a = Fraction. new(2,3); 
    let b = Fraction. new(1,5); 
    let c = a.plus(b); // c = a + b 24 
    do c.print(); // &quot;13/15&quot;가 출력되어야 한다. 
    return; 
  } 
}

1.3.3 클래스 구현하기

/* Fraction 타입 및 관련 기능을 제공한다. */ 
class Fraction { 
  field int numerator, denominator; 

  /* 주어진 분자 및 분모에서 
   * 새로운 (약분된) 분수를 생성한다. */ 
  constructor Fraction new(int a, int b) { 
    let numerator = a; 
    let denominator = b; 
    do reduce(); // a/b가 약분되지 않았으면 약분한다. 
    return this;
  }

   /* 이 분수를 약분한다. */ 
  method void reduce() {  
    var int g;
    let g = Fraction.gcd(numerator, denominator); 
    if (g &gt; 1) { 
      let numerator = numerator / g;
      let denominator = denominator / 9; 
    } 
    return; 
  }

  /** a와 b의 최대 공약수를 계산한다. */ 
  function int gcd(int a, int b){ 
    var int r; 
    while (~(b = 0)) { // 유클리드 알고리즘을 적용한다. 
      let r = a - (b * (a / b)); // rea/b의 나머지 
      let a = b; 
      let b = r; 
    } 
    return a; 
  }

  /** 접근자. */ 
  method int getNumerator() { return numerator; } 
  method int getDenominator() { return denominator; } 

  /* 이 분수와 또 다른 분수의 합을 반환한다. */ 
  method Fraction plus ( Fraction other){ 
    var int sum; 
    let sum = (numerator * other.getDenominator()) + (other.getNumerator() * denominator()); 
    return Fraction.new(sum, denominator * other.getDenominator()); 
  }
  // 추가 분수 관련 메서드들; minus, times, div 등 

  /* 이 분수를 출력한다. */ 
  method void print() { 
    do Output.printInt(numerator); 
    do Output.printString(&quot;/&quot;); 
    do Output.printInt(denominator); 
    return; 
  } 
}// Fraction 클래스

1.4 예제 4 : 연결 리스트 구현

/** List 클래스는 연결 리스트 추상화를 제공한다. */ 
class List { 
  field int data; 
  field List next; 

  /* 새로운 List 객체를 생성한다. */ 
  constructor List new(int car, List cdr) { 
    let data = car; 
    let next = cdr; 
    return this; 
  } 

  /* 이 List를 리스트 꼬리부터 재귀적으로 제거한다. */ 
  method void dispose() { 
    if (~(next = null)) { 
      do next.dispose(); 
    } 
    // OS 루틴을 이용해서 이 객체가 
    // 점유했던 메모리를 재활용한다. 
    do Memory.deAlloc(this); 
    return; 
  } 
  // 추가적인 리스트 관련 메서드는 여기에 쓴다. } // List 클래스
} List 클래스

이후 jack 언어에 대한 자세한 내용은 다루지 않겠습니다. 기본적으로 고수준 프로그래밍 언어를 다룰 줄 안다면 jack 언어를 사용하는데는 문제 없을것으로 생각됩니다. </description> Wed, 23 Mar 2022 00:00:00 +0000 https://sunghwan7330.github.io/make_computing_system/make_computing_system_09_high_level_language/</link> https://sunghwan7330.github.io/make_computing_system/make_computing_system_09_high_level_language/

    <category>밑바닥부터 만드는 컴퓨팅</category>
    
    <category>고수준 언어</category>
    
    
    <category>make_computing_system</category>
    
  </item>

  <item>
    <title>[밑바닥부터 만드는 컴퓨팅] 08장 가상 머신 2 : 프로그램 제어</title>
    <description># 개요 

해당 내용은 ‘밑바닥부터 만드는 컴퓨팅 시스템’ 책의 8장 내용을 정리하였습니다.

1. 배경

고수준 언어로 프로그램을 작성할 떄는 고수준의 표현을 사용할 쉬 있습니다. 근의공식의 경우 x=-b+sqrt(power(b, 2)-4*a*c) 와 같이 나타낼 수 있습니다. 고수준 언어는 다음과 같은 특징이 있습니다.

  1. sqrt나 power와 같은 고급 연산을 필요에 따라 정의할 수 있음
  2. 이러한 서브루틴을 기본 연산자처럼 호출할 수 있음
  3. 서브 루틴이 실행되고 나면 코드 내에 다음 명령으로 반환됨

이러한 특징이 있기 때문에 우리는 알고리즘을 작성할 때 한층 더 가까운 추상화 코드를 작성할 수 있습니다. 추상화 수준이 높아질수록 저수준에서 처리할 작업은 늘어납니다. 서브루틴 호출이 일어날 때 뒷단에서는 다음과 같은 처리가 필요합니다.

  • 호출지에서 호출된 서브루틴으로 매개변수 전달
  • 호출된 서브루틴을 실행하기 전에 호출자의 상태 저장
  • 호출된 서브루틴의 지역 변수에 공간 할당
  • 호출된 서브루틴을 실행하기 위해 점프
  • 호출된 서브루틴의 결팟값을 호출자로 반환
  • 값이 반환될 때, 호출된 서브루틴이 점유한 메모리 공간을 재활용
  • 호출자의 상태를 복구
  • 호출자 코드에서 이전에 점프했던 지점 바로 다음 코드를 실행하기 위해 점프

이러한 작업들은 고수준 언어를 해석하는 컴퍼일러에서 해결해줍니다.

1.1 프로그램 흐름 제어

컴퓨터 프로그램은 기본적으로 한 명령씩 순차적으로 실행합니다. 이러한 순차적 흐름은 가끔씩 분기 병령으로 끊기게 됩니다. (if, switch 등에 의한 분기) 저수준 프로그래밍에서는 분기 논리가 있을 때 goto destination 명령으로 프로그램의 특정 위치부터 실행을 지속하도록 하게 합니다. 위치 설정의 경우 여러가지 방법이 있지만, 일반적으로는 다음에 실행할 명령어의 물리적 주소를 지정하는 것이 일반적입니다.

다음은 if 문과 while 문의 제어 흐름 구조와 이를 의사 VM으로 변환한 코드입니다.

  • if 문 제어 흐름 구조
    if (cond)
    s1
    else
    s2
    ...
    
  • 의사 VM 코드
    VM code for computing ~(cond)
    if-goto L1
    VM code for executing s1
    goto L2
    label L1
    VM code for executing s2
    label L2
    ...
    
  • while 제어 흐름 구조
    while(cond)
    s1 
    ...
    
  • while 의사 VM 코드
    label L1
    M code for computing ~(cond)
    if-goto L2
    VM code for executing s1
    goto L1
    label L2
    ...
    

1.2 서브루틴 호출

프로그래밍 언어들마다 특정한 내장형 명령 집합이 있습니다. 이러한 기본적인 명령 집합을 프로그래머가 자유롭게 고수준 연산들로 확장 가능하다는 점이 현대 개발 언어의 핵심적인 추상화 원리입니다. 이처럼 정의된 고수준 연산 단위를 서브루틴(또는 프로시저, 함수 메서드 등)라고 부릅니다.

잘 설계된 프로그래밍 언어는 기본 내장형 멸령들과 유사하한 느낌을 갖습니다. 그렇기때문에 새로운 서브루틴을 구현하게 된다면 호출자 관점에서 기존의 멍령들고 비슷하게 보이는 게 이상적일 것입니다. 두 연산을 자연스럽게 결합해서 일관성 있고 읽기 쉬운 코드를 만들 수 있습니다.

image

서브루틴 호출시에 call 키워드가 쓰인다는 점을 제외하면 내장형 명령 호출과 사용자 정의 서브루틴 호출의 차이는 없습니다. 두 연산 모두 호출자가 인수를 설정해줘야 하며, 스택에서 인수가 삭제되고 반환값이 스택 최상단에 입력됩니다.

power 와 같은 서브루틴은 보통 지역변수를 임시 저장소로 활용합니다. 지역변수는 서브루틴 시작부터 return 시점까지 메모리에 유효하며, return 이후에는 메모리 공간이 해제됩니다. 만약 서브루틴 안에 서브루틴을 호출하는 구조가 여러번 반복된다면 중첩되는 상황이 점점 복잡해지게 됩니다. 이렇게 되면 서브루틴 안에서 새로운 서브루틴을 호출한다면, 그 서브루틴이 끝나야 기존의 서브루틴을 진행한다는 특징이 있습니다. 이는 후입선출 구조인 스택의 구조와 맞아떨어지게 됩니다. 아래의 그림은 서브루틴 내부에서 서브루틴을 호출하는 구조가 여러번 반복되었을 때에 대한 스택 구조를 나타낸 그림입니다.

image

2. VM 명세 2부

이번 장에서는 7장의 기초 VM 명세에 프로그램 흐름 제어 명령과 함수 호출 명령을 추가해서 전체 VM 명세를 완성하는 것을 목표로 합니다.

2.1 프로그램 흐름 제어 명령

제작하는 VM 언어에는 세가지 프로그램 흐름 제어 명령이 있습니다.

2.1.1 lable lable

이 명령은 함수코드의 위치에 레이블을 붙입니다. 프로그램 내 다른 지접에서는 레이블이 표시된 위치로만 점프 가능합니다. 레이블의 유효 범위는 레이블이 정의된 함수 내부까지입니다.

2.1.2 goto lable

이 명령은 무조건 분기 명령으로 lable 부터 실행하라는 명령입니다. 점프할 위치는 같은 함수 내에 있어야 합니다.

2.1.3 if-goto lable

이 명령은 조건 문기 명령입니다. 스택의 최상단 값을 꺼내서 그 값이 0이 아니면 lable 로 표시된 위치에서 실행을 계속하고, 값이 0이면 프로그램 다음 령명을 실행합니다. 점프할 위치는 같은 함수 내에 있어야 합니다.

2.2 함수 호출 명령

함수는 전역적으로 호출할 때 쓰이는 기호로 된 이름이 있습니다. 함수 이름의 범위는 전역적입니다. 즉, 모든 파일 내의 함수 이름들이 공유되며, 그 함수 이름으로 서로 호출할 수 있습니다. 이 VM 언어에서는 세종류의 함수 관련 명령이 있습니다.

2.2.1 function f n

해당 위치부터 이름이 ⨍ 고 지역변수가 n개인 함수가 시작됩니다.

2.2.2 call f m

함수 ⨍ 를 호출합니다. 이때 호출자가 스택에 m개의 인수를 push 했다는 사실도 같이 전달됩니다.

2.2.3 return

호출한 함수로 반환됩니다.

2.3 함수 호출 규약

함수를 호출하고 반환되는 이벤트는 호출하는 함수와 호출되는 함수의 두 관점에서 볼 수 있습니다.

  • 호출하는 함수 관점
    • 함수를 호출하기 전에 호출자는 필요한 수의 인수를 스택에 push 해야 함
    • 다음으로 호출자는 call 명령으로 함수를 불러옴
    • 호출된 함수가 반환된 후에는, 호출자가 호출하기 전에 push 한 인수들은 스택에서 사라지고 반환값이 스택 최상단에 있게 됨
    • 호출된 함수가 반환된 후에, 호출자의 메모리 세그먼트들은 호출 전과 동일하며, temp 세그먼트는 미정의 상태임
  • 호출되는 함수 관점
  • 호출된 함수가 실행을 시작할 때 argument 세그먼트는 호출자가 넘겨준 실제 인수 값으로 초기화되며, local 변수 세그먼트는 할당되고 0으로 초기화됨
  • 호출된 함수가 바라보는 static 세그먼트는 그 함수가 속하는 VM 파일의 static 세그먼트로 설정되며, 작업 스택은 비어있음
  • this, that, pointer, temp 세그먼트들은 시작 전에는 미정의 상태임
  • 반환되기 전에 호출된 함수는 값을 하나 스택에 push해야함

2.4 초기화

VM 프로그램은 고수준 프로그램을 컴파일한 결과로 나온 VM 함수들의 모음입니다. VM 이 실행될 때는 항상 Sys.init이라는 인수 없는 VM 함수를 먼저 실행하게 됩니다. 이 함수는 사용자 프로그램의 메인 함수를 호출합니다. 따라서 VM 코드를 생성하는 컴파일러는 프로그램 번역시 Sys.init 함수가 있는지 확인해야 합니다. </description> Sat, 12 Mar 2022 00:00:00 +0000 https://sunghwan7330.github.io/make_computing_system/make_computing_system_08_program_control/</link> https://sunghwan7330.github.io/make_computing_system/make_computing_system_08_program_control/

    <category>밑바닥부터 만드는 컴퓨팅</category>
    
    <category>컴퓨터 아키텍쳐</category>
    
    <category>가상머신</category>
    
    
    <category>make_computing_system</category>
    
  </item>

  <item>
    <title>[밑바닥부터 만드는 컴퓨팅] 07장 가상 머신 1 : 산술 스텍</title>
    <description># 개요 

해당 내용은 ‘밑바닥부터 만드는 컴퓨팅 시스템’ 책의 7장 내용을 정리하였습니다.

1. 배경

1.1 가상 머신 패러다임

고수준 언어가 컴퓨터에서 실행되려면 먼저 기계어로 번역되어야 합니다. 이 과정을 컴파일이라고 부르는데, 이는 상당히 복잡한 프로세스입니다. 고수준 언어를 대상 기계어로 번역하려면 별도의 컴파일러를 따로 만들어야 합니다. 그렇기때문에 고수준 언어와 기계어의 특성에 따라 수많은 컴파일러가 생겨야 합니다.

컴파일러에서 기계어의 종속성을 분리하는 방법 중 하나는 전체 컴파일 과정을 두 단계로 나누는 것입니다. 이 단계는 고수준 언어와 기계어의 중간 단계입니다. 즉, 고수준 언어는 중간단계 언어로 번역되고, 중간단계 언어에서 각 하드웨어에 맞게 기계어로 변환하는 것입니다.

먼저 먼저 중간단계 언어를 명령어로 하는 가상 머신을 정의합니다. 그리고 컴파일러를 고수준 언어 -&gt; 중간 단계 언어, 중간 단계 언어 -&gt; 기계어 이 두 단계로 나눕니다.

이러한 컴파일 모델은 여러종류의 컴파일러에 적용되어 있습니다. 많이 사용되는 자바를 예로 들면 javac라는 컴파일러를 이용하여 자바 코드를 바이트코드로 변환합니다. .java 파일을 컴파일하였을때 나오는 .class 파일이 바이트코드로 변환된 파일입니다. 그리고 바이트코드는 JVM이라는 가상머신에서 동작하게 됩니다. JVM은 바이트코드를 기계어로 변환하여 실행하는 역할을 하게 됩니다.

이러한 가상 머신 언어 개념은 실용적이며 몇몇 장점을 가지고 있습니다. 먼저 가상머신만 바꾸면 여려 플랫폼에서 실행할 수 있다는 점입니다. 다음으로 여러 언어의 컴파일러들이 같은 VM 백엔드를 공유함으로써 코드를 공유하고 운용하기 편하다는 점힙니다.

가상머신의 또 다른 장점은 모듈성입니다. VM 구현이 효율적으로 될대마다 해당 언어로 번역되는 모든 프로그램이 개선된다는 점입니다.

아래의 그림은 앞써 말한 특장점을 표현하는 그림립니다.

image

1.2 스택 머신 모델

VM도 다른 프로그래밍 언어들처럼 산술 연산, 메모리 접근 연산, 프로그램 흐름 제어 연산, 서브루틴 호출 연산으로 구성됩니다. 언어를 구현하는 방법론은 어려가지 있지만, 이 중 핵심은 VM의 연산자와 피연산자와 결과값을 어디에 저장하는가 입니다. 스택 데이터 구조를 사용하는 방법이 가장 깔끔할 것입니다.

스택 머신 모델에서 산술 명령은 스택의 최 상단에서 피연산자를 꺼내고(pop), 그 결과를 다시 스택의 최 상단에 넣습니다.(push) 그 외의 다른 명령들의 경우 스택의 최상단과 지정된 메모리 주소 사에에 데이터가 이동됩니다. 이 간단한 스택 연산으로 산술 및 논리 연산도 구현이 가능합니다. 또한 어떠한 프로그래밍 언어로 작성되었더라도 동일한 기능을 하는 스택 머신 프로그램으로 번역이 가능합니다.

1.2.1 기본 스택 연산

스택은 push와 pop을 기본 연산으로 하는 자료구조입니다. image

스택 머신은 기존의 메모리 접근방식과 다릅니다. 먼저 스택은 최상단에 있는 한 항목씩만 접근이 가능합니다. 또한 스택 읽기는 손식 연산입니다. 스택에서 pop 하는순간 자로구조에서 사라지기 때문입니다. 일반적인 메모리 구조에서는 메모리에 읽을때는 메모리에 변화가 없다는 점이 차이가 있습니다. 또한 기존의 메모리 구조는 값을 쓰면 기존 값을 덮어쓰는 반면 스택구조는 기존의 데이터를 유지하고 스택의 맨 위에 값을 쓰게 됩니다.

image 예를 들어 덧셈의 처리는 위의 그림과 같습니다. add 연산자 처리를 위해 스택에서 pop을 두번해서 나온 값을 더한 뒤 다시 스택에 push합니다.

image 다른 예로 d=(2-x)*(y+5) 라는 식을 계산하는 방법이다.

image 이번에는 불 표현식인 if (x&gt;7) or (y=8) then ... 을 스택 기반으로 계산하는 방법입니다.

2. VM 명세 1부

2.1 일반

해당 책에서 사용하는 가상머신은 스택 기반입니다. 즉, 모든 연산은 스택 위에서 이뤄집니다. 또한 함수 기반으로 프로그램이 수행됩니다. 이 VM 언어에는 정수, 불 대수, 포인터로 쓰이는 16비트 데이터 타입 하나만 정의됩니다. 이 언어에는 네 가지 종류의 명령으로 구성됩니다.

  • 산술 명령 : 스택에서 산술 및 논리 연산을 수행함
  • 메모리 접근 명령 : 스택과 가상 메모리 세그먼트 사이에 데이터를 주고 받는 명령
  • 프로그램 흐름 명령 : 조건 및 무조건 분기 연산을 가능하게 함
  • 함수 호출 명령 : 함수를 호출하고 결과를 반환함

가상머신은 크게 두 단계로 나눠집니다. 해당 장에서는 산술 명령 및 메모리 접근 명령을 정의하고, 그 명령들을 구현한 기본 VM 번역기를 만드는 것을 목표로 합니다. 다음 장에서는 프로그램 흐름 제어 명령 및 함수 호출 명령을 정의하여 가상 머신을 확장할 예정입니다.

2.2 산술 및 논리 명령

이 VM 언어에는 9개의 스택 기반 산술 및 논리 명령들이 있습니다. 이 명령 중에 7개는 2항 명령입니다. 즉, 스택에서 두개의 항목을 꺼네서(pop) 연산한 후 그 결과를 다시 스택에 넣습니다.(push) 나머지 두개는 단항 명령으로 스택에서 하나의 항목을 꺼네서(pop) 연산한 후 그 결과를 다시 스택에 넣습니다.(push)

명령 반환값 설명
add x + y 정수 덧셈
sub x - y 정수 뺄셈
neg -y 산술 부정
eq true if x = y, else false 같음
gt true if x > y, else false ~ 초과 (greater than)
lt true if x < y, else false ~ 미만 (less than)
and x And y 비트 단위
or x Or y 비트 단위
not Not y 비트 단위

2.3 메모리 접근 명령

이 VM 에서는 8개의 가상 메모리 세그먼트들을 조작하는 메모리 접근 명령을 수행합니다.

image

2.2.1 메모리 접근 명령

모든 메모리 세그먼트는 두 개의 동일한 명령으로 접근 가능합니다.

  • push segment index : segment[index] 값을 스택에 넣는다 (push)
  • pop segment index : 최상단 스택에서 값을 꺼내서(pop) segment[index] 에 저장한다

여기서 segment는 8개의 세그먼트 이름 중 하나이며, index는 음수가 아닌 정수입니다. 예를 들어 push argment 2 다음 pop local 1 명령은 함수의 세번째 인수 값을 함수의 두번째 지역 변수에 저장하라는 의미가 됩니다.

아래는 VM파일, VM 함수 및 가상 메모리 세그먼트간의 관계를 나타낸 그림입니다.

image

VM의 push와 pop 명령을 명시적으로 관리하는 8개의 메모리 세그먼트 외에도 VM은 스택과 힙이라고 하는 두개의 두개의 데이터 구조를 내부에서 관리합니다.

힙은 VM 뒷단에 존제하는 메모리 공간으로 객체와 배열 데이터를 저장하는데 사용하는 RAM 영역입니다.

2.4 프로그램 흐름과 함수 호출 명령

본 VM에서 자세히 설명해야할 6가지 명령이 있습니다. 이는 아래와 같으며 해당 내용은 다음장에 설명할 예정입니다. (책에 그렇게 써있음 ㅠㅠ)

프로그램 흐름 명령
label symbol    // 레이블 선언
goto symbol     // 무조건 분기
if-goto symbol  // 조건 분기

함수 호출 명령
function functionName n Locals // 함수 선언. 함수의 지역 변수 개수를 지정함.
call functionName nargs        // 함수 호출. 함수의 인수 개수를 지정함.
return                         // 호출하는 함수로 제어를 되돌림

2.5 잭 VM 핵 플랫폼의 프로그램 요소들

image

위의 그램은 jack 언어가 컴파일되는 과정을 나타냅니다. jack 언어는 9장에서 설명합니다.

잭 클래스는 각각 하나 이상의 메서드로 구성됩니다. 잭 컴파일러가 n 개의 클래스 파일이 들어있는 디렉터리에 적용되면 n개의 VM 파일이 생성됩니다.

다음으로는 VM 번역기가 VM 파일들이 있는 디렉토리에 적용되어 하나의 어셈블리 프로그램이 생성됩니다.

번역된 어셈블리 코드는 어셈블러에 의해 2진 코드로 번역되어 실행 가능한 프로그램으로 변경합니다.

2.6 VM 프로그래밍 예제

해당 절에서는 일반적인 프로그래밍 작업들이 어떻게 VM 추상화로 표현할 수 있는지를 설명합닏닫.

2.6.1 일반적인 산술 작업

  • C 언어
    int mult(int x, int y) {
    int sum;
    sum = 0;
    for(int j = y; j != 0; j--)
      sum += x; // 514
    return sum;
    }
    
  • 1차 근사
    function mult
    args x, y
    vars sum, j
    sum = 0
    j = y
    loop:
    if j = 0 goto end
    sum = sum + X
    j = j - 1
    goto loop
    end:
    return sum
    
  • 의사 VM 코드
    function mult(x,y)
    push 0
    pop sum
    push y
    pop j
    label loop
    push 0
    push j
    eq
    if-goto end
    push sum
    push x
    add
    pop sum
    push j
    push 1
    sub
    pop j
    goto loop
    label end
    push sum
    return
    
  • 최종 VM 코드 ``` function mult 2 // 2개의 지역 변수 push constant 0 pop local 0 // sum=0 push argument 1 pop local 1 // j=y label loop push constanta push local 1 Eq if-goto end // if j=0 goto end push local 0 push argumento Add pop local 0 // SUM=SUM+X push local 1 push constant 1 Sub pop local 1 1/ j=j-1 goto loop label end push local 0 return // return sum

![image](https://user-images.githubusercontent.com/35713051/159123330-667385a9-81c9-4599-83b2-6de35526d2e8.png)

### 2.6.2 베열 처리 

배열은 인덱스가 매겨진 객체들의 집합입니다. 
`int bar[10]` 배열에 각 10개의 정수값이 입력되어있고, bar의 주소가 4315라고 가정해보겠습니다. 

만약 `bar[2] = 19` 를 연산하려면 어떻게 해야될까요?

* VM 코드 

// bar 배열이 이 고수준 프로그램에서 처음 선언된 지역 변수라 가정하자. // 다음 M 코드는 bar[2]=19, 즉 *(bar+2)=19를 구현한 것이다. push local o // bar의 시작 주소를 얻는다. push constant 2 add pop pointer 1 // that의 시작 주소를 (bar+2)로 설정한다. push constant 19 pop that 0 // *(bar+2)=19 …


![image](https://user-images.githubusercontent.com/35713051/159123347-00a6b599-216a-4931-b7ce-1d8e4e2bc53a.png)

![image](https://user-images.githubusercontent.com/35713051/159123354-d4c0e7d6-e48b-4221-9021-fe0f1b35d754.png)


### 2.6.3 객체 처리 

고수준 프로그래머 관점에서 객체는 데이터와 관련 코드를 캡슐화한 개체입니다. 
이때 실제 구현을 보면 각 객체 인스턴스는 객체의 필드 값들을 나타내는 숫자들로 직렬화된 RAM 상의 데이터입니다. 

예시로 스크린에서 공들을 저글링하는 애니메이션 프로그램을 생각해보겠습니다. 
Ball 객체의 특성들로 x, y, radius, color이라는 정수 필드들이 있다고 하겠습니다. 
그리고 프로그램에서는 Ball 객체를 하나 생성하고 b라고 이름을 붙였습니다. 

객체도 역시 RAM에 저장됩닏다. 
아래의 그림은 객체 b가 RAM의 3012 ~ 3015 에 할달되었다고 하였을때의 구조입니다. 

![image](https://user-images.githubusercontent.com/35713051/159123360-b515d895-b8eb-4598-87f8-0a848d4269cf.png)

![image](https://user-images.githubusercontent.com/35713051/159123364-c7cce036-1e5f-4f10-9ea7-f32af7f43578.png)

// 객체 b와 정수 r이 이 함수의 처음 두 인수로 전달된다고 가정한다. // 다음 코드는 b.radius=r을 구현한 코드다. push argument 0 // b의 시작 주소를 얻는다. pop pointer 0 // 이 세그먼트가 b를 가리키게 한다. push argument 1 // r의 값을 얻는다. pop this 2 // b의 세 번째 필드에 을 설정 …

</description>
        <pubDate>Thu, 24 Feb 2022 00:00:00 +0000</pubDate>
        <link>https://sunghwan7330.github.io/make_computing_system/make_computing_system_07_vm_operation/</link>
        <guid isPermaLink="true">https://sunghwan7330.github.io/make_computing_system/make_computing_system_07_vm_operation/</guid>
        
        <category>밑바닥부터 만드는 컴퓨팅</category>
        
        <category>컴퓨터 아키텍쳐</category>
        
        <category>가상머신</category>
        
        
        <category>make_computing_system</category>
        
      </item>
    
      <item>
        <title>[밑바닥부터 만드는 컴퓨팅] 06장 어셈블러</title>
        <description># 개요 

해당 내용은 '밑바닥부터 만드는 컴퓨팅 시스템' 책의 6장 내용을 정리하였습니다.

# 1. 배경

기계어는 기호형과 2진 형식으로 정의됩니다. 
예시로 2진 코드는 `110000101000000110000000000000111` 와 같이 작성되며, 이는 실제 하드웨어에서 해석되어 수행합니다. 
이 명령어에서 왼쪽 8비트는 연산코드(LOAD), 다음 8비트는 레지스터(R3), 나머지 16비트는 주소(7)를 뜻합니다. 
이를 해석하면 메모리 주소 7의 값을 레지스터 R3에 로드하라는 뜻이 됩니다. 
현대의 컴퓨터는 이러한 기초적인 연산을 수십 수백개를 지원하고 있습니다. 

하지만 이러한 2진 코드는 사람이 보기에는 너무 불편합니다. 
그렇기때문에 2진값으로 표현하는 대신 `LOAD R3 7` 과 같이 약속된 문법으로 명령어를 표기하는 방법이 있는데, 이러한 표현 방법을 어셈블리라고 부릅니다. 
작성된 어셈블리는 어셈블러를 이용하여 2진 코드로 변경하며, 컴퓨터는 변경된 2진 코드로 프로그램을 실행하게 됩니다. 

## 1.1 기호 

2진 명령어는 2진 코드로 표현됩니다. 
2진 명령어들은 실제 숫자를 이용하여 메모리 주소 참조를 표기합니다. 
숫자는 미리 정의된 기호로 치환할 수 있습니다. 
만약 `weight`라는 변수의 주소값이 7이라면 메모리 주소의 값 7 대신에 `weight`로 치환하여 작성할 수 있습니다. 
어셈블리는 일반적으로 두가지 방법으로 기호를 도입합니닫. 

* 변수 : 프로그래머가 변수명으로 기호를 사용하면 번역기는 자동적으로 그 기호를 주소에 할당합니다. 
* 레이블 : 프로그래머는 프로그램 내 다양한 위치 기호를 표시할 수 있습니다. 예를 들어 어떤 코드의 시작 부분을 loop라는 레이블로 정의할 수 있습니다. 

어셈블리 언어에서 기호를 사용한다는 말은 어셈블러가 단순한 텍스트 처리 프로그램보다 더 복잡하다는 뜻입니다. 
미리 정의된 기호를 미리 정의돈 2진 코드로 변경하는 작업은 복잡하지 않습니다. 
하지만 프로그램에서 사용하는 변수명과 기호 레이블을 실제 메모리 주소에 매핑하는 일은 간단하지 않습니다.
실제로 하드웨어에서 소프트웨어로 전환되는 과정 중 가장 먼저 접하는 복잡한 작업이 이러한 교환 변환 작업입니다. 
이러한 문제는 일반적으로 다음과 같이 처리합니다. 

### 1.1.1 기호 변환 

![image](https://user-images.githubusercontent.com/35713051/156351844-e0c219b2-ef30-4b8c-bb47-c41ffb218f2f.png)

위 프로그램에서는 `i`와 `sum` 두개의 변수와 `loop`, `end` 두개의 레이블이 있습니다. 

번역된 코드는 주소 0부터 시작하는 메모리에 저장하고, 변수들은 1024부터 할당하도록 합니다.
다음으로 소스코드에 새로운 기호가 나타날 때 마다 테이블에 추가하는 방식으로 기호 테이블을 구성합니다. 
기호 테이블이 다 만들어지면 이 테이블을 이용해 프로그램을 기호가 없는 코드로 옮기게 됩니다. 

위 규칙에 따르면 `i`와 `sum`은 각각 주소 1024와 1025에 할당됩니다. 
다른 주소라고 하더라도 같은 물리적 주소를 참조할 수 있다면 상관은 없습니다. 

이 프로그램은 단순하게 설명하기 위하여 작성된 규칙이며, 실제의 컴퓨터는 이것보다는 복잡합니다. 

이 규칙에 따르면 실행 가능한 가장 큰 프로그램은 명령어가 1024개인 프로그램입니다. 
하지만 실제 프로그램은 훨씬 더 크고, 보통 변수 저장 시작 주소도 더 뒤에서 시작합니다. 

그리고 명령어들이 word 하나에 매핑된다는 것은 아주 단순한 가정입니다. 
일반적인 번역 과정에서 어떤 어셈블리 명령들은 명령어 몇 개로 분리되어 여러 메모리 위치를 차지하게 됩니다. 

또한 각 변수가 하나의 메모리 위치를 점유한다는 가정도 단순한 가정입니다. 
프로그래밍 언어에는 다양한 변수 타입이 있습니다.
C언어에는 char(1 byte), int(4 byte), long(8 byte) 와 같이 다양한 변수 타입이 존재합니다. 


### 1.1.2 어셈블러 

어셈블러 프로그램은 2진 기계어로 변역해야만 컴퓨터에서 실행할 수 있습니다. 
번역 작업은 어셈블러가 수행합니다. 
어셈블러는 어셈블러를 한줄씩 입력받아 같은 의미인 2진 명령어로 출력합니다. 
이렇게 번역된 2진 명령어는 프로그램 실행 시 메모리에 로드되어 실행됩니다. 

어셈블러는 기본적으로 번역 기능이 있는 텍스트 처리 프로그램입니다. 
따라서 어셈블러를 작성할 때는 어셈블리 문법에 대하여 2진 코드로 변환할 수 있는 목록이 있어야 합니다. 
`기계어 명세`라 불리는 이 규칙을 따르면, 작업을 수행하는 프로그램을 만들 수 있습니다. 

* 구문 분석을 통해 기호 명령 내 필드를 식별한다.
* 각 필드에 대응하는 기계어 비트를 생성한다. 
* 모든 기호 참조를 메모리 주소를 가르키는 숫자로 바꾼다. 
* 2진 코드들을 조립하여 완전한 기계 명령어로 바꾼다. 

# 2. 핵 어셈블리 - 2진 번역 명세서 

## 2.1 문법 관례와 파일 형식 

2진 기계어 코드는 hack, 어셈블리 코드는 asm 확장자로 하는 텍스트 파일에 저장합니다. 
예를들어 aaa.asm을 번역하면 aaa.hack이 생성됩니다. 

2진 코드 파일은 텍스트 라인으로 이뤄집니다. 
각 라인마다 0과 1로 구성된 16비트의 문자열이 있으며, 이 문자열은 16비트 기계어 명령 하나를 변환한 것입니다. 
이러한 16비트 기계어 명령이 모여서 프로그램을 구성합니다. 

어셈블리어 파일은 명령어나 기호 선언 라인들로 구성됩니다. 

* 명령어 : A-명령어 또는 C-명령어
* Symbol : 의사명령은 symbol을 프로그램의 다음 명령어가 저장되는 메모리 위치에 연결합니다. 

상수는 10진법으로 표기된 음수가 아닌 숫자여야 합니다. 사용자 정의 기호는 맨 앞 글자가 숫자가 아닌 문자열로 이뤄집니다. 

주석은 두개의 빗금(//)의 시작부터 라인 끝까지로 간주되어 무시됩니다. 

공백과 빈 라인은 무시됩니다. 

## 2.2 명령어 

핵 기계어는 주소 명령어(A-명령어) 와 계산 명령어(C-명령어) 라는 두 종류의 명령어로 구성됩니다. 
명령어 형식은 다음과 같습니다. 

![image](https://user-images.githubusercontent.com/35713051/156351896-6dfe6b48-714f-45ab-96d0-1747d2cc3fd8.png)

comp, dest, jump 필드들을 2진 형태로 번역하는 방법은 아래의 표를 참고바랍니다. 

![image](https://user-images.githubusercontent.com/35713051/156351924-99fd337a-6c26-4563-8903-a1c158d9854f.png)

![image](https://user-images.githubusercontent.com/35713051/156351942-fc082cc9-0383-4b6b-94bc-eb98a100d10c.png)


## 2.3 기호 

핵 어셈블리 명령은 상수나 기호를 이용해 메모리 주소를 참조할 수 있습니다. 
어셈블리 프로그램에서 기호를 사용하는 방법은 세가지입니다. 

### 2.3.1 선언 기호 

핵 어셈블리 프로그램에서는 다음과 같은 선언 기호를 사용할 수 있습니다. 

| 레이블 | RAM 주소 | (hexa) |
|------|--------|--------|
| SP| 0 | 0x0000|
| LCL | 1 | 0x0001|
| ARG | 2 | 0x0002|
| THIS | 3 | 0x0003|
| THAT | 4 | 0x0004|
| R0 - R15 | 0 ~ 15 | 0x0000 ~ f|
| SCREEN | 16384 | 0x4000|
| KBD | 24576 | 0x6000|

### 2.3.2 레이블 기호 

의사명령은 기호 Xxx 가 프로그램의 다음 명령이 있는 명령어 메모리 위치를 참조하도록 정의합니다. 
한 레이블은 한번만 정의할 수 있으며, 어셈블리 프로그램 내 어디서든 사용할 수 있습니다. 

### 2.3.3 변수 기호 

선언 기호가 아니거나, 프로그램 내에서 (Xxx)명령으로 정의되지 않은 기호 Xxx는 변수로 취급됩니다. 
변수는 등장한 순서대로 RAM주소 16에서 시작하는 메모리 주소에 차례대로 매핑됩니다. 

## 2.4

아래는 정수 1부터 100까지 더하는 프로그램의 어셈블리 코드와 2진 코드입니다. 

![image](https://user-images.githubusercontent.com/35713051/156351976-d1304645-8f80-4bcb-a657-04a3d1734152.png)
</description>
        <pubDate>Tue, 22 Feb 2022 00:00:00 +0000</pubDate>
        <link>https://sunghwan7330.github.io/make_computing_system/make_computing_system_06_assambler/</link>
        <guid isPermaLink="true">https://sunghwan7330.github.io/make_computing_system/make_computing_system_06_assambler/</guid>
        
        <category>밑바닥부터 만드는 컴퓨팅</category>
        
        <category>컴퓨터 아키텍쳐</category>
        
        <category>어셈블러</category>
        
        <category>assambler</category>
        
        
        <category>make_computing_system</category>
        
      </item>
    
      <item>
        <title>[밑바닥부터 만드는 컴퓨팅] 프로젝트04 어셈블리</title>
        <description># 개요 

해당 내용은 '밑바닥부터 만드는 컴퓨팅 시스템' 책의 프로젝트 04 의 내용을 정리하려 합니다. 
혹시나 따라하실 분은 실습준비 포스팅을 참고해서 프로그램을 다운받아주시면 됩니다. 

# 프로그램 실행 

다운받은 프로그램에서 `Assembler.bat` 를 실행해줍니다. 

![image](https://user-images.githubusercontent.com/35713051/152643376-f2de5a20-20d3-4484-bd75-90534f3dbe6d.png)

왼쪽의 문서 버튼을 눌러 파일을 열 수 있고, 그 옆에 화살표 버튼을 눌러 어셈블리를 실행할 수 있습니다. 

작성한 asm 파일을 연 뒤 실행 버튼을 누르면 프로그램에서 어셈블리를 읽어 우측에 기계어로 출력합니다. 
출력된 기계어는 hack 파일로 저장됩니다. 

이제 `CPUEmulator.bat` 를 실행해줍니다. 

![image](https://user-images.githubusercontent.com/35713051/152643406-06c47026-5bb8-4989-87d2-68cf74fd387a.png)

왼쪽의 문서 버튼을 눌러 파일을 열고 , 그 옆에 화살표 버튼을 눌러 어셈블리를 실행할 수 있습니다. 

이전에 생성된 hack 파일과 스크립트를 읽은 뒤 프로그램을 실행버튼을 누르면 프로그램이 수행됩니다. 
프로그램이 다 실행되면 out 파일이 생성되며, 이를 cmp 파일과 비교하여 프로그램이 정상적인지 확인할 수 있습니다. 


# multi 어셈블리 작성 

multi 는 곱셈을 수행하는 연산입니다. 

`Mult.asm` 파일을 열어 곱셈 연산을 수행하는 어셈블리를 작성하면 됩니다. 
파일을 열어 상단에 작성된 주석을 확인해보도록 하겠습니다. 

Multiplies R0 and R1 and stores the result in R2. (R0, R1, R2 refer to RAM[0], RAM[1], and RAM[2], respectively.)

This program only needs to handle arguments that satisfy R0 >= 0, R1 >= 0, and R0*R1 < 32768.


R0과 R1을 곱하고, 그 결과를 R2 에 저장합니다. (R0, R1, R2는 각각 RAM[0], RAM[1], RAM[2]를 나타냅니다.)

이 프로그램은 다음을 만족하는 인수를 처리하면 됩니다. R0 >= 0, R1 >= 0 그리고 R0 * R1 < 32768


주석을 확인해보면 R0과 R1메모리를 읽어 두 값을 곱한 뒤 R2 메모리에 저장하면 됩니다. 
작성한 어셈블리는 아래와 같습니다. 

@R2 M=0 // R2=0

(LOOP) D=M[R1]

@END      // if(R1==0)
D;JEQ     //     goto END

@R0
D=M       // D=R0

@R2
M=M+D     // R2=R2+R0

@R1
M=M-1     // R1--

@LOOP
0;JMP     // goto (LOOP)

(END)


저도 어셈블리가 친숙하지는 않지만, 한줄한줄씩 보도록 하겠습니다. 

@R2 M=0 // R2=0


`@R2` 를 작성하면 `M` 이 `R2` 의 메모리 주소로 변경됩니다.
이후 `M=0`을 작성하면 `R2`의 메모리에 값을 0으로 저장합니다. 

다음에 보이는 `(LOOP)` 와 `(END)` 는 jump 하기 위한 메모리 주소를 가리킵니다. 
C언어의 goto문과 유사하다고 보시면 됩니다. 

`D=M[R1]` 은 `D`레지스터에 `R1`의 값을 저장하게 됩니다. 

@END      // if(R1==0)
D;JEQ     //     goto END ``` 이 구문은 조건을 만족하였을 때 주소를 jump 하게 되는 코드입니다.  `D`의 값이 0인 경우 jump를 하게 되는데, jump하는 위치는 윗 라인에서 작성한 `(END)`입니다.  위에서 `D`레지스터에 `R1`의 값을 셋팅하였으니, 이는 즉 `R1`의 값이 0이 되면 `(END)`로 jump하여 프로그램이 종료하게 됩니다. 
    @R0
    D=M       // D=R0

@R0 을 입력하면 MR0의 메모리 주소로 변경됩니다. 이후 D=M 을 입력하면 D레지스터에 R0 메모리의 값이 셋팅됩니다.

    @R2
    M=M+D     // R2=R2+R0

@R2 를 입력하면 MR2 의 주소로 변경합니다. MR2가 셋팅되어있고, 위에서 D레지스터에는 R0의 값을 입력하였습니다. 따라서 M=M+D 를 입력하면 R2 = R2 + R1 이 수행되게 됩니다.

    @R1
    M=M-1     // R1--

@R1 을 입력하면 MR1의 메모리 주소로 변경됩니다. 이후 M=M-1 을 입력하면 R1 메모리의 값에서 1을 빼어 다시 R1 메모리에 저장합니다.

    @LOOP
    0;JMP     // goto (LOOP)

이 구문은 loop로 jump 하는 로직입니다. 조건에 관계없이 무조건 loop로 이동합니다.

위를 다시 보면 R1 이 0이 될때까지 R2R0의 값을 더하고 R1 의 값을 1씩뺍니다. 즉, R0R1 번 더하게 됩니다. C 코드로 작성하면 아래와 같게 됩니다.

r2 = 0;

while(true) {
  if (r1 == 0)
    break;
  r2 = r2 + r0;
  r1 = r1 - 1;
}

더 심플하게 작성할 수도 있지만 최대한 위의 어셈블리와 비슷하게 작성해보았습니다. 참고용으로 봐주시면 되겠습니다.

fill 어셈블리 작성

다음으로 fill.asm 파일을 열어 어셈블리를 작성하시면 됩니다. 이번에도 역시 파일을 열어 작성되어 있는 주석 메시지를 확인해보도록 하겠습니다. (번역기의 힘을 빌려…!)

Runs an infinite loop that listens to the keyboard input.
When a key is pressed (any key), the program blackens the screen,
i.e. writes &quot;black&quot; in every pixel;
the screen should remain fully black as long as the key is pressed. 
When no key is pressed, the program clears the screen, i.e. writes
&quot;white&quot; in every pixel;
the screen should remain fully clear as long as no key is pressed.

------------------------------------------------------------------

키보드 입력을 수신하는 무한 루프를 실행합니다. 
키를 누르면 (아무 키나) 프로그램의 화면 을 검게, 즉 모든 픽셀을 검정색으로 씁니다. 
키를 누르고 있는 동안은 완전히 검은색으로 유지되어야 합니다. 
아무 키도 누르지 않으면 프로그램이 화면을 깨끗하게, 즉 모든 픽셀을 흰색으로 씁니다. 
아무키도 누르지 않는 한 화면은 완전히 깨끗한 상태를 유지해야 합니다. 

이번 프로그램은 무한루프를 반복하는 프로그램입니다. 키보드에 값이 입력되었다면 모든 픽셀의 값을 검정색으로 쓰고, 값이 입력되지 않았다면 흰색 값으로 쓰면 됩니다.

전체 코드는 다음과 같습니다.

@color         // declare color variable
M=0            // by default is white

(loop)
    @SCREEN    // RAM address 16384
    D=A

    @pixels 
    M=D       // pixel address (starting point: 16384, max: 16384+8192=24576)

    @KBD      // D = ascii code of a keyboard input
    D=M

    @black
    D;JGT     // if(keyboard &gt; 0) goto black

    @color
    M=0       // otherwise white
    
    @color_screen
    0;JMP     // jump to subroutine that actually changes the color of screen

    (black)
        @color
        M=-1  // set to black

    (color_screen)
        @color
        D=M
        @pixels
        A=M
        M=D   // M[pixels] = @color

        @pixels
        M=M+1
        D=M

        @24576 // loop until end of pixels
        D=D-A
        @color_screen
        D;JLT

@loop
0;JMP         // infinite loop

이번에도 라인 단위로 살펴보도록 하겠습니다.

@color         // declare color variable
M=0            // by default is white

@color 입력하여 메모리 주소를 이동한 메모리 위치의 값을 0으로 설정합니다.

다음의 @LOOP 는 jump하기 위한 주소를 가르킨다고 보시면 됩니다.

    @SCREEN    // RAM address 16384
    D=A
	
	@pixels 
    M=D       // pixel address (starting point: 16384, max: 16384+8192=24576)

먼저 @SCREEN 의 주소를 D레지스터에 입력합니다. 이후 @pixels의 메모리 주소에 D레지스터의 값을 입력합니다. 이렇게 되면 @pixels 에 메모리에 @SEREEN 의 주소값이 저장되게 됩니다.

    @KBD      // D = ascii code of a keyboard input
    D=M
	
    @black
    D;JGT     // if(keyboard &gt; 0) goto black

@KBD는 키보드 입력 값이 기록되는 메모리 주소입니다. 키보드에 입력된 값을 D 레지스터에 저장합니다.

이후 D 의 값이 0이라면 (black) 부분으로 jump를 합니다. 키보드의 입력이 없다면 메모리 주소에 0이 입력되고, 아니라면 키보드의 아스키코드 값이 입력됩니다. 즉, D의 값이 0이 아니라면 (black) 부분으로 jump를 하고, 아니라면 아래로 이동하게 됩니다.

    @color
    M=0       // otherwise white
    
    @color_screen
    0;JMP     // jump to subroutine that actually changes the color of screen

이 전 확인한 코드에서 키보드 입력이 없었다면 jump를 하지 않기 때문에 위에 보이는 코드로 이동하게 됩니다. @color 부분의 메모리 값을 0으로 변경한 뒤 @color_screen 의 주소로 jump 하게 됩니다.

    (black)
        @color
        M=-1  // set to black

다음으로는 이전의 코드에서 키보드 입력이 되었을 때 jump하여 이동되는 부분입니다. 이 코드에서는 @color 메모리의 값을 -1로 변경하게 됩니다.

    (color_screen)
        @color
        D=M
        @pixels
        A=M
        M=D   // M[pixels] = @color

        @pixels
        M=M+1
        D=M

        @24576 // loop until end of pixels
        D=D-A
        @color_screen
        D;JLT

다음으로는 @color_screen 의 메모리 주소 부분입니다. 해당 부분에서는 위에서 설정된 @color의 값으로 화면을 체우는 부분입니다. @color의 값은 키보드가 입력된 상태라면 -1이고 아니라면 0 으로 설정되어 있을것입니다.

먼저 @color 메모리 주소의 값을 읽어 D레지스터에 저장합니다.

다음으로 @pixels에는 위에서 @SCREEN 의 주소가 저장되어 있습니다. @pixels 의 값을 A=M 로 입력하면 @pixels의 값에 저장된 메모리 주소로 이동하게 됩니다. 이후 M=D 입력시 해당 주소 값에 D의 값, 즉 @color 의 값이 입력됩니다.

다음으로 @pixels 에 저장된 값을 1 증가시킨 후 D레지스터에 그 값을 입력합니다.

24576 은 스크린 메모리의 마지막 주소를 뜻합니다. D 레지스터와 24576 의 값을 빼줍니다. 이때 값이 0이 아니라면 @color_screen 으로 이동합니다.

@color_screen 의 부분은 스크린 메모리 주소의 값을 1씩 증가시키면서 해당 메모리의 값을 @color 값으로 입력하여 화면을 체워나가게 됩니다.

참고

  • https://github.com/simulacre7/nand2tetris

</description> Fri, 04 Feb 2022 00:00:00 +0000 https://sunghwan7330.github.io/make_computing_system/make_computing_system_pracice_04/</link> https://sunghwan7330.github.io/make_computing_system/make_computing_system_pracice_04/

    <category>밑바닥부터 만드는 컴퓨팅</category>
    
    
    <category>make_computing_system</category>
    
  </item>

  <item>
    <title>[밑바닥부터 만드는 컴퓨팅] 05장 컴퓨터 아키텍쳐</title>
    <description># 개요 

해당 내용은 ‘밑바닥부터 만드는 컴퓨팅 시스템’ 책의 5장 내용을 정리하였습니다.

1. 배경

1.1 내장식 프로그램 개념

컴퓨터는 정해진 명령어 집합들을 실행하는 고정된 하드웨어 플랫폼에 기반합니다. 동시에 컴퓨터가 실행하는 명령어들은 기본 블록이 되어 무한히 복잡한 프로그램으로 조합됩니다. 이러한 프로그램들은 하드웨어에 내장되지 않고 컴퓨터의 메모리상에 데이터처럼 저장되고 연산되는데, 이를 소프트웨어라고 부릅니다. 사용자가 현재 실행하는 소프트웨어에 따라 연산이 달라지므로, 같은 하드웨어라고 하더라도 어떤 프로그램을 실행하느냐에 따라 제각각의 일을 수행할 수 있습니다.

1.2 폰 노이만 구조

폰 노이만 기계는 현대의 거의 모든 컴퓨터 플랫폼의 개념적 설계도이자 실제 구조이기도 합니다.

image

폰 노이만 구조는 중앙 처리 장치를 기반으로 하며, 이 장치는 메모리 장치와 통신하고, 입력 장치에서 데이터를 받고, 출력 장치에 데이터를 보내는 일을 합니다. 이 구조의 중심에 내장식 프로그램의 개념이 있습니다. 즉 메모리는 컴퓨터가 조작하는 데이터 뿐만 아니라, 컴퓨터가 수행하는 명령어도 저장합니다.

1.3 메모리

폰 노이만 기계의 메모리는 데이터와 명령어, 두 종류의 정보를 저장합니다. 이 두 정보는 보통 다르게 취급되며, 일부 컴퓨터에서는 메모리 장치를 분리해서 따로 저장하기도 합니다. 두 정보의 기능은 다르지만, RAM에 2진수로 저장된다는 점은 같습니다. 이 구조는 word나 loaction이라 불리는 정해진 크기의 셀들이 배열된 것으로 유일한 주소를 가집니다.

1.3.1 데이터 메모리

고수준 프로그램은 변수, 배열, 객체같은 추상적 개념들을 다룹니다. 이런 추상적 데이터들은 기계어로 번역하면 2진 숫자열로 바뀌어 데이터 메모리에 저장됩니다. 주소를 통해 데이터 메모리에서 한 word를 선택하면, 그 word에 읽거나 쓰는게 가능해집니다. 읽기의 경우 word의 값을 가져오며, 쓰기의 경우 word의 위치에 새로운 값을 덮어씌웁니다.

1.3.2 명령어 메모리

고수준 명령어들을 기계어로 변역하면 2진수 기계어를 뜻하는 word가 됩니다. 이 명령어들은 메모리에 저장됩니다. 컴퓨터 연산의 각 단계마다 CPU는 명령어 메모리에서 하나의 word를 인출(fetch)하여 해석한 뒤 정해진 명령어를 수행하고, 다음 수행할 명령어를 찾아냅니다. 그래서 명령어 메모리의 내용을 바꾸면 컴펴튜 연산이 완전히 바뀌게 됩니다.

명령어 메모리에 상주하는 명령어는 기계어라 불리는 미리 정의된 형식으로 작성됩니다. 어떤 컴퓨터에서는 연산과 피연산자를 나타내는 코드가 한 word로 표현되는 반면 어떠한 컴퓨터는 여러 word로 쪼개져 기술되기도 합니다.

1.4 중앙 처리 장치

중앙 처리 장치(CPU)는 불러온 프로그램의 명령어를 실행하는 일을 담당합니다. CPU는 이 명령어들을 이용해 다양한 계산을 수행하고, 메모리의 값을 읽거나 쓰며, 조건에 따라 다른 명령어로 점프합니다. CPU는 이러한 작업을 수행할 때 산술 논리 연산장치(ALU), 레지스터, 제어장치 세가지의 주요 하드웨어가 관여합니다.

1.4.1 산술 논리 연산 장치 (ALU)

ALU는 컴퓨터에서 지원하는 모든 저수준 산술 연신 및 논리 연산을 수행하는 장치입니다. 앞에서 실습한 바와 같이 두 값을 더하거나, 비트 조작 등의 연산을 수행합니다.

1.4.2 레지스터

CPU는 간단한 계산을 빠르게 수행하도록 설계됩니다. 계산 성능을 올리려면 이런 계산의 결과를 매번 메모리에 로드하고 저장하기보다는, CPU 근처에 저장할 필요가 있습니다. 이러한 이유로 모든 CPU는 word를 저장할 수 있는 고속 레지스터를 가지고 있습니다.

1.4.3 제어 장치

요즘의 컴퓨터 명령어는 보통 폭이 32비트 또는 64비트인 2진 코드로 표현됩니다. 이러한 명령어 하나가 실행되려면 명령어를 해석해서 다양한 하드웨어 장치가 명령어를 어떻게 실행해야 하는지 정보를 알아내고 각각에 전달해야 합니다. 명령어 해석은 제어 장치에서 이뤄지며, 동시에 이 장차는 어떤 명령어를 인출하고 다음에 실행해야 하는지 알아내는 역할도 합니다.

CPU 연산은 이제 명령어를 메모리에서 인출하고, 해석하고, 실행하고, 다음 명령어를 인출하는 루프가 반복되는 것을 알 수 있습니다. 명령어가 실행될 때는 ALU에서 어떤 값을 계산하고, 내부 레지스터를 조작하고, 메모리에서 word 하나를 읽고, 메모리에 단어 하나를 쓰는 작업들 중 하나 이상을 하게 됩니다. CPU는 이 작업을 실행하는 과정에서 다음에 인출하고 실행할 명령어를 찾는 작업도 합니다.

1.5 레지스터

메모리 접근 작업은 컴퓨터의 입장에서는 느린 작업입니다. CPU가 메모리 주소 j의 내용을 가져오는 과정은 다음과 같습니다.

  1. j가 CPU에서 RAM으로 전달됨
  2. RAM의 직접 접근 논리에 따라 주소가 j인 메모리 레지스터가 선택됨
  3. RAM[j]의 내용이 CPU로 전달됨

레지스터는 데이터 추출 및 저장 기능이 RAM과 동일하지만, 데이터가 옮겨지거나 메모리를 탐색하는 비용이 들지 않습니다. 그 이유로 첫번째는 CPU칩 내부에 물리적으로 위치하여 바로 접근이 가능하고, 두번째로는 메모리 셀은 수백만개가 있는 반면 레지스터는 소수만 있기 때문입니다. 따라서 조작할 레지스터를 지정하는데 몇 개의 비트만 사용하므로 명령어 길이도 더 짧게 됩니다.

CPU들은 다양한 종류의 레지스터를 각각 다른 개수만큼 사용합니다. 어떤 컴퓨터 아키텍쳐에서는 다음과 같은 레지스터가 하나 이상의 기능을 수행합니다.

1.5.1 데이터 레지스터

이 레지스터들은 CPU의 단기 기억 메모리 기능을 합니다. 예를 들어 (a-b)*c 값을 계산한다고 할 때, 먼저 (a-b) 의 값을 계산하고 기억하애 합니다. 이 중간 값을 메모리에 저장하는 것 보다 금방 사용할 것이기 때문에 내부에 저장하는 것이 더 좋습니다. 이러한 용도로 데이터 레지스터를 사용합니다.

요약하자면 데이터 레지스터는 어떠한 연산을 수행하였을 때의 결과값을 저장하는 레지스터입니다.

1.5.2 주소 지정 레지스터

CPU는 데이터를 읽고 쓰기 위해 계속해서 메모리에 접근해야 합니다. 그리고 이러한 연산을 하게되면 접근해야 할 메모리 워드를 지정해야 합니다.

주소가 현재 명령어에 포함될 수도 있고, 예전 명령어의 실행 결과를 이용하기도 합니다. 이 중 후자의 경우 주소가 될 값을 저장하는데, 이때 주소 지정 레지스터가 사용됩니다.

요약하자면 주소 지정 레지스터는 어떠한 메모리에 접근하기 위한 주소값을 저장하는 레지스터입니다.

1.5.3 프로그램 계수기 레지스터

프로그램을 실행할 때 CPU 명령어 메모리에서 인출해야 할 다음 명령어의 주소를 항상 알고 있어야 합니다. 이 주소는 PC(program counter)라 불리는 레지스터가 저장하고 있습니다. 그리고 PC의 값은 명령어 메모리에서 인출할 명령어의 주소로 쓰입니다.

PC는 명령어를 수행한 뒤 다음 명령어를 가르키도록 일정하게 증가하며, 명령어의 수행 결과로 메모리 주소가 jump 하는 경우에 PC에 해당 주소를 저장하여 해당 메모리의 명령어를 수행하도록 하게 됩니다.

요약하자면 PC 레지스터는 CPU가 수행하기 위한 명령어 주소를 저장하는 레지스터입니다.

1.6 입력과 출력

컴퓨터는 다양한 입력 / 출력 장치를 통해 외부 환경과 통신합니다.

입력장치는 컴퓨터에 신호를 입력할 수 있는 장치로 키보드, 마우스, 스캐너 등이 있습니다. 출력장치로는 모니터, 프린터 등이 있습니다.

이러한 입출력 장치를 처리하는 기법 중 가장 간단한 기법은 메모리 매핑 입니다. 기본적인 아이디어는 입출력 장치를 에뮬레이션하여 CPU에게 마치 일반적인 메모리에 접근하는것처럼 보이도록 하는 방법입니다. 구체적으로는 각 입출력 장치마다 메모리 내에 전용 영역이 할당됩니다. 키보드, 마우스와 같은 입력장치의 경우 할당된 메모리 영역에 입력값을 지속적으로 읽어 반영하도록 되어있습니다. 출력 장치의 경우 특정 메모리의 값을 지속적으로 읽어들여 물리적 장치에 구동하도록 만들어집니다.

2. 핵 하드웨어 플랫폼 명세

2.1 개요

핵 플랫폼은 16비트 폰 노이만 기계로, CPU와 명령어 메모리 및 데이터 메모리로 분리된 메모리 모듈, 두 개의 메모리 매핑 I/O 장치인 스크린과 키보드로 구성되어 있습니다.

핵 컴퓨터는 명령어 메모리에 있는 프로그램을 실행합니다. 명령어 메모리는 읽기 전용 장치로, 프로그램은 외부 수단을 통해 메모리에 로드됩니다. 핵 플랫폼 하드웨어 시뮬레이터에서 핵 기계어 프로그램이 담긴 텍스트 파일을 명령어 메모리에 불러와 실행하게 됩니다.

핵 CPU 는 2장에서 제작한 ALU와 세 종류의 레지스터(데이터, 주소지정, PC 레지스터)로 구성됩니다.

핵 기계어는 두 종류의 16비트 명령이 있습니다. 0vvvvvvvvvvvvvvv 의 형태인 주소 명령어와 111acccccdddjjj 의 형태인 계산 명령어가 있습니다.

컴퓨터 아키텍쳐에서 PC칩의 출력에서 나온 선은 ROM의 칩의 주소의 입력으로 연결됩니다. 여기서 ROM 칩은 PC가 가리키는 명령어 메모리 위치의 값인 ROM[PC]를 계속 출력합니다. 이 값은 현재 명령어입니다. (current instruction) 전반적인 컴퓨터 연산이 한 클럭 주기동안 이뤄지는 행동은 아래와 같습니다.

2.1.1 실행 (execute)

현재 명령어들의 비트들은 여러 칩에 동시에 전달됩니다. 만약 명령어가 주소명령어라면 명령어에 있는 15비트 상수는 A레지스터에 기록됩니다. (주소 명령어는 최상위 비트가 0) 명령어가 계산 명령어라면 그 안에 a, b, d, j 비트는 제어 비트가 되어 ALU나 레지스터가 그 명령어를 수행하게 됩니다. (계산 명령어는 최상위 비트가 1)

2.1.2 인출 (Fetch)

다음에 인출할 명령어는 현재 명령어의 점프 비트 및 ALU 출력에 따라 결정되게 됩니다. 점프가 실제로 이뤄질때는 이 두 값을 같이 봐야 합니다. 만약 점프가 이뤄진다면 PC는 A레지스터의 값으로 설정됩니다. 점프가 이뤄지지 않는다면 PC는 1이 증가합니다. 다음 클럭 주기에는 PC가 가리키는 명령어가 ROM에 출려이 되어 다음 주기가 이어집니다.

2.2 중앙 처리 장치

핵 플랫폼은 4장에서 제작한 핵 기계어로 작성된 16비트 명령어를 실행할 수 있도록 설계되었습니다. CPU는 두개의 별도 메모리 모듈에 연결됩니다. 하나는 필요한 명령어를 인출하는데 쓰는 명령어 메모리이고, 다른 하나는 데이터 값을 읽거나 쓰는데 사용하는 메모리입니다.

image

2.3 명령어 메모리

핵 명령어 메모리는 직접 접근 읽기 전용 메모리 또는 ROM이라 불리는 칩 상에 구현됩니다. 핵 ROM은 주소 지정 가능한 32K개의 16비트 레지스터들로 구성됩니다.

image

2.4 데이터 메모리

핵의 데이터 메모리 인터페이스는 3장에서 구현한 일반적인 RAM 장치의 인터페이스와 동일합니다. 레지스터 n의 내용을 읽으려면 메모리의 address 입력에 n을 넣고 out 출력을 검사하면 됩니다. 레지스터 n의 값 v를 쓰려면 in입력에 v를 넣고 address 입력에 n을 넣고 메모리의 load 비트를 활성화하면 됩니다.

데이터 메모리는 컴퓨터의 범용 데이터 저장소 역할 외에도, 메모리 맵을 통한 CPU와 입력/출력 장치 사이의 통신도 맡는다.

2.4.1 메모리 맵을

핵 플랫폼은 사용자와 상호작용을 위한 주변 장치로 스크린과 키보드를 사용합니다. 그리고 이 두 장치는 메모리 매핑 버퍼를 통해 컴퓨터와 통신하게 됩니다. 예를 들어 스크린 메모리 맵이라 불리는 특정 메모리 주소에 word를 읽고 쓰면서 스크린에 이미지를 확인하거나 입력할 수 있습니다. 또한 키보드 메모리 맵이라 불리는 특정 메모리 word를 읽어 키보드에서 어떠한 키가 눌린 상태인지 알 수 있습니다.

2.4.2 스크린

핵 컴퓨터의 스크린은 512 * 256 흑백 픽셀로 되어 있습니다. 실제 통신은 메모리 맵을 통해서 하며, 메모리 맵은 스크린이라는 이름의 칩으로 구현되어 있습니다. 이 칩은 보통의 메모리처럼 읽고 쓰기가 가능합니다. 또한 이 칩에 비트를 기록하면 실제 스크린의 픽셀로 표시됩니다.

2.4.3 전체 메모리

핵 플랫폼의 전체 주소 공간은 메모리라는 칩으로 제공됩니다. 이 메모리 칩에는 RAM과 스크린 맵 및 키보드 맵이 포함되어 있습니다. 이 모듈들은 세개로 나뉜 메모리의 주소 공간에 각각 위치합니다.

image

2.5 컴퓨터

핵 하드웨어 계층에서 최상위 칩은 핵 기계어로 작성된 프로그램을 실행하도록 설계된 컴퓨터 시스템입니다. 컴퓨터 칩은 CPU, 데이터 메모리, 명령어 메모리, 스크린, 키보드 및 그 외 컴퓨터 작동에 필요한 모든 하드웨어 장치들로 구성됩니다. 이 컴퓨터에서 프로그램을 실행하려면 코드를 ROM에 미리 로드해야 합니닫. 스크린 및 키보드는 메모리 맵을 통해 제어합니다.

image </description> Sat, 29 Jan 2022 00:00:00 +0000 https://sunghwan7330.github.io/make_computing_system/make_computing_system_05_computer_achitecture/</link> https://sunghwan7330.github.io/make_computing_system/make_computing_system_05_computer_achitecture/

    <category>밑바닥부터 만드는 컴퓨팅</category>
    
    <category>컴퓨터 아키텍쳐</category>
    
    
    <category>make_computing_system</category>
    
  </item>

  <item>
    <title>[밑바닥부터 만드는 컴퓨팅] 04장 기계어</title>
    <description># 개요 

해당 내용은 ‘밑바닥부터 만드는 컴퓨팅 시스템’ 책의 4장 내용을 정리하였습니다.

1. 배경

해당 장에서는 언어에 대해 설명합니다. 하드웨어 플랫폼에 대한 자세한 설명은 다음장에서 설명합니다. 기계어를 일반적으로 설명하는데는 프로세서, 메모리, 레지스터로 설명할 수 있겠습니다.

1.1 기계

기계어는 프로세서와 레지스터들을 이용하여 메모리를 조작할 수 있도록 미리 정의한 규칙이라 할 수 있습니다.

1.1.1 메모리

메모리는 컴퓨터에서 데이터와 명령어를 저장하는 하드웨어 장치들을 대략적으로 통칭하는 장치입니다. (주로 RAM을 사용합니다) 메모리는 word나 location이라 불리는 정해진 폭의 셀들이 연속적으로 배열되어 있으며, 이 셀은 각각의 주소를 가지고 있습니다. 그렇기에 각각의 word는 address를 통해 특정할 수 있습니다. 앞으로 이러한 단어들을 memory, RAM, M 과 같은 용어로 표현됩니다.

1.1.2 프로세서

중앙처리장치 또는 CPU라고 불리는 프로세서는 기초 연산을 수행하는 장치입니다. 산술 및 논리연산, 메모리 접근 연산, 제어, 분기와 같은 연산이 포함된다. 이와 같은 연산의 피연산자들은 선택된 메모리 위치와 레지스터에 있는 바이너리 값이 됩니다.. 연산의 결과값은 선택된 메모리 주소 또는 레지스터에 저장됩니다.

1.1.3 레지스터

메모리 접근 연산은 상대적으로 느리고 긴 명령어가 필요합니다. 그렇기 때문에 대부분의 프로세서는 값 하나를 저장할 수 있는 레지스터를 여러개 두고 있습니다. 레지스터는 프로세서의 바로 옆에 위치합니다. 그렇기때문에 프로세서가 명령어와 데이터를 빠르게 처리할 수 있는 로컬 고속 메모리 역할을 합니다. 레지스터가 있기 때문에 메모리 접근 단계를 줄일 수 있으며, 이로인해 프로그램의 실행 속도가 올라가게 됩니다.

1.2 언어

기계어 프로그램은 명령어를 코드화한 것입니다. 예를 들어 16비트 컴퓨터라고 하면 명령어는 1001010110100111 과 같이 16비트의 바이너리로 구성되어 있습니다. 이러한 명령어가 무엇을 뜻하는지 알려면 하드웨어 플랫폼의 명령 집합을 알아야 합니다.

예를들어 어떤 기계어가 4개의 필드로 명령이 구성되어 있고, 맨 왼쪽에는 CPU 연산 코드, 나머지 세개는 피연산자라고 생각해봅시다. 16비트를 4비트로 나누었을때 맨 처음의 4비트로 CPU의 연산을 결정하고, 나머지 세부분은 피연산자가 될 수 있습니다. CPU 연산을 해석하여 R1 + R2를 수행하여 R3에 저장하라는 해석이 될 수 있습니다.

2진 코드는 암호같아보이기 때문에 보통은 연상기호를 사용하도록 되어 있습니다. 예를들어 연산코드 1010이 ADD라고 하고 레지스터들을 R1, R2와 같은 기호로 표기할 수 있습니다. 그러면 10100001001000111010, 0001, 0002, 0003으로 나눌 수 있으며, 이는 연상기호로 ADD R1 R2 R3 으로 표현할 수 있습니다.

이 표기법을 더 추상화한다면 기호를 읽는것 뿐만 아니라 2진 명령어 대신 기호를 텍스트로 입력하여 프로그램을 작성할 수도 있습니다. 또한 이 명령어들을 텍스트 처리 프로그램으로 분석하여 2진 코드로 변환할 수 있을 것입니다. 이러한 기호 표기법을 어셈블리 언어 또는 어셈블리라고 부르며, 어셈블리를 2진 코드로 번역하는 프로그램을 어셈블러 라고 합니다.

CPU마다 연산이나 레지스터의 종류, 어셈블리 문법 등이 다를 수 있습니다. 하지만 문법이 다르더라도 일반적으로는 비슷한 명령으로 되어있습니다.

1.3 명령

1.3.1 산술 및 논리연산

컴퓨터에서 산술 연산은 더하기, 빼기, 곱하기, 나누기 와 같은 연산을 의미합니다. 뿐만 아니라 컴퓨터는 And, Or, Shift, 반전 등와 같은 비트 논리연산도 필요합니다. 다음은 일반적인 기계어 문법으로 작성된 예시입니다.

ADD R2, R1, R3 // R2&lt;-R1+R3, 여기서 R1,R2,R3는 레지스터다.
ADD R2, R1, foo // R2&lt;-R1+foo, 여기서 foo는 사용자가 정의한 이름으로,
               // foo가 가리키는 메모리 주소의 값을 뜻한다.
AND R1, R1, R2 // R1&lt;-R1과 R2을 비트 단위로 And 연산한 결과

1.3.2 메모리 접근

메모리 접근 명령은 두 부류로 나뉩니다. 산술 및 논리연산 명령은 레지스터 외에도 특정 메모리 주소에 접근할 수 있습니다. 또는 어느 컴퓨터나 지원하는 load, store 명령을 이용해 레지스터와 메모리 사이에 데이터를 이동시키는 명령이 있습니다.

메모리 접근 명령에는 주소 지정 모드가 있습니다. 컴퓨터 CPU 마다 다를 수 있지만 일반적으로 아래의 방법들을 지원합니다.

  • 직접 주소 지정 방식
    • 특정 주소에 직접 쓰거나, 그 주소를 나타내는 기호를 활용
      LOAD R1, 67 // R1&lt;-Memory [67]
              // 또는 bar가 에모리 주소 67을 나타낸다고 가정하면
      LOAD R1, bar // R1&lt;-Memory (67)
      
  • 즉시 주소 지정 방식
    • 명령어 코드에 있는 상수를 불러오는데 쓰임
      LOAD R1,67 // R1&lt;-67
      
  • 간접 주소 지정 방식
    • 명령어에 메모리 주소가 하드코딩되지 않고, 필요한 주소값을 저장하고 있는 메모리 위치를 참조하는 방법
    • 이 주소방식은 poointer을 사용할 때 쓰임
      // x = foo[j] 또는 x = *(foo + j)
      ADD R1, foo, j // R1 &lt;- foo+j
      LOAD R2, R1    // R2 &lt;- Memory[R1]
      STR R2, x      // x &lt;- R2
      

1.3.3 제어 흐름

프로그램은 보통 명령어를 순차작으로 실행합니다. 하지만 상황에 따라 다음 명령어가 아닌 특정 위치의 명령어를 수행하기도 합니다. if, switch 문과 같은경우 상황에 맞게 명령어를 분기해줘야합니다. 또한 for, while 과 같은 반복문은 loop의 끝에 오면 다시 위의 명령어를 실행하도록 해야합니다.

모든 기계어들은 이런 프로그래밍 구조를 지원하기 위해 프로그램 내에 선택된 주소의 명령으로 점프하는 기능을 가지고 있습니다.

2. 핵 기계어 명세

2.1 개요

핵 컴퓨터는 폰 노이만 플랫폼입니다.

CPU, 명령용과 데이터용으로 분리된 두 개의 메모리 모듈, 두개의 메모리 매핑 I/O 장치로 구성된 16비트의 장치입니다.

2.1.1 메모리 주소 공간

핵에서는 주소 공간이 명령어 메모리와 데이터 메모리라는 두 부분으로 분리되있습니다. 폭이 16비트인 두 메모리 공간은 15비트로 최대 32K 개의 16비트 워드를 주소에 할당할 수 있습니다.

핵의 CPU는 명령어 메모리에 존재하는 프로그램만을 수행합니다. 명령어 메모리는 읽기전용입니다. 내부적인 방법으로는 프로그램을 로드할 수 없고 외부적인 방법으로만 가능합니다. 프로그램을 미리 ROM에 입력하는 방식입니다.

핵의 하드웨어 시뮬레이터에서는 이런 ROM 교체 방식을 시뮬레이션하기 위해 텍스트파일에 있는 기계어 프로그램을 명령어 메모리 로드하는 기능을 제공하게 됩니다.

2.1.2 레지스터

핵 시스템에는 D와 A라 불리는 16비트의 레지스터 두개가 있습니다. 이 레지스터들은 A=D-1과 같은 산술 명령이나 D=!A같은 논리 명령으로 직접 조작됩니다. D는 데이터 값을 저장하는 용도로 쓰이는 반면 A 는 데이터 레지스터와 주소 레지스터라는 두 역할을 합니다.

2.2 A-명령어

A-명령어는 A레지스터에 15비트 값을 설정하는데 씁니다.

이 명령어는 컴퓨터가 A 레지스터에 특정 값을 저장하도록 명령합니다. 예를들어 @5 명령어는 0000000000000101 와 동일한데, A 레지스터에 2진수로 표현된 5를 저장하라는 뜻을 의미합니다.

A-명령어는 세가지 용도로 쓰입니다.

  • 상수 입력 용도
  • C 명령어에서 주소값을 참조할 수 있도록 A 레지스터에 데이터 주소 입력
  • A 레지스터에 점프할 주소를 미리 입력

image

2.3 C-명령어

C-명령어는 핵 플랫폼에서 거의모든 일을 수행하는 명령어입니다. C-명령어는 A명령어와 함께 쓰이면 본 책에서 만들 컴퓨터에서 가능한 모든 연산을 수행합니다.

맨 왼쪽 비트 값이 1이면 명령어가 C-명령어임을 나타내는 코드입니다. 그 다음의 두 비트는 사용되지 않습니다. 나머지 비트들은 명령어에 있는 세가지 필드를 뜻합니다. comp 필드는 ALU 가 할 연산이 무엇인지를 뜻합니다. dest 필드는 계산된 값을 어디에 저장할지 가리킵니다. jump 필드는 점프 조건으로 다음에 불러와서 실행할 명령어가 무엇인지를 의미합니다.

image

2.3.1 계산 필드

핵 ALU 는 D와 A 및 M 레지스터 상에서 미리 정해진 함수들을 계산합니다. 계산할 함수는 명령어의 comp 필드에서 정의되는데, 1개의 a-비트 6개의 c-비트로 총 7비트로 표현됩니다. 이 7비트 패턴으로는 최대 128개의 서로 다른 함수를 코드화할 수 있습니다. 아래의 그림은 그 중 28개의 패턴만 언어 명세에 포함됩니다.

image

C-명령어의 형식은 111a cccc ccdd djjj로 구성되어 있습니다. ALU에 D-1연산, 즉 D레지스터의 값에 1을 빼는 연산을 할 떄의 명령어를 확인해보겠습니다. 그림에서 보았을 때 D-1의 C-명령어 계산 필드는 001110 입니다. 따라서 D-11110 0011 1000 0000 로 표현할 수 있습니다. D|M1111 0101 0100 0000이며, 상수-1 연산은 1110 1110 1000 0000 이 됩니다.

2.3.2 목적지 필드

C-명령어의 comp 부분에서 계산된 값은 3비트 dest 부분이 가리키는 몇가지 목적지에 저장됩니다. 첫번째와 두번째 d-비트는 계산된 값을 각각 A 레지스터와 D레지스터에 저장할지를 결정하는 코드입니다. 세번째 d-비트는 계산된 값을 M에 저장할지를 결정하는 코드입니다.

C-명령어의 형식은 111a cccc ccdd djjj 입니다. 만약 메모리의 [7] 의 값을 1만큼 증가시키고 그 결과를 D레지스터에 저장하는 명령어는 아래와 같습니다.

0000 0000 0000 0111  // @7
1111 1101 1101 1000  // MD = M + 1

[그림 추가 예정]

2.3.3 점프 필드

C-명령어의 jump 필드는 컴퓨터가 다음에 수행할 일을 지시합니다. 컴퓨터가 하게될 일은 둘중 하나가 됩니다.

  • 프로그램에서 다음번 명령을 불러와서 실행
  • 프로그램 내 다른곳에 위치한 명령을 불러와서 실행 (A레지스터에 점프할 주소가 저장된경우)

실제로 점프할지 아닐지는 jump 필드의 세개의 j비트와 ALU 출력 값에 달렸습니다. ALU 출력값이 음수일 때는 첫 번째 j-비트, 0일 때는 두번째 j-비트, 양수일 때는 세번째 j-비트를 보고 점프 여부를 결정합니다.

  • 논리
    if Memory[3] = 5 then goto 100
    else goto 200
    
  • 구현
    @3
    D=M     // D = Memory[3]
    @5
    D=D-A   // D = D - 5
    @100
    D;JEQ   // If D = 0 goto 100
    @200
    0;JMP   // Goto 200
    

[그림 추가 예정]

마지막 명령어(0;JMP)는 무조건 점프를 뜻합니다. C-명령어 문법에 따르면 항상 어떠한 계산이라도 해야하기 떄문에, ALU가 0을 계산하고 그 결과값을 그냥 무시하기 위함입니다.

2.4 기호

어셈블리 명령은 상수나 기호를 이용하여 메모리 주소를 참조할 수 있습니다. 어셈블리는 세가지 방식으로 기호를 활용합니다.

2.4.1 미리 정의된 기호

RAM 내의 특정 주소들은 다음과 같이 미리 정의된 기호를 사용하며, 이 기호를 통해 어떤 어셈블리 프로그램에서도 주소를 참조합니다.

  • 가상 레지스터 : 어셈블리 프로그램을 단순화하기 위해 기호 R0 ~ R15 는 RAM 주소 0~15를 가리키도록 정의함
  • 미리 정의된 포인터 : SP, LCL, ARG, THIS, THAT 기호는 각각 RAM 주소 0 ~ 4를 참조하도록 정의된 기호임, 이 메모리 주소들은 이름이 둘임
  • I/O 포인터 : SCREEN과 KBD 기호는 각각 RAM 주소 16384(0x4000)와 24576(0x6000)를 참조하도록 미리 정의된 기호로, 이 주소들은 스크린 및 키보드의 메모리 매핑 시작 주소가 됨

2.4.2 레이블 기호

goto 명령어의 목적지를 나타내는 레이블 기호는 사용자가 (Xxx) 라는 의사 명령으로 직접 선언합니다. 이 명령은 다음에 실행할 명령을 담고 있는 명령어 메모리 주소를 기소 Xxx로 선언하라는 뜻입니다. 레이블은 한번 선언되면 어셈블리 프로그램 내 어디서든지 쓸 수 있으며, 심지어 선언된 라인의 앞쪽 라인에서도 사용할 수 있습니다.

2.4.3 변수 기호

어셈블리 프로그램 내에서 미리 정의된 기호가 아니거나, (Xxx) 명령으로 선언되지 않은 모든 사용자 정의 기호 Xxx 는 변수로 취급되며, 어셈블러는 RAM 주소 16(0x0010)에서부터 차례대로 변수마다 유일한 메모리 주소를 할당합니다.

2.5 입력 / 출력 조작

핵 플랫폼은 스크린과 키보드, 두 단말 장치에 연결될 수 있습니다. 두 장치는 메모리맵을 통해 컴퓨터 플랫폼과 통신하게 됩니다. 메모리 세그먼트에 2진 값을 쓰면 그에 대응하는 스크린 위에 픽셀이 그려지는 방식이 되겠습니다. 키보드 입력은 해당 키 입력에 대응하는 메모리 위치를 읽어 들이는 식으로 처리하게 됩니다. 물리적 I/O 장치와 메모리 맵은 계속 갱신되는 루프를 통해 동기화된다.

2.5.1 스크린

핵 컴퓨터의 스크린은 512개의 열과 256개의 행의 흑백 픽셀로 구성됩니다. 그리고 스크린에 표시되는 픽셀은 RAM 주소 16384(0x4000)부터 시작하는 8K의 메모리 맵에 대응됩니다. 물리적 스크린에서 각 행은 화면 맨 왼쪽부터 시작하며, RAM 내에서는 32개의 연속된 16비트 단어로 표현됩니다. 물리적 스크린에 픽셀을 쓰거나 읽으려면 RAM 내 메모리 맵에 해당 픽셀에 대응하는 비트에 읽거나 쓰면 됩니다. (1 = 검정, 0 = 흰색)

2.5.2 키보드

핵 컴퓨터는 RAM 주소 24576(0x6000)에 위치한 1워드의 메모리 맵을 통해 물리적 키보드 장치와 통신합니다. 실제 키보드에서 키가 눌릴 때마다, 눌린 키의 16비트 ASCII 코드가 RAM[24576]에 기록되게 됩니다. 키가 눌리지 않는 동안에는 해당 메모리에 값은 0이 기록됩니다. 핵 키보드에서 일반적인 문자 ASCII 코드 외의 특수 입력은 다음과 같습니다.

눌린 키 코드
새 라인 128
백스페이스 129
왼쪽 화살표 130
위쪽 화살표 131
오른쪽 화살표 132
아래쪽 화살표 133
홈(home) 134
엔드(end) 135
페이지 업 (page up) 136
페이지 다운 (page down) 137
인서트 (insert) 138
딜리트 (delete) 139
이스케이프 (esc) 140
f1 ~ f12 141 ~ 152

2.6 구문 규칙과 파일 형식

2.6.1 2진 코드 파일

2진 코드 파일은 텍스트 라인들로 구성됩니다. 각 라인은 16개의 ASCII 문자 ‘0’ 과 ‘1’로 되어있으며, 하나의 기계어 명령어를 부호화한것으로 보시면 됩니다. 그리고 파일 내 모든 라인들이 모여 하나의 기계어 프로그램을 이루게 됩니다. 컴퓨터 멍령어 메모리에 이 기계어 프로그램이 로드될 떄는 파일의 n번째 라인에 있는 2진 코드가 명령어 메모리의 주소 n에 저장되는 규칙을 따릅니다. 규칙에 따라 기계어 프로그램은 ‘hack’확장자를 가지는 텍스트파일에 저장됩니다.

2.6.2 어셈블리 언어 파일

어셈블리 언어 프로그램은 ‘asm’ 확장자를 가지는 텍스트 파일에 저장됩니다. 어셈플리 언어 파일은 텍스트 라인으로 구성되며, 각 텍스트 라인은 명령어나 기호를 뜻합니다.

  • 명령어 : A-명령어나 C-명령어
  • Symbol : 이 의사명령은 다음번 명령이 저장되는 메모리 symbol 레이블을 할당하라고 어셈블러에게 지시합니다.

2.6.3 상수와 기호

상수는 음수가 아니어야 하며, 항상 10진법으로 표기됩니다. 맨 앞 문자가 숫자만 아니면 문자, 숫자, 밑줄, 마침표, 달러기호, 콜론으로 이뤄진 어떤 문자열이라도 사용자 정의 기호로 선언 가능합니다.

2.6.4 주석

슬래시 두개(//) 에서 라인 끝까지의 텍스트는 주석으로 간주되어 무시됩니다.

2.6.5 공백

공백 문자는 무시됩니다. 빈 라인이라도 무시됩니다.

2.6.6 대소문자 규칙

모든 어셈블리 연상기호는 대문자로 써야합니다. 그 외 사용자 정의 레이블과 변수명은 대소문자를 구분합니다. </description> Tue, 28 Dec 2021 00:00:00 +0000 https://sunghwan7330.github.io/make_computing_system/make_computing_system_04_machine_code/</link> https://sunghwan7330.github.io/make_computing_system/make_computing_system_04_machine_code/

    <category>밑바닥부터 만드는 컴퓨팅</category>
    
    <category>기계어</category>
    
    
    <category>make_computing_system</category>
    
  </item>

  <item>
    <title>[밑바닥부터 만드는 컴퓨팅] 프로젝트03 순차논리장치 제작</title>
    <description># 개요 

해당 내용은 ‘밑바닥부터 만드는 컴퓨팅 시스템’ 책의 프로젝트 03 의 내용을 정리하려 합니다. 혹시나 따라하실 분은 실습준비 포스팅을 참고해서 프로그램을 다운받아주시면 됩니다.

프로그램 실행

다운받은 프로그램에서 HardwareSimulator.bat 를 실행해줍니다.

image

UI에서 보이는 화면 중 각 표시한 버튼은 아래와 같습니다.

  • ① : hdl 파일을 로드합니다.
  • ② : 스크립트를 실행합니다.
  • ③ : 스크립트를 로드합니다.

Bit 제작

CHIP Bit {
    IN in, load;
    OUT out;

    PARTS:
    Mux(a=dffOut, b=in, sel=load, out=muxOut);
    DFF(in=muxOut, out=dffOut);
    Or(a=false, b=dffOut, out=out); /*A limitation of HDL*/
}

image

bit는 1비트 레지스터입니다. 이전 포스팅에서 설명한바와 같이 Mux와 DFF를 이용하였습니다. Bit는 시간이 지나도 1비트의 값을 저장할 수 있는 기본적인 저장장치입니다.

load가 1이라면 Mux에서 입력된 값인 in이 출력됩니다. 이후 출력된 값이 DFF에 입력됩니다. 다음 클럭에서 load가 0이라면 이전에 입력된 값이 Mux 로 입력되어 다시 출력됩니다. 그렇기 때문에 load가 1이 입력되기 전까지는 이전에 입력된 값이 계속 유지되게 됩니다. (OR은 무시하셔도 됩니다.)

값의 read 는 DFF의out을 받아오면 되고, 값의 저장은 in에 값 입력후 load를 1로 주면 되겠습니다.

16bit 레지스터 제작

CHIP Register {
    IN in[16], load;
    OUT out[16];

    PARTS:
    Bit(in=in[0], load=load, out=out[0]);
    Bit(in=in[1], load=load, out=out[1]);
    Bit(in=in[2], load=load, out=out[2]);
    Bit(in=in[3], load=load, out=out[3]);
    Bit(in=in[4], load=load, out=out[4]);
    Bit(in=in[5], load=load, out=out[5]);
    Bit(in=in[6], load=load, out=out[6]);
    Bit(in=in[7], load=load, out=out[7]);
    Bit(in=in[8], load=load, out=out[8]);
    Bit(in=in[9], load=load, out=out[9]);
    Bit(in=in[10], load=load, out=out[10]);
    Bit(in=in[11], load=load, out=out[11]);
    Bit(in=in[12], load=load, out=out[12]);
    Bit(in=in[13], load=load, out=out[13]);
    Bit(in=in[14], load=load, out=out[14]);
    Bit(in=in[15], load=load, out=out[15]);
}

위에서 Bit를 제작하였습니다. 이를 16개 연결하면 16비트의 레지스터를 만들 수 있습니다. 레지스터는 bit와 같지만 값을 16비트 단위로 저장할 수 있다는 차이만 있습니다.

PC 제작

CHIP PC {
    IN in[16],load,inc,reset;
    OUT out[16];

    PARTS:
    Inc16(in=regOutput, out=regOutputInc);    
    Mux16(a=regOutput, b=regOutputInc, sel=inc, out=out1);
    Mux16(a=out1, b=in, sel=load, out=out2);
    Mux16(a=out2, b=false, sel=reset, out=regInput);
    
    // load == load OR inc OR reset !!!
    Or(a=load, b=inc, out=loadOrInc);
    Or(a=loadOrInc, b=reset, out=loadOrIncOrReset);
    
    Register(in=regInput, load=loadOrIncOrReset, out=regOutput);    
    
    Or16(a=false, b=regOutput, out=out); // dummy OR gate for output
}

HDL 이 조금 복잡하여 그림을 직접 그려보았습니다.

image

어떻게 보이실지는 모르겠지만, 저는 이정도 되니 그나마 볼만했습니다. 입력되는 신호는 색상으로 표현하였습니다.

  • in : 붉은색
  • inc : 파란색
  • load : 초록색
  • reset : 분홍색

PC는 값을 1 증가시키는 것이 기본 동작이며, 그 외에추가적으로 특정 값으로 이동하거나 카운트를 초기화하는 기능이 있습니다.

hdl로 제작한 PC에서는 inc, load, reset 의 신호를 입력하여 PC를 제어합니다.

  • inc : PC의 값을 1 증가
  • load : 카운트를 in에 입력된 값으로 변경
  • reset : 카운트를 0으로 변경

그럼 위에 첨부된 그림을 기준으로 설명드리겠습니다.

가장먼저 왼쪽의 Inc16 을 보시면 맨 오른쪽에 Register 에서 출력된 값이 나와 값을 1 증가시켜줍니다.

이후 첫번째 Mux16 에서 a로 입력되는 값은 Register에서 출력된 값이고, b에 입력되는 값은 Inc16에서 입력된 값입니다. inc로 입력된 신호가 Mux16sel로 입력하여 어떠한 값을 out으로 출력할지 결정합니다. inc가 1로 입력된다면 a에 입력된 1이 증가된 값이 출력되게 됩니다.

그다음에 보이는 Mux16 에서 a로 입력되는 값은 이전의 Mux16에서 출력된 값이고, b에 입력되는 값은 in으로 입력된 값이며, sel에 입력되는 값은 load로 입력된 값입니다. load가 1이라면 in 의 값이 선택되고, 아니라면 이전의 값이 선택되게 됩니다.

이후 보이는 Mux16 에서 a로 입력되는 값은 이전의 Mux16에서 출력된 값이고, b에 입력되는 값은 false이며, sel에 입력되는 값은 reset로 입력된 값입니다. reset가 1이라면 false 가 입력되어 카운트가 0이 되며, 아니라면 이전의 값이 선택되게 됩니다.

여기까지 진행되었을 때 가장 마지막 Mux16 에서 나온 값을 Register 에 입력합니다. Register 의 load 값은 PC에 입력된 flags에 의해 결정되게 됩니다. OR게이트를 이용하여 inc, load, reset 셋 중 하나가 1이라면 1이 되도록 하여 Registerload 에 입력될 수 있도록 합니다. 만약 inc, load, reset 이 모두 0으로 입력되었다면 Register의 값은 변하지 않게 됩니다.

위와 같이 살펴보았을 때 PC는 inc, load, reset 의 값에 의해 카운트를 변화하는 역할을 한다는 것을 알 수 있었습니다. 요약하자면 PC는 Register의 값과, 1 추가된 값, in 에 입력된 주소값, 0 이 네개의 값을 멀티플렉서를 이용하여 선택할 수 있도록 한 뒤 결과값을 Register 에 입력해주도록 구성해주면 되겠습니다.

RAM8 제작

CHIP RAM8 {
    IN in[16], load, address[3];
    OUT out[16];

    PARTS:
    DMux8Way(in=load, sel=address, a=sel0, b=sel1, c=sel2, d=sel3, e=sel4, f=sel5, g=sel6, h=sel7);
    Register(in=in, load=sel0, out=r0);
    Register(in=in, load=sel1, out=r1);
    Register(in=in, load=sel2, out=r2);
    Register(in=in, load=sel3, out=r3);
    Register(in=in, load=sel4, out=r4);
    Register(in=in, load=sel5, out=r5);
    Register(in=in, load=sel6, out=r6);
    Register(in=in, load=sel7, out=r7);
    Mux8Way16(a=r0, b=r1, c=r2, d=r3, e=r4, f=r5, g=r6, h=r7, sel=address, out=out);
}

DMux 는 in에 입력된 값을 sel을 보고 해당 위치로 출력하는 게이트입니다. sel이 1이면 in에 입력된 값을 a에 출력하고, sel이 7이라면 in에 입력된 값을 h에 출력합니다.

우선 모든 레지스터에 in의 값을 입력합니다. 이후 Dmux에 load 값을 입력하여 특정 Register에만 load 값이 전달되도록 합니다. 그렇게 되면 여러 레지스터 중 하나의 레지스터에만 load 값이 전달되어 in 값이 입력됩니다.

이후 모든 레지스터에서 출력된 값을 Mux를 이용하여 출력합니다. Mux의 sel에 address를 입력하여 원하는 레지스터의 값만 받아옵니다.

위와 같이 하였을 때 특정 주소의 레지스터에 값을 입력하거나 출력할 수 있게 됩니다.

RAM 64 제작

CHIP RAM64 {
    IN in[16], load, address[6];
    OUT out[16];

    PARTS:
    DMux8Way(in=load, sel=address[3..5],a=sel0,b=sel1,c=sel2,d=sel3,e=sel4,f=sel5,g=sel6,h=sel7);
    RAM8(in=in, load=sel0, address=address[0..2], out=r0);
    RAM8(in=in, load=sel1, address=address[0..2], out=r1);
    RAM8(in=in, load=sel2, address=address[0..2], out=r2);
    RAM8(in=in, load=sel3, address=address[0..2], out=r3);
    RAM8(in=in, load=sel4, address=address[0..2], out=r4);
    RAM8(in=in, load=sel5, address=address[0..2], out=r5);
    RAM8(in=in, load=sel6, address=address[0..2], out=r6);
    RAM8(in=in, load=sel7, address=address[0..2], out=r7);
    Mux8Way16(a=r0, b=r1, c=r2, d=r3, e=r4, f=r5, g=r6, h=r7, sel=address[3..5], out=out);
}

RAM 64는 RAM8을 8개 연결하여 제작합니다. address 가 6비트이기 때문에 주소는 0 ~ 63까지 입력이 가능합니다.

앞에서 제작한 RAM8은 3비트의 arrress인 0~7까지 입력이 가능합니다. 따라서 DMux8Way 에 주소값 3비트를 입력하여 어느 RAM8에 입력할 지를 결정합니다. 이후 나머지 주소의 3비트를 RAM8에 입력하여 올바른 주소의 레지스터에 작성될 수 있도록 합니다. 마지막으로 레지스터로부터 나온 RAM8의 출력을 Mux8Way16 에 의해 분류되어 출력될 수 있도록 합니다.

참고

  • https://github.com/simulacre7/nand2tetris

</description> Wed, 22 Dec 2021 00:00:00 +0000 https://sunghwan7330.github.io/make_computing_system/make_computing_system_pracice_03/</link> https://sunghwan7330.github.io/make_computing_system/make_computing_system_pracice_03/

    <category>밑바닥부터 만드는 컴퓨팅</category>
    
    
    <category>make_computing_system</category>
    
  </item>

  <item>
    <title>[밑바닥부터 만드는 컴퓨팅] 03장 순차 논리</title>
    <description># 개요 

해당 내용은 ‘밑바닥부터 만드는 컴퓨팅 시스템’ 책의 3장 내용을 정리하였습니다.

1, 2장에서 만든 칩은 모두 조합칩이였습니다. 조합칩은 입력값의 조합에만 의존하는 함수를 계산합니다. 비교적 단순한 칩들은 중요한 기능을 하지만 상태를 유지하지는 않습니다. 컴퓨터는 값을 계산하는것은 물론 시간이 지나도 데이터를 보존할 수 있어야 합니다. 이러한 메모리 소자는 순자 칩으로 만듭니다.

메모리 소자 구현은 동기화, 클로킹, 피드백 루프 등을 포함하는 복잡한 기술입니다. 해당 장에서는 클럭과 플립플롭에 대해 살펴보고, 이 소자를 기본으로 하는 메모리 칩에 대해 알아보겠습니다.

1. 배경

1.1 클럭

대부분의 컴퓨터는 연속적으로 신호를 발생하는 마스터 클럭으로 시간 진행을 표현합니다. 보통 이 하드웨어는 (0, 1) (고, 저) (tic, tock) 등과 같이 두 신호 상태를 번갈아가면서 구현됩니다. 이때 한 신호에서 다른 신호로 바뀔때의 시간, 예를들어 0에서 1로 바뀔때까지 시간을 사이클이라 부릅니다. 현재의 클럭 사앹는 2진 신호로 표현되고, 이 신호는 하드웨어의 회로망을 통해 모든 컴퓨터에 있는 순차 칩들에 동시에 전달됩니다.

1.2 플립플롭

컴퓨터에서 가장 기본적인 순차소자입니다. 플립플롭에는 몇가지 종류가 있습니다. 해당 책에서는 데이터 플립플롭(DFF)이라는 소자를 사용하며, 이 소자는 1비트 입력과 1비트 출력으로 구성됩니다. 또한 이 DFF는 마스터 클럭에 따라 계속적으로 바뀌는 클럭 입력 신호를 받습니다. DFF는 데이터 입력과 클럭 입력을 종합하여 시간에 따른 동작을 수행합니다.

out(t) = in(t-1)  // 1클럭 전의 값을 출력

이 기본 기능은 2진 셀에서 레지스터, 임시 기억장치 등 컴퓨터에서 상태를 유지하는 모든 하드웨어에 기초가 됩니다.

1.3 레지스터

레지스터는 시간이 지나도 값을 저장하고 로드할 수 있는 장치로 기본적인 저장기능인 out(t) = in(t-1) 을 구현한 것입니다.

DFF 는 이전 입력을 출력하는 장치로 out(t) = in(t-1)을 수행합니다. 이러한 특성을 이용하여 Mux와 DFF를 이용해 레지스터를 만들 수 있습니다.

image

멀티플렉서의 선택비트는 전체 레지스터의 로드 비트가 됩니다. 만약 레지스터가 새로운 값을 저장하게 하려면 in에 새로운 값을 넣고 로드 비트를 1로 설정하면 됩니다. 그리고 다음번 변경 전까지 내부 값을 계속 유지하려면 로드 비트를 0으로 맞추면 됩니다.

이와 같이 레지스터는 하나의 비트를 기억할 수 있습니다. 이를 이용하여 더 많은 비트를 기억하는 레지스터를 만들 수 있습니다. 멀티 비트를 저장하는 레지스터를 만드려먼 1비트 레지스터를 필요한 만큼 배열하면 됩니다. 레지스터에 저장되는 멀티비트의 값은 보통 워드(word)라고 표현합니다.

1.4 메모리

레지스터를 이용하여 워드를 구성할 수 있으면 이제 임의의 길이의 메모리 뱅크를 구성할 수 있습니다.

image

위 그림은 1비트 레지스터와 멀티비트 레지스터입니다. 값을 저장할 수 있는 크기를 제외하면 둘은 같은 기능을 수행합니다.

image

위 그림은 여러개의 레지스터를 연결하여 만든 임시접근메모리(Random Access Memory)입니다. 임시접근메모리는 접근 순서와 관계없이 무작위로 선택된 워드를 읽고 쓸수 있다는 말에서 나왔습니다. 즉 메모리 내의 어떤 워드라도 물리적 위치에 상관없이 같은 속도로 직접 접근이 가능하다는 뜻입니다.

RAM은 데이터 입력, 주소 입력, 로드 비트라는 세가지 입력을 받습니다. 이때 주소는 현재 시간 사이클에서 어떤 RAM 레지스터에 접근할지를 가르킵니다. 읽기 연산(load=0)인 경우, RAM은 서너택된 레지스터의 값을 바로 출력합니다. 쓰기 연산(load=1)인 경우, 다음 사이클 때 선택된 메모리 레지스터에 입력된 값을 받아 해당 값을 출력하기 시작합니다.

RAM 장치의 기본 변수로는 각 워드의 비트 수를 뜻하는 데이터 폭과 RAM 내의 워드 수를 나타내는 크기가 있습니다. 최신 컴퓨터는 보통 폭이 64비트이면서 크기가 수억만개에 달하는 RAM을 사용합니다.

1.5 계수기

계수기(counter) 은 시간 단위마다 내부 상태의 값을 증가시키는 칩입니다. out() = out(t-1) + c 를 수행합니다.

image

계수기는 컴퓨터에서 매우 중요한 역할을 수행합니다. 일반적인 예로는 CPU에서 다음에 실행할 명령어의 추소를 나타내는 기능을 수행합니다.

계수기 칩은 표준 레지스터의 입/출력 논리와 상태값에 상수를 더하는 논리가 조합된 것입니다. 일반적으로 계수기는 값을 0으로 다시 맞추거나 새로운 값을 불러오거나 값을 증가 또는 감소시키는 기능이 추가됩니다.

2. 명세

해당 절에서는 아래의 칩들을 정의합니다.

  • 데이터 플립플롭 (DFF)
  • 레지스터 (DFF 기반)
  • 메모리 뱅크 (레지스터 기반)
  • 계수기 칩 (레지스터 기반)

2.1 데이터 플립플롭

데이터 플립플롭 게이트는 모든 메모리 소자의 기본 부품이 됩니다. DFF 게이트는 1비트 입력을 받아 1비트를 출력합니다.

칩 이름: DFF
입력: in
출력: out
기능: out(t)-in(t-1)

Nand 게이트와 더불어 DFF 게이트는 컴퓨터 아키텍쳐에서 가장 기본이 됩니다. 특히 컴퓨터 내 모든 조합 칩(레지스터, 메모리, 계수기)들은 수많은 DFF 로 구성됩니다. 이 DFF 들은 마스터 클럭에 연결되어 행동하게 됩니다. 클록 사이클이 시작할 때 모든 DFF 출력들은 전 사이클에 입력에 따라 맞춰집니다. 그 시간 외에는 DFF가 잠금 상태가 됩니다. 즉 입력이 변해도 곧바로 출력에 영향을 받지 않습니다. 컴퓨터에서는 이러한 연산이 초당 십억번씩 모든 DFF에서 수행됩니다.

2.2 레지스터

우리가 비트 또는 2진 셀이라 부르는 1비트 레지스터는 0 또는 1의 값을 저장하도록 설계된 소자입니다. 칩 인터페이스는 데이터 비트를 전달하는 입력 핀과 쓰기 기능을 설정하는 load 핀, 그리고 셀의 현재 상태랄 내보내는 출력 핀으로 이뤄집니다.

2진 셀의 인터페이스와 API는 다음과 같습니다.

칩 이름: Bit
입력: in, load
출력: out
기능: If load(t-1) then out(t)=in(t-1)
      else out(t)=out(t-1)

레지스터ㅓ는 입력 핀과 출력 핀이 멀티비트를 처리할 수 있다는 점을 제외하고는 기본적인 2진 셀과 같습니다.

입력: in[16], load
출력: out [16]
기능: If load(t-1) then out(t)=in(t-1)
      else out(t)=out(t-1)
설명: &quot;=&quot;는 16비트 연산이다.
  • 읽기 : 레지스ㅓ 내부 값을 읽으려면 단순히 출력 신호를 보면 됩니다.
  • 쓰기 : 레지스터에 새로운 값 d를 쓰려면 in에 d를 입력하고, load에 명련신호(1)을 줍니다. 다음 클럭 사이클이 되면 레지스터는 새로운 데이터 값을 받아 d를 내보내기 시작합니다.

2.3 메모리

RAM은 직접 접근 메모리 장치로, n개의 w-비트 레지스터를 배열하고 직접 접근 회로를 연결한 소자입니다. 메모리에 들어간 레지스터의 수(n) 및 비트수(w)는 각각 메모리의 크기(size)와 폭(width)이라고 부릅니다.

이제 폭이 16비트이고 크기가 다양한 메모리 계층을 구축해야 합니다. (RAM8, RAM64, RAM512 등) 이러한 메모리 칩들은 모두 API가 동일합니다.

image

칩 이름: RAMn // no k 목록은 맨 아래에 있음
입력: in(16), address[k], load
출력: out [16]
기능: out(t)=RAM (address(t)](t)
      If load(t-1) then
        RAM (address(t-1)](t)=in(t-1)
설명: &quot;=&quot;는 16비트 연산이다.
  • 읽기 : 레지스터 번호 m에 해당하는 주소의 내부 값을 읽으려면 address에 m을 넣습니다. 그러면 RAM의 직접 접근 논리에 따라 레지스터 번호 m이 선택되고, RAM의 출력 핀에 그 값이 출력됩니다. 이 과정은 조합 연산으로 클럭의 영향을 받지 않습니다.

  • 쓰기 : 세로운 데이터 값 d를 레지스터 번호 m 에 해당하는 주소에 쓰려면 address에 m을, in에 d를 입력하고 load 비트를 활성화합니다. RAM의 직접접근 논리에 따라 레지스터 번호 m이 선택되고, load 비트로 인해 RAM이 쓰기 모드로 바뀝니다. 다음 클럭 사이클에서 선택된 레지스터가 새로운 값을 받아 출력하기 시작합니다.

2.4 계수기

계수기는 그 자체로는 독립적인 추상화 계층입니다. 컴퓨터에서 계수기가 어떤식으로 사용되는지 간단하게만 살펴보겠습니다.

컴퓨터가 다음번 실행할 명령의 주소를 기록하는 계수기 칩이 있다고 생각해보겠습니다. 일반적인 상ㅌ태에서는 계수기는 매 클럭 사이클마다 상태값을 1씩 증가시켜 컴퓨터가 다음 명령을 불러올 수 있게 합니다. 만약 if문과 같은 분기로 인해 명령 주소를 이동할 때는 계수기의 값을 주소만큼 더하여 jump하도록 할 수 있습니다. 또한 프로그램의 시작주소로 jump 하여 프로그램을 재시작할 수도 있습니다.

이러한 기능을 생각하고 보면 reset 과 inc라는 제어비트가 추가된 것을 제외하면 레지스터와 비슷합니다. inc=1 일때 계수기는 매 클럭사이클마다 상태 값을 증가시키고 out(t)=out(t-1)+1 을 출력합니다. reset 비트를 활성화하면 계수기를 0으로 설정할 수 있습니다. 이때 in 에 특정 값을 넣으면 해당 값으로 초기화됩니다.

3. 마무리

1, 2장까지는 그래도 천천히 보면 이해할만한 내용이였는데, 3장은 클럭 개념이 들어가면서 조금 많이 어려워졌습니다 ㅠㅠ 저도 책을 보고 작성하면서 개념적인 부분은 이해했지만, 아직 누구에게 자세히 설명하기는 힘들어보입니다. 실습을 하여 실제 구현을 해보면 조금 더 이해가 잘 될 수 있을듯 합니다.

</description> Wed, 01 Dec 2021 00:00:00 +0000 https://sunghwan7330.github.io/make_computing_system/make_computing_system_03_seq/</link> https://sunghwan7330.github.io/make_computing_system/make_computing_system_03_seq/

    <category>밑바닥부터 만드는 컴퓨팅</category>
    
    <category>순차 논리</category>
    
    
    <category>make_computing_system</category>
    
  </item>

  <item>
    <title>[밑바닥부터 만드는 컴퓨팅] 02장 불 연산</title>
    <description># 개요 

해당 내용은 ‘밑바닥부터 만드는 컴퓨팅 시스템’ 책의 2장 내용을 정리하였습니다.

이번 장에서는 숫자를 표현하고 계산하는 논리게이트를 설계합니다. 1장에서는 기본적인 논리 게이트를 만들었다면, 이번장에서는 산술 논리 연산장치를 만드는 것을 목표로 합니다. ALU는 컴퓨터의 모든 산술 및 논리 연산이 이루어지는 집이며, ALU를 구현하여 컴퓨터가 어떻게 작동하는지 이해할 수 있습니다.

1. 배경

1.1 2진수

2진수는 기수(base)를 2로 합니다. 1100 이라는 이진수가 있다면 다음과 같이 10진수로 변환할 수 있습니다.

(1*8) + (1*4)) + (0*2) + (0*1) = 8 + 4 = 12

1.2 2진 덧셈

2진수의 덧셈은 어릴 때 배운 10진수의 덧셈 방법과 같습니다. 다만 자릿수가 바뀌는 값이 10이 아닌 2라는 것이 차이입니다. 가장 오른쪽부터 더하고, 값이 2을 넘을 때 자릿수를 넘겨 계산하면 됩니다. 이러한 방식으로 가장 높은 자리까지 더해주면 됩니다.

만약 마지막 비트를 더하고 나서도 자리올림수가 1이라면 overflow가 발생합니다.

* overflow 발생 없는 경우 


  1 0 0 1
+ 0 1 0 1
-----------
0 1 1 1 0


* overflow 가 발생하는 경우 

  1 0 1 1 
+ 0 1 1 1
-----------
1 0 0 1 0

1.3 부호가 있는 2진수

n비트의 수로 표현 가능한 비트는 총 2^n개입니다. 예를들어 2비트라면 표현가능한 값은 00, 01, 10, 11`으로 총 2^2개입니다. 만약 부호를 표시해야 한다면 표현할 수 있는 두개의 집합으로 나누는 것이 자연스러울 것입니다. 또한 하드웨어 구현이 가능한 덜 복잡하도록 구성하는것이 좋을 것입니다.

이러한 문제를 해결하기 위해 2진 표현법에서 부호있는 숫자를 표현하는 여러가지 방법이 개발되었습니다. 그 중 오늘날 거의 대부분의 컴퓨터들은 2의 보수법 이라는 방법을 사용합니다. (컴퓨터구조 시간에 들어봤죠?)

2의 보수법의 음수표현은 해당 비트의 가장 큰 값에서 표현할 값을 빼서 계산합니다. 만약 4비트 2진수에서 -2를 나타내는 2의 보수는 아래와 같습니다.

1111 - 0010 = 1101

이러한 2의 보수법은 아래와 같은 특성을 가집니다.

양수 음수
0 :0000  
1 : 0001 -1 : 1111
2 : 0010 -2 : 1110
3 : 0011 -3 : 1101
4 : 0100 -4 : 1100
5 : 0101 -5 : 1011
6 : 0110 -6 : 1010
7 : 0111 -7 : 1001
  -8 : 1000
  • 이 방식으로 총 2^n개의 수를 표현할 수 있음
  • 모든 양의 코드는 0으로 시작함
  • 모든 음의 코드는 1로 시작함

저는 학교다닐 때 이 2의보수법을 왜 사용하는지 몰랐습니다. 하지만 지금에서는 이 방법이 하드웨어적으로 계산하기 가장 편한 방법이라고 생각이 됩니다. 2의 보수법은 가산기, 즉 덧셈만으로 연산을 할 수 있습니다.

1 - 11 + (-1) 과 같습니다. 이를 2진 계산법으로 아래와 같이 표현할 수 있습니다.

0001 + 1111 = 0000

캐리를 무시하고 덧셈을 하면 0이 되죠.

한번 더 해보도록 하겠습니다. 5 - 32입니다. 이진 계산법으로 아래와 같이 표현할 수 있습니다.

0101 + 1101 = 0010

이처럼 2의 보수법은 가산기만을 이용하여 뺄셈을 계산할 수 있게됩니다.

2. 명세

2.1 가산기

다음 세 개의 가산기 칩들은 멀티비트 가산기 칩으로 이어집니다.

  • 반가산기 (half-adder) : 두 비트를 더함
  • 전가산기 (full-adder) : 세 비트를 더함
  • 가산기 (adder) : 두 개의 n비트 숫자를 더함

2.1.1 반가산기

반가신기는 두 비트를 더하는 기능을 합니다. 덧셈안 값의 최하위 비트를 sum, 최상위 비트를 carry라고 합니다. 1+1이라면 sum은 0, carry는 1이 되겠습니다.

image

칩 이름: HalfAdder
입력: a,b
출력: sum, carry
기능: sum = a + b의 LSB(최하위 비트)
    carry = a + b의 MSB(최상위 비트)

2.1.2 전가산기

반가산기와 마찬가지로 전가산기 칩의 출력은 덧셈 값의 최하위 비트와 자리올림 비트의 2개입니다.

image

칩 이름: FullAdder
입력: a,b,c
출력: sum, carry
기능: sum = a+b + c의 LSB
    carry = a+b + c의 MSB

2.1.3 가산기

메모리와 레지스터 칩은 n비트 패턴으로 된 정수를 저장합니다. 예를들어 32bit의 운영체제를 사용한다면 메모리와 레지스터는 32bit 단위롸 정수를 읽고 쓰게 됩니다. 이러한 범위 단위로 덧셈을 하는 칩을 멀티비트 가산기 또는 간단하게 가산기라고 부릅니다.

image

칩 이름: Add16
입력: a[16], b[16]
출력: out [16]
기능: out = a + b
설명: 정수의 2의 보수 덧셈
    오버플로는 감지나 처리가 되지 않는다.

2.1.4 증분기

증분기는 단순하게 숫자에 1을 더하는 기능을 하는 칩입니다.

칩 이름: Inc16
입력: in[16]
출력: out[16]
기능: out = in + 1
설명: 점수의 2의 보수 덧셈
오버플로는 감지나 처리가 되지 않는다.

</description> Wed, 10 Nov 2021 00:00:00 +0000 https://sunghwan7330.github.io/make_computing_system/make_computing_system_bool_oper/</link> https://sunghwan7330.github.io/make_computing_system/make_computing_system_bool_oper/

    <category>밑바닥부터 만드는 컴퓨팅</category>
    
    <category>불 연산</category>
    
    
    <category>make_computing_system</category>
    
  </item>

</channel> </rss>


# robots.txt

블로그 레퍼지토리의 최상단에 `robots.txt` 파일을 생성해준다. 
이 파일은 검색 엔진이 홈페이지를 크롤링할 떄 사용된다고 한다. 
`robots.txt` 파일에 아래의 내용을 추가해준다. 

User-agent: * Allow: /

Sitemap: http://jinyongjeong.github.io/sitemap.xml ```

사이트 등록

google 검색 허용 작업

Google Search Console 에 접속합니다.

image

위 이미지와 같이 URL 접두어에 자신의 블로그 주소를 입력해줍니다. 그러면 다음과 같은 소유권 확인 다이얼로그가 나타납니다.

image

다이얼로그에 포함된 html 파일을 다운로드합니다. 이후 해당 파일을 블로그 레퍼지토리의 최상단 경로에 업로드한 후 push해줍니다.

이 작업은 도메인에 대한 소유권을 인증하는 작업이라고 합니다. 사이트의 소유자만이 구글 검색에 대한 허가를 해줄 수 있기 때문입니다.

파일 업로드 후 push 한 뒤 약 1~2분 뒤 완료 버튼을 눌러줍니다. 정상적으로 파일이 업로드되었다면 아래와 같은 화면이 나타나게 됩니다.

image

(진행중)

네이버 웹마스터 도구 에 접속한 뒤 로그인해줍니다. 이후 우측 상단에 웹마스터 도구를 클릭합니다.

image

사이트 등록하는 화면에서 자신의 블로그 주소를 입력해줍니다.

image

그러면 위의 구글 검색 작업했던것과 같이 소유권 증명을 해달라고 합니다. 구글에서 햇던것과 같이 1. HTML 확인 파일을 다운로드합니다. 의 확인 파일을 받은 뒤 자신의 블로그 레퍼지토리 최상단 루트에 업로드해줍니다.

push를 한 뒤 약 1~2분 뒤 소유 확인 버튼을 눌러주면 됩니다.

Leave a comment