웹 개발 초기단계에서 효율적인 애플리케이션 구조를 설계하는 것은 매우 중요한 일이다. 이러한 구조를 정의하는데 있어서 MVC(Model-View-Controller) 패턴과 Flux 패턴은 널리 사용되는 설계 패턴이다. 이 글에서는 두 패턴의 개념, 차이점, 그리고 사용 사례를 비교하여 쉽게 이해할 수 있도록 설명해 보겠다.

 

MVC 패턴이란?

MVC는 애플리케이션을 세 가지 역할로 나누는 설계 패턴으로, Model, View, Controller로 구성된다. 역할을 분리하여 코드의 재사용성과 유지보수성을 높이는 데 중점을 둔다.

1. Model(모델)

애플리케이션의 데이터와 비즈니스 로직을 관리. 데이터베이스와 상호작용하여 데이터를 가져오거나 저장하고, 비즈니스 규칙을 적용한다. View나 Controller에 데이터를 제공한다.

2. View(뷰)

사용자가 보는 UI를 담당한다. Controller로부터 전달받은 데이터를 화면에 렌더링한다. HTML, CSS, JavaScript로 구현되며, 데이터 표시가 주된 역할이다.

3. Controller(컨트롤러)

Model과 View 사이를 연결하는 중계 역할을 한다. 사용자의 요청(ex. 버튼 클릭)을 처리하고, 적절한 Model을 호출하거나, View를 갱신한다.

MVC의 동작 흐름을 살펴보면 다음과 같다. 먼저 사용자가 입력(ex. URL 요청)을 전송하면, Controller가 요청을 처리하고 필요한 데이터를 Model에서 가져온다. View는 Controller로부터 데이터를 받아 사용자에게 출력한다. MVC 패턴을 사용하는 데 있어 장단점이 존재하는데, 역할 분리가 명확해 유지보수가 쉽고 코드 재사용성이 높다는 장점이 있지만 복잡한 애플리케이션에서는 데이터 흐름이 복잡해질 수 있다.

 

Flux 패턴이란?

Flux는 Facebook이 React 애플리케이션을 위해 설계한 아키텍쳐. 패턴이다. Flux는 단방향 데이터 흐름(Unidirectional Data Flow)을 중심으로 상태(state)관리를 단순화한다. Flux의 구성 요소에는 네 가지가 있는데, 조금 더 자세히 살펴보면 아래와 같다.

1. Action

상태 변경의 의도를 나타낸다. 예를 들어, 사용자가 버튼을 클릭하면 "ADD_TODO"라는 Action이 생성된다.

const addTodoAction = {
	type: 'ADD_TODO',
    payload: { text: 'Learn Flux' }
};

2. Dispatcher

Action을 Store로 전달하는 중앙 허브 역할을 한다. 등록된 Store에 Action을 브로드캐스트 한다.

dispatcher.dispatch(addTodoAction);

3. Store

애플리케이션의 상태를 저장하고 관리한다. Dispatcher로부터 전달받은 Action을 처리하여 상태를 변경하고, View에 변경 사실을 알린다.

class TodoStore {
	constructor() {
    	this.todos = [];
    }
    
    handleAction(action) {
    	if (action.type === 'ADD_TODO') {
        	this.todos.push(action.payload.text);
            this.emitChange();
        }
    }
    
    emitChange() {
    	// View에 상태 변경 알림
    }
}

4. View

Store에서 상태를 구독하여 UI를 업데이트한다. React 컴포넌트로 구현되는 경우가 많다.

const TodoView = () => {
	const todos = TodoStore.getTodos();
    return (
    	<ul>
        	{todos.map((todo, index) => (
            	<li key={index}>{todo}</li>
            ))}
        </ul>
    );
};

Flux의 동작 흐름은 1~4 순서로, 사용자가 입력(ex. 버튼 클릭)을 통해 Action을 생성하면, Dispatcher가 Action을 Store에 전달하고, Store는 상태를 변경하고 View에 알린다. 그러면 마지막으로 View는 새로운 상태를 렌더링한다. Flux 패턴도 역시 장단점이 존재하는데, 단방향 데이터 흐름으로 인해 데이터 변경의 추적과 디버깅이 쉽다는 장점이 존재하지만, 구성 요소가 많아 초기 학습이 어려울 수 있다는 단점이 있다.

728x90

다차원 리스트의 정렬이나 딕셔너리의 value 기준 정렬 등에 람다 함수를 사용하게 될 일이 많은데, 알음알음 남의 코드 보면서 알게 되어 아무생각 없이 람다 함수를 사용하다가, 한번 개념을 정리해 보는 것이 좋을듯 하여 남겨본다.

 

람다 함수란?

파이썬에서 람다 함수는 lambda 키워드를 사용하여 간단하게 익명 함수를 만드는 방법이다. 주로 한 줄로 표현할 수 있는 간단한 함수에 유용하게 사용된다. 내가 처음 람다 함수를 이해한 방식은 수학에서 집합을 나타낼 때, S = { x | ... } 로 시작하는 문법과 거의 동일하다고 이해하고 사용했다. 람다 함수는 일반 함수처럼 이름을 가지지 않고, 선언과 동시에 사용된다. 람다 함수의 기본 문법을 보면 다음과 같다.

# 람다 함수 기본 문법
lambda arg1, arg2: return_value

# 두 숫자를 더하는 람다 함수의 예시
add = lambda x, y: x + y
print(add(5, 3)) # 출력: 8

 

람다 함수로 리스트 정렬하기

람다 함수는 리스트를 특정 조건에 따라 정렬할 때 매우 유용하다. 예를 들어, 다중 리스트를 특정 인덱스 기준으로 정렬하려고 할 때 사용할 수 있다. 예를 들어 학생들의 이름과 점수 리스트가 있을 때, 이 리스트를 점수 기준으로 정렬하려면 람다 함수를 활용할 수 있다.

students = [("철수", 85), ("민수", 75), ("범수", 90)]

# 점수를 기준으로 오름차순 정렬
sorted_students = sorted(students, key=lambda x: x[1])
print(sorted_students)
# 출력: [("민수", 75), ("철수", 85), ("범수", 90)]

# 점수를 기준으로 내림차순 정렬
sorted_students_reverse = sorted(students, key=lambda x: x[1], reverse=True)
print(sorted_students_reverse)
# 출력: [("범수", 90), ("철수", 85), ("민수", 75)]

여기서 key=lambda x: x[1] 부분은 x[1] 값인 점수 기준으로 정렬하라는 의미이다.

 

딕셔너리를 value 기준으로 정렬하기

딕셔너리는 for 문에 넣어 반복문을 돌리면 key를 기준으로 돌게 된다. 그런데 key가 아니라 value를 기준으로 무언가 하고 싶을 때가 많은데, value를 기준으로 정렬할 때에도 람다 함수를 사용하면 편리하다.

scores = {"철수": 85, "민수": 75, "범수": 90}

# value 기준으로 정렬하기
sorted_scores = dict(sorted(scores.items(), key=lambda x: x[1]))
print(sorted_scores)
# 출력: {'민수': 75, '철수': 85, '범수': 90}

여기서 sorted(scores.items(), key=lambda x: x[1]) 부분은 딕셔너리의 (key, value) pair를 리스트로 변환한 후, value를 기준으로 정렬한 것이다. 마지막에 dict()로 감싸서 다시 딕셔너리 형태로 변환한 것이다.

 

리스트 필터링하기

람다 함수는 filter()와 함께 특정 조건을 만족하는 요소만 걸러낼 때도 유용하다. 예를 들어 숫자 리스트에서 짝수만 걸러내고 싶다면 다음과 같이 할 수 있다.

numbers = [1, 2, 3, 4, 5, 6]

# 짝수만 필터링하기
even_numbers = list(filter(lambda x: x%2 == 0, numbers))
print(even_numbers)
# 출력: [2, 4, 6]

 

마무리

람다 함수는 파이썬에서 간단한 함수를 작성할 때 매우 유용한 도구이다. 리스트나 딕셔너리의 특정 기준에 따라 정렬하거나, 특정 조건에 맞는 요소만 필터링할 때 간결하게 사용할 수 있다.

728x90

'🔨 개발 > 📚 개발지식모음집' 카테고리의 다른 글

YAML 파일이란?  (1) 2024.09.13
20240903 requirements.txt 문제  (0) 2024.09.03
20240808  (0) 2024.08.08
20240807  (0) 2024.08.07
20240806  (0) 2024.08.06

예전에 풀다 틀린 문제 하나 클리어

1016 - 제곱ㄴㄴ수

https://www.acmicpc.net/problem/1016

 

내 해답

더보기

에라토스테네스고 뭐시기고 필요없다. set() 자료형으로 쉽게 해결.

주어진 수 범위에서 제곱수로 나누어 떨어지는 수의 개수를 뺐다. 주어진 수 범위를 반복문으로 돌면 시간초과.

주어진 예제 입력 외에도 최소/최대 조합인 [1,000,000,000,000, 1,000,001,000,000] 범위로 테스트 해보기. [4, 4] 범위도 0 나오는지 잘 확인해보기

min_num, max_num = map(int, input().split(" "))
# 2 부터 (max의 제곱근 정수부분) 까지 반복문을 돌려서 검사하면?
max_sqrt = int(max_num**0.5)
# min_num보다 크거나 같고 max_num보다 작거나 같은 수 중에서
# 제곱수로 나누어 떨어지는 수를 리스트에 집어넣고 set으로 변환
num_list = []
for a in range(2, max_sqrt+1):
	if a != 1:
		square_a = a*a
		Q_min = min_num//square_a
		Q_max = max_num//square_a
		
		for b in range(max(Q_min,1), Q_max+1):
			if square_a*b >= min_num and square_a*b <= max_num:
				num_list.append(square_a*b)

num_set = set(num_list)
print(max_num-min_num+1-len(num_set))
728x90

'🔨 개발 > ✏️ Algorithm' 카테고리의 다른 글

2156 포도주 시식 파이썬  (2) 2024.10.20

간만에 녹슨 뇌를 깨울 겸 쉬운 문제부터 시작.

2156 - 포도주 시식

https://www.acmicpc.net/problem/2156

 

내 해답

더보기

기초적인 DP 문제이니 점화식을 잘 짜보자.

지금까지 마셔왔던 최대 양과 잔에 든 양을 바탕으로 n번째 잔에서 마실 수 있는 최대 양을 계산하는 것이 관건. 어떤 것을 고르고 어떤 것을 스킵할지에 주목. 반복문 초기에 0, 1, 2 케이스까지 실수없이 계획 세우기.

n = int(input())
wine_list = []

for a in range(n):
	wine_list.append(int(input()))

max_list = [0]*n

'''
n 번째 max_list 값은
1. max_list[n-1] -> n을 skip
2. n + max_list[n-2] -> n-1을 skip
3. n + n-1 + max_list[n-3] -> n-2를 skip
1,2,3 중 max값을 가져간다

itr==2 인 경우는?
1. max_list[itr-1] -> 2를 skip
2. wine_list[2]+wine_list[0] -> 1을 skip
3. wine_list[2]+wine_list[1] -> 0을 skip
'''

for itr in range(n):
	if itr == 0:
		max_list[itr] = wine_list[itr]
	elif itr == 1:
		max_list[itr] = max_list[itr-1] + wine_list[itr]
	elif itr == 2:
		max_list[itr] = max(max_list[itr-1], wine_list[itr]+wine_list[itr-2], wine_list[itr]+wine_list[itr-1])
	else:
		max_list[itr] = max(max_list[itr-1], wine_list[itr] + max_list[itr-2], wine_list[itr]+wine_list[itr-1]+max_list[itr-3])

print(max_list[n-1])

 

728x90

'🔨 개발 > ✏️ Algorithm' 카테고리의 다른 글

1016 제곱ㄴㄴ수 파이썬  (0) 2024.10.20

카카오 Adfit으로 블로그에 광고를 달고 나서 대략 한 달 정도의 시간이 지난듯 하다. 광고는 블로그를 지속하게 해 주는 소소한 원동력이 되기 때문에 아예 광고 없이 가겠다는 생각은 진즉에 포기했다. 그럼에도 광고로 인해 본문의 가독성이 떨어지고, 종종 다른 탭이나 다른 창으로 갔다오면 전체화면에 뜨는 광고는 읽는 사람으로 하여금 짜증을 유발하기 때문에 광고의 배치 위치 또한 신중히 결정하기로 했다.

티스토리 관리 페이지에서 광고 위치를 설정할 수 있다.

우선 광고 위치는 본문하단, 그리고 사이드바 두 종류를 넣고 있다. 사이드바의 경우도 블로그의 카테고리 아래에 보이도록 설정해서 최대한 눈에 덜 거슬리도록 배치했다. 물론 그만큼 수익은 덜 올라가겠지만, 내 블로그 수익이 그걸로 몇만원씩 왔다갔다 하지도 않고 어차피 십원단위니까 괜찮다고 생각한다.

그럼 지난 한 달간 광고 수익은 얼마나 들어왔을까? 수익은 카카오 애드핏 웹사이트나 티스토리 관리 페이지에서 확인할 수 있다. 

카카오 애드핏 홈페이지에서 확인한 최근 30일 수익

그래프가 들쭉날쭉 뭔가 많아 보이지만... 가장 높게 치솟은 9월23일이 56원이고 나머지는 뭐 2원, 7원 이정도다ㅋㅋㅋㅋㅋ 최근 30일 수익은 75원으로 확정! 하지만 이걸로 귀여운 나의 광고수익은 이제 시작되었을 뿐! 앞으로 힘내서 양질의 글을 올려봐야겠다. 최근 30일 수익이 750원을 돌파하면 다시 올려봐야겠다.

 

 

728x90

'😃 신변잡기 > 👨‍💻 블로그' 카테고리의 다른 글

블로그에 코드 하이라이팅 적용하기  (6) 2024.08.11
블로그 오픈~  (0) 2024.08.03

AI에 대해 이리저리 연구하다 보면 .safetensors 파일을 수도 없이 사용하게 되는데, 보안이 유지되는 회사 내부에서는 이미 학습된 딥러닝 모델을 huggingface로부터 가져오는 요청이 나가지 않거나, 또는 민감한 회사의 정보 자체를 클라우드 서비스에서 이용하기 꺼려하는 환경에 있다면 .safetensors 파일로 모델을 로컬에 저장해서 필요한 작업을 하게 된다. 나역시 .safetensors 형식으로 된 stable diffusion 모델을 열어서 ComfyUI나 webui 등의 오픈소스 도구를 사용하지 않고 모델로부터 이미지를 출력해보고 싶은 욕구가 생겼다. 바닥부터 한번 다져가보자.

 

.safetensors 파일이란

.safetensors 파일은 머신러닝 모델의 가중치(weight) 데이터를 안전하고 빠르게 저장하고 불러오기 위한 파일 형식이다. 이전의 .ckpt 형식에 비해 보안에 중점을 두고 있다고 말하는데, safetensors는 텍스트 기반 저장 형식과 달리 저장된 파일이 손상되거나 임의의 코드가 포함될 가능성을 줄여준다. .safetensors 파일의 구조는 바이너리 포맷으로 설계되어 있으며, 주요 목적은 머신러닝 모델의 가중치 데이터를 안전하게 저장하고 빠르게 불러오는 것이다. 이 형식은 특히 pickle의 취약성을 피하기 위해 설계되었으며, 모델 구조나 하이퍼파라미터와 같은 메타데이터를 포함하지 않고, 가중치 데이터만을 안전하게 저장한다.

.safetensors 파일의 세부 구조는 크게 헤더와 데이터 블록으로 구성된다. 헤더는 JSON형식으로 된 각 텐서의 메타데이터 정보이고, 데이터 블록은 실제 가중치 값들이 모여있는 바이너리 형태라고 보면 된다.

예시 헤더
{
  "weight_1": {"dtype": "float32", "shape": [128, 256], "offsets": [0, 131072]},
  "bias_1": {"dtype": "float32", "shape": [128], "offsets": [131072, 131584]},
  ...
}

헤더에는 텐서의 이름, 데이터 유형, 모양(크기), 데이터의 시작 위치와 크기 등이 기록된다. JSON 포맷을 보면, "dtype"은 데이터 타입(ex. float32, int64 등), "shape"는 텐서의 크기를 정의하는 배열, "offsets"는 텐서 데이터의 바이너리 블록에서의 시작과 끝 위치를 나타내는 오프셋 값이 된다. 예시 헤더에서 float32 데이터 타입의 가중치가 [128, 256] 차원으로 존재하기 때문에 4(byte)x128x256 =. 31,072로 실제 바이너리 데이터의 사이즈와 일치하는 것을 볼 수 있다.

 

python에서 .safetensors 파일 열어서 확인하기

Python의 safetensors 라이브러리를 이용해서 .safetensors 파일의 구조를 살펴볼 수 있다. 다음은 .safetensors 파일을 열고 내부 텐서의 메타데이터와 데이터를 확인하는 예시이다.

from safetensors import safe_open

# safetensors 파일 열기
safetensors_file = "path_to_model.safetensors"
with safe_open(safetensors_file, framework="pt") as f:
    # 파일 내 텐서 이름 확인
    tensor_names = f.keys()
    print("텐서 목록:", tensor_names)
    
    # 각 텐서의 메타데이터 출력
    for name in tensor_names:
        tensor = f.get_tensor(name)
        print(f"텐서 '{name}'의 데이터 타입: {tensor.dtype}")
        print(f"텐서 '{name}'의 크기: {tensor.shape}")

 

.safetensors 파일을 이용해서 nsfw_image_detection 모델 돌려보기

그럼 이제 갖고 있는 .safetensors 파일을 이용해서 딥러닝 모델을 로컬 환경에서 실행해보자. 본 포스팅에서는 이미지 분류 모델 중 하나인 nsfw_image_detection 모델을 예시로 가져왔다.

https://huggingface.co/Falconsai/nsfw_image_detection

 

Falconsai/nsfw_image_detection · Hugging Face

Model Card: Fine-Tuned Vision Transformer (ViT) for NSFW Image Classification Model Description The Fine-Tuned Vision Transformer (ViT) is a variant of the transformer encoder architecture, similar to BERT, that has been adapted for image classification ta

huggingface.co

NSFW(Not Safe For Work) Image Classification 모델은 입력받은 이미지가 safe 한지(보통은 19금을 걸러내는 게 주목적인것 같다.) 분류하는 모델이다. 많은 이미지 작업의 시작점에서 입력받은 이미지의 NSFW score를 검사해서 내가 정한 threshold를 넘기는지 아닌지 확인하는 모델이라 할 수 있겠다. 위의 huggingface 리포지토리를 clone하면 model.safetensors 파일과 config.json 파일이 존재한다. 세이프텐서 파일과 config 파일은 짝을 이루는 파일이라고 생각하면 편하다.

{
  "_name_or_path": "Falconsai/nsfw_image_detection",
  "architectures": [
    "ViTForImageClassification"
  ],
  "attention_probs_dropout_prob": 0.0,
  "encoder_stride": 16,
  "hidden_act": "gelu",
  "hidden_dropout_prob": 0.0,
  "hidden_size": 768,
  "id2label": {
    "0": "normal",
    "1": "nsfw"
  },
  "image_size": 224,
  "initializer_range": 0.02,
  "intermediate_size": 3072,
  "label2id": {
    "normal": "0",
    "nsfw": "1"
  },
  "layer_norm_eps": 1e-12,
  "model_type": "vit",
  "num_attention_heads": 12,
  "num_channels": 3,
  "num_hidden_layers": 12,
  "patch_size": 16,
  "problem_type": "single_label_classification",
  "qkv_bias": true,
  "torch_dtype": "float32",
  "transformers_version": "4.31.0"
}

config.json 파일을 보면, transformers.ViTForImageClassification 아키텍쳐를 이용해서 해당 모델을 불러와 사용할 수 있음을 알 수 있다. 파이썬 코드를 아래와 같이 작성해서 실행시켜 보자.

# Load model directly
import torch
from PIL import Image
from transformers import ViTForImageClassification, ViTImageProcessor

# 모델과 전처리기 로드
model = ViTForImageClassification.from_pretrained("./", use_safetensors=True)
processor = ViTImageProcessor.from_pretrained("./")

# 입력 이미지 로드 및 전처리
input_image = Image.open("./test.png")
inputs = processor(images=input_image, return_tensors="pt")

# nsfw score 예측
with torch.no_grad():
	outputs = model(**inputs)
	logits = outputs.logits

# 결과 확인
predicted_label = torch.argmax(logits, dim=-1).item()
print(f"Predicted label: {model.config.id2label[predicted_label]}")

이렇게 코드를 작성하고 돌려보면 내가 입력한 이미지에 대한 label을 확인할 수 있다.

 

728x90

Revit 애드인을 개발하던 중, 생성한 PathOfTravel 라인 중에 내 애드인에서 만든 라인만 제거하는 기능을 추가하게 되었다. 여러 방식으로 구현할 수 있겠지만, 싱글톤 패턴을 이용해서 내가 만든 라인들의 리스트를 관리하는 방식을 선택하게 되었다. 일종의 애드인 내 전역변수로의 활용인 셈이다. 이 과정에서 개념적으로만 알고 있던 싱글톤 패턴을 직접 구현하는 과정을 되짚어본다.

 

싱글톤 패턴이란?

싱글톤 패턴(Singleton Pattern)은 클래스의 인스턴스를 하나만 생성하도록 보장하는 디자인 패턴이다. 즉, 프로그램이 실행되는 동안 해당 클래스의 객체는 단 하나만 존재하며, 이 인스턴스는 어디서든지 동일하게 접근할 수 있다. 그럼 싱글톤 패턴은 언제 사용할까? 주로 전역적으로 접근 가능한 객체가 필요할 때(이번에 내가 사용한 경우이다.), 여러 객체가 공통의 상태를 공유해야할 때 등이 있겠다. 그럼 바로 구현 방법을 살펴보자.

 

싱글톤 패턴의 구현 (C# 예제)

기본 싱글톤 패턴 구현

public class Singleton
{
    // 정적 변수로 클래스의 유일한 인스턴스를 저장합니다.
    private static Singleton _instance;

    // 외부에서 인스턴스를 생성하지 못하도록 생성자를 private으로 설정합니다.
    private Singleton() 
    {
        // 생성자 내부에는 초기화 작업이 들어갈 수 있습니다.
    }

    // Singleton 인스턴스에 접근할 수 있는 정적 메서드를 제공합니다.
    public static Singleton Instance
    {
        get
        {
            // 인스턴스가 없으면 생성하고, 있으면 기존 인스턴스를 반환합니다.
            if (_instance == null)
            {
                _instance = new Singleton();
            }
            return _instance;
        }
    }

    // 예제 메서드: 싱글톤 객체가 하는 작업
    public void DoSomething()
    {
        Console.WriteLine("싱글톤 인스턴스에서 메서드 실행");
    }
}

사용 방법

class Program
{
    static void Main(string[] args)
    {
        // 싱글톤 인스턴스를 얻고 메서드 호출
        Singleton singleton1 = Singleton.Instance;
        singleton1.DoSomething();

        // 다른 곳에서 다시 싱글톤 인스턴스를 얻어도 같은 인스턴스가 반환됩니다.
        Singleton singleton2 = Singleton.Instance;

        // 두 인스턴스가 동일한지 확인
        Console.WriteLine(singleton1 == singleton2);  // True 출력 (같은 인스턴스)
    }
}

 

Eager Initialization vs Lazy Initialization

위에서 구현한 기본 예제는 멀티스레딩 환경에서 안전하지 않다. 여러 스레드가 동시에 싱글톤 인스턴스를 요청할 때, 두 개의 인스턴스가 생성될 위험이 있다. 이를 예방하기 위해 초기화 시기를 조절할 수 있는데, 초기화 시기에 따라 Eager Initialization과 Lazy Initialization으로 나눌 수 있다. 직역하면 빠른 초기화와 느린 초기화 정도가 되겠다. 멀티스레드 환경에서 안전한 싱글톤을 구현하기 위해서는 Eager Initialization 방식을 채택하여 클래스가 로드될 때 미리 인스턴스를 생성하여 멀티스레드 환경에서 안전하도록 한다. Lazy Initialization 방식은 메모리 자원 낭비를 막기 위해 필요할 때까지 생성을 최대한 늦추는 초기화 방법이다. 아래는 Eager Initialization 방식의 예시이다.

public class Singleton
{
    // 정적 변수에 미리 인스턴스를 생성해 둡니다.
    private static readonly Singleton _instance = new Singleton();

    // 생성자를 private으로 설정하여 외부에서 인스턴스 생성을 막습니다.
    private Singleton() 
    {
    }

    // 미리 생성된 인스턴스를 반환하는 정적 프로퍼티
    public static Singleton Instance
    {
        get
        {
            return _instance;
        }
    }

    public void DoSomething()
    {
        Console.WriteLine("스레드 안전한 싱글톤에서 작업 수행");
    }
}

 

애드인에서 실전 사용 예시

내가 제작한 애드인에서 사용된 예시를 살펴보면, DrawCircle() 메서드로 특정 원을 그리고, 해당 원의 ElementId를 리턴으로 받아온다. 그렇게 생성한 원을 나중에 제거 기능을 위해 AddinStorage라고 하는 싱글톤 인스턴스에 리스트로 저장해 둔다.

원을 그리고 싱글톤 인스턴스에 저장하는 부분

ElementId circleId = DrawCircle(doorPoint); // 문 위치에 원을 그려 강조 표시하고 ElementId 저장
if (circleId != null)
{
    AddinStorage.Instance.AddCircleId(circleId); // 그린 원의 ElementId를 AddinStorage 싱글톤 패턴에 저장
}

private ElementId DrawCircle(XYZ center)
{
    double radius = 2.5; // 반지름 설정
    Plane plane = Plane.CreateByNormalAndOrigin(XYZ.BasisZ, center);
    Arc circle = Arc.Create(plane, radius, 0, 2 * Math.PI);
    SketchPlane sketchPlane = SketchPlane.Create(_doc, plane);
    ModelCurve modelCurve = _doc.Create.NewModelCurve(circle, sketchPlane); // 원을 ModelCurve로 그리기

    // **가시성 및 순서 조정** - OverrideGraphicSettings 적용
    OverrideGraphicSettings ogs = new OverrideGraphicSettings();
    Color red = new Color(255, 0, 0);
    ogs.SetProjectionLineColor(red); // 색상 설정
    ogs.SetProjectionLineWeight(9); // 선 두께 설정
    _doc.ActiveView.SetElementOverrides(modelCurve.Id, ogs); // 활성 뷰에서 모델 라인의 그래픽 설정을 덮어쓰기

    return modelCurve?.Id; // 그린 원의 ElementId 반환
}

싱글톤 패턴 클래스 AddinStorage.cs

public class AddinStorage
{
    private static AddinStorage _instance;
    private List<ElementId> _createdCircleIds;

    private AddinStorage()
    {
        _createdCircleIds = new List<ElementId>();
    }

    public static AddinStorage Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new AddinStorage();
            }
            return _instance;
        }
    }

    public List<ElementId> CreatedCircleIds
    {
        get { return _createdCircleIds; }
    }

    public void AddCircleId(ElementId id)
    {
        _createdCircleIds.Add(id);
    }

    public void ClearCircleIds()
    {
        _createdCircleIds.Clear();
    }
}

내 애드인에서 제작된 circle만 제거하는 remove 기능 부분

// 생성한 원 삭제
List<ElementId> circleIds = AddinStorage.Instance.CreatedCircleIds;
foreach (ElementId circleId in circleIds)
{
    doc.Delete(circleId);
}
AddinStorage.Instance.ClearCircleIds();

 

발견된 추가적인 문제점

이렇게 remove 기능으로 제거한 원을 Undo(Ctrl+Z) 기능으로 Revit에서 살려내면 제거된 원이 다시 살아난다. 그런데 문제점은 되살아난 해당 Element가 싱글톤 인스턴스의 리스트에 다시 복원되지 않아서 remove 기능을 다시 사용하고자 할 때 제거되지 않는 문제가 존재한다. 결국 AddinStorage 클래스의 ClearCircleIds() 메서드를 없애고, remove 부분에서도 AddinStorage.Instance.ClearCircleIds()를 실행하지 않는 것으로 해결하였다. 즉, 생성된 원들의 ElementId List는 삭제 뒤에도 계속 싱글톤 인스턴스에 남아있게 되는 것이다. 그렇다면 어디에선가는 Clear를 해 주어야 하는데, Clear는 새로운 원들을 그릴 때 Clear하는 것으로 수정하였다.

728x90

코드를 작성하고 원격 저장소로 push를 하면, 이게 로컬이 아닌 서버 pc에서 문제없이 작동할지에 대한 의문을 갖게 된다. 코드 통합과 배포를 도와주는 CI/CD 툴을 사용하지 않고, Git 자체 기능을 활용해서 branch merge 이전에 테스트 코드를 실행시킬 수 있는 방법을 찾아보았다.

 

hook

GitLab 같은 CI/CD 도구를 사용하지 않고도, git bare repository에서 누군가 push를 했을 때, 이를 감지하여 자동으로 명령을 실행하는 스크립트를 작성할 수 있는데, 이것이 Git의 "hook" 이라는 기능이다. Git hooks는 특정 이벤트(ex, push, commit, merge)가 발생할 때 자동으로 실행되는 스크립트를 작성할 수 있는 Git의 내장 기능이다. Git의 hook 기능은 다양한 Git 이벤트에 대해 트리거를 설정할 수 있다. hook은 로컬에서 발생하는 이벤트에 대해 동작하는 client side hook과 bare repository에서 발생하는 server side hook으로 나눌 수 있다.

 

Client side hook

클라이언트 사이드에서 작동하는 hook은 개발자의 로컬 환경에서 발생하는 이벤트에 반응한다. 클라이언트 사이드 hook은 주로 commit이나 checkout과 같은 작업이 발생할 때 실행된다.

  • pre-commit: git commit을 실행하기 전에 트리거된다.
  • prepare-commit-msg: 커밋 메시지가 생성되기 전에 실행된다.
  • commit-msg: 커밋 메시지가 입력된 후 실행되며, 커밋 메시지를 검사하거나 수정하는 데 사용할 수 있다.
  • post-commit: 커밋 후에 실행되며, 로그를 남기거나 알림을 설정할 때 유용하다.
  • pre-rebase: git rebase를 시작하기 전에 실행된다.
  • post-checkout: 브랜치 변경 또는 특정 커밋으로 체크아웃한 후 실행된다.
  • post-merge: git merge 후에 실행된다.

클라이언트 사이드 훅의 예시로, 커밋 컨벤션에 어긋나는 커밋 메시지를 걸러내는 검사 코드를 보자.

#!/bin/sh
# commit-msg hook 예시
# 커밋 메시지가 규칙에 맞지 않으면 거부합니다.

COMMIT_MSG_FILE=$1
COMMIT_MSG=$(cat $COMMIT_MSG_FILE)

if ! echo "$COMMIT_MSG" | grep -qE "^(feat|fix|chore|docs): .+"; then
  echo "Invalid commit message format. Use 'feat|fix|chore|docs: <message>'"
  exit 1
fi

 

Server side hook

서버 사이드 훅은, git bare repository에서 특정 이벤트가 발생할 때 실행된다. 주로 코드가 푸시되었을 때, 배포와 관련된 작업을 자동화하는 데 사용된다.

  • pre-receive: 푸시된 내용이 서버에 적용되기 전에 트리거된다.
  • update: 특정 브랜치에 푸시가 발생했을 때 트리거된다. 개별 참조에 대한 푸시를 검사하거나 처리할 수 있다.
  • post-receive: 푸시된 내용이 서버에 저장된 후 트리거된다. 푸시된 코드에 대해 자동화된 테스트, 배포 등의 작업을 처리할 수 있다.
  • post-update: 업데이트된 브랜치나 태그에 대해 후처리 작업을 할 때 사용된다.
  • pre-rebase, post-rebase: 리베이스 작업 전후로 트리거된다.

서버 사이드 훅의 예시로, 푸시 이후 자동으로 배포 과정을 거치는 코드를 살펴보자.

#!/bin/bash
# post-receive hook 예시
# 푸시 후, 자동으로 서버에서 최신 코드를 pull하고 배포합니다.

# 배포 디렉터리로 이동
cd /path/to/deployment

# 최신 코드 가져오기
git pull origin main

# 빌드 및 배포
npm install
npm run build

 

post-receive hook을 이용해서 스크립트를 실행하는 예시

먼저 Git bare repository로 이동한다.

cd /path/to/your/bare/repository.git
cd hooks

그리고 post-receive hook을 작성해보자. post-receive라는 파일을 생성하고 실행 가능하도록 권한을 부여하자, push가 발생한 후 실행될 스크립트를 작성한다. push된 코드를 pull하여 로컬 서버에 배포하고, 테스트 스크립트를 실행하는 내용으로 예시를 들어 보겠다.

nano post-receive
chmod +x post-receive
#!/bin/bash

# 로컬 서버로 이동
cd /path/to/deployment/directory

# 저장소에서 최신 코드 가져오기
git pull /path/to/your/bare/repository.git main

# 필요한 경우, 빌드 또는 배포 명령 실행
# 예: npm install && npm run build
# npm install
# npm run build

# 테스트 실행
# 예: npm test 또는 curl을 이용한 간단한 서버 상태 체크
npm test

# 테스트 결과가 실패하면 경고 메시지 또는 알림 전송 (예: Slack, 이메일)
728x90

pop()은 O(1), shift()는 O(n)

자바스크립트에서 배열을 사용하다 보면, pop()과 shift() 메서드로 배열의 원소를 제거할 수 있다. 일반적으로 pop()은 배열의 맨 뒤 원소를, shift()는 배열의 맨 앞 원소를 제거하는 메서드로 알려져 있다. 그런데, pop()과 shift()를 구글에서 찾다 보면, 시간복잡도 이야기가 늘 따라붙는다. pop()은 O(1) time에, shift()는 O(n) time에 수행된다는 점이다.

 

기존 구현방식: 배열에 역순으로 값 넣기

배열의 가장 뒤에서 원소를 뽑아내는 것이 효율적이므로, 나는 큐를 사용하는 경우에 역순으로 배열에 집어넣는 선택을 하고 큐를 사용하고 있었다. 이 방법은 큐의 길이를 동적으로 조정해줘야 하는 경우가 아니라면 꽤 간단히 구현하고 사용할 수 있는 방법이었다. 그래서 최초 구현 당시 아무 고민없이 구현하고 사용했다. 1-2-3-4-5 의 순서로 이루어진 데이터를 5-4-3-2-1 순서로 배열에 넣으면 pop() 메서드로 1을 꺼낼 수 있기 때문이다.

하지만 이 방식도 늘 좋은 것은 아니다. 큐의 크기가 변하면 길이를 조정해 주어야 하는 문제가 있고, FIFO 원칙이 직관적으로 드러나지 않아 유지보수에 어려움을 겪을 수 있어 보인다. 향후 개발의 번거로움을 줄이기 위해 새로운 방식을 찾아보기로 했다.

 

대안1: 스택(Stack) 두 개를 사용하여 FIFO 원칙 구현

스택은 push()와 pop()이 O(1) 타임에 동작하기 때문에 이를 두 개 사용하여 효율적인 큐를 만들 수 있다. 첫 번째 스택(stack1)은 원소를 큐에 넣을 때 사용하고, 두 번째 스택(stack2)은 원소를 큐에서 꺼낼 때 사용하는 방법이다. 원소를 큐에서 꺼낼 때, stack2가 비어 있으면, stack1의 모든 원소를 pop()하여 stack2에 push()하고, 그 후 stack2에서 원소를 pop()하여 꺼내는 방식이다. 이렇게 하면 두 스택을 이용해 큐의 앞부분에서 꺼내는 것처럼 동작하게 된다. 그럼 개별 원소 입장에서 생각해보면, stack1.pop() -> stack2.push() -> stack2.pop() 순서로 꺼내지므로 O(3)로 사실상 O(1)같이 동작할 수 있는 것이다.

class Queue {
  constructor() {
    this.stack1 = [];
    this.stack2 = [];
  }

  // 큐에 원소를 추가하는 메서드
  enqueue(value) {
    this.stack1.push(value);
  }

  // 큐에서 원소를 제거하고 반환하는 메서드
  dequeue() {
    if (this.stack2.length === 0) {
      // stack2가 비어 있으면 stack1의 모든 원소를 옮긴다
      while (this.stack1.length > 0) {
        this.stack2.push(this.stack1.pop());
      }
    }
    return this.stack2.pop();
  }

  // 큐가 비어 있는지 확인하는 메서드
  isEmpty() {
    return this.stack1.length === 0 && this.stack2.length === 0;
  }
}

 

대안2: Linked List를 이용한 Queue 클래스 구현

링크드 리스트를 사용하면, 맨 앞에서 원소를 제거하는 dequeue()와 뒤에 원소를 삽입하는 enqueue() 모두 O(1) 시간복잡도를 유지할 수 있다. 성능을 더욱 최적화해야 하는 경우에는 링크드 리스트를 사용하는 것이 좋아 보인다.

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

class Queue {
  constructor() {
    this.front = null;
    this.rear = null;
  }

  enqueue(value) {
    const newNode = new Node(value);
    if (!this.rear) {
      this.front = this.rear = newNode;
    } else {
      this.rear.next = newNode;
      this.rear = newNode;
    }
  }

  dequeue() {
    if (!this.front) return null;
    const value = this.front.value;
    this.front = this.front.next;
    if (!this.front) this.rear = null;
    return value;
  }

  isEmpty() {
    return !this.front;
  }
}

 

Linked List를 이용한 Queue 구현이 O(n) 타임 피하기라는 원래의 목적과, 클래스화를 통한 유지보수 용이성 개선이라는 목적을 모두 달성할 수 있어서 이 방식으로 가기로 마음먹었다. 코드 수정하러 가자.

728x90

.msi 파일을 이용해서 revit 애드인 배포를 계획하고 있지만, 다른 애드인의 경우 .exe 파일 형식으로 배포하는 경우도 종종 존재한다. 그럼 .exe 파일과 .msi 파일은 어떻게 다르기에 다른 포맷으로 배포하는 것일까? 우선, .exe 파일과 .msi 파일은 모두 소프트웨어 설치를 위한 형식이지만, 설치 프로세스 관리와 구성 방식에서 중요한 차이점이 있다.

 

.exe 파일 (Executable File)

.exe 파일은 실행 파일로, 설치 프로그램뿐만 아니라 다양한 프로그램 실행 파일로 사용할 수 있다. .exe 파일의 특징은 다음과 같다.

  • 유연성: .exe 파일은 단순히 소프트웨어 설치를 넘어서 다양한 용도로 사용할 수 있다. 소프트웨어 설치를 위한 인스톨러를 실행하거나, 특정 명령을 수행하는 스크립트 또는 애플리케이션도 .exe 형식으로 제공된다.
  • 외부 스크립트 포함 가능: 설치 파일 내에서 외부 라이브러리, 설치 스크립트, 설정 파일 등을 포함하여 복잡한 설치 절차를 수행할 수 있다.
  • 복잡한 설치 과정 지원: .exe는 사용자 정의 스크립트 외에도 사용자로부터 여러 가지 입력을 받고, 다양한 소프트웨어 의존성을 확인하거나, 외부 서버에서 파일을 다운로드하는 등의 작업이 가능하다.
  • 인터페이스: 설치 프로그램의 사용자 인터페이스(UI)를 사용자 정의할 수 있다. 이로 인해 설치 과정에서 세부적인 제어와 더 많은 사용자 상호작용이 가능하다.
  • 동적 설치: .exe 인스톨러는 설치 중 추가 파일을 다운로드하거나, 실행 시 필요한 파일을 추출하는 동적 설치 방식을 사용한다.

 

.msi 파일 (Microsoft Installer File)

.msi 파일은 Windows Installer 패키지 파일로, 주로 Microsoft Windows에서 소프트웨어를 설치, 수정, 제거하기 위해 사용된다. .msi 파일의 특징을 정리하면 다음과 같다.

  • 구조화된 설치: .msi 파일은 Windows Installer 시스템을 사용하여 설치를 자동으로 처리한다. 이 시스템은 설치, 수정, 제거 등 설치 프로세스의 여러 단계를 자동화하고, 안정적으로 관리한다.
  • 표준화된 설치 방식: .msi 파일은 매우 표준화된 방식으로 소프트웨어를 설치한다. 설치 중 오류가 발생했을 때, 롤백이나 복구 같은 기능을 통해 안정성이 뛰어나다.
  • 의존성 관리: .msi 파일은 다른 설치 파일이 필요할 경우 의존성을 관리하거나, 소프트웨어 업데이트를 쉽게 처리할 수 있다.
  • 제어판 통합: .msi로 설치된 프로그램은 제어판의 프로그램 추가/제거 섹션에서 쉽게 찾을 수 있으며, 제거 또는 수정 기능을 간단하게 처리할 수 있다.
  • 기업 환경에서 관리: 많은 기업들은 .msi 파일을 사용하여 소프트웨어 배포 및 설치를 표준화하고, 자동화된 스크립트로 대규모 배포를 진행한다.

 

결론: 그래서 어느 상황에 어떤 파일 형식이 나은가

간단한 설치나 소규모 소프트웨어라면 표준화된 설치 방식과 안정적인 설치 및 제거 프로세스를 제공하기 때문에 .msi 파일을 사용하는 것이 좋다.

복잡한 설치 과정(ex. 여러 파일을 다운로드, 사용자 정의 스크립트 실행 등)이 필요하거나, 설치 UI를 커스텀해야 한다면 .exe 파일이 더 적합하다.

 

 

728x90

+ Recent posts