리펙토링 1강


Git
Github

CR, LF

Binary, ASCII 구분 안하는 Linux/Unix (?)

Linux/Unix - LF만 해도 CR까지 함
Windows - CR + LF 해줘야

Git

기본 환경 설정

1
2
git config --global user.name "NAME"
git config --global user.email "EXAM@PLE.COM"

Git 시작하기

1
git init

Tracking files

새로 생성된 파일들은 기본적으로 Untracked 상태이다.
버전 관리를 할 파일들은 Track 상태로 만들어줘야 한다.

1
git add "FILENAME"

Commit

1
git commit -m "DESCRIPTION"

Add remote URL

1
2
git remote add "SHORTNAME" "URL"
git remote -v

Push to remote

1
git push "REMOTE" "BRANCH"

Branch

1
2
3
4
5
# See branch list
git branch

# Make a branch
git branch "BRANCHNAME"

현재 작업중인 브랜치는 HEAD가 가리키고 있다.
이 HEAD를 다른 브랜치로 옮기고 싶다면 checkout을 해야함.

1
git checkout "BRANCHNAME"

Pull Request

Github Repository에 push를 하면 Reviewer가 보면서 Pull Request를 하면된다. 이때 Merge를 하게된다.

Refactoring

리팩토링이란?

마틴 파울러가 만든 단어.
코드를 개선하는 방법론에 대한 이야기.
마틴 파울러가 Refactoring이라는 책에서 언급한 내용으로는 외부에선 변화를 알 수 없지만 내부의 코드는 개선된 것을 뜻한다.

리팩토링은 디자인 패턴과 뗄래야 뗄 수 없다!

용어 설명

Bad smell

재사용성이 떨어지거나 가독성이 떨어지는 코드를 지칭.
22가지의 종류로 나눠놨음.

Extract Method

함수는 하나의 일만하도록 하자.

Lazy Class

하는 일이 별로없는 클래스.
지워버리자…

들어가기x

인터페이스의 개념

OCP (Open-Closed Principle)

Tightly-coupling <-> Loosely-Coupling

abstract method in Java, virtual function in C++
둘 다 자식에게 어떤 기능이 구현되지 않으면 객체가 생성되지 않도록 강제하는 문법

C++에서는 인터페이스에 대한 명시적인 문법이 없으므로 관습적으로 클래스 이름 앞에 대문자 I를 붙인다.

두 클래스가 부모-자식의 관계를 가지는 상속이라면 Upcasting이 가능하다는 것을 의미한다.

인터페이스의 내부 구현

1
2
3
4
5
class IPhone
{
public:
virtual void call(const char *) = 0;
};

인터페이스를 구현하기 위해 virtual function을 사용했는데 이 때 접근지시자는 항상 public이여야 한다. 그렇다면 굳이 쓸 필요가…

1
2
3
4
struct IPhone
{
virtual void call(const char *) = 0;
};

그래서 class를 struct로 바꿨다. 왜냐하면 class란 접근지시자가 private인 struct이닌까.
C++에서는 명시적으로 interface 키워드가 존재하지 않는다. 이렇게하면 어떨까?

1
2
3
4
5
6
#define interface struct

interface IPhone
{
virtual void call(const char *) = 0;
};

결국 interface란 class에 불과한 것임을 알 수 있다.

Java는 외부적으로 다중 상속을 지원 안하지만 implement, 즉 인터페이스 구현 부는 여러 개를 구현할 수 있다. 결국 내부적으로는 다중 상속을 지원하지만 설계 측면에서 클래스의 모호함을 제거하기 위해 제한을 둔 것이다.

중복 코드 해결

함수 포인터를 사용하면 중복 코드를 줄일 수 있다.
예를들면 stdlib.h의 qsort()가 있다.
하지만 이 함수의 문제는 함수를 call 한다는 것이다. 즉 이로인해 overhead가 너무 커지는 것.

그래서 inline 키워드를 함수 앞에 붙여주면 컴파일러가 컴파일 후 어셈블리 언어를 함수 호출 명령어에 대체시킨다. 하지만 꼭 그렇지는 않은데…

어셈블리 언어

1.cpp -> 1.i -> 1.asm
소스코드 -> 인터프리터가 처리한 코드 -> 니모닉 언어(어셈블리 코드) -> 0100101…

니모닉 언어 중 하나가 어셈블리 언어이다.

어셈블리 언어로 변환하는 방법

1
cl.exe 파일명.cpp /FAs

s 플래그는 주석으로 소스 코드를 넣어준다.

이렇게 하면 파일명.asm 파일이 나오게 되는데 들여다 보면 inline이 안된 것을 볼 수 있다. 왜냐하면 기본적으로 디버깅 모드로 컴파일하기 때문이다. inline으로 대체시키면 디버깅이 안되므로…

그래서 이 최적화 옵션을 꺼줄 필요가 있다. 그 플래그는 /Ob1이다. (/Ob0 이 기본값)

1
cl.exe 파일명.cpp /FAs /Ob1

그런데 함수 포인터를 사용하면 이렇게 해도 inline이 대체되지 않는다. 그 이유는 이 기법은 컴파일 타임에서 적용되는 것인데 함수 포인터로 함수를 호출시키는 것은 런타임에서 동작하는 것이므로 컴파일러로써는 최적화를 시킬 수 없는 것이다.

정확하게는 함수는 시그니쳐로만 구분되기 때문에, 즉 타입으로 구분되지 않기 때문에 구별할 수가 없는 것이다.

이러한 경우를 보면 inline 키워드는 컴파일러에게 주는 일종의 힌트라고 볼 수 있다. 최적화 할 수 있으면 하고 없으면 말아라. 라는 뜻.

inline을 수행하지 않는 경우

  1. 함수 포인터를 사용하는 경우
  2. 함수의 코드 길이가 너무 길 경우

이러한 제약을 해결하는 방법은 함수 객체를 사용하는 것이다.

함수 객체

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

struct Adder
{
int operator()(int a, int b) { return a + b; }
};

struct Subber
{
int operator()(int a, int b) { return a - b; }
};


int main() {
Adder add;
cout << add(1, 1) << endl;

Subber sub;
cout << sub(1, 1) << endl;
}

호출하는 시그니쳐는 같지만 객체이므로 다른 타입으로 본다. 따라서 inline 키워드로 최적화가 가능하다!

템플릿을 활용한 함수 객체 방식 vs 함수 포인터 방식

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <algorithm>
#include <functional>
using namespace std;

int asc_int(int a, int b) { return a > b; }
int dss_int(int a, int b) { return a < b; }

int main()
{
int arr[10] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 10 };

// 장점 : 성능상의 오버헤드가 없다
// 단점 : 목적파일의 크기가 커질수 있다. 메모리 이슈 (템플릿 함수는 사용한 타입의 수만큼 기계어 코드를 만들기 때문)
greater<int> g;
less<int> l;
sort(arr, arr + 10, g);
sort(arr, arr + 10, l);

// 장점 : 메모리 최적화
// 단점 : 성능이 떨어짐
sort(arr, arr + 10, asc_int);
sort(arr, arr + 10, des_int);

return 0;
}

Question

실제로 functional 헤더에 있는 greater 객체를 봐보니 inline 키워드가 없는데요??

Answer

클래스 내부에서 function을 정의하면 암시적 inline이 진행된다.
이를 이해하기 위해선 extern linkage와 internal linkage를 알아야 할 필요가 있다.
C++ 창시자의 책을 참조하세요.

Thin Template Pattern

Thin Template in More C++ Idioms

코드 블로우트 현상을 제거하는 디자인 기법

자유로운 타입을 사용하기 위해선 C에선 void를 쓴다. 대신 형 안정성이 떨어짐. 프로세스가 뻗을 수 있다. 그래서 C++에선 템플릿을 쓴다.

Java에선 할 수 없는 기법. template가 없기 때문.

C++에 specific한 기법이다.

[참고] 라이브러리와 프레임워크의 차이

라이브러리는 사용 흐름의 주도권이 사용자에게 있지만 프레임워크는 주도권이 프레임워크에게 있다. 왜냐하면 그 틀에 맞춰서 코딩을 해야하기 때문이다. 예를들면 안드로이드 코딩을 할 때 Activity를 만들때 항상 Activity 상속을 해서 구현해야 하는 점을 들 수 있다.

상속을 사용하는 이유?

일반적인 관념

  1. 재사용성
  2. 유지 보수

본질

서로 다른 두개의 타입을 하나의 타입으로 사용하는게 바로 상속의 본질이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;

// 추상화를 해보자.
class Unit {};

class Marine : public Unit
{

};

class Ghost : public Unit
{

};

int main()
{
Marine m;
Ghost g;

// void* grp[2] = { &m, &g }; // 다분히 C언어의 관점이다.
Unit* grp[2] = { &m, &g }; // upcasting이 되면서 하나의 그룹으로 묶을 수 있다.

// 서로 다른 두개의 타입을 하나의 타입으로 사용하는게 바로 상속의 본질이다.
// <-> 다형성을 가진 것을 하나의 타입으로 묶을 수 있다면 상속을 사용하면 된다.
}

Java에서는 void 포인터가 존재하지 않는다. 그렇기 때문에 모든 객체들은 Object를 상속할 수 밖에 없는 것이다.