요약

수학에서는 연산자의 특징에 따라, 연산 순서가 중요하거나 중요하지 않은 경우가 존재한다.
이는 교환 법칙, 결합 법칙으로 표현되며, 다음과 같이 설명할 수 있다.

  • 교환 법칙: 피연산자의 순서와 관계 없이 동일한 결과를 보증
    • e.g. A + B = B + A
    • 개발 최적화 이론에 적용 (조건문의 Short-circuit Evaluation 등)
    • 아키텍쳐 설계에서 CDC나 이벤트 스트리밍에 활용하기도 함
  • 결합 법칙: 그룹화 하여 처리하되, 그룹간의 연산 순서와 상관 없이 동일한 결과를 보증
    • e.g. A + (B + C) = (A + B) + C
    • 그래픽스의 행렬 연산 최적화 (Matrix Chain Multiplication)
    • 아키텍쳐 설계에서 병렬/배치과 같은 분산 처리시스템에 활용

단순히 수학적 수식으로만 보고 판단하는 것이 아닌, 실제 개발에 어떤 식으로 응용되는지도 자세히 분석하는 것을 목표로 학습하고 있다.

배경

학창시절부터, 수학에 대해 깊게 생각해보거나 몰입해본 경험은 크게 없었던 거 같다.
다만 개발자로 일하고, 여러가지 상황들을 마주하다보니 수학의 필요성에 대해 크게 체감했다.
기초적인 수학 지식부터 차분히 배워가면서, 이를 블로그에 하나씩 정리해 나가고자 한다.

내용

연산자의 우선 순위와 함께 자주 나오는 개념인 교환 법칙과 결합 법칙이다.

연산자의 우선순위는 *, /, +, -와 같이 어떤 연산자를 우선적으로 계산한 것인지 나타낸 것이라면,
교환 법칙과 결합 법칙은 연산 순서가 최종 결과에 영향을 주는 지 정리한 내용이다.

교환 법칙

정의

  • 연산을 수행할 때, 피연산자의 순서를 바꾸어도 그 결과는 동일
  • e.g. A + B = B + A
  • e.g. A * B = B * A
  • 적용되는 연산
    • 더하기
    • 곱하기
    • 벡터 내적 연산
    • 부울 대수 AND, OR
    • 행렬 덧셈
    • 집합 (합집합, 교집합)

수학적 정의는 위와 동일하다.
다만 이를 개발에서는 어떻게 적용하는 지가 꽤나 흥미롭다.

예를 들어 은행 시스템을 개발한다고 가정해보자
여기서 입금 트랜잭션의 경우, 어떤 순서로 들어오더라도 최종적으로 같은 결과를 도출하기에 교환 법칙이 성립한다고 말할 수 있다.
그렇다면 입금 트랜잭션은 교환 법칙이 성립하기에, 전송 단에서 TCP와 같은 순서 보증 또는 애플리케이션 내부의 트랜잭션 순서 보증과 같은 오버헤드를 줄일 수 있을 것이다.

이와 반대로 교환 법칙이 성립되지 않는 출금과 같은 로직의 경우 순서가 중요하기에, 이러한 순서를 보장해야하는 오버헤드가 반드시 시스템 설계상 필요할 것이다.
이런 설계는 순서 보증이 필요한 출금 트랜잭션만 선택적으로 보증하고, 교환 법칙이 성립하는 입금 트랜잭션에서는 보증하지 않으므로 전반적인 시스템 성능 및 처리량 향상이 기여할 수 있을 것이다.

분산 데이터베이스 환경에서도, 이러한 교환 법칙은 빛을 발한다.
분산된 노드가 각각 입금에 대해서 처리하고, 나중에 최종적으로 하나의 결과로 병합함으로 일관성을 유지할 수 있기 때문이다.
바로 DB에 요청을 보내 반영 하는 것이 아닌, 메모리 상 저장해두고 있다가 추후에 비동기적으로 작업함으로 빠른 처리 작업과 IO 작업을 최소화할 수 있을 것이다.

결합 법칙

  • 연산을 수행할 때, 그룹을 묶는 순서와 관계 없이 결과는 동일
  • e.g. (A + B) + C = A + (B + C)
  • e.g. (A * B) * C = A * (B * C)
  • 적용되는 연산
    • 더하기
    • 곱하기
    • 부울 대수 AND, OR
    • 행렬 곱셈

개발에서는 다음과 같이 적용한다.

전체 데이터를 여러가지의 스트림(그룹)으로 나누어서 처리함으로, 병렬처리를 효율적으로 적용하기 위한 판단 기준으로 삼을 수 있다.
Database 샤딩의 경우, 데이터를 각각의 노드 별로 나누어서 처리함으로 전체 시스템의 처리량을 늘릴 수 있고 최종적으로 종합된 결과는 동일이 보장된다.

행렬 연산이 주가 되는, 3D 그래픽스와 게임에서는 행렬 곱셈 시 결합 법칙이 적용됨을 이용하여 연산 최적화 (Matrix Chain Multiplication)를 진행한다.

교환 법칙 + 결합 법칙

이 두가지 법칙을 모두 달성한 경우, 시스템에서는 효율적으로 확장이 가능할 것이다.

  • 교환 법칙으로 인한 데이터 순서 보증 X
  • 서버의 수평적 확장을 통한 처리량 증가

우리는 프로그래밍을 하다보면, 한번쯤은 반복문을 사용하게 되는 경우가 있다.

이 때 반복문에서 사용할 변수를 선언하게 되는데, 이 경우 구체적으로 어디에 변수를 선언하면 좋을지 고민하게 된다.

 

//  1. 반복문 안에 선언하는 경우
public void foo() {
    while () {
        int i = 10;
    }
}
//  2. 반복문 밖에 선언하는 경우
public void foo() {
    int i = 10;
    while () {
    }
}

위의 두 경우를 가지고, 자바에서 각 상황에서 어떤 것이 구조에 있어서 장점을 가지고, 성능에 어떤 영향을 주는지 살펴보고자 한다.

 

1️⃣ 구조적 장점

우선 구조적 장점으로는 반복문 안에 선언하는 것이 가장 바람직하다.

이유로는 지역변수는 자동 변수이기 때문이다.

지역변수는 stack이라는 메모리 공간에 저장되기 때문에, 스코프 종료와 함께 자동으로 회수된다.

이 때문에 지역변수는 자동변수라는 이름으로 함께 불리기도 한다.

지역변수의 스코프를 제한하면, 메모리를 효율적으로 사용할 수 있으며 다른 스코프에서 참조하여 값이 변하는 걱정도 덜 수 있다.

예를 들어 반복문 밖에 선언한 i 변수는 반복문이 한 사이클을 돌더라도 해당 값이 남아있기 때문에, 이를 참조하는 다른 코드에서 문제가 발생할 여지가 있다.

반대로 반복문 안에 선언한 i 변수는 반복문이 돌 때마다 해당 값을 새로 선언하고, 초기화하기 때문에 잘못된 값이 들어갈 염려가 없다.

 

2️⃣ 성능

이 부분은 감히 어떤 방식이 더 좋은 지 말하기 어려운데, 그래도 상황별로 나눠서 생각해볼 수는 있겠다.

 

Stack에 저장되는 변수

stack에 저장될 지역변수라면, 크게 성능 차이가 날만한 부분은 딱히 없을 것으로 생각된다.

굳이 꼽자면, 지역변수가 반복문마다 선언되고 초기화 되는 부분이 반복된다는 부분 일 것이다.

이건 컴파일러 단에서 최적화를 해줘서 성능에 그렇게 큰 영향을 주지 못할 것 같다는 생각이 든다.

(구체적으로 바이트 코드를 까보고, 어셈블리를 봐야 알겠지만은…)

그렇기에 stack에 저장되는 형식의 변수라면 두 방식 역시 성능적인 차이가 크게 의미있을 것 같진 않다.

 

Heap에 저장되는 변수

다만 heap에 저장될 변수라면 이야기는 다르다.

이 때 위의 코드를 다음과 같이 수정하고 정수형 캐쉬는 없다고 가정한다.

 

Java Integer Cache - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org

//  1. 반복문 안에 선언하는 경우
public void foo() {
    while () {
        Integer i = 10;
    }
}
//  2. 반복문 밖에 선언하는 경우
public void foo() {
    Integer i = 10;
    while () {
        Integer i2 = i;
    }
}

 

우선 첫 번째 경우는 매번 반복문에서 새로운 정수형 객체를 생성하는 방식이다 (new Integer())

두 번째 경우는 이미 생성된 정수형 객체를 가르키는 새로운 정수형 변수를 선언하고, 초기화한 것이다.

따라서 엄밀히 말하면 첫 번째 경우와는 다르게 new Integer()를 사용하지 않았기에, heap에 새로운 메모리가 생성되지 않는다.

이 말은 GC의 대상이 되지 않기에, 크게 봤을 때 GC로 인한 성능 저하가 첫 번째에 비해 적다는 뜻이다.

자바에서는 문자열을 생성하는 과정에서도 new 키워드로 생성하냐, 아니면 리터럴로 생성하냐에 따라서 GC 퍼포먼스가 달라질 정도로 주의를 요한다.

과연 적지 않은 수의 반복문으로 객체를 생성하면 GC 성능에 영향이 적다고 말 할 수 있을까?

 

결론

반복문 내부에 생성되는 변수의 타입과 변수가 레퍼런스 하는 메모리 공간에 따라 권장하는 방법이 달라질 수 있다.

일반적인 원시형 변수를 사용하는 경우에는 코드 가독성 및 코드 구조를 위해 반복문 내부에 선언하는 것을 권장하고,
객체형 변수를 사용하는 경우에는 GC 성능에 영향을 줄 수 있기 때문에, 반복문 밖에서 선언하는 것을 권장할 수 있겠다.

 

참고자료

 

OKKY - 이 방식이 무리가 많이 가는 코딩방식일까요?

언어는 자바구요for 문을 돌릴때마다 String 객체를 생성해서 할당하는 코딩법이 너무나도 많이 보입니다.String 뿐만 아니라 다른 객체들도 종종 보입니다. 예를들면for(int i=0; i<length;; i++) {   String

okky.kr

PS. 자동 변수가 선언되는 것이 성능에 주는 영향이 적은 지 알려주는 글이 있어서 가져온다.

우선 C언어 같은 경우에는, 일반적인 타입은 이미 해당 크기가 정해져있기 때문에 컴파일 때 얼마만큼 스택에 저장해야할 지 알 수 있다.

따라서 미리 해당 크기만큼 stack에 저장하는 것으로 컴파일러가 최적화 한다면, 그만큼 선언에 필요한 cpu 사이클이 줄어드는 셈이다.

https://stackoverflow.com/questions/7959573/declaring-variables-inside-loops-good-practice-or-bad-practice

 

Declaring variables inside loops, good practice or bad practice?

Question #1: Is declaring a variable inside a loop a good practice or bad practice? I've read the other threads about whether or not there is a performance issue (most said no), and that you should

stackoverflow.com

 

자료구조를 공부하다 보니 문뜩 드는 생각이 있다.

stack을 공부하다 보면 수식 표기법인 전위(Prefix), 중위(Infix), 후위(Postfix)의 개념을 배우고, 이를 응용해서 계산기를 만든다.

근데 막상 계산기를 만드는 대부분의 예제는 후위 표기법으로 되어있는 걸 알 수 있다.

전위나 후위 둘 다 괄호도 없고, 연산자 우선순위를 따로 신경 쓸 필요가 없는데 왜 후위표기법만 사용해서 쓰는 건지 궁금해서 찾아봤다. (추가로 이유는 자세히 안 적혀 있어서, 나름대로 생각을 해봤다.)

 

 


 

우선 첫 번째 이유로는 후위 표기법이 다른 표기법에 비해 연산 과정이 쉽기 때문이란다.

내 생각으로 간단한 예제를 만들어봤다.

# 1 + (2 * 3)
# Prefix
+1*23

전위 표기법의 경우 위와 같은 상황에서 우선 stack으로 계산하려면 변수에 +와 1을 올려둔 상태 일 것이다.

하지만 뒤에 2 \* 3을 계산 해야지, 결과를 얻을 수 있는 상황이다.
그렇기에 전위 표기법은 추가적인 연산이 필요한 상황을 고려해야 할 필요가 있다.

 

 

# 1 + (2 * 3)
# Postfix
123*+

반면에 후위 표기법은 간단하게 왼쪽에서 오른쪽으로 쭉 읽으면서 stack에 저장하고, 연산자를 만나면 꺼내서 연산하면 된다.
그렇기에 전위 표기법에 비해 훨씬 간단하게 구현할 수 있기 때문에, 이런 차이점이 생긴게 아닐까 싶다.

 

두 번째 이유는 후위 표기법이 전위 표기법에 비해 적은 괄호를 사용하기 때문이란다.
근데 이건 후위나 전위 둘다 괄호를 사용하지 않는 게 특징인데, 왜 있는 건진 잘 모르겠다..
(혹시 아시는 분은 댓글 달아주시면 감사하겠습니다.)

 


 

이러한 이유로 계산기를 만들 때, 전위 표기법보다는 후위 표기법이 조금 더 우위를 가진다는 것 같다.
그럼에도 전위 표기법과 후위 표기법 역시 각각 사용되는 분야가 조금씩 다르고, 그 분야에서 장점이 있다고 한다.

 

전위 표기법

  • 주로 단일 연산자 (Unary Operator)에서 사용되며, 다른 표기법에 의해 연산이 빠름
  • LISP이나 Clojure 등의 프로그래밍 언어에서 사용됨

 

후위 표기법

  • 주로 컴파일러에 의한 코드 생성에서 사용됨
  • 다른 표기법에 비해 수식 평가(Evaluate)에 수월함

 

더 깊게 찾아보진 않아서, 구체적인 사항이 더 있을거 같다는 생각이 든다.
나중에라도 다시 궁금해지면, 글 내용에 더 추가해봐야겠다.


출처

Paste Into File

  • 설치
  • 1) 초코라티 이용해서 설치하기 (require Chocolately)
  • 2) 위 링크에 주어진 설치파일을 이용해서 직접 설치하기

 

1) 파일을 저장하고 싶은 폴더에서 오른쪽 클릭을 한 후 컨텍스트 메뉴에 있는 Paste Into File 클릭사용법

2) 파일 이름과 확장자, 경로를 설정한 후에 save 버튼을 누름

 

'Tips' 카테고리의 다른 글

[MD] 뱃지 이미지  (0) 2022.08.08

1) For문 사용

2) Stream 사용

3) java.util.Arrays.deepToString() 사용

 

 

- 참고

- https://hianna.tistory.com/511

JPA에서 Entity를 생성하기 위해서는 기본키(PK)로 사용할 @Id 어노테이션을 사용한다.
이때 뒤에 PK를 어떻게 생성할 것인지, 정의할 수 있는 옵션이 존재하는데 @GeneratedValue이다.

이 방법은 크게 5가지가 존재한다.

  1. 직접할당
  2. AUTO
  3. SEQUENCE
  4. IDENTITY
  5. TABLE

1) 직접할당

 

어플리케이션 내에서 직접 값을 생성하여, 할당하는 방법이다.

 

2) AUTO

 

사용하는 DB에 따라 다르게 동작하며, 가장 적절한 생성방식을 골라준다.

 

3) SEQUENCE

 

SQL:2003에 정의된 Sequnce Generator를 따른다면 사용할 수 있는 방식이라고 한다.
기본적으로 값 생성 및 처리가 빨라, 우선적으로 고려해봐야할 옵션이지만, Oracle, SQL Server2012, PostgreSQL, DB2, HSQLDB에서만 지원한다고 한다.
만일 지원하지 않는 DB라면, TABLE 생성으로 변경한다.
(SEQUENCE 지원 DB목록 정리)

 

4) IDENTITY

 

데이터베이스의 auto_increment를 사용해서 기본 값을 생성하는 방식이다.
편하게 쓸 수 있지만, 단점들로는 영속성 컨텍스트에 있는 객체들의 PK를 구할 수 없다는 점, JPA의 Batch Insert가 비활성화 된다는 점이있다. 참고

 

5) TABLE

테이블을 별도로 생성해서, 엔티티 생성시마다 기본 값을 갱신하는 방식이다.
제약사항이 없어서, 대부분의 DBMS에서 사용이 가능하지만 다른 방식들에 비해 성능이 최악이다.

 


처음 JPA를 사용한 프로젝트에서 아무것도 모르고 단순히 IDENTITY 옵션을 사용하다가, 영속 컨텍스트에서 PK를 못 구해서 많이 해맸던 기억이 있다.
아예 모르는 상황이다 보니, 궁여지책으로 UUID를 대체키로 넣어서, 매번 객체 생성시마다 직접 할당을 해줬었다....

(떠올리면 너무 부끄럽다 ㅠㅠ)

 

나중에 이런 구조가 잘못된 걸 알고 다시 고치려고 하니, 작업량도 만만치 않고, 다른 사람들과의 팀프로젝트라 무슨 문제가 발생할지 몰라 그대로 방치해버리게 되었다.

다른 사람들도 이런 마음 아픈 일을 겪지 않고, 스스로도 기억하기 위해 글을 써본다.


참고자료

Vue 공부를 하다보니, 그전에는 가볍게만 썼던 JS를 더 많이 보고 쓰게 되었다.

그 와중에 배열과 객체의 차이,

더 정확하게는 객체의 프로퍼티를 이용하는 것과 배열을 이용하는 것이 무엇이 다른지 궁금해졌다.

 


대부분의 객체지향 언어에서도 배열과 객체는 존재하고 그 차이는 명확하다.

배열은 주어진 자료형을 정해진 크기대로 넣어두고 인덱스를 이용해서 접근하는 방식으로 구성된다.

 

let arr = ['Javascript', 'is', 'Fun!'];

for (let i = 0; i < arr.length; i++) {
	console.log(i + ' : ' + arr[i] + '\n');
}

 

 

객체는 자바스크립트에서 리터럴 값을 제외한 모든 것이라고 할 수 있다. (심지어 함수 까지도!)

그리고 객체는 내부적으로 0개 이상의 데이터로 구성되어있는데, 이 때 데이터를 프로퍼티라고 한다.

프로퍼티는 키와 값으로 구성되어, 키를 통해서 접근할 수 있다.

(만약 프로퍼티가 문자열이 아니라면, 문자열로 변환이 된다.)

 

let obj = {
    name : 'js',
    fun : true
};

console.log(obj.name + ' ' + (obj['fun'] ? 'is fun!' : 'is not fun!'));

 

let arr = [1, 2, 3];
let obj = {
    1 : 1,
    2 : 2,
    3 : 3
};

console.log(`arr(${arr.length})`);
for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]);
}

// cause undefined error
// console.log(`\nobj(${obj.length})`);
console.log(`\nobj`);
for (let i = 1; i <= 3; i++) {
    console.log(obj[i.toString()]);
}

객체와 배열은 3가지로 구분이 가능하다.

 

  1. 값의 참조 방식
    • 값의 참조는 배열의 경우 인덱스로 참조하기 때문에 number 가능하며, 객체는 문자열로만 가능하다.
  2. 값의 순서
    • 배열은 연속되는 값들의 집합이기에 순서가 존재하지만, 객체에 순서는 존재하지 않다.
  3. length 프로퍼티
    • 배열은 length 프로퍼티에 접근이 가능하지만, 객체는 length 프로퍼티에 접근이 불가능하다.

 


참고 : 모던 자바스크립트 Deep Dive

참고1 : Mozila MDN

'Language > JavaScript' 카테고리의 다른 글

JS에서 Template literals가 동작하지 않을 때  (0) 2022.07.29

컴퓨터 공학에서는 가장 필수로 가르치는 몇몇 과목이 있는데, 대표적으로 자료구조와 알고리즘이 있다.

특히 정렬 알고리즘은 자료구조를 배우면서도 종종 언급되곤 하는데, 그만큼 기초적이면서 활용도가 많다는 뜻이다.

 

분명 학교 과목으로 배웠지만, 산업기사 자격증을 따고 나서 다 잊어버렸다...

코딩 테스트를 위해 공부할 겸, 스스로 복기하기 위해 글을 정리하게 되었다.

 

(앞으로 꾸준히 포스팅 있길)

 

 


정렬되지 않은 주어진 데이터가 있다고 하자.

이럴 때 가장 단순하게 이를 정렬하는 방법에는 어떤 것이 있을까?

 

array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]

 

맨 처음에 프로그램을 배울 때 당시의 내 생각으로는 모든 수를 비교하여, 가장 작은 순서대로 정렬하는 것을 떠올릴거 같다.

이미 첫 번째 원소로 7이 있으므로, 이 원소를 기준으로 더 작은 값만을 선택해 나가면 된다.

 

두 번째 원소인 5는 7보다 작으므로, 최소값의 기준을 5로 변경한다.

 

세 번째 원소인 9는 최소값인 5보다 크므로, 넘어간다.

 

네 번째 원소인 0은 최소값인 5보다 작으므로, 최소값의 기준을 0으로 변경한다.

이후 이와 같은 식으로 반복하다가, 마지막 원소까지 비교가 끝나면 첫 번째 원소와 최소 값을 가진 원소를 서로 바꾼다.

 

 

위 이미지를 코드로 옮겨보면 다음과 같다.

# 가장 첫번째 자리의 수로는 가장 작은 수가 와야하므로
# arr[min]과 arr[i]를 비교하여 arr[i]가 arr[min]보다 작을 경우
# min = i로 값을 변경

array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]

for i in range(1, len(array)-1):
    if array[min] > array[i]:
        min = i

if min != 0:
    array[0], array[min] = array[min], array[0]
    
print(array)

# [0, 5, 9, 7, 3, 1, 6, 2, 4, 8]

 

이제 이걸 배열 마지막까지 반복하면, 모든 수가 정렬될 것이다.

위와 같은 형식의 정렬 방법을 선택 정렬이라고 한다.

 

전체를 정렬하는 코드는 아래와 같다.

array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]

for i in range(len(array)):
    min = i
    for j in range(i + 1, len(array)):
        if array[min] > array[j]:
            min = j
    array[i], array[min] = array[min], array[i]

print(array)

# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

 

떠올리기 쉬운만큼 단순한 면이 있고, 그만큼 비효율적인 부분이 크다.

우선 시간복잡도로는 for 반복문이 2중으로 중첩되어 있으므로 O(n^2)가 될 것이다.

이는 주어진 입력의 크기가 커질수록 연산횟수가 커진다는 뜻이다.

 

단순히 10 크기의 입력이 주어졌을 때는 100회의 연산을 해야하지만,

100 크기의 입력이 주어졌을 때는 10,000회의 연산을 해야한다.

 

다른 정렬 알고리즘에 비해 비효율적인 부분이 크지만,

그래도 작은 수의 입력과 빠르게 코드를 작성해야할 때는 유용하게 사용 될 수 있다고 하니

몸에 익혀두는 것이 좋을거 같다.

 

 

참고 : 이것이 취업을 위한 코딩테스트다 with python

+ Recent posts