A business can have plenty of customers, strong sales and a healthy-looking profit while still experiencing financial pressure. The reason is simple: profit and cash flow are not the same thing . A business may record revenue when a customer is invoiced, but the actual payment might not arrive for several weeks. During that time, the business still has to pay employees, suppliers, rent, tax obligations and other operating expenses. For growing businesses in Perth and across Western Australia, understanding this difference is particularly important. As revenue increases, expenses and financial commitments can increase as well. Cash flow management helps business owners understand when money is expected to come in, when payments are due and whether there may be periods where available cash becomes tight. Professional accounting support can make this process easier. An experienced Business Accountant Perth can help business owners understand their financial information, monitor cash flow and plan for upcoming tax and business obligations. Why Cash Flow Matters to Every Business Cash is what allows a business to continue operating. Even a profitable business can experience difficulties if it does not have enough cash available when payments become due. Consider a business that invoices customers $150,000 during a month. On paper, the business may appear to have generated substantial revenue. However, if customers have 30 or 60 days to pay, the business may not actually receive the money immediately. Meanwhile, the business may need to pay: Employee wages Supplier invoices Rent Insurance Software subscriptions GST Tax obligations Loan repayments This creates a timing difference between revenue and available cash. What Is Business Cash Flow Management? Business cash flow management involves monitoring and planning the movement of money into and out of a business. It helps owners understand: How much cash is available now How much money is expected to arrive Which payments are due When tax obligations may need to be paid Whether upcoming expenses can be covered Whether there is enough cash to support planned growth The objective is not simply to keep more money in the bank. It is to give the business owner better visibility over the timing of financial commitments. Profit and Cash Flow Are Not the Same This is one of the most important concepts for business owners to understand. Profit generally reflects the difference between income and expenses over a particular accounting period. Cash flow looks at the actual movement of money into and out of the business. These can produce very different pictures. A Simple Example Imagine a Perth consulting business completes $80,000 of work in June. The business sends invoices to its clients with payment terms of 30 days. The revenue may be recognised in the financial records, but the $80,000 may not arrive in the bank account until July. At the same time, the business has $25,000 in employee and supplier payments due in June. The business may be profitable, but it still needs enough available cash to meet those immediate obligations. This is why business owners should monitor both profitability and cash flow. Common Causes of Business Cash Flow Problems Cash flow problems can happen for many reasons. Some are related to customers, while others come from business spending or poor planning. Slow Customer Payments Late payments are one of the most common causes of cash flow pressure. When customers take longer to pay, the business has less available cash even though the revenue has already been recorded. Businesses can monitor: Outstanding invoices Invoice ageing Customer payment patterns Overdue accounts Having clear payment terms and following up overdue invoices can also help improve the timing of incoming cash. Rapid Business Growth Growth may sound like the ideal solution to financial problems, but rapid growth can actually increase cash flow pressure. A growing business may need to spend money on: New employees Inventory Equipment Marketing Technology Premises Contractors These costs can occur before the additional revenue is received. For example, a business might hire three employees because sales are increasing. The payroll expense begins immediately, while some new customer invoices may not be paid for several weeks. Growth therefore needs to be planned alongside cash flow. How Financial Reporting Improves Cash Flow Visibility Financial reporting provides information that can help business owners understand what is happening financially. Useful reports may include: Profit and loss statements Balance sheets Cash flow reports Accounts receivable reports Accounts payable reports Each provides a different perspective. Profit and Loss Shows revenue and expenses over a period. Balance Sheet Shows assets, liabilities and equity at a particular point in time. Cash Flow Report Shows the movement of cash through the business. Accounts Receivable Shows money customers owe the business. Accounts Payable Shows money the business owes suppliers and other parties. Together, these reports provide a more complete view of the business. Managing Customer Payments Getting paid on time is an important part of cash flow management. Businesses should understand their customer payment patterns. If customers consistently take 60 days to pay invoices, the business should account for that timing when planning its expenses. Practical Steps Businesses Can Take Businesses can consider: Setting clear payment terms Issuing invoices promptly Monitoring overdue invoices Following up unpaid accounts Reviewing customer payment history The appropriate approach will depend on the type of business and its customer relationships. The important point is to avoid treating accounts receivable as money that is immediately available. Planning for Supplier Payments Cash flow management also involves understanding money going out of the business. Businesses may have regular supplier payments for: Stock Materials Professional services Software Utilities Contractors Knowing when these payments are due makes it easier to compare them with expected customer receipts. For example, if several large supplier payments fall during a period when customer payments are expected to be low, the business owner can identify the potential cash flow pressure in advance. Planning for Tax and GST Payments Tax obligations should also be included in cash flow planning. Depending on the business, this may include: GST BAS obligations PAYG withholding PAYG instalments Company tax These obligations should not come as a surprise. Regular accounting reviews can help business owners understand their financial position and plan for upcoming liabilities. Why Is Tax Planning Important for Cash Flow? Imagine a business has generated strong profits throughout the year but has not set aside enough money for its future tax obligations. When the payment becomes due, the business may suddenly need to find a substantial amount of cash. Regular tax planning can help business owners anticipate these obligations. Cash Flow Forecasting for Growing Businesses A cash flow forecast is an estimate of expected cash inflows and outflows over a future period. It does not guarantee what will happen. Instead, it provides a planning tool. A forecast may include: Expected Cash Inflows Customer payments Other business income Financing Investment income where applicable Expected Cash Outflows Wages Supplier payments Rent Tax Loan repayments Equipment purchases Other operating expenses The forecast can then show whether the business is likely to have enough cash available during each period. How Cash Flow Forecasting Can Support Better Decisions Imagine a business is considering purchasing $50,000 of new equipment. The owner may be able to afford the purchase based on annual profit. But the timing of the payment matters. If the purchase coincides with: Large tax payments Payroll increases Several overdue customer invoices Major supplier bills the business could experience temporary cash pressure. A cash flow forecast can help the owner consider the timing of the purchase alongside other commitments. This does not automatically determine whether the purchase should happen. It simply provides better financial information for the decision. Business Financial Planning Should Include Cash Flow Financial planning is broader than simply looking at last year's results. A business should consider where it expects to be in the coming months. Planning can include: Revenue expectations Expense budgets Staffing plans Tax obligations Capital expenditure Debt repayments Expansion costs Cash flow should be part of this planning process. A business may have ambitious growth plans, but those plans need to be supported by sufficient financial resources. Why Growing Businesses May Need Professional Accounting Support As a business becomes larger, financial administration usually becomes more complicated. There may be: More transactions More employees More suppliers More customers Higher revenue More assets More tax obligations At this stage, basic bookkeeping may not provide all the information the owner needs. A professional accountant can help businesses review financial reports, understand cash flow and prepare for tax obligations. For businesses that need more structured financial support, a Business Accountant Perth can help connect day-to-day accounting information with broader cash flow and financial planning. When Professional Accounting Support Becomes Valuable There is no universal revenue threshold at which every business needs an accountant. Instead, business owners can look at the complexity of their financial situation. Professional accounting support may be useful when: Cash flow becomes difficult to predict Revenue is growing quickly The business employs more staff Tax obligations are increasing Financial reports are difficult to understand The business is considering expans
오늘은, JPA N+1 문제에 대해서 알아보자 단순하게, LAZY 로딩 ( 지연로딩 ) 때문에 생기는게아니라, JPQL 이 무엇을 조회했고 연관관계가 어떤식으로 로딩이 되어지고, Proxy 초기화를 할때 어떤 SQL 이 실행되는지를 알아보자. 축구 팀과, 선수가 있다고 하자. @Entity public class Team { @Id private Long id; private String name; @OneToMany(mappedBy = "team", fetch = FetchType.LAZY) private List<Member> members = new ArrayList<>(); } @Entity public class Member { @Id private Long id; private String nickname; @ManyToOne(fetch = FetchType.LAZY) private Team team; } 현재 팀은 100개가 존재하고, 팀들을 조회하는 코드를 실행했다고 하자. List<Team> teams = teamRepository.findAll(); SQL 한번이면 당연히 가능할것같다. SELECT * FROM team; 실제로는, 당연히 해당 쿼리 한번으로 모든 팀들을 조회할 수 있다. 문제는, 다음과 같은 코드를 사용할때이다. for (Team team : teams) { System.out.println(team.getMembers().size()); } 이때, 갑자기 예상하지못한 SQL 들이 실행될 수 있다. SELECT * FROM member WHERE team_id = 1; SELECT * FROM member WHERE team_id = 2; SELECT * FROM member WHERE team_id = 3; ... SELECT * FROM member WHERE team_id = 100; 결과적으로는, 해당 코드들을 실행한다고 했을때 팀 조회 1번 회원 조회 100번 ---------------- 총 101번 이게, JPA 문제들중 대표적인 N + 1 문제이다. N + 1 문제를 잘 알아야하는 이유는, 문제가 어디서 발생했는지 잘 안보인다는것이다. 작성한 코드는 다음 두가지 뿐이다. List<Team> teams = teamRepository.findAll(); team.getMembers() Java 코드 2줄을 작성했지만, 실제 DB 에 날리는 SQL 은 수십~수백개 까지 많은 양의 sql 이 실행될 수 있다. 데이터가 적고, 개발중인 환경이라면 문제가 별로 심각해보이지는 않는것처럼 보인다. 예를들어, 팀이 3개뿐이라면 위에있는 코드들은 N+1 문제를 동반한다고해도, 쿼리가 4개만 나갈뿐이다. ( 팀들 조회 1번 + 팀에 있는 멤버들 조회 3번 ) 하지만, 실제 운영하는 환경에서는 팀이 3개인것처럼, 데이터가 적지않다. 수천개에서 많게는 수천만개까지도 있을수도있다. 이때, N + 1 문제가 발생한다고 생각해보자. 기대한 쿼리갯수는 1개인데, 잘못하면 쿼리가 수천만개가 나가는 최악의 상황이 발생할 수 있다. 각 SQL 에 대해서 DB 와의 왕복시간이 매우 적게걸린다고 생각해도 ( 예를들어, 5ms 이라고 가정하자. ) 해당 쿼리가 수천만개가 나간다면 잡아먹히는 시간은 엄청나게 오래걸린다. 그렇기에, N + 1 문제는 성능개선을 위한 필수적인 단계로 해석된다. 내부에서 어떤일들이 벌어지는 걸까? findAll() 을 실행했다. List<Team> teams = teamRepository.findAll(); Spring Data JPA 가, Hibernate 를 통해서 Team 을 조회한다. [[ DB => MySQL 을 사용한다고 하자. ]] Repository ↓ EntityManager ↓ Hibernate ↓ JPQL ↓ SQL ↓ MySQL ( DB ) SELECT t.id, t.name FROM team t; 해당 쿼리로, team 들을 조회한다. 결과로는 Team 1 Team 2 Team 3 ... Team 100 여기까지는 SQL 이 1번만 실행됐다. 위에있는 Team 엔티티를 다시한번 살펴보자. @Entity public class Team { @Id private Long id; private String name; @OneToMany(mappedBy = "team", fetch = FetchType.LAZY) private List<Member> members = new ArrayList<>(); } members 는 fetch = LAZY 로 잡혀져있다. LAZY 의 핵심은 Team 을 조회하는 순간에는, Member 까지는 반드시 DB 에서 조회하고 가져오지는 않는다. 즉, SELECT t.id, t.name FROM team t; 해당 sql 을 통해서 Team 을 조회했다면, 해당 Team 은 다음과 같이 설정되어져있을것이다. Team #1 id = 1 name = "FC Seoul" members = Hibernate PersistentCollection ( Proxy ... -> 실제 Member 가 아님.) 위처럼, LAZY 연관관계로 잡혀져있는 객체는 Team 을 만들때에는 우선, Hibernate 가 관리하는 지연로딩용 객체로 들어갈 수 있다. 아직, 멤버들을 조회하는 SELECT * FROM member WHERE team_id = 1; 해당 sql 쿼리는 실행되지 않았다. getMembers() 사용 이제, 다음 코드를 실행했다고 하자. team.getMembers().size(); Hibernate 입장에서는, members 의 실제 Member 데이터들에 대한것들이 필요하네? 라고 판단한다. 이때, 컬렉션을 초기화 해야한다. ( members -> Proxy 가 아닌, 실제 Member ... ) LAZY Collection ↓ 초기화 필요 ↓ Hibernate ↓ SELECT Member 에 대한 데이터가 필요하기때문에, 이제 해당하는 SQL 을 DB 에 날리게된다. SELECT m.id, m.nickname, m.team_id FROM member m WHERE m.team_id = 1; team_id. = 1 인 Team 에 속한 members 들을 가지고 와야하니, 해당 쿼리가 실행이된다. 이후, team_id = 1 인 Team 의 Member 들이 로딩된다. 이제, team_id = 1 인 팀 뿐만아니라, team_id. = 2 / team_id = 3 / team_id.= 4 .... team_id = 100 까지 members 가 필요하다고 하자. 각각의 팀들에 대해서, 해당 코드가 실행되는것이다. team.getMembers().size(); 그렇다면, 위team_id = 1 인 팀과 마찬가지로, 팀들을 가지고왔을때에는 Member 에 대한 값이 Hibernate 가 관리하는 지연로딩용 객체가 들어가있기때문에, 실제 Member 에 대한 데이터를 가지고오기위해 DB 에 쿼리를 날리게 된다. SELECT * FROM member WHERE team_id = 2; SELECT * FROM member WHERE team_id = 3; SELECT * FROM member WHERE team_id = 4; ... SELECT * FROM member WHERE team_id = 100; 결국, SELECT Team │ ├── Team 1 → SELECT Member ├── Team 2 → SELECT Member ├── Team 3 → SELECT Member │ ... └── Team 100 → SELECT Member 다음과 같은 상태가 된다. 그래서, 이름이 N + 1 ( 1 + N ) 이다. 즉, 메인인 쿼리 1개를 실행했는데, 부가적인 쿼리가 N 개가 발생한것이다. 그렇다면, 지연로딩때문에 발생한 문제이니까. LAZY -> EAGER 로 바꾸면 되는거아닌가? 라고 생각할 수 있다. @OneToMany(fetch = FetchType.LAZY) private List<Member> members; ↓ @OneToMany(fetch = FetchType.EAGER) private List<Member> members; 하지만, 이렇게 해결해서는 안된다. EAGER 는 항상 한번의 JOIN SQL 로 가져온다는 의미가 아니기때문이다. JPA 에게 Eager 상태로 되어져있는 것은, 이 연관관계는 반드시 로딩된 상태여야 한다. 라는것과 가깝다. JPA 구현체인 Hibernate 는, 해당 상태를 만족하기 위해 상황에 따라 추가적인 SQL 을 실행해서, EAGER 요구사항을 충족할 수도 있기때문에, EAGER 로 N+1 문제를 해결하려고 해서는 안된다. 또한, 더 큰 문제가 존재한다. 어떤 API 에서는, Member 가 필요하지 않을수도있다. 즉, Team 에 대한 정보만 필요할뿐 Member 에 대한것은 필요하지 않은 상황이다. { "id": 1, "name": "Footmatch FC" } 그런데, EAGER 를 사용하게되면 어쨋든, 사용하지도 않은 Member 에 대한 데이터를 항상 가져올 가능성이 생기므로, 해당 상황이라면 불필요하다고 생각된다. 그렇기에, 보통 N + 1 문제를 해결하기 위해 우선은, 연관관계를 LAZY 로 두고 필요한 곳에서만 fetch 전략을 명시적으로 작성하자. 로 해결한다. Fetch Join 이번 API 에서는 Team / members ( Member ) 가 필요하다고 하자. 그러면, 연관관계는 LAZY 로 두고 @OneToMany(fetch = FetchType.LAZY) private List<Member> members; 필요한곳에서 JPQL Fetch Join 을 사용하면 된다. @Query(""" select distinct t from Team t join fetch t.members """) List<Team> findAllWithMembers(); @Entity public class Team { @Id private Long id; private String name; @OneToMany(mappedBy = "team", fetch = FetchType.LAZY) private List<Member> members = new ArrayList<>(); } 핵심은, join fetch 이다. 일반 JOIN 과는 달리, 연관관계로 잡혀져있는 해당 Entity 까지 같이 로딩하라 ( 같이 가지고와라 ) 라는 의미이다. SELECT t.id, t.name, m.id, m.nickname, m.team_id FROM team t JOIN member m ON t.id = m.team_id; 즉, team / member 를 조인해서, Team 을 가지고올때 member 에 대한것도 같이 가지고오는것이다. 그렇다면, Team 조회 1번 + Member 조회 N번 이렇게 쿼리가 발생했던 것이, JOIN SQL 1번 즉, SELECT t.id, t.name, m.id, m.nickname, m.team_id FROM team t JOIN member m ON t.id = m.team_id; 해당 쿼리 1번만 나가게 된다. JPQL 로 작성한 쿼리를 한번 더 봐보자. @Query(""" select distinct t from Team t join fetch t.members """) List<Team> findAllWithMembers(); 왜 select distinct t 일까? 왜 Distinct 키워드가 들어간걸까? 이는, Join 으로 인한 데이터 뻥튀기때문이다. 예를들어, Team_id = 1 인 Team 이 다음과같다고 하자. team_id | team_name | member_id -------------------------------- 1 | A FC | 10 1 | A FC | 11 1 | A FC | 12 해당 예시의 DB 의 결과에서는 Team 에 대한 정보는 Member 의 숫자에 따라 계속해서 반복된다. 즉, team_id = 1 인 팀에 member 가 3명이 있기때문에 team_id | team_name -------------------------------- 1 | A FC 1 | A FC 1 | A FC 이렇게, team_id / team_name 이 중복되어서 나타난다. 그래서, Distinct 로 중복을 제거했다. 이는, 관계형 DB 에서 Join 으로 인한 데이터 뻥튀기가 존재하므로 나타나는 문제이다. 실제 코드로 예시를 살펴보자. @Entity public class Team { @Id private Long id; private String name; @OneToMany(mappedBy = "team", fetch = FetchType.LAZY) private List<Member> members = new ArrayList<>(); } 팀 목록 API 가 다음과 같이 있다고 하자. @Transactional(readOnly = true) public List<TeamResponse> getTeams() { return teamRepository.findAll() .stream() .map(team -> new TeamResponse( team.getId(), team.getName(), team.getMembers().size() )) .toList(); } teamRepository 를 통해, 모든 team 들을 조회하고, 가져온 팀들에 대한 정보들을 응답 DTO 로 바꾸고, 해당 dto 들을 List 로 반환하는 코드이다. 해당 코드는 당연히 자연스럽다. 팀들을 조회했고, stream 을 통해 각 Team 들에 대해서 원하는 dto 로 변환하고 List 로 합쳐서 반환하고 ... 매우매우 자연스럽다. 하지만, team.getMembers().size() 가 중요하다. ( members -> List<Member) .. fetch = LAZY ) ) findAll() ↓ SELECT team ↓ TeamResponse 변환 ↓ getMembers() ↓ LAZY 초기화 ↓ SELECT member WHERE team_id = ? ↓ 팀마다 반복 N + 1 이 발생할 수 있다. ( findAll -> jpql fetch Join 을 적용하지 않았다고 하자. ) Fetch Join 을 적용해보자. @Query(""" select distinct t from Team t join fetch t.members """) List<Team> findAllWithMembers(); team 을 가지고 올때, members 에 대한 Member 들도 같이 가지고온다. @Transactional(readOnly = true) public List<TeamResponse> getTeams() { return teamRepository.findAllWithMembers() .stream() .map(team -> new TeamResponse( team.getId(), team.getName(), team.getMembers().size() )) .toList(); } 이제, Team 을 가지고올때 members 또한 같이 가지고오므로, getMembers() 에서 팀마다 멤버를 가지고오기위한 Select 쿼리를 날릴 필요가없게됐다. 전체적인 흐름은 다음과 같다. findAll() ↓ SELECT Team ← 1 ↓ Team 1 ── getMembers() ── SELECT Member Team 2 ── getMembers() ── SELECT Member Team 3 ── getMembers() ── SELECT Member ... Team N ── getMembers() ── SELECT Member ← N 총 SQL = 1 + N Fetch Join 을 통해 ... Team \ JOIN ─── Member / ↓ SELECT 한 번
지난 주말, 로또 5등(5천 원)에 당첨됐다. 회사에서 이 얘기를 하다가 개발팀 팀원들과 “로또 번호 뽑아주는 로직 어떻게 구현하면 좋을까?” 라는 이야기가 나왔다. 생각의 흐름은 다음과 같았다. 1부터 45까지의 숫자를 랜덤하게 뽑으려면 어떻게 해야 할까? 여기서 중복까지 없애려면? 이 로직을 가장 효율적으로 구현하려면 어떤 자료구조가 좋을까? 번호가 뽑힌 순서까지 기억할 수 있을까? 이런 질문들을 따라가다 보니 자연스럽게 Set, 배열, Linked List, 이진 탐색 트리까지 이야기가 이어졌다. 로또 번호를 뽑는 상황을 예시로 Set, 배열, Linked List, 이진 탐색 트리의 특징과 시간복잡도를 정리해보자. 로또는 1부터 45까지의 숫자 중 중복 없이 6개의 번호를 뽑는다. 로또 난수는 어떻게 만들까? JavaScript에서 난수를 만들 때는 Math.random() 을 사용할 수 있다. Math.random() Math.random() 은 0 이상 1 미만 의 숫자를 반환한다. 따라서 1~45 사이의 정수를 만들려면 다음과 같이 작성할 수 있다. Math.floor(Math.random() * 45) + 1 예를 들어 Math.random() 의 결과가 0.52 라면, 0.52 × 45 = 23.4 Math.floor(23.4) = 23 23 + 1 = 24 최종적으로 24 라는 번호를 얻는다. 하지만 여기에는 문제가 있다. 3 → 17 → 3 → 22 처럼 이미 뽑은 숫자가 다시 나올 수 있기 때문이다. 따라서 새로운 숫자를 뽑을 때마다 이미 뽑은 숫자인지 확인하는 과정 이 필요하다. 가장 먼저 떠오른 방법, Set 사용하기 난수를 뽑은 뒤 중복을 제거하는 가장 간단한 방법으로 Set 을 사용할 수 있다. JavaScript의 Set 은 중복된 값을 허용하지 않는 자료구조 다. const numbers = new Set(); while (numbers.size < 6) { const number = Math.floor(Math.random() * 45) + 1; numbers.add(number); } 3 이 이미 들어 있는 상태에서 다시 3 을 추가해도 numbers.add(3); numbers.add(3); Set 에는 하나의 3 만 남는다. Set { 3 } 따라서 로또처럼 중복되지 않는 값을 뽑는 상황에서는 굉장히 간단하게 구현할 수 있다. 특정 값이 이미 들어 있는지 직접 확인하고 싶다면 has() 를 사용할 수도 있다. if (numbers.has(number)) { // 이미 뽑은 번호 } JavaScript의 Set 은 일반적으로 해시 기반으로 구현되기 때문에 add , has , delete 같은 연산을 평균적으로 O(1) 에 처리할 수 있다. 즉, 단순히 로또 번호 6개를 뽑는 것이 목적이라면 사실 다음 정도로도 충분하다. const numbers = new Set(); while (numbers.size < 6) { numbers.add(Math.floor(Math.random() * 45) + 1); } console.log([...numbers]); 여기서 이야기가 끝났다면 자료구조 이야기까지 가지 않았을 것이다...! Set을 사용하지 않고 직접 중복 여부를 관리한다면? 값을 더 빠르게 찾으려면 어떤 자료구조를 사용할 수 있을까? 이 질문을 시작으로 배열과 이진 탐색 트리까지 이야기가 이어졌다. 배열에서 이미 뽑은 숫자를 찾는다면 지금까지 다음 숫자를 뽑았다고 해보자. [3, 7, 12, 20] 새롭게 12 가 나왔다면 배열을 탐색하여 이미 존재하는 값인지 확인할 수 있다. 3 → 아님 7 → 아님 12 → 발견 일반적인 배열 검색은 최악의 경우 배열의 모든 원소를 확인해야 한다. 따라서 시간복잡도는 O(N) 이다. 여기서 중요한 점은 배열이라고 해서 모든 연산이 O(1)은 아니라는 것 이다. arr[3] 처럼 인덱스를 알고 직접 접근한다면 O(1) 이지만, arr.find(...) 처럼 특정 값을 찾는 작업은 O(N) 이 걸린다. 이진 탐색 트리를 사용하면? 숫자를 이진 탐색 트리(Binary Search Tree)에 저장할 수도 있다. 예를 들어 다음과 같은 구조가 있다고 하자. 12 / \ 7 20 / 3 7 을 찾는다면, 7 < 12 → 왼쪽으로 이동 → 7 발견 과 같이 탐색할 수 있다. 균형 잡힌 이진 탐색 트리라면 탐색 시간복잡도는 O(log N) 이다. 배열의 O(N) 탐색보다 빠르다. 다만 모든 이진 탐색 트리가 항상 O(log N) 인 것은 아니다. 트리가 다음처럼 한쪽으로 치우친다면, 3 \ 7 \ 12 \ 20 결국 Linked List와 비슷한 구조가 되고 탐색 시간이 O(N) 까지 증가할 수 있다. 따라서 정확하게는 균형 이진 탐색 트리의 탐색 시간이 O(log N) 이라고 이해하면 된다. 그런데 로또 번호라면 O(1)로 확인할 수 있다 로또 번호에는 중요한 특징이 있다. 숫자의 범위가 1~45로 정해져 있다는 것 이다. 그렇다면 굳이 배열 안에서 값을 검색하거나 트리를 만들 필요가 없다. 처음부터 번호별 공간을 만들어두면 된다. const picked = Array(46).fill(false); 예를 들어 7 이 뽑혔다면 picked[7] = true; 로 저장한다. 이후 다시 7 이 나왔는지 확인할 때는 if (picked[7]) { // 이미 뽑힌 번호 } 처럼 바로 접근하면 된다. 배열의 특정 인덱스로 직접 접근하는 작업은 O(1) 이다. 즉, 배열에서 값 검색 O(N) 균형 이진 탐색 트리 O(log N) 인덱스를 이용한 직접 접근 O(1) 순으로 탐색 속도를 줄일 수 있다. 이 방법이 가능한 이유는 찾으려는 값의 범위를 미리 알고 있기 때문 이다. 뽑힌 순서도 저장해야 한다면? picked 배열만 이용하면 특정 번호가 뽑혔는지는 빠르게 알 수 있다. 하지만 이것만으로는 실제 번호가 7 → 3 → 20 → 12 순서로 뽑혔다는 사실을 알 수 없다. 즉, "7이 뽑혔는가?" 와 "7이 몇 번째로 뽑혔는가?" 는 서로 다른 문제다. 여기서 두 가지 요구사항이 생긴다. 특정 번호가 존재하는지 빠르게 찾고 싶다. 번호가 뽑힌 순서도 그대로 유지하고 싶다. 이 두 가지를 각각 효율적으로 처리하기 위해 Tree와 Linked List를 조합하는 방법도 생각해볼 수 있다. 트리는 특정 값을 빠르게 탐색하는 역할을 하고, 12 / \ 7 20 / 3 Linked List는 실제로 번호가 뽑힌 순서를 유지하는 역할을 한다. 7 → 3 → 20 → 12 즉 같은 데이터를 서로 다른 목적의 자료구조로 관리하는 것이다. Tree → 특정 값 탐색 Linked List → 번호가 뽑힌 순서 유지 하나의 자료구조로 모든 요구사항을 해결하려고 하기보다, 필요한 연산에 따라 여러 자료구조를 조합할 수 있다. 그렇다면 여기서 순서를 유지하는 역할을 맡은 Linked List는 어떤 구조일까? Linked List 나와주세요 Linked List는 각각의 데이터를 노드(Node)로 만들고 서로 연결하는 자료구조다. [7] → [3] → [20] → [12] 일반적인 단방향 Linked List의 노드는 다음 정보를 가진다. Node ├─ value └─ next 양방향 Linked List라면 이전 노드도 저장한다. NULL ← [7] ↔ [3] ↔ [20] ↔ [12] → NULL 각 노드는 대략 다음 정보를 가진다. value prev next Linked List의 가장 큰 장점 중 하나는 삽입과 삭제 다. 예를 들어 7 ↔ 3 ↔ 20 ↔ 12 에서 20 을 삭제한다고 하자. 3 과 12 를 직접 연결하면 된다. 7 ↔ 3 ↔ 12 즉, 배열처럼 뒤에 있는 데이터를 한 칸씩 당길 필요가 없다. 삭제할 노드의 위치를 이미 알고 있다면 Linked List의 삽입과 삭제는 O(1) 에 가능하다. 반면 배열 중간의 값을 삭제하면 뒤쪽 원소들을 이동시켜야 하므로 일반적으로 O(N) 이 걸린다. 단, 여기서 주의할 점이 있다. Linked List에서도 삭제할 노드를 먼저 찾아야 하는 상황이라면 탐색에 O(N)이 추가된다. 따라서 Linked List 삭제는 무조건 O(1) 이라고 외우는 것보다 삭제할 노드의 위치를 이미 알고 있다면 O(1) 이라고 이해하는 것이 정확하다. Linked List에서 세 번째 값을 찾는다면? Linked List가 다음과 같이 구성되어 있다고 하자. 7 → 3 → 20 → 12 세 번째 값을 찾으려면 어떻게 해야 할까? Linked List에서는 보통 첫 번째 노드인 head 를 가지고 있다. head ↓ 7 → 3 → 20 → 12 따라서 세 번째 노드를 찾기 위해서는 1번째 → 7 2번째 → 3 3번째 → 20 처럼 연결을 따라가야 한다. 일반화하면 N번째 노드를 찾는 데 O(N) 이 걸린다. 반면 배열에서는 const numbers = [7, 3, 20, 12]; numbers[2]; 처럼 인덱스로 바로 접근할 수 있다. 따라서 배열의 N번째 원소 접근은 O(1) 이다. 이 부분이 Array와 Linked List의 대표적인 차이점이다. Linked List에서 N번째 값을 더 빠르게 찾으려면? Linked List의 삽입·삭제 성능은 유지하면서 특정 번째 노드에도 빠르게 접근하고 싶다면 다른 자료구조를 함께 사용할 수 있다. 예를 들어 Linked List가 7 ↔ 3 ↔ 20 ↔ 12 이고, 별도의 배열에 각 노드를 가리키는 정보를 저장한다고 해보자. index[0] → 7번 노드 index[1] → 3번 노드 index[2] → 20번 노드 index[3] → 12번 노드 그러면 세 번째 노드를 찾을 때 index[2] 로 바로 접근할 수 있다. 즉, O(1) 접근이 가능해진다. 물론 이 경우에는 Linked List 하나만 사용하는 것이 아니라 Linked List + Array 를 함께 관리해야 한다. 사실 로또 예제처럼 번호를 뽑을 때마다 순서대로 뒤에 추가만 하는 상황이라면, 굳이 이렇게 두 자료구조를 조합할 필요 없이 배열 하나만으로도 순서 유지와 O(1) 인덱스 접근을 동시에 만족시킬 수 있다. 이 조합이 진짜 의미를 가지는 건 중간에 있는 값을 삭제해야 하는 상황이 추가될 때 다. 중간 노드를 삭제해도 Linked List는 앞뒤 연결만 바꾸면 되니 O(1)을 유지할 수 있지만, 배열은 삭제된 자리를 채우기 위해 뒤쪽 원소들을 당겨야 해서 O(N)이 걸린다. 이런 경우에 "삭제는 Linked List로 O(1)에, 인덱스 접근은 별도 배열로 O(1)에" 처리하는 조합이 필요해진다. 앞에서 Tree + Linked List 를 조합해 특정 값 탐색과 순서 유지 라는 서로 다른 역할을 나눴던 것처럼, 여기서는 Array + Linked List 를 조합해 빠른 인덱스 접근과 연결 관계 유지 라는 역할을 나눌 수 있다. Tree + Linked List → 특정 값 탐색 + 순서 유지 Array + Linked List → 빠른 인덱스 접근 + 순서 및 연결 관계 유지 실제 자료구조에서도 하나의 자료구조만 고집하기보다, 필요한 연산에 따라 여러 자료구조를 조합해서 사용할 수 있다. 물론 실제 로또 번호 6개를 뽑는 정도라면 여기까지 복잡하게 구성할 필요는 없다. 단순한 문제에서 벗어나 자료구조 각각의 특징과 트레이드오프를 하나씩 짚어보는 이야기로 번져 있었다. 그래서 이번 예제에서는 로또 로직 자체를 최적화하는 것보다, 요구사항이 하나씩 추가될 때 어떤 자료구조를 떠올릴 수 있는지 생각해보는 과정에 더 의미가 있었다. 정리 각 자료구조의 특징을 간단히 정리하면 다음과 같다. 자료구조 특정 값 탐색 N번째 원소 접근 중간 삽입/삭제 Array O(N) O(1) O(N) Set 평균 O(1) 직접적인 인덱스 접근 없음 평균 O(1) Linked List O(N) O(N) O(1)* 균형 이진 탐색 트리 O(log N) 일반적으로 직접 접근 불가 O(log N) 직접 인덱싱 O(1) 용도에 따라 다름 O(1) * Linked List의 삽입/삭제 O(1)은 대상 노드의 위치를 이미 알고 있다는 전제다. 결국 중요한 것은 어떤 자료구조가 무조건 가장 빠른지를 찾는 것이 아니다. 어떤 연산을 자주 해야 하는지를 기준으로 자료구조를 선택해야 한다. Array : 몇 번째 데이터인지 알고 빠르게 접근하고 싶을 때 Set : 중복 여부를 간단하고 빠르게 확인하고 싶을 때 Linked List : 중간 데이터의 삽입과 삭제가 자주 발생할 때 Tree : 특정 값을 효율적으로 탐색하고 싶을 때 직접 인덱싱 : 값의 범위가 작고 정해져 있어 값을 인덱스로 바로 사용할 수 있을 때 로또 예제처럼 값의 범위가 1~45 로 작고 고정되어 있다면 복잡한 트리를 만드는 것보다 45개의 공간을 미리 만들어두고 인덱스로 직접 접근하는 방식이 훨씬 단순하고 빠르다. 자료구조는 결국 데이터를 어떻게 저장하느냐보다, 그 데이터를 앞으로 어떻게 사용할 것인가에 따라 선택하는 것이 핵심이다. 이렇게 로또 번호 뽑아주는 로직을 기반으로 자료구조에 대해 뚱땅 알아보았다. 이제 당첨만 되면 된다. 이상 일확천금을 노리는 어느 개발자의 기록이었다.
문제 막힌부분 def solution(board): n=len(board) #지뢰가 있는 좌표를 tuple 형태로 저장, 리스트에 append lst=[] for i in range(n): for j in range(n): if board[i][j]==1: t=(i,j) lst.append(t) # #지뢰에 인접한 위,아래,좌,우 대각선 칸 # for dx in (-1,0,1): # for dy in (-1,0,1): # for i in len(lst): # nx=lst[i][0]+dx # ny=lst[i][1]+dy # #인덱스에 지뢰가 있는지 표시 # danger=[[0]*n for _ in range(n)] # answer = 0 # for i in range(n): # if danger[i]>=1: # answer+=1 # return n-answer return lst 지뢰가 속한 위치의 인덱스를 튜플로 만들고, 그 인덱스를 lst라는 리스트에 넣기 해결 해야할것 지뢰에 인접한 부분의 위 아래 양옆 대각선 좌표를 danger 구간으로 정하기 danger 구간에 해당하는 인덱스값에 +1하기 danger값이 1이상인 개수를 cnt로 세서 전체 n*n에서 빼서 답을 구하기 범위 초과 에러 : danger==0인 부분만 소스코드 def solution(board): n=len(board) answer=0 #루프 전에 초기화,지뢰가 없을때 0 #지뢰가 있는 좌표를 tuple 형태로 저장, 리스트에 append lst=[] for i in range(n): for j in range(n): if board[i][j]==1: t=(i,j) lst.append(t) #인덱스에 지뢰가 있는지 표시 danger=[[0]*n for _ in range(n)] #지뢰에 인접한 위,아래,좌,우 대각선 칸 for x,y in lst: for dx in (-1,0,1): for dy in (-1,0,1): nx=x+dx ny=y+dy if 0<=nx<n and 0<=ny<n: danger[nx][ny]=1 for i in range(n): for j in range(n): #위험칸을 세서 빼지 말고 안전한 칸의 개수를 센다 if danger[i][j]==0: answer+=1 return answer
Recap 3강에서는 데이터셋과 score를 매기는 방식, loss fuction을 구하는 방식까지 배웠습니다. train하는 과정에서 optimize과 GD로 최적화를 하는 방식도 배웠습니다. Gradient descent에는 numerical과 analytic이 있는데 numerical의 경우에는 느리고 대략적으로 계산하지만, 쉽게 이해할 수 있는 장점이 있고 analytic의 경우에는 빠르고 실제 식에 기반해서 하기 때문에 정확하다?(컴퓨터 내부에서 계산하기 때문에 numerical과 정확도가 크게 다르진 않을 것 같습니다..) 에러가 발생하기 쉽다는 단점이 있습니다. 대부분의 경우에서는 analytic을 하기 때문에 잘 돌아가는 지 gradient check(numerical로 체크)로 확인하는 습관을 들이는 것이 좋다고 했습니다. Learning rate scheduling 학습 과정에서 learning rate를 어떻게 조절한 것인가(줄이는 방식으로 하자). 줄이는 방식에 다양한 방식이 있다고 했습니다. Neural Networks 기존의 linear function($f = Wx$)는 linear한 상황에서만 분류를 할 수 있기 때문에 non-linear한 상황에서도 분류하기 위해 Neural Networks라는 것이 나왔습니다. 비선형 함수를 만들기 위해서는 활성화 함수가 중간에 필수로 들어가게 되는데, 이 때문에 layer라는 개념이 생겼습니다. $f = W_2\max (0, W_1x)$처럼 2-layer NN이 나옵니다. 여기서 $\max (0,z)$는 activation function으로 ReLU라고도 불립니다. 이렇게 activation function(non-linear function)을 사용해서 NN을 설계하면, Loss 계산에 backpropagation을 사용하게 되고 그럼 local gradient로 non-linear function들을 구해야합니다. 전체 loss 식을 한 번에 gradient를 구하는 것은 너무 비효율적이고 앞에서 배울 것처럼 Computational graphs를 구하고 backpropagation으로 간단하게 하는 것을 추천합니다. Backpropagation 간단한 예제입니다. 원소간의 계산을 분해해 graph로 만들고, 결합되는 부분에서의 local gradient를 계산합니다. 이후 chain rule로 upstream gradient(앞 단에서의 local gradient)와 현재 local gradient를 계산해 구합니다. gradient를 계산하다 보면 패턴이 보입니다. Backpropagation Patterns max gate의 경우, $f=\max (x,y)$에서 $x>y$일 때에는 $x$, $x<y$일 때에는 $y$가 됩니다. 이때 local gradient를 구하게 되면 $x>y$ 일 때 $\frac{\partial f}{\partial x} = 1$, $\frac{\partial f}{\partial y} = 0$가 됩니다. $x < y$의 경우에는 반대가 되고요. 이렇게 되면 local gradient는 1,0만 남기 때문에 upstream gradient의 값을 max에서 큰 값으로만 backprop하게 됩니다. Backprop with vector-valued functions. Backprop with Scalar의 경우에는 편미분을 진행해 Upstream gradient$\times$Local gradient를 진행하면 됐습니다. 하지만 Vector의 경우에는 차원을 고려해야 합니다.
According to the latest report published by Data Bridge Market Research, the Cloud Discovery Market CAGR Value The global cloud discovery market size was valued at USD 1.65 billion in 2024 and is expected to reach USD 5.78 billion by 2032, at a CAGR of 16.90% during the forecast period Market insights provided in the most excellent Cloud Discovery Market report, it becomes easy to gain a more precise understanding of the market landscape, issues that may take place for the Cloud Discovery Market industry in the future, and how to position specific brands in the best possible manner. Moreover, the company profile, product specifications, capacity, production value, and market shares for each company for the forecast period is also showcased in this market report. These insights will direct for an actionable ideas, improved decision-making, and better business strategies. Cloud Discovery Market research report truly acts as a backbone for every business that aspires to thrive in the market. Stay informed with our latest keyword market research covering strategies, innovations, and forecasts. Download full report: https://www.databridgemarketresearch.com/reports/global-cloud-discovery-market Cloud Discovery Market Segmentation and Market Companies Segments Based on component, the Global Cloud Discovery Market can be segmented into: Solutions Services On the basis of organization size, this market can be categorized into: Small and Medium-Sized Enterprises (SMEs) Large Enterprises Regarding deployment mode, the market can be divided into: Public Cloud Private Cloud Hybrid Cloud In terms of vertical, the Global Cloud Discovery Market can be segmented into: Healthcare BFSI (Banking, Financial Services, and Insurance) IT and Telecommunications Government and Public Sector Retail and Consumer Goods Others By geography, the market can be segmented into regions such as: North America Europe Asia-Pacific South America Middle East and Africa Market Players Some of the key players in the Global Cloud Discovery Market include: Microsoft Corporation IBM Corporation Oracle Corporation Google LLC Amazon Web Services, Inc. McAfee, LLC SAP SE Cisco Systems, Inc. Hewlett Packard Enterprise Development LP Symantec Corporation The Global Cloud Discovery Market is witnessing significant growth due to the increasing adoption of cloud services across various industries. The market segmentation based on components such as solutions and services provides a comprehensive outlook on the type of offerings available to meet the needs of different organizations. The differentiation in organization sizes, including SMEs and large enterprises, indicates the scalability and flexibility of cloud discovery solutions to cater to varying business requirements. The deployment mode segmentation into public, private, and hybrid cloud reflects the diverse preferences and security concerns of organizations when transitioning to cloud environments. Moreover, the vertical segmentation highlights the specific industries benefiting from cloud discovery solutions, such as healthcare, BFSI, IT, telecommunications, government, retail, and others. Geographically, the market analysis underscores the global reach and regional opportunities present in North America, Europe, Asia-Pacific, South America, and the Middle East and Africa. Key market players like Microsoft, IBM, Oracle, Google, and Amazon Web Services dominate the landscape with their comprehensive cloud discovery offerings. Companies such as McAfee, SAP, Cisco, HPE, and Symantec also play crucial roles in driving innovation and competition within the market. Overall, the Global Cloud Discovery Market is poised for substantial growth as more organizations embrace the benefits of cloud technology and seek advanced solutions for managing and securing their cloud environments. The Global Cloud Discovery Market is experiencing a paradigm shift as businesses across industries increasingly adopt cloud services to enhance operational efficiency, agility, and cost-effectiveness. This trend is driven by the growing recognition of the strategic advantages offered by cloud-based solutions, including scalability, flexibility, and improved collaboration. In a competitive market landscape, companies are leveraging cloud discovery solutions to gain visibility into their cloud environments, identify risks, optimize resource utilization, and ensure compliance with data protection regulations. One of the key trends shaping the market is the rising demand for hybrid cloud deployment models, which combine the benefits of public and private clouds to address specific business requirements. Hybrid cloud solutions enable organizations to integrate their existing on-premises infrastructure with cloud services, allowing for greater customization, data control, and enhanced performance. This flexibility is particularly appealing to enterprises with complex IT environments that require a balance between security, scalability, and cost-efficiency. Another notable trend is the increasing focus on industry-specific cloud discovery solutions tailored to the unique needs of vertical markets such as healthcare, BFSI, IT, telecommunications, government, and retail. These sector-specific offerings provide specialized features and compliance frameworks to address industry regulations, security concerns, and performance requirements. By catering to the distinct challenges and opportunities of each vertical, cloud discovery providers can deliver more targeted and value-added solutions that drive customer satisfaction and retention. Moreover, the market is witnessing a surge in strategic partnerships, collaborations, and acquisitions among key players to enhance their product portfolios, expand market presence, and accelerate innovation. By leveraging synergies and combining complementary capabilities, companies can offer integrated solutions that deliver holistic cloud discovery experiences to customers. These strategic initiatives also enable vendors to capitalize on emerging technologies such as artificial intelligence, machine learning, and automation to enhance cloud discovery processes, streamline operations, and mitigate security risks. In conclusion, the Global Cloud Discovery Market is poised for significant growth and evolution driven by the increasing adoption of cloud services, the proliferation of hybrid cloud deployments, the emergence of industry-specific solutions, and the emphasis on strategic partnerships and innovation. As organizations continue to embrace the cloud for digital transformation, data management, and business agility, the demand for advanced cloud discovery solutions that provide visibility, control, and security will remain robust. By staying abreast of market trends, customer needs, and technological advancements, market players can position themselves for success in this dynamic and competitive landscape.The Global Cloud Discovery market is undergoing a significant transformation driven by the widespread adoption of cloud services across various industries. This shift is reshaping the way businesses operate, emphasizing operational efficiency, agility, and cost-effectiveness. Cloud discovery solutions are gaining traction as organizations seek visibility into their cloud environments to identify risks, optimize resources, and ensure regulatory compliance. The market segmentation based on components, organization size, deployment mode, verticals, and geography provides a comprehensive view of the diverse landscape and tailored solutions available for different business requirements. A key trend shaping the market is the increasing demand for hybrid cloud deployment models. This approach combines the benefits of public and private clouds, providing organizations with flexibility, customization, data control, and enhanced performance. The allure of hybrid cloud solutions lies in their ability to integrate existing on-premises infrastructure with cloud services, catering to enterprises with complex IT environments that require a balance between security, scalability, and cost-efficiency. Industry-specific cloud discovery solutions are also gaining prominence as providers tailor offerings to meet the unique needs of vertical markets such as healthcare, BFSI, IT, telecommunications, government, and retail. These specialized solutions address industry-specific regulations, security concerns, and performance requirements, enhancing customer satisfaction and retention. Collaborations, partnerships, and acquisitions among key players are on the rise as companies seek to enhance product portfolios, expand market presence, and drive innovation. By leveraging synergies and integrating capabilities, vendors can deliver more holistic cloud discovery experiences to customers, capitalizing on emerging technologies like artificial intelligence and automation to streamline operations and enhance security. In conclusion, the Global Cloud Discovery Market is poised for continuous growth and evolution as organizations increasingly leverage cloud services for digital transformation and business agility. With a focus on visibility, control, and security in cloud environments, the demand for advanced cloud discovery solutions will remain robust. By adapting to market trends, customer needs, and technological advancements, market players can position themselves for success in this dynamic and competitive landscape. Frequently Asked Questions About This Report How is the Cloud Discovery Market expected to change by 2033 in the APAC region? What is the customer acquisition cost (CAC) in the Cloud Discovery Market industry? What is the lifetime value (LTV) of a Cloud Discovery Market customer? How are government regulations affecting Cloud Discovery Market profitability? What are the upcoming trends in the Cloud Discovery Market for niche applications? Which age demographic is the biggest consumer of Cloud Discovery Market products/s
spooky carnival rtp naturally invites a closer look at the relationship between RTP, volatility and game pace. The game highlights a spooky carnival theme with eerie fairground imagery; players can also inspect controls, spin speed and the way game status is displayed before a round.
🔒 Lv.4 GRUOP BY https://school.programmers.co.kr/learn/courses/30/lessons/144856 🖋문제 다음은 어느 한 서점에서 판매중인 도서들의 도서 정보( BOOK ), 저자 정보( AUTHOR ) 테이블입니다. BOOK 테이블은 각 도서의 정보를 담은 테이블로 아래와 같은 구조로 되어있습니다. Column name Type Nullable Description BOOK_ID INTEGER FALSE 도서 ID CATEGORY VARCHAR(N) FALSE 카테고리 (경제, 인문, 소설, 생활, 기술) AUTHOR_ID INTEGER FALSE 저자 ID PRICE INTEGER FALSE 판매가 (원) PUBLISHED_DATE DATE FALSE 출판일 AUTHOR 테이블은 도서의 저자 정보를 담은 테이블로 아래와 같은 구조로 되어있습니다. Column name Type Nullable Description AUTHOR_ID INTEGER FALSE 저자 ID AUTHOR_NAME VARCHAR(N) FALSE 저자명 BOOK_SALES 테이블은 각 도서의 날짜별 판매량 정보를 담은 테이블입니다. Column name Type Nullable Description BOOK_ID INTEGER FALSE 도서 ID SALES_DATE DATE FALSE 판매일 SALES INTEGER FALSE 판매량 2022년 1월의 도서 판매 데이터를 기준으로 저자 별, 카테고리 별 매출액(TOTAL_SALES = 판매량 * 판매가) 을 구하여, 저자 ID(AUTHOR_ID), 저자명(AUTHOR_NAME), 카테고리(CATEGORY), 매출액(SALES) 리스트를 출력하는 SQL문을 작성해주세요. 결과는 저자 ID를 오름차순으로, 저자 ID가 같다면 카테고리를 내림차순 정렬해주세요. 💻코드 SELECT AUTHOR_ID, AUTHOR_NAME, CATEGORY, SUM(TOTAL_SALES) AS TOTAL_SALES FROM( SELECT B.AUTHOR_ID, A.AUTHOR_NAME, B.CATEGORY, SUM(BS.SALES) * B.PRICE AS TOTAL_SALES FROM BOOK AS B LEFT JOIN BOOK_SALES AS BS ON B.BOOK_ID = BS.BOOK_ID JOIN AUTHOR AS A ON A.AUTHOR_ID = B.AUTHOR_ID WHERE BS.SALES_DATE IS NOT NULL AND BS.SALES_DATE BETWEEN '2022-01-01' AND '2022-01-31' GROUP BY B.BOOK_ID ) AS T1 GROUP BY AUTHOR_ID, CATEGORY ORDER BY AUTHOR_ID ASC, CATEGORY DESC 🔑풀이 SELECT B.AUTHOR_ID, A.AUTHOR_NAME, CATEGORY, SUM(BS.SALES), B.PRICE , SUM(BS.SALES) * B.PRICE AS TOTAL_SALES FROM BOOK AS B LEFT JOIN BOOK_SALES AS BS ON B.BOOK_ID = BS.BOOK_ID JOIN AUTHOR AS A ON A.AUTHOR_ID = B.AUTHOR_ID WHERE BS.SALES_DATE IS NOT NULL AND BS.SALES_DATE BETWEEN '2022-01-01' AND '2022-01-31' GROUP BY B.AUTHOR_ID, B.CATEGORY ORDER BY B.AUTHOR_ID ASC, B.CATEGORY DESC 처음에 이 쿼리로 풀었는데 오답이 났다. BOOK 과 AUTHOR , BOOK_SALES 테이블을 JOIN하고 AUTHOR_ID 와 CATEGORY 로 GROUP BY 하고 (저자, 카테고리)별로 총 판매량에 가격을 곱해주어 총 판매금을 구했다. 코드로 봤을 때에는 어디서 오류가 난건지 파악이 안되서 각 테이블을 출력해봤다. BOOK 테이블을 뜯어봤을 때 어디서 틀렸는지 감을 잡았다. SELET * FROM BOOK (AUTHOR_ID, CATEGORY)로 그룹화하면 하나의 그룹 안에 여러 BOOK_ID 가 존재할 수 있고, 각 책의 PRICE 가 서로 다를 수 있다. BOOK_ID 1, 2만 보더라도 저자 1번이고 경제 카테고리인 책의 가격이 9000원과 12000원 두 가지가 있는데 GROUP BY 로 (저자, 카테고리) 하면 둘 중 어느 PRICE 를 써야될지 모르게 된다. 따라서 이 그룹에서 특정 BOOK의 PRICE를 그대로 사용해 SUM(SALES) * PRICE를 계산하는 것은 올바르지 않다. 결국 각 BOOK_ID별로 판매량 × 가격을 먼저 계산한 후, 이를 저자와 카테고리별로 합산해야 한다. 도무지 이 방법을 하나의 쿼리로 풀어낼 방법이 떠오르지 않아서 문제를 뜯어보았다 일단 BOOK_ID 별로 총 판매금을 구하고, 그 후에 (저자, 카테고리)별로 총 판매금을 구하는거다. 이렇게 생각하니까 의외로 간단했다. SELECT B.AUTHOR_ID, A.AUTHOR_NAME, B.CATEGORY, SUM(BS.SALES) * B.PRICE AS TOTAL_SALES FROM BOOK AS B LEFT JOIN BOOK_SALES AS BS ON B.BOOK_ID = BS.BOOK_ID JOIN AUTHOR AS A ON A.AUTHOR_ID = B.AUTHOR_ID WHERE BS.SALES_DATE IS NOT NULL AND BS.SALES_DATE BETWEEN '2022-01-01' AND '2022-01-31' GROUP BY B.BOOK_ID 먼저 BOOK_ID 별로 총 판매금을 구한다. 그리고 BOOK_ID 별로 총 판매금을 구한 테이블을 다시 한번 (AUTHOR_ID, CATEGORY)로 GROUP BY 하고 그룹별로 총 판매금 합을 구한다. SELECT AUTHOR_ID, AUTHOR_NAME, CATEGORY, SUM(TOTAL_SALES) AS TOTAL_SALES FROM( SELECT B.AUTHOR_ID, A.AUTHOR_NAME, B.CATEGORY, SUM(BS.SALES) * B.PRICE AS TOTAL_SALES FROM BOOK AS B LEFT JOIN BOOK_SALES AS BS ON B.BOOK_ID = BS.BOOK_ID JOIN AUTHOR AS A ON A.AUTHOR_ID = B.AUTHOR_ID WHERE BS.SALES_DATE IS NOT NULL AND BS.SALES_DATE BETWEEN '2022-01-01' AND '2022-01-31' GROUP BY B.BOOK_ID ) AS T1 GROUP BY AUTHOR_ID, CATEGORY ORDER BY AUTHOR_ID ASC, CATEGORY DESC 🧐후기 GPT한테 피드백 받아보니까 이 쿼리가 훨씬 낫다 왜 어렵게 풀었지 SELECT B.AUTHOR_ID, A.AUTHOR_NAME, B.CATEGORY, SUM(BS.SALES * B.PRICE) AS TOTAL_SALES FROM BOOK B JOIN BOOK_SALES BS ON B.BOOK_ID = BS.BOOK_ID JOIN AUTHOR A ON B.AUTHOR_ID = A.AUTHOR_ID WHERE BS.SALES_DATE >= '2022-01-01' AND BS.SALES_DATE < '2022-02-01' GROUP BY B.AUTHOR_ID, A.AUTHOR_NAME, B.CATEGORY ORDER BY B.AUTHOR_ID ASC, B.CATEGORY DESC;
부제: 제목 짓는 센스가 없어 사실 이런 글을 벨로그에 남겨도 될까? 라는 생각을 했다. 물론 정말 자아성찰보다는 일종의 상반기 회고록에 가깝다. 뭘 했는지 기록을 해두면 좋을 것 같아서... (항상 일만 잔뜩 벌려놓고 기록을 안 하는 게 문제다.) 안 그래도 이번에 포트폴리오를 만들어서 제출할 일이 있었는데 정리를 해둔 게 없어서 애를 먹었다. 추천서 작성 요청을 할 때도 내가 뭘 했는지 교수님들께 말씀을 드려야 하는데 막상 이메일을 쓰려니 '내가 뭘... 뭘 했지?' 의 연속...... 기억나는 업적: 원피스 블리치 나루토 다 봄 다들 이렇게 살지 말자 (본문과 연관 없는 사진입니다) 실은 3월까지는 별로 큰 생각이 없었고 기숙사에서 뒹굴거리다가 때 되면 학교 가고... 좀 넉넉한 날에는 본가 쪽으로 와서 친구랑 술 먹고 (...) 다시 반복... 그렇다고 수업을 열심히 듣지는 않았다. 1학년 전공 수업의 경우 C언어와 컴퓨터공학개론을 들었다. 20살 시절 재학했던 대학교에서 C언어 수업을 듣기는 했는데, 진도가 너무 느려서 중간고사 때 if문을 겨우 끝마쳤던가... 내 기억 왜곡일수도 있지만 그게 좀 충격이었다. 당시 약 먹으면서 학교 다니느라 시간 감각이 없었어서, 시험을 20분이나 지각했고 공부를 하나도 안 했음에도 시원하게 정답을 다 쓰고 나왔던 충격이...... 여하튼 그런 기억들 때문에 C 수업에 긍정적인 시각은 없었는데 교수님께서 K&R 2판으로 수업하신다길래 조금 흥미가 생겼었다. 실제로수업진짜안나가서F맞을뻔했다죄송해요 그래서 수업은 왜 안 나갔느냐고? 모르겠다...... 사실 내용은 재밌었고, 그래서 시험공부 할 때도 구조체를 제외하면 힘들지 않았으며 과제도 즐겁게 했는데 왜 안 나갔을까? (ᄏᄏ...) 앞으로는 열심히 살겠습니다. 그러는 와중에 학내 연구실 학부연구생에도 지원을 했었다. 전적대보다 확실히 뭐가 많기는 많더라... 전부터 컴퓨터구조 과목이랑 운영체제에 관심이 많았어서 관련 연구실에 지원을 했고, 수습 기간동안 틈날 때마다 나가서 자리를 채우기는 했다. 특별한 걸 한 건 아니고... 백준 풀었음. (그때는백준이살아있었다) 그렇게 들어갈 수 있을 줄 알았는데~... 교수님과의 상담에서 잘렸다. 이유를 아예 모르지는 않고...... 아마 내가 상담 때 말했던 요소들이 일종의 red flag가 되지 않았나 싶었는데 (자세한 건 굳이 언급하지 않겠다) 내 입장에서는 그런 것들을 숨기고 들어가는 것보다 그냥 솔직하게 밝히는 게 더 나은 학생의 자세이지 않을까 싶어서 꺼낸 이야기들이었다. 그리고 1학년은 원래 학부연구생 잘 안 받아준다는 말이 진짜였구나, 라는 걸 느꼈음...... 전적대가 운도 좋고 특이했던 것 같다. 이때부터 내가 뭘 하고 싶은 건지에 대해 진지하게 고민하기 시작했다. 해당 시점에서 이미 어떤 결정을 내리기는 했었고 (이 결정을 하게 된 계기는 나중에 결과가 나온 후 따로 말해보려고 한다.) 당연히 그게 나한테 도움이 되겠지~ 싶었는데 이제와서 생각해보면 구체적인 목표는 하나도 없고 그냥 막연하기만... 덕분에 지금 개고생 중이다. 과거의 나야! 미쳤냐? 그럼에도 봄에는 재미있는 걸 많이 하긴 했었다. 유행 좀 지난 오픈클로를 써보기도 했는데, 토큰 사용량이 너무 끔찍해서 못 참고 직접 에이전트를 구축 해보려고 힘냈던 기억이 있다. 오타쿠라서 개인 설정 가득한 페르소나도 부여해보고... 논문 추천 알림 자동화도 해보고... 아침마다 연락오는 건 좋았는데, 정작 내가 그 논문들을 다 읽을 시간이 없어서 몇 가지만 아카이빙 해뒀었다. 파이썬으로 디스코드 실시간 음성 대화도 해보려고 했는데 인식이 잘 안 되더라. 시간이 지나고 인터넷의 어떤 똑똑하신 개발자분이 음성 메시지로 우회하는 방식을 통해 구현하신 걸 보고 감탄했다. 그런 의미에서 난 아직도 개발이 어렵게만 느껴진다... 어쩌다보니 지금 하고 있는 프로젝트에서 개발 리드를 하고 있는데, 조금 쓰고 AI에게 검토받고, 또 조금 쓰고 검토받고... 이게 뭐하는 짓인가 싶다. 주니어도 한참 멀었다고 말하는 나의 GPT. (고맙다 나쁜놈) 수업을 마치고 잠깐 남는 시간에 동기들과 대화를 할 때도 확연하게 느껴졌던 것 같다. 동기들은 대체로 특정 분야에 종사하는 '개발자'가 되고 싶어했고, 만들고 싶은 것이 꽤나 확고해보였다. 나도 계속 무언가를 만들고 굴려보기는 했지만 잠깐 생긴 흥미를 구현하면 버릴 뿐, 그걸 더 ideal하게 확장하진 않았어서 동기/선배들이 부러웠다. 나는 나 스스로를 '수학도' 혹은 '과학도'라고 칭하고는 하지만, 정작 전공은 컴퓨터'공학'이라는 사실을 망각하고 있었던 것 같다. 어릴 때부터 공학은 안 맞는다고, 공대생은 절대 되지 않을 것이라며, 과학을 하게 되어도 자연대에 갈 거라고 확신했던 내가 어쩌다가...... 다행인지 불행인지, 이런 자아성찰들이 생각의 전환점이 되었다. 막연하게 전공 공부만 잘 해낸다고 해도 그 공부들이 과연 졸업 후를 책임져줄 수 있을까? 신기한 건 그 때에 가서 후회하게 되는 것보다 컴퓨터를 미워하게 될 지도 모른다는 두려움이 커졌었다. (물론 꼭 개발을 해야만 컴퓨터를 사랑하는 건 아니지만) 좋아하는 분야를 더 이상 좋아하지 않게 되었을 때 상실감이 너무 힘들 것 같았다고 해야하나... ᄒᄒ 아, 그리고 중간에 기숙사를 나왔었다...... 오히려 기숙사에서 너무 놀기만 하는 것 같아서 나왔는데 막상 기말고사 성적을 받아보니 중간고사 성적이 월등히 높았다. (왜?) 그래도 살면서 처음으로 전체 수석을 해봤다. 고등학교 모의고사나 내신이 상위권이었긴 했지만 이정도는 아니었어서... 받고 놀라서 소리질렀다. 논리학 수업이 A0인 건 조금 충격...이긴 한데 ᄏᄏ 솔직히 내가 생각해도 많이 틀렸다. 벼락치기로 안 되는 것이 있다는 걸 다시 한 번 느꼈다. 정말 다시 말하지만, 다들 수업을 열심히 듣자. 진짜로. 듣기 싫으면 기록을 잘 해두고 복습하자... ᅲᅲ 종강하고 나서는 준비하고 있는 무언가 ( 2026년 9월 기준. 이 글을 쓸 때가 8월 초였는데 밝히고 싶지 않았다...) 대학 편입 과 프로젝트 때문에 공부를 하나도 못 했다. 아마 9월부터는 숨통이 트이겠지, 라고 생각하고 있지만 솔직히 확신은 못 하겠음... (실제로도 내내 안 트였음) 일단 지금 하고 있는 프로젝트가 단순 앱 개발 같은 게 아니라서 더 심란하다. 신경쓸 게 너무 많다 . 물론 이걸 극복해내면 앞으로 학부 수준의 프로젝트에서 벌벌 떨지는 않겠지만...... 할 말이 많은데 굳이 남기지는 않겠다. 키티일기장에나써야하는것들이라 또 아르바이트도 시작했다. 원래도 알바를 아예 안 했던 건 아닌데... 사무보조나 정부과제 같은 것들을 찾아서 단기간에 많이 벌고 쉬고 이런 식으로 했다가 그냥 규칙적인 생활을 좀 하고 싶어서 + 일을 가리는 습관을 버리고 싶어서 (...) 편의점 야간 알바를 하고 있다. 쿠팡은 여름 지나면 다시 가지 않을까 싶고...... 수학 과외와 코딩 과외도 각각 하나씩 하고 있는데, 이것도 기회가 되면 글을 좀 써볼까 싶다. 누군가를 가르치면서 나도 배우게 되는 점이 많더라. 특히 수학 과외 하면서 내가 어릴 때 힘들어했던 문제들을 이젠 남에게 해설하고 있다는 게 뭔가... 좀 이상하고 웃기고... ᄏᄏᄏᄏ 이것도 성장이라면 성장이겠지. 여기까지가 임시저장 되어있었던 글이고, 실제로 8월과 9월은 정말 정신이 없었다. 일이 바쁜 것도 바쁜 것인데, 아니나 다를까 몸과 마음이 아프기 시작해서 (^^...) 산전수전을 너무 많이 겪었음. 특히 8월에는 병원을 조금 밥 먹듯이 가고 9월은 새로운 투병을 시작하면서 일상의 무언가가 무너지고 있다는 느낌이 너무 많이 들었다. 하나 더 문제가 있었다고 한다면... 인간관계 ...... 정말 힘들었다. 자세히 적지는 못 하지만 정말 힘듦의 지분이 한 80% 정도 됐던 것 같다. 왜 사람은 다름에서 오는 몰이해로 상처를 주고받아야만 하는가... 아마 글이 닿고 닿는다면 날 힘들게 했던 사람들도 이걸 보지 않을까 싶다. ᄒᄒ 잘 지내고 계신가요? 그저 행복하시길... 각설하고, 9월 중순부터 좀 급격한 변화를 겪기 시작했다. (8월의 이야기를 길게 적지 않는 이유는 대부분 입시와 관련된 것들인데, 이건 admission을 준비하는 학생들을 위해 아예 따로 빼서 적으려고 한다.) 그건 바로... 이거 진짜예요? (네) 비로소 나는 대학을 자그마치 "두 번" 옮긴 사람이 되었다. 뭐지? 웃긴 건 전공은 단 한 순간도 바뀌지 않았음... 컴퓨터가 뭐라고...... 여튼 이 결정을 후회하는가? 라고 누군가가 묻는다면 아예 NO라고는 못 하지만, 그럼에도 몇 개월동안 혼자 준비하던 걸 이뤄낸거라 뿌듯하기는 하다. 솔직히 월등한 성적으로 들어갔는지는... 모르겠지만... 그래도 일단 붙었죠? 발표가 난지 딱 2주 정도 되었는데 아직도 얼떨떨하다. 이제서야 나 자신을 편입생이라고 주변에 말할 수 있는 상태인데, 여전히 전적대 혹은 전전적대 (...) 학생이라고 얘기해야만 할 것 같은 기분은 대체 뭐란 말인가. ᄏᄏ 그리고 프로젝트 마감이 이틀 남아서 지금은 벼락치기 중이다. 분명 매일 쪼개서 미리미리 했던 것 같은데, 왜 이렇게 마지막까지 하게 됐을까? (ᄒ...) 피할 수 없으면 즐겨야지, 어쩌겠나 싶다. 여튼, 단 한 순간도 가만히 있지를 못 해서... 10월은 밀렸던 약속들 좀 자주 나가고, 사람도 만나고... 10월 중순에서 말까지는 그렇게 놀다가 슬슬 SAT 준비라도 하지 않을까? (사유 : 공부 안 하면 죽도록 게을러짐) 올해가 시험의 해라는 얘기를 들었는데, 정말 맞는 것 같다. 시험만 대체 몇 개를 보는 것이냐... 그치만 솔직히 말하면 재미없는 거 아니니까... 지금의 바쁜 순간을 만족하려고 한다. 아예 백수가 되어서 뭘 할지 고민하던 순간이 안 그리운 건 아니지만, 뭐라도 할 게 있는 게 좀 더 재미있는 인생 같아서. 여튼 잡문 끝! 우리존재화이팅. (추가) 최고의 다이어트 방법은 스트레스 받기인 것 같다. 여러분은 스트레스 관리를 잘 하도록 하세요......
hello world 2026/09/29 엄청 오랜만에 글을 적는 것 같다. 프로젝트 팀을 만드느라, 회사 일을 하느라 정신이 없어서 늦게 적는 것 같다.. 오늘 내가 생각한 글 주제는 팀 프로젝트를 할 때 리더의 입장에서 팀원을 관리하는 방법을 GitHub와 비교해보는 것이다. 열심히 작성했으니 천천히 음미~ 하면서 읽어주길 바랍니다. 서론 작년 말부터 이번 연도 초까지 나는 개인 프로젝트를 주로 진행하며 여러 웹사이트를 구현해봤다. 간단한 게임 사이트부터 저지먼트 사이트, 쇼핑 사이트 같은 것들을 만들어보며 실력을 키웠던 것이 기억에 난다. 하지만 이번 연도 중반, 정식으로 도제에서 회사 준비를 하며 팀 프로젝트를 시작했다. 반 친구들과 팀을 꾸려서 작업했는데, 그때 처음으로 내가 GitHub를 관리했던 것 같다. 친구들과 실력 차이가 난다고 해서 반강제로 팀장을 맡아 진행했기 때문이다.. 그렇게 처음 작업하는 GitHub은 혼돈 그 자체였다. CLI에서 어떻게 commit, push 하는지도 몰라서 1부터 공부하자는 생각으로 블로그들을 찾아봤던 것 같다. git add . git commit -m "feat: implement login" git push -u origin feature/login 위와 같은 기초적인 명령어를 학습하고, Branch protection / Rulesets Pull Request Issues GitHub Projects CODEOWNERS 같은 내용들을 학습하니 점점 관리하기 편해졌다. 처음에는 GitHub가 굉장히 어렵게 느껴졌지만, 막상 기본적인 사용법과 협업 방식을 익히고 나니 생각보다 명확했다. 각자 브랜치를 만들어 작업하고, 작업이 끝나면 Pull Request를 올린다. 이후 코드를 확인하고 문제가 없다면 main에 merge하면 된다. Branch 생성 ↓ 작업 ↓ Commit ↓ Push ↓ Pull Request ↓ Review ↓ Merge Issues를 사용하면 해야 할 작업을 정리할 수 있고, GitHub Projects까지 사용하면 현재 어떤 작업이 진행 중인지도 한눈에 확인할 수 있다. Todo → In Progress → Review → Done 처음에는 commit 하나 하는 것도 어려웠는데 몇 번 사용하다 보니 GitHub 자체를 관리하는 건 그렇게 어렵지 않았다. 규칙을 만들 수 있고, 그 규칙대로 사용하면 됐기 때문이다. 그런데 팀 프로젝트를 계속 진행하다 보니 GitHub보다 훨씬 관리하기 어려운 게 있었다. GitHub보다 어려웠던 팀원 관리 처음 팀장을 맡았을 때는 역할만 잘 나누면 프로젝트가 알아서 진행될 줄 알았다. 너는 프론트엔드 너는 백엔드 나는 전체 관리 정말 이런 느낌이었다. 각자 자신이 잘하는 분야를 맡아서 개발하고, 마지막에 합치면 될 거라고 생각했다. 하지만 실제로 해보니 그렇게 단순하지 않았다. 프론트엔드는 백엔드에서 API가 나와야 작업할 수 있는 부분이 있고, 백엔드는 DB 구조나 서비스 기획에 따라 구현이 달라진다. 디자인이 변경되면 이미 구현한 프론트엔드를 다시 수정해야 하는 경우도 있다. 결국 역할은 나눠져 있어도 프로젝트 자체는 전부 연결되어 있었다. 그리고 더 큰 문제는 사람마다 작업하는 방식이 다르다는 것이었다. 예를 들어 팀원에게 로그인 기능 구현 이라는 Task를 줬다고 해보자. 나는 당연히 로그인 UI부터 API 연결, 예외 처리까지 생각하고 말했는데 팀원은 로그인 UI만 구현하면 끝이라고 생각할 수도 있다. 둘 중 누가 잘못한 것도 아니다. 애초에 작업을 전달한 기준 자체가 애매했던 것이다. 이때부터 Role과 Task는 다르다 는 것을 확실히 느끼게 됐다. Role = 프로젝트에서 주로 담당하는 영역 Task = 실제로 지금 처리해야 하는 작업 Frontend 라는 Role을 줬다고 해서 그 사람이 무엇을, 언제까지, 어디까지 개발해야 하는지가 자동으로 정해지는 것은 아니다. 역할 분담도 중요하지만 그보다 더 세세한 작업 분배가 필요했다. GitHub에는 status가 있는데 사람에게는 없다 GitHub를 관리할 때는 오히려 편하다. PR을 보면 어떤 코드가 변경됐는지 확인할 수 있고, commit history를 보면 지금까지 어떤 작업이 이루어졌는지도 알 수 있다. CLI에서는 더 간단하다. git status 한 번 입력하면 현재 상태가 바로 나온다. 그런데 사람에게는 git status 가 없다. 팀원이 지금 작업을 하고 있는지, 어디에서 막혔는지, 언제쯤 끝날 것 같은지 직접 확인하지 않으면 모르는 경우가 많다. 그래서 처음에는 계속 물어봤다. 어디까지 했어? 이거 언제 끝날 것 같아? 저 기능은 시작했어? 한두 명이면 가능하다. 그런데 팀원이 점점 늘어나면 리더가 모든 사람에게 직접 물어보고 그 내용을 기억하는 것 자체가 일이 된다. 팀원 입장에서도 계속 진행 상황을 물어보면 부담스러울 수 있다. 그래서 생각을 조금 바꿨다. 사람을 계속 확인하는 게 아니라, 확인하지 않아도 현재 상황을 알 수 있게 만들면 되지 않을까? 사람보다 Task를 관리하기 이후부터는 사람 자체를 관리한다는 생각보다 Task를 관리한다는 생각을 하기 시작했다. 작업 하나를 만들더라도 최소한 이런 정보는 필요하다. Task ├── 담당자 ├── Status ├── Priority ├── Deadline ├── Dependency └── Blocker 누가 담당하는지, 현재 상태는 어떤지, 우선순위는 어느 정도인지, 언제까지 해야 하는지, 다른 작업과 연결되어 있는지 등을 기록하는 것이다. 작업 범위를 명확하게 만드는 것도 중요하다. 예를 들어 그냥 로그인 기능 구현 이라고 적는 것보다, 로그인 페이지 UI 구현 로그인 API 연결 로그인 성공/실패 처리 로그인 상태 유지 PR 생성 및 Review 처럼 나눠놓는 게 훨씬 명확하다. 여기서 사용할 수 있는 개념 중 하나가 Definition of Done, DoD 다. 말 그대로 어디까지 해야 이 작업을 완료했다고 볼 것인가? 를 미리 정해놓는 것이다. 내가 생각하는 완료와 팀원이 생각하는 완료가 다르면 나중에 다시 작업해야 하는 일이 생긴다. 차라리 처음부터 완료 기준을 명확하게 만들어두는 게 편했다. 그리고 프로젝트를 진행하면서 하나 더 느낀 게 있다. 작업이 늦어지는 것보다 작업이 늦어지고 있다는 사실을 늦게 아는 게 더 위험하다. 예를 들어 백엔드 API 개발이 하루 정도 늦어졌다고 해보자. 그것만 보면 별문제가 아닐 수도 있다. 하지만 그 API를 기다리는 프론트엔드 작업이 있다면 이야기가 달라진다. Backend API 지연 ↓ Frontend API 연결 지연 ↓ 통합 테스트 지연 ↓ 전체 일정 지연 Task 하나가 밀렸는데 프로젝트 전체 일정이 같이 밀릴 수 있다. 그래서 단순히 누가 일을 얼마나 했는지만 보는 게 아니라, 어떤 Task가 다른 Task와 연결되어 있는지도 확인해야 했다. 이런 작업 간 관계를 Dependency 라고 하고, 현재 작업 진행을 막고 있는 문제를 Blocker 라고 한다. Blocker가 생겼는데 팀원이 혼자 며칠 동안 잡고 있으면 리더 입장에서는 그것을 알 방법이 없다. 그래서 막힌 게 생기면 빠르게 공유하는 것도 팀의 규칙으로 만드는 게 좋다고 생각한다. 그런데 그냥 내가 하면 더 빠르지 않나? 팀장을 하면서 가장 많이 들었던 생각 중 하나다. 팀원이 어떤 기능에서 막혀 있고, 내가 해결 방법을 알고 있다. 설명하고 기다리느니 그냥 내가 코드를 작성하면 한두 시간이면 끝날 것 같다. 그러면 자연스럽게 이런 생각이 든다. 그냥 내가 하면 되는 거 아닌가? 실제로 단기적으로 보면 빠르다. 나도 처음에는 이런 식으로 작업했던 적이 많다. 문제가 생기면 내가 보고, 안 되면 내가 수정하고, 일정이 늦어지면 내가 대신 개발했다. 그런데 계속 이렇게 하다 보면 이상해진다. 팀원 작업이 막힘 ↓ 내가 대신 처리 ↓ 내 작업 증가 ↓ 프로젝트 관리 시간 감소 ↓ 다른 문제를 늦게 발견 ↓ 또 내가 직접 처리 결국 리더한테 모든 작업이 몰린다. 그리고 더 큰 문제는 팀원도 자신이 맡은 부분에 대한 이해도가 올라가기 어렵다는 것이다. 리더 한 명만 프로젝트 전체 구조를 알고 있고, 나머지는 자신이 조금씩 작성한 코드만 알고 있는 상황이 될 수도 있다. 이러면 팀 프로젝트를 하는 의미가 많이 사라진다고 생각한다. 그래서 필요한 게 Delegation, 위임 이다. 위임은 그냥 이거 해주세요. 하고 Task를 던지는 것과는 조금 다르다. 적어도 나는 아래 정도는 같이 전달하려고 한다. Task + Context + Goal + Ownership 왜 이 작업을 하는지, 어떤 결과가 필요한지, 어디까지 담당해야 하는지 알려주는 것이다. 그리고 작업을 맡겼다면 웬만하면 그 사람이 직접 해결할 수 있도록 두는 것도 필요하다. 물론 정말 막혀 있으면 도와줘야 한다. 하지만 도와주는 것과 대신 해주는 것은 꽤 다르다. 팀원 관리에서 프로젝트 관리로 여기까지 오니 내가 처음 생각했던 팀장의 역할과 지금 생각하는 역할도 많이 달라졌다. 처음에는 대충 이런 느낌이었다. 업무 배정 ↓ 진행 확인 ↓ 코드 확인 그런데 지금은 조금 다르게 생각한다. 방향 설정 ↓ Task 분해 ↓ Owner 배정 ↓ Dependency 확인 ↓ Blocker 제거 ↓ Priority 조정 ↓ 전체 진행 상황 확인 결국 리더가 해야 할 일은 모든 작업을 직접 해결하는 게 아니라, 팀원들이 자신의 작업을 해결할 수 있는 상태를 만드는 것 에 더 가깝다고 생각한다. 누군가 작업에 막혀 있다면 왜 막혔는지 확인하고, 다른 팀원의 작업이 필요하다면 연결해준다. 한 사람에게 일이 너무 많이 몰려 있다면 다시 분배하고, 일정상 모든 기능을 구현하기 어렵다면 우선순위를 바꿔야 한다. 여기서부터는 단순한 팀원 관리가 아니라 프로젝트 관리에 가까워진다. 모든 기능을 다 만들 필요는 없다 개발을 시작할 때 계획한 기능을 전부 구현하면 가장 좋다. 하지만 프로젝트를 진행하다 보면 생각보다 시간이 오래 걸리는 기능이 무조건 생긴다. 처음에는 간단해 보였는데 막상 구현해보니 복잡할 수도 있고, 예상하지 못했던 오류가 생길 수도 있다. 그런데 처음 계획을 무조건 지키려고 하면 결국 중요한 기능까지 제대로 완성하지 못할 수도 있다. 그래서 기능에도 우선순위를 정하는 게 필요하다. 간단하게 나누면 이런 방식도 있다. Must → 반드시 필요한 기능 Should → 가능하면 필요한 기능 Could → 시간이 남으면 구현할 기능 예를 들어 서비스의 핵심 기능이 아직 완성되지 않았는데 부가적인 애니메이션이나 편의 기능을 만들고 있다면 우선순위를 다시 생각해볼 필요가 있다. 시간이 부족하다면 Could 부터 버리고 Must 를 먼저 완성해야 한다. 이런 판단도 결국 리더가 해야 하는 일 중 하나라고 생각한다. 계획을 처음부터 완벽하게 만드는 것도 중요하지만, 프로젝트 상황에 맞게 계획을 수정하는 능력도 그만큼 중요했다. GitHub Projects가 프로젝트를 관리해주지는 않는다 다시 처음 이야기로 돌아가 보자. GitHub에는 정말 프로젝트 관리하기 좋은 기능이 많다. Issues를 만들 수 있고, 담당자를 지정할 수 있고, Projects에서 진행 상황을 볼 수도 있다. PR과 Review를 통해 코드가 main에 들어가기 전에 확인할 수도 있다. 처음 GitHub를 공부했을 때는 이런 기능을 잘 사용하면 팀 프로젝트도 자연스럽게 잘 관리될 거라고 생각했다. 그런데 아니었다. Issue를 만든다고 일이 진행되는 것은 아니다. Projects Board를 만든다고 일정이 맞춰지는 것도 아니다. PR 규칙을 만든다고 팀원들끼리 소통이 잘되는 것도 아니다. 결국 이런 기능들은 프로젝트를 관리하기 위한 도구 일 뿐이었다. 중요한 건 그 도구 안에 어떤 Task를 만들고, 누구에게 맡기고, 어떤 작업을 먼저 처리하고, 문제가 생겼을 때 어떻게 대응할지를 결정하는 것이다. Git보다 어려운 건 사람이었다 처음 팀 프로젝트를 시작했을 때 가장 어려운 건 GitHub라고 생각했다. commit도 몰랐고, branch도 헷갈렸고, PR은 왜 사용하는지도 잘 몰랐다. 그래서 공부했다. 명령어를 배우고, GitHub 기능을 찾아보고, 여러 번 사용하다 보니 어느 정도 익숙해졌다. Git은 잘못된 게 있으면 확인할 방법이라도 있다. git status git log git diff 문제가 생기면 기록을 보고 원인을 찾을 수도 있다. 그런데 사람은 그렇지 않았다. 사람마다 실력도 다르고, 개발 속도도 다르고, 프로젝트에 사용할 수 있는 시간도 다르다. 같은 말을 해도 서로 다르게 이해할 수도 있다. 그래서 처음에는 사람을 잘 관리해야 한다고 생각했다. 그런데 지금은 그 생각도 조금 바뀌었다. 사람을 관리하려고 하기보다, 사람들이 각자 움직일 수 있도록 프로젝트를 관리하는 게 더 중요하다고 생각한다. Task를 명확하게 만들고, 누가 담당하는지 정하고, 막힌 부분이 빠르게 보이게 만들고, 필요하면 우선순위를 바꾸고, 리더가 모든 일을 가져가는 대신 각자 자신의 영역에 Ownership을 가질 수 있게 만드는 것. 아직 나도 팀을 운영하고 있는 입장이라 이게 정답이라고 말할 수는 없다. 아마 지금 진행하고 있는 프로젝트가 더 커지고 팀원이 늘어나면 지금과는 또 다른 문제가 생길 것 같다. 그래도 예전에 반 친구들과 처음 GitHub를 켜놓고 git push 하나 때문에 헤매던 때와 비교하면, 지금은 적어도 하나는 알 것 같다. 팀 프로젝트에서 Git을 관리하는 것과 프로젝트를 관리하는 것은 완전히 다른 문제였다. 결론 결론은 생각보다 단순하다. 처음에는 GitHub를 잘 다루고, 역할을 잘 나누고, 팀원들에게 일을 잘 배정하면 그게 좋은 리더라고 생각했다. 하지만 직접 팀 프로젝트를 진행해보니 리더가 해야 하는 일은 단순히 사람들에게 일을 시키고 관리하는 것이 아니었다. 각자 맡은 작업이 명확하게 보이도록 만들고, 누군가 막혀 있다면 그 원인을 빠르게 찾고, 일정이 꼬이면 다시 우선순위를 정하고, 팀원들이 각자의 역할에 집중할 수 있도록 전체적인 흐름을 잡아주는 것이 더 중요했다. GitHub의 Ruleset이나 Branch protection처럼 사람에게 규칙 하나를 설정한다고 모든 게 해결되지는 않는다. 사람마다 실력도 다르고, 개발 속도도 다르고, 생각하는 방식도 다르기 때문이다. 그래서 결국 좋은 리더가 되려면 사람을 통제하는 방법보다 서로 다른 사람들이 하나의 프로젝트를 같이 만들어갈 수 있는 환경을 만드는 방법 을 알아야 한다고 생각한다. 나도 아직 팀을 만들고 운영하기 시작한 지 얼마 되지 않았고, 지금 진행 중인 프로젝트에서도 계속 새로운 문제를 겪고 있다. 아마 팀의 규모가 더 커지고 프로젝트가 복잡해지면 지금 적은 내용만으로 해결되지 않는 문제도 많이 생길 것 같다. 그래도 그런 문제들을 직접 겪어보고 해결하면서 프로젝트를 이끄는 방법도 조금씩 배워갈 수 있을 것 같다. 나중에 지금 진행하고 있는 프로젝트가 어느 정도 완성되면, 실제로 팀을 운영하면서 어떤 문제가 있었고 어떻게 해결했는지도 따로 글로 적어보려고 한다. 추가사항 현재 새로운 프로젝트를 같이 진행할 팀원을 모집하고 있다. 이번 글에서 이야기한 팀 운영 방식도 실제로 적용하면서 프로젝트를 진행해볼 생각이다. 관심이 있다면 아래 모집글을 한번 읽어봐주면 좋겠다. 프로젝트 팀원 모집글 많은 관심 부탁드린다! 이번 글은 여기까지 적어보겠다. 긴 글 읽어주셔서 감사합니다.