풀스택 웹🌐 개발자 지망생 🧑🏽‍💻
➕ 인공지능 관심 🤖


Categories


Recent views

  • 1
  • 2
  • 3
  • 4
  • 5

어떻게 코딩할 것인가-캡슐화

  1. 리팩토링과 캡슐화

어떻게 코딩할 것인가-기타

리팩토링과 캡슐화

현대의 소프트웨어는 수 많은 개발자와 수백, 수천만의 코드로 이루어져있다.

이때 사용에 필요한 다른 개발자의 코드를 찾고, 이름이 겹치지 않는 코드를 만드는 것은 힘드므로, 리팩토링과 캡슐화를 통해서 해결해야 한다.

리팩토링은 코드의 기능을 바꾸지 않고 구조를 바꾸는 것이며, 캡슐화는 외부에 공개할 필요없는 정보(함수, 변수, 값) 등을 숨기는 것이다.

리팩토링을 통해 캡슐화하는 과정을 알아보고, 노하우와 이점을 알아보자.

캡슐화 대상 코드 찾기

보통, 리펙토링은 이미 작성이 끝난 코드를 대상으로 한번 더 체크하면서 이루어진다.

리펙토링의 목적은 성능 최적화, 가독성 증가, 캡슐화 등 다양하지만, 우리는 캡슐화를 위해 예제를 리펙토링할 것이다.

만약, 코드의 변경이 필요하다면, 절대로 리펙토링과 변경을 동시에 진행하지 말고, 둘 중 하나씩 차근차근 진행하자.

캡슐화가 필요한 코드는 다음과 같은 특징이 있다.

  • 여러 보조 함수(Helper function)와 변수들로 이루어져 있음
  • 실질적인 수요는 메인 함수 하나만 사용되는 경우
  • 혹은 아예 너무 많은 기능을 하는 함수로 이루어져 여러 보조 함수로 나눠야하는 경우

다음 예시 코드를 보자

🧾️ 캡슐화 대상 코드 예시
; String Element -> Integer or false 
; String ListOfElement -> Integer or false???
; search the given tree for an element with the given name, produce data if found; false otherwise
(check-expect (find--loe "F3" empty) false)
(check-expect (find--element "F3" F1) false)
(check-expect (find--element "F3" F3) 3)
(check-expect (find--element "D4" D4) 0)
(check-expect (find--element "D6" D6) 0)
(check-expect (find--loe "F2" (cons F1 (cons F2 empty))) 2)
(check-expect (find--loe "F3" (cons F1 (cons F2 empty))) false)
(check-expect (find--element "F3" D4) false)
(check-expect (find--element "F1" D4) 1)
(check-expect (find--element "F2" D4) 2)
(check-expect (find--element "F1" D6) 1)
(check-expect (find--element "F3" D6) 3)
 
;(define (find--element n e) false) ;stubs
;(define (find--loe n loe)   false)

(define (find--element n e) ; 실질적으로 이용하는 메인 함수
  (if (string=? (elt-name e) n)
      (elt-data e) 
      (find--loe n (elt-subs e))))

(define (find--loe n loe) ; 보조 함수
  (cond [(empty? loe) false]
        [else
         (if (not (false? (find--element n (first loe)))) 
             (find--element n (first loe))
             (find--loe n (rest loe)))]))

위의 find--loe 함수는 그저 find--element의 보조 함수로만 이용되며, 공통으로 사용될 여지가 없다.
따라서, 캡슐화의 대상이 된다.

캡슐화

캡슐화의 주된 방법은 범위(Scope)를 나누는 것이다.
캡슐화의 이유인 이름 겹침 방지, 정보 은닉 등을 범위를 나누어 해결할 수 있다.

이를 이용한 캡슐화의 단계는 다음과 같다.

  1. 캡슐화 대상 코드들을 찾는다.
  2. 하나의 메인 함수 시그니처를 준비한다.
  3. 찾은 코드들을 하나의 로컬 범위로 묶는다.
  4. 하나의 리턴값을 내는 메인 함수를 정의한다.
  5. 필요없어진 시그니처, 스텁, 테스트들을 지운다.(혹은 옮긴다.)
    • 여러 프로그래밍 언어에서 내부 함수들을 위한 단위 테스트 또한 지원하므로 이를 위해 남길 수 있다.
  6. 메인 함수 역할을 하던 이전 테스트와 스텁의 이름을 바꾼다.

이를 이용해 앞선 예시 코드를 아래와 같이 캡슐화할 수 있다.

🧾️ 캡슐화된 코드
;; String Element -> Integer or false 
;; search the given tree for an element with the given name, produce data if found; false otherwise
(check-expect (find "F3" F1) false) ; 이전에 존재하던 find--loe 테스트는 삭제되거나 옮겨짐. 
(check-expect (find "F3" F3) 3)
(check-expect (find "D4" D4) 0)
(check-expect (find "D6" D6) 0)
(check-expect (find "F3" D4) false)
(check-expect (find "F1" D4) 1)
(check-expect (find "F2" D4) 2)
(check-expect (find "F1" D6) 1)
(check-expect (find "F3" D6) 3)
 
;(define (find n e) false) ;stubs ; 새로 캡슐화된 메인 함수(find)

(define (find n e)
  (local [ ; 새로운 범위 생성
          (define (find--element n e)
            (if (string=? (elt-name e) n)
                (elt-data e) 
                (find--loe n (elt-subs e))))
          (define (find--loe n loe) ; 이제 전역 범위에서 find--loe 이름이 겹처도 상관없으며, 접근할 수 없다. 
            (cond [(empty? loe) false]
                  [else
                   (if (not (false? (find--element n (first loe)))) 
                       (find--element n (first loe))
                       (find--loe n (rest loe)))]))
          ]
    (find-element n e))) ; 트램펄린 함수(대표 함수)로 리턴값 생성

기존 코드에 비해 짧아지고, 캡슐화의 장점이 적용되었다.

실무에 사용되는 프로그래밍 언어들은 더욱 더 세세하고 효율적인 캡슐화 기능들을 제공한다.

템플릿 설계 단계에서 캡슐화하기

데이터 설계 단계에서 여러 보조 함수로 나뉜 경우, 템플릿 단계에서 캡슐화를 적용할 수 있다.

(define F1 (make-elt "F1" 1 empty))
(define F2 (make-elt "F2" 2 empty))
(define F3 (make-elt "F3" 3 empty))
(define D4 (make-elt "D4" 0 (list F1 F2)))
(define D5 (make-elt "D5" 0 (list F3)))
(define D6 (make-elt "D6" 0 (list D4 D5)))
#;
(define (fn-for-element e)
  (... (elt-name e)    ;String
       (elt-data e)    ;Integer
       (fn-for-loe (elt-subs e))))
#;
(define (fn-for-loe loe)
  (cond [(empty? loe) (...)]
        [else
         (... (fn-for-element (first loe))
              (fn-for-loe (rest loe)))])) 
;; 위 코드를 ...
;; ----잠시 후-----
;; 아래처럼 바로 캡슐화한 뒤, 로직을 조금만 바꾸어 완성 할 수 있다.

;; Element -> Integer
;; ListOfElement -> Integer
;; produce the sum of all the data in element (and its subs)
(check-expect (sum-data--element F1) 1)
(check-expect (sum-data--loe empty) 0)
(check-expect (sum-data--element D5) 3)
(check-expect (sum-data--element D4) (+ 1 2))
(check-expect (sum-data--element D6) (+ 1 2 3))

(define (sum-data e)
  (local [(define (fn-for-element e)
            (+ (elt-data e)    
               (fn-for-loe (elt-subs e))))
          (define (fn-for-loe loe)
            (cond [(empty? loe) 0]
                  [else
                   (+ (fn-for-element (first loe))
                        (fn-for-loe (rest loe)))]))]
    (fn-for-element e)))

이럴 경우, 보조 함수의 테스트 작성 같은 설계 단계가 간소화되어 빠르게 설계할 수 있지만, 보조 함수들을 테스트하지 않는다는 단점이 있다.
따라서 주로, 간단한 함수나 이미 테스트된 보조함수를 사용하는 경우에 주로 사용할 수 있다.