F-LAB JAVA · 5주차 · Phase 3 · 전통 DAO의 문제
🌱 Part B 시작 — 토비의 스프링: 객체 설계의 진화
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
DAO (Data Access Object) 는 데이터베이스 접근·조작 기능만 전담하는 객체로, 비즈니스 로직과 데이터 접근 로직을 분리하는 패턴이다.
DAO 는 SQL 실행·연결 관리·결과 매핑 같은 데이터 접근 책임만 담당하고, 비즈니스 로직 (계산·검증·흐름) 은 별도 계층 (서비스) 이 담당하도록 분리한다.
비즈니스 로직 안에 SQL 을 직접 박으면, 비즈니스 규칙과 데이터 접근이 뒤섞여 한쪽 변경이 다른 쪽에 영향을 주고, 테스트·재사용·유지보수가 어려워진다.
DAO 는 1990년대부터 자바 진영의 표준 패턴으로, 계층형 아키텍처에서 Controller → Service → DAO → DB 의 데이터 접근 계층에 위치한다.
이 Unit 부터 시작하는 토비의 스프링 여정은 "DAO 코드 한 줄이 어떻게 Spring 의 IoC/DI 까지 진화하는가" 를 따라가며, DAO 의 책임 분리가 그 출발점이다.
DAO = 식당의 창고 담당:
역할 분담:
손님 응대 (Controller):
- 주문 받기
요리사 (Service):
- 비즈니스 로직 (조리)
창고 담당 (DAO):
- 재료 가져오기 (DB 접근)
- 재료 넣기 (저장)
DAO 분리:
- 창고 담당은 "재료 입출고" 만
- 요리는 요리사 (분리)
SQL 박으면 (분리 X):
- 요리사가 직접 창고 정리 + 요리
- 창고 구조 바뀌면 요리법도 수정
- 뒤죽박죽
DAO 분리하면:
- 창고 구조 (DB) 바뀌어도
- 요리사 (비즈니스) 영향 X
- 각자 책임
→ DAO = DB 접근 전담 (비즈니스와 분리), 계층 분리의 핵심.
1. DAO의 정의
2. DAO가 분리하는 책임
3. SQL을 박으면 생기는 문제
4. DAO 패턴의 역사
5. 계층 구조에서의 위치
6. DAO vs Repository
7. DAO의 장점
8. 인터페이스와 구현 분리
9. 면접 + 자기 점검
DAO (Data Access Object):
데이터베이스 접근·조작 기능만
전담하는 객체.
핵심:
- DB 접근 전담
- 비즈니스 로직과 분리
DAO 책임:
- SQL 실행
- 연결 관리
- 결과 → 객체 매핑
- CRUD (Create/Read/Update/Delete)
데이터 접근만 (비즈니스 X)
// DAO 기본 형태
public class ShipmentDao {
public void add(Shipment shipment) {
// DB 저장 (INSERT)
}
public Shipment get(Long id) {
// DB 조회 (SELECT)
return null;
}
public void update(Shipment shipment) {
// DB 수정 (UPDATE)
}
public void delete(Long id) {
// DB 삭제 (DELETE)
}
}
// DB 접근 메서드만
분리의 의미:
비즈니스 로직:
- 운임 계산
- 검증
- 흐름 제어
데이터 접근 (DAO):
- DB 저장/조회
- SQL
→ 각자 책임
// ILIC 의 ShipmentDao
public class ShipmentDao {
// DB 접근만 전담
public void add(Shipment shipment) {
// INSERT INTO shipments ...
}
public Shipment get(Long shipmentId) {
// SELECT * FROM shipments WHERE id = ?
return null;
}
public List<Shipment> findByStatus(String status) {
// SELECT * FROM shipments WHERE status = ?
return List.of();
}
}
// 비즈니스 로직은 Service 가
public class ShipmentService {
private final ShipmentDao shipmentDao;
public ShipmentService(ShipmentDao dao) {
this.shipmentDao = dao;
}
public void processShipment(Shipment shipment) {
// 비즈니스 로직 (검증, 계산)
validate(shipment);
calculateFreight(shipment);
// DB 접근은 DAO 에 위임
shipmentDao.add(shipment);
}
private void validate(Shipment s) { }
private void calculateFreight(Shipment s) { }
}
DAO의 정의는?
답:
1. 정의:
책임:
분리:
형태:
DAO 가 분리하는 책임:
비즈니스 로직 책임:
- 도메인 규칙
- 계산, 검증
- 흐름 제어
데이터 접근 책임 (DAO):
- DB 연결
- SQL 실행
- 결과 매핑
왜 분리:
관심사가 다름:
- 비즈니스: 도메인 (운임 정책)
- 데이터: 기술 (MySQL, SQL)
변경 이유 다름:
- 정책 변경 vs DB 변경
// ❌ 분리 전 (혼재)
public class ShipmentService {
public void processShipment(Shipment shipment) {
// 비즈니스 + DB 접근 혼재
if (shipment.getWeight() == null) { // 비즈니스
throw new IllegalArgumentException();
}
BigDecimal freight = calculate(shipment); // 비즈니스
// DB 접근 (혼재)
Connection c = DriverManager.getConnection(...);
PreparedStatement ps = c.prepareStatement("INSERT ...");
ps.executeUpdate();
// 비즈니스 메서드에 SQL
}
}
// ✓ 분리 후
public class ShipmentService {
private final ShipmentDao shipmentDao;
public void processShipment(Shipment shipment) {
// 비즈니스만
validate(shipment);
BigDecimal freight = calculate(shipment);
shipment.setFreight(freight);
// DB 접근은 DAO
shipmentDao.add(shipment);
}
}
// 책임 분리 명확히
public class ShipmentService {
private final ShipmentDao shipmentDao;
private final FreightCalculator freightCalculator;
public ShipmentService(ShipmentDao dao, FreightCalculator calc) {
this.shipmentDao = dao;
this.freightCalculator = calc;
}
public void createBooking(Shipment shipment) {
// === 비즈니스 책임 ===
validateShipment(shipment);
BigDecimal freight = freightCalculator.calculate(shipment);
shipment.setFreight(freight);
shipment.setStatus("BOOKED");
// === 데이터 접근 책임 (DAO 위임) ===
shipmentDao.add(shipment);
}
private void validateShipment(Shipment s) {
// 비즈니스 검증
}
}
DAO가 분리하는 책임은?
답:
1. 두 책임:
왜 분리:
혼재 시:
분리:
비즈니스에 SQL 박으면:
1. 책임 혼재
2. 변경 영향 확산
3. 재사용 어려움
4. 테스트 어려움
5. 중복
책임 혼재:
비즈니스 메서드에:
- 도메인 규칙
- SQL
- 연결 관리
→ 한 메서드가 여러 책임
→ SRP 위반
변경 영향 확산:
DB 변경 (MySQL → Oracle):
- SQL 박힌 모든 메서드 수정
- 비즈니스 코드 건드림
정책 변경:
- SQL 도 영향 (혼재)
재사용 어려움:
비즈니스에 SQL 박히면:
- 같은 데이터 접근 재사용 X
- 메서드마다 SQL 중복
- DRY 위반
테스트 어려움:
비즈니스 + DB 혼재:
- 비즈니스 테스트에 DB 필요
- 단위 테스트 어려움
- Mock 불가 (분리 안 됨)
분리하면:
- DAO Mock
- 비즈니스 단독 테스트
@Service
public class SqlEmbeddedProblem {
// ❌ SQL 박힌 비즈니스 (문제투성이)
public BigDecimal calculateAndSaveFreight(Shipment shipment)
throws SQLException {
// 비즈니스
BigDecimal freight = shipment.getWeight().multiply(BigDecimal.TEN);
// DB 접근 (혼재 — 문제)
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password");
PreparedStatement ps = c.prepareStatement(
"UPDATE shipments SET freight = ? WHERE id = ?");
ps.setBigDecimal(1, freight);
ps.setLong(2, shipment.getId());
ps.executeUpdate();
ps.close();
c.close();
return freight;
// 문제:
// - 비즈니스 + SQL 혼재
// - DB 변경 시 수정
// - 테스트 어려움 (DB 필요)
// - 다른 메서드도 같은 연결 코드 중복
}
}
비즈니스 로직에 SQL을 박으면 어떤 문제가?
답:
1. 책임 혼재:
변경 영향:
재사용 X:
테스트 어려움:
DAO 패턴 역사:
1990년대:
- 자바 진영 표준 패턴
- J2EE 핵심 패턴
목적:
- 데이터 접근 추상화
- 비즈니스 분리
J2EE Core Patterns:
DAO 가 핵심 패턴:
- 데이터 소스 추상화
- 영속성 메커니즘 은닉
→ 표준화
DAO 진화:
순수 JDBC DAO:
- Connection, PreparedStatement
→ Spring JdbcTemplate:
- 반복 제거
→ JPA/Hibernate:
- ORM
→ Spring Data Repository:
- 인터페이스만
현재:
Spring Data JPA:
- Repository 인터페이스
- 메서드 이름으로 쿼리
- 구현 자동 생성
DAO 개념은 여전히 유효
// 순수 JDBC DAO (전통, 학습용)
public class ShipmentDaoJdbc {
public void add(Shipment shipment) {
// Connection, PreparedStatement (전통)
}
}
// Spring Data JPA Repository (현대)
public interface ShipmentRepository
extends JpaRepository<Shipment, Long> {
// 메서드 이름으로 쿼리 자동 생성
List<Shipment> findByStatus(String status);
List<Shipment> findByBlNo(String blNo);
// 구현 자동 (인터페이스만)
}
// DAO 개념은 동일 (데이터 접근 분리)
// 구현 방식만 진화
DAO 패턴의 역사는?
답:
1. 역사:
J2EE:
진화:
현재:
계층형 아키텍처:
Controller (표현)
↓
Service (비즈니스)
↓
DAO/Repository (데이터 접근)
↓
DB
각 계층:
Controller:
- 요청/응답
- HTTP
Service:
- 비즈니스 로직
- 트랜잭션
DAO:
- 데이터 접근
- SQL
DB:
- 저장소
의존 방향:
Controller → Service → DAO → DB
- 위에서 아래로
- 단방향
- 하위 계층 모름 (상위)
DAO 위치:
Service 와 DB 사이:
- Service 가 DAO 호출
- DAO 가 DB 접근
→ 데이터 접근 계층
// 계층형 구조
// 1. Controller (표현)
@RestController
public class ShipmentController {
private final ShipmentService service;
@PostMapping("/shipments")
public ResponseEntity<?> create(@RequestBody Shipment shipment) {
service.createBooking(shipment); // Service 호출
return ResponseEntity.ok().build();
}
}
// 2. Service (비즈니스)
@Service
public class ShipmentService {
private final ShipmentDao shipmentDao;
public void createBooking(Shipment shipment) {
validate(shipment); // 비즈니스
shipmentDao.add(shipment); // DAO 호출
}
private void validate(Shipment s) { }
}
// 3. DAO (데이터 접근)
public class ShipmentDao {
public void add(Shipment shipment) {
// DB 접근
}
}
// Controller → Service → DAO → DB
계층 구조에서 DAO의 위치는?
답:
1. 계층:
역할:
의존 방향:
위치:
DAO vs Repository:
DAO:
- 데이터 접근 추상화
- DB 중심 (테이블)
- CRUD
Repository:
- 도메인 객체 컬렉션 추상화
- 도메인 중심 (DDD)
- 객체처럼 다룸
DAO 관점:
- "DB 에 접근하는 객체"
- 테이블 단위
- SQL/CRUD 중심
- 기술적
add, get, update, delete
Repository 관점 (DDD):
- "도메인 객체 컬렉션처럼"
- 애그리거트 단위
- 컬렉션 인터페이스 (save, findById)
- 도메인 중심
마치 메모리 컬렉션처럼
실무에서:
종종 혼용:
- Spring Data JpaRepository
- 이름은 Repository
- 역할은 DAO 와 유사
엄밀히:
- Repository: DDD 개념
- DAO: 데이터 접근 패턴
→ 맥락에 따라
// DAO 스타일 (기술 중심)
public class ShipmentDao {
public void insert(Shipment shipment) { } // DB INSERT
public Shipment selectById(Long id) { return null; } // SELECT
public void updateStatus(Long id, String status) { } // UPDATE
}
// Repository 스타일 (도메인 중심, DDD)
public interface ShipmentRepository {
void save(Shipment shipment); // 도메인 저장
Optional<Shipment> findById(Long id); // 도메인 조회
List<Shipment> findAll(); // 컬렉션처럼
}
// Spring Data JPA (혼용)
public interface ShipmentJpaRepository
extends JpaRepository<Shipment, Long> {
// 이름은 Repository, 역할은 데이터 접근
}
DAO와 Repository의 차이는?
답:
1. DAO:
Repository:
DAO:
Repository:
DAO 장점:
1. 관심사 분리
- 비즈니스 vs 데이터
2. 유지보수
- DB 변경 격리
3. 재사용
- 데이터 접근 공유
4. 테스트
- Mock 가능
5. 일관성
- 데이터 접근 표준화
관심사 분리:
- 비즈니스 로직 깨끗
- 데이터 접근 별도
- 각자 집중
유지보수:
DB 변경:
- DAO 만 수정
- 비즈니스 영향 X
→ 변경 격리
// 테스트 — DAO Mock
@Test
void testBusinessLogic() {
ShipmentDao mockDao = mock(ShipmentDao.class);
ShipmentService service = new ShipmentService(mockDao);
// 비즈니스 로직만 테스트 (DB 불필요)
service.createBooking(shipment);
verify(mockDao).add(shipment);
}
// DAO 분리 → Mock → 단위 테스트
@Service
public class DaoBenefits {
private final ShipmentDao shipmentDao;
public DaoBenefits(ShipmentDao dao) {
this.shipmentDao = dao;
}
// 1. 관심사 분리 (비즈니스만)
public void process(Shipment shipment) {
validate(shipment); // 비즈니스
shipmentDao.add(shipment); // 데이터 접근 위임
}
// 2. 재사용 (여러 서비스가 같은 DAO)
// ShipmentService, BookingService 등이 ShipmentDao 공유
// 3. 테스트 (Mock 가능)
// ShipmentDao 를 Mock 으로 → 비즈니스 단독 테스트
// 4. DB 변경 격리
// MySQL → PostgreSQL: DAO 만 수정
private void validate(Shipment s) { }
}
DAO의 장점은?
답:
1. 관심사 분리:
유지보수:
재사용:
테스트:
// DAO 인터페이스
public interface ShipmentDao {
void add(Shipment shipment);
Shipment get(Long id);
}
// 구현
public class JdbcShipmentDao implements ShipmentDao {
public void add(Shipment shipment) {
// JDBC 구현
}
public Shipment get(Long id) {
// JDBC 구현
return null;
}
}
인터페이스 분리 이유:
- 구현 교체 가능 (JDBC → JPA)
- 테스트 (Mock 구현)
- 의존성 역전 (DIP)
- 결합도 ↓
// 인터페이스 의존 → 구현 교체 자유
public class ShipmentService {
private final ShipmentDao dao; // 인터페이스
public ShipmentService(ShipmentDao dao) {
this.dao = dao; // 어떤 구현이든
}
}
// JDBC 구현
new ShipmentService(new JdbcShipmentDao());
// JPA 구현
new ShipmentService(new JpaShipmentDao());
// Mock (테스트)
new ShipmentService(mockDao);
DIP (의존 역전) 연결:
Service 는 인터페이스에 의존:
- 구체 구현 모름
- 추상화에 의존
→ 다음 Phase 들의 핵심
→ IoC/DI 로 발전
// 인터페이스
public interface ShipmentDao {
void add(Shipment shipment);
Shipment get(Long id);
List<Shipment> findByStatus(String status);
}
// JDBC 구현
public class JdbcShipmentDao implements ShipmentDao {
public void add(Shipment shipment) { /* JDBC */ }
public Shipment get(Long id) { return null; }
public List<Shipment> findByStatus(String status) { return List.of(); }
}
// JPA 구현 (교체 가능)
public class JpaShipmentDao implements ShipmentDao {
public void add(Shipment shipment) { /* JPA */ }
public Shipment get(Long id) { return null; }
public List<Shipment> findByStatus(String status) { return List.of(); }
}
// Service 는 인터페이스에 의존 (구현 무관)
public class ShipmentService {
private final ShipmentDao dao; // 인터페이스
public ShipmentService(ShipmentDao dao) { this.dao = dao; }
}
// → 이 분리가 IoC/DI 의 토대 (Phase 6~8)
DAO 인터페이스와 구현 분리는?
답:
1. 분리:
이유:
교체:
DIP:
| Q | 핵심 답변 |
|---|---|
| DAO? | DB 접근 전담 객체 |
| 분리 책임? | 비즈니스 vs 데이터 |
| SQL 박으면? | 혼재, 변경 영향 |
| 역사? | 1990년대 J2EE |
| 계층 위치? | Service 와 DB 사이 |
| DAO vs Repository? | 기술 vs 도메인(DDD) |
| 장점? | 분리/유지보수/테스트 |
| 인터페이스 분리? | 구현 교체 |
| DIP 연결? | 추상화 의존 |
| Spring 진화? | DAO 가 출발점 |
답:
답:
답:
답:
답:
1. DAO
2. 분리 이유
3. 진화의 출발점
이번 Unit에서 DAO 개념을 봤다면, 다음은 전통 DAO 코드 (문제 진단).
🌱 Phase 3 — 전통 DAO의 문제
✅ Unit 3.1 DAO란 무엇인가 ← 여기
⏭ Unit 3.2 전통 DAO의 코드
⏭ Unit 3.3 책임 혼재
✅ Part A — 동시성 마무리 (7 Unit)
🌱 Part B — 토비의 스프링
Phase 3 — 전통 DAO (1/3 진행)
총: 8/26 Unit
🌱 Part B 시작 — 토비의 스프링: 객체 설계의 진화