7. Applications of Core Spring Technologies

7장 스프링 핵심 기술 응용

7.1. SQL과 DAO의 분리

DAO는 원래 “테이블에서 데이터를 읽고/쓰는 작업”을 캡슐화하려고 존재합니다. 그런데 실제 코드에서는 SQL이 테이블/필드 정보를 고스란히 포함합니다. 이 SQL은 다음 이유로 자주 바뀝니다.

왜 SQL을 DAO에서 분리해야 하는가

  • DB 구조 변경의 직접 영향 컬럼명이 바뀌거나(예: passwordpwd), 하나의 컬럼이 여러 컬럼으로 쪼개지거나, 반대로 합쳐질 수 있습니다. SQL 문자열이 DAO 코드에 들어 있으면 DAO 코드 자체가 수정 대상이 됩니다.

  • 운영 중 SQL 변경 요구 성능 튜닝이나 장애 대응으로 SQL을 조정해야 하는 상황이 생깁니다. 그때마다 DAO 코드를 고치고 다시 빌드/배포하는 흐름은 번거롭고 위험합니다.

  • 실수 탐지의 어려움 SQL은 문자열이라 “컴파일 타임 검증”이 어렵습니다. 특히 XML/문자열로 옮기면 오타가 더 늘 수 있고, 실행 전까지 확인하기가 어렵습니다. 그래서 테스트(예: UserDaoTest) 로 SQL 정상 동작을 조기에 검증해야 합니다.

  • 역할 분리(관심사의 분리) DAO의 핵심 책임은 “어떤 SQL을 쓸지 결정”하는 것이 아니라 “필요한 데이터를 조회/갱신하는 기능 제공”입니다. SQL의 저장/관리/로딩 방식까지 DAO가 떠안으면 책임이 섞입니다.

7.1.1 XML 설정을 이용한 분리

가장 단순한 방법은 SQL을 스프링 XML 설정으로 빼고, DAO는 DI로 주입받아 사용하는 것입니다.

개별 SQL 프로퍼티 방식

  • 각 SQL을 sqlAdd, sqlGet 같은 프로퍼티로 DAO에 주입합니다.

  • SQL이 늘어날수록 프로퍼티/세터가 계속 늘어난다는 단점이 있습니다.

import org.springframework.jdbc.core.JdbcTemplate
import javax.sql.DataSource

class UserDaoJdbc(
    dataSource: DataSource
) {
    private val jdbcTemplate: JdbcTemplate = JdbcTemplate(dataSource)

    // XML에서 주입받는 SQL
    private var sqlAdd: String = ""

    fun setSqlAdd(sqlAdd: String) {
        this.sqlAdd = sqlAdd
    }

    fun add(user: User) {
        jdbcTemplate.update(
            sqlAdd,
            user.id,
            user.name,
            user.password,
            user.email,
            user.level.value,
            user.login,
            user.recommend
        )
    }
}

data class User(
    val id: String,
    val name: String,
    val password: String,
    val email: String,
    val level: Level,
    val login: Int,
    val recommend: Int
)

enum class Level(val value: Int) { BASIC(1), SILVER(2), GOLD(3) }

SQL 맵(Map) 프로퍼티 방식

개별 프로퍼티 방식은 SQL이 늘어날 때마다 DAO에 세터/필드가 계속 추가됩니다. 이를 줄이기 위해 SQL을 Map으로 한 번에 주입합니다.

  • DAO는 sqlMap["add"] 처럼 키로 SQL을 조회해서 사용합니다.

  • 장점: DAO 코드가 단순해지고 SQL 추가 시 DAO 변경이 줄어듭니다.

  • 단점: 키 오타/누락은 실행 전 검증이 어렵습니다. 그래서 테스트가 더 중요해집니다.

7.1.2 SQL 제공 서비스

XML에서 SQL을 DI로 주입하는 방식은 가능하지만, “DAO 설정”과 “SQL 설정”이 한 파일에 섞이면 관리가 지저분해질 수 있습니다. 또한 SQL을 XML이 아닌 다른 포맷/저장소(별도 파일, DB, 외부 서비스 등) 에서 가져오고 싶어질 수도 있습니다.

그래서 책은 한 단계 더 나아가서, DAO가 SQL을 직접 들고 있지 않고 SqlService라는 별도 컴포넌트에게 SQL을 요청하도록 만듭니다.

핵심은 다음과 같습니다.

  • DAO의 관점: “필요한 SQL을 키로 요청해서 받는다.”

  • SqlService의 관점: “SQL을 어디서/어떻게 보관하고 로딩할지 책임진다.”

  • 즉, SQL 저장/로딩 방식이 바뀌어도 DAO는 영향을 덜 받도록 구조를 분리합니다.

SQL 서비스 인터페이스

SQL 조회 실패 예외(RuntimeException)

SQL 조회 실패는 대부분 복구가 어렵다고 보고, 책에서는 런타임 예외로 정의합니다(필요하면 원인(cause)도 담도록 생성자 2개 제공).

DAO는 SqlService만 의존하도록 변경

이제 DAO는 sqlMap 같은 구체 구현 대신 sqlService.getSql("userAdd")처럼 서비스를 통해 SQL을 얻습니다. 또한 키 충돌을 피하려고 userAdd, userGet처럼 “DAO/도메인 이름을 포함한 키”를 쓰는 방향을 제시합니다.

스프링 설정을 사용하는 단순 SQL 서비스(SimpleSqlService)

가장 단순한 구현으로, SimpleSqlService는 내부에 Map<String, String>을 가지고 있다가 키로 SQL을 찾아 반환합니다. 못 찾으면 SqlRetrievalFailureException을 던집니다.

이 단계의 의미

  • XML에서 SQL을 주입하는 방식(7.1.1)은 “SQL을 코드에서 밖으로 빼는 것”이 목적이었다면,

  • SQL 제공 서비스(7.1.2)는 “DAO가 SQL의 저장/로딩 방식에 대해 몰라도 되게 만드는 것”이 목적입니다.

즉, DAO는 키만 알면 되고, SQL을 어디에 두고 어떻게 갱신하는지는 SqlService가 책임지는 구조로 옮겨가며, 이후 더 유연한 SQL 관리(다른 저장소/동적 갱신 등)로 확장할 기반을 만듭니다.

7.2. 인터페이스의 분리와 자기참조 빈

  • 7.2의 핵심은 하나의 클래스에 몰려 있던 책임을 인터페이스 기준으로 분리하는 것입니다.

  • XmlSqlService에는 사실 두 가지 책임이 있었습니다.

    1. SQL을 읽어오는 책임 (Reader)

    2. SQL을 저장하고 조회하는 책임 (Registry)

  • 이 두 책임을 각각 SqlReader, SqlRegistry 인터페이스로 분리하고, SqlService는 이 둘을 협력시키는 상위 인터페이스가 됩니다.

  • 이후 하나의 클래스가 여러 인터페이스를 구현하고, 스프링 DI를 통해 자기 자신을 주입(self reference) 하여 협력 구조를 구성합니다.

  • 마지막에는 기본 의존관계를 내부에서 생성하는 DefaultSqlService를 통해 디폴트 의존관계 전략까지 정리합니다.

SqlService를 도입했지만, 구현 클래스인 XmlSqlService는 여전히 많은 책임을 갖고 있었습니다.

  • XML을 읽는다.

  • JAXB로 변환한다.

  • SQL을 Map에 저장한다.

  • 키로 SQL을 조회한다.

이 상태는 단일 책임 원칙에 어긋날 가능성이 큽니다. 확장 포인트도 부족합니다.

7.2.1. XML 파일 매핑

SQL을 <map> 형태로 XML에 정의하고 JAXB로 매핑합니다.

이 XML 구조를 위한 XSD를 작성하고, xjc로 바인딩 클래스를 생성합니다.

생성되는 대표 클래스:

  • Sqlmap

  • SqlType

JAXB 언마샬링: (또는Jackson / Kotlin Serialization)

여기까지는 “XML → 객체 변환”에 대한 준비 단계.


7.2.2 XML 파일을 이용하는 SQL 서비스

XmlSqlService는 다음 작업을 수행합니다.

  1. XML 파일을 읽는다.

  2. JAXB로 SQL 목록을 변환한다.

  3. 내부 Map에 저장한다.

  4. getSql()로 반환한다.

초기 버전 구조:

이제 SQL은 완전히 XML 파일로 분리되었습니다.


7.2.3 빈의 초기화 작업

XML 파일 이름을 외부에서 주입 가능하도록 변경합니다.

스프링은 @PostConstruct를 통해 DI 이후 초기화 작업을 수행합니다.

스프링 컨테이너 초기화 순서:

  1. 빈 객체 생성

  2. 프로퍼티 주입

  3. @PostConstruct 실행


7.2.4 변화를 위한 준비: 인터페이스 분리

XmlSqlService에는 두 가지 책임이 있습니다.

책임 1: SQL을 읽는 역할

책임 2: SQL을 저장하고 조회하는 역할

이제 SqlService는 이 둘을 사용하는 상위 인터페이스가 됩니다.


다중 인터페이스 구현

하나의 클래스가 세 인터페이스를 모두 구현할 수 있습니다.

이제 책임이 인터페이스 단위로 분리되었습니다.


7.2.5 자기참조 빈으로 시작하기

XmlSqlService

  • SqlReader

  • SqlRegistry

두 인터페이스를 DI 받을 수 있게 합니다.

그리고 스프링 설정에서 자기 자신을 주입합니다.

하나의 빈이지만, 인터페이스 기준으로 보면:

  • SqlService

  • SqlReader

  • SqlRegistry

세 역할을 수행합니다.

이것이 자기참조 빈(self reference bean) 입니다.


7.2.6 디폴트 의존관계

매번 Reader/Registry 빈을 따로 등록하는 것이 번거로울 수 있습니다.

그래서 기본 구현을 내부에서 생성하는 DefaultSqlService를 만듭니다.

XML 설정:

DI 설정 없이도 동작합니다.

하지만 필요하면 교체할 수 있습니다.


구조적 핵심 정리

7.2의 핵심 설계 변화는 다음입니다.

  1. 클래스 단위 분리 → 인터페이스 단위 분리

  2. 구현을 인터페이스 뒤로 숨김

  3. 자기참조 빈을 이용한 다중 역할 구성

  4. 디폴트 의존관계 전략 도입

결과적으로 얻은 것:

  • SQL 읽기 방식 변경 가능

  • SQL 저장 방식 변경 가능

  • SQL 서비스 로직은 그대로 유지

  • DI를 통한 유연한 확장 구조 확보

7.3. 서비스 추상화 적용

  • 7.3의 핵심은 구현 기술(JAXB, Castor 등)에 의존하지 않도록 OXM을 추상화하는 것입니다.

  • 스프링은 OXM(Object XML Mapping)에 대해 Marshaller, Unmarshaller 인터페이스를 제공하여 구현 기술을 교체 가능하게 만듭니다.

  • OxmSqlService를 통해 OXM 기반 SQL 로딩 구조를 적용하고,

  • 내부 전략을 멤버 클래스로 고정하여 설정을 단순화합니다.

  • 마지막으로 Resource 추상화를 도입해 파일 위치(클래스패스, 파일 시스템, HTTP 등)에 독립적인 설계로 확장합니다.

  • 기술이 아니라 “추상화 계층을 어디에 두는가”가 핵심 메시지입니다.

7.3.1. OXM 서비스 추상화

JAXB 외에도 다양한 XML-자바 바인딩 기술이 존재합니다.

대표적으로 다음과 같은 기술이 소개됩니다.

  • Castor XML

  • JiBX

  • XmlBeans

  • XStream

이러한 기술을 통칭하여 OXM(Object XML Mapping)이라고 합니다.

문제는 다음과 같습니다.

  • JAXB API에 직접 의존하면 구현 기술 변경 시 코드 수정이 필요합니다.

  • 기술마다 설정 방식과 API가 다릅니다.

  • 테스트 작성도 기술 의존적이 됩니다.

이를 해결하기 위해 스프링은 OXM 추상 계층을 제공합니다.


OXM 서비스 인터페이스

스프링 OXM의 핵심 인터페이스는 다음 두 가지입니다.

SqlReader는 XML → 객체 변환이 필요하므로 Unmarshaller를 사용합니다.

이 구조를 사용하면 JAXB 대신 Castor를 사용하더라도 Unmarshaller 타입만 유지하면 됩니다.


JAXB 구현 적용

JAXB를 사용하는 경우, 스프링이 제공하는 Jaxb2Marshaller를 사용합니다.

XML 설정 예시는 다음과 같습니다.

OxmSqlService는 이 Unmarshaller를 주입받아 사용합니다.


OXM 테스트 코드 구조

OXM을 이용한 테스트의 핵심은 다음입니다.

  • XML을 Source로 변환

  • unmarshal() 호출

  • 반환 객체 검증

Kotlin 예시:

여기에는 JAXB API가 직접 등장하지 않습니다.

구현 기술을 교체해도 테스트는 그대로 유지됩니다.


Castor로 기술 교체

JAXB 대신 Castor를 사용하려면 설정만 변경하면 됩니다.

애플리케이션 코드는 수정되지 않습니다.

이것이 서비스 추상화의 힘입니다.


7.3.2 OXM 서비스 추상화 적용

이제 OXM을 적용한 OxmSqlService를 만듭니다.

핵심 구조:

  • SqlService 구현

  • 내부에 OxmSqlReader 멤버 클래스 포함

  • Unmarshaller DI

  • SqlRegistry는 기본 구현 사용

구조 예시:


멤버 클래스를 사용하는 이유

OxmSqlReader를 private 멤버 클래스로 둔 이유는 다음과 같습니다.

  • 외부에서 DI하지 않음

  • OXM 기반 전략으로 고정

  • 설정 단순화

  • 강한 결합을 의도적으로 선택

여기서 중요한 점은:

“항상 느슨한 결합이 좋은 것은 아니다.”

특정 기술로 최적화된 내부 전략이라면 외부 확장 포인트를 굳이 열 필요가 없습니다.


7.3.3 리소스 추상화

기존 문제점:

  • getResourceAsStream()은 클래스패스에 고정

  • 파일 시스템, HTTP 접근이 불편

  • URL 사용 시 코드 수정 필요

이를 해결하기 위해 스프링은 Resource 추상화를 제공합니다.

ResourceLoader도 함께 제공합니다.


Resource 적용

이제 String sqlmapFile 대신 Resource sqlmap을 사용합니다.

설정 예시:

또는:

또는:

코드는 수정되지 않습니다.

이것이 리소스 추상화입니다.


7.3의 설계적 의미

이 장의 핵심은 기술이 아닙니다.

핵심 질문은 이것입니다.

  1. 구현 기술이 바뀔 가능성이 있는가?

  2. 그렇다면 어디에 추상화 계층을 둘 것인가?

  3. 추상화는 너무 일찍 하지 않았는가?

  4. 내부 전략은 고정하고 외부 확장만 열어두는 것이 더 낫지 않은가?

7.3은 단순 OXM 설명이 아닙니다.

  • 기술 교체 가능성

  • 설정 단순화

  • 내부 결합 전략

  • Resource 추상화 설계

7.1.~7.3. 재해석

먼저 요약부터 정리합니다.

  • 7.1은 “SQL을 DAO 밖으로 꺼내는 단계”

  • 7.2는 “SQL을 관리하는 구현이 비대해져서 책임을 나누는 단계”

  • 7.3은 “그 구현이 특정 기술(JAXB/XML)에 종속되어 있어 추상화 레벨을 한 단계 더 올리는 단계”

  • 흐름은 “추상화를 위한 추상화”가 아니라, 구현이 커지면서 자연스럽게 분해되는 리팩터링 과정입니다.

이제 코드 흐름이 끊기지 않도록, “점진적 변화”를 그대로 보여드립니다.


1️. 시작점: DAO 안에 SQL이 있다 (7.1 이전 상태)

문제

  • SQL 변경 시 DAO 수정

  • SQL 관리가 흩어짐

  • SQL을 파일로 분리할 수 없음


2️. 7.1 — SQL을 서비스로 분리

DAO는 SQL을 “요청만” 합니다.

DAO 수정:

DAO는 이제 SQL 문자열을 모릅니다.


2️-1️. 7.1의 첫 구현: Map 기반 SqlService

이제 SQL을 외부에서 주입할 수 있습니다.

여기까지가 7.1의 핵심입니다.


3️. 7.1 후반 — SQL을 파일로 분리하고 싶다

Map을 코드로 직접 넣는 것은 한계가 있습니다.

그래서 독립 파일(sqlmap.xml)을 만들고 싶어집니다.

이제 SqlService는 다음 일을 해야 합니다.

  1. XML 파일 읽기

  2. JAXB로 파싱

  3. Map에 저장

  4. getSql로 반환

즉 구현이 이렇게 커집니다.

여기서 문제 발생.


4️. 7.2 — XmlSqlService가 너무 많은 일을 한다

현재 XmlSqlService는:

  • 파일 읽기

  • JAXB 파싱

  • 저장(Map)

  • 조회(getSql)

를 모두 담당합니다.

여기서 자연스럽게 질문이 나옵니다.

읽는 책임과 저장 책임은 분리할 수 있지 않을까?


5️. 7.2 — 책임을 인터페이스 단위로 분리

5-1️. 저장 책임 분리


5-2️. 읽는 책임 분리

XML 기반 구현:


5-3️ 이제 SqlService는 조립자 역할만 한다

여기까지가 7.2입니다.

이제 책임이 완전히 나뉘었습니다.

  • 읽기 전략 교체 가능

  • 저장 전략 교체 가능

  • SqlService는 orchestration만 담당


6️. 7.3 — 그런데 아직 JAXB에 종속되어 있다

XmlSqlReader 안에는 JAXB 코드가 있습니다.

문제:

  • JAXB → Castor로 바꾸려면 Reader 코드 수정

  • 테스트에서 JAXB 의존

  • 기술 교체 비용 존재

그래서 한 단계 더 올립니다.


7️. 7.3 — OXM 추상화 적용

Spring OXM 추상 인터페이스 사용:

Reader 수정:

이제:

  • JAXB → Castor 교체 시 Reader 수정 없음

  • 단지 Unmarshaller 구현체만 교체


8️. 최종 구조 (7.1 ~ 7.3 통합 결과)

계층 정리:

단계
추상화 대상

7.1

SQL 문자열

7.2

읽기 전략 / 저장 전략

7.3

파싱 기술 / 리소스 접근 방식


🔥 전체 흐름을 한 문장으로

  • 7.1은 “SQL을 코드에서 분리”

  • 7.2는 “그 분리된 구현을 다시 역할 단위로 쪼갬”

  • 7.3은 “그 역할 구현이 특정 기술에 묶이지 않게 한 단계 더 추상화”

즉, 점점 더 “변경 가능성의 축”을 위로 올리는 과정입니다.


🎯 이 흐름이 자연스러운 이유

각 단계는 이전 단계의 “구현이 커지면서 생긴 문제”를 해결합니다.

  1. DAO가 커서 SQL 분리

  2. SqlService 구현이 커서 책임 분리

  3. Reader 구현이 특정 기술에 묶여서 기술 추상화

추상화는 처음부터 과하게 설계한 것이 아니라, 구현이 커지면서 자연스럽게 도출된 결과입니다.

Last updated