ETF를 처음 사는 사람의 머릿속엔 대개 비슷한 그림이 있다. 주식 하나를 고르는 건 부담스럽다. 기업 하나에 내 돈을 건다는 감각이 너무 직접적이기 때문이다. 실적이 나쁘면 바로 내 판단이 틀렸다는 증거가 되고, 주가가 빠지면 그 책임도 고스란히 돌아온다. 반면 ETF는 다르다. 버튼 한 번으로 수백 개 기업을 산다. 이게 바로 분산이라고 배웠고, 그래서 마음이 편해진다. 내 선택이 조금 더 성숙해진 것 같고, 적어도 ‘한 방에 망하는 짓’은 피한 것처럼 느껴진다. 이 느낌은 거짓이 아니다. ETF는 실제로 어떤 종류의 위험을 분산한다. 딱 하나의 회사가 터져서 계좌가 같이 터지는 위험, 내가 고른 종목이 망해서 내 판단이 즉각적으로 처벌받는 위험, 이런 것들은 확실히 줄어든다. 그래서 ETF는 “생각을 덜 해도 되는 안전장치”처럼 받아들여진다. 선택이 무책임해 보이지 않는 이유도 여기에 있다. 하지만 ETF가 만들어주는 이 편안함에는 한 가지 착시가 섞여 있다. 사람들이 ‘분산’이라고 부르는 감각 안에는, 서로 다른 성질의 것들이 한꺼번에 묶여 있기 때문이다. 많은 사람이 ETF를 사고 나서 편해지는 이유는 기업이 많아졌기 때문만은 아니다. 기업 이름이 많아진 건 눈에 보이니까 가장 먼저 떠오를 뿐이고, 실제로 마음이 놓이는 지점은 훨씬 심리적인 곳에 있다. "내가 고르지 않아도 된다"는 느낌. 내 판단이 틀렸을 수 있다는 부담, 내가 책임져야 한다는 압박이 흐릿해진다. ETF는 돈을 나눠 담는 동시에, 책임도 같이 나눠 담는 것처럼 느끼게 만든다. 여기까지가 사람들이 ‘분산되었다’고 느끼는 지점이다.기업 이름이 많아졌고, 한 종목의 폭탄을 피했고, 무엇보다 내가 판단해야 한다는 부담이 줄었다. 이 세 가지가 하나로 묶여 ‘분산’이라는 단어로 정리된다. 문제는 여기서 시작된다.ETF가 분산해주는 건 주로 ‘개별 기업 사고’에 가까운 위험인데, 시장이 실제로 사람을 괴롭히는 위험은 그보다 더 큰 종류인 경우가 많다. 그리고 그 위험은 ETF가 분산하지 못한다. 그 위험은 아주 단순한 질문에서 드러난다. ETF 안에 기업이 수백 개나 있는데, 왜 시장은 늘 몇 개 기업의 뉴스에만 흔들리는 것처럼 보일까. 왜 다들 분산했다고 말하는데, 돈은 계속 비슷한 이름으로만 몰리는 걸까. 왜 ‘전체’를 샀다고 느끼는데, 성과는 늘 ‘몇 개’에 끌려가는 느낌이 들까. 이건 기분 탓이 아니라, ETF 구조가 실제로 그렇게 작동하기 때문이다. 대부분의 대표 지수 ETF는 시가총액 가중 방식으로 움직인다. 쉽게 말해, 기업을 공평하게 나눠 담지 않는다. 덩치가 큰 기업을 더 많이 담는다. ETF 하나로 500개 기업을 산다고 해도, 그 500개가 비슷한 무게로 담겨 있는 게 아니라, 덩치 순서대로 무게가 배분된 상태에 가깝다. 여기서 “덩치 순서대로 무게가 배분된다”는 말을 더 노골적으로 바꾸면 이런 뜻이다. ETF 속 기업이 500개든 1,000개든, 그 숫자는 ‘이름표’에 가깝다. 실제로 당신 돈이 눌리는 무게는 이름표 개수로 나뉘지 않는다. 돈은 “큰 곳”에 더 얹힌다. 그래서 ETF를 샀을 때 당신이 얻는 분산은 ‘이름의 분산’이고, ETF가 실제로 만들어내는 구조는 ‘무게의 집중’이다. 이 둘이 동시에 존재하기 때문에 혼란이 생긴다. 중요한 건 이게 단순히 ‘구성의 문제’로 끝나지 않는다는 점이다. 이 방식은 돈이 들어올수록 특정 기업을 더 유리하게 만드는 반복 구조를 만든다. 그리고 이 과정은 너무 자동적이고 조용해서, 대부분의 사람은 체감하지 못한 채 지나간다. 상황을 극도로 단순화해 보자. 당신이 어떤 지수 ETF를 산다. 당신은 기업을 고르지 않는다. 그냥 ETF를 산다. 그런데 ETF에 돈이 들어오면 운용사는 그 돈을 들고 규칙표대로 주식을 사야 한다. 이건 판단이 아니라 규칙이다. “이 기업이 좋아 보인다”가 아니라, “이 기업의 비중이 이만큼이니 이만큼 산다”는 식이다. 여기서부터 핵심이 드러난다. 비중이 큰 기업은 ETF로 들어오는 돈을 더 많이 받는다. ETF에 자금이 많이 들어오는 시기일수록, 비중이 큰 기업은 자동으로 더 큰 매수 압력을 받는다. 기업이 최근에 혁신을 했는지, 실적이 폭발했는지, 아니면 그냥 무난한 상태인지와는 상관없다. ‘비중이 크다’는 이유 하나만으로 더 많이 사준다. 이 장면을 더 구체적으로 그려보자. 당신이 ETF를 “한 주” 샀다고 느끼는 순간, 실제 시장에서는 훨씬 세분화된 거래가 일어난다. ETF는 단일 종목처럼 거래되지만, 그 안에서는 여러 종목이 동시에, 정해진 비율대로, 강제로 매수된다. 당신 입장에서는 “ETF 한 번 샀다”지만, 시장 입장에서는 “수십~수백 종목을 같은 순간에, 같은 규칙으로 샀다”가 된다. 그 규칙은 대개 이런 식이다. 지수에서 A기업 비중이 7%라면, 새로 들어온 돈의 7%가 A기업 매수로 간다. B기업 비중이 6%라면, 새로 들어온 돈의 6%가 B기업 매수로 간다. C기업 비중이 5%라면, 새로 들어온 돈의 5%가 C기업 매수로 간다. 그리고 이름조차 낯선 작은 기업들은 0.1%, 0.05% 같은 비중으로 아주 조금씩만 매수된다. 여기서 많은 사람이 무의식적으로 떠올리는 오해가 하나 있다. “그래도 어차피 500개를 사는데, 그럼 골고루 사는 거 아닌가?” 아니다. 골고루가 아니라 “비중대로”다. 비중대로라는 말은 결국 “크기대로”라는 뜻이고, 크기대로라는 말은 결국 “큰 기업에 더 많이”라는 뜻이다. 이 구조를 한 번 더 노골적으로 바꿔보자. ETF에 돈이 들어오면, 그 돈은 ‘신규 성장 기업을 찾아’ 배분되지 않는다. ETF에 돈이 들어오면, 그 돈은 ‘이미 큰 기업에 먼저’ 배분된다. 그래서 ETF 돈은 새 얼굴을 찾아다니지 않는다. 이미 커진 곳으로, 이미 익숙한 이름으로, 계속해서 돌아간다. 여기서 중요한 건, 이게 “가끔” 그러는 게 아니라 “매번” 그렇다는 점이다. ETF 자금은 한 번의 이벤트가 아니다. 개인이 한 번 사는 것도 있지만, 더 큰 흐름은 반복된다. 월급날마다 적립식으로 들어오는 돈, 퇴직연금에서 자동으로 들어오는 돈, 로보어드바이저가 규칙대로 집행하는 돈, 기관이 비용 절감 목적으로 인덱스를 쌓는 돈. 이 돈들은 매번 의견을 새로 묻지 않는다. 이미 정해진 규칙을 다시 실행할 뿐이다. 그래서 ETF의 매수는 “분석의 결과”가 아니라 “절차의 실행”에 가깝다. 개별주식은 누군가가 판단해서 사는 경우가 많다. 그 판단이 맞으면 수익, 틀리면 손실이다. 그 과정에는 이유가 있고 설명이 있다. 반면 ETF 매수는 이유를 묻지 않는다. “비중이 이만큼이니까”가 끝이다. 기업이 좋든 나쁘든, 비중이 크면 더 많이 산다. 기업이 뜨든 식든, 비중이 작으면 조금만 산다. 판단이 아니라 규칙이다. 주식시장에서 이런 꾸준한 매수는 가격에 영향을 준다. 지속적인 수요는 주가를 받치고, 주가가 잘 버티면 시가총액이 커진다. 시가총액이 커지면 지수에서 그 기업의 비중은 더 커진다. 비중이 더 커지면 다음 ETF 자금 유입 때는 또 더 많은 돈을 받는다. 이 과정을 ‘한 번’만 보면 티가 거의 안 난다. 100만 원이 들어왔을 때 A기업에 7만 원 배정되는 게 시장을 움직이는 것처럼 보이진 않는다. 하지만 ETF는 한 사람이 한 번 하는 행동이 아니라, 수많은 사람이 반복하는 습관이 된다. 리고 그 습관의 반복이 커지면 시장에서 의미 있는 힘이 된다. 오늘 7만 원이 내일 7만 원이 되고, 다음 주도 7만 원이 되고, 다음 달도 7만 원이 되고, 그걸 수만 명이 동시에 반복한다. 그러면 그 매수는 “작은 거래의 합”이 아니라 “구조적인 수요”가 된다. 이게 흔히 말하는 “지수가 지수를 부풀린다”는 말의 실제 모습이다. 지수라는 규칙이 ‘큰 기업을 더 많이 산다’고 정해놓으면, ETF 자금 유입은 그 규칙을 반복 실행한다. 실행이 반복될수록 큰 기업은 더 큰 비중을 갖게 되고, 비중이 커질수록 다음 실행에서 더 많은 돈을 받는다. 이 과정에는 조종자도, 음모도 없다. 규칙이 그렇게 설계돼 있을 뿐이다. 여기서 “부풀린다”는 표현이 불편하게 들릴 수도 있다. 마치 누군가가 주가를 인위적으로 만든 것처럼 들리기 때문이다. 하지만 여기서 말하는 건 ‘조작’이 아니라 ‘기울어진 자동화’에 가깝다. 큰 기업이 커진 이유가 실적 때문이든, 기술 때문이든, 브랜드 때문이든, 그건 별개다. 중요한 건 그 위에 추가로 얹히는 힘이다. ETF 구조는 “큰 기업이 더 큰 비중을 받도록” 설계되어 있다. 그러니까 실력으로 커지는 힘 위에, 자동 수요라는 관성이 더 붙을 수 있다. 그 관성은 눈에 잘 안 보이지만, 시간이 지나면 무게로 남는다. 그래서 ETF 시대에는 이런 미묘한 전환이 일어난다. “좋은 기업이라서 커진다”는 설명이, 어느 순간부터 “큰 기업이라서 더 커진다”는 설명으로 조금씩 이동한다. 처음엔 실력으로 컸을 수 있다. 하지만 어느 시점부터는 돈이 들어오는 방식 자체가 덩치를 유지하고 키우는 힘이 된다. 좋고 나쁨을 따져서 키우는 게 아니라, 크기 자체를 기준으로 계속 키우는 구조가 된다. 이때 자연스럽게 ‘지수 안과 지수 밖’이라는 감각도 생긴다. 지수에 들어가면 ETF의 자동 매수 흐름에 올라탄다. 지수에 없으면, 아무리 괜찮아 보여도 그 자동 흐름에서는 한 발 비켜나 있다. 물론 지수 밖에도 돈은 들어간다. 하지만 ETF 자금이라는 거대한 자동 흐름은 기본적으로 지수 구성에 종속된다. 그래서 지수에 포함된다는 사실 자체가, 추가 수요를 불러오는 사건이 된다. 지수에 편입된 순간부터, 그 기업은 매번 들어오는 돈의 일정 비율을 자동으로 배정받는 쪽에 놓인다. 여기서 이런 반박이 나온다. “그래도 나는 분산했잖아. 내 ETF엔 기업이 많고, 한 기업이 무너지면 큰일은 안 나잖아.” 이 말은 맞다. 다만 이 말은 위험을 하나만 정의하고 있다. ETF가 줄여주는 위험은 ‘내가 고른 한 기업의 사고’다. 반면 ETF가 그대로 남겨두는 위험은 ‘시장 전체가 특정 구조에 더 의존하게 되는 위험’이다. 이 둘은 성격이 완전히 다르다. 첫 번째 위험은 눈에 띈다. 뉴스에도 크게 나오고, 차트에도 바로 찍힌다. “내 종목 망했다”는 형태다. 그래서 이 위험을 피하면 안전해졌다고 느끼기 쉽다. 두 번째 위험은 훨씬 조용하다. 폭발이 아니라 쏠림이다. “분산돼 있으니 괜찮다”는 믿음 아래에서, 돈이 계속 비슷한 기업으로 되돌아가는 과정이 오랜 시간 이어진다. 쏠림이 커질수록 시장의 방향은 소수 기업의 실적과 뉴스에 더 민감해지고, 나머지 기업들은 숫자로는 많아도 영향력은 얇아진다. 이게 왜 문제냐고 묻는다면, 아주 현실적으로 이렇게 말할 수 있다. ETF를 산 사람은 보통 이렇게 기대한다. “나는 시장 전체를 샀으니, 시장 전체가 골고루 성장하면 따라간다.” 그런데 실제 체감은 이렇게 바뀌기 쉽다. “나는 시장 전체를 샀는데, 내 성과는 결국 상위 몇 개 기업이 결정한다.” 시장 전체를 산 게 아니라, 상위 몇 개 기업의 ‘무게’를 산 것처럼 느껴진다. 여기에 하나가 더 겹친다. ETF는 내 돈을 골고루 분산시켜 준다고 느끼게 만들지만, 많은 사람들이 분산에서 얻는 가장 큰 이득은 사실 ‘변동성의 공포가 줄었다’는 감각이다. 하루하루 개별 종목이 급락하는 공포는 줄어든다. 대신 다른 공포가 들어온다. 몇 년 동안 성과가 거의 없을 때, 내가 이걸 정말 버틸 수 있느냐는 공포다. 이 공포는 뉴스에 크게 나오지 않는다. 차트에도 잘 안 찍힌다. 조용히 사람을 지치게 만든다. 그래서 ETF가 만들어주는 분산은 이렇게 정리할 수 있다. 기업 이름은 분산되었다. 하루 단위의 개별 폭탄도 분산되었다. 내가 판단해야 한다는 부담도 분산된 것처럼 느껴졌다. 하지만 그 감각이 만들어지는 동안, 그대로 남아 있는 것이 있다. 돈이 반복적으로 되돌아가는 방향, 시장의 무게 중심, 영향력의 편중, 지수 안쪽에 붙어 있는 자동 매수의 관성이다. 이건 분산되지 않는다. 오히려 시간이 갈수록 더 단단해질 수 있다. 그리고 이건 ETF를 몰라서 당하는 손해라기보다, ETF의 편안함이 질문을 줄이는 방식에 가깝다. 여기서 독자는 이렇게 물을 수 있다. “그래서 어쩌라고. ETF 하지 말라는 거야? 그럼 개별주식을 사라는 거야?” 이 질문 자체는 자연스럽다. 투자 글은 보통 여기서 행동 지침을 기대하게 만들기 때문이다. 하지만 여기서 결론이 “그럼 이걸 사라”로 끝나면, 방금까지 얘기한 구조를 다시 지워버리게 된다. ETF의 문제를 말해놓고 다른 상품을 정답처럼 던져버리면, 독자는 다시 ‘정답 버튼’을 찾게 된다. 그리고 그 순간, 판단은 또 위탁된다. 여기서 말하는 건 ETF를 그만 사라는 얘기가 아니다. 개별주식이 더 낫다는 얘기도 아니다. 그 둘을 비교해서 우열을 정하는 얘기 자체가 지금 논점이 아니다. 같은 ETF를 사도, 두 상태는 완전히 다르다. 하나는 “나는 시장 전체를 고르게 산다”는 감각 속에서 산다. 다른 하나는 “나는 이미 큰 기업 비중이 큰 구조를 산다”는 이해 속에서 산다. 둘 다 ETF를 사고, 둘 다 장기라는 말을 쓰고, 둘 다 분산이라는 말을 한다. 하지만 흔들리는 구간에서 행동이 달라질 가능성은 여기서 갈린다. 구조를 모르면, 흔들릴 때 ‘왜 흔들리는지’를 이해하지 못하고 감정으로 반응할 가능성이 커진다. 구조를 알면, 같은 흔들림을 봐도 “이건 내가 이미 선택한 구조의 결과”로 받아들이는 쪽에 가깝다. ETF를 사는 게 잘못이라는 말은 아니다. 다만 ETF를 사는 순간 느끼는 ‘분산의 안도감’이 정확히 어떤 종류의 안도감이었는지, 그 감각이 어디서 왔는지, 그 감각이 덮어버린 구조가 무엇인지는 알고 있어야 한다는 얘기다. 마지막으로 하나만 남기자. 당신이 ETF를 사며 “분산됐다”고 느낀 건, 정확히 무엇이었나. 기업 이름이 많아졌다는 사실이었나, 하루하루 흔들림이 줄었다는 안정감이었나, 아니면 내가 판단하지 않아도 된다는 해방감이었나. 그리고 그 감각이 만들어지는 동안, 당신 돈의 무게는 실제로 어디에 더 붙어 있었나.
ETF는 왜 평생 들고 있으면 되는 자산 처럼 말해지는가. ETF 얘기를 하면 거의 반드시 따라붙는 문장이 있다. 이 말은 틀리지 않았다. 실제로 미국 지수는 폭락을 겪고도 결국 이전 고점을 넘겼다. 시간이 모든 걸 해결해 줬다. 그래서 ETF는 시간을 아군으로 만드는 선택 처럼 보인다. 여기까지만 보면 반박할 게 없다. 문제는 이 문장이 너무 많은 걸 생략하고 있다는 점이다. 닷컴버블 이후 시장이 회복하는 데 걸린 시간은 10년 가까웠다. 정확히 말하면, 다시 나스닥 지수가 원점으로 돌아오는 데 15년이 걸렸다. 여기서 질문 하나가 생긴다. 당신은 그 15년을 버틸 수 있었을까. ETF를 추천하는 사람들은 항상 "30년 들고 가" 라고 말한다. 하지만 아무도 "그중 15년은 아무 일도 없을 수 있다" 라고는 말하지 않는다. 우상향 그래프에서 제일 조용히 지워지는 구간이 바로 그 시간이다. 여기서 이런 반박이 나올 수 있다. 맞다. 이론적으로는 버티면 된다. 하지만 여기서 중요한 건 이론이 아니라 인간이다. ETF는 숫자로는 합리적이지만, 감정에는 전혀 합리적이지 않다. 20대가 ETF를 사는 이유를 생각해보자. 경제 구조를 이해해서가 아니다. 기업 분석을 끝냈기 때문도 아니다. "미국은 결국 오른다"는 문장을 믿기 때문이다. 이건 투자 철학이 아니라 신념에 가깝다. ETF의 본질은 뭔가. ETF는 "괜찮은 기업을 고르는 상품"이 아니다. ETF는 "이미 커진 기업을 더 많이 담는 구조"다. 잘될 것 같은 기업이 아니라, 이미 잘된 기업에 더 무게를 싣는 방식이다. 그래서 ETF는 안정적으로 보인다. 그리고 동시에, 왜 이 기업들이 담겨 있는지 묻지 않게 만든다. ETF의 위험성은 폭락이 아니다. 진짜 위험은 아무 일도 안 일어나는 시간이다. 10년 동안 지수가 오르지 않는다면, 당신은 매달 적립하면서 이런 생각을 하게 된다. “이걸 왜 하고 있지?” “내가 틀린 거 아니야?” “차라리 다른 걸 했어야 하는 거 아니야?” 이때 대부분의 사람은 이론적으로 옳은 선택을 하지 않는다. 감정적으로 편한 선택을 한다. ETF가 위험한 이유는 망할 수 있어서가 아니다. 지루해서 포기하게 만들 수 있기 때문이다. 여기서 또 이런 말이 나온다. 맞다. 그게 바로 ETF의 이상적인 사용법이다. 그런데 그 사용법을 지킬 수 있는 사람은 생각보다 많지 않다. 왜냐하면 ETF는 ‘아무 생각 안 해도 되는 상품’으로 팔리지만, 사실은 가장 오랫동안 생각을 참아야 하는 상품이기 때문이다. 30년 우상향을 말하는 사람들은 중간의 10년 침묵을 너무 쉽게 건너뛴다. 하지만 인생에서 10년은 짧지 않다. 특히 20대에게 10년은 인생의 방향이 몇 번은 바뀌는 시간이다. ETF 자체가 문제는 아니다. 문제는 ETF를 "뭔지 몰라도 되는 자산" 으로 소비하는 태도다. ETF를 사는 순간, 당신은 이미 하나의 전제를 받아들인다. “이 구조가 앞으로도 유효할 것이다.” 그 전제가 맞을 수도 있다. 틀릴 수도 있다. 중요한 건, 대부분은 그 전제를 인식조차 하지 않고 산다는 점이다. 그래서 마지막 질문은 이거다. ETF를 평생 들고 가면 좋을 수도 있다. 실제로 그럴 가능성도 높다. 하지만 당신은 그걸 ‘알고’ 사는가, 아니면 그냥 다들 괜찮다니까 사는가. 이 둘은 결과가 같아 보여도, 과정에서는 전혀 다른 선택이다.
Part 8 of 8 [제목]: 실전 프로젝트: 배운 것 총동원해서 자동화 스크립트 만들기[이번 파트 목표]: 1~7편에서 배운 모든 것을 활용해, 실무에서 쓸 법한 자동화 리포트 스크립트를 완성한다. 이것도 미검증상태 이번에 배울 것 드디어 마지막 편이야! 지금까지 배운 변수, 조건문, 반복문, 배열, 함수, DB 탐색, 파일 입출력을 모두 엮어서 '특정 조건에 맞는 파이프를 찾아 파일로 정리하는' 완전한 자동화 스크립트를 만들어 볼 거야. 왜 필요한데? 실무에서는 이런 일이 정말 많아. "이 구역에 있는 파이프 중에, 직경(BORE)이 100mm 넘는 것들만 목록으로 뽑아주세요." 같은 요청 말이야. 수백, 수천 개를 일일이 클릭해서 확인할 순 없잖아? 이럴 때 PML 스크립트 하나면 커피 한잔 마시는 동안 컴퓨터가 알아서 다 해줄 수 있지. 이게 바로 우리가 PML을 배우는 이유야! 핵심 개념: 프로젝트 설계하기 우리가 만들 스크립트는 이런 순서로 작동할 거야. 어때? 지금까지 배운 내용이 다 들어있지? 이제 코드로 옮겨보자. 코드 예제 (따라하기) 하나의 완성된 코드를 단계별로 나눠서 설명해 줄게. 전체 코드를 pipe_report.pmlfnc 같은 파일로 저장하고 로드해서 실행해 봐. 1단계: 기본 틀과 설정 잡기 먼저, 전체 로직을 담을 함수를 만들고 필요한 설정값들을 변수로 빼두는 거야. 이렇게 하면 나중에 조건을 바꾸기 쉬워. -- 전체 로직을 담을 메인 함수 define function !!runPipeReport() -- 1. 설정값 정의 !outputFile = 'C:\temp\pipe_report.txt' -- 결과가 저장될 파일 경로 !Threshold = 100 !boreThreshold = !Threshold.bore() $P Report generation started... $P Bore threshold: $!boreThreshold $P Output file: $!outputFile -- (이후 코드는 여기에 계속 추가될 거야) endfunction 2단계: 데이터 수집 및 처리 루프 만들기 이제 !!collectAllFor 함수로 현재 요소(CE) 아래의 모든 파이프를 수집하고, DO VALUES로 반복문을 돌릴 준비를 하자. 결과를 담을 빈 배열도 미리 만들어둬. define function !!runPipeReport() !outputFile = 'C:\temp\pipe_report.txt' !Threshold = 100 !boreThreshold = !Threshold.bore() -- ... (이전 코드 생략) ... -- 2. 결과 저장용 배열 생성 !foundPipes = object ARRAY() -- 3. 현재 위치(CE)에서 모든 PIPE 수집 !allPipes = !!collectAllFor('PIPE', '', !!ce) !totalCount = !allPipes.size() $P Found $!totalCount total pipes. Processing... -- 4. 수집된 파이프들을 하나씩 확인 do !pipe values !allPipes if (!pipe.bore GT !boreThreshold) then -- 조건에 맞으면 배열에 추가 !pipeInfo = 'Name: ' + !pipe.name + ', Bore: ' + !pipe.bore.string() !foundPipes.append(!pipeInfo) endif enddo -- (이후 코드는 여기에 계속 추가될 거야) endfunction 3단계: 파일에 결과 쓰고 최종 보고하기 이제 조건에 맞는 파이프 정보가 담긴 !foundPipes 배열을 파일로 쓸 차례야. 파일 작업은 에러가 날 수 있으니 HANDLE 블록으로 감싸주는 센스! define function !!runPipeReport() !outputFile = 'C:\temp\pipe_report.txt' !Threshold = 100 !boreThreshold = !Threshold.bore() !foundPipes = object ARRAY() !allPipes = !!collectAllFor('PIPE', '', !!ce) !totalCount = !allPipes.size() -- ... (데이터 처리 루프 코드 생략) ... do !pipe values !allPipes if (!pipe.bore GT !boreThreshold) then !pipeInfo = 'Name: ' & !pipe.name & ', Bore: ' & !pipe.bore.string() !foundPipes.append(!pipeInfo) endif enddo -- 5. 파일에 결과 쓰기 (에러 처리 포함) handle ANY -- 파일 열기 (쓰기 모드) !file = object FILE(!outputFile, 'WRITE') -- 헤더 추가 !file.writeString('--- Pipe Report ---') !file.writeString('Found pipes with BORE greater than ' & !boreThreshold.string()) !file.writeString('-------------------') -- 배열에 저장된 내용을 한 줄씩 파일에 쓰기 do !line values !foundPipes !file.writeString(!line) enddo -- 파일 닫기 (아주 중요!) !file.close() elsehandle NONE -- 정상 처리 시 메시지 $P Successfully wrote $!foundPipes.size() items to file. endhandle -- 6. 최종 결과 요약 $P --- Summary --- $P Total pipes checked: $!totalCount $P Pipes matching criteria: $!foundPipes.size() $P Report finished. endfunction 이제 이 함수를 로드하고 명령창에 !!runPipeReport()를 실행해 봐. C:\temp 폴더에 pipe_report.txt 파일이 멋지게 생성될 거야! 자주 하는 실수 ❌ 실수 1: 파일 닫기(close()) 까먹기 -- ❌ 잘못된 코드 !file = object FILE('C:\temp\test.txt', 'WRITE') !file.writeString('hello') -- !file.close() 를 안하면 파일이 계속 열려있거나 내용이 제대로 안 써질 수 있어. -- ✅ 올바른 코드 !file = object FILE('C:\temp\test.txt', 'WRITE') !file.writeString('hello') !file.close() -- 작업이 끝나면 무조건 닫아주기! ❌ 실수 2: 반복문 안에서 파일 열고 닫기 -- ❌ 비효율적인 코드 do !pipe values !foundPipes !file = object FILE('C:\temp\report.txt', 'APPEND') -- 매번 열고 !file.writeString(!pipe.name) !file.close() -- 매번 닫고... 너무 느려! enddo -- ✅ 올바른 코드 !file = object FILE('C:\temp\report.txt', 'WRITE') -- 처음에 한 번만 열고 do !pipe values !foundPipes !file.writeString(!pipe.name) enddo !file.close() -- 마지막에 한 번만 닫기! 연습 문제
Part 7 of 8 [제목]: 에러 처리와 파일 다루기, 이젠 쫄지 말자![이번 파트 목표]: 코드가 터져도 괜찮아! 에러를 잡고, 외부 파일을 읽고 쓰는 법을 배울 거야. 요번글은 PML AI 로 뽑아내고 아직 검증안했습니다 1. 이번에 배울 것 안녕! 지난 파트까지 잘 따라왔다면 이제 제법 PML과 친해졌을 거야. 이번엔 프로그램의 안전벨트, handle 에러 처리와 외부 세계와 소통하는 창구, FILE 객체 사용법을 배울 거야. 이걸 배우면 네 코드는 훨씬 더 안정적이고 강력해질 거야! 2. 왜 필요한데? 상상해봐. 네가 만든 멋진 자동화 매크로가 외부 텍스트 파일(pipe_list.txt)에서 파이프 목록을 읽어와서 한 번에 속성을 바꿔준다고 치자. 동료한테 "이거 써봐!" 하고 줬는데, 동료가 실수로 그 파일을 지웠네? 그럼 어떻게 될까? File not found 에러가 뻥! 터지면서 프로그램이 그냥 멈춰버릴 거야. 동료는 "이거 왜 안돼?" 하고 널 찾아오겠지. 민망하잖아. 이럴 때 필요한 게 바로 에러 처리야. 파일이 없으면 "파일이 없는데요? 확인 좀 해주세요." 라고 친절하게 알려주고 프로그램을 안전하게 종료시키는 거지. 그리고 파일 다루기는 애초에 그 pipe_list.txt 같은 외부 파일을 읽고 쓸 수 있게 해주는 기술이고. 둘은 환상의 짝꿍이야! 3. 핵심 개념 설명 프로그램의 안전망, handle! handle은 말 그대로 '처리하다'라는 뜻이야. 코드 실행 중에 뭔가 문제가 생겼을 때(에러 발생), 프로그램이 죽지 않고 우리가 미리 정해놓은 대처를 하게 만드는 안전망 같은 거지. 기본 구조는 이래. handle ANY -- 에러가 날 것 같은 위험한 코드 elsehandle NONE -- 에러 없이 성공했을 때 실행할 코드 endhandle 만약 어떤 에러가 났는지 궁금하면 !!error라는 전역 변수를 확인하면 돼. 에러에 대한 정보가 담겨있거든. 외부 파일과 대화하기, FILE 객체 PML 세상 밖의 파일들(txt, csv 등)과 데이터를 주고받으려면 FILE이라는 객체가 필요해. 이건 마치 우리 코드와 컴퓨터 파일 시스템을 연결해주는 '전화기' 같은 거야. 파일 작업은 보통 4단계로 이뤄져. 주요 open 모드: 4. 코드 예제 (따라하기) 자, 이제 직접 코드를 짜보면서 익혀보자. 예제 1: 안전하게 파일 읽어보기 바탕화면에 my_data.txt 파일이 있다고 상상하고 코드를 짜 볼 거야. 처음엔 파일이 없어서 에러가 나고, 두 번째엔 파일을 만들고 실행해서 성공하는 걸 보자. -- 1. 파일 경로를 변수에 저장 !filePath = 'C:\Users\Public\Desktop\my_data.txt' -- 2. handle로 안전하게 파일 열기 시도 handle ANY -- 이 안에서 에러가 나면 바로 아래 elsehandle로 점프! !file = object FILE(!filePath) !file.open('READ') elsehandle NONE -- 에러가 없었을 때 실행되는 곳 $P '$!filePath' 파일을 성공적으로 열었습니다! -- 여기서 파일 읽기 작업을 하면 돼. !file.close() -- 작업 끝났으면 닫기! endhandle -- 3. 만약 에러가 났다면, !!error 변수로 확인 가능 if (!!error.isSet()) then $P 에러 발생: $(!!error.text) endif 실행 결과 (파일이 없을 때): 에러 발생: File C:\Users\Public\Desktop\my_data.txt not found 이제 바탕화면에 my_data.txt 파일을 직접 만들고 아무 내용이나 적어봐. 그리고 위 코드를 다시 실행해봐. 실행 결과 (파일이 있을 때): 'C:\Users\Public\Desktop\my_data.txt' 파일을 성공적으로 열었습니다! 어때? handle 덕분에 프로그램이 죽지 않고 상황에 맞게 동작하지? 예제 2: 파일에 내 정보 기록하기 이번엔 내 이름과 현재 시간을 파일에 써보자. !logPath = 'C:\E3D_log.txt' !file = object FILE(!logPath) -- 'WRITE' 모드는 파일이 있으면 뒤에 이어서 써줘. !file.open('WRITE') -- 현재 시간을 가져오는 내장 함수 !now = !!getDateTime('datetime', '', ' ') -- 파일에 쓸 내용 만들기 !logMessage = 'User: Admin, Time: ' & !now -- 파일에 한 줄 쓰기 !file.writeString(!logMessage) -- 아주 중요! 꼭 닫아주기 !file.close() $P '$!logPath' 파일에 로그를 기록했습니다. 이제 C:\ 드라이브에 가보면 E3D_log.txt 파일이 생겼을 거고, 열어보면 방금 기록한 내용이 있을 거야. 코드를 여러 번 실행하면 내용이 계속 추가되는 것도 확인해봐! 5. 자주 하는 실수 ❌ 실수 1: 에러 처리만 하고 성공 처리를 안 할 때 -- ❌ 나쁜 코드 handle ANY !file = object FILE('a.txt') !file.open('READ') -- 성공했을 때 뭘 할 건데? 알 수가 없네. endhandle $P 에러가 났나? 성공했나? 이러면 안 돼! 에러가 안 났을 때 뭘 해야 할지가 코드에 없어서 로직이 꼬여. -- ✅ 좋은 코드 handle ANY !file = object FILE('a.txt') !file.open('READ') elsehandle NONE $P 성공! 이제 파일을 처리하자. !file.close() endhandle 이렇게 하자! elsehandle NONE을 사용해서 성공했을 때의 경로를 명확히 해주는 게 좋아. ❌ 실수 2: 파일 작업하고 .close() 안 하기 -- ❌ 절대 금지! !file = object FILE('C:\temp.txt') !file.open('OVERWRITE') !file.writeString('중요 데이터') -- !file.close() 를 까먹었다! 이러면 안 돼! 데이터가 파일에 제대로 안 써질 수도 있고, 파일이 계속 열려 있어서 다른 프로그램이 접근 못하는 문제가 생길 수 있어. -- ✅ 좋은 코드 !file = object FILE('C:\temp.txt') !file.open('OVERWRITE') !file.writeString('중요 데이터') !file.close() -- 작업이 끝나면 무조건 닫는다! 이렇게 하자! open()을 했으면 close()는 묻지도 따지지도 말고 세트로 따라와야 해. 6. 연습 문제 이제 네 차례야! 직접 해봐야 실력이 늘어.
Part 6 of 8 [제목]: E3D 모델링의 핵심, DB 탐색하고 속성 파헤치기[이번 파트 목표]: E3D 데이터베이스에서 원하는 요소를 찾고, 그 요소의 정보를 읽는 방법을 배운다. 1. 이번에 배울 것 오늘은 E3D 데이터베이스라는 거대한 도서관에서 내가 원하는 책(요소)을 정확히 찾아내고, 그 책의 정보(속성)를 읽는 방법을 배울 거야. 2. 왜 필요한데? 상황을 상상해 봐. 상사가 갑자기 "우리 프로젝트에 있는 100mm보다 큰 파이프들 전부 찾아서 목록 만들어 와!"라고 시켰어. 수천, 수만 개의 파이프를 눈으로 하나씩 클릭해서 확인할 거야? 절대 아니지! PML을 사용하면 단 몇 줄의 코드로 이런 작업을 10초 만에 끝낼 수 있어. 특정 조건의 부재를 찾고, 속성을 읽고, 심지어 한 번에 수정하는 것까지! 이게 바로 PML DB 탐색의 힘이야. 3. 핵심 개념 설명 CE: 너의 현재 위치 !CE는 'Current Element'의 약자야. 지금 네가 E3D 화면에서 선택한 바로 그 요소를 가리키는 특별한 변수지. 네가 파이프를 클릭하면 !CE는 그 파이프가 되고, 구조물을 클릭하면 !CE는 그 구조물이 돼. 앞으로 우리가 탐색을 시작할 출발점이 될 거야. -- 현재 선택한 요소의 이름을 알고 싶을 때 !CE = CE $P 현재 요소 이름: !CE.Name 속성(Attribute): 요소의 주민등록증 모든 E3D 요소(파이프, 장비, 노즐 등)는 자신만의 정보, 즉 '속성'을 가지고 있어. 이름(NAME), 재질(MATREF), 구경(BORE), 위치(POS) 같은 것들이지. 이 속성들은 .(점)을 찍어서 접근할 수 있어. -- 현재 선택한 파이프의 구경(Bore)을 알고 싶다면? !CE = CE !currentBore = !CE.Bore $P 현재 파이프 구경: $!currentBore !!collectAllFor(): E3D의 검색 엔진 이게 오늘 배울 것의 하이라이트야. !!collectAllFor() 함수는 E3D 데이터베이스 전체를 뒤져서 네가 원하는 요소들을 싹 긁어모아 주는 강력한 검색 도구야. 이 함수는 3개의 중요한 정보를 필요로 해. !!collectAllFor('타입', '조건', '검색 시작 위치') 4. 코드 예제 (따라하기) 자, 이제 직접 코드를 짜면서 감을 익혀보자. 목표: 현재 위치(SITE) 아래에 있는 모든 파이프 중에서, 구경(BORE)이 150mm 이상인 것들의 이름과 구경을 출력하기 -- 1. 먼저 현재 위치(SITE) 아래에 있는 모든 PIPE를 찾아보자. -- 타입: 'PIPE', 조건: 없음, 시작위치: !CE (현재 선택한 SITE) !allPipes = !!collectAllFor('PIPE', '', CE) -- 찾은 파이프가 몇 개인지 확인 !pipeCount = !allPipes.size() $P 찾은 파이프 개수: $!pipeCount -- 2. 찾은 파이프 묶음(Collection)을 하나씩 돌면서 정보를 확인하자. -- DO VALUES 구문은 Collection을 순회할 때 아주 유용해! $P --- 150mm 이상 파이프 목록 --- do !pipe values !allPipes -- 3. 각 파이프의 구경(Bore)을 확인해서 150 이상인지 검사 if (!pipe.Bore GE 150) then -- 4. 조건에 맞으면 이름과 구경을 출력 !pipeName = !pipe.Name !pipeBore = !pipe.Bore $P 이름: $!pipeName, 구경: $!pipeBore endif enddo $P --- 검색 완료 --- 실행 방법: 5. 자주 하는 실수 실수 1: !!collectAllFor 인자 개수 틀리기 ❌ 잘못된 코드 -- 3번째 인자(시작 위치)를 빼먹음 !pipes = !!collectAllFor('PIPE', 'BORE GT 100') -- (에러 발생) ✅ 올바른 코드 -- 시작 위치 CE 또는 WORLD 등을 반드시 명시해야 해 !pipes = !!collectAllFor('PIPE', 'BORE GT 100', CE) 실수 2: 비교 연산자 == 사용하기 PML에서는 같음을 비교할 때 ==가 아니라 EQ를 쓴다는 거, 이제는 익숙해졌지? ❌ 잘못된 코드 if (!pipe.Name == '/PIPE-001') then -- 에러! $P 찾았다! endif ✅ 올바른 코드 if (!pipe.Name EQ '/PIPE-001') then $P 찾았다! endif 6. 연습 문제
PML 초보자 가이드: Part 5 of 8 [제목]: 함수와 객체로 코드 재활용하기 [이번 파트 목표]: 반복되는 코드를 함수로 만들고, 나만의 데이터 타입을 객체로 정의할 수 있다. 1. 이번에 배울 것 오늘은 똑같은 코드를 계속 복사-붙여넣기 하지 않도록, 코드를 '재활용'하는 두 가지 핵심 기술, 함수(Function)와 객체(Object)에 대해 배울 거야. 이걸 배우면 코드가 훨씬 깔끔해지고 관리하기 쉬워질 거야! 2. 왜 필요한데? 상황을 하나 상상해보자. 파이프의 단면적을 계산하는 코드를 짰어. (반지름 * 반지름 * 3.14). 그런데 프로젝트 여기저기서 이 계산이 100번이나 필요한 거야. 그럼 이 코드를 100번 복사해서 붙여넣을 거야? 만약 나중에 계산 공식이 반지름 * 반지름 * 3.14159로 더 정확하게 바뀌면? 100군데를 전부 찾아서 고쳐야 해. 완전 끔찍하지? 이럴 때 함수를 쓰는 거야. !!getPipeArea()라는 '나만의 명령어'를 하나 만들어두면, 필요할 때마다 이 명령어만 부르면 돼. 수정도 한 군데만 하면 끝! 객체는 여기서 한 발 더 나아간 개념이야. 파이프 하나에 이름, 직경, 재질, 위치 등 여러 정보가 있잖아? 이걸 각각의 변수 !pipeName, !pipeBore, !pipeMaterial로 관리하면 너무 흩어져 있어서 헷갈려. 객체는 이 모든 정보를 '파이프'라는 하나의 데이터 꾸러미로 묶어주는 역할을 해. 3. 핵심 개념 설명 함수 (Function): '나만의 명령어' 만들기 함수는 특정 작업을 수행하는 코드 덩어리에 이름을 붙여놓은 거야. 레고 블록처럼, 한번 만들어두면 필요할 때마다 가져다 쓸 수 있어. -- 함수 기본 구조 define function !!나만의함수이름(!입력값1 is 타입, !입력값2 is 타입) is 반환타입 -- 여기서 작업 수행 !결과 = !입력값1 + !입력값2 return !결과 endfunction 객체 (Object): '나만의 데이터 꾸러미' 만들기 객체는 연관된 데이터(변수)와 기능(함수)을 하나로 묶은 거야. 우리만의 새로운 데이터 타입을 만드는 거지. -- 객체 기본 구조 define object 나만의객체이름 -- 데이터들 (멤버) member .데이터1 is STRING member .데이터2 is REAL -- 생성자 (초기 설정) define method .나만의객체이름() !this.데이터1 = '기본값' !this.데이터2 = 0 endmethod -- 기능들 (메서드) define method .기능1() $P '데이터1의 값은 ' & !this.데이터1 endmethod endobject 4. 코드 예제 (따라하기) 이제 직접 코드를 짜면서 익혀보자. 1단계: 간단한 함수 만들기 두 숫자를 더하는 간단한 함수를 만들어 볼게. -- 두 숫자를 더해서 결과를 반환하는 함수 정의 define function !!addNumbers(!num1 is REAL, !num2 is REAL) is REAL !result = !num1 + !num2 return !result endfunction -- 함수 호출(사용)하기 !sum = !!addNumbers(10, 20) $P 10 + 20 = $!sum -- 결과: 10 + 20 = 30 !anotherSum = !!addNumbers(5.5, 2.3) $P 5.5 + 2.3 = $!anotherSum -- 결과: 5.5 + 2.3 = 7.8 2단계: 나만의 객체 정의하기 이번엔 파이프 데이터를 담을 PipeData라는 객체를 만들어보자. -- PipeData 객체 정의 define object PipeData -- 멤버 변수 선언 member .name is STRING member .bore is REAL member .material is STRING -- 생성자 메서드 (객체 생성 시 초기화) define method .PipeData() !this.name = 'UNSET' !this.bore = 0 !this.material = 'A106' endmethod endobject 3단계: 객체에 기능(메서드) 추가하기 PipeData 객체에 자신의 정보를 출력하는 메서드와 단면적을 계산하는 메서드를 추가해볼게. define object PipeData member .name is STRING member .bore is REAL member .material is STRING define method .PipeData() !this.name = 'UNSET' !this.bore = 0 !this.material = 'A106' endmethod -- 자신의 정보를 출력하는 메서드 define method .printInfo() $P --- Pipe Info --- $P Name: $!this.name $P Bore: $!this.bore $P Material: $!this.material $P ----------------- endmethod -- 단면적을 계산해서 반환하는 메서드 define method .getArea() is REAL !radius = !this.bore / 2 !area = !radius * !radius * 3.14159 return !area endmethod endobject 4단계: 객체 생성하고 사용하기 이제 우리가 만든 PipeData 객체를 실제로 만들고 사용해보자. -- 1. PipeData 객체 생성 !myPipe = object PipeData() -- 2. 멤버 변수에 값 할당 !myPipe.name = '/P100-B-8' !myPipe.bore = 150.0 -- 3. 메서드 호출 !myPipe.printInfo() !pipeArea = !myPipe.getArea() $P Pipe Area is: $!pipeArea 5. 자주 하는 실수 ❌ 객체 생성 시 object 키워드 누락 다른 언어 습관 때문에 object를 빼먹는 경우가 많아. PML에서는 필수야! -- ❌ 잘못된 코드 !myPipe = PipeData() -- 에러! -- ✅ 올바른 코드 !myPipe = object PipeData() ❌ 메서드 안에서 !this 빼먹기 메서드 안에서 같은 객체의 다른 멤버나 메서드에 접근할 땐 !this를 꼭 붙여줘야 해. define object MyObject member .value is REAL define method .getValue() is REAL -- ❌ 잘못된 코드 -- return .value -- 에러! -- ✅ 올바른 코드 return !this.value endmethod endobject ❌ 함수에서 return 안 하기 함수가 값을 돌려주기로 약속(is REAL 처럼)했는데 return을 안 하면, 함수를 호출한 쪽은 UNSET 값을 받게 돼서 에러가 날 수 있어. define function !!badFunc() is REAL !result = 100 -- return을 안 함 endfunction !value = !!badFunc() $P Value is: $!value -- Value is: UNSET if (!value GT 50) then -- UNSET과 비교하면 에러 발생! $P Greater than 50 endif 6. 연습 문제