F-LAB JAVA · 6주차 · Phase 3 · JDBC 표준화의 등장
🏆 Phase 3 완주 — JDBC 의 한계와 상위 프레임워크의 등장
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
JDBC 는 API 는 통일했지만 SQL 문법 차이 (LIMIT vs ROWNUM, AUTO_INCREMENT vs SEQUENCE) 는 그대로 두었기에 DB 마다 SQL 을 다르게 써야 하며, 이를 해결하기 위해 JdbcTemplate (반복 제거), MyBatis (SQL 외부화), JPA/Hibernate (Dialect 로 SQL 자동 생성) 같은 상위 프레임워크가 등장했다.
JDBC 는 연결·실행·결과 처리는 통일했지만 SQL 문법 차이는 그대로 두었다.
대표 차이는 (1) LIMIT (MySQL) vs ROWNUM/OFFSET FETCH (Oracle), (2) AUTO_INCREMENT (MySQL) vs SEQUENCE (Oracle), (3) 페이징 처리 방식 등이다.
또 JDBC 는 매 쿼리마다 try/catch/finally 자원 관리 코드를 반복해야 하는 부담도 남겨 두었다.
이를 해결하기 위해 상위 프레임워크가 등장했다 — JdbcTemplate (Spring) 은 JDBC 반복 코드를 제거하고, MyBatis 는 SQL 을 XML 로 분리해 DB 별 분기를 가능하게 하며, JPA/Hibernate 는 Dialect (방언) 로 SQL 자체를 자동 생성해 SQL 문법 차이를 거의 다룰 필요가 없게 한다.
JDBC 가 해결하지 않은 것 = 전압 차이:
JDBC = 콘센트 표준화 (Unit 3.2):
- 플러그 모양 통일 (API)
- 어디서든 꽂힘
JDBC 가 안 한 것 = 전압:
- 한국 220V, 미국 110V (SQL 문법)
- 꽂혀도 동작 다름
SQL 문법 차이:
- LIMIT 10 (MySQL, "10개")
- ROWNUM (Oracle, 다른 문법)
- 같은 일, 다른 표현
JDBC 반복:
- 매번 try/catch/finally
- 자원 관리 보일러플레이트
해결 (상위 프레임워크):
- JdbcTemplate (반복 제거)
- MyBatis (SQL 외부화)
- JPA/Hibernate (Dialect, SQL 자동)
- 220V/110V 자동 변환 어댑터
→ JDBC = API 통일 (콘센트), SQL 차이 + 반복 남음 → 상위 프레임워크 (Dialect/템플릿).
1. JDBC의 한계
2. SQL 문법 차이 (LIMIT vs ROWNUM)
3. 자동 증가/페이징 차이
4. JDBC 반복 코드 부담
5. 상위 프레임워크 등장
6. JdbcTemplate
7. MyBatis
8. JPA/Hibernate (Dialect)
9. Phase 3 완주 정리
JDBC 한계:
1. SQL 문법 차이 그대로
2. 반복 코드 (try/catch/finally)
3. ResultSet → 객체 매핑 수동
4. SQL 문자열 (오타 위험)
JDBC 통일 vs 미통일:
통일 (3.2):
- 연결/실행/결과 API
- DB 무관 코드
미통일 (3.3):
- SQL 문법 (DB별)
- 코드 반복 부담
- 매핑 부담
왜 SQL 은 통일 X:
SQL 자체가:
- DB 별 방언 (Dialect)
- 표준 (ANSI) 있지만 차이
- JDBC 는 API 만
→ SQL 은 사용자 책임
JDBC 한계 (ILIC)
ILIC 가 MySQL → Oracle (JDBC 사용):
✓ 통일된 부분:
- DriverManager.getConnection (OK)
- prepareStatement (OK)
- ResultSet (OK)
→ 코드 동일
❌ 통일 안 된 부분:
- SQL: LIMIT → ROWNUM 변경 필요
- AUTO_INCREMENT → SEQUENCE
- 페이징 쿼리 재작성
+ 모든 메서드에 try/catch/finally 반복
+ ResultSet → Shipment 객체 매핑 매번
→ JDBC 가 충분치 않음 (상위 필요)
JDBC의 한계는?
답:
1. 한계:
통일/미통일:
왜:
결과:
페이징 문법 차이:
MySQL/PostgreSQL:
SELECT * FROM shipments LIMIT 10 OFFSET 20;
Oracle (구버전):
SELECT * FROM (
SELECT a.*, ROWNUM rn FROM (...) a
WHERE ROWNUM <= 30
) WHERE rn > 20;
Oracle 12c+ / SQL Server:
SELECT * FROM shipments
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
같은 일, 다른 문법:
의도: 21~30번째 행
MySQL: LIMIT 10 OFFSET 20
Oracle 구: ROWNUM 서브쿼리
Oracle 신: OFFSET FETCH
→ DB 별 학습 필요
DB 이식성 문제:
DB 교체:
- SQL 재작성
- 페이징 부분 다시
- 테스트
→ 코드 변경 필요
// SQL 문법 차이 (ILIC)
public class ShipmentDao {
// MySQL 페이징
public List<Shipment> findPageMysql(int offset, int size)
throws Exception {
String sql = "SELECT * FROM shipments " +
"ORDER BY id LIMIT ? OFFSET ?";
// MySQL 문법
return null;
}
// Oracle 페이징 (다른 SQL)
public List<Shipment> findPageOracle(int offset, int size)
throws Exception {
String sql = "SELECT * FROM (" +
" SELECT a.*, ROWNUM rn FROM (" +
" SELECT * FROM shipments ORDER BY id" +
" ) a WHERE ROWNUM <= ?" +
") WHERE rn > ?";
// Oracle 구버전 (복잡)
return null;
}
// 같은 페이징, SQL 두 벌 (JDBC 만으론 통일 X)
}
class Shipment {}
SQL 문법 차이 (LIMIT vs ROWNUM) 는?
답:
1. 페이징:
같은 일:
이식성:
DB 별:
자동 증가:
MySQL/PostgreSQL:
CREATE TABLE ...(
id BIGINT AUTO_INCREMENT PRIMARY KEY
);
Oracle:
CREATE SEQUENCE shipment_seq START WITH 1;
INSERT INTO ...(id, ...) VALUES (shipment_seq.NEXTVAL, ...);
SQL Server:
id BIGINT IDENTITY(1,1) PRIMARY KEY
ID 생성 방식 차이:
MySQL: AUTO_INCREMENT (테이블 옵션)
Oracle: SEQUENCE (별도 객체)
SQL Server: IDENTITY (컬럼 옵션)
→ DB 마다 다름
기타 SQL 차이:
- 날짜/시간 함수
- MySQL: NOW(), CURDATE()
- Oracle: SYSDATE
- 문자열 결합
- MySQL: CONCAT()
- Oracle: ||, CONCAT()
- NULL 처리
- MySQL: IFNULL()
- Oracle: NVL()
JDBC 표준화 시도:
Statement.RETURN_GENERATED_KEYS:
- 자동 키 조회 통일
- 일부 표준화
→ 하지만 SQL 자체는 그대로
-- MySQL (ILIC)
CREATE TABLE shipments (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
bl_no VARCHAR(50),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Oracle (다른 문법)
CREATE TABLE shipments (
id NUMBER(19) PRIMARY KEY,
bl_no VARCHAR2(50),
created_at DATE DEFAULT SYSDATE
);
CREATE SEQUENCE shipment_seq START WITH 1;
-- INSERT 도 다름
-- MySQL: INSERT INTO shipments(bl_no) VALUES('BL001'); (자동 ID)
-- Oracle: INSERT INTO shipments(id, bl_no)
-- VALUES(shipment_seq.NEXTVAL, 'BL001');
-- → ILIC 가 멀티 DB 지원 시 SQL 차이 부담
-- → ORM/MyBatis 등 도움 필요
자동 증가 컬럼 문법 차이는?
답:
1. 자동 증가:
ID 생성:
기타:
JDBC:
// JDBC 반복 코드
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
conn = DriverManager.getConnection(url, user, pwd);
stmt = conn.prepareStatement(sql);
stmt.setLong(1, id);
rs = stmt.executeQuery();
while (rs.next()) {
// 결과 처리
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (rs != null) try { rs.close(); } catch (SQLException e) { }
if (stmt != null) try { stmt.close(); } catch (SQLException e) { }
if (conn != null) try { conn.close(); } catch (SQLException e) { }
}
보일러플레이트:
매 쿼리마다 똑같이:
- try/catch/finally
- 자원 해제 (역순)
- null 체크
→ 본 로직 < 인프라 코드
실수 위험:
- close 누락 (자원 누수)
- 예외 처리 누락
- 역순 안 함
→ Connection 누수 발생
5주차 관점:
변하지 않는 흐름 + 변하는 부분:
- 흐름: 연결/해제 (반복)
- 변동: SQL/매핑
→ 템플릿 메소드 / 전략 패턴
→ JdbcTemplate (Phase 7)
// JDBC 반복 코드 (ILIC, 431 API 마다?)
public class ShipmentDao {
public Shipment get(Long id) throws Exception {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
conn = DriverManager.getConnection(url, user, pwd);
stmt = conn.prepareStatement(
"select * from shipments where id = ?");
stmt.setLong(1, id);
rs = stmt.executeQuery();
if (rs.next()) {
Shipment s = new Shipment();
s.setId(rs.getLong("id"));
return s;
}
return null;
} catch (SQLException e) {
throw e;
} finally {
if (rs != null) try { rs.close(); } catch (SQLException e) { }
if (stmt != null) try { stmt.close(); } catch (SQLException e) { }
if (conn != null) try { conn.close(); } catch (SQLException e) { }
}
}
public void add(Shipment s) throws Exception {
// 같은 try/catch/finally 또 반복
}
public void update(Shipment s) throws Exception {
// 또 반복
}
// 431 API 마다 이걸? → JdbcTemplate 필요
String url, user, pwd;
}
class Shipment {
void setId(Long id) {}
}
JDBC 반복 코드 부담은?
답:
1. 반복:
보일러플레이트:
위험:
해결:
상위 프레임워크 등장:
JDBC 의 한계:
- SQL 차이
- 반복 코드
- 매핑 부담
→ 상위 계층 추상화 필요
세 가지 흐름:
1. JdbcTemplate (Spring)
- 반복 코드 제거
- SQL 은 직접
2. MyBatis
- SQL 외부화 (XML)
- DB 별 분기
3. JPA/Hibernate
- ORM (객체 ↔ 테이블)
- SQL 자동 생성
추상화 수준:
JDBC: 저수준
↓
JdbcTemplate: 중간 (SQL 직접)
↓
MyBatis: 중간 (SQL 외부)
↓
JPA: 고수준 (SQL 자동)
선택 기준:
단순/성능: JdbcTemplate
SQL 제어/DB 차이: MyBatis
생산성/객체 중심: JPA
→ 프로젝트 성격
상위 프레임워크 선택 (ILIC)
ILIC = Spring Boot + JPA + JdbcTemplate:
- 일반 CRUD: JPA (Spring Data JPA)
- 객체 중심
- SQL 자동
- 복잡한 통계/리포트: JdbcTemplate
- 성능
- SQL 직접 제어
102 테이블 + 431 API:
- JPA 로 대부분 처리
- 특수 케이스만 JdbcTemplate
- 둘 다 JDBC 위에서 동작
→ 상위 프레임워크가 JDBC 한계 보완
상위 프레임워크 등장 배경은?
답:
1. 등장 배경:
세 흐름:
추상화 수준:
선택:
JdbcTemplate (Spring):
JDBC 반복 코드 제거:
- 자원 관리 자동
- 예외 변환
- SQL 만 작성
JdbcTemplate 동작:
- Connection 자동 (DataSource)
- PreparedStatement 자동
- ResultSet 자동
- 자원 해제 자동
- 예외 → RuntimeException
// JdbcTemplate 사용 (간결)
public List<Shipment> findAll() {
return jdbcTemplate.query(
"select * from shipments",
(rs, rowNum) -> {
Shipment s = new Shipment();
s.setId(rs.getLong("id"));
return s;
}
);
}
// try/catch/finally 없음, 자원 자동
SQL 은 직접:
JdbcTemplate:
- 반복 제거 O
- SQL 자동 X (직접)
- DB 차이 있음
→ Phase 7 에서 상세
// JdbcTemplate (ILIC 일부 사용)
@Repository
public class ShipmentStatisticsDao {
private final JdbcTemplate jdbcTemplate;
public ShipmentStatisticsDao(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// 복잡한 통계 (SQL 직접)
public List<MonthlyStat> monthlyStats() {
String sql = """
SELECT YEAR(created_at) as y, MONTH(created_at) as m,
COUNT(*) as cnt, SUM(weight) as total_weight
FROM shipments
GROUP BY YEAR(created_at), MONTH(created_at)
""";
return jdbcTemplate.query(sql, (rs, n) ->
new MonthlyStat(rs.getInt("y"), rs.getInt("m"),
rs.getInt("cnt"), rs.getBigDecimal("total_weight")));
// 반복 코드 없음 (JdbcTemplate)
// SQL 은 직접 (MySQL 문법)
}
}
record MonthlyStat(int year, int month, int count, java.math.BigDecimal weight) {}
class JdbcTemplate {
<T> java.util.List<T> query(String sql, RowMapper<T> m) { return null; }
interface RowMapper<T> { T mapRow(java.sql.ResultSet rs, int n) throws java.sql.SQLException; }
}
JdbcTemplate은?
답:
1. JdbcTemplate:
동작:
사용:
SQL:
MyBatis:
SQL Mapper:
- SQL 을 XML 외부화
- 객체 ↔ SQL 매핑
- DB 별 분기 가능
<!-- MyBatis XML -->
<mapper namespace="ShipmentMapper">
<select id="findById" resultType="Shipment">
SELECT * FROM shipments WHERE id = #{id}
</select>
</mapper>
<!-- DB 방언 분기 -->
<select id="page" resultType="Shipment">
<if test="dbType == 'mysql'">
SELECT * FROM shipments LIMIT #{size} OFFSET #{offset}
</if>
<if test="dbType == 'oracle'">
SELECT * FROM (...) WHERE ROWNUM <= ...
</if>
</select>
MyBatis 장점:
- SQL 외부화 (가독성)
- SQL 직접 제어
- DB 별 분기
- 동적 SQL (if, foreach)
<!-- MyBatis 예시 (ILIC 가 채택했다면) -->
<mapper namespace="com.ilic.dao.ShipmentMapper">
<select id="findByStatus" resultType="Shipment">
SELECT id, bl_no, status, weight
FROM shipments
WHERE status = #{status}
<if test="orderBy != null">
ORDER BY ${orderBy}
</if>
</select>
<!-- DB 별 분기 -->
<select id="page" resultType="Shipment" databaseId="mysql">
SELECT * FROM shipments LIMIT #{size} OFFSET #{offset}
</select>
<select id="page" resultType="Shipment" databaseId="oracle">
SELECT * FROM (
SELECT a.*, ROWNUM rn FROM (
SELECT * FROM shipments ORDER BY id
) a WHERE ROWNUM <= #{max}
) WHERE rn > #{offset}
</select>
</mapper>
<!-- ILIC: 주로 JPA, 일부 복잡 쿼리는 JdbcTemplate -->
<!-- MyBatis 는 SQL 제어 강한 프로젝트에 -->
MyBatis는?
답:
1. MyBatis:
XML SQL:
분기:
장점:
JPA / Hibernate:
ORM (Object-Relational Mapping):
- 객체 ↔ 테이블 매핑
- SQL 자동 생성
- SQL 거의 안 봄
// JPA 엔티티
@Entity
@Table(name = "shipments")
public class Shipment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "bl_no")
private String blNo;
// ...
}
// 사용 (SQL 없이)
shipmentRepository.save(shipment);
Shipment found = shipmentRepository.findById(1L).orElseThrow();
Dialect (방언):
DB 별 SQL 차이 자동 처리:
- MySQLDialect
- OracleDialect
- PostgreSQLDialect
Hibernate 가:
- 객체 호출 → Dialect 기반 SQL
- DB 별 자동 변환
ORM 이 해결:
SQL 문법 차이:
- Dialect 가 자동
- 페이징/시퀀스 등
- 자동 키 생성
→ "왜 ORM?" 의 답:
JDBC 가 안 한 SQL 차이를
ORM 이 해결
// JPA (ILIC 주력)
@Entity
@Table(name = "shipments")
public class Shipment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
// MySQL: AUTO_INCREMENT, Oracle: SEQUENCE 자동
private Long id;
private String blNo;
private BigDecimal weight;
private String status;
}
// Repository (SQL 자동)
public interface ShipmentRepository extends JpaRepository<Shipment, Long> {
// 메서드 이름 → SQL 자동
List<Shipment> findByStatus(String status);
Page<Shipment> findAll(Pageable pageable); // 페이징 자동 (DB별)
}
// application.yml
// spring:
// jpa:
// properties:
// hibernate:
// dialect: org.hibernate.dialect.MySQLDialect
// # Oracle 이면: OracleDialect
// → JPA 가 DB 차이 자동 처리
// → ILIC 가 102 테이블 관리 가능
// → 복잡 쿼리는 JdbcTemplate 보완
class Pageable {}
class Page<T> {}
interface JpaRepository<T, ID> {
java.util.List<T> findAll();
}
ORM이 SQL 차이 해결하는 방식 (Dialect) 은?
답:
1. ORM:
Dialect:
자동:
해결:
Phase 3 — JDBC 표준화
Unit 3.1 — JDBC 없던 시절의 고통
- DB 고유 API, 재작성
- OCP 위반
Unit 3.2 — JDBC의 표준화 효과
- 자바 DB 표준 API
- 같은 코드, 다른 DB
Unit 3.3 — JDBC가 해결하지 않는 것
- SQL 문법 차이
- 반복 코드
- 상위 프레임워크
Phase 3 핵심 메시지:
"JDBC 가 DB 접근 API 를 통일했지만
SQL 문법 차이와 반복 코드는 남았고,
이를 JdbcTemplate/MyBatis/JPA 같은
상위 프레임워크가 해결했다."
Phase 3 → Phase 4:
- JDBC → Connection Pool
Phase 4 — Connection Pool과 DB 세션 (4.2 ★깊이):
- 매 요청 연결의 비효율
- Connection Pool (★깊이)
- DB 세션과 연결
- DB Lock
| Q | 핵심 답변 |
|---|---|
| JDBC 한계? | SQL 차이/반복/매핑 |
| LIMIT vs ROWNUM? | 페이징 문법 차이 |
| 자동 증가? | AUTO_INCREMENT vs SEQUENCE |
| 반복 코드? | try/catch/finally |
| 상위 프레임워크? | JdbcTemplate/MyBatis/JPA |
| 왜 ORM? | SQL 차이 해결 |
| Dialect? | DB 방언 |
| JdbcTemplate? | 반복 제거 |
| MyBatis? | SQL 외부화 |
| JPA? | 객체-테이블 매핑 |
답:
답:
답:
답:
답:
1. JDBC 의 한계
2. 상위 프레임워크
3. "왜 ORM"의 답
💾 Phase 3 — JDBC 표준화의 등장
✅ Unit 3.1 JDBC 없던 시절의 고통
✅ Unit 3.2 JDBC의 표준화 효과
✅ Unit 3.3 JDBC가 해결하지 않는 것 ← 여기, Phase 3 완주
→ JDBC 전 (구체 강결합, OCP 위반)
→ JDBC (표준 API, 5주차 패턴)
→ JDBC 한계 + 상위 프레임워크
💾 Phase 4 — Connection Pool과 DB 세션
Unit 4.1 — 매 요청마다 연결의 비효율
Unit 4.2 — Connection Pool 개념 ★깊이
Unit 4.3 — DB 세션과 연결 구조
Unit 4.4 — DB Lock 개념
🧪 Part A (9 Unit) ✅
💾 Part B — DB 접근의 진화
✅ Phase 3 — JDBC (3 Unit) ← 완주
⏭ Phase 4 — Connection Pool (4 Unit, 4.2 ★깊이)
총: 12/28 Unit
🏆 Phase 3 완주 — JDBC 의 한계와 상위 프레임워크