Programming 기본 개념 - Sizuha/devdog GitHub Wiki

코딩의 기본

굳이 적을 필요가 있을까 싶을 정도로 당연한 것들.

남들이 알아볼 수 있게

들여쓰기는 필수

  • 최소한 제어 블록의 시작과 끝을 눈으로 봐서 알 수 있어야 한다.
    • 간단히 말해, if/for 등의 범위가 어디까지인지, 함수의 범위가 어디까지 인지 등.
  • 들여쓰기는 탭(Tab)으로 하는 걸 권장. (들여쓰기 할 때, 스페이스랑 탭을 섞어쓰지 말 것!)

빈 줄

이것 역시 코드의 해석을 돕기 위한 시각적인 장치로서, 글쓰기에서 문단을 나누는 것과 비슷한 개념이다.

  • 연관성이 높은 코드들 끼리는 가능한 가깝게 붙어 있게 한다.
  • 연관성이 낮은 코드들 과는 거리를 둔다.

이름짓기

  • 이름 그 자체가 해당 코드에 대한 기본적인 정보(사양)를 제공하고 있어야 한다.
    • 이름이 잘 못 지어졌다면, 사양에 대한 이해가 안되어 있다고 보면 된다.
  • 가능한 (최대한) 영어로 쓸 것. (사전을 찾아보는 한이 있더라도)

주석의 활용

  • 주석으로 인터페이스에 대한 설명을 달아둔다.
    • 간단히 생각하면, public 으로 선언한 것들에 대해서는 설명을 달아두자.
    • JavaDoc 등으로 주석을 바탕으로 자동으로 문서화 해주는 툴을 쓰면 좋다.
  • 주석은 많이 있다고 좋은 게 아니라, 꼭 필요한 설명이 있어야 한다.
    • 코드만 보면 바로 알 수 있는 뻔한 내용은 적지말자.
    • 왜 이런 코드를 작성했는지가 중요하다.

왜 이런걸 하냐면

  • 남들이 내 코드를 읽을 수 있어야, 남의 도움도 받을 수 있다.
  • 코드가 정리가 안되어 있으면 나중에 본인 스스로도 해석을 못한다.
  • 내 손을 떠나고 나서, 다름 사람이 그 코드를 보고 수정해야 할 수도 있다.

변수 선언

변수 선언은, 그 변수가 사용되어지는 시점에서 선언하는 것을 원칙으로

지역변수/임시 변수에 한정된 얘기.

C언어 처럼, 구세대의 언어들은 변수 선언을 맨 처음에 하는 것이 보통이었지만, 현대 언어는 대부분 코드 중간에 변수 선언을 넣을 수 있다.

왜 이렇게 하는가? - 변수 선언은 곧 메모리를 할당받는 것. 실제로 쓰이기 직전까지 메모리 할당을 미뤄두는, 절약의 의미. - 실제로 쓰이는 시작하는 시점에서, 해당 변수에 대해 참조하고자 할 때, 선언 내용이 가까운 위치에 있는 것이 도움이 된다. - 변수 선언을 보게 되면, 이제 곧 사용되는구나 하고 짐작할 수 있게 된다.

기본적으로 연관성있는 코드들은 한 구역에 몰려 있는 것이 코드를 파악하는 데 도움이 된다.

if문

기본적으로 if 조건식 else 나머지 상황으로 생각할 것.

예를들면, 이런 경우는...

if (name != null && age != null && address != null)  {
	// ...
}
else if (name == null || age == null || address == null) {
	// ...
}

첫번째 if조건이 부정된 상황(else)이 곧 3개 필드 중 하나라도 null인 경우이기 때문에, 두번째 if 비교는 의미가 없다.

if (name != null && age != null && address != null)  {
	// ...
}
else {
	// ...
}

드 모르간 법칙

!(a || b) == !a && !b
!(a && b) == !a || !b

활용예

if (name != null && age != null && address != null)  { .. }

// 이것은 곧 이런 의미 이므로
if (!(name == null) && !(age == null) && !(address == null))  { .. }

// 이렇게 고칠 수 있다
if (!(name == null || age == null || address == null)) { ... }

분배 법칙

x && (a || b || c) == x && a || x && b || x && c

Early Return

if ~ else 구조 보다는 조건을 체크해서 일찍 return 시키는 것이 가동성이 좋다. 그래야 뒤의 코드를 읽을 때 앞의 분기상황을 고려하지 않아도 되기 때문.

if (obj != null) {
    // ...
}
else {
    // ...
}

//--- 이렇게 하는게 좋다 ---
if (obj == null) {
    // ...
    return;
}

// ...

코드에 대한 힌트 및 실수방지 장치

변경 불가 선언

Java의 경우 final, kotlin의 val, swift의 let

final int BASIC_SCORE = 100;
// 이렇게 상수를 정의할 때도 쓸 수 있지만

final List<String> result = something();
/*
 이렇게 결과를 받아오기만 하는 경우라든지, 
 이후 변수에 대한 변경(값 타입의 경우 값의 변경, 레퍼런스 타입의 경우 참조하는 대상이 바뀌는 경우)이 일어나지 않으면
 일단 final로 선언하고 본다. 
*/

레퍼런스 변수의 경우, 대부분 final을 적용해도 된다. (그냥 습과적으로 final 선언을 써라!)

이렇게 적극적으로 final 키워드를 활용하면,

  • 실수에 대한 방지: 변경하면 안되는 걸 변경하는 경우 컴파일 단계에서 에러가 된다.
  • 코드에 대한 해석을 도와줌: 값/레퍼런스가 변경되지 않는다는 것만 확정되어도 코드 해석에 도움이 된다.
    • final 선언이 안된 변수를 보게되면, 뭔가 변경이 일어난다는 뜻으로 일관되게 해석될 수 있다.

예외상황을 먼저 확인해서 걸러내기

앞서 설명한 Early Return과 중복되는 설명인데,

if (id != null && id.length >= 8 && password != null && password.length >= 12) {
	// ...
}
else {
	// ...예외상황...
}

이렇게 하지 말고, 다음과 같이 한다.

if (id == null || id.length < 8) {
	// ...
	return;
}
if (password == null || password.length < 12) {
	// ...
	return;
}
// ...

예외 조건을 먼저 확인하고, 가능하면 예외 상황이 확인된 시점에서 에러를 발생시키고 진행을 멈추도록 한다.

모든 예외상황을 통과했으면 정상상황으로 간주하고 진행하면 된다.

객체지향 개념

interface

객체 A와 B가 같은 타입인가를 판단하는 데에는 두 가지 기준이 있다.

첫번째는 전통적인 기준으로, 데이터 타입(型)을 기준으로 판단하는 방식이 있다. class도 일종의 데이터 타입으로 취급할 수 있다. 다만 클래스의 경우는 상속이라는 특성 때문에 타입 캐스팅때 고려해야 경우의 수가 좀 더 늘어났다. 간단히 말해, 족보를 따진다고 보면 된다.

두번째는 행위(method)를 기준으로 판단하는 방식이 있다. A와 B에 공통이 되는 매서드가 존재한다면, 그 매서드를 실행할 수 있다는 점에 한에서는 같은 녀석이라고 판단해도 된다는 것이다.

class A {
    int productNo;
    public String printSelf() { ... }
}

class B {
   String text;
   public String printSelf() { ... }
}

위 경우, class A와 B는 완전히 다른 녀석이다. 다만 한가지 공통된 점은 완전히 똑같은 형식의 printSelf() 매서드(물론 매서드의 내용은 다르겠지만)를 가지고 있다는 것.

그렇다면 이 printSelf() 매서드를 기준으로 보면 A와 B는 같은 녀석이라고 판단해도 좋다. 하지만 이 중대한 사실을 컴파일러에게도 알려줘야 한다. 그래서 등장하는 것이 interface다.

interface Printable {
    String printSelf();
}

class A implements Printable {
    int productNo;
    public String printSelf() { ... }
}

class B implements Printable {
   String text;
   public String printSelf() { ... }
}

// 이제 데이터 타입과 상관없이, Printable라는 인터페이스 타입으로 A와 B의 인스턴스를 받을 수 있다.
Printable p;
p = new A();
p = new B();

에러에 대한 대처

에러가 발생하면 당황하지 말고 에러 메세지를 해석한다.

다음 사항을 파악한다.

  • 에러가 발생한 위치
  • 에러의 내용: 원인
  • 그리고 자신이 최근에 한 작업

에러는 아닌데 이상 동작인 경우 (버그)

  • 재현되는 조건을 파악
    • 입력값, 행위, 데이터의 상태 등
  • 해당 화면(기능)을 구현한 코드를 찾아간다
  • 재현 조건 대로 코드의 흐름을 따라가면서 분석한다
    • 가능하면 개발 툴의 디버깅 기능을 활용하라 본인이 작성한 코드를 절대로 신뢰하지 말고, 최대한으로 의심을 해서 원인을 분석할 것.

에러에 쉽게 대처하려면

  • 본인이 작성한 코드를 글자 하나 단위까지 확실히 이해하고 있을 것
    • 에러 메세지를 접했을 때, 어디에서 에러가 발생했는지 바로 확인 할 수 있어야 한다.
  • 한번에 많은 기능을 작성하지 말고, 하나씩 만들면서 수시로 테스트를 돌릴 것
    • 코드의 변화가 적은 상태에서 테스트를 돌려야 확인해야 될 범위가 줄어든다.
    • 버전 관리 도구(例: git)를 활용해서 잘 동작하는 가장 최신의 소스 상태를 유지시켜 둘 것.
  • OS, 프로그래밍 언어, 개발 도구, 기타 플랫폼 및 라이브러리 등에 대해서 자세히 파악해 둘 수록 에러 내용을 파악하기가 수월하다.
  • 에러는 분명한 인과관계에서 발생한다.
    • 에러가 발생한 것에는 반드시 원인이 있다. 우선은 그 원인을 찾는 것에서 출발한다.

대처 방법

검색하기 전에, 일단 에러 메세지를 읽어라!

다음 사항을 반드시 파악하고 나서 진행할 것

  • 에러가 발생한 시점 및 위치
  • 에러의 내용 단, 에러 메세지 자체가 매우 단순하게 나오는 경우도 있다. 특히 링크 에러의 경우. 그럴 경우에는 에러 내용을 추측해서 대처할 수 밖에 없다.

재현

  • 어떻게 했을 때(어떤 조건에서) 문제상황이 발생하는지 파악한다.
  • 가능하면 문제가 발생되지 않는 조건도 확인한다.

비교/추론

  • 최근에 잘 동작했던 케이스랑, 문제가 된 케이스를 비교해서, 변경점/차이점이 무엇인지 파악한다.
  • 차이점을 없애 보고 다시 시도해 봤을 때, 문제가 사라졌다면, 무엇 때문에 변경된 내용에서 문제가 발생했는지 추측해 본다.

S/W에서 변경은 왜 발생하는가?

개발 언어/툴의 발전은 기본적으로 유지보수를 잘 하기 위함이다. 즉, S/W에 대한 변경이 일어날 상활에 대비를 하는 것이다. 그럼 변경은 왜 일어나는가?

  1. 버그 발견
    1. 예상하지 못한 문제
    2. 개발자의 실수
    3. 사양의 실수
  2. 사양 변경
    1. 새로운 기능이 필요해 졌다
    2. 업무 내용이 바뀌었다
    3. 애초에 사양서가 잘못 만들어졌다
    4. 개발자가 사양을 잘 못 이해했다 혹은 고객으로부터 애매한 부분을 확인 받지 못했다
    5. 고객의 변덕
    6. 고객의 클레임
    7. 사용성 문제
    8. 등등...
  3. 환경의 변화
    1. OS가 버전업 됐다
    2. H/W 변경
      1. 부품 교체, 서버 변경, 클라이언트 기기 변경 등.
    3. 개발환경의 변화
      1. 개발 툴, 라이브러리 등의 업데이트
      2. 개발 환경에 대한 지원이 끊기는 경우
    4. 그냥 시간이 지나다 보니...
      1. 바보 같지만, 실제로 일어난다. 가령 '윤초' 문제랄지, 일본의 경우 연호가 바뀌는 경우라든지, 예상했던 것보다 오랫동안 운영이 되다보면 자연 발생하는 문제들이 생길 수 있다.
  4. 보안 문제
    1. 보안 이슈가 발견 되는 경우

하여튼 졸라 많다. 그러니까 변경에 대비하지 않는 코딩은 생각조차 하지 말자.

보수를 받는 개발 업무로서의 요구사항

  • 자기가 만들고 싶은 걸 만드는 게 아니라, 고객의 요구사항을 구현하는 것이 업무
    • 고객이 누구인지 파악할 것
    • 요구사항이 무엇인지 파악할 것
  • 관리(유지보수) 가능한 코드를 작성하는 것
    • 쓰고 버리는 1회성 코드를 작성하는 것은 프로가 해서는 안된다.
    • 한발 더 나아가, 이제는 테스트 가능하게도 작성해야 한다.
  • 예외 상황에 대한 대처를 할 것
    • 어느 정도 수준/범위에서 대처할 것인가는 사양에서 결정

개발방법에 관한

설계 우선

코드 타이핑 보다도 종이와 펜이 먼저. 간단하게라도 설계부터 하고 코딩.

최대한 문서 단계에서 검토하고 걸러내야 한다. 그리고 기록을 남겨놓아야 나중에 사양문제로 싸울 때 도움이 된다.

도구 우선

도구부터 먼저 만든다.

도구라 함은, 공유 라이브러리가 될 수 도 있고, 해당 프로젝트 내에서 사용할 유틸리티 클래스 및 함수들, 개발 과정을 보조하기 위한 스크립트(파이썬 혹은 쉘 스크립트 등) 등등이 있을 수 있다.

기본 방침은 일단 연장(도구)들을 준비(이미 만들어진 것을 활용하거나, 직접 만들거나)해 놓은 상태에서, 그것들을 조합해 가면서 프로그램을 완성해 나간다는 이미지로 보면 된다.

이러한 도구들은 프로젝트 진행중에도 필요할 때 마다 수시로 만들어 둔다. 반복/중복되는 코드가 있으면 일단 도구화 시킨다고 보면 된다.

틀(frame)을 만들고나서 속을 채워 넣는다

프로그램을 개발하는 과정 자체를 소규모의 프레임워크를 개발한다는 느낌으로.

일단 껍질(인터페이스: 참고로 UI를 말하는 것이 아님!)을 만들어서 전체에 대한 윤곽과 흐름을 만들어낸 다음에, 나중에 세부사항을 제어할 수 있게 군데군데 빈 자리를 만들어 놓는다. 그리고 그 부분에 속을 채운다.

Programming by Rule

if 조건문만 가지고 떡칠을 해도 프로그램은 돌아간다. 하지만 제대로된 프로그램은 보드 게임과 마찬가지다. 명확한 규칙을 가지고 구현해야 한다.

간단하게는 명명 규칙이 있다. 프로젝트명, 폴더명, 클래스/매서드/그외 각종 변수 이름에 대한 규칙들을 정할 수 있다.

또한 코딩 스타일도 정할 수 있다. 하지만 이건 가능하면 업계에 많이 쓰는 방식을 따르자.

다음으로 설계상의 규칙들이 있다. 프레임워크 레벨로 가면, 프레임워크가 정해놓은 나름의 규칙들이 있다. 이건 비단 프레임워크 뿐만 아니라 하나의 프로젝트 안에서도 규칙을 정해 놓을 수 있다. 예를들면, 데이터에 대한 접근은 항상 특정 매니져 클래스를 통해야 한다 든지. 디자인 패턴도 일종의 설계상 규칙으로 볼 수 있다.

또한 사용자가 체감하는, 인터페이스 관점에서의 규칙들이 있을 수 있다. 경고 메세지는 붉은색으로 표시한다든지, 데이터에 대한 변경이 일어나면 유저에게 일일이 확인을 받지 않는 대신 되돌리기 기능을 제공한다 든지, 화면의 어느 구석에는 항상 메뉴가 배치되어 있어야 한다 든지.

그리고 제품의 사용법(매뉴얼) 자체도 규칙이다. 사용법이 복잡하면 대게는 코드도 복잡해진다. 쉽고 간단한 규칙으로 제품을 사용할 수 있다면 그만큼 개발에서도 수고를 덜 수 있다. 물론 사용법만 간단하다고 OK가, 아니라 실제로 구현에 필요한 구체적인 방법까지 정의된 경우에 한정 된다.

Good Rule

좋은 규칙이란,

  1. 일관성: 되도록 대부분의 경우에 일관되게 적용될 수 있어야 한다. 예외 조건이 많으면 안된다.
  2. 단순성: 복잡하지 않을 것. 하나의 규칙에는 하나의 내용만 담는다. 복잡해진 규칙은 다시 세부적으로 나눠본다.
  3. 통일성: 규칙들이 하나의 통일된 체계 안에서 이뤄져야 한다.

지향점

사양 정리가 최우선: 코드 보다 문서를 수정하는 것이 쉽다

  • 코드 보다도 문서를 먼저 작성하라.
  • 문서 단계에서 문제점을 파악하고 수정하라.
  • 추후에 변경의 여지가 있는 부분과, 거의 변경이 없을 부분을 파악하라
    • 유지보수가 발생할 부분을 파악하고, 그 부분에 대해서 유연한 설계를 적용해 볼 수 있다.
    • 변경 가능성은 적지만, 만약 변경이 일어날 경우 어느 정도의 비용(돈, 시간, 인력, 리스크 등)이 소요될지도 생각해 본다.
    • 사양 변경으로 해결 할 수 있는 문제는 이 단계에서 결정 지어야 한다.

성능 보다는 유지 보수(관리)가 쉬운 방향을 우선한다

소프트웨어의 비용은 유지보수에서 더 많이 발생한다.

한번 만들고 끝나는 경우는 거의 없다. 따라서 변화에 대응가능한 수준에 따라 설계 수명이 결정된다.

다만, 경우에 따라서는 성능이 중요한 이슈가 될 수도 있으니, 정말로 유연한 설계가 필요한 부분인가는 신중히 생각해야 할 것.

소프트웨어의 설계 수명 = 얼마나 변경사항에 버틸 수 있는가

유지보수 관점에서 S/W를 설계해야 하는 이유가 바로 이거.

  • 변경이 발생할 가능성이 있는 것은 무엇인가?
  • 그것은 얼마나 자주 발생할까?
  • 잘못적용될 경우 얼마나 치명적인 문제인가?
  • 변경하고자 할 때, 얼마나 많은 부분을 고쳐야 되는가?
  • 차후에 변경하는 과정에서 작업자가 실수할 가능성은 없는가? 중요한건 변경사항에 대한 이슈를 얼마만큼 커버할 것인가다. 100% 완벽한 대비 보단 현실적인 고려(예산과 일정)해서 가능한 덜 치명적이 될 수 있는 수준에서 결정짓는게 좋다.

테스트(검증) 가능한 코드를 지향한다

이제는 테스트까지도 고려해야 한다.

최대한 자동화된 테스트가 돌아 갈 수 있게 코드를 작성해야 한다. 이것은 나중에 문제의 원인을 찾거나, 변경사항이 발생해서 코드를 수정해야 할 때 도움이 된다.

코딩 단계에서 적용할 수 있는 원칙

이것은 어디까지나 원칙이다. 법칙이 아니다. 상황에 맞게 적용할 것.

주로 객체지향적인 관점에서 설명한다.

상속(inheritance) 보다는 구현(implementation)

가능한 인터페이스(interface) 중심으로 설계하라

if 보다는 다형성

if/switch는 다형성으로 대체할 수 있다. 다만 앞으로의 변경 가능성, 즉 유지보수가 발생할 가능성이 높은 경우에 적용한다.

최대한 중복을 제거하라

  • 비슷한 코드를 2번 이상 사용하는 것은 일단 중복으로 본다.
  • 3번 이상 반복되면 문제가 있다는 신호이다. 코드 상에서 반복되는 과정 그 자체를 되도록이면 프로그래밍으로 처리하라. 즉, 노가다를 하지 마라.

최대한 분할하라

  • 한 매서드의 길이가 한 화면을 넘어가지 않게 한다
    • 이것은 대략적인 기준. 요는 매서드 길이가 길다는 것은 그 안에서 여러가지 역할을 맡고 있을 가능성이 있다는 것. 기본적으로 하나의 매서드는 하나의 역할만 수행하도록 한다. 가능한 작은 단위의 매서드로 분할하고, 조합해서 사용한다. '성능 보다는 유지 보수가 쉬운 방향을 우선한다'는 원칙과 연관된다.

마찬가지로 한 클래스가 너무 많은 역할을 담당하지 않도록 한다.

인터페이스부터 작성한다

알맹이는 비어있어도 상관없으니 껍질(인터페이스)부터 작성한다.

인터페이스가 구현되어 있으면, 일단 외부와의 상호작용을 정의할 수 있고, 다른 사람이 내가 만들 모듈이 일단 존재한다고 가정하고 작업을 시작할 수 있다.

인터페이스는 더미 데이터를 써서라도 입/출력이 일단 에러 없이 동작하는 상태여야 한다.

단순하게 시작해서 구체화 한다

위의 인터페이스부터 작성한다와도 연관되어 있다.

최대한 단순한 상태에서부터 구현을 시작해서, 동작을 확인(Unit Test)하고, 이상이 없으면 다음 단계의 기능을 하나씩 구체화해 나간다. (알맹이를 채운다)

의존성 제거

함수(매서드)나 클래스가 가능한 외부 정보에 대한 참조를 최소화하여, 독립적으로 동작할 수 있게 한다. 이것은 변경이 일어날 경우 연쇄적으로 수정이 벌어지는(산탄총 수술) 것을 방지하기도 하지만, 테스트 가능한 코드를 만들기에도 좋다.

대체로 인터페이스가 단순한 녀석이 의존성이 적다.

각 객체들간의 정보교환(인터페이스) 부분만 정형화하고 내부는 블랙박스화 한다. 더미 데이터를 인터페이스에 전달하여 테스트가 가능할 수 있게 한다.

⚠️ **GitHub.com Fallback** ⚠️