들어가며

특정 라이브러리, 최신 프로그래밍 언어, 인프라 기술 등은 '기술'로 분류되고,

이런 기술을 학습하는 것은 알고나면 당연한게 많기 때문에 '개발'을 공부하는 게 좋다: OOP, 테스트, 아키텍쳐 등

책 초반부터 찔리는 내용들이 상당히 많음..ㅋㅋ

 

<OOP>

1. 객체지향의 핵심 가치인 역할, 책임, 협력이 무엇인가?

-> 역할: 역할은 객체가 시스템 내에서 수행하는 기능이나 작업을 의미한다. ex. 은행 시스템에서 '계좌' 객체는 잔액 관리, 입금, 출금 등의 역할을 수행할 수 있다.

-> 책임: 책임은 객체가 수행해야 하는 행위와 상태를 관리하는 의무를 말한다. 또한 객체가 알아야 하는 정보와 수행할 수 있는 행동을 포함한다. ex. '계좌' 객체에서 계좌 잔액을 정확히 유지하고, 거래 내역을 기록하며, 잔액 부족 시 출금을 거부하는 등의 책임을 가질 수 있다.

-> 협력: 협력은 객체들이 상호작용하며 시스템의 기능을 수행하는 방식을 의미한다. ex. 은행 시스템에서 '고객' 객체가 '계좌' 객체에 출금 요청을 보낼 수 있다.

 

2. 진행하는 프로젝트를 설명해보고, 객체지향인지 아닌지 어떻게 판단할 수 있는가?

-> 객체지향적인지 판단하기 위해서는 다음과 같은 특징들을 살펴볼 수 있다: (온라인 쇼핑몰 프로젝트에서)

  • 추상화: 복잡한 시스템을 간단하고 이해하기 쉬운 객체들로 모델링했는지 확인 -> ex. '상품', '고객', '주문', '장바구니' 등의 실제 개념을 추상화하여 클래스로 모델링했는지
  • 캡슐화: 객체의 내부 상태와 구현 세부사항을 외부로부터 숨기고, 필요한 인터페이스만 노출하는지 확인 -> ex. 각 클래스가 자신의 데이터를 캡슐화하고 필요한 메서드만 외부에 노출하는지
  • 상속: 객체 간의 계층 구조를 통해 코드 재사용과 확장성을 제공하는지 확인 -> ex. '일반상품'과 '할인상품' 과 같은 클래스들이 '상품' 클래스를 상속받아 구현되었는지
  • 다형성: 동일한 인터페이스를 통해 다양한 객체들이 자신만의 방식으로 동작할 수 있는지 확인 -> 결제' 인터페이스를 통해 '신용카드결제', '계좌이체', 'PayPal결제' 등 다양한 결제 방식을 다형적으로 처리하는지
  • 객체 간 통신: 객체들이 메시지를 주고받으며 상호작용하는지 확인
  • 단일 책임 원칙: 각 객체가 명확하고 단일한 책임을 가지고 있는지 확인
  • 느슨한 결합: 객체들 간의 의존성이 최소화되어 있는지 확인

 

3. 객체지향이 무엇인가? 클래스를 이용해서 프로그래밍하는 방법 외에(ㅋㅋ) 어떤 설명을 할 수 있는가?

-> 2번

 

4. 객체와 클래스의 차이점은 무엇인가? 붕어빵과 붕어빵 틀의 관계(ㅋㅋ;;)라고 설명하지 말고.

-> 객체는 클래스(설계도)를 바탕으로 실제로 만들어진 인스턴스이다. 그래서 클래스는 메모리에 직접 할당되지 않고 객체를 생성하기 위한 '틀'로써 객체의 속성(데이터)과 행위(메서드)를 정의하며 컴파일 시 사용된다. 객체는 클래스의 추상적 정의를 구체화하여 실제 데이터와 상태를 가지며, 런타임에 생성되고 실제로 메모리에 할당되어 프로그램에서 사용된다.

 

5. 메서드는 왜 메서드라고 부르는가? 함수와는 어떤 차이가 있는가?

-> 함수는 독립적으로 존재하며, 어떤 특정 객체에 속하지 않는다. 반면 메서드는 객체에 종속되어 있어 객체를 통해 호출되며, 객체의 상태에 접근할 수 있다. 또한 오버라이딩과 오버로딩을 통해 다형성을 지원할 수 있다.

 

<테스트>

1. 테스트를 학습한다는 것은 무슨 의미인가? Mockito, JUnit, H2 학습 외에 어떤게 있는가?

-> 책 초반에도 말했듯이, 단순히 테스트 도구나 프레임워크 외를 물어본 것 같다.

a) 성능 테스트:

  • 부하 테스트: 시스템이 예상되는 최대 부하 상황에서도 정상적으로 작동하는지 확인한다. ex. 웹 서버가 동시에 N명의 사용자 요청을 처리할 수 있는지
  • 스트레스 테스트: 시스템의 한계를 넘어서는 극단적인 조건에서 시스템의 동작을 확인한다. ex. 데이터베이스 연결이 끊어졌을 때 애플리케이션이 어떻게 대응하는지
  • JMeter, Gatling: 대량의 가상 사용자를 생성하여 시스템에 부하를 가하고, 응답 시간, 처리량 등을 측정한다.

b) 보안 테스트:

  • OWASP 가이드라인: 웹 애플리케이션의 일반적인 보안 취약점(예: SQL 인젝션, 크로스 사이트 스크립팅)을 확인하고 방어
  • 침투 테스트: 실제 해커의 공격을 시뮬레이션하여 시스템의 보안 취약점을 찾아내는 테스트
  • 퍼지 테스트: 보안 쪽에 포함되는지는 잘 모르겠지만, 요번에 진행하는 오픈소스 프로젝트에서 하고 있더라. 용어만 들어봤었는데,  자동화된 도구를 사용해 프로그램 또는 프로그램 기능에 유효하지 않거나 예상치 못한 입력을 제공하고 나서, 결과를 확인해서 프로그램이 충돌하거나 부적절하게 작동하는지 확인하는 테스트라고 함. 힙 버퍼 오버플로우나 use-after-free 와 같은 c/c++ 및 임베디드 코드에서 메모리 손상을 찾는 데 특히 효과적이라고 한다.

c) 테스트 자동화:

  • CI/CD 파이프라인에 테스트 통합: 코드 변경이 있을 때마다 자동으로 테스트를 실행하여 즉시 피드백을 제공 -> Jenkins, GitLab CI와 같은 도구들로 코드 커밋, 빌드, 테스트, 배포 등의 과정을 자동화할 수 있다.

d) 테스트 데이터 관리:

  • 테스트 데이터 생성 전략: 테스트에 필요한 다양한 시나리오의 데이터를 효과적으로 생성하고 관리하는 방법
  • 데이터베이스 시딩: 테스트 실행 전에 데이터베이스에 미리 정의된 데이터를 삽입하여 일관된 테스트 환경을 제공

e) 테스트 더블 기법:

  • Stub: 미리 정의된 응답을 반환하는 간단한 대체 객체
  • Spy: 실제 객체의 일부 메서드만 오버라이드하여 호출을 기록하거나 수정
  • Mock: 예상되는 호출과 응답을 미리 프로그래밍한 객체로, 실제 호출이 예상과 일치하는지 검증
  • Fake: 실제 구현을 단순화한 가벼운 버전의 객체(예: 인메모리 데이터베이스)

f) 계약 테스트:

  • 서비스 제공자와 소비자 사이의 인터페이스 계약을 정의하고 검증한다.
  • Consumer-Driven Contracts: 소비자가 기대하는 서비스의 동작을 계약으로 정의하고, 제공자가 이를 만족하는지 테스트 -> Spring Cloud Contract와 같은 마이크로서비스 간의 계약을 정의하고 검증하는데 사용되는 도구가 있다.

 

2. 단위 테스트, 통합 테스트, end to end 테스트의 차이점이 무엇인가? 또한 단위 테스트에서 말하는 단위는 무엇이고, 통합 테스트에서 말하는 통합은 무엇인가?

-> 단위 테스트: 개별 코드 단위(주로 메서드 수준)의 정확성을 검증한다. 외부 의존성을 모킹 또는 스터빙 해주어야 한다.

-> 통합 테스트: 여러 단위가 함께 올바르게 작동하는지 검증한다. 특히 여러 컴포넌트 또는 모듈 간의 상호작용을 검증하는 데 사용하기 때문에, 실제 의존성을 사용하며 단위 테스트보다 느리고 복잡하다.

-> End-to-End 테스트: 전체 시스템의 흐름과 기능성을 검증한다. 사용자 관점에서의 전체 애플리케이션을 테스트하며, 실제 환경과 유사한 조건에서 수행하기 때문에 가장 복잡하고 시간이 오래 걸린다.

 

  • 단위 테스트에서의 '단위': 주로 메서드나 함수 수준을 의미하는 것 같고, 이 단위가 독립적으로 테스트 가능해야 하며, 외부 의존성은 모킹하거나 스터빙해야 한다.
  • 통합 테스트에서의 '통합': 여러 컴포넌트나 모듈이 함께 작동하는 것을 의미하고, 실제 의존성을 사용하여 여러 부분이 올바르게 결합되어 작동하는지 확인해야 한다. ex. 데이터베이스와의 상호작용, 외부 서비스와의 통신, 여러 레이어 간의 상호작용

 

3. Mockito, JUnit, H2 없이 테스트를 작성해야할 때, 스프링 프로젝트에 테스트를 어떻게 넣을까?

-> 모르겠다.. 직접 구현해야하지 않을까..? 

 

<아키텍쳐>

1. 아키텍쳐라고 하면 떠오르는 것이 무엇인가? SOLID? SOLID를 알고 있다면 의존성 역전과 의존성 주입이 어떻게 다른지 설명해라.

-> 의존성 역전: 모듈 간의 결합도를 낮추고 유연성을 높이기 위해 고수준 모듈이 저수준 모듈에 의존하지 않도록 하는 설계 원칙이다. 

// 의존성 역전 적용 전
class EmailService {
    public void sendEmail() { /* 이메일 전송 로직 */ }
}

class NotificationManager {
    private EmailService emailService = new EmailService();
    public void notify() {
        emailService.sendEmail();
    }
}

// 의존성 역전 적용 후
interface MessageService {
    void sendMessage();
}

class EmailService implements MessageService {
    public void sendMessage() { /* 이메일 전송 로직 */ }
}

class NotificationManager {
    private MessageService messageService;
    public NotificationManager(MessageService service) {
        this.messageService = service;
    }
    public void notify() {
        messageService.sendMessage();
    }
}

-> 의존성 주입: 의존성 역전 원칙을 구현하는 방법 중 하나로, 객체의 의존성을 외부에서 제공하는 테크닉이다. 생성자 주입, 세터 주입, 인터페이스 주입 등의 방식이 있다.

class NotificationManager {
    private MessageService messageService;
    
    public NotificationManager(MessageService service) {
        this.messageService = service;
    }
    
    public void notify() {
        messageService.sendMessage();
    }
}

// 사용
MessageService emailService = new EmailService();
NotificationManager manager = new NotificationManager(emailService);

 

 

2. 의존성이 무엇인가?

-> 의존성은 한 모듈이나 클래스가 다른 모듈이나 클래스를 필요로 하는 관계를 말하며, 만약 A가 B에 의존하게 되면 B의 변경이 A에 영향을 줄 수 있다.

 

3. 인터페이스가 하나 있고, 인터페이스의 구현체는 프로젝트에 하나밖에 없다면, 굳이 엔지니어가 구현체와 인터페이스를 분리한 이유가 무엇일까?

-> 일단은 '무엇을 할 것인가'(인터페이스)와 '어떻게 할 것인가'(구현체)를 분리함으로써 코드의 구조를 개선할 수 있고, 이후 다른 구현체가 필요할 때 쉽게 추가할 수 있으며 리팩토링이 쉬워진다.

-> 그 외에도 인터페이스를 사용하면 목(mock) 객체를 쉽게 만들 수 있어 단위 테스트가 용이해진다는 장점이 있다고 함. 

 

 

 

 

 

 

1부. 객체지향

01. 절차지향과 비교하기

  • 순차지향 프로그래밍(sequential oriented programming)과 절차지향 프로그래밍(procedure=함수 oriented programming): 순차지향 프로그래밍은 어셈블리어로 작성된 코드와 같이 함수의 개념이 존재하지 않아 코드를 위->아래로 순차적으로 읽는다. 만약 목적지 주소를 건너뛰어야 하는 경우가 있으면 jmp나 goto와 같이 프로그램의 실행 위치를 옮기는 방식으로 코드가 동작한다. 반면, 절차지향 프로그래밍은 함수를 만들어 프로그램을 만드는 방식으로, 일반적인 c언어로 작성된 코드가 이에 해당된다.
  • 코드 1.6: 책 예시에 있는 코드 1.4, 1.5 방식으로만 작성해왔던 것 같은데, 수익을 RestaurantChain - Store - Order - Food 으로 수익 계산 로직을 객체가 처리하도록 변경되었다. 단순히 food.getPrice()로 계산하지 않고 food 내부에 수익을 리턴하는 함수 calculateRevenue()를 만들고 price를 리턴하도록 함으로써 비즈니스 로직을 객체가 처리하도록 변경했다. 
    • 객체지향으로 코드를 작성하는 이유가 가독성을 높이기 위해서는 아니다. 객체지향은 가독성보단 객체 자체가 맡고 있는 책임에 더 집중한다. 그렇다고 해서 '책임'이라는 개념이 객체지향만의 특징은 아니다. 
  • 코드 1.10: 수익을 계산하는 Calculable 인터페이스를 추가하고, 기존 클래스들이 인터페이스를 구현하도록 변경했다. 즉, 객체 각각에 할당되어 있던 책임을 인터페이스로 분할하게 됐는데, 이 쪽이 더 자연스러워 보이고 이해하기 쉬웠다. C언어는 이러한 추상적인 인터페이스 개념을 지원하지 못하므로 절차지향 언어라고 한다.
    • 객체지향에서는 엄밀히 말해 '책임을 객체를 추상화한 역할에 할당'하는 것을 중요하게 보고, 책임을 어떻게 나누고 어디에 할당하는지 집중한다.
  • 코드 1.12: Food 외에 새로운 수익 아이템이 추가되는 경우, 기존 Calculable 인터페이스로 분리하지 않았다면 계산 로직 자체를 변경해야 했을 것이다. 하지만 Calculable 인터페이스를 사용하게 되면 리스트로 한번에 묶어 코드 변경 없이 기능을 확장할 수 있다.
  • 코드 1.14: canAfford 메서드 파라미터로 Product를 전달하면 더 깔끔하지 않은가 잠깐 생각했는데, Account 클래스에서 금액 비교를 할 때, Product name 등은 의미가 없고 오히려 Product 객체에 의존하게 된다는 단점이 있는 것 같다.

 

02. 객체의 종류

  • Value Object: 코드 2.2에서 Color은 객체이지만 동시에 값이다. 값은 불변성(final, 순수함수)/동등성(equals, hashCode)/자가검증(생성자) 이라는 특징을 가지고 있는데, 이 세가지 특징을 만족할 때 VO라고 부른다. = 신뢰할 수 있는 객체
    • 코드 2.4: 모든 멤버 변수가 final로 선언되어 있더라도 참조타입이라면 불변성이 보장되지 않을 수 있다. 즉, 불변 객체 내부의 참조 객체가 불변이 아니라면 그 객체는 불변이 아니다.
    • 코드 2.6: 또한, VO의 모든 함수는 매번 동일한 값을 반환하는 순수 함수여야 한다.
    • VO를 만드는 이유에 대해 더 집중하는 게 좋다. VO의 불변성으로 객체를 신뢰할 수 있게 만들 수 있다.(멀티 스레드 환경)
    • VO에서 값과 상태가 같으면 같은 객체로 봐야한다. 실제로는 객체를 비교하는 상황이 많지 않아 equals/hashCode를 오버라이딩하는 것을 생략하기도 한다. 동등성도 결국 VO의 신뢰성을 위한 것이므로, 신뢰할 수 있는 객체라면 생략할 수 있다고 본다. (+ 단 VO는 id와 같은 식별자 필드를 멤버 변수로 가지고 있어서 안된다.)
    • 값객체를 만들 때, 롬복의 @Value 어노테이션을 사용할 수 있다. - equals/hashCode 자동생성, 멤버변수 및 클래스 final 선언
    • java16부터 record 키워드를 사용하여 데이터를 담는 간단한 클래스를 만들 수 있다. - equals/hashCode 자동생성, 멤버변수 및 클래스 final 선언/getter 자동 생성
  • Data Transfer Object: 다른 객체의 메서드를 호출하거나 시스템을 호출할 때, 매개변수를 나열하는 것이 불편하니, 데이터를 구조적으로 만들어 전달하기 위한 객체
    • 데이터를 전달하는 데만 집중하며, 그 밖의 역할이나 책임을 가지지 않는다. 데이터 읽기/쓰기 외의 비즈니스 로직이 들어가면 안된다.
    • 롬복의 @Data 어노테이션을 사용할 수 있다. - getter/setter/toString/EqualsAndHashCode와 같은 어노테이션이 한 번에 지정된다.
  • Data Access Object: 데이터베이스 접근과 관련된 역할을 지닌 객체로, 데이터베이스와의 연결/CRUD 연산 수행/보안 취약성을 고려한 쿼리 작성 과 같은 역할을 담당한다. = 스프링에서 Repository 개념 = 도메인 로직과 데이터베이스 연결 로직을 분리하기 위함
  • Entity: 
    • Domain Entity: Domain Model 중 특화된 비즈니스 로직이나 생애주기를 가지는 등, 특별한 기능을 가지는 모델을 말한다. 식별자와 비즈니스 로직을 갖는다.
    • DB Entity: 데이터베이스에서 정보를 담는 객체를 표현하기 위한 수단을 말한다.
    • JPA Entity: 관계형 데이터베이스에 있는 데이터를 객체로 매핑하는 데 사용되는 클래스를 말한다. = 영속성 객체
      • 데이터베이스와의 연동을 추상화하고, 데이터 접근을 담당함으로써 애플리케이션의 비즈니스 로직과 영속성 로직을 분리하기 위함 
      • @Entity 어노테이션을 지정하여 사용한다. 

 

03. 행동

어떤 클래스를 만들 때(ex. 자동차)

클래스에 필요할 것으로 예상되는 속성(=데이터 위주의 사고) vs 클래스가 어떤 행동을 해야하는지(=행동 위주의 사고)

  • 인터페이스와 행동은 다르다. 인터페이스는 외부에서 어떤 객체에게 행동을 시키고자 할 때, 어떤 행동을 지시하는 방법이라고 할 수 있다. 따라서 별도의 지시자가 없으면 public으로 동작한다.
  • 인터페이스로 객체 간의 통신이 이루어질 때, 코드가 실행되기 전까지는 실제로 어떤 메서드가 호출될지 알 수 없다.

 

 

 

<대학원생 때 알았더라면 좋았을 것들>은 전반적인 대학원 생활, 논문을 읽는 방법, 논문 리뷰 발표하는 방법 등에 대해 다루고 있고

<그렇다면 실험실 죽순이가 될 수밖에>는 실험 실패, 대학원 생활 시 느끼는 감정과 다짐, 그리고 극복 방법 등을 위주로 서술되어 있다.

 

사실 이 책들은 어쩌다보니 대학원 입학 후 1년간 방황하고 난 뒤 읽게 되었는데, 마인드 셋에 큰 도움이 됐다.

요즘 회사를 퇴사하고 대학원 가시는 분들도 많아졌고, 대학원에 갈지 말지 고민하는 분들이 주변에 많이 생겼다. 회사 경험, 대학원 경험 모두 삶을 살아가는데 중요하다고 할 수 있어서 대학원을 갈지, 회사를 갈지 둘 중에 하나를 선택하는 양자택일의 관점으로 생각하기보다는 둘 중에 무엇을 먼저 할지 선후의 문제로 바라보면 고민이 간단해지는 것 같다.

 

대학원 생활에서 배울 수 있는 점은 크게 4가지라고 할 수 있다.

  • 논문을 통해 지식을 얻는 습관
  • 문제 정의 및 과학적 방법론으로 문제 해결 경험
  • 시간 관리부터 연구에 대한 실행까지 스스로 규율을 세우고, 스스로 탐색해나가는 경험
  • 실패했다면 왜 실패했는지 분석

 

우리 연구실은 비교적 자유롭기 때문에 시간 관리는 스스로 해야했고, 이로 인해 나태해지기 쉬운 환경이었다. 처음 대학원에 왔을 때는 학부 때와는 달리 동기나 선배가 없어서 적응하기가 조금 힘들었다. 그리고 내 벼락치기 성격으로 인해 결과를 내야 할 때는 며칠간 밤을 새서 수면 사이클이 엉망이 됐었다. 그래도 요즘에는 새로 들어오신 박사 과정 언니랑 연구 얘기도 하고, 플래너를 다시 쓰기 시작하면서 많이 안정된 것 같다.

 

아무튼 대학원에 와서 배운 가장 큰 것은 지식 습득뿐만 아니라, 더 크게 와닿는 점은 자율적인 시간 컨트롤 방법을 깨닫게 된 것이다. 논문 작성을 목표로 한정된 내 시간을 분배하고 관리하면서 목표를 이뤄나가기 위해 일의 우선순위대로 처리하는 과정에 대해 배웠다.

 

그렇지만 목표를 세우더라도 실험이 항상 성공하는 것은 아니었다. 주제를 잡는데 결과가 잘 안나와서 몇 번이나 갈아엎었고, 지금도 사실 연구가 잘 되고 있진 않는 것 같다. 그~래~도 이전에는 뭐가 잘 안되면 바로 구글링해서 문제를 해결하거나 다른 방법을 시도했지만, 지금은 적어도 실험자의 책임감을 가지고 실패했다면 왜 실패했는지에 대한 원인을 분석하게 됐다. 만약 명확한 이유를 모르겠더라도 가장 가능성이 높은 이유에 대해 생각해보고 동기 언니와 의견을 공유할 수 있게 된 것 같다.

 

대학원에 오니까 다시 느끼는거지만, 커뮤니케이션 능력의 중요성을 매번 느끼고 있다. 수업 때, 내가 하고 있는 연구를 다른 사람이 이해하기 쉽게 전달하는 것이 너무 어렵고, 논문 읽은 것을 다른 사람들과 공유하는 것도 너무 어렵다.

 

학기 중에는 매 달 소프트 런치를 진행하면서 다른 사람들의 연구를 듣곤 하는데, 혼자 연구만 하는 것 보다 확실히 생각 환기도 되는 것 같고 좋더라. 자주 만나진 못하지만, 코스웍에서 만난 다른 분야의 분들이나 종종 연구실 외 사람들, 그리고 이미 취업한 친구들에게 말을 걸다 보면 지금의 기술 트렌드와 내 연구 분야에 대해 새로운 의견을 들으면서 생각이 확장되는 것 같다. 

 

 


 

대학원에 처음 들어왔을땐 (지금도 그렇지만) 논문 읽는게 너무너무 어려웠다. 영어인데 집중도 안되고, 10페이지도 넘으면서 배경지식도 없어서 논문 하나를 일주일 걸려 읽었었다. 논문을 읽기 전에 논문을 선택하는것부터도 너무 어려웠다.

 

이 책에도 나오듯이 논문은 google scholar에서 주제와 키워드를 넣어서 검색하도록 하고, 출판연도/논문 인용수/저자 확인을 하는 것이 좋다.

키워드: review(검토), overview(훑어보기), survey(조사), tutorial(쉽게 설명)

 

그리고 논문을 읽을 땐 왜 읽는지 생각하면서 읽는 것이 좋다. 

💡 논문을 읽으며 생각할 것들

1. 이 논문이 왜 중요한가?
논문을 읽을 때 다 이해할 필요는 없으나 해당 논문의 중요한 내용(컨트리뷰션)이 무엇인지, 왜 accept 되었는지 이해하며 읽자

2. 결론이 무엇인가?
결국 이 논문을 어떻게 써먹을 수 있을까? 여기서 얻어갈 것은 무엇이 있을까? 에 대한 대답은 결론에서 나온다.
⇒ 왜 읽었는지, 이 논문에서 알고 넘어가야 할 것은 무엇인지 읽으면서 항상 생각

3. 세미나에서 어떻게 설명할 것인가?
데이터 셋, 풀려고 하는 문제, 목적, 결과 (큰 그림을 보자)
논문에 있는 그림과 글을 매칭시키며 읽고, 그림에 있는 것들의 물리적인 의미를 잘 생각할 것
⇒ 그림의 순서 (일반적으로 본격적인 설계에 앞서 기본 이론을 설명하기 위한 개념도, 기본 설계 구조, 설계 시 발생하는 문제점 소개, 어떻게 해결했는지, 성능 지표와 비교표 등의 순서)

4. 논문 리뷰 시 대략적인 틀
1) ~을 제안한 논문
2) ~을 사용
3) 풀려고 하는 문제는 ~을 목적으로 ~ 데이터 셋을 사용하여 ~ 라는 결과를 얻었다
4) 기존연구에는 ~라는 한계가 존재했다
5) 그래서 이 연구는 ~라는 컨트리뷰션해서 accept 되었다
6) 우리는 이걸 읽고 ~에 필요한 ~를 얻으면 된다

 

마지막으로 논문 읽은 것을 정리해 둘 땐, 만약 이 논문을 인용한다면 어떻게 할지 각 단락을 한 문장으로 요약해보도록 하자.

  • abstract: 나는 이런 문제를 풀거야
    • 무슨 문제를 풀고있고, 어떤 기여를 담았는지 관한 내용
    • ex) 딥 러닝을 웨어러블에 적용할 때, 데이터가 부족한 경우, 이를 증강하는 방법 연구
  • introduction: 이 문제는 이런 동기에서 연구가 시작됐는데,
    • 관련 연구 요약 (1. 풀려는 문제가 무엇인지 2. 왜 이 연구가 중요한지 3. 다른 연구와의 차별점)
    • ex) 파킨슨병 환자의 증상을 생활속에서 진단하려면 딥 러닝이 필요한데, 수집할 수 있는 데이터 수가 적다.
  • related works: 관련해서 이런 접근들이 있었다
    • ex) 데이터가 적은 경우 비전, 음성과 같은 다른 도메인에서는 어떻게 다루었는지 조사 & 파킨슨 병 환자의 모니터링 연구에 대한 조사
  • method: 나는 이런 방식으로 접근해보려 하는데
    • ex) data augmentation 방법 제시
  • experiment: 정말 이게 효과적인지 실험을 해 보니
    • 10% 가량의 성능 향상
  • discussion: 실험 결과는 이렇게 해석할 수 있다.
    • 적절한 data augmentation 방법은 타깃 태스크마다 다를 수 있어 직접 실험, 검증하는 것이 더 좋다는 한계점
  • conclusion: 요약
    • abstract와 비슷하나, 과정, 결과, 의의가 좀 더 자세하다

 

 

리뷰논문은 조금 다르다. 나는 이 블로그 글이 많이 도움 됐다.

리뷰논문은 구성이 어떻게 되는지 파악하고(abstract, introduction 이 중요하다), 주제의 발전 과정과 각 연구의 차별점/한계점을 중점으로 알아둔다. 

 

 


 

랩 미팅을 할 땐 무작정 가지 말고 준비를 해서 가도록 하자.

💡 미팅 체크리스트
1. 주제
2. 연구 목적/배경
    - 논문의 연구목적 연구 배경에 들어갈만한 내용
    - 연구를 해야 하는 이유를 문장으로 작성
3. 연구 문제 및 가설
    - 가설이나 연구 문제는 자질구레한 설명 없이 문장으로 모두 완성해야 피드백을 받을 수 있다.
4. 연구 방법
    - 가설을 이렇게 세웠기 때문에 이런 분석 방법을 통해 검증하고자 합니다

실험은 성공하기도, 실패하기도 한다. 주의할 점은 실험결과가 긍정적일수록 몇번을 더 확인하고, 보수적인 자세로 결과를 해석해야 한다는 것이다. 좋은 결과든 나쁜 결과든 결과 보고를 할때는 신중하게 팩트만 보고하고, 주관적인 해석보다는 데이터를 객관화 시켜 결론을 도출하는 것이 좋다. 그리고 다음에는 이번 실험 결과를 바탕으로 어떻게 해 보겠다는 내용으로 마무리 지어야 한다. 실험 결과가 좋고 나쁘고는 의미가 없으며, 그 결과를 바탕으로 다음 방향을 의미있게 설정하는 것이 중요하다.

 

  • 실패했으면 죄송해하지 말고 원인을 찾아보자
    • 실험을 기획한 사람 입장에서 잘 모르겠거나 애매모호한 부분은 최대한 가능성 있는 대안을 첨부하자
    • 다음에 추가로 꼭 확인해볼 것을 분명히 해서 책임감있는 태도를 전하자
    • 마지막으로 랩미팅 자료에 무엇이 들어있어야 하는지, 교수님과 무엇을 논의해야 하는지 생각하자

 


 

첫 논문을 작성할 때는 아무리 논문 수정을 해도 비슷한 내용 반복에 어떻게 써야하는지 감이 전혀 잡히지 않았었다. 이 책에서도 나오는데, 논문 작성은 일단은 써야 수정과정을 거칠 수 있다.

 

논문은 연구를 다 하고 쓰는 순차적인 과정이 아닌, 연구 <-> 논문쓰기 를 반복 하면서 연구만 할 때 몰랐던 논문조사/추가실험 필요성 보완하는 것이 중요하다. 따라서 일단 목차를 나누고, 각 섹션에 들어갈 내용을 정리한 뒤, 어떤 그래프가 들어가야 할지 일단 생각해둔다.

  • abstract 과 introduction: 서론에서는 넓은 범위에서의 문제 제기부터 시작하여 본인의 연구 영역까지 점진적으로 범위를 좁히며 초점을 맞춘다. 그 과정에서 다른 사람들이 문제를 해결해온 방식도 간략히 소개한다. 그리고 기존 연구의 한계점을 설명한 뒤 본 연구의 필요성을 역설하고 마지막으로 본 연구의 기여를 요약해준다.
    • 초록과 서론을 읽고 난 뒤 독자가 어떤 느낌을 받으면 좋겠는지
      • 이 논문이 다루는 문제는 정말 꼭 해결해야 하는 문제같아
      • 이전의 솔루션들은 아직 많은 한계점을 가지고 있군
      • 여러 시도와 비교해도 이 논문의 기여는 의미가 있겠어
      • 이 논문의 기여 중 핵심은 ~~이군
      • 앞으로 논문의 나머지 부분에선 이런 내용이 나올 것 같아
  • related works: 다른 사람의 연구를 평가할 땐 객관적인 수치와 함께 조심스럽게 그 한계점을 드러낸다.
    • ex) B연구는 어떤 뿐에 어떤 아이디어를 적용하여 A연구를 정확도 ~에서 ~로 개선하였다.
  • methods 와 experiments
  • conclusion
    • abstract 내용을 반복하지 않는다. abstract는 논문을 읽지 않은 사람들이 처음 읽는 글이고, 결론은 논문을 모두 읽은 뒤 전체 내용을 조망하는 글이다.
    • abstract는 문제 제기와 연구의 중요성에 조금 더 큰 방점을 두고 결론은 실험을 통해 얻은 지식과 의의에 대해 조금 더 큰 방점을 두고 풀어낸다.
    • 결론에서 갑자기 새로운 내용을 꺼내는 것은 부적절하다

 


 

논문을 쓰고 나면 컨퍼런스 등에 발표를 하게 되는데,

우리 지도 교수님께서 가장 중요하게 생각하시는 점은 다른 사람들이 이해하기 쉽게 발표를 하는 것이다. 

  • 그래프의 x축과 y축이 무엇인지 짚어주자
  • 그래프를 보고 그 경향성이 의미하는 바가 무엇인지 짚어주자
  • 발표 내용을 이해하는데 중심이 되는 이론을 설명하는데 인색하지 말자
  • 서론에서 자신의 연구 분야의 필요성이 잘 전달되어야 한다. 문제 제기를 명확히 하고, 자신의 연구가 어떤 솔루션을 제시하는지 밝힌다.
  • 매 슬라이드가 넘어갈 때 앞장과 뒷장의 인과관계가 있도록 만들자. 슬라이드가 넘어갈 때 적절한 연결어를 적극적으로 활용하자

 

 

 

 

후기

이제 벌써 석박 통합 5학기 차인데, 그동안 배운점도 많고 아직 부족한 점도 많은 것 같다.

이 책을 읽고 계속 되새겨볼만한 점과 대학원 생활을 하면서 깨달은 점을 정리해봤다. 

이 책은 목차만 보고 읽기로 결정했는데, 책의 전반부는 코드를 잘 읽는 방법, 후반부는 코드를 잘 작성하는 방법을 다루고 있다. 흥미로웠던 점은 기초적인 뇌과학 원리를 설명하면서, 동시에 프로그래밍을 할 때 어떠한 방식으로 행동해야하는지 여러 연구를 통해 보여준다. 요즘 선배들이 연구했던 코드를 볼 일이 생겨서 틈틈이 보고 있는데.. 시간만 가고 잘 이해가 되지 않았는데.. 흑흑 😭 이 책에서 언급하는 방법들을 통해 처음 보는 코드도 잘 이해할 수 있도록 해봐야겠다 

 

 

1. 코드 더 잘 읽기

프로그래밍을 하다보면, 마냥 "이해가 안된다" 또는 "어렵다" 는 단순한 혼란이 아닌, 알고보면 서로 다른 3가지 유형의 혼란이 발생한다. 그리고 각 혼란은 뇌의 서로 다른 부분에서 발생한다.

코드 분석 시 3가지 인지 과정

 

(1) 지식의 부족: 우리는 APL 언어에 대해 알지 못하고, 특히 T 가 의미하는 바를 모르기 때문에 혼란스럽다. = LTM

// APL에서의 이진수 표현
2 2 2 2 2 T n

(2) 정보의 부족: Integer.toBinaryString 이라는 메서드 이름으로 어떤 일을 하는지 유추할 수 있지만, 내부적으로 어떻게 동작하는지에 대한 정보를 얻으려면 메서드 내부 코드를 살펴봐야 한다. = STM

// Java 에서 이진수 표현
public class BinaryCalculator {
	public static void main(Integer n){
    	System.out.println(Integer.toBinaryString(n));
    }
}

(3) 처리능력의 부족: 변수에 임시로 저장되는 값 등을 따라가기 위해 머릿속으로만 처리하기 어려워서 종이에 따로 표시 등을 해둬야 한다. = 작업기억공간

// 베이직에서 이진수 표현
LET N2 = ABS(INT(N))
LET B$ = ""
FOR N1 = N2 TO 0 STEP 0
	LET N2 = INT(N1/2)
    LET B$ = STR$(N1-N2*2) + B$
    LET N1 = N2
NEXT N1
PRINT B$

 

 

그래서 코드를 잘 읽기 위해서는 뇌의 다양한 부분이 사용되고 있다는 점을 인지하고, 뇌의 각 부분에 대한 특징을 이해해야한다.

우리가 코드를 기억할 때는 짧은 기간동안 정보를 저장하는 STM 과 장기적으로 정보를 저장하는 LTM 이 함께 작용하는데, STM 의 용량이 적고 정보를 저장하는 시간이 짧기 때문에 이해하거나 기억한 정보를 처리할 때는 LTM 과 상호작용한다. 이때 기억 공간에는 `청크` 단위로 저장된다. 청크는 배경 지식에 따라 큰 단위가 될 수도, 작은 단위가 될 수도 있다. 숙련된 프로그래머들은 LTM 에 저장된 익숙한 키워드, 구조, 도메인 개념 등을 활용하여 코드를 이해한다. 실험 결과, 특히 디자인 패턴에 대한 지식을 가지면 청킹 능력이 향상되고, 코드를 빠르게 이해 및 수정 할 수 있었다. 주석도 도움이 될 수 있다.

 

이 책에서는 쉽게 정보를 찾을 수 있는 환경에서는 문법을 기억하지 않아도 된다는 인식이 발생하여 인출 강도가 약해지고(알긴 아는데 계속 입 안에서만 맴도는 상태), 악순환이 이어진다고 주장한다. 이를 해결하기 위해서는 검색하기 전에 먼저 의도적으로 기억하려고 노력해야 한다.


 

복잡한 코드를 읽는 일은 작업 기억 공간에 과부하를 일으키기도 한다. 작업 기억 공간은 STM과는 다르게, 정보를 처리하는 프로세서 역할을 한다는 차이점이 있다. 작업 기억 공간 또한 STM 과 마찬가지로 용량(인지 부하)이 정해져있으며, 청크 단위로 이루어진다. 만약 청크 단위로 나뉘지 않는 너무 많은 요소를 가진 문제를 풀려고 하면 과부하가 걸린다.

 

여기서 말하는 인지부하는 크게 3가지로 나뉘는데,

  • 내재적 부하: 문제 자체가 얼마나 복잡한지
  • 외재적 부하: 외부적 요인에 의해 문제에 추가된 것
"""
10보다 큰 항목을 선택하는 파이썬 프로그램 = 동일한 내재적 인지 부하 (문제 그 자체)
"""

# List Comprehension 에 대한 배경지식에 따라 외재적 인지 부하가 달라질 수 있다
above_ten = [a for a in items if a > 10]

# 일반적인 for 문
above_ten = []
for a in items:
	if a > 10:
    	above_ten.append(a)
  • 본유적 부하: 생각을 LTM 에 저장하는 과정에서 일어나는 인지적 부하

 

 

인지 부하를 줄이기 위해서는 리팩터링을 하면 된다. 리팩터링은 코드의 기능은 유지하되, 구조를 개선하는 것이다. 일반적으로는 코드의 유지 보수를 쉽게 하기 위한 목적으로 이루어지지만, 저자는 장기적으로 가독성이 높은 코드를 작성하도록 리팩터링 하는, 인지적 리팩터링도 중요하다고 한다.

 

의존 그래프(왼쪽)와 상태표(오른쪽)

그러나 리팩터링을 해도 코드의 구조가 복잡하다면, 예를 들어 정확히 코드의 어디를 파악해야 하는지 모르거나 코드가 서로 밀접하게 연결되어 많은 코드를 읽어야 하는 경우 작업 기억 공간 용량을 초과할 수 있다. 이런 상황에서는 코드 기반으로 의존 그래프 또는 상태표를 만들어 작업 기억 공간을 보조하면 도움이 된다. 

 

 

 

 

변수 역할의 11가지 분류 - 사야니에미의 프레임워크

 

프로그램을 깊이 이해하기 위해서는 변수가 중심적인 역할을 하는데, 변수를 파악할 때에는 변수 이름뿐만 아니라 타입과 역할을 살펴보는 것이 도움된다.

 

앞서 코드를 이해하는데 도움이 되는 의존 그래프  상태표를 소개했는데, 이 방식은 다소 국지적이라는 단점이 있다. 코드를 보다 깊게 이해하기 위해서는 텍스트 구조 지식 계획 지식에 대해 살펴봐야 한다.

  • 텍스트 구조 지식: 키워드가 하는 일이나 변수의 역할 등과 같은 프로그램의 표면적인 이해와 관련됨
  • 계획 지식: 프로그래머가 프로그램을 작성할 때, 계획한 것이 무엇인지 혹은 무엇을 달성하려고 했는지

 

뿐만 아니라, 코드가 어떤 구조로 되어 있고 어떻게 연결되어 있는지 살펴봐야 하는데

다음 4가지 단계를 거쳐 표면적 지식으로부터 좀 더 깊은 이해로 진행할 수 있다.

  1. 프로그램의 시작점(초점, focal point)을 찾는다. ex) main()
  2. 초점으로부터 지식을 확장 ex) 초점에서 시작해서 변수, 메서드, 클래스 등의 연결 파악
  3. 관련된 개체로부터 개념 이해 ex) 입력값에 대한 함수의 결과 이해, 클래스가 가지는 필드의 지식 이해 등
  4. 여러 개체에 걸쳐있는 개념 이해 ex) 제약조건이나 오류 등

코드를 읽을 때에는 모든 라인을 순차적으로 읽기보다는, 전체적으로 스캔 후 함수의 콜 스택을 따라 읽으면 좋다.

 

 

 

2. 코드에 대해 생각하기

지금까지 코드를 읽고 이해하는 데 도움이 되는 방법들에 대해 알아봤으니, 문제를 해결을 위한 통찰력을 얻는 방법에 대해 알아보자. '모델링'은 소프트웨어 설계 결정을 내리는 데 도움이 된다. 상태표, 의존 그래프, 개체 관계도 등 실제 모델이

아니라, 우리가 머릿속에서 생각하는 정신적 모델을 사용하는 것도 문제 해결에 도움이 된다.

  • 정신모델: 풀어야 할 문제에 대해 추론하기 위해 사용할 수 있는 작업 기억 공간 내의 추상화
    • 방향/무방향 그래프 또는 다양한 형태의 리스트와 같은 자료구조
    • 디자인 패턴
    • 모델 뷰 컨트롤러와 같은 아키텍쳐 패턴
    • 개체 관계도 또는 시퀀스 다이어그램과 같은 도표
    • 상태도 또는 페트리 넷과 같은 모델링 도구 등

 

또한, 이미 배운 지식은 다른 영역에서도 유용하다. 이를 학습 전이학습 도중 전이라고 한다. 

그림과 같이 새로운 정보를 습득하면, 먼저 감각 기억 공간과 STM에 의해 처리되며 작업 기억 공간으로 전송된다. 작업 기억 공간은 이에 대해 생각하는 동시에 LTM에서 관련 정보를 검색하여 가져온다. 그래서 자바를 알고 있는 상태에서 파이썬 메서드에 대해 배우면 조금 다르게 동작하더라도 파이썬 메서드에 대해 더 빨리 학습할 수 있는 것이다.

  • 저도 전이: 자동화된 기술을 이전하는 것 ex) Ctrl+C, Ctrl+V
  • 고도 전이: 복잡한 작업이 전이되는 것 ex) 프로그래밍 시 변수를 사용하려면 먼저 선언을 해야한다고 생각하는 것
  • 근거리 전이: 서로 가까운 영역 사이에서 지식이 전이되는 것 ex) 미적분학과 대수학
  • 원거리 전이: 서로 먼 영역 간에 일어나는 전이 ex) 라틴어와 논어학

 

그렇다고 해서 전이가 항상 도움되는 것은 아닐 수 있다. 

  • 긍정적 전이: 무언가를 알고 있어 새로운 작업을 할 때 도움이 되는 전이
  • 부정적 전이: 기존 지식이 새로운 것을 배우는 데 방해가 되는 경우

긍정적 전이가 일어나면, 새로운 상황에 대한 정신 모델을 처음부터 만들 필요 없이 LTM에 저장된 정신 모델을 부분적으로 활용하여 형성할 수 있다. 그러나 이미 알고 있던 지식이 새로운 지식을 배울 때 유사하지 않음에도 불구하고 비슷하다고 인식될 경우 오개념을 발생시킬 수 있는데, 이러한 상황을 부정적 전이라고 한다. 예를 들어 자바에서는 변수를 초기화하지 않으면 사용할 수 없지만, 파이썬에서는 이러한 가정이 혼란을 야기할 수 있다. 오개념을 방지하는 데 코드베이스 내의 테스트문서화가 도움이 된다.

 

 

 

3. 좋은 코드 작성하기

좋은 코드를 작성하기 위해서는 변수, 클래스 및 메서드 등 코드 내 구성 요소들에 이름을 잘 명명하는 것이 중요하다. 변수명은 그자체로 코드를 설명하는 주석문처럼 작용하기도 하며, 초기에 부여한 이름이 대부분 유지되기 때문이기도 하다.

 

좋은 변수명을 인지적 관점으로 바라보면, 의미가 담긴 구성요소가 명확한 이름이라고 할 수 있다. 1부에서 배운대로 변수명을 읽을 때는 각 부분을 분할하고, 작업 기억 공간으로 전송될 때 각 부분과 관련된 정보를 LTM에서 검색하여 작업 기억 공간으로 전송한다. 

nmcntrlst 보다 name_counter_list 가 이해하기 쉽다

 

이 책에서는 더 나은 변수명을 위해 페이텔슨은 3단계 모델을 제시하고 있다.

  1. 이름에 포함할 개념을 선택: 의도를 고려하여 개체가 어떤 정보를 보유하고 있으며 무엇을 위해 사용되는지 나타낸다.
  2. 각 개념을 나타낼 단어 선택: 일관성 있는 단어를 사용한다.
  3. 단어들을 사용하여 이름 구성: 자연어에 맞추어 이름 틀을 사용 ex) points_max (x), max_points (o)

주의할 점은 클린 코드를 위반하는 구조적 안티패턴뿐만 아니라 언어적 안티패턴도 고려하여 이름을 지어야 한다는 점이다. 이러한 안티패턴은 우리 뇌에 혼란을 초래하여 인지 부하를 일으킬 수 있다. 

 

 


 

좋은 코드를 작성하면서 어려운 문제를 해결하는 것은 LTM을 강화하는 것이 가장 도움 된다. 문제 해결은 문제 이해, 계획 수립, 계획 실행 단계로 구성되며, 각 단계에서 LTM이 중요한 역할을 한다. 뿐만 아니라 LTM에 저장된 다양한 유형의 기억이 문제 해결을 돕는데 기여한다.

기억의 종류

  • 절차적(암시적) 기억: 운동 능력이나 의식하지 않고 발휘하는 기술로, 반복 연습에 의해 형성된다. ex) 단축키, 괄호를 열면 자동으로 괄호를 닫는 코드 작성 등
  • 선언적(명시적) 기억: 기억할 수 있는 사실이 있고, 그 사실을 자신이 알고있다는 것도 안다. 명시적 기억은 명시적 주의가 필요하다.  
    • 일화적 기억(경험): 과거에 문제를 어떻게 해결했는지 기억할 때 사용된다. 전문가들은 주로 익숙한 문제를 '해결' 하기보다는 '재현'한다. 즉 새로운 해결책을 찾는 대신 이전에 유사한 문제에 효과가 있었던 해결책에 의존하는 경향이 있다. ex) 계층 구조와 관련된 문제를 해결할 땐 트리를 사용했었지~
    • 의미적 기억(사실): 
    • 일화적 기억은 추가적인 노력을 하지 않아도 생성되지만, 의미적 기억과 마찬가지로 기억을 많이 생각해야만 인출 강도가 높아진다.

위 기억 분류에 따르면 암시적 기억은 작업을 신속하게 실행하는데 도움이 됨을 알 수 있으며, 암시적 기억이 형성되기 위해서는 인지 단계, 연상 단계, 자율 단계가 필요하다.

  • 인지 단계: 새로운 것을 배우는 때로, 새로운 정보를 더 작은 부분으로 나누고 당면한 작업에 대해 명시적으로 생각한다. ex) 배열의 인덱스는 0부터 시작하니까, 인덱스 3인 원소는 4번째 원소구나!
  • 연상 단계: 응답 패턴이 나타날 때까지 새 정보를 적극적으로 반복한다. ex) 인덱스를 계산할 때는 항상 1을 빼면 되네!
  • 자율 단계: 기술을 자동화 한 상태로, 인지 부하가 증가하지 않는다. ex) 리스트와 리스트 연산을 마주하면 숫자를 세거나 명시적으로 생각하지 않고도 인덱스를 즉시 알 수 있는 상태

 

문제 해결 능력을 향상시키는 또 다른 방법은 본유적 부하이다. 프로그래밍 능력을 향상시키기 위해 모든 프로그램을 처음부터 작성하는 것이 중요하다고 생각하는 사람들도 있지만, 연구 결과에 따르면 다른 사람이 작성한 코드를 읽고 해당 코드를 분석하는 것이 프로그래밍 능력 향상에 도움이 될 수 있다고 한다. 이는 작업 기억 공간의 인지 부하(본유적 부하)와 관련 있다.

세번째 유형의 인지 부하, 본유적 부하 - 두뇌가 정보를 LTM에 다시 저장하기 위해 수행하는 노력

작업 기억 공간의 내용이 LTM 에 다시 저장되기 위해서는 본유적 부하가 뒤따른다. 그러나 작업 기억 공간의 부하가 너무 많으면 결과가 LTM으로 옮겨지는 것이 어려울 수 있다. 따라서 본인만의 힘으로 프로그래밍 구현을 연습하는 것은 물론 실력이 향상되지만, 내재적 부하와 외재적 부하로 인해 본질적 부하가 부족할 수 있다. 다른 사람의 코드를 읽을 때는 인지 부하가 그렇게 높지 않으므로 코드 구조를 자세히 검토하고 이를 기억할 수 있게 된다. 따라서 프로그래밍 실력을 향상시키기 위해서 GitHub 탐색, 오픈 소스 코드 분석 등을 통해 코드를 연구하는 것이 유용하다.

 

 

 

4. 코딩에서의 협업

실제로 소프트웨어는 팀에 의해 개발되므로 동료와 함께 몰입하며서 코드를 작성하는 것이 중요하다. 특히 다른 사람들과 함께 작업할 때는 업무 중단이 흔하고 다시 돌아와 원래 하던 업무를 계속하기가 어려운데, 어떻게 하면 중단에 더 잘 대비할 수 있을까?

  • 정신 모델 저장(brain dump): 정신 모델은 중단으로부터 복구하는데 유용할 수 있으므로 정신 모델에 대한 메모를 주석문으로 남기는 것이 도움된다. 코드는 프로그래머의 사고 과정을 모두 설명하진 못하므로 코드에 특정 접근 방식을 선택한 이유, 코드의 목표 또는 구현을 위해 고려한 다른 대안과 같은 내용은 주석으로 남겨두면 좋다. 
  • 미래 기억 향상: 미래 기억은 미래에 무언가를 할 것에 대한 기억으로, to-do 주석문 등을 활용할 수 있다. 
  • 하위 목표 라벨링: 문제를 어떤 작은 단계로 나눌 수 있는지 명시적으로 기록함으로써 업무가 중단된 후 하던 업무를 계속할 때 자신의 생각을 정리하는데 유용하다.

 

 

 

후기

뇌가 코드를 처리하는 방식에 대해 새롭게 알 수  있었다.

저자가 LTM의 중요성에 대해 정말 많이 강조하고 있어서 그동안 모르면 구글링 하면 되지~ㅋ 하고 대수롭지 않게 생각했던 나를 반성함 🥲

새로운 코드를 읽을 때는 왠지 집중도 안되고 코드 분석도 어려워서 그냥 ADHD인줄 알았는데,,,

이 책을 읽고나서부터는 여기 나오는 방법대로 새로운 코드를 마주칠 때 잘 대처할 수 있을 것 같다.

 

4부도 딱히 정리는 안했지만 흥미로웠던 부분이, 사람은 멀티태스킹이 안된다고 한다. 그런데 멀티태스킹을 하는 사람들은 자신이 매우 생산적이라고 느낀다고 한다...!! 그들 자신의 수행능력이 만족스럽다고 느끼지만, 실상은 한가지 일만 하는 사람들에 비해 결과도 좋지 않고 시간도 오래쓴다는 것이다

 

아무튼 뇌의 행동 기반으로 인간이 왜 이렇게 되는지 설명해줘서 좋았고, 사회 과학 분야에서 이루어진 실험과 함께 예시를 들어줘서 이해가 쉬웠다. 앞으론 학교에서 수업을 듣거나.. 시험공부하거나.. 연구할땐 이 책 내용을 떠올리면서 의식적으로 노력해야겠다.

머신 러닝 문제의 복잡도를 평가하는 가장 좋은 방법 중 하나는, 필요한 데이터의 종류와 이런 데이터를 사용할 수 있는 기존 모델이 있는지 살펴보는 것이다.

 

데이터 패턴을 찾을 후 모델을 선택할 때

  • 특성 스케일이 차이나는 경우 ex. 나이(1~100)와 수입(1~1억) 특성의 크기와 관계 없이 가장 예측 성능이 좋은 특성을 활용해야 한다.
    • 방법1) 특성의 scale을 정규화하는 전처리를 수행해 평균이 0이고 단위 분산을 가지도록 한다. = 정규화
    • 방법2) 특성 scale의 차이에 영향을 받지 않는 모델로 바꾼다. = decision tree, random forest, gradient-boosted decision tree
  • 특성의 선형 조합만 사용하여 좋은 예측을 만들 수 있는 경우
    • 회귀 문제에는 linear regression, 분류 문제에는 logistic regression 또는 naive bayes => 간단하고 효율적이며 종종 모델의 가중치를 직접 보고 중요한 특성을 식별할 수 있다.
    • 만약 feature과 target의 관계가 더 복잡한 경우 다층 신경망 등 비선형 모델을 사용하거나 교차특성을 생성하도록 한다
  • 데이터에 시계열 특징이 있는 경우 = 현재 값이 이전 시간의 값에 의존하는 경우
    • 시계열 정보를 명시적으로 인코딩할 수 있는 모델을 사용한다. = ARIMA 모델, RNN 등
  • 각 샘플이 패턴의 조합인 경우 = 이미지 분야, 문장 분류, 음성 인식 등
    • CNN의 경우 translation invarient학습하는 능력 때문에 유용하다. 이미지에서의 위치와 상관없이 패턴 추출 가능

https://scikit-learn.org/stable/tutorial/machine_learning_map/index.html

 

 

데이터 분할은 일반적으로 학습 70% 검증 20% 테스트 10% 사용한다.

데이터 분할 방법은 검증을 위해 매우 중요한 부분이며, 검증 셋과 테스트 셋을 본적 없는 데이터와 비슷하게 만드는 것을 목표로 해야한다.

종종 랜덤하게 샘플링하여 훈련, 검증, 테스트 셋을 나누는데, 경우에 따라 이로 인해 데이터 누수가 일어난다.

데이터 누수는 실전에서 사용자가 사용할 때 얻을 수 없는 정보를 훈련 과정에서 모델이 얻을 때 일어난다.

데이터 누수를 일으키기 쉬운 랜덤 데이터 분할

  • 시계열 데이터 누수: 모델이 과거 데이터에서 학습하여 아직 일어나지 않은 미래 이벤트를 예측하게 되는데, 이런 시계열 데이터를 랜덤하게 나누면 데이터 누수가 발생한다. 모델을 랜덤하게 나눈 데이터에서 훈련하고, 남은 데이터에서 평가하면 예측하려는 이벤트 뒤에 발생한 데이터에 모델이 노출되게 된다. => 검증 셋과 테스트 셋에는 잘 동작하지만, 실전에는 나쁜 성능
    • 실제 세상에서는 불가능한 미래 정보를 사용하여 학습했기 때문
  • 샘플 오염: 데이터 누수의 일반적인 원인은 랜덤한 작업이 일어나는 수준 때문이다. 어려운 작업에서 너무 잘 동작한다면 버그나 데이터 누수가 없는지 확인하도록 해야한다.
    • ex. 학생 수필 점수 예측 프로그램: 동일한 학생의 여러 수필이 데이터셋, 테스트셋에 모두 들어가게 되면 모델이 학생을 식별하는 특성을 감지하여 점수를 예측하게 된다 => 한 학생이 작성한 수필은 모두 비슷한 점수를 받게 된다 => 따라서 각 학생의 데이터가 훈련 셋이나 검증 셋 중 한 곳에만 나타나야 한다.

 

 

과적합을 피하는 방법 중 하나는 regularization이다. = 정보를 표현하는 모델의 능력에 벌칙을 부과하여 관련 없는 많은 패턴에 초점을 맞추는 모델의 능력을 제한하고, 적은 개수의 더 유용한 특성을 사용하게 한다.

  • 가중치의 절댓값을 cost로 부과한다 => ex. 선형 회귀나 로지스틱 회귀 같은 모델에서 L1/L2 regularization은 loss function에 큰 가중치에 대한 cost항을 추가한다
    • L1: 가중치의 절댓값의 합 => 유용하지 않은 특성을 0으로 만들어 정보가 풍부한 특성을 선택하도록 돕는다. 일부 특성에 상관관계가 있을 때 L1이 유용하다. ex) Lasso
    • L2: 가중치의 제곱합
  • drop out: 신경망이 자주 사용하는 regularization으로, 훈련 과정에서 신경망의 뉴런 중 일정 비율을 랜덤하게 무시한다. => 하나의 뉴런이 과도한 영향력을 발휘하지 못하도록
  • random forest와 같은 트리 기반 모델에서 트리의 최대 깊이를 줄이면 각 트리가 데이터에 과대적합되는 능력이 감소된다.
    • random forest에서는 트리 개수를 늘려도 regularization의 효과가 있다

 

 

머신 러닝 애플리케이션의 배포 방식에는 크게 두 가지가 있는데, 배포 방식을 선택 할 때 속도, 하드웨어, 네트워크 요구 사항, 개인 정보 보호, 비용 복잡도 등 여러 요인을 고려해야 한다. 

server-side deployment

  • server-side deployment: 클라이언트로부터 요청을 받아 inference 파이프라인을 실행하여 결과를 반환하는 웹 서버로 구성된다. 서버 측 모델 작업에는 스트리밍과 배치 두가지로 이루어질 수 있다. 호스팅 서버가 필요하여 제품의 인기가 높아지면 비용이 빠르게 증가하고, 호스팅 서버가 애플리케이션의 단일 장애점이 될 수 있다.
    • 스트리밍: 요청을 받자마자 즉시 처리 => 사용자가 개별적인 inference 요청을 보내기 떄문에 동시 사용자 수에 맞추어 선형적으로 인프라를 늘려야 한다. 서버가 처리할 수 있는 용량 이상으로 트래픽이 증가하면 지연/실패하게 된다.
      • 스트리밍 애플리케이션은 속도에 대한 요구 사항이 높을 때 필요하다. 모델에서 필요한 정보가 예측 시점에서 제공되고 모델의 예측이 즉시 필요하다면, 스트리밍 방식이 적합하다.
    • 배치: 한 번에 많은 개수의 요청을 처리 => inference 파이프라인을 여러 샘플에서 동시에 실행할 수 있는 하나의 job으로 간주한다. 배치 잡은 많은 샘플에서 모델을 실행하고 필요할 때 사용할 수 있도록 결과를 저장한다.
      • batch job: 모델의 inference가 필요하기 전에 모델의 필요한 특성을 얻을 수 있을 때 적합하다. (미리 저녁에 돌려놓고 다음날 아침에 확인한다던가)
      • 배치 방식은 스트리밍 방식과 동일한 횟수만큼 추론을 실행한다. 하지만, 자원을 더 효율적으로 사용할 수 있다. => 자원을 할당하고 병렬화 하기가 쉬움
      • 배치 방식은 미리 계산된 결과를 저장하고, inference 시에는 저장한 내용을 추출하기만 하면 되므로 추론 속도가 매우 빠르다 == 캐싱이랑 비슷하다고 생각하면 된다.
    • 가능한 많은 샘플을 계산하고, 추론 시에 사전에 계산된 결과를 추출하도록 하는 하이브리드 방식을 사용하는 것도 가능하다. 만약 사전 계산한 결과가 없거나 오래되었다면 즉시 계산을 수행한다. => 계산할 수 있는 모든 것을 미리 수행하기 때문에 결과를 신속하게 제공한다. 단 배치 파이프라인 + 스트리밍 파이프 라인을 모두 유지하기 위한 비용과 시스템 복잡도가 단점이라고 할 수 있다.

client-side deployment

  • client-side deployment: 클라이언트 측에 모델을 배포하는 목적은 모델 실행 서버가 없더라도 모든 계산을 클라이언트에서 실행하기 위해서이다. 모델은 앱에 포함되어 디바이스에서 사용되거나 웹 브라우저에 로딩될 수 있다. => 모든 사용자에 대한 inference를 실행하기 위해 인프라 구축할 필요가 없다. 또한 디바이스에서 모델 실행 시 디바이스-서버 사이 전송할 데이터 양이 줄어든다. (네트워크 지연을 줄일 수 있다) 또한 inference에 필요한 데이터가 민감한 정보를 담고 있을 때 장치에서 모델을 실행하게 되면 데이터를 원격 서버로 전송할 필요가 없으므로 제 3자가 데이터에 접근할 위험이 줄어든다. 단, 일반적으로 사용자 디바이스는 서버보다 컴퓨팅 성능이 떨어지므로, 디바이스에서 모델을 실행할 수 있도록 모델의 복잡도를 제한해야 한다.
    • on-device: 태블릿이나 핸드폰 프로세서는 일반적으로 머신러닝 모델 실행에 최적화 되어 있지 않아 inference 파이프라인을 느리게 실행한다. 배터리를 많이 소모하지 않고 클라이언트 측 모델을 빠르게 실행하려면 모델이 가능한 한 작아야 한다. => 모델 크기를 줄이려면 간단한 모델을 사용하거나, 모델의 파라미터 개수, 계산 정밀도를 줄여야 한다
      • 스마트폰과 같은 장치에서 실행하기 너무 복잡한 최첨단 모델에 의존하는 경우 서버에 배포되어야 한다. 일반적으로 장치에서 (처리 지연 시간) > (네트워크 지연 시간) 일 경우 클라우드 상에서 모델을 실행하는 것을 고려해봐야 한다.
      • 온 디바이스 배포는 속도, 인프라, 개인 정보에 대한 이득이 커서 엔지니어링 작업에 노력을 투자할만큼 충분한 가치가 있다.
    • browser: 대부분의 스마트 장치는 브라우저를 가진다. 브라우저는 종종 빠른 그래픽 계산을 지원하기 위해 최적화되어 있다. 사용자는 추가 애플리케이션 설치 없이 브라우저를 통해 모델과 상호작용할 수 있으며, 사용자 디바이스에서 계산이 수행된다. 서버 인프라는 모델의 가중치가 포함된 웹 페이지만 사용자에게 제공하면 된다. 단, 대역폭 비용을 늘린다는 단점이 있다. (애플리케이션 설치 방식은 모델을 한 번만 다운로드 하면 되지만, 브라우저 방식을 클라이언트가 웹 페이지를 열때마다 모델을 다운로드 함) => 사용하는 모델이 매우 작거나 빠르게 다운로드 가능하다면 브라우저 방식이 용이하다.
      • ex. tensorflow.js: 대부분의 미분 가능한 모델을 브라우저 상에서 자바스크립트로 훈련하거나 추론을 실행할 수 있다. 다른 언어로 훈련한 모델을 사용할 수도 있음

정리하자면, 서버 측 모델의 경우 가장 큰 latency는 종종 서버로 데이터를 전송할 때 발생한다.(네트워크 지연)

클라이언트 측 모델에서는 시간 지연 없이 실행되지만, 하드웨어 성능 제약으로 서버보다 느리게 샘플을 처리하게 된다. (처리 지연)

 

 

클라이언트 상에 배포 뿐만 아니라 훈련도 할 수 있다. 이는 특히 사용자마다 다른 모델을 원할 경우 유용하다.

이 아이디어가 federated learning(연합 학습) 의 핵심이다. 연합 학습에서는 클라이언트가 각자의 모델을 가지고, 각 모델은 사용자의 데이터에서 학습 및 요약되어 업데이트를 서버로 보낸다. 서버는 모든 업데이트를 사용해 모델을 향상시키고 새로운 모델의 업데이트를 개별 클라이언트에 전달한다. => 각 사용자는 각자의 요구에 맞는 개인화 모델을 얻을 수 있고 & 여전히 다른 사용자로부터 집계된 정보에서 도움 받을 수 있다.

연합 학습에서는 사용자 데이터를 서버로 바로 전송하지 않고, 집계된 모델 업데이트 값만 전송하기 때문에 개인 정보가 더욱 보호될 수 있다. 단, 개별 모델이 잘 동작하면서 서버로 전송된 데이터를 적절하게 익명화시키는 것은 단일 모델을 훈련하는 것보다 복잡성이 증가한다.

 

 

머신 러닝 모델을 제품으로 배포할 때 성능과 트래픽을 관리하는 것은 매우 어렵다.

머신러닝은 늘어난 트래픽을 처리하기 위해 캐싱을 사용할 수 있다. 미래에 동일한 매개변수로 함수를 호출한다면, 저장된 결과를 추출하면 되므로 함수를 빠르게 실행할 수 있다.

  • 추론 결과 캐싱하기: LRU 캐시는 가장 간단한 캐싱 방법이다. 가장 최근 사용된 모델 입력과 해당 결과를 추적하여 캐시에 유지한다. 새로운 입력에서 모델을 실행하기 전 캐시에서 이 입력을 조회하고 해당 항목을 찾으면 캐시에서 결과를 바로 제공한다.  -> 사용자가 동일한 종류의 입력을 제공하는 애플리케이션에 적합하다.
    • 캐시 확인 후 캐시에 이미지 없음을 확인 => 모델 실행(병목 지점) => 캐시에 추가 => 결과 반환
    • 캐시 확인 후 캐시에 이미지 있음을 확인 => 결과 반환
    • 주의) 캐싱을 사용할 때 부수 효과가 없는 함수만 캐싱해야 한다. ex) 결과 반환 및 데이터 저장하는 함수는 캐싱하면 데이터베이스에 값을 저장하지 않게 됨
  • 인덱스 캐싱: ex. 검색 시스템을 만들 때 일반적인 접근 방법은 먼저 모든 인덱싱 문서를 의미있는 벡터로 임베딩 -> 사용자가 검색 쿼리를 입력하면 추론 시 임베딩 벡터로 변환되어 데이터베이스에서 가장 비슷한 임베딩을 찾는다. => 이런 방싱은 대부분 계산을 미리 해 놓으므로 추론 속도를 크게 높여준다.
    • 주의) 캐싱 성능을 향상시킬 수 있지만 복잡도가 증가한다. 모델이나 내부 데이터가 업데이트 된 경우 캐시를 삭제해야 하며, 캐시 크기도 조정하며 최적의 값을 찾아야 한다.

 

 

 

 

*** 용어정리

  • 분류: 두 개 이상의 카테고리로 데이터를 분류
  • 회귀: 연속적인 값을 예측
  • 지식 추출: 큰 모델(Teacher network)에서 추출한 지식을 작은 모델(Student network)로 transfer 하는 과정
  • 카탈로그 구성: 데이터 메타데이터 관리 서비스 
  • 생성 모델: 주어진 데이터를 학습하여 데이터 분포를 따르는 유사 데이터를 생성하는 모델 ex. GAN
  • 시계열: 미래 사건을 예측하기 위해 여러 개의 과거 데이터 포인트를 사용

 

후기

이 책은 머신러닝 전반적인 내용 뿐만 아니라 실제 머신러닝 기반 애플리케이션을 만들 때 전체 파이프라인에 대해 배울 수 있다.

 

그리고 데이터의 중요성을 끊임없이 말하고 있으며 데이터셋 자체를 효과적으로 조사하는 팁들이 있다. 데이터를 수집할 때에는 대표적이고 다양성을 가진 데이터 셋을 준비해야 하며, 데이터를 살펴보고 특정 종류의 데이터가 표현되지 않으면 더 많은 데이터를 모아야 한다. 이 과정에서 데이터 셋에 클러스터링 알고리즘을 적용해 보고 이상치를 살펴보면 좋다고 한다. 

 

머신러닝에 대해 전혀 모르는 내가 처음 들어보는 내용들도 많았는데,

모델에서 특성 중요도를 평가할 때 도움이 되는 방법 중 하나인 `블랙박스 설명 도구`라는 것이 있는데, 이는 내부 동작 방식과 상관 없이 모델의 예측을 설명할 수 있다고 한다.

https://github.com/rickiepark/ml-powered-applications/blob/main/notebooks/black_box_explainer.ipynb

 

GitHub - rickiepark/ml-powered-applications: <머신러닝 파워드 애플리케이션>의 코드 저장소입니다. 원서

<머신러닝 파워드 애플리케이션>의 코드 저장소입니다. 원서 깃허브는 https://github.com/hundredblocks/ml-powered-applications 입니다. - GitHub - rickiepark/ml-powered-applications: <머신러닝 파워드 애플리케이션>의

github.com

이러한 도구의 예시로 LIME과 SHAP이 있다.

 

마지막으로 전문가 인터뷰와 모델 서빙, 모니터링쪽 내용이 많은 도움이 된다.

 

 

컴파일러의 목적은 한 언어를 다른 언어로 변환하는 것이다. 그리고 어떤 컴파일러는 시스템에서 바로 실행할 수 있는 저수준 기계어로 컴파일하지만, 어떤 컴파일러는 가상 머신에서 실행하기 위한 중간 언어로 컴파일한다.

  • 닷넷 CLR, 자바 등은 여러 시스템 아키텍쳐에서 사용할 수 있는 중간 언어로 컴파일한다.
  • C, Go, C++, 파스칼 등은 바이너리 실행 파일로 컴파일한다. 바이너리는 컴파일한 플랫폼과 동일한 플랫폼에서만 사용할 수 있다.

파이썬 애플리케이션은 보통 소스코드 형태로 배포한다. 파이썬 인터프리터는 소스 코드를 변환 후 한 줄씩 실행한다.

CPython 런타임이 첫 번째 실행될 때 코드를 컴파일 하지만, 이 단계는 사용자에게 노출되지 않는다.

 

python interpreter 내부 동작 과정

 

파이썬 코드는 기계어 대신 `바이트코드` 라는 저수준 중간 언어로 컴파일된다.

바이트코드는 .pyc 파일에 저장되고, 실행을 위해 캐싱된다.

코드를 변경하지 않고 같은 파이썬 애플리케이션을 다시 실행하면 매번 다시 컴파일하지 않고 컴파일 된 바이트코드를 불러와서 빠르게 실행할 수 있다.

 

 

 

CPython 컴파일러는 왜 C로 작성되는가?

윈도우와 리눅스 커널 api는 모두 C로 작성되었기 때문에, 여러 표준 라이브러리모듈이 저수준 OS api에 접근하기 위해 C로 작성되었다. 

 

컴파일러는 크게 두 가지 유형이 있는데,

  1. 셀프 호스팅 컴파일러는 Go 컴파일러처럼 자기 자신으로 작성한 컴파일러다. 셀프 호스팅 컴파일러는 부스트래핑이라는 단계를 통해 만들어진다.
  2. Source to Source 컴파일러는 컴파일러를 이미 가지고 있는 다른 언어로 작성한 컴파일러이다.

c.f. PyPy라는 파이썬으로 작성된 파이썬 컴파일러도 있다.

 

 

 

메모리 관리

먼저 C에서의 메모리 할당에 대해 알아보자면, 

  • 정적 메모리 할당: 필요한 메모리는 컴파일 시간에 계산되고, 실행 파일이 실행될 때 할당된다.
    • C에서는 타입 크기가 고정되어 있으며, 컴파일러는 모든 정적/전역 변수에 필요한 메모리 계산 후 필요한 메모리 양을 애플리케이션에 컴파일해 넣는다. 메모리 할당 시에는 시스템 콜을 이용한다. 
  • 자동 메모리 할당: 스코프에 필요한 메모리는 프레임에 진입 시 콜스택 내 할당되고, 프레임이 끝나면 해제된다.
    • 마찬가지로 컴파일 시간에 필요한 메모리를 계산한다.
#include <stdio.h>
static const double five_ninths = 5.0/9.0; // const: 정적 할당

double celsius(double fahrenheit){
	double c = (fahrenheit - 32) * five_ninths; // 함수 내 변수는 자동 할당
    return c;
}

int main(){
	double f = 100; // 함수 내 변수는 자동 할당
    printf("%f F is %f C\n", f, celsius(f)); // celsius(f) 반환값은 자동 할당
    return 0;
}
  • 동적 메모리 할당: 메모리 할당 api를 호출하여 런타임에 메모리를 동적으로 요청하고 할당한다
    • 사용자 입력에 따라 필요한 메모리가 결정되는 경우
    • OS는 프로세스가 메모리를 동적 할당 할 수 있도록 시스템 메모리의 일부를 예약해 두는데, 이 공간을 heap 이라 한다.
    • 동적 할당 메모리는 제대로 반환하지 않으면 메모리 누수가 발생한다.

 

 

C 기반인 CPython은 C 메모리 할당 방식들의 제약조건을 따르기 쉽지 않다. 왜냐하면

  1. 파이썬은 동적 타입 언어로, 변수의 크기를 컴파일 시간에 계산할 수 없다.
  2. 코어 타입의 크기는 대부분 동적이다. 리스트 타입은 크기를 정할 수 없고, 딕셔너리 타입은 제한없이 키를 가질 수 있다.
  3. 값의 타입이 달라도 같은 변수명을 재사용할 수 있다.

따라서 C 메모리 할당 방식의 제약을 극복하기 위해 CPython은 동적 메모리 할당에 크게 의존하며 `참조 카운팅`과 `가비지 컬렉션`을 사용하여 메모리를 자동 해제하도록 했다. Python의 객체 메모리는 개발자가 직접 할당하는 대신, 하나의 통합 api를 통해 자동으로 할당된다.

CPython은 세가지 동적 메모리 할당자 도메인을 제공한다.

  • 저수준 도메인: 시스템 힙이나 대용량 메모리 또는 비객체 메모리 할당 시 사용된다.
  • 객체 도메인: 파이썬 객체와 관련된 메모리 할당 시 사용된다.
  • PyMem 도메인: PYMEM_DOMAIN_OBJ 와 동일하며, 레거시 api 용도로 제공된다.

또한 CPython은 두가지 메모리 할당자를 사용한다.

  • malloc: 저수준 메모리 도메인을 위한 OS 할당자
  • pymalloc: PyMem 도메인과 객체 메모리 도메인을 위한 CPython 할당자
    • 기본적으로 CPython에 같이 컴파일 된다. pyconfig.h에서 WITH_PYMALLOC=0으로 설정 후 파이썬을 컴파일하면 pymalloc을 제거할 수 있으며, 이 경우 PyMeM과 객체 메모리 도메인 api도 시스템 할당자를 사용한다.

 

CPython 메모리 할당자에 대해 알아보기 전에 먼저 Python 의 메모리 구조 레벨에 대해 알아보자.

  • Arenas
    • 힙 메모리에 할당 된 고정된 크기의 256KiB, 가장 큰 메모리 단위를 나타낸다.
      • 아레나는 익명 메모리 매핑을 지원하는 시스템에서는 mmap()으로 할당한다.
    • 파이썬 할당자 pymalloc에서 사용하는 메모리 매핑이다.
    • 시스템 페이지 크기에 맞추어 정렬하며, 시스템 페이지 경계는 고정 길이의 연속 메모리 청크다.
  • Pools
    • arena 당 pool 개수는 64개로 고정되어 있다.
    • pool의 크기는 OS의 default 메모리 페이지 사이즈와 일치해야 한다.
    • 고정된 크기의 4KB 크기를 가지며, 세 가지 상태를 가질 수 있다.
      • Empty; pool이 비어있으므로 할당할 수 있다.
      • Used; pool에 비어있지도 가득차지도 않게 개체가 포함되어 있다.
      • Full; pool이 가득차서 더 이상 할당할 수 없다.
    • pool은 요청이 들어오면 할당되며, 요청된 크기 인덱스에 맞는 사용 가능한 pool이 없는 경우 새로운 pool을 생성한다.
  • Blocks
    • 블록의 크기는 고정되어 있지 않으며, 블록 사이즈는 8 ~ 512 bytes 인 8의 배수이다.
    • 각 블록은 특정 크기의 파이썬 객체를 하나만 저장할 수 있으며, 세 가지 상태를 가질 수 있다.
      • Untouched; 할당되지 않음
      • Free; 할당 되었지만, 해제되어 할당에 사용할 수 있다.
      • Allocated; 할당됨
    • 아래 그림과 같이 사용되지 않은 블록은 풀 내부의 freeblock 단일 연결 리스트에 연결된다. 할당 해제된 블록은 freeblock 리스트의 맨 앞에 추가되며, 풀이 초기화 되면 첫 두 블록은 freeblock 리스트에 연결된다.

할당된 블록과 해제된 블록, 사용되지 않은 블록들을 포함한 사용중인 풀

python 디버그 api `debugmallocstats`를 사용하면 할당된 아레나의 개수와 사용된 블록의 총 개수를 알 수 있다.

크기 인덱스 테이블과 할당 상태를 비롯한 다양한 통계 정보가 출력된다.

더보기

Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys._debugmallocstats()
Small block threshold = 512, in 32 size classes.

class   size   num pools   blocks in use  avail blocks
-----   ----   ---------   -------------  ------------
    0     16           1             128           893
    1     32           3            1143           387
    2     48          10            3147           253
    3     64          47           11794           191
    4     80          33            6697            35
    5     96           7            1102            88
    6    112           5             595           130
    7    128           3             370            11
    8    144          15            1641            54
    9    160           2             150            54
   10    176          27            2451            33
   11    192           2              96            74
   12    208           1              73             5
   13    224           3             206            10
   14    240           1              57            11
   15    256           1              52            11
   16    272           1              46            14
   17    288           1              34            22
   18    304           5             232            33
   19    320           1              31            20
   20    336           1              20            28
   21    352           1              18            28
   22    368           1              23            21
   23    384           1              17            25
   24    400           2              42            38
   25    416           2              71             7
   26    432           2              54            20
   27    448           2              48            24
   28    464           2              43            27
   29    480           2              34            34
   30    496           1              29             3
   31    512           2              48            14

# arenas allocated total           =                    3
# arenas reclaimed                 =                    0
# arenas highwater mark            =                    3
# arenas allocated current         =                    3
3 arenas * 1048576 bytes/arena     =            3,145,728

# bytes in allocated blocks        =            2,795,680
# bytes in available blocks        =              261,600
2 unused pools * 16384 bytes       =               32,768
# bytes lost to pool headers       =                9,024
# bytes lost to quantization       =               13,888
# bytes lost to arena alignment    =               32,768
Total                              =            3,145,728

arena map counts
# arena map mid nodes              =                    1
# arena map bot nodes              =                    1

# bytes lost to arena map root     =              262,144
# bytes lost to arena map mid      =              262,144
# bytes lost to arena map bot      =              131,072
Total                              =              655,360

            8 free PyDictObjects * 48 bytes each =                  384
           4 free PyFloatObjects * 24 bytes each =                   96
          6 free PyFrameObjects * 360 bytes each =                2,160
           21 free PyListObjects * 40 bytes each =                  840
   2 free 1-sized PyTupleObjects * 32 bytes each =                   64
  16 free 2-sized PyTupleObjects * 40 bytes each =                  640
   5 free 3-sized PyTupleObjects * 48 bytes each =                  240
   2 free 4-sized PyTupleObjects * 56 bytes each =                  112
   2 free 5-sized PyTupleObjects * 64 bytes each =                  128
   3 free 6-sized PyTupleObjects * 72 bytes each =                  216
   3 free 7-sized PyTupleObjects * 80 bytes each =                  240
   3 free 8-sized PyTupleObjects * 88 bytes each =                  264
   0 free 9-sized PyTupleObjects * 96 bytes each =                    0
 0 free 10-sized PyTupleObjects * 104 bytes each =                    0
 3 free 11-sized PyTupleObjects * 112 bytes each =                  336
 0 free 12-sized PyTupleObjects * 120 bytes each =                    0
 3 free 13-sized PyTupleObjects * 128 bytes each =                  384
 2 free 14-sized PyTupleObjects * 136 bytes each =                  272
 2 free 15-sized PyTupleObjects * 144 bytes each =                  288
 0 free 16-sized PyTupleObjects * 152 bytes each =                    0
 1 free 17-sized PyTupleObjects * 160 bytes each =                  160
 0 free 18-sized PyTupleObjects * 168 bytes each =                    0
 0 free 19-sized PyTupleObjects * 176 bytes each =                    0

 

또는 표준 라이브러리 `tracemalloc` 모듈로 객체 할당자의 메모리 할당 동작을 디버깅할 수도 있다. 이 모듈은 객체 할당 위치나 할당된 메모리 블록의 개수 등에 대한 정보를 제공한다. 메모리 추적을 활성화 하려면 파이썬 실행 시 -X tracemalloc=1 옵션을 추가하면 된다. 여기서 1은 추적할 프레임 수이며, 더 늘릴수도 있다.

 

 

CPython 할당자에 대해 다시 돌아가보자면, 시스템 메모리 할당자 위해 구축된 할당자로 독자적인 할당 알고리즘을 구현한다. 이 알고리즘은 시스템 할당자와 비슷하지만 CPython에 특화되어 있다.

참고💡 대부분의 메모리 할당 요청은 고정된 크기의 작은 메모리를 요구한다.
- PyObject는 16byte, PyASCIIObject는 42byte, PyCompactUnicodeObject는 72byte, PyLongObject는 32byte 크기
- pymalloc 할당자로는 메모리 블록을 최대 256kb까지 할당할 수 있고, 그보다 큰 할당 요청은 시스템 할당자로 처리한다.

그리고 중앙 레지스터가 블록 위치와 사용할 수 있는 블록 개수를 관리한다. 한 풀이 모두 사용되면 다음 풀이 사용된다.

이러한 방식은 CPython에서 주로 만들어지는 작고 수명이 짧은 객체를 할당할 때 적합하다. 그리고 힙 할당 대신 메모리 맵(mmap())을 사용하게 된다.

 

이렇게 CPython은 C의 동적 메모리 할당 시스템 위에 구축되었으며, 메모리 요구 사항은 런타임에 결정되고 PyMem api를 사용하여 메모리를 시스템에 할당한다고 했다. 파이썬은 메모리 관리를 단순화 하기 위해 `객체 메모리 관리 전략`을 도입했다. 여기에는 참조 카운팅과 가비지 컬렉션이 있다.

 

 

파이썬 프로세스 내부 구조

컴퓨터의 CPU는 프로세스를 실행할 때 다음과 같은 추가 데이터가 필요하다.

  • 실행 중인 명령이나 명령을 실행하는 데 필요한 다른 데이터를 보관하는 레지스터
  • 프로그램 시퀀스의 어떤 명령을 실행 중인지 저장하는 프로그램 카운터(명령 포인터)

CPython 프로세스는 컴파일된 CPython 인터프리터+모듈로 구성된다.

모듈을 런타임에 불러들여서 CPython 평가 루프를 통해 명령으로 변환한다.

프로그램 레지스터와 프로그램 카운터는 프로세스에 저장된 명령 중 한 명령만을 가리킨다.

즉, 한번에 하나의 명령만 실행할 수 있다. = CPython은 한 번에 하나의 파이썬 바이트 코드 명령만 실행할 수 있다.

c.f. 따라서 프로세스의 명령을 병렬로 실행하려면 다른 프로세스를 포크하거나 스레드를 스폰하는 방법을 사용한다.

 

 

 

 

후기

이 책에는 CPython 컴파일러가 어떻게 동작하는지, 파이썬 코드를 어떻게 파싱하는지에 관한 설명이 8장까지 매우매우 자세하게 나와있고,

9장 이후 파이썬에서 메모리 관리가 어떻게 구현되는지 나와있다. 9장 이후의 내용이 굉장히 많이 도움이 되었다.

 

이책을 읽다가 새롭게 알게된 사실은 python의 long과 c의 long이 다르다는 점이었다...!!

python의 long은 숫자 리스트로, 12378562834를 [1,2,3,7,8,5,6,2,8,3,4]로 표현한다고 한다.

Python 정수 표현 시 메모리 사용량 변화 - https://mortada.net/can-integer-operations-overflow-in-python.html

(그치만 numpy/pandas 같은 패키지를 사용할 때는 C 스타일이 유지되기 때문에 오버플로우 발생을 고려해야 한다.)

 

그리고 파이썬에서 가비지 컬렉터가 카운터=0일때 회수된다는것은 여러 곳에서 찾아볼 수 있었지만

어느 주기로 실행되는지는 잘 나와있지 않아 그동안 궁금했는데, 

가비지 컬렉션은 정해진 양만큼의 연산이 일어났을 때 주기적으로 실행된다는 것을 알게 되었다. 

 

이 책에는 CPython을 위해 설계된 다양한 도구를 이용하는 벤치마킹, 프로파일링, 실행 추적 등이 자세하게 나와있어서 쉽게 따라할 수 있었다.

 

 

참고:

https://scoutapm.com/blog/python-memory-management

The Proxy Pattern provides a surrogate or placeholder for another object to control access to it.

 

프록시 패턴은 다른 개체에 대한 대체 또는 자리 표시자를 제공할 수 있는 구조적 디자인 패턴이다. 

프록시는 원래 개체에 대한 액세스를 제어하므로 요청이 원래 개체에 전달되기 전이나 후에 수행할 수 있다.

 

 

 

실제 작업은 RealSubject 에서 처리되며, Proxy 는 RealSubject 객체를 대신하면서 접근을 제어한다.

Proxy 에는 RealSubject 에 대한 레퍼런스가 들어 있고, 클라이언트는 항상 Proxy 를 통해 RealSubject 과 데이터를 주고 받는다.

 

 

 

 

장점

  • 클라이언트가 알지 못하는 상태에서 서비스 개체를 제어할 수 있다
  • 클라이언트가 신경 쓰지 않을 때 서비스 개체의 수명 주기를 관리할 수 있다 (프록시는 서비스 개체가 준비되지 않았거나 사용할 수 없는 경우에도 작동함)
  • 개방/폐쇄 원칙 => 서비스나 클라이언트를 변경하지 않고 새 프록시를 도입할 수 있다

단점

  • 많은 새 클래스를 도입해야 하므로 코드가 더 복잡해질 수 있다
  • 서비스의 응답이 지연될 수 있다

왜사용하는가?

Proxy 패턴은 다양한 방식으로 변형 가능하다

 

  • 동적 생성 프록시 => 실제 요청(action() 메소드 호출)이 들어 왔을 때 실제 객체를 생성
    • 실제 객체의 생성에 많은 자원이 소모 되지만 사용 빈도는 낮은 경우

 

  • 지연 초기화(가상 프록시) => ex. 항상 가동되어 시스템 리소스를 낭비하는 무거운 서비스 개체가 있는 경우
    • 앱이 시작될 때 객체를 생성하는 대신 객체 초기화가 실제로 필요한 시점으로 지연될 수 있다
    • 생성하기 힘든 자원에 대한 접근 제한

 

  • 액세스 제어(보호 프록시) => 특정 클라이언트만 서비스 개체를 사용할 수 있도록 하는 경우
    • ex. 접근 권한이 필요한 자원에 대한 접근 제어 
    • 프록시는 클라이언트의 자격 증명이 일부 기준과 일치하는 경우에만 서비스 개체에 요청을 전달할 수 있다
    • ex. 개체가 운영 체제의 중요한 부분이고 클라이언트가 다양한 실행 응용 프로그램(악의적인 응용 프로그램 포함)인 경우

 

  • 원격 서비스의 로컬 실행(원격 프록시) => 서비스 개체가 원격 서버에 있는 경우
    • 프록시는 네트워크를 통해 클라이언트 요청을 전달하여 네트워크 작업의 모든 세부 사항을 처리

 

  • 로깅 요청(로깅 프록시) => 서비스 개체에 대한 요청 기록을 유지하려는 경우
    • 프록시는 서비스에 전달하기 전에 각 요청을 기록할 수 있다

 

  • 캐싱 요청 결과(캐싱 프록시) => 클라이언트 요청의 결과를 캐시하고, 이 캐시의 수명 주기를 관리해야 하는 경우
    • 프록시는 항상 동일한 결과를 생성하는 반복 요청에 대해 캐싱을 구현할 수 있다
    • 프록시는 요청의 매개변수를 캐시 키로 사용할 수 있다

 

    • 클라이언트가 없을 때 알아서 해제해야 하는 경우 => ex. 무거운 서비스 개체인 경우
      • 프록시는 서비스 개체 또는 해당 결과에 대한 참조를 얻은 클라이언트를 추적할 수 있다. 즉, 프록시는 클라이언트를 통해 클라이언트가 여전히 활성 상태인지 확인할 수 있다. 클라이언트 목록이 비어 있으면 프록시가 서비스 개체를 닫고 기본 시스템 리소스를 해제할 수 있다
      • 프록시는 클라이언트가 서비스 개체를 수정했는지 여부도 추적할 수 있다. 그리고 변경되지 않은 개체는 다른 클라이언트에서 재사용할 수 있다

'책을 읽자 > Design Patterns' 카테고리의 다른 글

Decorator Pattern  (0) 2021.08.18
Template Method Pattern  (0) 2021.08.02
Strategy Pattern  (0) 2021.08.02
Flyweight Pattern  (0) 2021.01.19
Bridge Pattern  (0) 2021.01.19

The Decorator Pattern attaches additional responsibilities to an object dynamically

Decorators provide a flexible alternative to subclassing for extending functionality.

 

Decorator Pattern 은 동일한 타입의 객체를 품고 있는 패턴으로, 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 것 외에 원하는 추가적인 작업을 수행할 수 있다.

 

 

아래는 커피를 주문할 때 주문시스템을 구현하는 방식을 데코레이터 패턴으로 나타낸 것이다.

  • 한 객체를 여러 개의 데코레이터로 감쌀 수 있는데, 이때 decorator 의 super class 는 자신이 감싸고 있는 객체의 super class 와 같다 => 따라서 싸여져 있는 객체(ex. darkroast) 대신 다른 프록시를 넣어도 됨
  • 데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 것 외에 원하는 추가적인 작업을 수행할 수 있다
  • 상속을 통해 형식을 맞춘다 (상속을 통해 행동을 물려받음X)
  • 팩토리/빌터 패턴과 함께 사용한다.

 

 

 

장점

  • 새 하위 클래스를 만들지 않고 개체의 동작을 확장할 수 있다
  • 런타임에 개체에서 책임을 추가하거나 제거할 수 있다
  • 개체를 여러 데코레이터로 래핑하여 여러 동작을 결합할 수 있다
  • 단일 책임 원칙 => 많은 동작 변형을 구현하는 모놀리식 클래스를 여러 개의 작은 클래스로 나눌 수 있다.

단점

  • 래퍼 스택에서 특정 래퍼를 제거하는 것은 어렵다
  • 동작이 데코레이터 스택의 순서에 의존하지 않는 방식으로 데코레이터를 구현하는 것은 어렵다
  • 구성요소를 초기화하는 데 필요한 코드가 복잡해진다
    • java i/o library `InputStream` => InputStream in = LineNumerInputStream(new BufferedInputStream(new FileInputStream(file)))

언제사용하는가?

  • 개체를 사용하는 코드를 손상시키지 않고 런타임에 개체에 추가 동작을 할당할 수 있어야 하는 경우
  •  상속을 사용하여 개체의 동작을 확장하는 것이 어색하거나 불가능할 때
    • final 클래스의 추가 확장이 필요한 경우 - 기존 동작을 재사용하는 유일한 방법은 데코레이터 패턴을 사용하여 클래스를 자체 래퍼로 래핑하는 방법밖에 없다

'책을 읽자 > Design Patterns' 카테고리의 다른 글

Proxy pattern  (0) 2021.08.18
Template Method Pattern  (0) 2021.08.02
Strategy Pattern  (0) 2021.08.02
Flyweight Pattern  (0) 2021.01.19
Bridge Pattern  (0) 2021.01.19

The Template Method defines the steps of an algorithm and allows subclasses to provide the implementation for one or more steps.

 

Template Method 패턴은 실행 순서에 대한 골격을 제공하고, 알고리즘의 특정 단계를 재정의 함으로써 다양화 할 수 있는 패턴이다.

 

 

 

커피와 차를 제공하는 프로세스를 보면 겹치는 부분이 많다. 그리고 순서도 같다.

커피와 차를 만드는 프로세스의 중복을 제거하기 위해서는 추상화가 필요하므로, 아래와 같이 템플릿 메소드 패턴을 사용한다.

 

 

주의할 점은 서브클래스에서 CaffeineBeverage를 만드는 프로세스를 재정의 할 수 없도록 템플릿 메소드를 final로 선언해야 한다.

세부 메소드 중 변하는 알고리즘은 하위 클래스들이 구현할 수 있도록 추상 메소드로 선언한다.

마지막으로 세부 메소드 중 변하지 않는 알고리즘은 상위 클래스가 이를 구현하고 하위 클래스들은 아예 볼 수 없도록 private 메소드로 선언하자.

(+ 전체 알고리즘 골격의 스텝을 나눌 때, 필수적이지 않은 부분은 hook로 빼서 구현한다.)

 

 

템플릿 메소드 패턴 vs 스트래티지 패턴

템플릿 메소드 패턴 스트래티지 패턴
상속을 기반으로 한다. 하위 클래스에서 해당 부분을 확장하여 알고리즘의 일부를 변경할 수 있다.
=> 알고리즘의 순서를 정의하고, 실제 작업 중 일부는 서브클래스에서 구현함으로써 각 단계마다 다른 구현을 사용하면서도 알고리즘 구조를 유지할 수 있다.
구성을 기반으로 한다. 해당 동작에 해당하는 다른 전략을 제공하여 개체 동작의 일부를 변경할 수 있다. 

=> 일련의 알고리즘군을 정의하고, 클라이언트에서는 그 알고리즘들을 서로 바꿔가며 사용할 수 있다.
클래스 수준에서 작동하므로 정적이다. 개체 수준에서 작동하므로 런타임에 동작을 전환할 수 있다.
코드 중복이 거의 없음 객체 구성을 사용하므로 유연하다 & 의존성이 없다

 

 

장점

  • 클라이언트가 전체 알고리즘의 특정 부분만 재정의 => 알고리즘의 다른 부분에 발생하는 변경 사항의 영향을 줄일 수 있다
  • 중복 제거

단점

  • 알고리즘의 제공된 골격이 제한적이다
  • 하위 클래스를 통해 기본 단계 구현을 억제 하여 리스코프 치환 원칙을 위반할 수 있다
  • 하위 클래스에서 코드를 구현하기때문에 의존적일 수 있음

언제사용하는가? 

  • 클라이언트가 알고리즘의 특정 단계만 확장하고 전체 알고리즘이나 해당 구조는 확장하지 않도록 할 때
  • 약간의 차이가 있지만 거의 동일한 알고리즘을 포함하는 여러 클래스가 있는 경우

'책을 읽자 > Design Patterns' 카테고리의 다른 글

Proxy pattern  (0) 2021.08.18
Decorator Pattern  (0) 2021.08.18
Strategy Pattern  (0) 2021.08.02
Flyweight Pattern  (0) 2021.01.19
Bridge Pattern  (0) 2021.01.19

+ Recent posts