블로그

제갈진우

[인프런 워밍업 클럽 스터디 3기] PM 워밍업클럽 1주차 회고

1주차 학습 회고: PM으로서 문제 정의와 해결의 중요성이번 1주차 강의를 통해 PM의 핵심 역할을 다시금 정리할 수 있는 기회가 되었습니다. PM은 고객과 비즈니스의 가치를 균형 있게 만들어가는 역할이며, 이를 위해 Valuable(가치 있는), Usable(사용 가능한), Feasible(개발 가능한), Viable(사업적으로 지속 가능한) 제품을 만들어야 한다는 것이 다시 한번 강조되었습니다.특히 문제 정의의 중요성을 깊이 실감했습니다. PM은 단순히 문제를 해결하는 것이 아니라, 해결할 가치가 있는 문제를 찾는 것이 핵심 역할입니다. 이 과정에서 "문제 공간을 2차원으로 시각화하고 Deep Dive하는 방법"과 "Feature Work와 Growth Work를 구분하여 접근하는 방식"이 인상적이었습니다. 기존에도 문제를 정의하려고 노력했지만, 실제로 이를 구조화하는 데 어려움이 있었는데, 이번 강의를 통해 좀 더 명확한 기준을 가질 수 있을 것 같습니다.또한, "모든 고객의 문제가 비즈니스로 연결되지는 않는다"는 점이 다시금 와닿았습니다. 문제를 정의하고 나면, 본능적으로 해결책을 고민하게 되는데, 정작 이 문제가 해결할 가치가 있는지에 대한 검증을 소홀히 할 때가 많습니다. 앞으로는 데이터 기반으로 문제를 분석하고, 해결책을 검증하며 이터레이션을 반복하는 과정을 더 의식적으로 적용해야겠다고 다짐했습니다. 강의를 들으면서 가장 와닿았던 부분은 "학습이 끝이 아니라 실천해야 한다"는 점입니다. 결국 이 내용을 업무와 실제 프로젝트에 적용해보면서 나만의 방법론으로 체득하는 것이 중요합니다. 사이드 프로젝트에서도 이번 강의 내용을 활용해 문제 정의를 더욱 명확히 하고, 분석 툴을 적극적으로 활용해볼 계획입니다. 마지막으로, 학습한 내용을 단순히 듣고 끝내는 것이 아니라, 직접 회고를 작성하면서 다시 한번 정리하는 것이 도움이 되었습니다. 앞으로도 학습한 내용을 적용하고 기록하는 습관을 들여야겠다고 생각합니다.

기획 · PM· PO워밍업클럽pmpo데이터분석가1주차회고

천준민

워밍업 클럽 3기 BE 클린코드&테스트 - 1주차 발자국

📖 강의 요약 2⃣ 추상우리가 클린 코드를 추구하는 이유"가독성"혼자만 사용한다면 아마 필요 없는 이야기 일수도 있을 것이다 하지만 개발자는 협업이 중요하다고 들었다.나만 코드를 읽는 것이 아니라 협업을 하는 사람들끼리 컨벤션도 중요하지만 그 전에 가독성 있는 코드를 짜면 원활한 작업을 할수 있을 것이라고 생각한다. 추후 발생하는 비용들도 줄어들 것이다.이번 섹션에서 강조하는 것은 제목처럼 추상인 것 같다.추상을 알아보자"추상"중요한 정보는 가려내어 남기고 덜 중요한 정보는 생략하여 버린다.미션에서 제출한 내가 이해한 추상과 구체를 위와 같이 예시를 들수 있을 것 같다.[추상] 인프런 워밍업 클럽 3기 BE 클린코드 & 테스트를 수료 한다. [구체] 2 개 강의(Readable Code: 읽기 좋은 코드를 작성하는 사고법&Practical Testing: 실용적인 테스트 가이드 )를 100 % 수강한다. 주 1회(총 4회) 발자국 작성을 한다. OT를 포함한 총 3회 온라인 라이브를 출석한다 미션 6개 중 5개 이상 기한 내 제출 완료한다.추상화의 가장 대표적인 행위는 이름 짓기,메서드 선언, 동등한 추상화 레벨, 매직 넘버,매직 스트링 제거 등 이 있다.위 주제에서 공통적으로 이름을 잘 짓는 것이 기반이 되는 것 같다. ex) 변수이름, 메서드 이름, 상수 이름 3⃣ 논리, 사고의 흐름이번 섹션에서 한마디로 정리하자면 빼고 생각하자 였다. (로직이 복잡하면 그 내가 먼저 생각 할 수 있는 상황부터 먼저 거르고 생각하기)추가적으로 부정어, 예외처리, null,optional에 대해 한번 더 생각해보고 작성해보기 4⃣ 객체 지향 패러다임이번 섹션은 좀 반성을 좀 하였다. 특히 강의 중 getter 사용 에 대한 이야기 였는데 무조건 getter를 사용하면 객체안에 정보를 가져올수 있잖아라고 생각하는 나에게 생각을 많이하게 되었다.핵심 : 관심사를 분리, 높은 응집도, 낮은 결합도, getter를 사용하기 전에 객체의 메시지를 보내서 그 값을 사용할수 있는 지 부터 생각하는 능력 기르기 SOLID 원칙✅SRP : 하나의 클래스는 단 한 가지 변경 이유(= 책임) 만을 가져야 한다.✅OCP: 확장에는 열려 있고, 수정에는 닫혀 있어야 한다.✅LSP: 자식 클래스는 부모클래스의 책임을 준수하며, 부모 클래스의 행동을 변경하지 않아야 한다.✅ISP : 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안된다.✅DIP : 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다. 5⃣ 객체 지향 적용하기이번 섹션은 내가 잘 이해를 잘못한 것 같아 다시 보아야 할 것 같다. 지금은 일기 정도로 작성 하지만 이해하여 꼭 적어보자강의를 듣고 옛날에 내가 작성한 코드(전체 코드 x)가 생각 났다.public enum ReservationStatus { PENDING("PENDING") { @Override public boolean canChangeTo(ReservationStatus status) { switch (status) { case APPROVED, CANCELED, EXPIRED -> { return true; } } return false; } }, APPROVED("APPROVED") { @Override public boolean canChangeTo(ReservationStatus status) { return status.equals(APPROVED); } }, CANCELED("CANCELED") { @Override public boolean canChangeTo(ReservationStatus status) { return status.equals(CANCELED); } }, EXPIRED("EXPIRED") { @Override public boolean canChangeTo(ReservationStatus status) { return status.equals(EXPIRED); } };지금 보니 메서드 명도 바꿀 필요가 있고 switch 사용으로 유지보수 어려움, if-else 사용 등 고쳐야할 부분이 많은 것 같다.목표 : 강의를 어느 정도 이해하여 코드를 바꿔보기 💡 미션 코드와 설명을 보고, [섹션 3. 논리 의 사고 흐름]에서 이야기 하는 내용을 중심으로 읽기좋은 코드로 리팩토링 해보기 블로그: https://zunmin4030.tistory.com/56 먼저 사이즈가 0이면 어떤 행위를 하고 0보다 크면 어떤 행위 0보다 작으면 어떤 행위를 한다라는 접근으로 코드를 이해 하는 것 부터 시작하였다. 거기에 대한 궁금증을 가지고 섹션별 내가 적용할 수 있는 부분을 생각해보았다. 이름짓기 , 부정 연산자 제거, Early return을 생각하여 과제를 해결하다보니 처음 코드를 말로 정리했을 때 생긴 궁금증을 해소 할수 있었다. 💬 회고 👍 잘한 점배운 것을 하나씩 적용해 보며 과제를 수행 해 나갔다.😮‍💨 아쉬웠거나 보완하고 싶은 점섹션 5에 대한 이해를 하지 못해 한번 더 공부 해야겠다. 📎출처Readable Code: 읽기 좋은 코드를 작성하는 사고법https://www.inflearn.com/course/readable-code-%EC%9D%BD%EA%B8%B0%EC%A2%8B%EC%9D%80%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%EC%82%AC%EA%B3%A0%EB%B2%95/dashboard

백엔드인프런워밍업클럽backend3기

강동훈

[인프런 워밍업 클럽 3기 - CS] - 1주차 발자국 (운영체제) - 3

💻 CPU 스케줄링📌 CPU 스케줄링 개요1. 프로그램들이 실행되면 메모리에 올라가 프로세서가 되고2. 프로세서들은 1개 이상의 쓰레드가 있으며3. CPU를 차지하기 위해 운영체제의 명령을 기다린다. ✅ CPU 스케줄링 고려사항1. 어떤 프로세스에게 CPU 사용권을 줘야 하나?2. CPU를 할당받은 프로세스가 얼마의 시간동안 CPU를 사용할 수 있는가?CPU Burst: 프로세스가 CPU를 할당받아 연속적으로 실행하는 구간I/O Burst: 프로세스가 입출력(I/O) 작업을 수행하는 구간 📌 다중큐✅ 프로세스 실행 과정1. 프로세스가 생성되면 준비 상태로 전환2. 준비 상태에서 CPU 사용권을 받으면 실행 상태3. 실행 중 I/O 요청은 대기 상태, 할당시간을 초과하면 준비 상태4. 작업이 끝난다면 완료 상태로 전환된다. ✅ 준비 & 대기 상태 관리1. PCB 정보 내에 우선순위 정보를 탐색하여 준비 상태 다중 큐(Ready Queue) 에 넣는다.2. 준비 상태 다중 큐에서 운영체제는 적당한 PCB를 선택하여 실행 상태로 전환시킨다. (적당한 - 스케쥴링 정책에 따라)3. I/O 요청으로 대기 상태가 된다면 I/O 작업에 따라 분류된 큐에 들어간다.4. I/O 작업이 마무리되면 인터럽트를 발생시켜 실행 상태로 전환한다.준비 & 대기 상태 -> Queue로 관리 📌 스케줄링 목표1. 리소스 사용률- CPU, I/O device2. 오버헤드 최소화- 스케쥴링 계산, 컨텍스트 스위칭의 빈도3. 공평성- 모든 프로세스 공평하게 할당- 프로세스의 중요도에 따라 상대적으로 공평하게 할당4. 처리량- 같은 시간 내 더 많은 처리5. 대기 시간- 작업 요청부터 작업 실행 전까지 대기 시간을 짧게6. 응답 시간- 작업 응답 시간을 짧게모든 목표를 동시에 달성할 수는 없다.사용자가 사용하는 시스템에 따라 목표를 다르게 설정한다.- 터치 스크린 -> 응답시간이 중요- 과학 계산 -> 처리량이 중요- 일반 목족 -> 밸런스 유지 📌 FIFOFIFO(First In First Out): 스케줄링 큐에 들어온 프로세스 순서대로 먼저 실행된다. ✅ 특징1. 한 프로세스가 완전히 끝나야 다른 프로세스가 시작함.2. 짧은 작업의 프로세스가 긴 작업의 프로세스를 대기할 수도 있음.3. I/O 작업을 대기하는 동안 CPU는 다른 작업을 하지 않음 => CPU 사용률 저하 ✅ 평균대기시간프로세스가 여러 개 실행될 때 모두가 실행되기까지의 평균 대기 시간- 프로세스_1 (Burst Time: 25초) - 대기 시간: 0초- 프로세스_2 (5초) - 대기 시간: 25초- 프로세스_3 (4초) - 대기 시간: 30초❗평균 대기 시간: 55 / 3 = 18.3초- 프로세스_3 (4초) - 대기 시간: 0초- 프로세스_2 (5초) - 대기 시간: 4초- 프로세스_1 (Burst Time: 25초) - 대기 시간: 9초❗ 평균 대기 시간: 13 / 3 = 4.3초프로세스 실행 순서에 따라 평균 대기 시간의 편차가 크기 때문에, 현대 운영체제에서는 사용 X일괄처리시스템에서 주로 사용 📌 SJFSJF(Shortest Job First): Burst Time이 가장 짧은 작업부터 우선 실행 ✅ 특징1. FIFO의 단점이었던 '실행 순서에 따른 평균 대기 시간 차이'를 극복하기 위해 설계2. 프로세스의 종료 시간를 예측하기 어려움 (Burst Time 파악이 힘듦)3. Burst Time이 긴 프로세스는 계속 뒤로 밀려가서 대기 시간이 점차 길어진다.4. 2,3 의 문제점으로 CPU 스케쥴링으로 채택되지 않음. 📌 RRRR(Round Robin): 일정 시간동안만 프로세스를 실행하고 다음 프로세스를 실행 ✅ 특징1. 타임 슬라이스(`Time Slice`) : CPU가 할당되는 일정한 시간 ✅ 평균대기시간타임 슬라이스 : 10s- 프로세스\_1 (Burst Time: 25초) - 대기 시간: 0 + 14 + 0초- 프로세스\_2 (4초) - 대기 시간: 10초- 프로세스\_3 (10초) - 대기 시간: 14초❗평균 대기 시간: 38 / 3 = 12.67초 FIFO 알고리즘과 평균 대기 시간이 비슷하다면 RR 알고리즘이 더 비효율적RR 알고리즘은 Context Switching이 일어나기 때문에 자원의 사용이 더 필요.- 타임 슬라이스가 큰 경우- 먼저 들어온 프로세스가 실행되니 FIFO 알고리즘과 동일 (타임 슬라이스: 무한대)- 웹 브라우저, 음악 플레이어가 5초마다 끊김(타임 슬라이스: 5s)- 타임 슬라이스가 작은 경우- 여러 프로세스가 동일하게 실행되는 것처럼되지만 Context Switching 처리량의 증가로 오버헤드가 너무 크다(1ms) 최적의 타임 슬라이스1. 여러 프로세스가 버벅이지 않고 동시에 실행하는 것 같은2. 오버헤드가 너무 크지 않은 운영체제 타임 슬라이스1. Windows: 20ms2. Unix: 100ms 📌 MLFQMLFQ(Multi Level Feedback Queue):✅ 가정- CPU Bound Prcess(P1): CPU 연산만 하는 프로세스 - CPU 사용률, 처리량 중요- I/O BOund Process(P2): 대부분 I/O 작업만 진행 - 응답 속도1. Time Slice가 100초일 때P2 (1초) 작업 후 I/O 작업 요청P1 (100초) 실행P1 10초 정도 작업 중, P2의 작업이 완료되어 인터럽트 발생 후 준비 큐에 삽입P1 90초 나머지 실행P2 1초 실행 I/O 작업...2. Time Slice가 1초일 때P2 (1초) 작업 후 I/O 작업 요청P1 (1초) 실행P2 I/O 작업이 마무리되지 않았기 때문에 P1 준비 큐에 삽입되었다가 바로 실행10 번 과정P2 I/O 작업 완료 후, 인터럽트 실행, 준비 큐 삽입P2 1초 실행 후 I/O작업 - 1번 CPU 사용률을 100% 지만 I/O 사용률은 10/101 (I/O 작업 시간 / 두 프로세스 총 실행시간) = 약 10%- 2번 CPU 사용률을 100% 지만 I/O 사용률은 10/11 (I/O 작업 시간 / 두 프로세스 총 실행시간) = 약 90%- 1번에 비해서 CPU와 I/O 이용률이 높아 효율적이지만- 컨텍스트 스위칭을 자주하기 때문에 P1은 손해- P2는 이득 ✅ 특징- MLFQ는 기본적으로 CPU 사용률과 I/O 사용률이 좋게 나오는 타임 슬라이스를 가진다.- CPU Bound Process는 큰 타임슬라이스를 준다.- 주어진 타임 슬라이스 내에 작업이 완료되면 I/O Bound Process, 초과하면 CPU Bound Process로 추측- 우선순위가 매겨진 여러 큐를 준비하여 우선순위가 높을 수록 타임 슬라이스가 크게 설정한다.- 타임슬라이스가 초과될수록 점처 우선순위가 낮은 큐로 이동- 결국 FIFO처럼 프로세스 연속적으로 작업이 가능 📌 더 찾아본 점❓선점형(preemptive) VS 비선점형(non-preemptive)?✅ 선점형 스케쥴링은 하나의 프로세스가 현재 사용중인 CPU의 사용권을 점유할 수 있다.반대로 비선점형 스케쥴링은 하나의 프로세스가 마무리되어야 CPU의 사용권을 할당받을 수 있다. ❓추가적인 스케쥴링 방식?✅1. SRTF(Shortest-Remaining-Time First)- SJF의 단점 "100초짜리 A가 실행 중에 1초, 2초의 B,C 프로세서가 준비 상태라면, 100 초를 기다려야 한다"- SJF는 비선점형이기 때문에 작업을 기다려야 하지만, STCF는 큐의 남아있는 작업 시간을 계산하여 CPU의 점유를 뺏는 선점형이다.- 100초 A작업 도중 2초 작업 B가 도착하면, A의 남은 작업 시간과 비교하여 짧은 B가 먼저 실행된다.2. Priority- 비선점형: 도착한 프로세스마다 우선순위가 부여되고 동시에 도착한 프로세스의 경우 우선순위로 실행- 만약 우선순위가 낮더라도 먼저 실행 중이라면 작업이 종료될 때까지 대기- 선점형: 도착한 순서가 다르더라도 프로세스의 priority에 따라 높은 우선순위 별로 CPU 점유3. MLQ(Multi-Level Queue)- 여러 개의 우선순위가 부여된 큐로 관리되며 프로세스의 우선순위에 따라 각 큐에 삽입- 프로세스의 타입, 특징, 중요도에 따라 우선순위가 부여 - I/O 작업은 백업과 같은 배치 작업보다 높은 우선순위를 갖는다.- 각 큐의 요구조건마다 다른 CPU 스케쥴링 알고리즘을 사용한다.1. 고정 우선순위 선점 스케쥴링 (선점형)- 큐별로 우선순위가 고정되어 있고, 가장 하위 큐는 상위 큐에 프로세스가 없을 때만 동작이 가능하다.- 하위 큐의 프로세스가 동작하는 도중, 상위 큐의 프로세스가 들어오면 CPU 사용권을 빼앗긴다.2. 타임 슬라이싱 (비선점형)- 큐의 우선순위 별로 CPU 사용 시간의 점유율을 산출한다. > 1순위 50% / 2순위 30% / 3순위 20%- 프로세스의 큐 간 이동이 불가하여 유연성이 떨어진다.- 특정 큐에 프로세스가 몰리거나 우선순위 큐에 의해 하위 우선순위의 프로세스 대기 시간이 길어질 수 있음(기아 현상)4. MFLQ(Multi-Level Feedback Queue)- MLQ에서 큐 간 이동이 불가하여 유연성이 부족한 문제 극복- 우선순위에 따른 여러 큐가 존재하며 프로세스의 우선순위는 동적으로 변경된다.- CPU 점유 시간을 초과하면 우선 순위가 낮아지며, 점유 시간 이내에 작업이 마무리된다면 우선 순위 유지- 에이징 - 모든 프로세스를 일정 주기마다 최상위 큐로 이동 / 오래 기다린 프로세스 상위 큐로 승격 (기아 현상 방지) 📔 회고블로그 글을 통해 "인프런 워밍업 클럽"에 대해 알게 되었고 마침 CS 공부를 계획하였던 나는 새로운 기수가 시작되자마자 바로 신청하였다.이번에 CS 로드맵에 참여하면서 목적없이 강의를 듣고 공부하기 보다는이 로드맵을 통해 어떤 것을 얻고 싶고 클럽이 진행하는 동안 어떤 것을 꾸준히 지켜나갈 것인지 정하여 공부하는 것이 나의 성장을 더 효율적으로 끌어올려 줄 것 같아, 미리 선정하고 수업에 임하였다. 🚀 최종 목표 : 더 효율적인 백엔드 개발을 위해 기본적인 운영체제 지식들을 확실히 잡아가기🚀 매주 규칙:각 섹션마다 하나의 .md 파일을 생성하고, 섹션 내 각 유닛은 헤더로 구분강의를 듣고 최대한 이쁘게 (?) 정리해놓기매 강의를 듣고 궁금하거나 이해가 안가는 부분은 추가적으로 더 찾아서 정리해두기 강의를 듣고 정리하는 것이 손에 익지 않아, 초반에는 공부하는 시간보다 틀을 잡는데 시간이 더 소요되었지만점차 강의를 듣고 정리하는 방법이 손에 익으면서 정리하는 시간이 많이 줄었고 요약에 대한 아웃라인도 대강 잡혀나갔다. 다만 회고를 하다보니 목표는 "백엔드 관점에서의 운영체제"로 설정하였는데, 1주차는 운영체제에 대해서만 공부를 한 것 같아서 다음 주부터는 각 강의를 듣고 백엔드 관점에서 고민해보는 시간을 가져보고 다양한 의견을 GPT와 주고 받으며 정리해보면 좋을 것 같다. 매 강의 내용을 백엔드 관점에서 고민해보고 GPT와 대화를 통해 정리    

시스템 · 운영체제운영체제워밍업클럽감자CS

강동훈

[인프런 워밍업 클럽 3기 - CS] - 1주차 발자국 (운영체제) - 2

💻 프로세스와 쓰레드 📌 프로그램과 프로세스 프로그램: 저장장치에 저장된 명령문의 집합체 (Application, .exe,,,) 프로세스: 실행 중인 프로그램. "실행 중"은 하드디스크에 저장된 프로그램이 메모리에 올라갔을 때를 의미한다. 프로그램은 저장장치에 저장만 되는 수동적인 존재이지만, 프로세스는 메모리, CPU 스케줄링 알고리즘, 입/출력도 사용하는 능동적인 존재.  ✅ 프로세스의 구조1. 코드 영역 - 자신을 실행한는 코드가 저장2. 데이터 영역 - 스태틱 변수와 전역 변수 저장3. 스택 영역 - 지역 변수와 함수 호출을 위한 정보 저장4. 힙 영역 - 프로그래머가 동적으로 메모리를 할당 - free(), malloc()을 통해 메모리 관리 ✅ 컴파일 과정1. 전처리기를 통해 매크로로 정의한 숫자를 치환하고 필요한 파일을 불러온다.2. 파일의 확장자(.i)3. 컴파일 (C언어 > 어셈블리어)4. 파일의 확장자(.s)5. 어셈블리어 > 기계어6. 파일의 확장자(.o)7. 링킹을 통해 파일의 확장자(.exe)로 변경 ✅ 프로세스 실행 과정1. 두 숫자를 더하는 코드를 작성2. 컴파일3. .exe 파일을 실행4. 메모리에 파일이 올라감 (프로세스)5. CPU 내 제어장치가 파일을 읽어가며 레지스터 저장 및 연산6. 결과 메모리 저장  📌 멀티프로그래밍과 멀티프로세싱 ✅ 유니 프로그래밍(메모리 관점) 메모리에 오직 하나의 프로세스가 올라온 것 (I/O 작업이 일어나면 대기) ✅ 멀티 프로그래밍(메모리 관점) 메모리에 여러 개의 프로세스가 올라온 것 (I/O 작업 대기 시, 다른 프로세스 실행) ✅ 멀티 테스킹메모리에 올라온 여러 프로세스들을 번갈아가며 짧게 실행시키며 동시에 실행시키는 체감 ✅ 멀티 프로세싱(CPU 관점) CPU가 여러 개 있는 것을 멀티 프로세서 라고 하며, 멀티 프로세서로 작업하는 것을 멀티 프로세싱 이라고 한다. 과거-> 유니 프로그래밍 + 스와핑 방식1. 메모리에 하나의 프로세스를 올려 처리 한 후, 저장장치에 저장2. 다른 저장장치의 프로세스를 메모리에 올려 CPU 처리3. 스와핑이란 하나의 프로세스가 마무리되면 저장장치로 내리고 새로운 프로세스 메모리에 올리는 기법 현재-> 멀티 프로그래밍 + 멀티 프로세싱 방식1. 메모리에 여러 프로세스가 올라오고2. 시분할 처리를 통해 각각의 프로세스 짧은 시간동안 교대로 처리  📌 PCB운영체제는 여러 개의 프로세스를 전부 관리하고 공평하게 실행해야 한다.프로세스 관리는 PCB(Proccess Control Block) 를 생성하여 연결리스트의 자료구조로 관리한다. 프로세스 종료 시에는 연결리스트에서 해당 PCB를 제거한다.  ✅ PCB 구조1. 포인터: 부모, 자식 프로세스에 대한 포인터를 저장 - 프로세스 계층 구조 파악 및 프로세스 종료 시, 프로세스 상태 추적 가능2. 프로세스 상태: 현재 어떤 상태에 있는지 나타내는 정보 (생성, 준비, 실행, 대기, 완료)3. 프로세스 ID: 프로세스 식별자 저장 - 프로세스는 고유한 PID를 가짐 - 프로세스 구분하는데 사용되며 시스템 내에 유일해야 함4. 프로그램 카운터: 다음에 실행될 명령어의 주소를 저장 - 현재 실행 중인 명령어의 다음 주소를 저장 - Context Switch가 발생하면, 해당 프로세스가 진행해야 할 명령어의 주소로 복구5. 레지스터 정보: 프로세스가 실행될 때 사용된 레저스터 정보 - 프로세스 중단, 전환 시, PCB에 레지스터 값 저장한 후, 다시 실행될 때 복구6. 메모리 관련 정보: 프로세스가 메모리에 저장된 위치 정보 - 프로세스가 사용하는 메모리 영역(코드, 데이터, 스택, 힙,,)과 주소를 관리 - 가상 메모리 시스템에서 중요 - 프로세스 간 메모리 영역 분리7. CPU 스케줄링 정보: 우선순위, 최종 실행시간, 점유 시간 등 CPU를 얼마나 효율적으로 사용할 지에 대한 정보8. I/O 상태 정보 : 프로세스가 현재 어떤 입출력 작업을 수행 중인지에 대한 정보9. 열린 파일 목록(Open File List) : 현재 열고 있는 파일들의 목록 관리  📌 프로세스 상태1. 생성 (New): 메모리에 프로그램 적재 요청 - PCB 생성2. 준비 (Ready): CPU 자원 할당을 위해 기다리는 상태3. 대기 (Waiting): 입출력 요청 시, 입출력이 완료될 때까지 대기하는 상태 - 입출력 완료까지 CPU 대기는 비효율적 - 입출력을 기다리는 것은 대기 상태로 두고 CPU는 다른 작업 (시스템 자원 낭비 최소화) - 입출력이 완료되면 준비 상태로 전환4. 실행 (Running): 스케쥴러에 의해 CPU를 할당받아, 부여된 시간만큼 실행되는 상태 - CPU 개수만큼 실행 프로세스가 생성 - 부여된 시간을 초과하면 CPU를 강제로 빼앗으며 준비 상태로 전환. - 실행 도중, 입출력 요청을 하면 다시 준비 상태로 전환5. 완료 (Terminated): 프로세스 종료 - 메모리에서 데이터 제거 및 PCB 제거  📌 컨텍스트 스위칭프로세스를 실행 중인 상태에서 다른 프로세스를 실행하기 위해 실행중인 프로세스 상태를 저장하고 교체운영체제에 의해 실행 중인 프로세스의 내용이 PCB에 수정되고, 다음 프로세스의 PCB 내용대로 CPU가 세팅 됨.  ✅ 과정1. 프로세스 A의 CPU 점유 시간 초과2. 인터럽트 발생3. CPU의 레지스터 값 등을 PCB A에 저장4. PCB B를 참조해서 이전 프로세스 B의 작업을 이어감 - 프로그램 카운터(다음에 실행될 명령어 주소)를 통해 이어서 명령어 실행 가능5. 프로세스 B의 CPU 점유 시간 초과6. 인터럽트 발생7. CPU의 레지스터 값 등을 PCB B에 저장  📌 프로세스 생성과 종료 프로세스 생성1. .exe 실행2. 코드 영역, 데이터 영역를 메모리에 올려 스택과 힙 구조의 공간을 확보.3. PCB를 만들어서 값을 초기화컴퓨터 부팅 시에 0번 프로세스가 한번만 생성되며, 나머지 실행되는 모든 프로세스는 fork()로 복사하여 자식프로세스를 생성한다.exec()함수를 통해 자식 프로세스의 코드를 복사된 부모 프로세스의 코드에 덮어쓸 수 있다. #include <stdio.h> #include <unistd.h> int main() { int pid; pid = fork(); // 부모 프로세스는 pid = 1; // 자식 프로세스는 pid > 1; // 실패 시 -1 리턴 if(pid == 0){ // 자식 execlp("InternetBrowser", "0", NULL); exit(0); } else { // 부모 wait(NULL); printf("인터넷 브라우저 닫힘"); exit(0); } }1. 해당 프로세스를 fork()를 통해 부모와 자식 프로세스는 동일한 코드를 갖고 있는다. (pid는 서로 다름)2. CPU 스케쥴링에 따라 프로세스가 실행된다.3. 만약 부모 프로세스가 먼저 실행되면, 조건문에 의해 else절이 실행4. wait() 함수는 exit() 신호가 오기 전까지 하위 코드를 실행하지 않음5. 스케쥴링에 의해 자식 프로세스로 스위칭6. if절이 실행되어 execlp()를 통해 InternetBrowser 프로세스가 실행7. 인터넷 브라우저 실행에 실패할 경우, exit() 함수가 실행 (성공 시, InternetBrowser 프로세스 실행)8. 스케쥴링에 의해 부모 프로세스로 스위칭9. 자식 프로세스의 exit() 신호로 wait() 함수를 통과하여 하위 코드가 실행exit(): 자식 프로세스가 부모 프로세스에게 정상 종료를 알리는 함수부모 프로세스는 자식 프로세스의 exit status를 읽고 자식 프로세스를 정리❗ 부모 프로세스가 자식 프로세스보다 먼저 종료되거나, 자식 프로세스의 비정상 종료 시 좀비 프로세스라고 부른다.  📌 쓰레드프로세스가 생성될 때마다 PCB와 메모리(코드, 데이터, 스택, 힙 등)영역을 만들어줘야 한다. 웹브라우저의 탭이 추가될 때마다 프로세스의 복사가 계속해서 이루어진다면 시스템 자원의 낭비가 점차 늘어난다.=> 하나의 프로세스 안에 1개 이상의 쓰레드를 생성.운영체제는 프로세스 내 쓰레드 단위로 관리 비교1. 안정성: 프로세스는 독립적이기 때문에 다른 프로세스의 영향을 받지 않지만 쓰레드는 자원을 공유하기에 프로세스에 문제가 생기면 모든 프로세스에 영향을 끼친다.2. 속도와 자원: 각 프로세스는 IPC를 통해 다른 프로세스와 통신을 해야하니 오버헤드가 크다. 쓰레드간에 통신은 자원이 공유되니 오버헤드가 적다.  📌 더 찾아본 점❓프로그래밍 개념 구분?❓멀티 프로그래밍과 멀티 태스킹의 차이?✅ 멀티 프로그래밍은 메모리 관점에서 여러 프로그램을 메모리에 올려두고 CPU가 필요할 때마다 실행멀티 테스킹은 CPU 관점에서 여러 작업을 빠르게 전환(시분할)하며 동시에 작업하는 것처럼 보임 ❓`exec()`함수는 무엇인가?✅ 자식 프로세스가 새로운 프로그램을 실행할 때, 자식 프로세스의 코드, 데이터, 힙, 스택 등을 새로운 프로그램으로 덮어쓰는 함수.- 자식 프로세스의 메모리 공간을 새로운 프로그램으로 대체하며 exec() 이후에는 자식 프로세스 코드가 실행되지 않음.- execl(), execp(), execv(), execvp(), execlp() 등이 exec()함수의 계열에 속한다.- 예제 코드에서 execlp("InternetBrowser", "0", NULL); 가 성공적으로 실행되면 자식 프로세스 코드가 새로운 프로세스로 대체되었기에, 하위 exit(0) 코드는 영영 실행되지 못하지만 실패 시에는 하위 코드가 실행된다. ❓좀비 프로세스는 무엇인가?✅ 부모 프로세스가 자식 프로세스의 종료 상태를 수거하지 않아 남아있는 프로세스- 실행되지 않은 채로 PCB만 남아있음- 메모리를 계속 점유하고 있어, 시스템 자원을 낭비함 ❓쓰레드는 어떤 정보를 포함하고 있는가?✅ TCB(Thread Control Block)에 쓰레드의 상태 및 제어 정보를 저장1. TID & PID: 쓰레드 식별 고유 ID, 쓰레드가 속한 프로세스 ID2. State: 쓰레드의 실행 상태 (`Running`, Waiting, Ready, Terminated)3. Register Contents: 스위칭 복원을 위한 CPU 레지스터 정보4. Program Counter: 쓰레드 마지막 명령어 주소5. Stack Pointer: 쓰레드 스택에서 현재 실행 중인 함수 위치 포인터6. Priority : 여러 쓰레드 동시 실행 시, 우선순위 

시스템 · 운영체제운영체제'워밍업클럽CS감자

szun

워밍업 클럽 스터디 3기(PM) 1주차 발자국

강의명: 시작하는 PM/PO들에게 알려주고 싶은, 프로덕트의 모든 것코치: 김민우링크: https://www.inflearn.com/course/%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-pmpo-%EB%AA%A8%EB%93%A0%EA%B2%83/dashboard1주차 강의 포인트PM의 역할은 제품으로써 고객 가치와 사업적 가치를 만들어 내는 것여기서 제품이 '성공적인 프로덕트'이기 위해서는 4가지 조건이 필요함.ValuableUsableFeasibleViablePM은 주로 Valuable과 Viable을 담당하는 역할임PM은 고객, 데이터, 비즈니스, 산업에 대한 심층적인 지식을 가지고 있어야 함.결국 중요한 것은 PMF(Product/Market Fit)을 찾는 것!문제를 잘 정의해야 해결 할 수 있다.문제를 잘 정의 했다면 이제는 해결책을 만들 차례, 좋은 해결책을 내기 위해서는 다음 두 가지가 필요좋은 아이디어이터레이션(Iteration): 만들고, 테스트하고, 학습하기를 반복하여 제품을 발전시키는 것하지만 모든 고객 문제들이 비즈니스로 연결되지는 않는다는 것을 명심해야 함.우리는 Problem Worth Solving, 즉 해결할 가치가 있는 문제를 찾아서 해결해야 함. 1주차 회고스타트업을 창업하면서 항상 중요하다고 인식을 해온 개념이었지만, 과연 내가 실천하고 있었을까 돌아보는 시간이 되었다. 특히 문제 정의 부분과 나의 문제 assumption이 해결할 가치가 문제가 맞는지를 판단하는데에 막연함이 있었는데, 코치님께서 알려주신 해당 상황에서의 판단 기준은 앞으로 나의 업무에 있어 큰 도움이 될 것 같은 기준들이었다. 강의를 들으면서도 결국 중요한 것은 내가 직접 실천하여 나의 것으로 만드는 과정이라는 생각이 들었다. 업무를 할 때에 있어 의식적으로 생각의 방향을 잘 정립하여 수행해보고자 한다. 

기획 · PM· PO

윤기숙

[인프런 워밍업 클럽 스터디 3기] 프로덕트 디자인 1주차 발자국

📖 1주차 - 학습 내용 정리1. 피그마 배리어블과 디자인 토큰 개념 정리피그마 배리어블과 디자인 토큰의 개념 다시 한번 정리하기 Semantic이 중요하게 필요한 이유 Mode로 구분 가능하고, 로컬 베리어블 관리가 필요하기 때문2. 배리어블과 파운데이션 세팅하기색상, 타이포그래피, 간격, 선, 그림자, 그리드 등 베리어블 직접 등록해보기다양한 효과(Effect) 알아보기, Drop Shadow외에도 다른 효과들도 사용해보기높낮이(Elevation)는 UI 요소 간의 계층 구조를 시각적으로 명확하게 나타내고, 베리어블로 정리하면 유용그리드 종류 Fixed Grid(centered), Fluid Grid(stretch), Hybrid Grid 살펴보기.디바이스별 반응형 디자인 기준점(viewport) 정리해보고, Min width와 Max width를 지정해주기.3. 유용한 피그마 플러그인들 적용해보기  💡 1주차 - 회고 및 느낀 점 😆 좋았던 점강의를 듣고 직접 실습하면서 배리어블 개념, 용어, 피그마 플러그인등을 정리할 수 있었던 점이 좋았다.UI3에 맞춰서 업데이트 된 강의를 들을 수 있는게 너무 좋았다. 이전에 강의 들을 당시 예전 UI와 다른점이 많아서 이해하기 어려운 부분들도 있었는데, 이번에 다시 정리하면서 들으니 너무 좋았고, 좀더 체계적으로 추가 업데이트 된 내용을 배울 수 있어서 좋았다.😅 아쉬웠던 점스터디 첫 주였는데 계획했던 것처럼 매일 강의를 듣지는 못했고, 정해진 시간에 아직 루틴화 되지 못한 점, 강의 따라가기에 바빠서 스타일가이드 문서화 정리까지는 제대로 못했던 점이 아쉽다.😍 앞으로 바라는 점다음 주부터는 정해진 시간에 조금씩이라도 매일 공부하고, 문서화 하는 부분도 염두해두면서 작업해보자 💪 

UX/UI인프런_워밍업_클럽디자인시스템FigmaUX/UI

최범수

CQRS와 조회 전용 저장소, 그리고 카테고리-상품 관계 고민기

도메인 주도 설계를 배우면서 겪은 고민최근 도메인 주도 개발(DDD)에 관한 책을 읽으면서 여러 가지 설계에 대한 깊은 고민을 하게 되었다. 특히 관심을 끈 부분은 CQRS(Command Query Responsibility Segregation) 개념이었다. 이 글에서는 공부한 CQRS 개념과 함께, 카테고리와 상품 사이의 관계를 어떻게 효율적으로 설계할 수 있는지 정리해 보려고 한다. 쓰기 모델과 조회 모델을 분리한 이유일반적인 웹 애플리케이션은 데이터베이스 하나에서 읽기 쓰기를 모두 처리한다. 이렇게 구현해도 초반에는 큰 문제없이 잘 동작하지만, 서비스가 성장하면서 읽기(조회) 트래픽이 급증하면 다음과 같은 문제가 발생할 수 있다.읽기 요청 증가로 인한 데이터베이스 성능 저하복잡한 검색 및 필터링으로 인한 성능 한계이를 해결하기 위한 방법 중 하나가 CQRS 이다. CQRS의 핵심 개념쓰기(명령)와 읽기(쿼리)의 책임을 분리쓰기 작업은 RDB에 반영하고, 읽기는 ElasticSearch나 Redis 같은 빠른 저장소를 활용CQRS 흐름쓰기 발생: 데이터를 RDB에 저장이벤트 또는 동기화: Kafka 등을 통해 조회 전용 저장소(ElasticSearch)에 데이터 동기화조회 요청 처리: 조회 전용 저장소에서 빠르게 검색/조회이를 통해 조회 성능 향상과 시스템 부하 분산을 얻을 수 있다. 하지만 이벤트 동기화 과정에서 구현 복잡도가 늘어나고, 실시간성이 필요한 경우 동기화 지연이 단점으로 작용할 수 있다.간단 예시: CQRS 적용 시나리오 상품 생성 시:상품 정보를 RDB에 저장이벤트를 발생 (예: "상품 생성됨")이벤트 소비자가 ElasticSearch에 해당 상품 정보를 인덱싱사용자 조회 시:ElasticSearch에서 상품 목록 검색 -> 빠른 검색 결과 반환카테고리와 상품 관계 설계카테고리와 상품 간의 설계는 애플리케이션 성능, 특히 페이징 처리와 깊은 관련이 있다.데이터 조회와 성능에 큰 영향을 미치는 두 가지 대표적인 설계 방식을 자세히 살펴보자. N-1 관계 (상품 -> 카테고리 참조)N-1 관계는 상품(Product)이 특정 카테고리(Category)를 직접 참조하는 방식이다. 일반적으로 상품이 반드시 하나의 카테고리에만 속할 때 이 방식을 사용한다.@Entity public class Product { @Id private Long id; @ManyToOne @JoinColumn(name = "category_id") private Category category; }장점특정 카테고리에 속한 상품을 효율적으로 페이징 가능하다.category_id컬럼에 DB 인덱스를 설정하면 빠른 조회가 가능하다.단점카테고리에서 직접적으로 상품 목록에 접근하는 것이 어렵다.N+1 문제가 발생할 가능성이 높다.이 문제는 JPA의 Fetch Join이나 EntityGraph 같은 기법으로 해결할 수는 있으나 본 글의 내용과는 관련성이 떨어지기 때문에 자세히 다루진 않도록 하겠다.  M-N 관계 (상품 <-> 카테고리 ID로 연결)M-N 방식은 하나의 상품이 여러 개의 카테고리에 속할 때 사용하는 구조로, 중간 테이블을 사용하여 다대다 관계를 처리한다. 특히 ID기반으로 연관관계를 표현하여 직접적인 엔티티 참조를 피하는 방식이다.@Entity public class Product { @Id private Long id; @ElementCollection @CollectionTable(name = "product_category", joinColumns = @JoinColumn(name = "product_id")) @Column(name = "category_id") private Set<Long> categoryIds; }장점엔티티 간 직접적인 양방향 참조 없이 ID로만 관계를 맺어 결합도를 낮춘다.데이터가 많은 때도 페이징 및 필터링을 DB에서 효과적으로 처리할 수 있다.단점특정 카테고리에 속한 상품을 조회하려면 추가적인 쿼리가 필요하다.카테고리와 상품 정보를 함께 조회할 경우 성능 저하가 발생할 수 있다. 어떤 방식을 선택해야 할까?관계를 설계할 때는 애플리케이션의 사용 패턴과 비즈니스 요구사항을 깊게 고민해야 한다. 각각의 관계 방식은 다음과 같은 상황에 효과적이다.N-1 방식상품이 주로 하나의 카테고리에 속하는 경우상품 중심으로 자주 조회하는 기능이 필요할 때조회 성능 및 빠른 페이징이 중요한 경우M-N 방식하나의 상품이 여러 카테고리에 동시에 속할 수 있는 경우다양한 조건으로 필터링하거나 복잡한 조회가 필요한 경우많은 데이터를 효율적으로 처리해야 할 경우 글을 마무리하며이 글을 작성하면서 설계 방식 하나를 선택하는 일이 얼마나 중요한 고민인지 새삼 느끼게 되었다.이론적으로는 두 방식 모두 장단점이 있기 때문에 어느 하나가 더 우월하다 말할 수는 없겠지만, 결국 실제 서비스에서 어떤 방식이 더 효율적일지는 요구사항을 고려하여 더 효율적인 선택을 하는건 개발자의 몫인 것 같다.앞으로도 단순히 이론에 그치지 않고 실무에서 끊임없이 고민하며 상황에 맞는 좋은 설계 방식을 찾아가야겠다는 생각이 들었으며 끝으로 이 글의 내용과 비슷한 고민을 하고 있는 사람들에게 조금이나마 도움이 되었기를 바란다.

백엔드CQRSManyToOneManyToMany조회전용저장소

Pleos25

[현대자동차그룹] 개발자 컨퍼런스 'Pleos 25' 참가자 모집 (~3/27)

개발자들을 위한 새로운 무대, Pleos 25지난 4년간 개발자들과 모빌리티 생태계를 만들어온 HMG developer conference가현대자동차그룹의 통합 소프트웨어 브랜드 'Pleos'와 함께 완전히 새로워진 모습으로 찾아왔습니다.​🧐Pleos가 무엇인가요?Pleos는 HMG의 소프트웨어 역량을 하나로 모은 그룹사 통합 소프트웨어 브랜드입니다.📍Pleos 25는 어떻게 진행되나요? 이번 행사 현장에 오시면 SDV, AI, 자율주행 등 다양한 주제의 트랙 세션을 들으실 수 있고, 차량용 앱 개발 및 SDK 활용 실습을 할 수 있는 핸즈온 세션에 참여하실 수 있습니다!관심있는 개발자 분들 모두 참가하실 수 있으니 많은 관심 부탁드리며 현대자동차그룹 전문가들과 함께 개발 경험과 지식을 나누어 성장하는 기회가 되셨으면 좋겠습니다.다가오는 3월 28일, 코엑스에서 열릴 'Pleos 25'에서 만나요!▪일시 : 2025년 3월 28일 (금) 오전 10:00▪장소 : 코엑스 오디토리움 & 전시홀 D▪참가 대상 : 차량용 앱 생태계에 관심있는 개발자 및 비즈니스 파트너▪행사 구성- Track : SDV, AI, 자율주행 등 다양한 분야의 개발자들이 전하는 생생한 개발 경험과 인사이트를 공유합니다.- Exhibition : 차량 데이터를 활용한 현대자동차그룹과 파트너사들의 다양한 협업 사례와 모빌리티 최신 기술 및 솔루션을 만날 수 있습니다.- Hands-on : 차량용 앱 개발 및 SDK 활용 방법에 대해 직접 체험해 볼 수 있습니다.- 채용 상담 : 현대자동차그룹에서 제공하는 다양한 분야의 채용 상담도 준비되어 있습니다.▪사전 등록 기간 : 2월 24일 (월) ~ 3월 27일 (목)▪신청 방법 : 홈페이지 내 '참가 신청하기'를 통해 사전 등록▪홈페이지 : https://pleos.ai/pleos25▪참가비 : 무료👉문의Pleos 25 운영 사무국 pleos.help@hyundai.com

모빌리티개발자컨퍼런스강연행사자동차모빌리티IT정보

애완로트와일러개

[K-DEVCON] 고투런 2기 멘티 모집!

[유료] K-DEVCON 고투런(Go-to-Learn) 2기 모집고투런이란?고투런(Go To Learn)이란 기술적 성장을 원하는 신입, 주니어 개발자들을 위한 멘토링 트랙은 최소 4주, 최대 8주간 미들급 이상의 개발자들이 멘토가 되어 직접 멘토링 주제 기획부터 멘티 선발, 학습까지의 과정을 진행하는 멘토 리딩 트랙입니다. 1⃣ 멘티 모집 대상, 모집 기간, 참가비📌 모집 대상: 기술적 성장이 필요한 신입 ~ 주니어 개발자 (전/현직 개발자분들을 대상으로 합니다) 📅 모집 기간: 2월 15일 ~ 2월 28일 23:59까지 📢 선발 발표: 3월 5일 💰 참가비: 50,000 2⃣ 지원 절차📌 지원서 제출 -> 멘토님의 지원서 검토 (또는 인터뷰) -> 합류 결과 안내 📝 지원 링크: https://forms.gle/c4sFfYFxpwgRVPRw93⃣ 멘토링 주제✅ 확장성, 회복탄력성, 고가용성을 보장하는 애플리케이션 설계 / 온오프라인 / 김정우 멘토님✅ 프로젝트로 배우는 안드로이드 프로그래밍 A to Z / 온오프라인 / 서준수 멘토님 ✅ 객체지향 101 - 객체지향을 쓰게된 이유 / 오프라인 / 이현재 멘토님✅ 20년차 AI 엔지니어에게 배우는 AI 에이전트의 모든 것 / 온오프라인 / 정유선 멘토님4⃣ 멘토링 세션에서 얻을 수 있는 것🚀 멘티가 얻을 수 있는 혜택:🔹 사수가 없거나 도움을 받기 어려운 상황이라면 검증된 멘토와 함께 빠르게 성장할 수 있다.🔹 기술적 성장을 목표로 하는 트랙이기 때문에, 본인의 실무 역량 향상에 도움을 받을 수 있다.🔹 4 ~ 8주간의 깊이 있는 멘토링을 통해서 기술 성장 이상의 깊이있는 성장 경험을 할 수 있어 향후 커리어 빌딩에도 도움을 받을 수 있다.자세한 내용은 Go To Learn 노션을 참고해주세요.https://bluepicture08.notion.site/K-DEVCON-Go-To-Learn-2-16becf714ca380f58b78edac54401608📩 문의: [이메일 or Slack DM]info@k-devcon.com 지금 바로 지원하고 성장할 기회를 잡으세요! 🚀K-DEVCON이 궁금하다면? 홈페이지 / Slack 대화방 / 링크드인 / 인스타그램

커리어 · 자기계발 기타K-DEVCON스터디멘토링

Masocampus

[Gen AI 인사이트] HeyGen, AI로 가상의 캐릭터를 만들다!

AI 기술의 발전으로 이제 원하는 캐릭터를 만들고, 자연스러운 목소리까지 입힐 수 있어요.오늘 소개할 HeyGen은 가상의 인물과 음성을 생성하는 혁신적인 AI 기술입니다.HeyGen은 AI 기술을 활용해 가상의 인물과 음성을 생성하는 플랫폼이에요.실제 사람처럼 말하고 움직이는 AI 캐릭터를 쉽게 만들 수 있죠!특히 영상 제작을 위한 캐릭터가 필요할 때, 빠르고 효율적인 솔루션을 제공합니다.1. AI 기반 아바타 생성사용자가 원하는 외모의 캐릭터를 AI가 만들어 줘요.기본 제공 아바타를 선택하거나, 직접 제작할 수도 있어요.​2. 자연스러운 음성 합성한국어를 포함해 다양한 언어로 자연스럽게 말할 수 있어요.별도의 성우 녹음 없이 원하는 대사를 입력하면 음성이 생성됩니다.​3. 입모양 싱크 기술대사에 맞춰 입모양까지 자동으로 조정되어 더욱 자연스러운 영상이 완성돼요.1단계: 캐릭터 선택: 기본 제공 아바타를 선택하거나 직접 만들 수 있어요.2단계: 스크립트 입력: 원하는 대사를 입력하면 AI가 음성을 생성해 줍니다.3단계: 입모양 & 표정 조정: 말하는 방식에 맞춰 입모양과 표정을 자연스럽게 수정할 수 있어요.4단계: 영상 제작 & 활용: 완성된 영상은 SNS, 교육 콘텐츠, 마케팅 영상 등 다양한 분야에서 활용할 수 있습니다!기존 영상 제작촬영 및 편집 시간이 오래 걸림성우 녹음이 필요해 비용이 높음 수정이 어려움HeyGen 활용AI가 자동으로 생성하여 제작 시간이 단축됨별도의 성우 없이도 음성을 합성할 수 있어 비용 절감간단한 수정만으로도 빠르게 콘텐츠를 업데이트할 수 있음HeyGen은 다양한 곳에서 활용할 수 있어요! 마케팅 영상 – 기업 홍보 및 광고 콘텐츠 제작교육 콘텐츠 – 온라인 강의 및 튜토리얼 영상 제작가상 인플루언서 – 유튜브 및 SNS 콘텐츠 제작엔터테인먼트 – 애니메이션, 게임 캐릭터 활용자연스러운 AI 음성 합성 : 실제 사람처럼 말하는 AI 음성 제공빠르고 간편한 영상 제작 : 복잡한 편집 과정 없이 영상 완성다국어 지원 : 다양한 언어로 콘텐츠 제작 가능맞춤형 캐릭터 생성 : 원하는 외모와 목소리를 가진 AI 캐릭터 제작 가능1. 기업 홍보 영상 제작 – 브랜드 광고 및 마케팅 콘텐츠2. 온라인 강의 및 튜토리얼 – 교육 콘텐츠 제작3. 유튜브 및 SNS 콘텐츠 – 가상 캐릭터를 활용한 콘텐츠 제작4. 가상 캐릭터 기반 광고 – 디지털 마케팅에 활용HeyGen을 활용하면 빠르고 간편하게 고품질 영상 제작이 가능합니다.촬영이나 성우 비용 없이 비용 효율적인 콘텐츠 제작이 가능해요.마케팅, 교육, SNS 등 다양한 분야에서 활용할 수 있는 강력한 도구이니 꼭 사용해보세요!마소캠퍼스와 함께 AI를 활용해 업무 혁신을 이뤄보세요!효율적이고 스마트한 일의 방식을 통해 성장할 수 있도록 도와드릴게요. 📌관련 강의<미드저니 비법 클래스, 현직 AI 디자인 전문가의 이미지 프롬프트 엔지니어링 핵심 정리>기초 사용 방법부터 실제 디자인 프로젝트 실습을 통한 실무 적용까지 한 번에!

AI · ChatGPT 활용HeyGenAI영상제작가상캐릭터마케팅영상가상인플루언서디지털혁신AI기술SNS콘텐츠브랜딩광고제작

징니

인프런 워밍업 클럽 3기 BE 스터디 4주차

💻 강의입문자를 위한 Spring Boot with Kotlin - 나만의 포트폴리오 사이트 만들기 📚 학습@Transactional(readOnly = true)읽기 전용 트랜잭션으로 설정하면 데이터 변경이 일어나지 않기 때문에 스냅샷을 저장하는 동작을 생략해 좀 더 성능을 개선할 수 있다readOnly 참고Service 테스트@ExtendWith : 테스트 확장을 지원하며 JUniit5와 Mockito를 연동해 테스트를 진행할 경우에는 MockitoExtension.class를 사용@InjectMocks : Mockito에서 테스트 대상이 되는 클래스에 인스턴스를 주입하기 위해 사용@Mock : Mockito에서 Mock 객체를 생성할 때 사용하며 실제로 메서드는 갖고 있지만 내부 구현이 없는 상태참고 Repository 테스트를 했을 때와는 다르게 Service 테스트를 할 때는 Mockito를 사용한다@ExtendWith(MockitoExtension::class) class PresentationServiceTest { @InjectMocks lateinit var presentationService: PresentationService @Mock lateinit var presentationRepository: PresentationRepository }Service 테스트 코드 해석given일단 홀수이면 isActive = false, 짝수이면 true로 설정한다필터링을 해 isActive = true인 것만 남긴다presentationRepository.getActiveIntroductions()를 실행하면 필터링 한 데이터를 반환하도록 한다["2", "4", "6"]when테스트 대상 메서드를 실행한다thenDATA_SIZE = 7이므로 DATA_SIZE / 2 = 3.5이지만 정수 연산에서는 3이 반환 된다필터링 한 데이터 수가 일치하면 첫 번째 검증은 통과이다content 값을 정수로 변환하고, isEven()을 사용해 짝수인지 검증한다짝수이면 두 번째 검증도 통과이다@Repository class PresentationRepository( ...) { ... fun getActiveIntroductions(): List<Introduction> { return introductionRepository.findAllByIsActive(true) }... @Test fun testGetIntroductions() { // given val introductions = mutableListOf<Introduction>() for (i in 1..DATA_SIZE) { // 1, 3, 5, 7 -> false / 2, 4, 6 -> true val introduction = Introduction(content = "${i}", isActive = i % 2 == 0) introductions.add(introduction) } val activeIntroductions = introductions.filter { introduction -> introduction.isActive } Mockito.`when`(presentationRepository.getActiveIntroductions()) .thenReturn(activeIntroductions) // when val introductionDTOs = presentationService.getIntroductions() // then assertThat(introductionDTOs).hasSize(DATA_SIZE / 2) for (introductionDTO in introductionDTOs) { assertThat(introductionDTO.content.toInt()).isEven() } } Controller 테스트@SpringBootTest : 실제 애플리케이션과 유사한 환경을 구성하여 테스트 가능@AutoConfigureMockMVC : Spring MVC를 모의로 테스트하는 데 사용@SpringBootTest @AutoConfigureMockMvc @DisplayName("[API 컨트롤러 테스트]") class PresentationApiControllerTest(@Autowired private val mockMvc: MockMvc) { }Thymeleaf 문법th:fragment : 템플릿의 일부를 재사용 가능한 fragment로 정의th:replace : 해당 요소를 다른 요소로 대체할 때 사용 fragment 이름을 navigation으로 지정하고, 해당 경로에 있는 파일의 navigation fragment를 찾아서 대체한다// /templates/presentation/fragments/fragment-navigation.html <nav class="navbar navbar-expand-lg navbar-light bg-white py-3" th:fragment="navigation">// /templates/presentation/index.html <div th:replace="~{presentation/fragments/fragment-navigation :: navigation}"></div>  🎯 미션 6과 미션 7가상 프로필을 나의 프로필로 바꾸기강의 실습 프로젝트의 데이터 초기화 클래스 내용을 나의 프로필로 바꾼 뒤 커밋커밋 메시지 : [미션6] 가상 프로필을 나의 프로필로 바꾸기미션6 제출 스레드에 깃허브 커밋 링크를 공유프로젝트를 배포한 뒤 브라우저에서 접속미션7 제출 스레드에 도메인 주소를 공유문제이미지 태그를 지정하고 Dockerfile을 Build 하는 과정에서 test 실패 오류가 발생하였다테스트 코드가 문제인 건지 확인하기 위해 TestApplication을 실행해 전체 테스트 코드를 실행시키니 오류는 없었다원인을 찾을 수 없어서 계속 구글링을 해보니 아래 코드가 원인이 될 수도 있다고 한다tasks.withType<Test> { useJUnitPlatform() }해결아래 코드를 주석 처리하니 정상적으로 Build가 됐다tasks.withType<Test> { useJUnitPlatform() }참고

백엔드

wonderson

[워밍업 클럽 3기 CS 전공지식] 자료구조와 알고리즘 특별 미션

문제실수로 워밍업 클럽 출석을 빼먹었는데 우연히 데이터를 수정할 수 있는 권한이 주어졌습니다. 러너분의 이름(name)과 출석수(count)가 저장된 배열에서 여러분(나)의 데이터를 퀵정렬을 이용해 오름차순 정렬하고 가장 첫 번째 데이터인 여러분의 출석수를 변경하도록 코드를 작성해주세요. (퀵정렬 구현 부분도 변경)// 퀵소트 구현 부분...(생략) let user1 = { name: "홍길동", count: 5 }; let user2 = { name: "임꺽정", count: 4 } let user3 = { name: "이순신", count: 3 } let user4 = { name: "나", count: 1 } let user5 = { name: "짱구", count: 5 } let arr = [user1, user2, user3, user4, user5] console.log("===== 정렬 전 ====="); console.log(arr); quickSort(arr, 0, arr.length - 1); console.log("===== 정렬 후 ====="); console.log(arr);예상 결과===== 정렬 전 ===== [ { name: '홍길동', count: 5 }, { name: '임꺽정', count: 4 }, { name: '이순신', count: 3 }, { name: '나', count: 1 }, { name: '짱구', count: 5 } ] ===== 정렬 후 ===== [ { name: '나', count: 5 }, { name: '이순신', count: 3 }, { name: '임꺽정', count: 4 }, { name: '홍길동', count: 5 }, { name: '짱구', count: 5 } ] 문제 해결[깃허브 코드 - Java로 작성]이전에 작성한 퀵소트 정렬 코드 참고하기다른 점숫자 배열이 아닌 객체 배열을 만들어야 한다.객체 배열 안에서 count 로 오름차순을 한다.첫 번째 해결 - count로 오름차순하기name과 count를 함께 저장할 수 있는 Runner 클래스 생성이전 코드에서 int[] 배열 대신 Runner[] 배열로 변경package sectionThree.quickSort.specialMission; public class Runner { private String name; private int count; public Runner(String name, int count) { this.name = name; this.count = count; } public int getCount() { return count; } @Override public String toString() { return "{name: '" + name + "', count: " + count + '}'; } }package sectionThree.quickSort.specialMission; import java.util.Arrays; public class QuickSortMission { public static void quickSortM(Runner[] runners, int left, int right) { if (left <= right) { int pivot = divideM(runners, left, right); quickSortM(runners, left, pivot - 1); quickSortM(runners, pivot + 1, right); } } public static int divideM(Runner[] runners, int left, int right) { int pivot = runners[left].getCount(); int leftStartIndex = left + 1; int rightStartIndex = right; while (leftStartIndex <= rightStartIndex) { while (leftStartIndex <= right && pivot >= runners[leftStartIndex].getCount()) { leftStartIndex++; } while (rightStartIndex >= (left + 1) && pivot <= runners[rightStartIndex].getCount()) { rightStartIndex--; } if (leftStartIndex <= rightStartIndex) { swapM(runners, leftStartIndex, rightStartIndex); } } swapM(runners, left, rightStartIndex); return rightStartIndex; } public static void swapM(Runner[] runners, int index1, int index2) { Runner temp = runners[index1]; runners[index1] = runners[index2]; runners[index2] = temp; } public static void main(String[] args) { Runner user1 = new Runner("홍길동", 5); Runner user2 = new Runner("임꺽정", 4); Runner user3 = new Runner("이순신", 3); Runner user4 = new Runner("나", 1); Runner user5 = new Runner("짱구", 5); Runner[] runners = {user1, user2, user3, user4, user5}; System.out.println("===== 정렬 전 ====="); System.out.println(Arrays.toString(runners)); quickSortM(runners, 0, runners.length - 1); System.out.println("===== 정렬 후 ====="); System.out.println(Arrays.toString(runners)); } }결과===== 정렬 전 ===== [{name: '홍길동', count: 5}, {name: '임꺽정', count: 4}, {name: '이순신', count: 3}, {name: '나', count: 1}, {name: '짱구', count: 5}] ===== 정렬 후 ===== [{name: '나', count: 1}, {name: '이순신', count: 3}, {name: '임꺽정', count: 4}, {name: '홍길동', count: 5}, {name: '짱구', count: 5}] 두 번째 해결 - 가장 첫 번째 데이터인 여러분의 출석수를 변경3번 코드에서 추가한 부분quickSortM()을 호출하고 모든 재귀 함수 호출이 끝난 시점인 if 가 필요Runner 클래스에서 setCount() 을 호출해서 변경최소/최대 참여수는 불변값으로 변수 선언문제점이 있다. 5번에서 해결하기package sectionThree.quickSort.specialMission; public class Runner { private String name; private int count; private static final int MIN_CHECK = 0; // 최소 참여수 private static final int MAX_CHECK = 5; // 최대 참여수 public Runner(String name, int count) { this.name = name; this.count = count; } public int getCount() { return count; } public void setCount(int count) { if (!(MIN_CHECK <= count && count <= MAX_CHECK)) return; this.count = count; } @Override public String toString() { return "{name: '" + name + "', count: " + count + '}'; } }package sectionThree.quickSort.specialMission; import java.util.Arrays; public class QuickSortMission { public static void quickSortM(Runner[] runners, int left, int right) { if (left <= right) { int pivot = divideM(runners, left, right); quickSortM(runners, left, pivot - 1); quickSortM(runners, pivot + 1, right); } // 정렬이 다 끝난 후 - 가장 첫 번째 데이터인 '나'의 출석수를 변경 if (right == runners.length - 1) { runners[0].setCount(5); } } public static int divideM(Runner[] runners, int left, int right) { int pivot = runners[left].getCount(); int leftStartIndex = left + 1; int rightStartIndex = right; while (leftStartIndex <= rightStartIndex) { while (leftStartIndex <= right && pivot >= runners[leftStartIndex].getCount()) { leftStartIndex++; } while (rightStartIndex >= (left + 1) && pivot <= runners[rightStartIndex].getCount()) { rightStartIndex--; } if (leftStartIndex <= rightStartIndex) { swapM(runners, leftStartIndex, rightStartIndex); } } swapM(runners, left, rightStartIndex); return rightStartIndex; } public static void swapM(Runner[] runners, int index1, int index2) { Runner temp = runners[index1]; runners[index1] = runners[index2]; runners[index2] = temp; } public static void main(String[] args) { Runner user1 = new Runner("홍길동", 5); Runner user2 = new Runner("임꺽정", 4); Runner user3 = new Runner("이순신", 3); Runner user4 = new Runner("나", 1); Runner user5 = new Runner("짱구", 5); Runner[] runners = {user1, user2, user3, user4, user5}; System.out.println("===== 정렬 전 ====="); System.out.println(Arrays.toString(runners)); quickSortM(runners, 0, runners.length - 1); System.out.println("===== 정렬 후 ====="); System.out.println(Arrays.toString(runners)); } } 결과===== 정렬 전 ===== [{name: '홍길동', count: 5}, {name: '임꺽정', count: 4}, {name: '이순신', count: 3}, {name: '나', count: 1}, {name: '짱구', count: 5}] ===== 정렬 후 ===== [{name: '나', count: 5}, {name: '이순신', count: 3}, {name: '임꺽정', count: 4}, {name: '홍길동', count: 5}, {name: '짱구', count: 5}] 두 번째 해결 이어서 - 가장 첫 번째 데이터인 여러분의 출석수를 변경현재 코드결과는 예상 결과처럼 잘 나온다.그런데 피벗을 기준으로 정렬할 때마다 Runner[] 첫 번째 배열의 count가 5로 변경된다.정렬 도중에 여러 번 첫 번째 요소의 값을 변경할 가능성이 있어서 의도한 대로 동작하지 않을 수 있다.내가 원했던 로직모든 정렬이 끝난 후 Runner[] 첫 번째 배열의 count가 5로 변경된다.해결 방법quickSortM() 함수 종료 후 main() 함수에서 runners[0].setCount(5); 진행 - 강사님 픽quickSortM() 에 depth 변수를 추가해서 정렬 후 0 depth인 경우에 runners[0].setCount(5); 를 진행해결 방법 2번으로 진행해보자package sectionThree.quickSort.specialMission; import java.util.Arrays; public class QuickSortMission { public static void quickSortM(Runner[] runners, int left, int right, int depth) { if (left <= right) { int pivot = divideM(runners, left, right); quickSortM(runners, left, pivot - 1, depth + 1); quickSortM(runners, pivot + 1, right, depth + 1); } if (depth == 0) { // 정렬이 다 끝난 후 - 가장 첫 번째 데이터인 '나'의 출석수를 변경 runners[0].setCount(5); } } public static int divideM(Runner[] runners, int left, int right) { int pivot = runners[left].getCount(); int leftStartIndex = left + 1; int rightStartIndex = right; while (leftStartIndex <= rightStartIndex) { while (leftStartIndex <= right && pivot >= runners[leftStartIndex].getCount()) { leftStartIndex++; } while (rightStartIndex >= (left + 1) && pivot <= runners[rightStartIndex].getCount()) { rightStartIndex--; } if (leftStartIndex <= rightStartIndex) { swapM(runners, leftStartIndex, rightStartIndex); } } swapM(runners, left, rightStartIndex); return rightStartIndex; } public static void swapM(Runner[] runners, int index1, int index2) { Runner temp = runners[index1]; runners[index1] = runners[index2]; runners[index2] = temp; } public static void main(String[] args) { Runner user1 = new Runner("홍길동", 5); Runner user2 = new Runner("임꺽정", 4); Runner user3 = new Runner("이순신", 3); Runner user4 = new Runner("나", 1); Runner user5 = new Runner("짱구", 5); Runner[] runners = {user1, user2, user3, user4, user5}; System.out.println("===== 정렬 전 ====="); System.out.println(Arrays.toString(runners)); quickSortM(runners, 0, runners.length - 1, 0); System.out.println("===== 정렬 후 ====="); System.out.println(Arrays.toString(runners)); } } 결과===== 정렬 전 ===== [{name: '홍길동', count: 5}, {name: '임꺽정', count: 4}, {name: '이순신', count: 3}, {name: '나', count: 1}, {name: '짱구', count: 5}] ===== 정렬 후 ===== [{name: '나', count: 5}, {name: '이순신', count: 3}, {name: '임꺽정', count: 4}, {name: '홍길동', count: 5}, {name: '짱구', count: 5}]  

클라우드 기반 스마트팩토리 - 이슈들

이 글은 제가 NIA [한국지능정보사회진흥원]의 < 디지털서비스 이슈리포트 > 2025년 3월호에 기고한 글입니다. 원본 글 '2025년 AI 현황 보고서 리뷰'를 이곳 브런치에서도 공유합니다.들어가며지난 두 편의 클라우드 기반 스마트팩토리에 대한 소개에 이어 이번 회에서는 현장에서 실제로 부딪히는 문제들을 정보통신의 관점에서 몇몇 사례를 들어 이야기 해 보겠다. 아래 내용들은 필자가 함께 하는 인이지를 비롯한 여러 회사들이 제조 공정 관련한 과제들을 수행하면서 만난 문제들과 이들을 해결하려는 방법들에 대한 내용들이다. 제조 산업에서도 스마트팩토리라는 키워드를 중심으로 여러 혁신의 노력들이 모이고 있고, 클라우드를 이용한 기술은 효율성과 유연성을 극대화하는 데 중요한 역할을 하며, 정보통신 업계에서의 노하우들을 다양하게 적용하면서 그 영향력을 넓혀 가고 있다. 특히 클라우드를 이용한 방법을 통해서는 다음과 같은 이득을 기대할 수 있다.  데이터 기반 의사 결정  유연성과 확장성  비용 효율성  디지털 전환(DX)의 가속화  이는 일반적으로 쓰이는 의미의 IT 시스템 도입과 이전으로 인한 이득과 같은 맥락이지만, 스마트팩토리는 현장의 물리적인 변화와 공정의 특성들을 고려할 때 단순한 IT 시스템 이전과는 다른 추가적인 복잡한 문제들이 발생한다.   물리적 한계와 엔트로피 문제  실시간 운영과 신뢰성 문제  데이터 관리의 어려움  디지털 전환과 문제 해결 가능성   이번 회에서는 이 추가적인 내용들에 대해 조금 더 구체적으로 살펴보겠다. 물리적 한계와 엔트로피 문제기존의 데이터센터 중심의 IT 환경과는 달리, 스마트팩토리는 물리적 환경과 밀접하게 연결되어 있으며, 다양한 기계적 요소와 데이터가 실시간으로 상호작용하는데, 이는 예상치 못한 물리적 한계와 엔트로피 문제를 야기할 수 있다.스마트팩토리는 제조 현장의 특성 상 생산 라인의 변화, 기계 설비의 교체, 센서 추가 등 물리적 변화가 빈번하게 발생한다. 이러한 변화는 데이터 흐름과 시스템 아키텍처에 직접적인 영향을 미치며, 클라우드 환경과의 통합을 복잡하게 만든다. 예를 들어, 새로운 센서를 추가할 때마다 데이터 수집 및 처리 시스템을 재설정해야 하고, 생산 라인 변경 시 데이터 분석 모델을 수정해야 하는 등. 이러한 물리적 변화에 유연하게 대응하지 못하면 시스템의 효율성이 저하되고 운영 비용이 증가할 수 있다.또한 스마트팩토리는 다양한 기계와 센서, IT 시스템이 복잡하게 연결된 시스템이다. 전선으로 연결되어 빛의 속도로 연결되는 환경이 아니라 어느 곳에서 어떤 연료를 투입하면, 몇미터 떨어진 곳의 온도가 몇 분 후에 어떻게 변하는 등의 주변의 상황들이 통제되지 않는 상황이 생기고, 대기의 온도, 습도 등에 따라 예기치 않은 변화들이 생기기도 한다. 시간이 지남에 따라 장치가 마모되거나 유지보수의 부담이 늘어나는 등 열역학 제2법칙인 엔트로피 증가의 법칙과 유사한 현상이 일어나게 되는데, 이로 인해 시스템의 안정성이 저하되고 오류 발생 가능성이 높아지며, 이는 생산 효율성 저하와 직결된다.실제로 디지털 전환을 도입하려는 많은 제조 현장의 경우 온도와 습도 등의 환경이 많은 영향을 미치기 때문에, 온프레미스 시스템을 구성하기 위해서도 오차 없이 사용 가능한 장비들의 사용이 필요하고, 여기에 특히 클라우드 환경을 도입할 경우, 온프레미스 시스템과의 연동, 데이터 동기화, 네트워크 안정성 등 추가적인 유지보수 요소가 발생하는데, 클라우드 도입의 이점을 제대로 누리기 위해 이들로 인한 복잡성을 효과적으로 관리해야 한다. 이후 데이터가 모여서 분석을 하는 경우, 도면이나 공정 같은 현장의 지식이 없이 센서 데이터만으로는 분석의 한계가 생긴다. 시멘트나 철강 등의 공정을 생각한다면 물리적으로 수십 미터 떨어져 있는 데이터들이며, 센서들 사이에 어떤 간섭이 있는지, 바람은 잘 통하는지, 하루에 몇 번씩 청소를 하는지 등의 내용들이 고려되어야 현장의 문제에 접근할 수 있게 된다. 마찬가지로 용해로 시계열 온도 예측 같은 경우, 900도 온도를 맞추기 위해서 850도인 현재 상황에서 용해물질에 어떤 재료를 얼마만큼 넣으면 몇 분 후에 온도가 올라가는지 등 데이터로 모아 놓기에 어려움이 많고, 산업공학, 기계공학, 화학공학 등의 정보들이 도메인 지식들을 익힌 후에 더 나은 분석을 할 수 있는 경우가 많다.  실시간 운영과 신뢰성 문제스마트팩토리는 고도의 자동화와 데이터 기반 운영을 통해 생산성을 극대화하는 것을 목표로 하는데, 이러한 목표를 달성하기 위해서는 실시간 운영과 시스템의 높은 신뢰성이 필수적이다. 하지만 스마트팩토리 환경은 다양한 변수와 복잡성으로 인해 실시간 운영과 신뢰성을 확보하는 데 어려움을 겪을 수 있다.스마트팩토리는 24시간, 365일 가동되는 경우가 많은데, 이는 생산 효율성을 극대화하고 시장 수요에 신속하게 대응하기 위한 필수적인 조건이다. 따라서 예기치 않은 시스템 다운타임은 생산 차질, 납기 지연, 고객 신뢰도 하락 등 심각한 문제를 야기할 수 있다. 특히, 실시간 데이터 처리와 제어가 중요한 생산 라인에서는 단 몇 분의 다운타임도 재가동하는 비용을 포함한 큰 손실로 이어질 수 있다. 따라서 스마트팩토리는 시스템의 안정성을 최우선으로 고려해야 하며, 다운타임 발생 시 신속하게 복구할 수 있는 체계를 구축해야 한다. 예를 들면 용해로 재시작 재가동 등의 일들은 일반 컴퓨터 재부팅보다 훨씬 더 준비해야 할 게 많으므로 이런 점이 고려가 되어야 한다. 또한 시스템의 신뢰성을 높이기 위해서는 이중화 시스템 구축이 필수적이고, 환경에 따라서 데이터 백업, 네트워크 이중화, 서버 이중화 등 다양한 방법을 통해 시스템 장애에 대비해야 한다. 자원들이 유기적으로 연결되어 있는 클라우드 환경에서는 상대적으로 이중화 혹은 다중화 지원이 용이하지만, 온프레미스 환경에서는 이를 지원하기 위해 네트워크 장비나 실제 서버들의 추가적인 설치와 운영이 필요하다. 온프레미스에 저장되어 있는 데이터를 클라우드에 저장하고 운영하는 것은 추가적인 네트워크 연결을 도입하는 것이기에 이로 인한 위험도 있게 되므로, 실시간 데이터 처리 및 중요 데이터는 온프레미스에 저장하고, 분석 및 장기 데이터는 클라우드에 저장하는 하이브리드 모델을 많이 고려한다.  이러한 하이브리드 모델은 데이터 처리 속도와 안정성을 동시에 확보할 수 있으며, 엣지 컴퓨팅을 활용하여 실시간성이 요구되는 데이터의 경우 추가적인 데이터 이동을 절약함으로써 현장에서 필요한 데이터 처리 속도를 향상시킬 수 있다. 그림 1. 고가용 시스템 네트워크 구조 예제스마트팩토리는 수많은 스마트 센서를 통해 데이터를 수집하고 분석하는데, 클라우드 기반 스마트 센서는 네트워크 연결이 필수적이므로, 안정적인 네트워크 환경을 구축해야 한다. 주변의 환경에 영향을 받기에 유무선 네트워크 장애는 센서 데이터 수집 및 전송에 문제를 일으킬 수 있고, 스마트 센서는 민감한 생산 데이터를 수집하므로, 데이터 보안을 강화해야 한다. 각각의 센서 혹은 시스템이 노출되는 형태이므로 데이터 암호화, 접근 제어, 보안 프로토콜 적용 등을 통해 데이터 유출 및 해킹을 방지해야 하고, 마지막으로 수많은 스마트 센서를 효율적으로 관리하고 유지보수해야 하는데, 원격 관리, 자동 업데이트, 센서 상태 모니터링 시스템 등을 통해 센서 관리 효율성을 높여야 한다. 데이터 관리의 어려움: 저장과 조회의 균형스마트팩토리는 생산 과정에서 발생하는 방대한 양의 데이터를 효율적으로 관리하는 것이 핵심이다. 현장에서 일어나는 모든 데이터가 관리 대상이기에 데이터의 양이 기하급수적으로 증가함에 따라 저장과 조회에 대한 어려움이 발생하고, 효율적인 데이터 관리 전략이 필수적이다.스마트팩토리에서 생성되는 데이터는 실시간 데이터와 장기 분석용 데이터로 나눌 수 있다. 실시간 데이터는 생산 라인 제어, 품질 검사 등 즉각적인 응답이 필요한 데이터이며, 온프레미스에 저장하는 것이 유리한 반면, 장기 분석용 데이터는 생산 공정 최적화, 설비 예지 보전 등에 활용되며, 클라우드 스토리지를 활용하는 것이 좋다. 클라우드는 이 확장성에 대해 확실한 강점이 있어, 이후 효율적으로 저장하고 처리할 수 있다. 전사적 자원관리(ERP: Enterprise Resource Planning)의 내용과 같이 현장 바깥의 정보들과 같이 사용하는 경우 훨씬 유용하게 쓰일 수 있다.스마트팩토리에서 생성되는 데이터는 많게는 초당 수백, 수천 건의 고화질 대용량 데이터들이 이용되기도 하는데, 이를 다루기 위해서 데이터 처리 성능이 중요하다. 데이터를 모으는 시스템과 읽는 시스템이 자원을 공유하기에 그 사이에서 오는 문제가 생기기도 한다. 실제 조회가 필요할 경우 제대로 운영하기 위해 알맞은 데이터베이스를 선택하고, 인덱스 설정, 쿼리 최적화 등을 통해 대응해야 하는데, 데이터를 이동시키는 데 드는 자원이 원래 시스템을 운영하는 데 방해가 되지 않아야 하고, 본래 시스템이 주어진 역할에 지장이 없도록 운영해야 한다.데이터 양이 증가함에 따라 저장 비용이 기하급수적으로 증가할 수 있다. 예를 들어 이미지를 통한 불량 탐지의 경우 불량률이 적어질 수록 중복된 정상 이미지들이 불필요하게 쌓이는 상황이 생기기도 하고, 모든 것들을 저장해야 한다고 하면 데이터 백업 등에도 추가적인 노력과 비용을 들여야 한다. 대용량 데이터를 실시간으로 처리하는 것은 클라우드 환경에서도 기술적으로 어려운데, 온프레미스에서도 분산 처리 시스템, 인메모리 데이터베이스, 엣지 컴퓨팅 등 다양한 방법들을 도입해야 한다.이처럼 스마트팩토리의 데이터 관리는 저장 위치 결정, 읽기/쓰기 성능 최적화, 저장 비용 및 처리 성능 한계 극복 등 다양한 어려움을 내포하는데, 이러한 문제를 해결하기 위해 먼저 현장을 이해한 후에 데이터의 특성과 사용 목적에 맞는 데이터 관리 전략을 수립하고, 최신 기술을 적극적으로 활용해야 한다. 디지털 전환과 문제 해결 가능성디지털 전환은 스마트팩토리의 생산성과 효율성을 향상시키기 위한 핵심 전략으로, 클라우드는 디지털 전환의 중요한 요소로 작용하며, 데이터를 중앙에서 실시간으로 분석할 수 있도록 지원한다. 이를 통해 공정 최적화와 이상 탐지가 가능하며, 공장 운영의 자동화를 가속화할 수 있다. 다양한 장점에도 불구하고, 현장의 문제를 풀어 낼 수 있는가 라는 문제에 많은 고민들이 있다.온도를 재는 아날로그 센서의 경우 고온고압의 환경을 센서가 버티지 못하는 경우도 있고, 최근의 화두인 탄소 수치의 경우 가상의 새로운 장치들이 필요하다. 제품의 완성도는 화면으로 100% 잡히지 않는 경우도 많고, 농도는 샘플링에 의존할 수밖에 없고, 물성은 완제품으로부터만 얻을 수 있는 경우가 대부분이다. 각각의 사례들이 데이터로 디지털화 되었다고 해도 세상의 물리와 화학은 이진 수학으로 떨어지지 않는 부분도 많고, 통계와 예측은 신뢰구간과의 끊임없는 싸움이다.앞의 여러 이슈들을 겪은 후 데이터가 모인 후에는, 실제 문제를 정의하고 풀어 나가는 마지막 단계에 오게 되고, 이 경우 인력 문제로 귀결이 된다. 이미 제조 현장은 소수의 인원이 오랜 세월의 노하우로 운영을 하고 있고, 그 문제를 데이터로 풀려 하는 인력들과 거리가 있어 많은 현장에서 디지털 전환을 했음에도 실질적인 이득을 얻기 힘들다는 현실과 닿아 있다. 이 거리가 좁혀진 후에는 시계열 예측 혹은 설명 가능 인공지능 등이 추가적인 가치를 창출해 낼 수 있겠으며, 필자가 속한 인이지를 비롯해 많은 인공지능 관련 업체들이 기존의 제조산업 업체들과 문제를 정의하고 풀어 나가고 있다.그림 2. 인이지의 산업용 공정 효율 최적화 솔루션 예제 맺으며클라우드 기반 스마트팩토리는 분명 제조업의 혁신을 가속화하고 생산성을 극대화할 수 있는 강력한 도구이지만, 앞서 살펴본 바와 같이, 물리적 한계, 실시간 운영의 신뢰성, 데이터 관리의 어려움 등 해결해야 할 과제들이 산적해 있다. 이러한 문제들을 극복하기 위해서는 단순히 기술을 도입하는 것을 넘어, 현장의 특성을 깊이 이해하고, 데이터 기반의 의사 결정을 통해 지속적인 개선을 추구하는 노력이 필요하다.또한, 스마트팩토리를 포함한 디지털 전환은 기술적인 변화뿐만 아니라, 조직 문화와 인력의 변화를 수반한다. 현장의 경험과 지식을 데이터 분석 및 활용 능력과 결합하여 시너지를 창출하는 것이 중요하겠으며, 클라우드 기반 스마트팩토리가 진정한 가치를 발휘하기 위해서는 기술, 사람, 그리고 조직의 조화로운 발전이 필수적이라 하겠고, 이 요소들이 다 고려되었을 때 비로소 디지털 전환이 되었다 할 수 있겠다.

대학 교육 기타

rjf1138

[인프런 워밍업 클럽 3기] BE 클린코드&테스트 - 4주차 발자국

💡 강의 핵심 내용 정리💻Practical Testing🔹 1. 왜 Mocking이 필요한가?외부 시스템(메일 전송, 결제, 알림 등)을 호출하는 코드는 실제 실행하면 부작용이 발생할 수 있음.테스트 시 외부 의존성을 제거하고 예측 가능한 결과를 주기 위해 Mock 객체를 사용함.이 과정을 Stubbing이라 하며, 원하는 동작을 가짜 객체에게 명시함.Mockito.when(mailSendClient.sendEmail(...)).thenReturn(true); 🔹 2. Mail 전송은 @Transactional을 붙이면 안 되는 이유메일 전송은 외부 네트워크 요청이며, 트랜잭션 범위 안에서 실행되면 DB Connection을 오래 점유하게 됨.이런 긴 작업은 트랜잭션 바깥에서 실행되어야 함.🔹 3. Test Double의 종류유형 설명 Dummy 사용되지 않는, 껍데기 객체 Fake 간단한 구현을 가진 실제 객체 (Map 기반 Repository 등) Stub 미리 정의된 응답을 제공하는 객체 (상태 검증용) Spy 일부는 실제처럼, 일부는 Stub. 호출 기록 추적 가능 Mock 행위 기반 검증용 객체 (몇 번 호출되었는지 등 검증)💡 Stub은 상태 검증, Mock은 행위 검증에 사용됨.🔹 4. 순수 Mockito 사용법@Mock, @InjectMocks, @ExtendWith(MockitoExtension.class) 조합으로 Spring Context 없이도 테스트 가능@Spy: 실제 객체 기반으로 필요한 부분만 StubbingdoReturn(true).when(mailSendClient).sendEmail(...); 🔹 5. BDDMockitowhen(...).thenReturn(...) 대신 Given-When-Then 스타일을 지향BDDMockito.given(mailSendClient.sendEmail(...)).willReturn(true); 테스트의 목적과 구조가 명확하고 선언적으로 표현됨.🔹 6. Classicist vs Mockist구분 Classicist Mockist 테스트 단위 실제 객체로 통합 테스트 협력 객체는 모두 Mock 강조점 시스템 동작 검증 객체 간 상호작용 검증 사용 시점 DB, HTTP 연동 등 진짜 동작 필요할 때 외부 의존이 많거나, 로직 복잡도 높은 객체✅ 일반적으로는 Classicist 접근을 사용하고, 외부 시스템 등 불가피한 경우에만 Mocking을 하자!🔹 7. 테스트 코드 개선 전략 요약📌 한 문단에는 한 주제테스트는 명확하게 하나의 동작만 검증해야 함.if, for 등의 분기/반복문이 테스트 코드에 들어가면 테스트 목적이 흐려짐.📌 제어 가능한 값만 사용하라현재 시간, UUID, 랜덤, 환경 변수 등은 직접 주입하거나 인터페이스로 추출하여 테스트 가능하게 설계.📌 테스트 간 독립성 보장static 공유 객체 금지 (@BeforeAll/@BeforeEach도 주의)fixture는 되도록 각 테스트 내에서 명시적으로 구성📌 Test Fixture는 생성자 기반 / Builder 활용팩토리 메서드 대신 Builder 패턴 선호data.sql로 데이터 주입 지양 → 테스트 목적 파악 어려움📌@ParameterizedTest, @DynamicTest 적극 활용경계값, 다양한 조건 검증이 필요한 경우 유용@DynamicTest는 상태 변화 시나리오 테스트에 특히 적합📌 공통 테스트 환경 통합@SpringBootTest를 반복 호출하지 않도록 상위 추상 클래스로 환경을 공통화@SpringBootTest @ActiveProfiles("test") public abstract class IntegrationTestSupport {} 📌 private 메서드는 테스트하지 말자테스트가 필요할 정도로 복잡해졌다면, 새로운 객체로 추출해서 테스트 가능하도록 리팩토링.

Yang HyeonBin

[인프런 워밍업 클럽 3기] 풀스택 과정 4주차 발자국 👣

이번 주차에는 SupabaseAuth, Realtime, RLS를 사용해봤고, Next.js 팀에서 만든 배포 플랫폼인 Versel을 이용해 지금까지의 작업물을 배포해보았다!  먼저 Auth를 살펴보자.1. 회원가입무료 Supabase 프로젝트는 이메일 인증 기본 제공량이 1시간에 3통⇒ SMTP 서버 직접 세팅하면 무제한 전송 가능1. Confirmation URL 방식 - 메일의 인증 링크 클릭, 리다이렉트supabase.auth의 signUp, exchangeCodeForSession, getSession을 이용 signUp 함수 options에 emailRedirectTo 값으로 exchangeCodeForSession을 실행할 페이지의 경로를 넘겨줌 경로 속 route.tsx 파일에서 exchangeCodeForSession 실행, redirect url의 code search param 값 이용해 세션을 얻어 로그인 처리 후, NextReseponse.redirect 이용해 기본 주소로 리다이렉트 처리 기본 주소에서 getSession 이용해 로그인 여부 확인, 상태에 맞는 화면(기본 화면 / 로그인(회원가입) 화면 중 하나) 보여주게 처리메일 인증 링크 형식https://{뭔가고유한문자열}.supabase.co/auth/v1/verify?token={tokenHash}&type=signup&redirect_to={리다이렉트url}절차메일의 ‘인증’ 버튼 클릭 시 위 링크로 이동하는데, 인증을 마친 뒤 {리다이렉트url}로 이동, code라는 searchParam과 함께 리다이렉트함Web client에서 code searchParam 값을 이용해 로그인 세션 획득, supabase.auth.exchangeCodeForSession 함수 이용해 로그인 처리supabase.auth.signUp 함수에 options 값으로 넘긴 redirectUrl 경로에서 열릴 파일을 정의 options: { emailRedirectTo: "<http://localhost:3000/signup/confirm>", // 인증 완료 후 리다이렉트 url }, 위 경로의 경우, app/signup/confirm/route.tsx에 정의하면 됨url과 일치하도록 폴더와 route.tsx 파일을 생성Next.js 14 이상부터 사용되는 개념API 정의 및 GET, POST, UPDATE 요청 등을 관장해당 url에 접속했을 때, 이 파일에 작성된 작업을 수행ex. GET을 정의 → GET 오퍼레이션을 날림// app/signup/confirm/route.tsx import { NextResponse } from "next/server"; import { createServerSupabaseClient } from "utils/supabase/server"; export async function GET(request: Request) { const requestUrl = new URL(request.url); // 이메일 인증 완료 후 redirect url에 'code' search param이 포함되어 돌아오게 됨 const code = requestUrl.searchParams.get("code"); if (code) { const supabase = await createServerSupabaseClient(); // 'code' search param 값을 이용, // supabase auth에서 제공하는 exchangeCodeForSession 메서드를 사용하여 // code를 이용해 로그인 세션 획득, 로그인 처리가 됨 await supabase.auth.exchangeCodeForSession(code); } // redirect url로 이동 (localhost:3000/signup/confirm/?code=... -> localhost:3000/) return NextResponse.redirect(requestUrl.origin); } 코드 → 세션 교환해 로그인 처리 후 NextResponse.redirect를 호출해 리다이렉트 (기본 url로 가게 설계)기본 url에서는 유저 로그인 상태를 체크해 상태에 맞는 페이지를 보여주도록 함// app/layout.tsx // async function RootLayout 안에서 (서버 컴포넌트여서 async function으로 정의 가능) const supabase = await createServerSupabaseClient(); const { data: { session }, } = await supabase.auth.getSession(); const loggedIn = !!session?.user; // return문 안에서 {loggedIn ? <MainLayout>{children}</MainLayout> : <Auth />} layout.tsx:23 Server Using the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.2. 6자리 OTP 방식 - 메일의 6자리 OTP 토큰을 사용자가 복사해 인증 진행, OTP 입력 및 제출을 위한 인풋 UI 마련 필요1번에서 구현한 이메일, 패스워드로 signUp하는 것은 동일, 그 사이에 verifyOtp 를 사용해 인증을 한번 거치는 것mutationFn 작성email, password만 넘겨줬던 signUp과 달리, type, email, token을 넘겨줘야 함type은 “signup”mutationFn: async ({ email, otp }: { email: string; otp: string }) => { const { data, error } = await supabase.auth.verifyOtp({ type: "signup", // 왜 signup이지? email, token: otp, }); if (error) { handleError(error); } return data; }, 회원가입 form에서, confirmationRequired 상태일 때 보여줄 <input /> 추가(otp 입력 받을 창)otp용 useState, input 추가하고onSubmit과 button에 otp 관련 상태 처리 코드 추가3. 카카오로 로그인Kakao Developers에서 선행 작업 필요signInWithOAuth 함수 사용이 함수는, authentication에 없는 계정일 경우 새로 추가하고 아니면 다음 단계로 넘어가도록 설계가 되어 있나봄. 그래서 signin, signup에 별도로 구현할 필요 없이 이 함수 이용하는 동일한 하나의 로직을 이용하면 됨provider에 “kakao”, options에 redirectTo 전달authentication에는 Provider에 Kakao(1번 방법에선 Email)인 유저가 추가됨route 정의 필요1번 방법과 마찬가지로, redirect url로 설정했던 경로와 일치하는 route.ts 파일을 추가ex. app/auth/callback/route.ts마찬가지로 searchParams로 받은 code 값을 이용해 로그인 처리 가능 (exchangeCodeForSession)2. 로그인signInWithPassword로 간단히 구현 가능return useMutation({ mutationFn: async ({ email, password, }: { email: string; password: string; }) => { const { data, error } = await supabase.auth.signInWithPassword({ email, password, }); if (error) { handleError(error); } return data; }, }); 다음으로 Realtime도 살펴보자.Broadcast - 직접 방출과 리슨을 구현하는 듯전체 공지를 보낼 때 등에 유용강력하지만 구현 복잡성이 높음Presence - 현재 연결된 사용자를 실시간으로 추적. 유저가 들어왔다/나갔다를 파악 가능 (sync, join, leave)현재 온라인인 유저들 파악에 사용이 채널에 특정 유저가 있나 없나 확인 가능유저가 subscribe()하면 join으로, unsubscribe하면 leave로 인식됨subscribe할 때 user의 onlineAt 같은 컬럼 값을 업데이트해줌으로써 유저가 언제 온라인이었는지 파악 가능하다?sync는 join, leave 모두 감지 가능한, 말그대로 싱크가 맞춰져 있게 되는 것주로 sync를 사용하고, join, leave 발생 시 따로 노티를 띄워준다거나 하는 별도의 처리를 하고 싶을 때 join, leave를 사용하자Postgres Changes - DB에 INSERT, UPDATE, DELETE 이벤트가 발생하는 것을 스트림channel명을 정하고 subscribe하면 그 이후부터 db에 발생하는 이벤트를 리슨 가능유용한 패키지Javascript time agonpm install --save javascript-time-ago몇분전, 과 같은 문자열 표시를 돕는 패키지다. 로케일도 지원해 다국어를 지원해줘야 할 때 더 빛을 발한다. 그리고 RLS도 다뤄보았다.RLS = Row Level SecurityLow가 아니라 Row테이블의 Row 단위로 보안 규칙/제약을 설정할 수 있다는 의미테이블 컬럼 수정default value에는 함수 호출도 가능, auth.uid()는 로그인된 유저의 uid가 디폴트로 들어감!Can either be a literal or an expression. When using an expression wrap your expression in brackets, e.g. (gen_random_uuid()) - 함수 사용 시 ()까지 붙여 호출해주기 마지막으로 배포를 해보았는데, 깃허브 연동도 깔끔하게 되고 정말 매끄러운 배포 경험을 할 수 있었다.Vercel이란Next.js 만든 팀이 쉽게 배포할 수 있게 만든 플랫폼회원가입하고 github 연동해서 손쉽게 배포 가능배포하기배포 전 ide에서 npm run build 실행해 빌드 에러 없는지 먼저 체크하고,거의 다 디폴트 값으로 따르면 되는데,환경변수 값 넣어주기 - Environment Variables.env 파일을 복사해서 붙여넣어주면 알아서 들어감배포 전 실제 환경변수 값과 일치하는지 한번 더 체크하기!환경변수 값이 잘못 들어가 500 에러가 발생했지만, 수정과 재배포가 간단해서 금방 수정할 수 있었다. 전역 상태관리 Jotai 사용지난 주차에 전역 상태관리 툴 Recoil 버전 오류로 context api를 써서 갈음했었는데,이번 주차 내용은 좀더 많은 전역 상태 관리가 필요해, 가벼운 라이브러리라는 Jotai를 사용해보았다.정확히 어떤 원리로 동작하는지는 아직 뜯어보지 못했는데, 정말 간단하게 Provider를 만들어서 사용할 수 있었다.타입도 명시하여 정의하는 방식이라 명확한 부분이 마음에 들었다.사용법이 리코일보다도 간단해서 어떤 식으로 동작이 되는건지 살펴보고 싶다!

인프런 워밍업 스터디 클럽 3기 4주 차 발자국

섹션 7: Mockito로 Stubbing하기Test Double 종류Dummy: 아무 기능도 없는 객체.Fake: 간단한 형태로 실제 기능 수행 가능하지만, 프로덕션에 적합하지 않은 객체.Stub: 미리 준비된 결과를 반환하는 객체로, 상태 검증에 사용.Spy: Stub이면서 호출 기록을 제공하며 일부는 실제 객체처럼 동작 가능.Mock: 특정 행위를 명세하고 동작하도록 설계된 객체로, 행위 검증에 사용.Mockito 주요 어노테이션@Mock: Mock 객체 생성.@Spy: 실제 객체의 메서드를 수행하되 특정 메서드만 Mocking.@InjectMocks: Mock과 Spy 객체를 자동 주입.BDDMockito행동 주도 개발 방식으로 given().willReturn()을 사용하여 테스트 작성.Classicist vs MockistClassicist: 실제 객체를 사용해 자연스럽고 직관적인 테스트. 리팩토링에 유리하지만 상태 검증만으로는 동작 오류를 놓칠 수 있음.Mockist: 모든 협력자를 Mock으로 대체하여 동작을 명확히 검증. 세부 구현에 의존적이라 리팩토링 시 테스트가 깨질 가능성이 있음.섹션 8: 더 나은 테스트 작성법테스트 작성 원칙한 테스트는 한 목적만 가져야 하며, if와 for 사용을 지양.LocalDateTime.now() 같은 제어 불가능한 값은 파라미터로 넘겨 제어.테스트 환경 독립성공유 자원을 사용하지 말고, 테스트 간 독립성을 보장.픽스처(Test Fixture)는 각 테스트가 내부 구현을 몰라도 이해 가능해야 함.Test Fixture 클렌징deleteAll(): 순차적으로 삭제, 성능 저하 우려.deleteAllInBatch(): 단일 SQL DELETE 쿼리로 대량 데이터 삭제 가능.반복 및 동적 테스트@ParameterizedTest: 여러 파라미터 값으로 반복 테스트.예: CsvSource, MethodSource 활용.@DynamicTest: 런타임에 동적으로 테스트 케이스 생성 및 실행.통합 환경 최적화서버 부팅 횟수를 줄이기 위해 통합 테스트 클래스를 상속 구조로 설계.예: @SpringBootTest, @ActiveProfiles("test").Private 메서드와 테스트 전용 코드Private 메서드는 직접 테스트하지 말고, 객체 분리를 통해 public 메서드로 검증.테스트 전용 메서드는 필요하면 작성 가능하나 신중히 접근.추가 내용Spring REST Docs vs SwaggerREST Docs: 신뢰도 높고 프로덕션 비침투적이지만 설정이 복잡함.Swagger: 적용이 쉽고 API 호출 가능하지만 프로덕션 코드에 침투적이고 신뢰도가 낮음.    후기  생각보다 봐야할 강의 양도 많고 이해하기가 어려워서 한 강의당 3번씩 돌려본것 같다. 특히나 하다가 테스트를 시도했는데. 아니 강의에서는 통과하는데 나는 실패했을때 실패지점 찾는데 진짜 애먹었다. 정신나갈뻔한 상황도 많았지만 많이 배울 수 있었다. 그럼에도 아직 어려운 부분이 많았다.   강의 : https://www.inflearn.com/course/readable-code-%EC%9D%BD%EA%B8%B0%EC%A2%8B%EC%9D%80%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%EC%82%AC%EA%B3%A0%EB%B2%95/dashboard

채널톡 아이콘