#include <stdio.h> // (standard input-output header file) int test(int x) { // 함수 선언 printf("x는 %d입니다.\n", x); // %d (decimal 10진수) return x; } int main(void) { int a = 10; // 변수 선언 -> 메모리 할당 printf("a는 %d입니다.\n", a); // %d (decimal 10진수) , \n -> 줄바꿈 char b = 'A'; // 변수 선언 -> 메모리 할당 printf("b는 %c입니다.", b); // %c (character 문자) char c[4] = "ABCD"; // 문자열 선언 -> 메모리 할당 printf("c는 %s입니다.", c); // %s (string 문자열) int d; puts("Hello World!"); // puts() 함수는 문자열을 출력하고 줄바꿈을 수행한다. scanf("%d", &d); // scanf() 함수는 키보드로부터 입력을 받는다. &d -> d의 주소값, 앰퍼센트(&) printf("입력된 값은 %d입니다.", d); float e; scanf("%f", &e); // %f (float 실수) printf("입력된 실수는 %.2f입니다.", e); //.2 -> 소수점 2자리까지 입력받음 printf("%f",(float)a); // 정수형 a를 실수형으로 형변환하여 출력 a = ++a; // ++a(전위 연산자)는 값을 먼저 1 증가시킨 뒤에 연산에 사용하는 반면 a = a++; // a++(후위 연산자)는 현재 값을 먼저 연산에 사용한 후에 값을 1 증가시킵니다 if(a > 10) { // 조건 printf("a는 10보다 큽니다."); // 실행문 1 } else if(a == 10) { printf("a는 10과 같습니다."); // 실행문 2 } else { printf("a는 10보다 작습니다."); // 실행문 3 } while(a < 20) { // 조건 printf("a는 20보다 작습니다.\n"); // 실행문 a++; // a를 1 증가시킴 } do { // 1회는 무조건 실행 printf("a는 20보다 작습니다.\n"); // 실행문 a++; // a를 1 증가시킴 } while(a < 20); // 조건 switch(a) { // 조건 case 10: // a가 10일 때 printf("a는 10입니다."); // 실행문 break; // switch문 종료 case 20: // a가 20일 때 printf("a는 20입니다."); // 실행문 break; // switch문 종료 default: // 위의 조건에 해당하지 않을 때 printf("a는 10도 아니고 20도 아닙니다."); // 실행문 } for(int i = 0; i < 10; i++) { // 초기값, 조건식, 증감값 printf("i는 %d입니다.\n", i); // 실행문 } return 0; // main() 함수 종료 } // 관계 연산자 // == : 같다, // != : 같지 않다, // > : 크다, // < : 작다, // >= : 크거나 같다, // <= : 작거나 같다 // 논리 연산자 // && : 그리고, // || : 또는, // ! : 아니다 // 변수명 규칙 // 1. 변수명은 영문자(대소문자), 숫자, 언더스코어(_)로 구성되어야 한다. // 2. 변수명은 숫자로 시작할 수 없다. // 3. 이름 사이에는 공백을 사용할 수 없다. // 4. C언어에서 미리 정의된 예약어(키워드)는 변수명으로 사용할 수 없다. // 진법 변환 , 비트연산 // 10진수 -> 2진수 : %b (2로 나눈 나머지를 역순으로 나열) // 2진수 -> 8진수 : %o (3자리씩 끊어서 8진수로 변환, 3자리당 최대 7까지 표현 가능 1,2,4/ 3자리마다 2진수 1인 값 더하기) // 2진수 -> 16진수 : %x (4자리씩 끊어서 16진수로 변환, 4자리당 최대 15까지 표현 가능 1,2,4,8/ 4자리마다 2진수 1인 값 더하기) // 비트 연산자 // & : 비트 AND 연산자, 두 비트가 모두 1이면 1, 아니면 0 // | : 비트 OR 연산자, 두 비트 중 하나라도 1이면 1, 아니면 0 // ^ : 비트 XOR 연산자, 두 비트가 서로 다르면 1, 같으면 0 // ~ : 비트 NOT 연산자, 양수일 경우 +1 후에 부호를 바꾼다. 음수일 경우 -1 후에 부호를 바꾼다. // << : 비트 왼쪽 시프트 연산자, 비트를 왼쪽으로 이동시키고 오른쪽에 0을 채움 // >> : 비트 오른쪽 시프트 연산자, 비트를 오른쪽으로 이동시키고 왼쪽에 0을 채움
Michael Lynch(전 Google, Microsoft 소프트웨어 엔지니어)가 제시한 '효과적인 소프트웨어 설계 문서(Software Design Document) 작성법'의 핵심 내용 분석 및 체계 요약임. 1. 설계 문서 작성 판단 기준 및 리스크 평가 설계 문서는 복잡도와 실패 리스크가 임계치를 넘는 프로젝트에서 개발 리소스 낭비를 방지하기 위한 조정 도구임. 작성 여부 결정 체크리스트 구현 단계에 2인 이상의 협업이 필요한가 개발 기간이 풀타임 기준 3개월 이상 소요되는가 프로덕션 환경에서 수년간 유지보수될 시스템인가 타 팀과의 교차 협업(Cross-team collaboration)이 수반되는가 프로젝트 목표 및 요구사항이 모호한가 설계 단계에서 사전 차단해야 하는 치명적 리스크(보안 취약점, 법적 규제 등)가 존재하는가 의사결정 기준: 1개 이상 해당 시 문서 작성 권장, 2개 이상 해당 시 필수 작성 대상임. 2. 포함 대상 판별 원칙: 실패 비용 (Cost of Getting It Wrong) 설계 문서는 세부 구현 명세서가 아니며, 핵심 판별 기준은 "해당 결정이 틀렸을 때 치러야 할 페널티"임. 포함 대상: 기술 스택 선정, 데이터 스토리지 아키텍처, 서비스 간 통신 프로토콜 등 사후 변경 비용이 극도로 높거나 시스템 전면 재작성을 초래하는 결정. 제외 대상: UI의 세부 배치, 페이지네이션 방식(예: '더보기' 버튼 vs 무한 스크롤) 등 피드백 수집 후 수 시간 내 수정 가능한 가역적 구현 상세. 3. 설계 문서 구성 요소 (Components) 범주 구성 섹션 기술 목적 및 핵심 요구사항 메타데이터 & 기본 정보 Title 3단어 내외의 고유하고 직관적인 프로젝트 식별 명칭 Metadata 작성자(이메일 포함), 작성일자, 표준 URL(사내 shortlink), 승인자(Sign-off) 및 승인일 Objective 프로젝트의 최종 목표를 평이한 언어로 요약한 단일 문장 (문서 1면에 배치) Background 추진 배경, 해결 과제, 이전 시도의 실패 원인 기술 (외부 맥락 없이도 이해 가능하도록 서술) Related docs 테스트 계획서, 기능 명세서(PRD), 선행 시스템 설계 문서 링크 연결 범위 및 시나리오 Goals 구현 완료 시 나타나는 엔드포인트 임팩트 (사용자/팀/비즈니스 중심, 구현 상세 지양) Non-goals 의도적으로 범위에서 제외하는 항목 명시 (이해관계자의 스코프 오판 차단) Scenarios 시스템 완료 시 사용자의 실제 엔드투엔드 상호작용 흐름 예시 시스템 아키텍처 Diagrams 데이터 흐름, 컴포넌트 결합도, 통신 프로토콜 도식화 (수정 용이한 툴/다이어그램 코드 기반 관리) Glossary 신규 입사자 및 타 팀을 위한 사내 고유 도구/용어 정의 (가능한 인라인 설명 권장) Constraints 인프라, 하드웨어 아키텍처(예: RISC-V), 예산 등 변경 불가능한 제약 조건 신뢰성 및 운영 SLOs 가용성(Uptime), 지연 시간(p50/p99 Latency), 처리 용량(Scale)의 정량적 목표 지표 Monitoring / Alerting 시스템 장애 및 급격한 성능 저하 감지 매커니즘, 알림 임계치 기준 Timeline 주요 마일스톤 단위의 인도 일정 및 구현 순서 Interfaces & Dependencies 통신 규격, 언어, 런타임 환경, 영구 저장소, 외부 서드파티 라이브러리 명세 보안 및 미결 과제 Security & Privacy 공격 표면(Attack surface), 신뢰 경계(Trust boundaries), 민감 데이터 보관 주기 및 암호화 방식 Logging 중요 이벤트 기록 기준, 로그 레벨 체계, 보존 주기, 개인정보/민감정보 제외 정책 Open / Resolved Issues 미해결 쟁점(문제, 선택지, 해결을 위한 후속 액션) 및 논의 완료된 결정 내역 추적 Alternatives Considered 검토 후 채택하지 않은 대안 기술/접근법 및 배제 사유 명시 4. 설계 검토(Review) 및 운영 단계 단계적 피드백 수집: 초안 작성 완료 후 핵심 이해관계자 및 파트너 팀을 대상으로 단계적 리뷰 진행. 불확실성 관리: 설계 도중 발생하는 공백은 숨기지 않고 Open issues 섹션에 공식 등재하여 논의를 집중 유도함. 살아있는 문서(Living Document) 지양: 설계 문서는 구현 개시 전 의사결정과 정렬을 위한 도구이며, 코드베이스 완성 후에는 시스템 실제 명세(Wiki/API 문서)로 역할을 이관하는 것이 원칙임.
🏗 예제 도메인 모델 순수 JPA로 리포지토리를 직접 구현해보고, Spring Data JPA의 공통 인터페이스가 어떻게 그 반복을 없애주는지, 내부적으로 어떻게 동작하는지 알아보도록 하자. ⛓️ Member와 Team의 관계 예제에서 사용할 도메인은 단순하다. 회원( Member )은 하나의 팀( Team )에 소속될 수 있고, 팀은 여러 회원을 가질 수 있다. 즉, 다대일(N:1) 관계다. 엔티티 필드 Member id (PK), username, age, team (FK) Team id (PK), name, members 🙋🏻♂️ Member 엔티티 @Entity @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @ToString(of = {"id", "username", "age"}) public class Member { @Id @GeneratedValue @Column(name = "member_id") private Long id; private String username; private int age; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "team_id") private Team team; public Member(String username, int age) { ... } public Member(String username, int age, Team team) { ... } public void changeTeam(Team team) { this.team = team; team.getMembers().add(this); } } 👫 Team 엔티티 @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @ToString(of = {"id", "name"}) public class Team { @Id @GeneratedValue @Column(name = "team_id") private Long id; private String name; @OneToMany(mappedBy = "team") List<Member> members = new ArrayList<>(); } ❗️ 엔티티 설계 시 챙겨야 할 것들 @NoArgsConstructor(access = AccessLevel.PROTECTED) : JPA는 내부적으로 리플렉션을 사용해 기본 생성자를 호출한다. 완전히 막을 수는 없지만 PROTECTED 로 설정해두면 외부에서 new Member() 로 직접 생성하는 실수를 막을 수 있다. @ToString 에는 연관관계 필드 넣지 않기: team 을 @ToString 에 포함시키면 Member → Team → Member → ... 무한 루프로 StackOverflowError 가 난다. 반드시 기본 타입 필드만 포함해야 한다. @Setter 는 실무에서 지양: Setter를 열어두면 어디서든 엔티티 상태를 바꿀 수 있어 추적이 어려워진다. 상태 변경이 필요하다면 changeTeam() 처럼 의도가 명확한 메서드를 따로 만드는 게 좋다. 지금은 학습의 편의를 위해 임시로 사용했다. 연관관계 편의 메서드: 양방향 관계에서 한쪽만 설정하면 반대편이 동기화되지 않는다. changeTeam() 에서 this.team = team 과 team.getMembers().add(this) 를 함께 처리해두면 실수를 줄일 수 있다. 모든 연관관계는 지연 로딩( LAZY )으로 설정하는 게 기본이다. EAGER 는 예상치 못한 쿼리가 발생하기 쉽고, 특히 JPQL에서 N+1 문제의 원인이 된다. 💥 순수 JPA 리포지토리의 문제점 Spring Data JPA를 쓰기 전에, 순수 JPA로 리포지토리를 직접 구현해보자. EntityManager 를 사용해 기본 CRUD를 작성하면 아래와 같다. @Repository public class MemberJpaRepository { @PersistenceContext private EntityManager em; public Member save(Member member) { em.persist(member); return member; } public void delete(Member member) { em.remove(member); } public List<Member> findAll() { return em.createQuery("select m from Member m", Member.class) .getResultList(); } public Optional<Member> findById(Long id) { Member member = em.find(Member.class, id); return Optional.ofNullable(member); } public long count() { return em.createQuery("select count(m) from Member m", Long.class) .getSingleResult(); } } 잘 동작한다. 근데 팀 리포지토리도 만들어야 한다. 그럼 TeamJpaRepository 에도 save , delete , findAll , findById , count ... 똑같은 코드가 반복될 것이다. 엔티티가 늘어날수록 이 반복은 계속된다. CRUD 코드가 엔티티마다 그대로 복붙된다 는 게 핵심 문제다. ✨ Spring Data JPA 공통 인터페이스 Spring Data JPA는 이런 반복을 단 한 줄로 해결해준다. // 구현체를 따로 만들 필요가 없음 public interface MemberRepository extends JpaRepository<Member, Long> { } JpaRepository<Member, Long> 을 상속하는 것만으로 위에서 직접 구현했던 save , delete , findAll , findById , count 가 모두 제공된다. 🤔 어떻게 동작하는 걸까? 애플리케이션이 로딩될 때 Spring Data JPA가 JpaRepository 를 상속한 인터페이스들을 스캔한다. 그리고 각 인터페이스에 대한 프록시 구현체를 자동으로 생성 해서 스프링 컨테이너에 빈으로 등록해준다. 1. 애플리케이션 로딩 2. Spring Data JPA가 JpaRepository 상속 인터페이스 스캔 3. 각 인터페이스에 대한 프록시 구현체 자동 생성 4. 스프링 컨테이너에 빈으로 등록 5. @Autowired로 주입받아 사용 실제로 주입된 객체를 출력해보면 직접 만든 클래스가 아니라 Spring Data JPA가 만든 프록시 객체 임을 확인할 수 있다. System.out.println(memberRepository.getClass()); // class com.sun.proxy.$ProxyXXX 😁 @Repository 애노테이션 생략 가능 Spring Data JPA 리포지토리는 @Repository 를 붙이지 않아도 동작한다. 컴포넌트 스캔과 JPA 예외 → Spring 공통 예외 변환 을 Spring Data JPA가 자동으로 처리해주기 때문이다. 자동 스캔 범위는 @SpringBootApplication 이 선언된 패키지와 그 하위 패키지다. 리포지토리가 다른 패키지에 있다면 @EnableJpaRepositories(basePackages = "...") 를 직접 지정해야 한다. 🔍 JPA 영속성 컨텍스트와 테스트 Spring Data JPA를 사용하다 보면 테스트에서 자주 막히는 지점이 있다. JPA의 영속성 컨텍스트 개념을 모르면 이유를 알 수 없는 실패들이다. 👥 1차 캐시와 엔티티 동일성 같은 트랜잭션 안에서 동일한 ID로 조회하면 DB가 아닌 1차 캐시 에서 같은 인스턴스를 반환한다. Member a = memberRepository.findById(1L).get(); Member b = memberRepository.findById(1L).get(); assertThat(a).isEqualTo(b); // true — DB 쿼리는 1번만 실행 JPA는 동일 트랜잭션 내에서 같은 PK로 조회한 엔티티는 항상 같은 인스턴스 임을 보장한다( a == b ). 별도로 equals() 를 구현하지 않아도 이 보장 덕분에 isEqualTo() 테스트가 통과한다. 🫣 @Transactional이 없으면 테스트가 실패하는 이유 @Transactional 이 없으면 save() 와 findById() 가 서로 다른 트랜잭션 으로 실행된다. findById() 호출 시점에 이전 영속성 컨텍스트는 이미 닫혀 있고, DB에서 새 인스턴스 를 만들어 반환한다. Member 에 equals() 가 없으면 Object.equals() (참조 비교)로 fallback되어 테스트가 실패한다. // @Transactional이 없으면 실패 @Test void test() { Member saved = memberRepository.save(new Member("kim", 20)); Member found = memberRepository.findById(saved.getId()).orElseThrow(); assertThat(found).isEqualTo(saved); // 다른 인스턴스 → 실패 } 테스트 클래스에 @Transactional 을 붙이면 각 테스트가 하나의 트랜잭션 안에서 실행되고, 끝나면 자동으로 롤백된다. 🚨 변경 감지 (Dirty Checking) JPA에서 수정은 별도의 update 메서드가 필요 없다. 트랜잭션 안에서 엔티티 필드를 직접 변경하면, 트랜잭션 커밋 시점에 자동으로 UPDATE SQL이 실행된다. @Transactional void update(Long id, String newName) { Member member = memberRepository.findById(id).orElseThrow(); member.changeUsername(newName); // 이것만으로 UPDATE 실행됨 // memberRepository.save(member) 불필요 } 동작 원리는 아래와 같다. 엔티티 조회 시 원본 스냅샷 저장 트랜잭션 커밋 직전 flush() 자동 호출 현재 상태와 스냅샷 비교 달라진 필드가 있으면 UPDATE SQL 실행 🎭 flush() vs clear() 메서드 동작 em.flush() 변경 내용을 DB에 반영 (트랜잭션은 유지) em.clear() 1차 캐시 초기화 → 이후 조회 시 DB에서 다시 로딩 테스트에서 DB 반영 결과를 캐시 없이 검증하고 싶을 때 flush() + clear() 를 함께 쓴다. em.flush(); // DB에 반영 em.clear(); // 1차 캐시 비우기 Member fresh = memberRepository.findById(id).get(); // DB에서 새로 조회 📐 JpaRepository 계층 구조 JpaRepository 가 제공하는 기능들이 어떻게 구성되어 있는지 계층 구조를 보면 이해하기 쉽다. <<스프링 데이터>> Repository ← 마커 인터페이스 (기능 없음) └── CrudRepository ← save, findById, delete, count 등 기본 CRUD └── PagingAndSortingRepository ← findAll(Sort), findAll(Pageable) │ <<스프링 데이터 JPA>> └── JpaRepository ← JPA 특화 기능 추가 (flush, deleteInBatch 등) Repository 부터 PagingAndSortingRepository 까지는 Spring Data Commons 에 속한다. JPA 말고 MongoDB, Redis 등 다른 저장소에서도 똑같은 인터페이스를 쓸 수 있도록 추상화되어 있다. JpaRepository 는 거기에 JPA에 특화된 기능을 얹은 것이다. 🖋️ 제네릭 타입 JpaRepository<T, ID> 타입 파라미터 의미 T 엔티티 타입 ID 식별자(PK) 타입 S 엔티티와 그 자식 타입 (save 등에서 사용) 📖 주요 메서드 메서드 설명 save(S) 새 엔티티면 persist , 이미 있으면 merge findById(ID) Optional<T> 반환. 내부적으로 em.find() 호출 getReferenceById(ID) 프록시 반환. 실제 필드 접근 시에 SELECT 실행. em.getReference() 호출 findAll(...) 전체 조회. Sort 나 Pageable 파라미터 가능 delete(T) 내부적으로 em.remove() 호출 count() 전체 개수 반환 existsById(ID) 존재 여부 확인 🎏 findById vs getReferenceById 연관관계를 설정할 때 굳이 엔티티 전체를 조회할 필요가 없다. ID만 필요한 경우 getReferenceById() 를 쓰면 SELECT 없이 프록시만 받아올 수 있다. // findById: SELECT 즉시 실행 Member member = memberRepository.findById(memberId).orElseThrow(); // getReferenceById: SELECT 없음. 연관관계 설정 등 ID만 필요한 경우에 유용 Member memberRef = memberRepository.getReferenceById(memberId); order.setMember(memberRef); // 여기서도 SELECT 안 함 😂 공통 인터페이스의 한계 JpaRepository 는 모든 엔티티에 공통으로 적용할 수 있는 기능만 제공한다. username 으로 회원을 찾는 것처럼 도메인에 특화된 쿼리는 제공하지 않는다. 이걸 보완하는 게 쿼리 메서드 다. 메서드 이름 규칙만 지키면 Spring Data JPA가 자동으로 쿼리를 만들어준다. // 선언만 해도 동작 List<Member> findByUsernameAndAgeGreaterThan(String username, int age); 📝 정리 단계 핵심 포인트 도메인 모델 연관관계는 LAZY, @ToString에 연관관계 필드 제외, 편의 메서드로 양방향 동기화 순수 JPA EntityManager로 직접 구현 — 엔티티마다 CRUD 코드 반복 Spring Data JPA 인터페이스 선언만으로 프록시 구현체 자동 생성, @Repository 생략 가능 영속성 컨텍스트 1차 캐시로 동일 트랜잭션 내 엔티티 동일성 보장, 변경 감지로 update 불필요 테스트 @Transactional 없으면 엔티티 동일성 테스트 실패 JpaRepository Repository → CrudRepository → PagingAndSortingRepository → JpaRepository 계층 구조
4주차에는 객체 포인터와 객체 배열, 동적 메모리 할당을 학습했다. new 와 delete 를 이용하여 실행 중에 필요한 만큼 메모리를 할당하고 반환하는 방법과 this 포인터, string 클래스의 주요 함수에 대해서도 학습했다. 1. 객체 포인터 객체 포인터는 객체의 주소를 저장하는 포인터 변수 이다. 일반 변수의 주소를 포인터에 저장했던 것처럼 객체의 주소 역시 해당 클래스 타입의 포인터에 저장할 수 있다. 예시 Circle donut; Circle *p; p = &donut; p 에는 donut 객체의 주소가 저장되므로 p 를 이용하여 donut 객체의 멤버에 접근할 수 있다. . 과 -> 일반 객체에서 멤버에 접근할 때는 . 연산자를 사용하지만, 객체 포인터를 통해 멤버에 접근할 때는 -> 연산자 를 사용한다. 예시 Circle donut; Circle *p = &donut; donut.getArea(); p->getArea(); (*p).getArea(); p->getArea() 와 (*p).getArea() 는 같은 의미이다. 객체 → . 객체 포인터 → -> 2. 객체 배열 같은 클래스의 객체를 여러 개 저장해야 할 때 객체 배열 을 사용할 수 있다. 일반적인 배열과 선언 방법은 동일하지만, 배열의 각 원소가 하나의 객체가 된다는 차이가 있다. 예시 int n[3]; // int형 배열 Circle circles[3]; // Circle 객체 배열 circles[0] , circles[1] , circles[2] 가 각각 하나의 Circle 객체가 된다. 객체 배열과 생성자 객체 배열을 생성하면 배열의 각 원소에 대해 생성자가 호출된다. 예시 Circle circles[3]; 위와 같이 객체 배열을 생성하면 세 객체에 대해 기본 생성자가 각각 호출된다. circles[0] → Circle() circles[1] → Circle() circles[2] → Circle() 따라서 객체 배열을 생성하기 위해서는 호출할 수 있는 기본 생성자 가 필요하다. 예시 class Circle { public: Circle(int r) { radius = r; } }; Circle circles[3]; // 오류 Circle(int r) 만 직접 정의하면 컴파일러가 기본 생성자 Circle() 을 자동으로 만들어주지 않는다. 따라서 Circle circles[3] 에서 각 객체를 생성할 기본 생성자가 없어 오류가 발생한다. 배열을 생성하면서 각 객체가 사용할 생성자를 직접 지정할 수도 있다. 예시 Circle circles[3] = { Circle(10), Circle(20), Circle() }; 객체 배열이 소멸될 때는 각 객체의 소멸자가 생성의 반대 순서 로 호출된다. 생성 : circles[0] → circles[1] → circles[2] 소멸 : circles[2] → circles[1] → circles[0] 3. 객체 배열과 포인터 배열의 이름이 첫 번째 원소의 주소를 나타내는 것처럼 객체 배열도 포인터를 이용하여 접근할 수 있다. 예시 Circle circles[3]; Circle *p = circles; cout << p->getArea() << endl; p++; cout << p->getArea() << endl; 처음에는 p 가 circles[0] 을 가리키고 있다. p++ 을 실행하면 다음 객체인 circles[1] 을 가리키게 된다. circles[0] circles[1] circles[2] ↑ p p++ circles[0] circles[1] circles[2] ↑ p 4. 동적 메모리 할당 프로그램을 작성하는 시점에 필요한 메모리의 크기를 미리 정하는 것을 정적 할당 이라고 한다. 반면 필요한 메모리의 크기를 미리 알 수 없는 경우에는 프로그램 실행 중에 필요한 만큼 메모리를 할당할 수 있다. 이를 동적 메모리 할당 이라고 한다. C++에서는 동적 메모리를 할당하고 반환하기 위해 new 와 delete 연산자를 사용한다. new : 힙(heap) 메모리에 필요한 공간을 할당 delete : new 로 할당받은 메모리를 반환 예시 int *p = new int; *p = 5; cout << *p << endl; delete p; new int 를 통해 int 하나를 저장할 공간을 동적으로 할당하고, 그 주소를 p 에 저장한다. 사용이 끝난 뒤에는 delete 를 이용하여 해당 메모리를 반환한다. 5. 배열의 동적 할당 배열 역시 new 를 이용하여 동적으로 생성할 수 있다. 예시 int n; cin >> n; int *arr = new int[n]; 배열의 크기 n 을 사용자에게 입력받기 때문에 프로그램을 작성할 때 배열의 크기를 미리 정하지 않아도 된다. 동적으로 생성한 배열을 반환할 때는 일반 delete 가 아니라 delete[] 를 사용해야 한다. 예시 int *p = new int[5]; // 배열 사용 delete[] p; 하나의 데이터를 동적 할당했다면 delete , 배열을 동적 할당했다면 delete[] 를 사용한다. new ↔ delete new [] ↔ delete [] new 로 할당하지 않은 메모리를 delete 하거나 이미 반환한 메모리를 다시 delete 해서는 안 된다. 잘못된 예시 int n; int *p = &n; delete p; // X p 가 가리키는 공간은 new 를 이용하여 동적으로 할당받은 공간이 아니기 때문에 delete 할 수 없다. 6. 객체의 동적 생성 기본 자료형뿐만 아니라 객체도 동적으로 생성 할 수 있다. 예시 Circle *p = new Circle(30); cout << p->getArea() << endl; delete p; new Circle(30) 을 실행하면 객체를 위한 메모리가 할당되고 Circle(int r) 생성자가 호출된다. 이후 delete p 를 실행하면 객체의 소멸자가 호출된 뒤 메모리가 반환된다. new Circle(30) ↓ 메모리 할당 ↓ 생성자 호출 delete p ↓ 소멸자 호출 ↓ 메모리 반환 일반적인 지역 객체는 범위를 벗어나면 자동으로 소멸하지만, new 를 이용하여 생성한 객체는 사용이 끝난 뒤 직접 delete 를 해주어야 한다. 7. 객체 배열의 동적 생성 객체 배열 역시 동적으로 생성할 수 있다. 예시 int n; cin >> n; Circle *circles = new Circle[n]; 사용자가 입력한 n 의 값만큼 Circle 객체가 생성된다. 각 객체에 대해서는 기본 생성자가 호출된다. 동적으로 생성한 객체 배열은 일반 객체 배열과 동일하게 사용할 수 있다. 예시 circles[0].setRadius(10); circles[1].setRadius(20); cout << circles[0].getArea() << endl; 포인터 방식으로도 접근할 수 있다. circles->setRadius(10); (circles + 1)->setRadius(20); 사용이 끝나면 delete[] 를 이용하여 반환한다. delete[] circles; 이때 배열에 있는 각 객체의 소멸자가 배열의 마지막 객체부터 역순으로 호출 된다. 8. 메모리 누수 동적으로 할당한 메모리를 더 이상 사용할 수 없는데도 반환하지 않은 상태를 메모리 누수(memory leak) 라고 한다. 예시 Circle *p = new Circle(10); // 객체 사용 // delete p;가 없음 new 를 이용하여 메모리를 할당했지만 delete 로 반환하지 않았다. 따라서 동적으로 할당한 메모리는 더 이상 필요하지 않을 때 적절하게 반환해야 한다. 9. this 포인터 this 는 현재 객체 자신을 가리키는 포인터 이다. 개발자가 직접 선언하는 변수가 아니라 클래스의 멤버 함수에서 사용할 수 있도록 컴파일러가 제공한다. 예시 class Circle { private: int radius; public: void setRadius(int radius) { this->radius = radius; } }; 여기서 this->radius → 현재 객체의 멤버 변수 radius → 함수의 매개변수 를 의미한다. this가 필요한 경우 특히 멤버 변수와 매개변수의 이름이 같은 경우 this 를 사용하면 둘을 명확하게 구분할 수 있다. 예시 void setRadius(int radius) { radius = radius; } 이렇게 작성하면 두 radius 모두 매개변수를 의미한다. 따라서 멤버 변수에 값을 저장하려면 다음과 같이 작성해야 한다. void setRadius(int radius) { this->radius = radius; } 각 객체는 자신의 this 를 가지므로 같은 멤버 함수를 호출하더라도 this 가 가리키는 객체는 서로 다르다. 10. string 클래스 C++의 string 클래스는 문자열을 저장하고 처리하기 위한 다양한 기능을 제공한다. 기본적인 문자열 입출력은 앞선 주차에서도 사용했기 때문에 이번에는 문자열 처리에 사용하는 주요 멤버 함수를 중심으로 정리했다. length() length() 는 문자열의 길이를 반환한다. 예시 string str = "Hello"; cout << str.length() << endl; 결과는 5 가 된다. substr() substr() 는 문자열의 일부를 잘라 새로운 문자열로 반환한다. 예시 string str = "Hello, Everyone!"; cout << str.substr(0, 5) << endl; cout << str.substr(7) << endl; substr(x, y) 는 x번째 인덱스부터 y개의 문자 를 반환한다. substr(x) 는 x번째 인덱스부터 문자열의 마지막까지 반환한다. string 역시 객체이기 때문에 포인터를 이용하거나 동적으로 생성할 수 있다. 예시 string *p = new string("C++"); p->append(" Great!!"); cout << *p << endl; delete p; p 가 string 객체를 가리키므로 멤버 함수에 접근할 때 -> 를 사용한다. Lab 4_1 - 동적 Dog 객체 배열 첫 번째 과제에서는 Dog 클래스를 정의하고 사용자가 입력한 강아지 수에 따라 Dog 객체 배열을 동적으로 생성 했다. 이번 과제는 3주차에서 학습한 클래스, 생성자와 소멸자, Getter/Setter에 이번 주에 배운 this , 객체 배열, 동적 메모리 할당을 함께 적용하는 문제였다. Dog 클래스 Dog 클래스에는 다음 네 가지 정보를 private 멤버 변수로 저장했다. class Dog { private: string name; int age; double weight; bool boosterShot; // ... }; 강아지의 예방접종 여부는 입력할 때는 y/n 문자열을 사용하지만, 클래스 내부에서는 bool 타입으로 저장한다. this를 이용한 Setter 이번 과제에서는 모든 함수 정의에서 this 를 사용했다. Setter에서도 매개변수와 멤버 변수의 이름이 같기 때문에 this 를 이용하여 둘을 구분할 수 있다. void Dog::setName(string name) { this->name = name; } void Dog::setAge(int age) { this->age = age; } void Dog::setWeight(double weight) { this->weight = weight; } void Dog::setBoosterShot(bool boosterShot) { this->boosterShot = boosterShot; } Getter에서도 현재 객체의 멤버에 접근할 때 this 를 사용할 수 있다. string Dog::getName() { return this->name; } int Dog::getAge() { return this->age; } 예방접종 여부 처리 예방접종 여부는 사용자에게 문자열 y/n 으로 입력받은 뒤 bool 값으로 변환하여 저장했다. string booster; cin >> booster; if (booster.compare("y") == 0) this->boosterShot = true; else this->boosterShot = false; 문자열 비교에는 compare() 를 사용했다. str1.compare(str2) == 0 이면 두 문자열이 같다는 의미이다. 출력할 때는 bool 값을 그대로 출력하는 대신 접종 여부에 따라 O 또는 X 를 출력하도록 했다. if (this->boosterShot) cout << "O" << endl; else cout << "X" << endl; Dog 객체 배열 동적 생성 이번 과제의 핵심은 사용자가 입력한 강아지 수만큼 객체를 생성하는 부분이었다. int n; cout << "How many dogs?"; cin >> n; Dog *dogs = new Dog[n]; 강아지 수가 프로그램을 실행하기 전에는 정해져 있지 않기 때문에 고정된 크기의 객체 배열 대신 new 를 이용하여 필요한 만큼 동적으로 생성했다. 각 객체의 정보는 반복문을 이용하여 입력받을 수 있다. for (int i = 0; i < n; i++) { dogs[i].inforInput(i + 1); } 출력 역시 반복문을 이용하여 각 객체의 writeOutput() 을 호출한다. for (int i = 0; i < n; i++) { dogs[i].writeOutput(); } 프로그램의 전체적인 흐름은 다음과 같다. 강아지 수 n 입력 ↓ new Dog[n] ↓ Dog 객체 n개 생성 ↓ 반복문으로 정보 입력 ↓ 반복문으로 정보 출력 ↓ 조건에 맞는 Dog 탐색 ↓ delete[] dogs 조건에 맞는 객체 찾기 마지막에는 두 살보다 많고 예방접종을 하지 않은 강아지 의 이름을 출력했다. for (int i = 0; i < n; i++) { if (dogs[i].getAge() > 2 && dogs[i].getBoosterShot() == false) { cout << dogs[i].getName() << endl; } } 객체의 멤버 변수가 private 이기 때문에 main() 에서는 직접 접근하지 않고 Getter를 이용하여 조건을 확인했다. 동적으로 생성한 객체 배열을 모두 사용한 뒤에는 메모리를 반환한다. delete[] dogs; 이 과정에서 배열에 포함된 각 Dog 객체의 소멸자가 호출된다. Lab 4_2 - string과 객체 포인터 두 번째 과제에서는 문자열을 입력받아 length() 와 substr() 을 이용하여 문자열을 처리했다. 첫 번째 문자열은 일반적인 string 객체를 이용하고, 두 번째 문자열은 string 객체의 포인터 를 이용하여 같은 작업을 수행했다. 문자열에 공백이 포함될 수 있기 때문에 입력에는 getline() 을 사용했다. string str; getline(cin, str); substr()을 이용한 문자열 처리 substr() 을 이용하면 문자열을 두 부분으로 나누어 다시 연결할 수 있다. str.substr(i) + str.substr(0, i) 예를 들어 다음 문자열이 있다고 하자. Hello i = 2 라면, str.substr(2) // "llo" str.substr(0, 2) // "He" 가 되므로 두 문자열을 연결하면 lloHe 가 된다. 이를 반복문과 함께 사용하면 시작 위치를 한 칸씩 이동시키며 문자열을 출력할 수 있다. for (int i = 0; i < str.length(); i++) { cout << str.substr(i) + str.substr(0, i) << endl; } string 객체 포인터 두 번째 문자열에서는 일반 객체가 아니라 string 객체의 포인터를 이용했다. string *p = new string; getline(cin, *p); p 는 string 객체를 가리키고 있기 때문에 length() 와 substr() 에 접근할 때 -> 를 사용한다. for (int i = 0; i < p->length(); i++) { cout << p->substr(i) + p->substr(0, i) << endl; } 사용이 끝난 뒤에는 동적으로 생성한 string 객체를 반환한다. delete p; 첫 번째 문자열과 두 번째 문자열은 같은 작업을 수행하지만 접근 방법에는 다음과 같은 차이가 있다. string 객체 str.length() str.substr() ↓ string 객체 포인터 p->length() p->substr() 이를 통해 객체 자체를 사용할 때와 객체 포인터를 사용할 때 멤버에 접근하는 방법의 차이를 다시 확인할 수 있었다. 마무리 4주차에는 3주차에서 배운 클래스와 객체를 확장하여 객체 포인터와 객체 배열을 다루는 방법을 학습했다. 일반 객체에서는 . 을 사용하지만 객체 포인터에서는 -> 를 사용하여 멤버에 접근 한다는 점과, 객체 배열을 생성하면 배열의 각 객체마다 생성자와 소멸자가 호출된다는 점을 확인했다. 또한 new 와 delete 를 이용 하면 프로그램 실행 중 필요한 만큼 메모리를 동적으로 할당하고 반환 할 수 있다는 것을 배웠다. 특히 하나의 객체에는 delete , 배열에는 delete[] 를 사용 해야 하며 동적으로 할당한 메모리를 적절히 반환하지 않으면 메모리 누수가 발생할 수 있다는 점이 중요했다. 첫 번째 과제에서는 사용자 입력에 따라 Dog 객체 배열을 동적으로 생성하면서 this , Getter/Setter, 객체 배열, new 와 delete[] 를 함께 적용했다. 두 번째 과제에서는 ** string 의 length() 와 substr() 을 활용 하고 같은 문자열 처리 작업을 일반 객체와 객체 포인터 방식으로 각각 구현했다. 이번 주차를 통해 하나의 객체를 정의하고 사용하는 것에서 나아가, 여러 객체를 효율적으로 관리하고 실행 중 필요한 만큼 객체를 동적으로 생성하는 방법 까지 확장하여 이해할 수 있었다.
GitHub 링크 Notion 링크 오늘 보고서부터 기존에 ‘TODO’ 항목으로 정리하던 향후 개선 및 검토 사항은 노션 보고서에 기록하고, 본 게시물에는 실제 개발 내용과 개발 과정에서 새롭게 알게 된 내용만 작성할 예정이다. 적 캐릭터의 공격 스킬 추가 위처럼 Special Attack 타입의 PerformAttack 노드를 만들어주고, 위와 같이 Selector를 하나 더 두고 Decorator를 Selector로 올려준다. 이제 Special Attack의 발동 조건을 설정해주자. 반경과 발동 확률을 기준으로 Decorator를 설정해줄 예정이다. 위와 같이 Decorator들을 만들어준다. 첫번째는 확률을 체크하는 Decorator이고, 또 다른 하나는 반경을 확인하는 Decorator이다. BTDecorator_Chance.h #pragma once #include "CoreMinimal.h" #include "BehaviorTree/BTDecorator.h" #include "BTDecorator_Chance.generated.h" /** * */ UCLASS() class SOULLIKEGAME_API UBTDecorator_Chance : public UBTDecorator { GENERATED_BODY() protected: UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 ChanceRate = 30; protected: virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override; }; BTDecorator_Chance.cpp #include "AI/Decorator/BTDecorator_Chance.h" bool UBTDecorator_Chance::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const { return ChanceRate > FMath::RandRange(1, 100); } BTDecorator_InRangeCheck.h #pragma once #include "CoreMinimal.h" #include "BehaviorTree/BTDecorator.h" #include "BTDecorator_InRangeCheck.generated.h" /** * */ UCLASS() class SOULLIKEGAME_API UBTDecorator_InRangeCheck : public UBTDecorator { GENERATED_BODY() protected: UPROPERTY(EditAnywhere, BlueprintReadWrite) FBlackboardKeySelector TargetBlackboardKey; UPROPERTY(EditAnywhere, BlueprintReadWrite) float RangeMin = 100.f; UPROPERTY(EditAnywhere, BlueprintReadWrite) float RangeMax = 200.f; protected: virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override; }; BTDecorator_InRangeCheck.cpp #include "AI/Decorator/BTDecorator_InRangeCheck.h" #include "AIController.h" #include "BehaviorTree/BlackboardComponent.h" #include "Kismet/KismetMathLibrary.h" bool UBTDecorator_InRangeCheck::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const { const APawn* ControlledPawn = OwnerComp.GetAIOwner()->GetPawn(); if (!ControlledPawn) { return false; } const AActor* TargetActor = Cast<AActor>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(TargetBlackboardKey.SelectedKeyName)); if (!TargetActor) { return false; } const float Distance = ControlledPawn->GetDistanceTo(TargetActor); return UKismetMathLibrary::InRange_FloatFloat(Distance, RangeMin, RangeMax); } 위와 같이 Decorator를 설정해준다. CalculateRawConditionValue() 함수를 오버라이딩해서 구현하면 된다. 실행해보면 스킬 자체는 사용하고 있는데, 방향을 제대로 못잡고 있는 것을 확인할 수 있다. 그전에 적의 무기 DA를 따로 만들어주자. 적 캐릭터의 행동 패턴 구현 Contents
예를들어, memberRepository.findById() 가 실행될 때, Spring 은 매번 DB 와 새로운 네트워크 연결을 만드는걸까 ? -> 보통은 그렇지않다, JPA 를 사용하는 경우 HikariCP 가 미리 관리하고 있는 DB Connection 을 빌려서 사용하고, 다시 반환한다. 보통 코드를 구현할때 아래와 같이 코드를 작성하고 끝낸다. @Transactional public void changeNickname(Long memberId, String nickname) { Member member = memberRepository.findById(memberId) .orElseThrow(); member.changeNickname(nickname); } 위에있는 코드가 서비스 코드에 있다고하면 위 코드를 실행한다면, 대략 아래와 같은 과정이 일어나게 된다. Service ↓ JPA / EntityManager ↓ Hibernate ↓ JDBC ↓ DataSource ↓ HikariCP ↓ Connection 대여 ↓ MySQL 오늘은 HikariCP ↕ JDBC Connection ↕ MySQL 위 과정을 중점적으러 보자. 일반적으로 DB Connection 을 새로 만드는것은 일반 JAVA 객체를 만드는것에 비해, 비용이 훨씬 많이든다. 새로운 DB Connection 을 생성하기 위해서는, Application ↓ 네트워크 연결 ↓ MySQL 접속 ↓ 인증 ↓ 세션 생성 ↓ Connection 사용 가능 위와같은 과정들이 필요하다. 이를, 매번 DB 요청마다 DB Connection 을 생성하고, 다 사용하면 없애는 식으로 하게되면 비용적으로 엄청난 손해이다. 이를 해결하기위해 ( 매번 새로운 DB Connection 을 생성 ) HikariCP의 Connection Pool 을 사용한다. HikariCP DB Connection Pool ┌───────────────┐ │ Connection 1 │ │ Connection 2 │ │ Connection 3 │ │ Connection 4 │ │ ... │ └───────────────┘ Spring 요청 → 빌림 → 사용 → 반환 HikariCP DB Connection Pool는, 이미 만들어놓은 DB Connection 을 보관하고 있다. 그래서, 요청이 들어오면 이미 만들어놓은 DB Connection 을 넘겨준다. 이를 사용하고나서, 작업들이 종료되게 되면 빌려준 DB Connection 을 받고 다시 Connection Pool 에 넣어놓는다. 흔히, DB 를 사용한다고 하면 설정정보 파일에 아래와 같은 정보들을 넣는다. spring: datasource: url: jdbc:... DB URL username: DB username password: DB password Spring Boot 가 해당 설정들을 읽는다. Spring Boot ↓ DataSource ↓ HikariDataSource ↓ HikariPool 이후, HikariCP는, DB Connection 들을 관리한다. 해당 요청이 들어왔다고 하자. @Transactional public void changeNickname(Long memberId, String nickname) { Member member = memberRepository.findById(memberId) .orElseThrow(); member.changeNickname(nickname); } DB작업이 필요해진다. memberRepository.findById(memberId); ( 영속성 컨텍스트에 해당 객체가 없다고 가정 ) Repository ↓ EntityManager ↓ Hibernate ↓ JDBC Hibernate 가 SQL 을 실행하려면, JDBC Connection 이 필요하다. 예를들어, SQL 이 다음과 같이 실행된다면 .. SELECT * FROM member WHWERE member_id = ? JDBC Driver 가, DB 에 이 SQL 을 보내야한다. ( DB -> MySQL ... ) 그럼, Connection 을 어디서 가져오는걸까? 여기서, HikariCP 가 등장한다. Connection 을 가지고 오기위해, Hibernate 가 DataSource 에게 Connection 을 요청을 한다. Connection connection = dataSource.getConnection(); dataSource 가, HikariDataSource 라면, dataSource.getConnection() ↓ HikariCP ↓ Pool에서 사용 가능한 Connection 검색 이 된다. 예를들어 혅재 Pool 이 Connection Pool C1 → 사용 중 C2 → 사용 가능 C3 → 사용 중 C4 → 사용 가능 이런식으로 있다면, 사용가능한 C2 / C4 중 1개를 빌려주게 된다. C2 를 빌려준경우 아래와 같은 과정이 된다. Application ↓ C2 ↓ DB ( MySQL ... ) 즉, 애플리케이션은 C2 ( DB Connection ) 을 통해, DB 에 SQL 을 보낼 수 있게된다. 여기에서 중요한것은, DB Connection 을 새로 만들어서 반환해주는게아니라, 미리 만들어놓은것중 사용가능한 DB Connection 을 반환한다는것이다. 이제, Hibernate 가 DB 에 SQL 을 실행한다. SELECT * FROM member WHWERE member_id = ? SQL 을 실행하고서, 결과가 있다면 결과를 반환해줄것이다. MySQL ↓ JDBC ResultSet (데이터베이스 쿼리 실행 결과를 저장하는 JAVA 인터페이스) ↓ Hibernate ↓ Member Entity ↓ Persistence Context 이후에는, 이전에 배웠던 JPA 내용과 연결된다. member.changeNickname(nickname); 현재 Member Entity 는 영속 상태이다. ( findById -> Member 를 찾아왔다고 하자. ) 따라서 아래와 같은 상황이 된다. Persistence Context Member nickname: old → Hoon 스냅샷을 통해서, 이전값과 현재값이 다른것을 확인했으므로 트랜잭션 종료 시점에 ( commit 전, flush ) Dirty Checking ↓ flush ↓ UPDATE SQL 생성 그리고, UPDATE SQL 도, 같은 트랜잭션안에서 사용하는 Connection 을 통해 실행이된다. ( 위 예제에서는 C2 를 사용했기에, 업데이트문도 C2 를 통해서 ... ) @Transactional 종료 ↓ Hibernate flush ↓ UPDATE ↓ Connection ↓ COMMIT Spring 의 TransactionManager 가 트랜잭션을 Commit 을 한다. JDBC 수준에서는, 현재 Connection 과 연결된 DB 트랜잭션이 commit 된다. Connection.close() 일반적으로, 서버에서 자원을 끌어다가 사용한 경우 반드시 해당 자원을 닫아줘야만 한다. Connection Pool 을 사용하지 않는 일반적인 Connection 이였다면, 반드시 connection.close() 를 호출해줘야 한다. 그래야, 실제 DB 연결 종료를 의미한다. 하지만, HikariCP 에서 빌린 Connection 의 close() 는 연결을 종료하는 의미가 아니다. HikariCP 에서 빌린 Connection close() -> 커넥션을 Connection Pool 에 반납. connection.close() ↓ HikariCP ↓ Connection Pool에 반환 즉, close -> 현재 예시에서는 C2 ( Connection 2 ) 를 Connection Pool 에 반납해서, 해당 Connection ( Connection 2 ) 는 사용가능하다라는 상태가 된다. C2 사용 중 ↓ close() ↓ 사용 가능 이후에, 또 다른 요청이 들어오면 C2 는 사용가능하기때문에 해당 요청에 Connection 을 넘겨줄 수 있다. 이전에 배웠던 내용들과 합쳐서, 전체적인 흐름은 다음과 같을것이다. HTTP Request ↓ Tomcat Thread ↓ Filter ... ( Spring Security ) ↓ DispatcherServlet ↓ Controller ↓ Service Proxy ↓ @Transactional ↓ JpaTransactionManager ↓ EntityManager ↓ Hibernate ↓ JDBC ↓ HikariDataSource ↓ HikariCP ↓ Connection 대여 ↓ MySQL ↓ SELECT / UPDATE ↓ COMMIT ↓ Connection 반환 Connection Pool 을 사용하면서 문제가 발생할 수 있다. Connection Pool 은 Connection 을 어느정도 미리 만들어놓고 사용하도록 반환해주기때문에, 사용중인 Connection 보다 새로운 요청이 더 많게되면, 새로운 요청은 Connection 을 얻지 못할수도있다. 예를들어, HikariCP Connection Pool 에 Connection 이 10개 있다고 하자. HikariCP C1 C2 C3 ... C10 동시에 DB 작업을 수행하는 요청들이 Connection 을 모두 잡았다고 하자. C1 사용 중 C2 사용 중 ... C10 사용 중 즉, 현재 Connection Pool 에 있는 Connection 들을 사용할 수 없다. 이러한 상황에서 11번째 요청이 들어왔다고 하자. 그렇다면, 해당 요청은 요청을 하고서 바로 Connection 을 얻을 수 없다. Request 11 ↓ getConnection() ↓ 사용 가능한 Connection 없음 ↓ 대기 . . . 이렇게 대기를 하게되는데 ... 일정시간동안 대기하고도 Connection 을 얻지못한다면 timeout 이 발생할 수 있다. 해당 문제는 주로, 다음과 같은 상황에서 발생한다. ( 트랜잭션 안에서 외부 API 를 호출하는 ) @Transactional public void process() { Member member = memberRepository.findById(1L) .orElseThrow(); externalApi.call(); ( 해당 작업이 5초가 걸린다고 가정. ) member.update(); } (1L 의 Member 가 현재, 영속성 컨텍스트에 없는 상황이라고 가정.) 위에 코드에서 Connection 을 확복한 상태라고하자. 그렇다면, 외부 API 를 호출하는 코드에서 5초동안 기다려야 하므로, DB Connection 을 마찬가지로 5초동안 가지고있는다. Connection 획득 ↓ SELECT ↓ 외부 API 호출 ████████ 5초 ████████ ↓ UPDATE ↓ COMMIT ↓ Connection 반환 만약, Connection Pool 에 Connection 이 10개만 존재하고, Connection 10개가 모두 해당 과정을 거친다고 하자. 그렇다면, 해당 작업들이 진행되고 있는동안에는 사용할 수 있는 Connection 이 존재하지 않기때문에, 이후 요청들은 모두 대기하는 상태가 된다. 그래서, 트랜잭션은 필요한 DB 작업 중심으로 짧게 유지한다. 라는것이 중요하다. 그렇다면, Connection Pool 안에, Connection 이 부족하지 않은 상황을 만들면 되는거아닌가? 라고 생각할 수 있다. 즉, Connection Pool 안에 Connection 들을 충분히 많이 만들어놓는것이다. spring: datasource: hikari: maximum-pool-size: 500 500개의 Connection 을 만들었다고하자. Connection 은 DB 입장에서도, HikariCP 입장에서도, 자원이다. 결국, 해당 Connection 들을 모두 관리해줘야 하기에 관리비용도 많이 들어갈것이다. 또한, DB 가 감당해야할 연결 수가 많아질것이고, 동시 작업도 커지게된다. 그렇기에, 적당한 Pool 크기를 잡아야한다. ( 이는 운영환경마다 다 다르니까 ... )