728x90

서비스를 출시한 지 두 달이 되어갈 무렵, 여러 건의 견적 문의가 들어오면서 실제 매출로 이어지기 시작했다. 이 과정을 통해 기존 B2C를 넘어 B2B까지 확장할 수 있는 기회도 엿볼 수 있었다.

 

무엇보다도 고객들로부터 직접 견적 문의가 들어오고, 실제 시공까지 연결되어 수익이 발생했다는 점에서 큰 보람을 느꼈다. 내가 만든 서비스가 고객의 니즈를 충족시켰다는 사실이 뿌듯하게 다가왔다. 예전에 배달의민족 김범준 대표가 “개발자는 코딩하는 사람이 아니라 문제를 해결하는 사람이다”라고 말했던 것이 문득 떠올랐다. 그 말을 처음 들었을 땐 단순히 좋은 말이라 생각했지만, 지금은 그 의미를 몸소 체감하고 있다.

 

샷시 가격을 측정하는 데 불편함을 겪던 고객들, 그리고 그 가격이 합리적인지 판단할 기준조차 없었던 상황을 ‘호빵’이 해결했다. 그 결과, 현재까지 약 250명의 고객이 300건 이상의 견적을 받아보았다. 이 수치는 고객의 실질적인 문제를 해결했기 때문에 가능한 성과라고 생각한다. 그리고 그런 문제 해결이 결국 수익으로 이어졌기에, “해결하는 사람”이라는 말이 더욱 깊게 와 닿았다.

 

최근에는 샷시 공장을 운영하는 한 기업으로부터 연락도 받았다. 그 회사는 이전에 SI 업체에 호빵과 유사한 견적 시스템을 의뢰했지만, 가격 오차 범위가 너무 커서 결국 프로젝트를 포기한 경험이 있었다고 한다. 그런데 이번엔 내 서비스를 보고 직접 개발을 맡기고 싶다는 제안을 해왔다. 아직 협의 중이지만, 이 일을 통해 공장 내부에서 사용할 수 있는 수요가 분명히 존재한다는 사실도 확인할 수 있었다.

 

이런 흐름 속에서 ‘호빵 비즈니스(가칭)’라는 이름으로 기업용 서비스를 별도로 기획해볼 계획이다. 서비스의 가능성이 B2C를 넘어 B2B까지 확장될 수 있다는 점에서 새로운 도전이자 기회라고 생각한다.

'난중(개발)일기 > [프로젝트] 호빵' 카테고리의 다른 글

서비스 시작 후 1달차 회고  (1) 2025.03.23
출시 후 2주차 회고  (1) 2025.03.03
728x90

org.apache.kafka.connect.errors.ConnectException: Error configuring an instance of KafkaSchemaHistory; check the logs for details\n\tat io.debezium.storage.kafka.history.KafkaSchemaHistory.configure(KafkaSchemaHistory.java:208)\n\tat io.debezium.relational.HistorizedRelationalDatabaseConnectorConfig.getSchemaHistory(HistorizedRelationalDatabaseConnectorConfig.java:137)\n\tat io.debezium.relational.HistorizedRelationalDatabaseSchema.<init>(HistorizedRelationalDatabaseSchema.java:50)\n\tat io.debezium.connector.binlog.BinlogDatabaseSchema.<init>(BinlogDatabaseSchema.java:79)\n\tat io.debezium.connector.mysql.MySqlDatabaseSchema.<init>(MySqlDatabaseSchema.java:41)\n\tat io.debezium.connector.mysql.MySqlConnectorTask.start(MySqlConnectorTask.java:99)\n\tat io.debezium.connector.common.BaseSourceTask.start(BaseSourceTask.java:251)\n\tat org.apache.kafka.connect.runtime.AbstractWorkerSourceTask.initializeAndStart(AbstractWorkerSourceTask.java:278)\n\tat org.apache.kafka.connect.runtime.WorkerTask.doStart(WorkerTask.java:175)\n\tat org.apache.kafka.connect.runtime.WorkerTask.doRun(WorkerTask.java:224)\n\tat org.apache.kafka.connect.runtime.WorkerTask.run(WorkerTask.java:280)\n\tat org.apache.kafka.connect.runtime.AbstractWorkerSourceTask.run(AbstractWorkerSourceTask.java:78)\n\tat org.apache.kafka.connect.runtime.isolation.Plugins.lambda$withClassLoader$1(Plugins.java:237)\n\tat java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)\n\tat java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)\n\tat java.base/java.lang.Thread.run(Thread.java:1583)\

 


문제의 Debezium-DB 연결 명령

curl -X POST http://localhost:8083/connectors \

-H "Content-Type: application/json" \

-d '{

  "name": "{커넥터_이름}",

  "config": {

    "connector.class": "io.debezium.connector.mysql.MySqlConnector",

    "database.hostname": "{MySQL_호스트_이름_or_컨테이너_이름}",

    "database.port": "{MySQL_포트}",

    "database.user": "{MySQL_사용자명}",

    "database.password": "{MySQL_비밀번호}",

    "database.server.id": "{MySQL_복제_서버_ID(고유숫자)}",

    "database.include.list": "{감지할_DB명}",

    "topic.prefix": "{Kafka_토픽_프리픽스}",     

    "database.server.name": "{Kafka_토픽_서버명}",

 

    "database.history.kafka.bootstrap.servers": "{Kafka_호스트:포트}",

    "database.history.kafka.topic": "{Kafka_스키마_히스토리_토픽명}",

    

    "include.schema.changes": "true"

  }

}'

 

여기에서 에러가 났다.

이유는 Debezium 버전문제였다. 1.x 버전은 Kafka 스키마 히스토리 토픽명의 key값으로 database.history.kafka.topic 으로 해야하고 2.x 버전은 schema.history.internal.kafka.topic 으로 해야했다. 그래서 

 

curl -X POST http://localhost:8083/connectors \

-H "Content-Type: application/json" \

-d '{

  "name": "{커넥터_이름}",

  "config": {

    "connector.class": "io.debezium.connector.mysql.MySqlConnector",

    "database.hostname": "{MySQL_호스트_이름_or_컨테이너_이름}",

    "database.port": "{MySQL_포트}",

    "database.user": "{MySQL_사용자명}",

    "database.password": "{MySQL_비밀번호}",

    "database.server.id": "{MySQL_복제_서버_ID(고유숫자)}",

    "database.include.list": "{감지할_DB명}",

    "topic.prefix": "{Kafka_토픽_프리픽스}",     

    "database.server.name": "{Kafka_토픽_서버명}",

 

    "database.history.kafka.bootstrap.servers": "{Kafka_호스트:포트}",

    "schema.history.internal.kafka.topic": "{Kafka_스키마_히스토리_토픽명}",

    

    "include.schema.changes": "true"

  }

}'

 

이렇게 하니까 잘 된다.

728x90

한 달 만에 195명, 호빵 유저를 모으며 느낀 마케팅의 힘

 

호빵 서비스를 본격적으로 알리기 시작한 지 어느덧 한 달이 넘었습니다. 그 사이 약 180명의 유저가 가입했고, 그 과정을 통해 가장 크게 느낀 점은 “마케팅은 사업의 절반 이상”, 어쩌면 80% 이상일 수도 있다는 사실이다.

 

단순히 좋은 서비스를 만드는 것만으로는 충분하지 않았습니다. 어떻게 알릴지, 어디에 노출할지, 어떤 메시지로 전달할지가 오히려 더 중요하다는 것을 매일 실감하고 있다.

 

 

데이터 기반 마케팅의 위력

 

마케팅 전략을 세우는 데 있어 통계 데이터의 힘은 정말 강력했다.

총 가입자 수

등록된 견적 수

앱 유입 비율

일별 유저 유입/탈퇴/견적 등록 추이

지역별 견적 등록 수

유입 경로별 클릭 수

 

이 모든 데이터를 시각화하여 매일 확인하고 분석했다.

 

예를 들어, 지역별 견적 등록 수를 분석해보니 서울/경기권의 수요가 압도적으로 높다는 것을 확인할 수 있었고, 이를 기반으로 인스타그램 광고의 타겟 지역을 정했다.

 

또한 일별 유저 유입 그래프를 통해 신규 유저가 주말에 더 많이 유입된다는 사실을 파악했고, 그에 맞춰 광고 예산을 주말에 집중적으로 투입하는 전략을 실행했습니다. 작은 실험이었지만, 분명한 효과를 체감할 수 있었다.

 

 

고객의 ‘진짜 니즈’에 다가가기

 

마케팅을 하며 또 한 가지 중요한 것을 배웠다.

바로 잠재고객들이 무엇을 원하는지를 명확히 파악해야 한다는 것다.

 

서비스에 대한 메시지를 조금씩 바꿔가며 실험해봤습니다. 블로그, 카페, SNS 등에 홍보 글을 올릴 때마다 서비스 URL에 쿼리 파라미터를 다르게 설정해 경로별 클릭 수를 추적했다.

 

그 결과, 유저들이 가장 관심을 가지는 포인트는 단연 “가격”이었다.

 

예를 들어 인스타그램 광고에서 단순한 이미지보다, 정확한 수치가 명시된 이미지를 올렸을 때 클릭율이 훨씬 높았다.

 

단순 텍스트 설명 이미지 (클릭률 2.2%)

 

 

정확한 가격 수치 포함 이미지 (클릭률 7.8%)

 

무려 5.6%포인트 차이였다.

이 작은 차이가 실제 유저 유입에는 큰 차이를 만들어냈다.

 

 

결과

 

지금도 호빵의 마케팅은 실험 중이다.

어떤 문구가 사람들의 마음을 움직이는지, 어떤 플랫폼에서 전환율이 높은지,

그리고 결국 어떤 정보가 고객에게 ‘신뢰’로 다가가는지.

 

이 모든 것을 고민하고 실험하며, “가장 정확하고, 가장 합리적인 창호 견적 플랫폼” 으로 자리잡기 위한 여정을 계속하고 있다.

 

앞으로 구글, 네이버 등 광고를 진행 하여 고객님들의 반응을 살펴 볼 예정이다.

 

추가적으로 할인 이벤트를 할 예정인데 문구를 어떻게 해야 할지 광고를 해서 반응을 살펴 봐야할 것 같다.

 

728x90

현재 Java 21에서는 실행 중인 JVM에 에이전트를 동적으로 로드할 때 경고를 발생시키도록 변경되었다. 이는 향후 릴리스에서 기본적으로 동적 에이전트 로딩을 금지하기 위한 준비 단계로, 애플리케이션의 무결성과 보안을 강화하기 위한 조치를 한 것이라고 한다.

 

여기서 에이전트 (Agent) 란?

Java 에이전트 (Agent)는 JVM (Java Virtual Machine)에서 프로그램의 실행 중간에 개입해 동작을 변경하거나 분석할 수 있는 특별한 모듈임. 주로 성능 모니터링, 디버깅, 트래픽 분석, 보안 검사 등을 위해 사용된다.


 

그러면 어플리케이션이 로딩 되면서 에이전트가 무엇을 하길래 금지를 시킬까?

 


에이전트의 주요 역할

1. 바이트코드 조작 (Bytecode Manipulation)
   - 실행 중인 클래스의 바이트코드를 변경하거나 주입할 수 있다.
   - 예를 들어, 메서드 호출 전에 로그를 남기거나, 특정 메서드의 실행 시간을 측정할 수 있다.
2. 프로파일링 (Profiling)
   - CPU 사용량, 메모리 사용량, 메서드 호출 빈도 등을 측정해 성능 분석을 한다.
3. 보안 검사 (Security Monitoring)
   - 악의적인 코드 실행을 방지하고 보안 정책을 강제할 수 있다.
4. 트랜잭션 관리
   - 분산 환경에서 트랜잭션 추적로그 수집에 사용됩니다.
   - 예: APM (Application Performance Monitoring) 툴


에이전트를 동적 로딩을 금지하면서 취할 수 있는 이득은?

동적 에이전트 로딩실행 중인 JVM에 외부 에이전트를 주입해 코드의 동작을 변경하거나 모니터링할 수 있다. 이 과정에서 심각한 보안 이슈가 발생할 수 있습니다. 아래는 그 이유이다.

1. 임의의 코드 실행
동적 에이전트를 통해 JVM 내부에서 임의의 바이트코드를 주입할 수 있다. 예를 들어, 악성 에이전트가 System.exit() 호출을 우회하거나, 데이터베이스 접근 코드를 삽입해 기밀 정보를 유출할 수 있다.

더나아가 루트 권한으로 실행되는 JVM의 경우, 시스템 전체를 장악하는 심각한 보안 위험을 가져올 수 있다.

2. 무결성 침해
동적 에이전트는 JVM 실행 중에 클래스의 바이트코드를 변경할 수 있다. 예를 들어, 권한 검사를 우회하거나, 보안 검사 코드를 비활성화시킬 수 있다.

이로 인해 암호화 모듈이 변조되어 평문 데이터를 유출할 수 있어 API 요청 검증 로직을 무력화 될 것이다.

3. 메모리 및 자원 침해
에이전트를 통해 JVM의 메모리와 리소스를 모니터링하고 제어할 수 있다. 예를 들어, 자바 힙 메모리의 데이터를 읽어 세션 토큰이나 비밀번호를 유출할 수 있을 것이다.

Heap Dump 를 통해서 서버 민감 정보를 획득할 수 있을 것이고, GC 설정을 조작 해서 서비스가 마비될 수도 있을 것 같다.


결론

그래서 Java 21에서 동적 로딩 금지의 목적은 

- 무결성 강화: JVM 실행 중 클래스 변조 방지.
- 보안성 향상: 악성 에이전트 주입을 통한 취약점 방어.
- 안정성 확보: 실행 중 JVM의 예측 불가능한 동작 방지.

으로 정리 될 수 있겠네요.


 

그럼에도 불구하고 동적 로딩을 허용하고 싶다면 다음 설정으로 동적 로딩 허용은 가능하다

-XX:+EnableDynamicAgentLoading: 동적 에이전트 로딩을 명시적으로 허용
-javaagent 또는 -agentlib: JVM 시작 시 에이전트를 로드

728x90

728x90

개인프로젝트를 앱스토어, 플레이스토어에 둘 다 출시 했다.

 

앱 명은 인테리어를 하면서 사람들이 소위 호구를 많이 당하는데, 구를 명으로 만들고자 호빵으로 지었다.

 


기획부터 프론트엔드(웹, 앱), 백엔드, 인프라까지 혼자 맡아서 하다 보니 퇴근 후 작업이 매우 힘들었지만, 값진 경험이었다. 게다가 실제 유저들까지 유입되어 보람을 느꼈다. 현재 유저는 총 81명으로 (가족과 지인을 제외한 수치) 특별히 돈을 안 들이고 블로그 글 4개로 얻은 성과인 점을 감안하면 개인적으로 매우 의미 있는 숫자라고 생각한다.

혼자서 많은걸 하다 보니 UI/UX에 신경을 충분히 쓰지 못한 것 같다. 현업에서는 백엔드 개발을 하고 있고, 프론트엔드는 학교 과제나 개인 프로젝트를 통해 일부 경험해 본 것이 전부라 어려움이 있었다. 또한, 처음 해본 앱 개발이 가장 큰 난관이었다.

실제 유저가 유입되었으며, 전체 서비스 이용률은 약 30~40%였고, 회원가입 과정에서의 이탈률은 약 20%였다. 이를 기반으로 데이터를 분석해 보았다.

1. 유저의 UI에 대한 불신
앱이 렌더링되는 디바이스 화면에 맞게 CSS가 제대로 적용되지 않았다. (출시 후 조금씩 조정하자는 생각으로 미뤄두었다. 사실상 선택과 집중을 한 것이었다고 생각한다. 한정된 시간 속에서 익숙한 백엔드 쪽에 더 공을 들인 것이 가장 큰 이유였다.)

2. 불편하고 친절하지 않은 UX로 인한 불신
예를 들어, 최초 회원가입 후 유저의 휴대폰 인증 과정에서 인증번호 실패 시의 대처 전략이 미흡했다. 렌더링되는 디바이스에 맞게 오류 메시지가 보여야 하는데, 너비가 좁은 디바이스에서는 해당 메시지가 보이지 않아 유저가 당황하고 이탈한 것으로 판단했다.

 

 

결론

위 문제점들을 개선한 결과, 유저 이탈률이 감소하고 전체 서비스 이용률이 증가하는 긍정적인 변화를 경험했다.

728x90

RN 으로 배포 후 카카오로그인을 했는데 자꾸 안 됐다.

 

로그를 보니 해시키값이 문제인 것 같았다.

 

그래서 배포할 때 사용한 배포용 keystore 해시키를 추출해서 사용 했는데 안 됐다. (다음과 같이)

keytool -exportcert -alias androiddebugkey -keystore ~/.android/release.keystore -storepass 123456 -keypass 123456 | openssl sha1 -binary | openssl base64

 

이런식으로 해도 안됐다.

 

그래서 GooglePlayConsole > 앱 무결성 > Play 앱 서명 >  앱 서명 키인증서 > SHA-1 인증서 지문 값을 복사 해서 Base64로 컨버팅 한 후 등록 하니까 됐다. 

 

사용 한 컨버터는 다음과 같다. https://tomeko.net/online_tools/hex_to_base64.php?lang=en

728x90

https://ydontustudy.tistory.com/207

 

CGI (Common Gateway Interface)

CGI 란서버와 애플리케이션 간에 데이터를 주고받는 이 프로세스 또는 규칙을 공통 게이트웨이 인터페이스(CGI)라고 한다. 동적인 컨텐츠를 생성할 수 있는 프로그램을 갈망하는 사람들의 요구로

ydontustudy.tistory.com

이 글에 이어서 CGI를 Servlet 과 비교 해본다.

해당 글을 요약하자면, CGI는 웹 서버와 외부 프로그램 간의 통신을 위한 표준 인터페이스이다. 각각의 요청마다 새로운 프로세스가 fork 되는데 이는 서버 리소스가 많이 들고, 동시처리가 어려운 치명적인 단점이 있다. 그래서 이를 개선한 Fast CGI 가 나와서 프로세스 재사용이 가능해졌다. 그러나 여전히 부족한 부분이 많아서 각 플랫폼마다 각자의 웹 서버 기술을 가지게 된다.

 

CGI는 동적인 웹 서비스를 할 수 있게 하는 새로운 페러다임을 소개한다. 그러나 단점이 존재하고 이를 보완하기 위해 Fast CGI라는게 나왔지만 일부만 해결된 정도이다. 그래서 각 플랫폼에서 CGI 의 아이디어를 딴 웹서버가 탄생한다. 이 중에서 CGI와 JAVA의 Servlet을 비교 해보겠다.

 

 

프로세스 vs 쓰레드

CGI의 가장 큰 단점은 매 요청마다 새로운 프로세스를 생성하는데 반면 Servlet은 멀티스레딩을 사용한다. 하나의 프로세스 내에서 여러 스레드를 생성하여 요청을 처리한다. 이는 CGI에 비해 훨씬 적은 시스템 리소스를 사용하며, 더 빠른 응답 시간을 제공한다.

 

 

생명주기 관리 측면

CGI 프로그램은 요청이 들어올 때마다 실행되고 종료된다. Servlet은 한 번 로드되면 메모리에 상주하며, 여러 요청을 처리할 수 있다. 이는 Servlet의 초기화 시간을 줄이고 성능을 향상시킨다.

 

 

이식성

Servlet은 자바의 "Write Once, Run Anywhere" 철학을 따른다. 따라서 다양한 플랫폼에서 실행될 수 있다. 반면 CGI는 플랫폼에 종속적일 수 있다. 자바의 Servlet은 JVM 위에서 돌아가기 때문에 플랫폼에 독립적이라 클라우드 환경에서도 쉽게 적용이 가능하다. 반면 CGI는 스크립트 작성방식, 프로세스 생성방식 등 플랫폼에 종속적이다.

728x90

CGI 란

서버와 애플리케이션 간에 데이터를 주고받는 이 프로세스 또는 규칙을 공통 게이트웨이 인터페이스(CGI)라고 한다. 동적인 컨텐츠를 생성할 수 있는 프로그램을 갈망하는 사람들의 요구로 인해 만들어진 규약이다.

 

 

CGI 작동 방식

프로세스기반으로 동작한다. 즉 매 요청마다 프로세스를 새로 시작하는 방식이다.

이로 인해 요청 수가 많아 지면 서버에 많은 프로세스가 생성되고,요청을 처리 후 종료 된다. 그래서 요청 수가 많아지면 서버에 부담을 준다.

 

FastCGI 의 탄생

이를 보완하기 위해 FastCGI와 같은 대안이 나왔다.

개선점은 매 요청 프로세스를 생성하지 않고 프로세스를 재사용할 수 있게 했다.

그래서 FastCGI 부터는 프로세스 풀이 생김.

IPC 최적화로 인해 멀티스레드와 멀티프로세스 지원이 가능해짐

 

 

결과적으로, FastCGI의 프로세스 풀 개념은 CGI가 겪던 성능 문제를 크게 개선해주었다. 이후의 다른 웹 애플리케이션 인터페이스들이 이러한 개념을 적용하면서 더 발전하게 됨.

 

 

참고

- https://kldp.org/node/73386

 

흠....PHP도 CGI 인가요? | KLDP

제가 CGI에 대한 정의들을 찾아보니 CGI란 클라이언트의 요청을 서버에서 받아 그 요청을 다른 응용 프로그램으로 넘겨서 필요에 따라서 그 결과를 다시 서버로 보내 서버에서 클라이언트로 그결

kldp.org

- https://jongminlee0.github.io/2020/10/10/cgivsservlet/

 

[Server] CGI와 Servlet에 대해서 - Jongmin's Blog

기존 Java를 이용하여 개발하고 현재는 PHP를 사용하여 업무를 하고 있습니다. 그렇다보니 두 언어의 차이점이 존재하고 그 차이점에 대해 궁금하였습니다. 그 중 PHP의 Fast-CGI를 맞닥드렸습니다.

JongMinLee0.github.io

- https://www.geeksforgeeks.org/difference-between-java-servlet-and-cgi/

 

Difference between Java Servlet and CGI - 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

- https://www.javatpoint.com/difference-between-java-servlets-and-cgi

728x90

서버 로그를 그라파나에 보여지게 하려는데, 에러가 나를 몇시간 동안 괴롭혔다.

 

해당 에러를 해석 해보자면 연결은 됐지만 라벨을 받지 못했으니 Loki, Promtail 설정을 잘해봐라. 라는 뜻으로 이해했다.

 

처음에는 Docker compose 파일에 다음과 같이 Promtail 컨테이너를 세팅했다.

 

 

문제는 Promtail 설정이 담긴 promtail-config.yml 파일명이었다. 하이픈을 제거해서 promtail.yml로 변경하니 됐다.

 

 

+ Recent posts