Post

1주차 Spring MSA 학습

웹 서버와 애플리케이션 아키텍처

  • Tomcat (웹 서버): 외부 클라이언트의 요청을 시스템에서 가장 먼저 받아들이는 문지기입니다 (대안: Undertow)
  • Servlet (라우팅): 들어온 요청을 분석하여 알맞은 Controller로 연결해 주는 역할을 수행합니다
  • Domain (도메인): 시스템 내에서 다루는 핵심 비즈니스 개념들의 논리적인 모음입니다

데이터베이스 인프라 관리 (Flyway & HikariCP)

🛠 Flyway: DB 마이그레이션 도구

  • 신뢰성 있는 DB 배포: 모든 변경 사항이 코드로 관리되므로 개발/스테이징/운영 환경 모두 동일한 스키마를 보장합니다.
  • 명확한 변경 이력 관리: flyway_schema_history 테이블을 통해 누가, 언제, 왜 DB를 변경했는지 추적할 수 있습니다.
  • 팀원 간의 손쉬운 동기화: Git을 통해 파일을 공유하여 git pull 후 애플리케이션 실행만으로 최신 스키마를 맞춥니다.

    🏊 HikariCP: DB 커넥션 풀(Connection Pool)

  • 개념: DB 접속 객체를 미리 여러 개 만들어 ‘풀(Pool)’에 저장해두고 빌려 쓰고 반납하는 기술입니다.
  • 채택 배경: Spring Boot 2.0부터 기본 채택되었으며, 바이트 코드 레벨의 저수준 최적화로 ‘빛(Hikari)’처럼 빠른 속도를 지향합니다.
  • ✅ 장점: 압도적인 성능, 높은 안정성, 간결한 설정, 활발한 유지보수.
  • ❌ 단점: 내부 구현이 매우 복잡하여 문제 발생 시 디버깅 진입 장벽이 높음.

DB 접근 기술 전략: MyBatis vs ORM

SQL Mapper (MyBatis)의 문제점

  • 특정 DB 의존성: SQL 문법에 의존하므로 DB 변경 시 모든 쿼리를 수정해야 합니다.
  • 서비스 로직 종속: 로직이 SQL에 치우쳐 Service 레이어가 비어있는 경우가 많습니다.
  • DB 부하 가중: 단순한 필터링 로직도 쿼리로 실행해버려 DB에 부담을 줍니다.
  • 방대한 코드량: SQL 결과를 객체(Class)로 변환하는 코드가 길어지고 유지보수가 어렵습니다.

    비유를 통한 비교

  • MyBatis: 특정 지역의 사투리(SQL 직접 작성)를 직접 쓰는 것. 지역(DB)을 옮기면 소통이 안 됩니다.
  • ORM (JPA/Hibernate): 표준어(객체)로 말하면 통역사(ORM)가 알아서 해당 지역의 사투리로 번역해주는 것. DB 독립적이며 생산성이 뛰어납니다.

데이터 삭제 및 제약 조건 전략

삭제 전략 (Soft vs Hard Delete)

  • Soft Delete: 데이터를 물리적으로 지우지 않고 is_deleted 상태만 변경합니다. 메인 User 테이블 등에 주로 사용합니다.
  • Hard Delete: 실제 DB에서 데이터를 삭제합니다. 로그성 데이터나 특정 연관 테이블에 적용합니다.

    🔗 Foreign Key(외래키) 사용 전략

  • ✅ 사용하는 이유 (무결성 최우선): 데이터 무결성 강제 보장(고아 데이터 방지), 앱 개발 단순화, 관계 명시 정의.
  • ⚠️ 사용하지 않는 이유 (유연성/성능 최우선): 쓰기 작업 시 무결성 확인 오버헤드 발생, 마이그레이션 복잡성, 분산 시스템(MSA)에서의 물리적 연결 불가.
  • 핵심 트렌드: 정규화 시 개념적 RDB 모델은 유지하되, 물리적 제약(빡빡한 FK 등)은 최소화하여 서버 애플리케이션 로직으로 정합성을 해결하는 추세입니다.

데이터베이스 설계 및 ORM 연관관계

식별자 및 구조

  • Identity PK vs UUID: 순서 보장이 필요한지, 글로벌 고유성이 필요한지에 따라 전략적으로 선택합니다.
  • ID 체계: ORM에서는 각 테이블마다 고유 ID를 가지는 것이 유리하며, 관련 테이블은 user_id와 같은 FK로 연결합니다.

    연관관계 설정

  • 1:N 관계: Entity에서는 객체 자체를 필드로 인식(FK)하여 활용합니다.
  • N:N (다대다) 관계: 복잡성이 높아 실무에서는 기피합니다. 대신 중간에 Mapping Table을 두어 1:N 관계로 풀어내는 것이 조회 속도와 관리 면에서 훨씬 빠릅니다.
  • FK vs Index: FK를 걸면 Index가 자동으로 따라오는 경우가 많지만, “FK(관계 제약)와 Index(조회 성능)”는 반드시 분리해서 이해해야 합니다.

네이밍 컨벤션 (Naming Convention)

  • 테이블명: ‘집합’을 의미하는 복수형(users)보다 객체 매핑의 직관성을 위한 단수형(user)을 선호합니다. (프로젝트 내 일관성이 중요)
  • Boolean 필드: is-, has-, can- 등 질문 형태의 접두사를 사용하여 가독성을 높입니다.

영속성 전이 (Cascade)와 고아 객체 (Orphan Removal)

설명:

부모 객체(Aggregate Root)가 자식 객체의 생명주기(생성, 삭제)를 완벽하게 통제하기 위한 설정입니다. 자식 객체가 두 개의 부모를 가질 경우, 생사를 같이하는 ‘진짜 부모’ 딱 한 곳에만 이 설정을 부여해야 연쇄 삭제라는 대참사를 막을 수 있습니다.

예시:

  • Cascade: 주문(부모)을 save() 하면, 그 안에 담긴 주문 상품(자식) 리스트도 자동으로 DB에 INSERT 됩니다.
  • Orphan Removal: 주문(부모) 객체의 리스트에서 특정 주문 상품(자식)을 remove()로 빼버리면, DB에서도 해당 자식이 DELETE 됩니다.

ORM 데이터 로딩 전략 (FetchType)

설명:

“ORM의 경우 객체 안의 객체를 찾기 위해 추가적으로 쿼리를 날린다”는 정확히 맞는 말입니다. DB는 테이블끼리 JOIN을 하지만, 객체 지향(ORM)에서는 객체 그래프를 탐색(.으로 파고듦)할 때마다 쿼리가 발생할 수 있습니다. 이를 제어하는 것이 FetchType입니다.

  • LAZY (지연 로딩): 처음에는 가짜 객체(Proxy)만 끼워 넣고, 실제로 그 객체의 데이터를 꺼내 볼 때(메서드 호출 시) 쿼리를 날립니다. (실무 기본값)
  • EAGER (즉시 로딩): 처음부터 JOIN 쿼리를 날려서 연관된 객체를 다 끌고 옵니다. (예측 불가능한 쿼리가 나가므로 실무 사용 금지) 예시:
1
2
3
4
// LAZY 설정 시
Product product = productRepository.findById(1L); // Product 쿼리만 1번 발생
Category category = product.getCategory(); // 여기까지도 쿼리 안 나감 (Proxy 상태)
String name = category.getName(); // 실제 데이터에 접근하는 이 순간! Category 조회 쿼리 발생

N+1 문제 발생 원인

설명:

1번의 쿼리로 N개의 데이터를 가져왔는데, 그 N개의 데이터가 가지고 있는 연관 객체를 초기화하기 위해 N번의 추가 쿼리가 발생하는 최악의 성능 이슈입니다. 주로 연관관계 컬렉션을 반복문(for문)으로 돌릴 때 발생합니다.

예시:

게시글(부모) 10개를 목록으로 조회하는 쿼리 1번 발송 ➔ 화면에 댓글(자식) 개수를 표시하려고 for문으로 10개의 게시글을 돌면서 post.getComments().size()를 호출 ➔ 댓글을 가져오는 쿼리가 10번 추가로 발송됨. (총 1+10 = 11번의 쿼리 발생)


N+1 해결 전략: Fetch Join

설명:

방치하면 절대 안 되는 N+1을 해결하는 가장 확실하고 1순위인 방법입니다. 일반적인 로딩 전략(LAZY)을 무시하고, 개발자가 직접 명시한 쿼리에서만 한방에 JOIN해서 연관된 데이터를 다 끌고 오는 기능입니다.

예시:

1
2
# 테이블 명이 아닌, Product 엔티티(p)  안의 category 필드를 사용합니다.
SELECT p FROM Product p JOIN FETCH p.category

N+1 해결 전략: Batch Size

설명:

Fetch Join을 쓸 수 없는 상황(예: 페이징 처리)에서 N+1을 완화하는 2순위 방법입니다. Batch Size는 FetchType.LAZY 로딩 환경에서 동작하는 최적화 옵션입니다. N번 쿼리가 나갈 것을 지정한 사이즈(보통 100~1000)만큼 모아서 SQL의 IN 쿼리 한 방으로 묶어서 쳐줍니다.

예시:

  • 설정 전: WHERE category_id = 1, WHERE category_id = 2 … (100번 나감)
  • 설정 후: WHERE category_id IN (1, 2, 3, … 100) (1번 나감)

Spring Data JPA 네이밍 쿼리 (Find / First / All)

설명:

메서드 이름만으로 쿼리를 생성해 주는 기능입니다. 작성하신 내용처럼 All은 생략 가능하며, First나 Top은 LIMIT 쿼리를 날릴 때 사용합니다.

예시:

  • findAllByName(String name) ➔ findByName(String name)과 완벽히 동일하게 동작합니다.
  • findFirstByStatusOrderByCreatedAtDesc(String status) ➔ 상태 조건으로 찾은 뒤 최신순으로 정렬하여 딱 1건만 가져옵니다. (LIMIT 1)

DTO (Data Transfer Object) 분리 및 네이밍

설명:

API 스펙(클라이언트와 주고받는 데이터)과 내부 도메인(Entity)을 철저히 분리하기 위해 사용합니다. Request와 Response는 화면(Controller) 계층의 요구사항에 맞춰 자유롭게 변형될 수 있어야 하므로, 엔티티를 직접 반환하지 않고 DTO를 거쳐야 순환 참조 에러도 막을 수 있습니다.

예시:

  • Request DTO: 클라이언트가 서버로 데이터를 보낼 때 사용 (예: ProductCreateRequest)
  • Response DTO: 서버가 클라이언트에게 데이터를 응답할 때 사용 (예: ProductResponse)

This post is licensed under CC BY 4.0 by the author.