7주차 자료의 모든 토픽을 두 개의 큰 흐름으로 정리한 학습 경로.
1) 데이터 모델링과 ORM — SQL JOIN → ORM 패러다임 → JPA 입문 → 엔티티 매핑
2) 트랜잭션 추상화의 진화 — 수동 관리 → PlatformTransactionManager → @Transactional6주차에서 JDBC와 JdbcTemplate으로 DB 접근을 익혔다면, 7주차는 한 단계 더 높은 추상화로 올라간다.
사전 메모: 자료 첫 부분의 "스프링 빈이란?"은 5주차 Phase 8(ApplicationContext + DI)의 복습 포인트이므로 본 커리큘럼에서는 Phase로 분리하지 않고 학습 운영 팁에서 짧게 다룬다.
[Part A — 데이터 모델링과 ORM]
[Phase 1] SQL JOIN — 관계형 DB의 본질
↓
[Phase 2] ORM 패러다임 — 객체와 관계의 만남
↓
[Phase 3] JPA 입문
↓
[Phase 4] JPA 엔티티 매핑
[Part B — 트랜잭션 추상화의 진화]
[Phase 5] 수동 트랜잭션의 한계
↓
[Phase 6] PlatformTransactionManager (인터페이스 추상화)
↓
[Phase 7] @Transactional (선언적 트랜잭션)
총 7 Phase × 24 Unit
| 주차 | 주제 | 핵심 변화 |
|---|---|---|
| 1주차 | OOP·JVM·GC·컬렉션·I/O 개론 | 자바 큰 그림 |
| 2주차 | JVM 내부·바이트코드·G1 GC | "어떻게 돌아가나" |
| 3주차 | 컬렉션·제네릭·함수형 | 자바 표현력 |
| 4주차 | 멀티스레딩·동시성·Executor | 동시성 정복 |
| 5주차 | Atomic + Spring IoC/DI 입문 | 자바 → Spring 다리 |
| 6주차 | 테스트 + 웹 인프라 + DB 접근 진화 | Spring 실전 환경 |
| 7주차 (지금) | ORM/JPA + 트랜잭션 추상화 | DB 추상화의 정점 |
| Day | Phase | 학습 목표 |
|---|---|---|
| 1일차 | Phase 1 | SQL JOIN 4종 마스터 |
| 2일차 | Phase 2 + 3 | ORM 패러다임 + JPA 입문 |
| 3일차 | Phase 4 | JPA 엔티티 매핑 어노테이션 |
| 4일차 | Phase 5 + 6 | 수동 트랜잭션 → PlatformTransactionManager |
| 5일차 | Phase 7 | @Transactional 동작 원리 |
| 6일차 | 종합 자기 점검 + 실습 | 전체 정리 |
자료 분량이 5·6주차보다 가벼움. 6일 일정으로 충분하나, JPA 실습을 더 하려면 +3일.
목표: 4가지 JOIN의 결과를 머릿속에서 그릴 수 있게 된다. JPA를 이해하기 위한 필수 기초.
선수 지식: 6주차 Phase 4 (DB 세션)
핵심 개념
왜 JOIN이 필요한가:
employees (직원) departments (부서)
┌──┬───────┬───────────┐ ┌────┬──────────────┐
│id│ name │dept_id │ │ id │ dept_name │
├──┼───────┼───────────┤ ├────┼──────────────┤
│ 1│Alice │ 101 │ │101 │ HR │
│ 2│Bob │ 102 │ │102 │ Engineering │
│ 3│Charlie│ NULL │ │103 │ Sales │
└──┴───────┴───────────┘ └────┴──────────────┘
→ "Alice가 어느 부서?" 답하려면 두 테이블을 합쳐야 함 → JOIN
자기 점검
선수 지식: Unit 1.1
핵심 개념
"두 테이블에서 공통된 값 이 있는 행만 반환"
SELECT e.id, e.name, d.department_name
FROM employees e
INNER JOIN departments d ON e.department_id = d.id;
결과:
id │ name │ department_name
───┼───────┼─────────────────
1 │ Alice │ HR
2 │ Bob │ Engineering
Charlie(부서 NULL) 와 Sales(직원 없음) 는 제외됨.
자기 점검
선수 지식: Unit 1.2
핵심 개념
LEFT JOIN: 왼쪽 테이블의 모든 행 반환, 오른쪽 매칭 없으면 NULL
SELECT e.id, e.name, d.department_name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id;
id │ name │ department_name
───┼─────────┼─────────────────
1 │ Alice │ HR
2 │ Bob │ Engineering
3 │ Charlie │ NULL ← 부서 없는 직원도 포함
RIGHT JOIN: 왼쪽/오른쪽이 반대
SELECT e.id, e.name, d.department_name
FROM employees e
RIGHT JOIN departments d ON e.department_id = d.id;
id │ name │ department_name
─────┼───────┼─────────────────
1 │ Alice │ HR
2 │ Bob │ Engineering
NULL │ NULL │ Sales ← 직원 없는 부서도 포함
자기 점검
A LEFT JOIN B 와 B RIGHT JOIN A 는 같은가?선수 지식: Unit 1.3
핵심 개념
"양쪽 테이블의 모든 행 반환, 매칭 없으면 NULL"
SELECT e.id, e.name, d.department_name
FROM employees e
FULL OUTER JOIN departments d ON e.department_id = d.id;
id │ name │ department_name
─────┼─────────┼─────────────────
1 │ Alice │ HR
2 │ Bob │ Engineering
3 │ Charlie │ NULL ← 부서 없는 직원
NULL │ NULL │ Sales ← 직원 없는 부서
MySQL은 FULL OUTER JOIN을 지원하지 않음 → LEFT JOIN UNION RIGHT JOIN 으로 우회.
자기 점검
선수 지식: Unit 1.4
선택 매트릭스:
| 시나리오 | JOIN 종류 |
|---|---|
| 양쪽에 모두 있는 데이터만 필요 | INNER JOIN |
| 왼쪽 테이블의 모든 데이터 + 매칭되는 오른쪽 | LEFT JOIN |
| 두 테이블 모두의 모든 데이터 | FULL OUTER JOIN |
| "부서 없는 직원만" 같은 차집합 | LEFT JOIN + WHERE NULL |
실무 팁:
자기 점검
목표: ORM이 왜 필요한지를, 객체와 관계 모델의 본질적 미스매치로 이해한다.
선수 지식: 1주차 Phase 1 (OOP), Phase 1 (JOIN)
핵심 개념
객체와 관계형 DB는 다른 패러다임:
| 측면 | 객체 (OOP) | 관계형 DB |
|---|---|---|
| 모델링 | 상태 + 행동 | 행과 열 |
| 상속 | 있음 | 없음 |
| 연관 관계 | 참조 (order.member) | 외래 키 (FK) |
| 식별 | 객체 식별자 (==) | PK |
| 데이터 타입 | 풍부 (List, Map, ...) | 제한적 |
→ 이 차이를 메우는 코드를 매번 손으로 쓰는 게 ORM 등장 전의 고통
자기 점검
Order 객체가 List<OrderItem> 을 갖는 구조를 RDB로 어떻게 표현하는가?선수 지식: Unit 2.1
핵심 개념
ORM (Object-Relational Mapping):
효과:
대중적 ORM:
자기 점검
목표: JPA가 자바 진영의 ORM 표준임을 이해하고, JdbcTemplate/MyBatis와의 결정적 차이를 잡는다.
선수 지식: 6주차 Phase 7 (JdbcTemplate)
핵심 한계
JdbcTemplate, MyBatis 같은 SQL Mapper 는:
예시 (JdbcTemplate):
String sql = "SELECT * FROM item WHERE id = ?";
Item item = jdbcTemplate.queryForObject(sql, itemRowMapper, id);
// ↑ SQL과 RowMapper를 직접 작성
→ "SQL 자체를 안 쓰는 방법은 없을까?"
자기 점검
선수 지식: Unit 3.1
핵심 개념
JPA (Java Persistence API):
SQL Mapper vs JPA:
| SQL Mapper (JdbcTemplate, MyBatis) | JPA | |
|---|---|---|
| SQL 작성 | 개발자 | JPA가 자동 생성 |
| 매핑 | RowMapper로 수동 | 어노테이션으로 선언 |
| 객체 그래프 | 수동 처리 | 자동 (Lazy Loading) |
| 학습 곡선 | 낮음 | 높음 |
| 복잡 쿼리 | 자유 | 어려움 (네이티브 쿼리 필요) |
자기 점검
show-sql, format-sql)선수 지식: Unit 3.2
핵심 그림
┌─────────────────────────────────┐
│ Application Code │
│ (Item, Member, Order 객체...) │
└────────────┬────────────────────┘
│ JPA API
↓
┌─────────────────────────────────┐
│ JPA │ ← 객체 ↔ SQL 변환
│ (Hibernate 구현체) │
└────────────┬────────────────────┘
│ JDBC API
↓
┌─────────────────────────────────┐
│ JDBC │ ← Connection, PreparedStatement
└────────────┬────────────────────┘
│ DB Driver
↓
┌─────────────────────────────────┐
│ DB │
└─────────────────────────────────┘
핵심 위치:
자기 점검
선수 지식: Unit 3.3
핵심 개념
JPA를 더 편리하게 쓰기 위한 도구들:
Spring Data JPA:
Repository 인터페이스만 만들면 자동 구현findByName(name) 같은 메서드 이름으로 쿼리 자동 생성public interface ItemRepository extends JpaRepository<Item, Long> {
List<Item> findByItemNameContaining(String keyword);
// ↑ 구현체는 Spring이 자동 생성
}
Querydsl:
queryFactory
.selectFrom(item)
.where(item.price.gt(1000)
.and(item.itemName.contains(keyword)))
.fetch();
실무 조합: Spring Data JPA + Querydsl (가장 일반적)
자기 점검
목표: 자바 객체를 DB 테이블과 매핑하는 어노테이션 4종을 손에 익힌다.
선수 지식: Phase 3
핵심 개념
@Entity
public class Item {
// ...
}
@Entity: 이 클래스가 JPA가 관리하는 객체임을 표시@Entity 가 붙은 객체 = 엔티티(Entity)엔티티의 조건:
@Entity 어노테이션자기 점검
선수 지식: Unit 4.1
핵심 개념
@Entity
public class Item {
@Id
private Long id;
// ...
}
@Id: 테이블의 PK 컬럼과 매핑@Id 가 반드시 1개 (혹은 복합 키)자기 점검
@Id 없이 엔티티를 만들면?@IdClass, @EmbeddedId)선수 지식: Unit 4.2
핵심 개념
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
4가지 생성 전략:
| 전략 | 동작 | 적합 DB |
|---|---|---|
| IDENTITY | DB의 auto_increment 사용 | MySQL, PostgreSQL |
| SEQUENCE | DB 시퀀스 객체 사용 | Oracle, PostgreSQL |
| TABLE | 키 생성용 테이블 사용 | 모든 DB (성능 ↓) |
| AUTO | DB에 따라 자동 선택 | (기본값) |
MySQL에서 IDENTITY 의미:
자기 점검
선수 지식: Unit 4.3
핵심 개념
@Column(name = "item_name", length = 10)
private String itemName;
주요 옵션:
name: DB 컬럼명 (지정 안 하면 필드명 그대로)length: 문자열 최대 길이 (DDL 생성 시 활용)nullable: NULL 허용 여부unique: UNIQUE 제약columnDefinition: 컬럼 DDL 직접 작성예시:
@Column(name = "user_email", length = 100, nullable = false, unique = true)
private String email;
자기 점검
@Column 옵션이 DB 스키마에 어떻게 반영되는가? (힌트: ddl-auto 설정)length만 지정하면 다른 옵션은 어떻게 되는가? (힌트: 기본값)선수 지식: Unit 4.4
핵심 개념
Spring Boot의 JPA 통합:
itemName (camelCase)item_name (snake_case)즉:
@Column(name = "item_name") // 명시적
private String itemName;
// 또는
private String itemName; // 자동으로 item_name으로 매핑
→ Spring Boot 환경에서 @Column(name = "item_name") 은 생략 가능.
자기 점검
목표: "왜 @Transactional 이 필요한가" 라는 질문에 직접 코드로 답할 수 있게 된다.
선수 지식: 6주차 Phase 6 (트랜잭션 ACID)
핵심 시나리오
계좌 이체:
원자성 보장 조건:
public void transfer(Connection conn, ...) { // ← Connection이 매개변수
accountDao.withdraw(conn, fromId, amount);
accountDao.deposit(conn, toId, amount);
}
→ 비즈니스 로직이 Connection을 신경 써야 함 = 추상화 깨짐
자기 점검
선수 지식: Unit 5.1
핵심 코드
public void performTransaction() throws SQLException {
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false); // 트랜잭션 시작
// 비즈니스 로직 1
PreparedStatement ps1 = conn.prepareStatement("INSERT ...");
ps1.executeUpdate();
// 비즈니스 로직 2
PreparedStatement ps2 = conn.prepareStatement("UPDATE ...");
ps2.executeUpdate();
conn.commit(); // 모두 성공하면 커밋
} catch (Exception e) {
if (conn != null) conn.rollback(); // 실패 시 롤백
throw e;
} finally {
if (conn != null) conn.close(); // 자원 해제
}
}
3가지 함정:
1. 트랜잭션 누수: setAutoCommit, commit, rollback 코드 매번 반복
2. 예외 누수: SQLException이 비즈니스 로직까지 전파
3. JDBC 반복: Connection 생성·close 코드 반복
→ 6주차의 JdbcTemplate은 부분적 해결, 트랜잭션은 그대로 남음
자기 점검
목표: Spring이 트랜잭션을 어떻게 추상화했는지를 본다. 5주차의 DataSource와 같은 사상.
선수 지식: Phase 5
핵심 개념
"스프링 트랜잭션 처리의 중심 인터페이스"
핵심 메서드:
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition def);
void commit(TransactionStatus status);
void rollback(TransactionStatus status);
}
역할:
자기 점검
선수 지식: Unit 6.1
핵심 구현체
| 구현체 | 사용처 |
|---|---|
| DataSourceTransactionManager | JDBC, JdbcTemplate, MyBatis |
| HibernateTransactionManager | Hibernate (직접 사용) |
| JpaTransactionManager | JPA |
상황별 선택:
Spring Boot의 자동 구성:
spring-boot-starter-data-jpa → JpaTransactionManager 자동 설정자기 점검
@Transactional("name"))선수 지식: Unit 6.2
Before (수동 관리):
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
try {
userDao.insertUser(conn, "John");
accountDao.updateBalance(conn, 1, -100);
conn.commit();
} catch (Exception e) {
conn.rollback();
throw e;
} finally {
conn.close();
}
After (PlatformTransactionManager):
TransactionStatus status = transactionManager.getTransaction(
new DefaultTransactionDefinition()
);
try {
userDao.insertUser("John"); // Connection 안 보임
accountDao.updateBalance(1, -100); // Connection 안 보임
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
얻은 것:
아직 남은 것:
자기 점검
목표: 트랜잭션 코드의 보일러플레이트를 어노테이션 한 줄로 줄이는 마지막 단계. AOP의 첫 만남.
선수 지식: Phase 6, 5주차 디자인 패턴
핵심 개념
프록시 패턴:
"원본 객체의 대리자(Proxy)를 만들어서, 원본 호출 전후에 추가 작업을 끼워 넣는 패턴"
[클라이언트] ──> [Proxy] ──┐
│ 트랜잭션 시작
↓
[원본 객체.메서드()]
│
│ 커밋/롤백
↓
┘
Spring의 활용:
@Transactional 이 붙은 메서드가 있는 빈은자기 점검
선수 지식: Unit 7.1
핵심 개념
선언적 트랜잭션:
@Service
public class TransferService {
@Transactional
public void transfer(Long fromId, Long toId, int amount) {
accountDao.withdraw(fromId, amount);
accountDao.deposit(toId, amount);
}
}
한 줄 어노테이션으로 다음이 자동:
프록시가 하는 일 (의사코드):
public void transfer(Long fromId, Long toId, int amount) {
TransactionStatus status = txManager.getTransaction(...);
try {
targetService.transfer(fromId, toId, amount); // 실제 메서드
txManager.commit(status);
} catch (Exception e) {
txManager.rollback(status);
throw e;
}
}
핵심 통찰:
자기 점검
선수 지식: Unit 7.2
핵심 함정 5가지:
@Service
public class MyService {
@Transactional
private void internal() { } // ❌ 프록시가 못 가로챔
}
@Service
public class MyService {
public void outer() {
this.inner(); // ❌ 프록시 거치지 않음 → 트랜잭션 안 걸림
}
@Transactional
public void inner() { ... }
}
@Transactional
public void method() throws Exception { // 체크 예외
throw new IOException(); // ❌ 롤백 안 됨!
}
// 해결
@Transactional(rollbackFor = Exception.class)
REQUIRED (기본): 기존 트랜잭션 있으면 참여, 없으면 새로 시작REQUIRES_NEW: 항상 새 트랜잭션NESTED: 중첩 트랜잭션 (Savepoint)@Transactional(readOnly = true)
public List<Item> findAll() { ... }
자기 점검
@Transactional(readOnly = true) 의 효과는?이번 자료 도입부의 "스프링 빈이란?"은 5주차 Phase 8(ApplicationContext + DI)의 복습 포인트입니다. 다음 3가지를 다시 짚고 가시면 충분해요:
반드시 깊이 파기:
[ ] Phase 1 — SQL JOIN (Unit 1.1~1.5)
[ ] Phase 2 — ORM 패러다임 (Unit 2.1~2.2)
[ ] Phase 3 — JPA 입문 (Unit 3.1~3.4)
[ ] Phase 4 — JPA 엔티티 매핑 (Unit 4.1~4.5)
[ ] Phase 5 — 수동 트랜잭션의 한계 (Unit 5.1~5.2)
[ ] Phase 6 — PlatformTransactionManager (Unit 6.1~6.3)
[ ] Phase 7 — @Transactional (Unit 7.1~7.3)
[ ] 종합 자기 점검 24문항 통과
6주차에서 본 추상화의 사상이 이번 주에 두 번 더 반복됩니다:
| 6주차 | 7주차 |
|---|---|
| DataSource (커넥션 추상화) | PlatformTransactionManager (트랜잭션 추상화) |
| JdbcTemplate (반복 제거) | @Transactional (보일러플레이트 제거) |
| SQL 매핑 RowMapper | JPA 어노테이션 매핑 |
→ "Spring은 끊임없이 추상화한다" — 6, 7주차의 일관된 메시지.