# 7. Applications of Core Spring Technologies

## 7.1. SQL과 DAO의 분리

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

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

* ***DB 구조 변경의 직접 영향***\
  컬럼명이 바뀌거나(예: `password` → `pwd`), 하나의 컬럼이 여러 컬럼으로 쪼개지거나, 반대로 합쳐질 수 있습니다. 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이 늘어날수록 프로퍼티/세터가 계속 늘어난다는 단점이 있습니다.

```kotlin
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) }
```

```xml
<!-- 예: 개별 SQL을 프로퍼티로 주입 -->
<bean id="userDao" class="springbook.user.dao.UserDaoJdbc">
    <property name="dataSource" ref="dataSource" />
    <property name="sqlAdd"
              value="insert into users(id, name, password, email, level, login, recommend) values(?,?,?,?,?,?,?)" />
</bean>
```

***SQL 맵(Map) 프로퍼티 방식***

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

* DAO는 `sqlMap["add"]` 처럼 **키로 SQL을 조회**해서 사용합니다.
* 장점: DAO 코드가 단순해지고 SQL 추가 시 DAO 변경이 줄어듭니다.
* 단점: 키 오타/누락은 실행 전 검증이 어렵습니다. 그래서 테스트가 더 중요해집니다.

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

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

    private var sqlMap: Map<String, String> = emptyMap()

    fun setSqlMap(sqlMap: Map<String, String>) {
        this.sqlMap = sqlMap
    }

    fun add(user: User) {
        val sql = sqlMap["add"]
            ?: throw IllegalStateException("SQL key 'add'를 찾을 수 없습니다.")

        jdbcTemplate.update(
            sql,
            user.id,
            user.name,
            user.password,
            user.email,
            user.level.value,
            user.login,
            user.recommend
        )
    }
}
```

```xml
<!-- 예: map 태그로 SQL을 한 번에 주입 -->
<bean id="userDao" class="springbook.user.dao.UserDaoJdbc">
    <property name="dataSource" ref="dataSource" />
    <property name="sqlMap">
        <map>
            <entry key="add"
                   value="insert into users(id, name, password, email, level, login, recommend) values(?,?,?,?,?,?,?)" />
            <entry key="get" value="select * from users where id = ?" />
            <entry key="getAll" value="select * from users order by id" />
            <entry key="deleteAll" value="delete from users" />
            <entry key="getCount" value="select count(*) from users" />
            <entry key="update"
                   value="update users set name = ?, password = ?, email = ?, level = ?, login = ?, recommend = ? where id = ?" />
        </map>
    </property>
</bean>
```

### 7.1.2 SQL 제공 서비스

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

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

핵심은 다음과 같습니다.

* DAO의 관점: “필요한 SQL을 키로 요청해서 받는다.”
* SqlService의 관점: “SQL을 어디서/어떻게 보관하고 로딩할지 책임진다.”
* 즉, SQL 저장/로딩 방식이 바뀌어도 **DAO는 영향을 덜 받도록** 구조를 분리합니다.

***SQL 서비스 인터페이스***

```kotlin
interface SqlService {
    @Throws(SqlRetrievalFailureException::class)
    fun getSql(key: String): String
}
```

***SQL 조회 실패 예외(RuntimeException)***

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

```kotlin
class SqlRetrievalFailureException(
    message: String,
    cause: Throwable? = null
) : RuntimeException(message, cause)
```

***DAO는 SqlService만 의존하도록 변경***

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

```kotlin
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.RowMapper
import javax.sql.DataSource

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

    private lateinit var sqlService: SqlService

    fun setSqlService(sqlService: SqlService) {
        this.sqlService = sqlService
    }

    private val userMapper: RowMapper<User> = RowMapper { rs, _ ->
        User(
            id = rs.getString("id"),
            name = rs.getString("name"),
            password = rs.getString("password"),
            email = rs.getString("email"),
            level = Level.values().first { it.value == rs.getInt("level") },
            login = rs.getInt("login"),
            recommend = rs.getInt("recommend")
        )
    }

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

    fun get(id: String): User {
        return jdbcTemplate.queryForObject(
            sqlService.getSql("userGet"),
            userMapper,
            id
        )!!
    }

    fun getAll(): List<User> {
        return jdbcTemplate.query(
            sqlService.getSql("userGetAll"),
            userMapper
        )
    }

    fun deleteAll() {
        jdbcTemplate.update(sqlService.getSql("userDeleteAll"))
    }

    fun getCount(): Int {
        return jdbcTemplate.queryForObject(
            sqlService.getSql("userGetCount"),
            Int::class.java
        )!!
    }

    fun update(user: User) {
        jdbcTemplate.update(
            sqlService.getSql("userUpdate"),
            user.name, user.password, user.email,
            user.level.value, user.login, user.recommend,
            user.id
        )
    }
}
```

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

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

```kotlin
class SimpleSqlService : SqlService {
    private var sqlMap: Map<String, String> = emptyMap()

    fun setSqlMap(sqlMap: Map<String, String>) {
        this.sqlMap = sqlMap
    }

    override fun getSql(key: String): String {
        val sql = sqlMap[key]
        if (sql == null) {
            throw SqlRetrievalFailureException("키 '$key'에 대한 SQL을 찾을 수 없습니다.")
        }
        return sql
    }
}
```

```xml
<bean id="userDao" class="springbook.user.dao.UserDaoJdbc">
    <property name="dataSource" ref="dataSource" />
    <property name="sqlService" ref="sqlService" />
</bean>

<bean id="sqlService" class="springbook.user.sqlservice.SimpleSqlService">
    <property name="sqlMap">
        <map>
            <entry key="userAdd"
                   value="insert into users(id, name, password, email, level, login, recommend) values(?,?,?,?,?,?,?)" />
            <entry key="userGet" value="select * from users where id = ?" />
            <entry key="userGetAll" value="select * from users order by id" />
            <entry key="userDeleteAll" value="delete from users" />
            <entry key="userGetCount" value="select count(*) from users" />
            <entry key="userUpdate"
                   value="update users set name = ?, password = ?, email = ?, level = ?, login = ?, recommend = ? where id = ?" />
        </map>
    </property>
</bean>
```

***이 단계의 의미***

* 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
<sqlmap xmlns="http://www.epril.com/sqlmap">
    <sql key="userAdd">insert into users(...)</sql>
    <sql key="userGet">select * from users ...</sql>
</sqlmap>
```

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

생성되는 대표 클래스:

* `Sqlmap`
* `SqlType`

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

```kotlin
import javax.xml.bind.JAXBContext

val contextPath = Sqlmap::class.java.`package`.name
val context = JAXBContext.newInstance(contextPath)
val unmarshaller = context.createUnmarshaller()

val input = UserDao::class.java.getResourceAsStream("sqlmap.xml")
val sqlmap = unmarshaller.unmarshal(input) as Sqlmap

val sqlList = sqlmap.sql
```

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

***

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

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

1. XML 파일을 읽는다.
2. JAXB로 SQL 목록을 변환한다.
3. 내부 Map에 저장한다.
4. `getSql()`로 반환한다.

초기 버전 구조:

```kotlin
class XmlSqlService : SqlService {

    private val sqlMap: MutableMap<String, String> = HashMap()

    @PostConstruct
    fun loadSql() {
        val contextPath = Sqlmap::class.java.`package`.name
        val context = JAXBContext.newInstance(contextPath)
        val unmarshaller = context.createUnmarshaller()

        val input = UserDao::class.java.getResourceAsStream("sqlmap.xml")
        val sqlmap = unmarshaller.unmarshal(input) as Sqlmap

        for (sql in sqlmap.sql) {
            sqlMap[sql.key] = sql.value
        }
    }

    override fun getSql(key: String): String {
        return sqlMap[key]
            ?: throw SqlRetrievalFailureException("$key 에 대한 SQL을 찾을 수 없습니다.")
    }
}
```

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

***

### 7.2.3 빈의 초기화 작업

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

```kotlin
class XmlSqlService : SqlService {

    private lateinit var sqlmapFile: String

    fun setSqlmapFile(sqlmapFile: String) {
        this.sqlmapFile = sqlmapFile
    }

    @PostConstruct
    fun loadSql() {
        // sqlmapFile 사용
    }
}
```

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

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

1. 빈 객체 생성
2. 프로퍼티 주입
3. `@PostConstruct` 실행

***

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

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

***책임 1: SQL을 읽는 역할***

```kotlin
interface SqlReader {
    fun read(registry: SqlRegistry)
}
```

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

```kotlin
interface SqlRegistry {
    fun registerSql(key: String, sql: String)
    fun findSql(key: String): String
}
```

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

```kotlin
interface SqlService {
    fun getSql(key: String): String
}
```

***

#### 다중 인터페이스 구현

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

```kotlin
class XmlSqlService :
    SqlService,
    SqlReader,
    SqlRegistry {

    private val sqlMap = HashMap<String, String>()

    override fun registerSql(key: String, sql: String) {
        sqlMap[key] = sql
    }

    override fun findSql(key: String): String {
        return sqlMap[key]
            ?: throw SqlNotFoundException("$key 를 이용해 SQL을 찾을 수 없습니다.")
    }

    override fun read(registry: SqlRegistry) {
        // XML 읽어서 registry.registerSql() 호출
    }

    override fun getSql(key: String): String {
        return findSql(key)
    }
}
```

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

***

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

`XmlSqlService`는

* `SqlReader`
* `SqlRegistry`

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

```kotlin
class XmlSqlService : SqlService {

    private lateinit var sqlReader: SqlReader
    private lateinit var sqlRegistry: SqlRegistry

    fun setSqlReader(sqlReader: SqlReader) {
        this.sqlReader = sqlReader
    }

    fun setSqlRegistry(sqlRegistry: SqlRegistry) {
        this.sqlRegistry = sqlRegistry
    }

    @PostConstruct
    fun loadSql() {
        sqlReader.read(sqlRegistry)
    }

    override fun getSql(key: String): String {
        return sqlRegistry.findSql(key)
    }
}
```

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

```xml
<bean id="sqlService"
      class="springbook.user.sqlservice.XmlSqlService">

    <property name="sqlReader" ref="sqlService"/>
    <property name="sqlRegistry" ref="sqlService"/>
    <property name="sqlmapFile" value="sqlmap.xml"/>

</bean>
```

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

* SqlService
* SqlReader
* SqlRegistry

세 역할을 수행합니다.

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

***

### 7.2.6 디폴트 의존관계

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

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

```kotlin
class DefaultSqlService : BaseSqlService() {

    init {
        setSqlReader(JaxbXmlSqlReader())
        setSqlRegistry(HashMapSqlRegistry())
    }
}
```

XML 설정:

```xml
<bean id="sqlService"
      class="springbook.user.sqlservice.DefaultSqlService"/>
```

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

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

```xml
<bean id="sqlService"
      class="springbook.user.sqlservice.DefaultSqlService">
    <property name="sqlRegistry"
              ref="ultraFastSqlRegistry"/>
</bean>
```

***

#### 구조적 핵심 정리

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의 핵심 인터페이스는 다음 두 가지입니다.

```kotlin
interface Marshaller {
    fun marshal(graph: Any, result: Result)
}

interface Unmarshaller {
    fun unmarshal(source: Source): Any
    fun supports(clazz: Class<*>): Boolean
}
```

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

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

***

#### JAXB 구현 적용

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

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

```xml
<bean id="unmarshaller"
      class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
    <property name="contextPath"
              value="springbook.user.sqlservice.jaxb"/>
</bean>
```

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

***

#### OXM 테스트 코드 구조

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

* XML을 `Source`로 변환
* `unmarshal()` 호출
* 반환 객체 검증

Kotlin 예시:

```kotlin
@Autowired
lateinit var unmarshaller: Unmarshaller

@Test
fun unmarshalSqlMap() {
    val source = StreamSource(
        this::class.java.getResourceAsStream("/sqlmap.xml")
    )

    val sqlmap = unmarshaller.unmarshal(source) as Sqlmap

    assertThat(sqlmap.sql.size).isEqualTo(3)
}
```

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

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

***

#### Castor로 기술 교체

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

```xml
<bean id="unmarshaller"
      class="org.springframework.oxm.castor.CastorMarshaller">
    <property name="mappingLocation"
              value="classpath:mapping.xml"/>
</bean>
```

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

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

***

### 7.3.2 OXM 서비스 추상화 적용

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

핵심 구조:

* `SqlService` 구현
* 내부에 `OxmSqlReader` 멤버 클래스 포함
* `Unmarshaller` DI
* `SqlRegistry`는 기본 구현 사용

구조 예시:

```kotlin
class OxmSqlService : SqlService {

    private val sqlRegistry: SqlRegistry = HashMapSqlRegistry()
    private val oxmSqlReader = OxmSqlReader()

    fun setUnmarshaller(unmarshaller: Unmarshaller) {
        this.oxmSqlReader.setUnmarshaller(unmarshaller)
    }

    fun setSqlmap(sqlmap: Resource) {
        this.oxmSqlReader.setSqlmap(sqlmap)
    }

    @PostConstruct
    fun loadSql() {
        this.oxmSqlReader.read(this.sqlRegistry)
    }

    override fun getSql(key: String): String {
        return this.sqlRegistry.findSql(key)
    }

    private class OxmSqlReader : SqlReader {

        private lateinit var unmarshaller: Unmarshaller
        private lateinit var sqlmap: Resource

        fun setUnmarshaller(unmarshaller: Unmarshaller) {
            this.unmarshaller = unmarshaller
        }

        fun setSqlmap(sqlmap: Resource) {
            this.sqlmap = sqlmap
        }

        override fun read(sqlRegistry: SqlRegistry) {
            val source = StreamSource(sqlmap.inputStream)
            val sqlmapObj = unmarshaller.unmarshal(source) as Sqlmap

            for (sql in sqlmapObj.sql) {
                sqlRegistry.registerSql(sql.key, sql.value)
            }
        }
    }
}
```

***

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

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

* 외부에서 DI하지 않음
* OXM 기반 전략으로 고정
* 설정 단순화
* 강한 결합을 의도적으로 선택

여기서 중요한 점은:

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

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

***

### 7.3.3 리소스 추상화

기존 문제점:

* `getResourceAsStream()`은 클래스패스에 고정
* 파일 시스템, HTTP 접근이 불편
* URL 사용 시 코드 수정 필요

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

```kotlin
interface Resource : InputStreamSource {
    fun exists(): Boolean
    fun isReadable(): Boolean
    fun getURL(): URL
    fun getFile(): File
}
```

`ResourceLoader`도 함께 제공합니다.

```kotlin
interface ResourceLoader {
    fun getResource(location: String): Resource
}
```

***

#### Resource 적용

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

설정 예시:

```xml
<property name="sqlmap"
          value="classpath:sqlmap.xml"/>
```

또는:

```xml
<property name="sqlmap"
          value="file:/opt/resources/sqlmap.xml"/>
```

또는:

```xml
<property name="sqlmap"
          value="http://example.com/sqlmap.xml"/>
```

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

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

***

#### 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 이전 상태)

```kotlin
class UserDao(
    private val jdbcTemplate: JdbcTemplate
) {

    fun add(user: User) {
        jdbcTemplate.update(
            "insert into users(id, name, password) values (?, ?, ?)",
            user.id, user.name, user.password
        )
    }

    fun get(id: String): User {
        return jdbcTemplate.queryForObject(
            "select * from users where id = ?",
            { rs, _ ->
                User(
                    rs.getString("id"),
                    rs.getString("name"),
                    rs.getString("password")
                )
            },
            id
        )!!
    }
}
```

#### 문제

* SQL 변경 시 DAO 수정
* SQL 관리가 흩어짐
* SQL을 파일로 분리할 수 없음

***

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

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

```kotlin
interface SqlService {
    fun getSql(key: String): String
}
```

DAO 수정:

```kotlin
class UserDao(
    private val jdbcTemplate: JdbcTemplate,
    private val sqlService: SqlService
) {

    fun add(user: User) {
        jdbcTemplate.update(
            sqlService.getSql("userAdd"),
            user.id, user.name, user.password
        )
    }

    fun get(id: String): User {
        return jdbcTemplate.queryForObject(
            sqlService.getSql("userGet"),
            { rs, _ ->
                User(
                    rs.getString("id"),
                    rs.getString("name"),
                    rs.getString("password")
                )
            },
            id
        )!!
    }
}
```

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

***

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

```kotlin
class SimpleSqlService(
    private val sqlMap: Map<String, String>
) : SqlService {

    override fun getSql(key: String): String =
        sqlMap[key] ?: error("SQL not found: $key")
}
```

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

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

***

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

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

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

```xml
<sqlmap>
    <sql key="userAdd">
        insert into users(id, name, password) values (?, ?, ?)
    </sql>
    <sql key="userGet">
        select * from users where id = ?
    </sql>
</sqlmap>
```

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

1. XML 파일 읽기
2. JAXB로 파싱
3. Map에 저장
4. getSql로 반환

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

```kotlin
class XmlSqlService : SqlService {

    private val sqlMap = mutableMapOf<String, String>()

    fun loadFromXml(inputStream: InputStream) {
        val sqlmap = unmarshal(inputStream) // JAXB
        sqlmap.sql.forEach {
            sqlMap[it.key] = it.value
        }
    }

    override fun getSql(key: String): String =
        sqlMap[key] ?: error("SQL not found: $key")
}
```

여기서 문제 발생.

***

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

현재 XmlSqlService는:

* 파일 읽기
* JAXB 파싱
* 저장(Map)
* 조회(getSql)

를 모두 담당합니다.

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

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

***

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

### 5-1️. 저장 책임 분리

```kotlin
interface SqlRegistry {
    fun registerSql(key: String, sql: String)
    fun findSql(key: String): String
}

class HashMapSqlRegistry : SqlRegistry {

    private val storage = mutableMapOf<String, String>()

    override fun registerSql(key: String, sql: String) {
        storage[key] = sql
    }

    override fun findSql(key: String): String =
        storage[key] ?: error("SQL not found: $key")
}
```

***

### 5-2️. 읽는 책임 분리

```kotlin
interface SqlReader {
    fun read(registry: SqlRegistry)
}
```

XML 기반 구현:

```kotlin
class XmlSqlReader(
    private val resource: Resource
) : SqlReader {

    override fun read(registry: SqlRegistry) {
        val sqlmap = unmarshal(resource.inputStream)
        sqlmap.sql.forEach {
            registry.registerSql(it.key, it.value)
        }
    }
}
```

***

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

```kotlin
class DefaultSqlService(
    private val reader: SqlReader,
    private val registry: SqlRegistry
) : SqlService {

    init {
        reader.read(registry)
    }

    override fun getSql(key: String): String =
        registry.findSql(key)
}
```

여기까지가 7.2입니다.

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

* 읽기 전략 교체 가능
* 저장 전략 교체 가능
* SqlService는 orchestration만 담당

***

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

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

```kotlin
val sqlmap = unmarshal(resource.inputStream) // JAXB API 사용
```

문제:

* JAXB → Castor로 바꾸려면 Reader 코드 수정
* 테스트에서 JAXB 의존
* 기술 교체 비용 존재

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

***

### 7️. 7.3 — OXM 추상화 적용

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

```kotlin
interface Unmarshaller {
    fun unmarshal(source: Source): Any
}
```

Reader 수정:

```kotlin
class OxmSqlReader(
    private val unmarshaller: Unmarshaller,
    private val resource: Resource
) : SqlReader {

    override fun read(registry: SqlRegistry) {
        val source = StreamSource(resource.inputStream)
        val sqlmap = unmarshaller.unmarshal(source) as Sqlmap

        sqlmap.sql.forEach {
            registry.registerSql(it.key, it.value)
        }
    }
}
```

이제:

* JAXB → Castor 교체 시 Reader 수정 없음
* 단지 Unmarshaller 구현체만 교체

***

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

```
UserDao
   ↓
SqlService
   ↓
(SqlReader + SqlRegistry)
   ↓
Unmarshaller + Resource
```

계층 정리:

| 단계  | 추상화 대상            |
| --- | ----------------- |
| 7.1 | SQL 문자열           |
| 7.2 | 읽기 전략 / 저장 전략     |
| 7.3 | 파싱 기술 / 리소스 접근 방식 |

***

### 🔥 전체 흐름을 한 문장으로

* 7.1은 “SQL을 코드에서 분리”
* 7.2는 “그 분리된 구현을 다시 역할 단위로 쪼갬”
* 7.3은 “그 역할 구현이 특정 기술에 묶이지 않게 한 단계 더 추상화”

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

***

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

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

1. DAO가 커서 SQL 분리
2. SqlService 구현이 커서 책임 분리
3. Reader 구현이 특정 기술에 묶여서 기술 추상화

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

## 7.6. 스프링 3.1의 DI

객체의 생성과 관계 설정을 애플리케이션 로직에서 분리하고, 이를 유연하게 돕는다는 DI의 기본 철약은 유지됩니다.\
다만, 이제는 DI 정보를 담는 메타데이터 형태가 변합니다.

초기 스프링에서 XML를 활용하여, 자바 코드. 밖에서 객체 생성과 의존 관계, 트랜잭션 정책 등을 선언할 수 있었고, 프레임워크 설정을 외부화하는 좋은 방법이었습니다.

하지만 자바5 이후 애너테이션과 리플랙션 기반 메타데이터 활용을 하게 되었고, 자바 코드 안에서도 설정 정보를 표현할 수 있게 되면서, XML의 역할들을 애노테이션이 대신하게 됩니다.

결국 DI 설정이 자바 친화적으로 바뀌었습니다.

* XML은 여전히 설정을 외부화하고 환경별로 분리하는 장점을 갖고 있고,
* @Configuation, @Bean, @Autowired 같은 방식이 유지보수와, IDE 지원 측면에서 자바 코드가 안전하다는 장점이 있습니다.

### 7.6.1. 자바 코드를 이용한 빈 설정

XML에 있던 `<bean/>`, `<tx:annotation-driven/>`, `<jdbc:embedded-database/>` 같은 설정을 `@Configuration`, `@Bean`, `@EnableTransactionManagement` 같은 자바 코드 기반 설정으로 옮기는 것입니다. 다만 처음부터 XML을 버리지 않고, `@ImportResource`를 통해 기존 XML을 임시로 가져오면서 천천히 전환합니다.

***테스트가 바라보는 설정 진입점 변경***

기존 테스트는 XML 파일을 직접 바라봤습니다.

```kotlin
import org.junit.runner.RunWith
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner

@RunWith(SpringJUnit4ClassRunner::class)
@ContextConfiguration(locations = ["/test-applicationContext.xml"])
class UserDaoTest
```

이 구조에서는 테스트가 DI 설정을 XML로부터 가져옵니다. 먼저 이 연결점을 자바 설정 클래스로 바꿉니다.

```kotlin
import org.junit.runner.RunWith
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner

@RunWith(SpringJUnit4ClassRunner::class)
@ContextConfiguration(classes = [TestApplicationContext::class])
class UserDaoTest
```

이제 테스트는 XML 파일 경로가 아니라 `TestApplicationContext`라는 자바 설정 클래스를 바라봅니다. 하지만 이 시점에는 아직 `TestApplicationContext` 안에 아무 설정도 없습니다. 따라서 바로 테스트가 성공하지는 않습니다.

***기존 XML을 임시로 가져오기***

그래서 책은 `@ImportResource`를 사용합니다. 이 단계가 매우 중요합니다. 진입점은 자바 설정 클래스로 옮겼지만, 실제 빈 정보는 기존 XML을 그대로 재사용합니다.

```kotlin
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.ImportResource

@Configuration
@ImportResource("/test-applicationContext.xml")
class TestApplicationContext
```

이렇게 하면 리팩터링의 시작점은 자바 설정으로 옮기되, 빈 정의는 그대로 유지할 수 있습니다. 즉, 구조를 먼저 바꾸고 내용을 나중에 옮기는 방식입니다.

***일반\*\*\*\* ****`<bean/>`**** ****정의를**** ****`@Bean`**** \*\*\*\*메서드로 이동***

그다음에는 XML 안의 `<bean/>` 정의를 하나씩 자바 코드로 옮깁니다. \
먼저 `dataSource`, `transactionManager` 같은 인프라 빈을 옮기고, 이후 `userDao`, `userService`, `testUserService`, `mailSender`, `sqlService`, `sqlRegistry`, `unmarshaller` 같은 빈들을 차례대로 이동합니다.

예를 들어 XML의 `dataSource` 빈을

```xml
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
    <property name="driverClass" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost/springbook?characterEncoding=UTF-8" />
    <property name="username" value="spring" />
    <property name="password" value="book" />
</bean>
```

이를 자바 기반 설정으로 옮기면 다음과 같이 됩니다.

```kotlin
import com.mysql.jdbc.Driver
import javax.sql.DataSource
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.jdbc.datasource.SimpleDriverDataSource

@Configuration
class TestApplicationContext {

    @Bean
    fun dataSource(): DataSource {
        val ds = SimpleDriverDataSource()
        ds.setDriverClass(Driver::class.java)
        ds.setUrl("jdbc:mysql://localhost/springbook?characterEncoding=UTF-8")
        ds.username = "spring"
        ds.password = "book"
        return ds
    }
}
```

문자열 기반 XML에서 타입 기반 자바 코드로 바뀌면서 컴파일러와 IDE의 도움을 받을 수 있게 됩니다. \
클래스명 변경, 프로퍼티명 변경, 메서드명 변경, 리팩터링 안전성이 훨씬 좋아집니다.

***전용 태그도 자바 코드로 전환***

`<bean/>`뿐 아니라 XML 전용 네임스페이스 태그도 자바 코드로 옮길 수 있음을 보여줍니다.\
대표적으로 `<jdbc:embedded-database/>`는 `EmbeddedDatabaseBuilder`로 바꿀 수 있습니다.

```kotlin
import javax.sql.DataSource
import org.springframework.context.annotation.Bean
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.HSQL

@Bean
fun embeddedDatabase(): DataSource {
    return EmbeddedDatabaseBuilder()
        .setName("embeddedDatabase")
        .setType(HSQL)
        .addScript("classpath:springbook/user/sqlservice/updatable/sqlRegistrySchema.sql")
        .build()
}
```

또한 `<tx:annotation-driven/>`는 `@EnableTransactionManagement`로 대체할 수 있습니다.

```kotlin
import org.springframework.context.annotation.Configuration
import org.springframework.transaction.annotation.EnableTransactionManagement

@Configuration
@EnableTransactionManagement
class TestApplicationContext
```

즉, XML이 제공하던 스프링 전용 편의 기능도 자바 기반 설정으로 대부분 이동할 수 있게 되었습니다.

#### 요약

핵심은 **DI 설정을 더 자바 친화적인 방식으로 표현할 수 있게 되었다**는 점입니다.

***과거 방식***

* XML이 설정의 중심입니다.
* 객체 관계와 트랜잭션 정책이 XML에 들어갑니다.
* 테스트도 XML 파일을 직접 참조합니다.

***전환 방식***

* `@ContextConfiguration(classes=...)`로 진입점을 자바 설정 클래스로 바꿉니다.
* `@ImportResource`로 기존 XML을 임시 가져옵니다.
* `<bean/>`과 전용 태그를 `@Bean`, `@EnableTransactionManagement` 등으로 옮깁니다.

***현재 실무 방식***

* XML 대신 `@Configuration`과 `@Bean`을 사용하기는 하지만, 그마저도 필요한 경우에만 직접 작성합니다.
* 공통 인프라는 Spring Boot 자동 설정을 활용하는 경우가 많습니다.
* DB 접속 정보는 코드에 박아 넣기보다 `application.yml`이나 환경변수로 외부화합니다.
* 테스트는 JUnit 5와 스프링 테스트 지원 애노테이션을 사용하는 경우가 많습니다.

현재 기준으로 같은 예제를 다시 쓴다면 보통 아래처럼 갑니다.

```kotlin
import java.time.Clock
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration(proxyBeanMethods = false)
class AppConfig {

    @Bean
    fun clock(): Clock = Clock.systemUTC()
}
```

그리고 DB 설정은 자바 코드보다 외부 설정 파일로 둡니다.

```yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/springbook
    username: spring
    password: book
```

### 7.6.2. 빈 스캐닝과 자동와이어링

7.6.1이 XML 설정을 자바 코드로 옮긴 단계였다면, 7.6.2는 **자바 설정 클래스 안에 남아 있는 수동 연결 코드 자체를 줄이는 단계**입니다.

1. `@Autowired`를 이용해 빈이 필요한 의존관계를 스스로 선언하게 만듭니다.&#x20;
2. `@ComponentScan`과 `@Component` 계열 애노테이션을 사용해 아예 빈 등록까지 자동화합니다.&#x20;

결과적으로 설정 클래스는 점점 얇아지고, 애플리케이션 클래스는 자신이 필요한 협력 객체를 선언적으로 표현하게 됩니다.

***첫 번째 변화:****&#x20;****`@Autowired`\*\*\*\*로 수동 주입 코드 줄이기***

7.6.1까지는 설정 클래스 안에서 객체를 생성한 뒤, setter를 호출해서 직접 의존관계를 주입했습니다.

```kotlin
@Bean
fun userDao(): UserDao {
    val dao = UserDaoJdbc()
    dao.setDataSource(dataSource())
    dao.setSqlService(sqlService())
    return dao
}
```

이 방식은 분명하고 명시적이지만, 빈이 많아질수록 설정 코드가 장황해집니다. \
그래서 `UserDaoJdbc` 클래스 쪽으로 의존관계 선언을 이동시킵니다.

```kotlin
import javax.sql.DataSource
import org.springframework.beans.factory.annotation.Autowired

class UserDaoJdbc : UserDao {

    private lateinit var dataSource: DataSource
    private lateinit var sqlService: SqlService

    @Autowired
    fun setDataSource(dataSource: DataSource) {
        this.dataSource = dataSource
    }

    @Autowired
    fun setSqlService(sqlService: SqlService) {
        this.sqlService = sqlService
    }
}
```

그러면 설정 클래스는 이렇게 단순해집니다.

```kotlin
@Bean
fun userDao(): UserDao = UserDaoJdbc()
```

이제 설정 클래스는 객체를 만들기만 하고, 실제 의존관계 주입은 컨테이너가 담당합니다. \
즉, DI 메타데이터의 일부가 설정 클래스에서 빈 클래스 내부의 애노테이션으로 이동한 것입니다.

***두 번째 변화: 필드 주입과 setter 주입의 장단점***

책은 설명상 `@Autowired`를 필드나 setter에 붙이는 예제를 사용합니다. 당시에는 XML에서 자바 기반 설정으로 넘어오면서 수동 주입 코드를 줄이는 효과를 보여주기에 적절한 방식이었습니다.&#x20;

현재 기준으로 반드시 재해석했을때:

필드 주입은 코드가 짧고 보기에 편할 수 있지만, 객체가 어떤 의존성을 필수로 요구하는지 생성 시점에 드러나지 않습니다. setter 주입 역시 선택적 의존성 표현에는 좋지만, 필수 의존성이 많은 경우 객체가 덜 완성된 상태로 존재할 여지가 있습니다.

그래서 현재 실무에서는 보통 생성자 주입을 사용합니다.

```kotlin
import javax.sql.DataSource
import org.springframework.stereotype.Repository

@Repository
class UserDaoJdbc(
    private val dataSource: DataSource,
    private val sqlService: SqlService,
) : UserDao
```

이 방식의 장점은 객체를 만들 때 필요한 의존성이 전부 드러나고, `val`로 불변성을 확보하기 쉬우며, 테스트에서도 직접 생성이 간단합니다.

***책의 설명 방식***

* `@Autowired`를 setter나 필드에 붙여 수동 주입 코드를 줄입니다.
* 리팩터링 단계에서 효과가 분명하게 드러납니다.

***현재 권장 방식***

* 의존관계 주입의 방향은 동일하지만, 표현은 생성자 주입을 더 선호합니다.

***세 번째 변화:****&#x20;****`@ComponentScan`\*\*\*\*으로 빈 등록 자동화***

`@Autowired`를 적용해도 여전히 `@Bean fun userDao()` 같은 메서드는 남아 있습니다. \
빈으로 자동 등록하고 싶은 클래스에 `@Component`를 붙이고, 설정 클래스에서 `@ComponentScan`을 선언하면 컨테이너가 해당 패키지를 스캔해서 빈을 찾아 등록합니다.

```kotlin
import javax.sql.DataSource
import org.springframework.stereotype.Repository

@Repository
class UserDaoJdbc(
    private val dataSource: DataSource,
    private val sqlService: SqlService,
) : UserDao
```

```kotlin
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.transaction.annotation.EnableTransactionManagement

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = ["springbook.user"])
class TestApplicationContext
```

이렇게 되면 `userDao()` 메서드 자체가 필요 없어집니다. \
컨테이너가 클래스패스를 검사해 `UserDaoJdbc`를 찾아 자동 등록해주기 때문입니다.

***`@Component`****,**** ****`@Repository`****,****&#x20;****`@Service`\*\*\*\*의 의미***

`@Repository`는 “이 클래스는 데이터 접근 계층이다”라는 의미를 드러내고, `@Service`는 “이 클래스는 서비스 계층의 핵심 비즈니스 로직을 담당한다”는 의미를 드러냅니다. 즉, 둘 다 빈 등록 대상이라는 공통점이 있지만, 역할을 더 분명히 표현합니다.

Kotlin으로 정리하면 아래처럼 됩니다.

```kotlin
import org.springframework.stereotype.Repository

@Repository
class UserDaoJdbc(
    private val dataSource: DataSource,
    private val sqlService: SqlService,
) : UserDao
```

```kotlin
import org.springframework.mail.MailSender
import org.springframework.stereotype.Service

@Service("userService") // 테스트용 testUserService와의 충돌 맥락까지 함께 설명하기 위함
class UserServiceImpl(
    private val userDao: UserDao,
    private val mailSender: MailSender,
) : UserService
```

***Autowired에서 발생하는 충돌 문제***

7.6.2에서 반드시 이해해야 하는 장면이 있습니다. `UserServiceImpl`까지 자동 등록으로 바꾸면, 컨테이너 안에 `UserService` 타입 빈이 두 개 생길 수 있습니다. 하나는 실제 서비스 빈이고, 다른 하나는 테스트를 위한 `testUserService`입니다.

타입만 보고 주입하려 하면 어느 빈을 넣어야 할지 모호해집니다.\
&#x20;타입 후보가 하나일 때는 편리하지만, 둘 이상이면 이름이나 별도 규칙이 필요합니다.

그래서 책에서는 빈 이름을 조정하거나 자동 등록 방식을 일부만 적용해 문제를 해결하지만, 현재 기준으로는 아래와 같은 방법들을 사용합니다.

***과거 방식***

* 빈 이름을 XML이나 `@Service("name")`로 명시해 해결합니다.
* 테스트 전용 빈은 수동 `@Bean` 등록을 유지합니다.

***현재 방식***

* `@Qualifier`로 어떤 빈을 원하는지 명시합니다.
* 기본 후보 하나를 `@Primary`로 정합니다.
* 테스트에서는 `@TestConfiguration`, `@MockBean`, 별도 프로파일을 활용합니다.

예를 들면 다음과 같습니다.

```kotlin
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.mail.MailSender
import org.springframework.stereotype.Service

@Service
class UserServiceImpl(
    @Qualifier("userDaoJdbc")
    private val userDao: UserDao,
    private val mailSender: MailSender,
) : UserService
```

또는

```kotlin
import org.springframework.context.annotation.Primary
import org.springframework.stereotype.Service

@Primary
@Service("userService")
class UserServiceImpl(
    private val userDao: UserDao,
    private val mailSender: MailSender,
) : UserService
```

***메타 애노테이션과 자동 등록***

책은 `@Component`를 직접 붙이는 것에서 한 걸음 더 나아가, `@Component`를 메타 애노테이션으로 활용하는 방식도 소개합니다.&#x20;

예를 들어 어떤 클래스들을 하나의 그룹으로 묶고 싶다면 커스텀 애노테이션을 만들고 그 위에 `@Component`를 붙일 수 있습니다.

```kotlin
import org.springframework.stereotype.Component

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@Component
annotation class SnsConnector
```

이제 `@SnsConnector`가 붙은 클래스는 자동 등록 대상이 됩니다.

```kotlin
@SnsConnector
class FacebookConnector
```

이 방식의 장점은 단순히 빈 등록만이 아닙니다. “이 클래스는 SNS 연동 역할을 한다”는 의미도 동시에 표현할 수 있습니다. 즉, 애노테이션은 빈 등록과 역할 구분을 함께 수행합니다.

#### 요약

***책에서 보여주는 변화***

* 설정 클래스 안에서 setter를 직접 호출하던 코드를 `@Autowired`로 줄입니다.
* `@ComponentScan`으로 `@Bean` 등록 메서드까지 줄입니다.
* `@Component`, `@Repository`, `@Service`를 이용해 빈 역할을 구분합니다.

***현재 실무에서 정착된 형태***

* 자동 등록과 자동 주입의 방향은 그대로 유지합니다.
* 다만 `@Autowired` 필드 주입보다 생성자 주입을 선호합니다.
* `@Configuration`은 꼭 필요한 커스텀 빈에만 사용합니다.
* 대부분의 공통 설정은 Spring Boot 자동 설정과 외부 설정을 활용합니다.
* 테스트는 JUnit 5, `@SpringJUnitConfig`, `@SpringBootTest`, `@TestConfiguration` 등을 함께 사용합니다.

현재식으로 정리한 최종 예시는 다음과 같습니다.

```kotlin
import javax.sql.DataSource
import org.springframework.mail.MailSender
import org.springframework.stereotype.Repository
import org.springframework.stereotype.Service

@Repository
class UserDaoJdbc(
    private val dataSource: DataSource,
    private val sqlService: SqlService,
) : UserDao

@Service("userService")
class UserServiceImpl(
    private val userDao: UserDao,
    private val mailSender: MailSender,
) : UserService
```

그리고 설정 클래스는 필요한 최소한만 남습니다.

```kotlin
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.transaction.annotation.EnableTransactionManagement

@Configuration(proxyBeanMethods = false)
@EnableTransactionManagement
@ComponentScan(basePackages = ["springbook.user"])
class AppConfig
```

### 7.6.3 컨텍스트 분리와 `@Import`

지금까지 만든 설정은 점점 자바 코드답게 바뀌었지만, 아직 **성격이 다른 설정이 한 군데 섞여 있다**는 문제가 남아 있습니다. 애플리케이션을 실제로 실행할 때 필요한 빈, 테스트에서만 필요한 빈, 그리고 SQL 서비스처럼 별도로 묶어도 되는 빈이 모두 같은 설정 안에 있으면 관리가 어려워집니다. 그래서 7.6.3의 주제는 단순합니다. **DI 설정도 역할에 따라 분리하자**는 것입니다.

핵심은 테스트용 설정, 운영용 설정, 공통 설정을 분리하고, 이를 다시 `@Import`로 조합하는 것입니다.  \
`TestAppContext`, `AppContext`, `SqlServiceContext` 같은 여러 설정 클래스로 나눈 뒤, 필요한 설정만 합쳐서 테스트와 운영 환경에 맞게 컨텍스트를 구성합니다.&#x20;

즉, 앞 절에서 자바 코드로 옮긴 DI 설정을 이제는 **관심사별로 분리하고 재조합 가능한 구조로 정리**하는 단계입니다.

앞 절에서 `userDao`, `userService`는 자동 등록과 자동 주입으로 많이 단순해졌습니다. \
하지만 `testUserService`, `DummyMailSender` 같은 테스트 전용 빈은 운영 환경에 들어가면 안 됩니다.&#x20;

반대로 운영용 `MailSender`는 테스트 중에는 원치 않습니다. SQL 서비스도 애플리케이션 핵심 설정과는 성격이 다릅니다. 이처럼 설정 성격이 다르면 수정 주기와 영향 범위가 달라집니다. 따라서 한 클래스에 전부 몰아두기보다 나누는 편이 자연스럽습니다.

책은 먼저 테스트 전용 설정을 `TestAppContext`로 분리합니다. 이 안에는 테스트에서만 필요한 `testUserService`, `mailSender` 같은 빈이 들어갑니다.

```kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.mail.MailSender

@Configuration
class TestAppContext {

    @Bean
    fun testUserService(): UserService = TestUserService()

    @Bean
    fun mailSender(): MailSender = DummyMailSender()
}
```

이렇게 분리하면 설정 클래스를 읽는 순간 “이 클래스는 테스트 전용이구나”를 바로 알 수 있습니다. \
빈 설정 정보는 단순히 동작만 맞으면 되는 것이 아니라, **설정의 의도와 범위를 드러내는 구조**여야 합니다.

***여러 설정 클래스를 함께 사용하는 방식***

설정을 나눴다면 이제 이를 다시 조합해야 합니다. \
테스트에서 `@ContextConfiguration(classes = [...])`에 여러 설정 클래스를 지정하는 방식을 보여줍니다.

```kotlin
import org.junit.runner.RunWith
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner

@RunWith(SpringJUnit4ClassRunner::class)
@ContextConfiguration(classes = [TestAppContext::class, AppContext::class])
class UserDaoTest
```

이 방식의 의미는 분명합니다. 테스트는 운영용 핵심 설정인 `AppContext`에 테스트 전용 보정 설정인 `TestAppContext`를 추가해서 동작합니다. 즉, 하나의 거대한 설정을 만들지 않고도 필요한 설정 조각만 합쳐서 컨텍스트를 만들 수 있습니다.

***`@Import`\*\*\*\*로 설정 클래스 조합하기***

하지만 설정 클래스가 많아질수록 매번 테스트나 bootstrap 코드에서 조합 목록을 직접 적는 것도 번거로워집니다. 그래서 책은 `@Import`를 사용합니다.&#x20;

예를 들어 `AppContext`가 `SqlServiceContext`를 포함해야 한다면 아래처럼 조합할 수 있습니다.

```kotlin
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.transaction.annotation.EnableTransactionManagement

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = ["springbook.user"])
@Import(SqlServiceContext::class)
class AppContext
```

이렇게 하면 `AppContext` 하나만 지정해도 내부적으로 `SqlServiceContext`가 함께 적용됩니다. 즉, 설정 클래스도 독립적으로 관리하면서 상위 설정이 필요한 하위 설정을 포함하는 구조를 만들 수 있습니다.

***과거 방식에서 현재 방식으로 연결하기***

책에서 `@Import`는 XML의 `<import/>`를 자바 코드로 옮긴 대응물로 사용됩니다. 이 방향은 지금도 그대로 유효합니다. 여러 `@Configuration` 클래스를 나누고 `@Import`로 조합하는 방식은 현재 스프링에서도 여전히 정식 방법입니다.

다만 현재 실무에서는 테스트 컨텍스트를 나눌 때 책처럼 테스트 전용 `@Configuration` 클래스를 직접 연결하는 방식 외에도 조금 더 목적별 도구를 많이 씁니다.

***과거 방식***

* `AppContext`, `TestAppContext`, `SqlServiceContext`를 나눕니다.
* 테스트에서는 `@ContextConfiguration(classes = [...])`로 여러 설정 클래스를 조합합니다.
* 공통 설정은 `@Import`로 포함합니다.

***현재 방식***

* 설정 분리 원칙은 그대로 유지합니다.
* 다만 테스트 전용 설정은 `@TestConfiguration`이나 테스트 슬라이스, `@SpringBootTest`와 함께 사용하는 경우가 많습니다.
* 라이브러리성 설정은 `@Import`로 조합하거나, Spring Boot에서는 자동 설정으로 옮기는 경우도 많습니다.

예를 들어 현재식 테스트 전용 설정은 아래처럼 정리할 수 있습니다.

```kotlin
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.mail.MailSender

@TestConfiguration
class TestMailConfig {

    @Bean
    fun mailSender(): MailSender = DummyMailSender()
}
```

그리고 테스트에서는 필요한 설정만 가져와 붙입니다.

```kotlin
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.Import

@SpringBootTest
@Import(TestMailConfig::class)
class UserServiceTest
```

즉, 7.6.3이 말하는 본질은 **설정을 한 군데에 몰아넣지 말고, 성격에 따라 나누고, 필요한 곳에서 다시 조합하라**는 것입니다.

### 7.6.4 프로파일

설정을 여러 클래스로 나눈 것만으로도 구조는 좋아졌지만, 여전히 “운영용 설정과 테스트용 설정 중 무엇을 현재 실행에서 활성화할 것인가”를 사람이 직접 조합해야 합니다.&#x20;

프로파일은 바로 이 문제를 해결합니다. **같은 종류의 빈이라도 환경에 따라 다른 구현을 선택해서 활성화할 수 있게 만드는 장치**가 프로파일입니다.

***요약***

`@Profile`과 `@ActiveProfiles`를 이용해 환경별 빈 설정을 분기하는 것입니다. 테스트에서는 `test` 프로파일을 활성화하고, 운영에서는 `production` 프로파일을 활성화해 각각 다른 `MailSender`나 테스트 전용 빈을 쓰게 만듭니다. 설정 클래스를 나눠놓은 뒤 프로파일을 붙이면 조합을 사람이 직접 관리하는 부담이 줄어듭니다.

***프로파일이 필요한 이유***

앞 절에서 `mailSender` 빈은 테스트와 운영에서 구현이 달라야 했습니다. \
테스트에서는 실제 메일을 보내면 안 되므로 `DummyMailSender`를 쓰고, 운영에서는 실제 SMTP나 메일 서버와 연결된 구현을 써야 합니다. \
이런 차이를 단순히 설정 클래스 분리만으로 관리할 수도 있지만, 설정 수가 늘어나면 어떤 조합이 어느 환경용인지 더 헷갈릴 수 있습니다.

프로파일은 “이 설정은 test 환경에서만”, “이 설정은 production 환경에서만”이라는 조건을 설정 클래스나 빈 정의에 직접 붙이는 방식입니다.

예를 들어 테스트 전용 설정은 아래처럼 표현할 수 있습니다.

```kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import org.springframework.mail.MailSender

@Configuration
@Profile("test")
class TestAppContext {

    @Bean
    fun testUserService(): UserService = TestUserService()

    @Bean
    fun mailSender(): MailSender = DummyMailSender()
}
```

운영용 설정도 같은 방식으로 정의할 수 있습니다.

```kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import org.springframework.mail.MailSender
import org.springframework.mail.javamail.JavaMailSenderImpl

@Configuration
@Profile("production")
class ProductionAppContext {

    @Bean
    fun mailSender(): MailSender {
        val sender = JavaMailSenderImpl()
        sender.host = "localhost"
        return sender
    }
}
```

이제 컨테이너는 활성 프로파일에 따라 둘 중 하나만 선택해서 등록합니다.

***테스트에서\*\*\*\* ****`@ActiveProfiles`**** \*\*\*\*사용하기***

테스트에서 `@ActiveProfiles("test")`를 이용해 test 프로파일을 활성화합니다. 이 방식은 지금도 그대로 사용됩니다.

```kotlin
import org.junit.runner.RunWith
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner

@RunWith(SpringJUnit4ClassRunner::class)
@ActiveProfiles("test")
@ContextConfiguration(classes = [AppContext::class])
class UserServiceTest
```

이 구조가 깔끔한 이유는 `AppContext`는 공통 설정만 담고 있고, `TestAppContext`, `ProductionAppContext`는 프로파일에 따라 자동으로 걸러지기 때문입니다. 테스트는 test 프로파일만 켜면 되고, 운영은 production 프로파일만 켜면 됩니다.

***중첩 클래스 방식의 의미***

`AppContext` 안에 `TestAppContext`, `ProductionAppContext`를 정적 중첩 클래스로 넣는 방식도 보여줍니다. \
이 방식은 설정 클래스 파일 수를 줄이고, “이 하위 설정은 이 상위 설정과 밀접하게 관련 있다”는 점을 드러내기에 좋습니다.

Kotlin으로 옮기면 다음처럼 정리할 수 있습니다.

```kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.context.annotation.Profile
import org.springframework.mail.MailSender
import org.springframework.mail.javamail.JavaMailSenderImpl
import org.springframework.transaction.annotation.EnableTransactionManagement

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = ["springbook.user"])
@Import(SqlServiceContext::class)
class AppContext {

    @Configuration
    @Profile("production")
    class ProductionAppContext {
        @Bean
        fun mailSender(): MailSender {
            val sender = JavaMailSenderImpl()
            sender.host = "localhost"
            return sender
        }
    }

    @Configuration
    @Profile("test")
    class TestAppContext {
        @Bean
        fun testUserService(): UserService = TestUserService()

        @Bean
        fun mailSender(): MailSender = DummyMailSender()
    }
}
```

이 방식을 통해 설정 클래스가 많아져도 하나의 구조 안에 정리할 수 있음을 보여줍니다.

***과거 방식과 현재 방식 비교***

***과거 방식***

* `@Profile("test")`, `@Profile("production")`를 설정 클래스에 직접 붙입니다.
* 테스트에서는 `@ActiveProfiles("test")`를 사용합니다.
* 중첩 `@Configuration` 클래스로 구조를 정리합니다.

***현재 방식***

* `@Profile`과 `@ActiveProfiles`는 그대로 유효합니다.
* 하지만 환경별 값 차이는 보통 `application-test.yml`, `application-prod.yml`로 분리합니다.
* 빈 구현 자체가 달라야 할 때만 `@Profile`을 적극적으로 사용합니다.
* Boot 애플리케이션에서는 `spring.profiles.active`나 테스트 애노테이션 조합으로 프로파일을 전환합니다.

예를 들어 현재식으로는 메일 발송기 구현을 다르게 둘 수도 있지만, 더 자주 보는 형태는 같은 빈을 유지하고 설정값만 프로파일별 파일에서 바꾸는 방식입니다.

```yml
# application-test.yml
app:
  mail:
    host: localhost
    enabled: false
```

```yml
# application-prod.yml
app:
  mail:
    host: smtp.mycompany.com
    enabled: true
```

**구현이 달라지면 `@Profile`, 값이 달라지면 외부 설정 파일**이라는 구분이 점점 선명해졌습니다.

### 7.6.5 프로퍼티 소스

환경에 따라 바뀌는 것이 항상 빈 구현 자체인 것은 아닙니다. \
실제로는 DB URL, 사용자 이름, 비밀번호, 호스트, 타임아웃처럼 **같은 빈이 쓰는 값만 달라지는 경우**가 훨씬 많습니다. 이때 코드 안에 문자열을 박아 넣는 대신 외부 프로퍼티 소스에서 읽어오자는 것입니다.

***요약***

핵심은 `@PropertySource`, `Environment`, `@Value`, `PropertySourcesPlaceholderConfigurer`를 이용해 환경별 설정 값을 코드 바깥으로 분리하는 것입니다.&#x20;

`database.properties` 파일에서 DB 접속 정보를 읽어와 `dataSource` 빈을 구성합니다. 즉, 환경에 따라 달라지는 값은 자바 코드가 아니라 프로퍼티 파일이 담당하게 만드는 단계입니다.

***왜 프로퍼티 소스가 필요한가***

앞 절까지의 예제에서는 `dataSource`를 만들 때 URL, 계정, 비밀번호, 드라이버 클래스를 코드에 직접 써 넣었습니다. \
학습용으로는 단순하지만 실제 운영 환경에서는 좋지 않습니다. 테스트, 개발, 운영 환경마다 값이 다를 수 있고, 보안상 코드에 비밀번호를 박아두는 것도 바람직하지 않습니다.

그래서 `database.properties` 같은 파일을 도입합니다.

```yml
db.driverClass=com.mysql.jdbc.Driver
db.url=jdbc:mysql://localhost/springbook?characterEncoding=UTF-8
db.username=spring
db.password=book
```

이제 설정 클래스는 이 파일을 읽어 값을 가져오게 됩니다.

***`@PropertySource`****와**** ****`Environment`**** \*\*\*\*방식***

먼저 `@PropertySource`로 파일을 등록하고, `Environment`를 주입받아 값을 읽어오는 방식을 보여줍니다.

```kotlin
import javax.sql.DataSource
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.context.annotation.PropertySource
import org.springframework.core.env.Environment
import org.springframework.jdbc.datasource.SimpleDriverDataSource
import org.springframework.transaction.annotation.EnableTransactionManagement

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = ["springbook.user"])
@Import(SqlServiceContext::class)
@PropertySource("/database.properties")
class AppContext : SqlMapConfig {

    @Autowired
    lateinit var env: Environment

    @Bean
    fun dataSource(): DataSource {
        val ds = SimpleDriverDataSource()
        ds.setDriverClass(Class.forName(env.getProperty("db.driverClass")) as Class<out java.sql.Driver>)
        ds.url = env.getProperty("db.url")
        ds.username = env.getProperty("db.username")
        ds.password = env.getProperty("db.password")
        return ds
    }
}
```

이 방식의 장점은 분명합니다. 프로퍼티 값을 중앙에서 관리하고, 코드에서는 이름으로 참조합니다. 다만 관련 값이 많아질수록 `env.getProperty(...)` 호출이 반복되어 코드가 장황해질 수 있습니다.

***`@Value`\*\*\*\*와 치환자 사용***

그래서 책은 `@Value`를 이용한 방식도 보여줍니다. 필드에 바로 값을 주입받으면 `Environment`를 직접 다루는 코드가 줄어듭니다.

```kotlin
import javax.sql.DataSource
import org.springframework.beans.factory.config.PropertySourcesPlaceholderConfigurer
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.PropertySource
import org.springframework.jdbc.datasource.SimpleDriverDataSource

@Configuration
@PropertySource("/database.properties")
class AppContext {

    @Value("\${db.driverClass}")
    lateinit var driverClass: Class<out java.sql.Driver>

    @Value("\${db.url}")
    lateinit var url: String

    @Value("\${db.username}")
    lateinit var username: String

    @Value("\${db.password}")
    lateinit var password: String

    @Bean
    fun dataSource(): DataSource {
        val ds = SimpleDriverDataSource()
        ds.setDriverClass(driverClass)
        ds.url = url
        ds.username = username
        ds.password = password
        return ds
    }

    @Bean
    fun placeholderConfigurer(): PropertySourcesPlaceholderConfigurer {
        return PropertySourcesPlaceholderConfigurer()
    }
}
```

여기서 `PropertySourcesPlaceholderConfigurer`가 필요한 이유도 설명합니다. `${...}` 치환자를 제대로 처리하려면 이를 해석하는 빈 후처리기가 필요하기 때문입니다.

***현재 기준으로 어떻게 읽어야 하는가***

`@PropertySource`, `Environment`, `@Value`는 지금도 모두 유효합니다. 하지만 Boot 환경에서는 이 방식이 “주요 방식”이라기보다 “가능한 방식”에 가깝습니다.&#x20;

현재는 보통 `application.yml`, `application.properties`, 환경 변수, 커맨드라인 인자 등으로 외부 설정을 관리하고, 여러 관련 값을 하나의 타입으로 묶는 `@ConfigurationProperties`를 더 선호합니다.

***과거 방식***

* `@PropertySource("/database.properties")`로 파일을 직접 등록합니다.
* `Environment#getProperty()` 또는 `@Value`로 값을 읽습니다.
* `${...}`를 쓸 때 `PropertySourcesPlaceholderConfigurer`를 명시적으로 등록하기도 합니다.

***현재 방식***

* Spring Boot에서는 `application.yml`, `application.properties`, 환경 변수, 시크릿 저장소 등을 우선 사용합니다.
* 여러 값을 한 번에 다룰 때는 `@ConfigurationProperties`로 묶습니다.
* 표준 `spring.datasource.*` 같은 Boot 자동 설정 키를 활용하면 `DataSource` 빈을 직접 만들 필요조차 없는 경우가 많습니다.

현재식으로 바꾸면 다음처럼 정리할 수 있습니다.

```kotlin
import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties(prefix = "app.datasource")
data class AppDataSourceProperties(
    var url: String = "",
    var username: String = "",
    var password: String = "",
)
```

```yml
app:
  datasource:
    url: jdbc:mysql://localhost:3306/springbook
    username: spring
    password: book
```

그리고 대부분은 직접 `SimpleDriverDataSource`를 만드는 대신 Boot가 `spring.datasource.*`를 보고 `DataSource`를 구성하게 둡니다.&#x20;

#### 7.6.6 빈 설정의 재사용과 `@Enable*`

앞 절들에서 설정을 자바 코드로 옮기고, 자동 등록과 자동 주입을 적용하고, 환경별 값과 프로파일을 분리했습니다. 이제 **이렇게 만든 설정을 다른 프로젝트나 다른 모듈에서도 재사용할 수 없을까**

***요약***

SQL 서비스 관련 설정을 별도의 설정 모듈로 분리하고, 이를 `@Import`나 커스텀 `@Enable*` 애노테이션으로 손쉽게 가져다 쓰게 만드는 것입니다.

&#x20;`SqlServiceContext`를 재사용 가능한 설정 클래스로 만들고, `SqlMapConfig` 인터페이스를 도입해 사용자 애플리케이션별 SQL 매핑 리소스 위치만 바깥에서 제공받게 합니다. 마지막에는 `@EnableSqlService` 같은 커스텀 애노테이션으로 이 설정을 더 간단히 활성화하는 방식까지 보여줍니다.

***재사용 가능한 설정으로 분리하기***

책은 먼저 SQL 서비스 설정을 `SqlServiceContext`로 묶습니다. 이 안에는 `SqlService`, `SqlRegistry`, `Unmarshaller` 등 SQL 서비스 구현에 필요한 빈이 들어갑니다. 이렇게 하면 애플리케이션 핵심 설정과 분리되어 재사용성이 좋아집니다.

하지만 아직 한 가지 문제가 SQL 매핑 파일 위치가 사용자 애플리케이션에 종속적이라는 점입니다. 이를 해결하기 위해 `SqlMapConfig` 인터페이스를 도입합니다.

```kotlin
import org.springframework.core.io.Resource

interface SqlMapConfig {
    fun sqlMapResource(): Resource
}
```

이제 애플리케이션 쪽은 자신이 사용할 SQL 매핑 파일 위치만 구현해 제공하면 됩니다.

```kotlin
import org.springframework.core.io.ClassPathResource
import org.springframework.core.io.Resource

class UserSqlMapConfig : SqlMapConfig {
    override fun sqlMapResource(): Resource {
        return ClassPathResource("sqlmap.xml", UserDao::class.java)
    }
}
```

그러면 `SqlServiceContext`는 구체적인 파일 위치를 몰라도 됩니다.

```kotlin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class SqlServiceContext {

    @Autowired
    lateinit var sqlMapConfig: SqlMapConfig

    @Bean
    fun sqlService(): SqlService {
        val sqlService = OxmSqlService()
        sqlService.setUnmarshaller(unmarshaller())
        sqlService.setSqlRegistry(sqlRegistry())
        sqlService.setSqlmap(sqlMapConfig.sqlMapResource())
        return sqlService
    }
}
```

이 구조의 핵심은 **재사용되는 설정은 내부 구현을 감추고, 바뀌는 지점만 인터페이스로 외부에 노출한다**는 것입니다.

***설정 클래스 자체가 인터페이스를 구현하는 방식***

한 단계 더 나아가 `AppContext` 자체가 `SqlMapConfig`를 구현하도록 바꿉니다. 그러면 별도의 `UserSqlMapConfig` 빈을 만들지 않아도 됩니다.

```kotlin
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.context.annotation.PropertySource
import org.springframework.core.io.ClassPathResource
import org.springframework.core.io.Resource
import org.springframework.transaction.annotation.EnableTransactionManagement

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = ["springbook.user"])
@Import(SqlServiceContext::class)
@PropertySource("/database.properties")
class AppContext : SqlMapConfig {

    override fun sqlMapResource(): Resource {
        return ClassPathResource("sqlmap.xml", UserDao::class.java)
    }
}
```

이렇게 하면 `SqlServiceContext`는 `SqlMapConfig` 타입 빈만 찾으면 되고, 그것이 `AppContext`이든 별도 설정 클래스이든 상관없습니다. 설정 재사용성과 애플리케이션별 커스터마이징 지점을 동시에 만족시키는 구조입니다.

***`@Enable*`\*\*\*\* \*\*\*\*애노테이션으로 더 단순하게 만들기***

마지막으로 `@Import(SqlServiceContext::class)`마저 감추고, 의미를 더 잘 드러내는 커스텀 애노테이션을 만듭니다.

```kotlin
import org.springframework.context.annotation.Import

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@Import(SqlServiceContext::class)
annotation class EnableSqlService
```

그다음 `AppContext`에서는 아래처럼 사용합니다.

```kotlin
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.PropertySource
import org.springframework.transaction.annotation.EnableTransactionManagement

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = ["springbook.user"])
@EnableSqlService
@PropertySource("/database.properties")
class AppContext : SqlMapConfig {
    override fun sqlMapResource(): Resource {
        return ClassPathResource("sqlmap.xml", UserDao::class.java)
    }
}
```

이제 코드만 봐도 “이 애플리케이션은 SQL 서비스를 활성화했다”는 의미가 드러납니다. 단순한 `@Import`보다 의도가 더 명확합니다.

***과거 방식과 현재 방식 비교***

이 절은 현재 Spring Boot 시대와 가장 강하게 연결해서 볼 수 있습니다.

***과거 방식***

* 공통 설정을 `SqlServiceContext` 같은 설정 클래스로 분리합니다.
* 애플리케이션별로 달라지는 부분은 `SqlMapConfig` 인터페이스로 외부화합니다.
* 재사용을 쉽게 하기 위해 `@Import` 또는 커스텀 `@Enable*` 애노테이션을 제공합니다.

***현재 방식***

* 명시적 opt-in 기능이라면 지금도 `@Enable*` 패턴이 유효합니다.
* 하지만 여러 프로젝트에 공통 기능을 배포할 때는 Spring Boot auto-configuration과 starter 모듈을 쓰는 경우가 더 많습니다.
* 즉, 단순 라이브러리 수준에서는 `@Import`나 `@Enable*`, Boot 생태계에서는 `@AutoConfiguration`과 starter 방식으로 발전했다고 볼 수 있습니다.

현재식으로 감각을 맞추면 다음처럼 정리할 수 있습니다.

***책의 관점***

* 공통 설정을 코드로 패키징해서 다른 애플리케이션이 재사용하게 한다.
* 애플리케이션은 필요한 위치 정보나 일부 값만 제공한다.

***현재의 관점***

* 기능을 켜는 명시적 토글이 필요하면 `@Enable*`를 쓴다.
* Boot 기반으로 널리 배포할 기능이면 auto-configuration과 starter로 제공한다.
* 설정값 주입은 `@ConfigurationProperties`와 외부 설정으로 푼다.

**설정 코드도 재사용 가능한 모듈로 만들 수 있고, 그 재사용 지점을 어떻게 추상화할 것인지가 핵심**이기 때문에 중요합니다.

#### 7.6.3부터 7.6.6까지 흐름 정리

7.6.3부터 7.6.6까지는 각각 따로 떨어진 기술 설명이 아닙니다. 하나의 큰 흐름으로 이어집니다.

7.6.3에서는 테스트용, 운영용, 공통 설정을 분리하고 `@Import`로 조합했습니다. 즉, 설정을 관심사별로 잘게 나누는 단계입니다.

7.6.4에서는 그 분리된 설정 중 어떤 것을 현재 환경에서 활성화할지 `@Profile`과 `@ActiveProfiles`로 결정했습니다. 즉, 환경에 따라 설정 조합을 동적으로 선택하는 단계입니다.

7.6.5에서는 같은 빈이 쓰는 값이 환경마다 다를 때, 그 차이를 프로퍼티 파일과 외부 설정으로 분리했습니다. 즉, 구현보다 설정값의 외부화가 필요한 영역을 정리하는 단계입니다.

7.6.6에서는 이렇게 쌓아온 설정을 재사용 가능한 모듈로 뽑아내고, `@Import`와 `@Enable*`를 통해 다른 애플리케이션에서도 쉽게 활성화하게 만들었습니다. 즉, 설정 자체를 라이브러리처럼 다루는 단계입니다.

결국 7.6.3부터 7.6.6까지를 한 문장으로 요약하면 이렇습니다. **DI 설정은 단순히 빈을 나열하는 파일이 아니라, 관심사별로 분리되고, 환경별로 선택되며, 값은 외부화되고, 필요하면 재사용 가능한 모듈로 추출될 수 있는 구조여야 한다**는 것입니다.

#### 7.6.3부터 7.6.6까지 현재 기준으로 다시 정리

***컨텍스트 분리***

과거에는 `AppContext`, `TestAppContext`, `SqlServiceContext`를 나눠 직접 조합했습니다. 지금도 설정 분리 원칙은 같지만, 테스트 전용 설정에는 `@TestConfiguration`, Boot 테스트 슬라이스, `@SpringBootTest`를 더 자주 사용합니다.

***프로파일***

과거에는 `@Profile("test")`, `@Profile("production")`를 설정 클래스에 붙이고 테스트에서 `@ActiveProfiles("test")`를 사용했습니다. 지금도 그대로 유효합니다. 다만 실제 실무에서는 `application-test.yml`, `application-prod.yml`처럼 외부 설정 파일과 함께 쓰는 경우가 훨씬 많습니다.

***프로퍼티 소스***

과거에는 `@PropertySource`, `Environment`, `@Value`로 외부 설정을 읽었습니다. 지금도 가능합니다. 하지만 현재는 Boot 외부 설정과 `@ConfigurationProperties`가 더 일반적이며, `spring.datasource.*` 같은 표준 키를 사용하면 `DataSource`를 직접 만들 필요도 줄어듭니다.

***설정 재사용***

과거에는 `@Import`와 커스텀 `@Enable*` 애노테이션으로 공통 기능을 묶어 재사용했습니다. 지금도 opt-in 성격의 기능에는 아주 좋은 방식입니다만 여러 프로젝트에서 재사용할 기능은 Spring Boot starter와 auto-configuration으로 제공하는 쪽이 더 일반적입니다.

**결국, XML을 없애는 것이 목적이 아니라, 설정을 코드로 옮긴 뒤 그 설정을 분리하고, 조합하고, 외부화하고, 재사용 가능하게 만드는 것이 목적**입니다.

### 7.6 전체 요약

처음에는 7.6 전체의 문제의식을 설명합니다. 핵심은 “XML을 자바 코드로 바꾸는 것” 자체가 목적이 아니라, **설정을 더 잘 나누고, 더 잘 조합하고, 더 잘 바꾸기 위한 구조**를 만드는 것이라고 잡으면 됩니다.

그다음에는 7.6.1, 7.6.2를 설명합니다. 여기서는 XML의 `<bean/>`, `<tx:annotation-driven/>`, `<jdbc:embedded-database/>`가 `@Bean`, `@EnableTransactionManagement`, 자바 코드 기반 DB 설정으로 이동하고, 이후 `@Autowired`, `@ComponentScan`, `@Repository`, `@Service`를 통해 수동 wiring 코드가 줄어드는 흐름을 설명하면 됩니다.

마지막에는 7.6.3부터 7.6.6까지를 하나의 흐름으로 묶으면 됩니다. 설정을 분리하고(`@Import`), 환경에 따라 선택하고(`@Profile`), 값은 외부 파일로 빼고(`@PropertySource`, 현재는 보통 외부 설정), 마지막으로 공통 설정을 재사용 가능한 모듈로 만들고(`@Enable*`) 활성화한다는 흐름입니다. 이게 7.6의 완성입니다.

#### 7.6.1 자바 코드를 이용한 빈 설정

7.6.1의 핵심은 XML에 있던 빈 설정을 자바 코드로 옮기는 것입니다. 기존에는 테스트가 XML 파일을 직접 바라봤습니다.

```kotlin
import org.junit.runner.RunWith
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner

@RunWith(SpringJUnit4ClassRunner::class)
// 기존 방식: XML 파일 경로를 직접 지정해서 DI 설정을 읽어옵니다.
@ContextConfiguration(locations = ["/test-applicationContext.xml"])
class UserDaoTest
```

이제는 자바 설정 클래스를 바라보게 바꿉니다.

```kotlin
import org.junit.runner.RunWith
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner

@RunWith(SpringJUnit4ClassRunner::class)
// 변경된 방식: XML 경로 대신 설정 클래스를 직접 지정합니다.
@ContextConfiguration(classes = [TestApplicationContext::class])
class UserDaoTest
```

이 변화의 의미는 단순히 문법이 바뀌었다는 데 있지 않습니다. DI 설정도 이제 자바 코드가 되므로 IDE 리팩터링, 타입 체크, 컴파일 시점 검증의 도움을 받을 수 있습니다.

하지만 처음부터 XML을 완전히 없애지는 않습니다. 책은 `@ImportResource`를 사용해 기존 XML을 임시로 가져오게 만듭니다.

```kotlin
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.ImportResource

@Configuration
// 과도기적 방식: 진입점은 자바 설정 클래스로 바꾸되,
// 실제 빈 정의는 아직 XML을 재사용합니다.
@ImportResource("/test-applicationContext.xml")
class TestApplicationContext
```

이 단계는 매우 중요합니다. 설정의 진입점은 자바로 바꿨지만, 실제 빈 정의는 아직 XML을 재사용합니다. 즉, **한 번에 갈아엎는 것이 아니라 점진적으로 옮기는 리팩터링**입니다.

그다음에는 XML의 `<bean/>`을 `@Bean` 메서드로 옮깁니다.

```kotlin
import javax.sql.DataSource
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.jdbc.datasource.SimpleDriverDataSource

@Configuration
class TestApplicationContext {

    @Bean
    fun dataSource(): DataSource {
        val ds = SimpleDriverDataSource()

        // XML의 <property name="driverClass" ... /> 가 자바 코드로 옮겨진 부분입니다.
        ds.setDriverClass(com.mysql.jdbc.Driver::class.java)

        // XML의 url 프로퍼티가 코드 안으로 들어왔습니다.
        ds.setUrl("jdbc:mysql://localhost/springbook?characterEncoding=UTF-8")

        // username, password 역시 문자열 기반 XML 설정에서 코드 기반 설정으로 이동했습니다.
        ds.username = "spring"
        ds.password = "book"

        return ds
    }
}
```

또 XML 전용 태그도 자바 코드로 바꿉니다. 예를 들어 `<tx:annotation-driven/>`는 `@EnableTransactionManagement`로 바뀝니다.

```kotlin
import org.springframework.context.annotation.Configuration
import org.springframework.transaction.annotation.EnableTransactionManagement

@Configuration
// XML의 <tx:annotation-driven/> 를 애노테이션 한 줄로 대체한 것입니다.
@EnableTransactionManagement
class TestApplicationContext
```

여기까지가 7.6.1의 핵심입니다. **XML을 자바 코드로 옮기되, 한 번에 전환하지 않고 안전하게 단계적으로 옮긴다**는 것입니다.

#### 7.6.1 현재식 해석

책의 방식은 스프링 3.1 기준으로는 매우 자연스럽습니다. 하지만 지금 실무에서는 보통 여기서 더 나아갑니다.

과거에는 `@Configuration`과 `@Bean`으로 XML을 대체하는 것 자체가 중요한 변화였습니다. 반면 현재는 Spring Boot가 널리 쓰이기 때문에 공통 인프라 빈은 자동 설정에 맡기고, 애플리케이션 고유 빈만 직접 정의하는 경우가 많습니다. 또한 환경별 설정값은 코드에 넣기보다 properties, YAML, 환경 변수 같은 외부 설정으로 분리하는 것이 기본입니다.

즉, 7.6.1을 현재 언어로 바꾸면 이렇게 됩니다. **XML → 자바 설정으로 이동**은 출발점이었고, 지금은 **자바 설정 → 최소 수동 설정 + 자동 설정 + 외부 설정**으로 더 발전한 상태입니다.

#### 7.6.2 빈 스캐닝과 자동와이어링

7.6.2는 7.6.1의 다음 단계입니다. 자바 코드로 설정을 옮겼더니 XML은 줄었지만, 여전히 설정 클래스 안에서 setter를 직접 호출하며 빈을 연결하는 코드가 남아 있습니다.

예를 들어 이런 코드입니다.

```kotlin
@Bean
fun userDao(): UserDao {
    val dao = UserDaoJdbc()

    // 기존 방식: 설정 클래스가 직접 의존 오브젝트를 넣어줍니다.
    dao.setDataSource(dataSource())
    dao.setSqlService(sqlService())

    return dao
}
```

책은 이 수동 연결 코드를 `@Autowired`로 줄여 갑니다. 즉, 의존관계 선언을 설정 클래스가 아니라 빈 클래스 쪽으로 옮깁니다.

```kotlin
import javax.sql.DataSource
import org.springframework.beans.factory.annotation.Autowired

class UserDaoJdbc : UserDao {

    private lateinit var dataSource: DataSource
    private lateinit var sqlService: SqlService

    @Autowired
    // 변경 포인트 1: dataSource를 설정 클래스가 넣는 대신,
    // 스프링 컨테이너가 자동으로 주입하게 만듭니다.
    fun setDataSource(dataSource: DataSource) {
        this.dataSource = dataSource
    }

    @Autowired
    // 변경 포인트 2: sqlService도 동일하게 자동 주입 대상으로 바꿉니다.
    fun setSqlService(sqlService: SqlService) {
        this.sqlService = sqlService
    }
}
```

이렇게 하면 설정 클래스는 객체를 만들기만 하면 됩니다.

```kotlin
@Bean
// 변경된 방식: 의존관계 주입 코드를 제거하고 생성만 남깁니다.
fun userDao(): UserDao = UserDaoJdbc()
```

이후에는 빈 등록 자체도 자동화합니다. `@ComponentScan`을 켜고 클래스에 `@Repository`, `@Service` 같은 애노테이션을 붙이면 빈을 자동 등록할 수 있습니다.

```kotlin
import javax.sql.DataSource
import org.springframework.stereotype.Repository

@Repository
// 변경 포인트: 이제 이 클래스는 @Bean 메서드 없이도 자동 등록 대상이 됩니다.
class UserDaoJdbc(
    // 현재식 해석: 생성자 주입으로 쓰면 의존관계가 더 명확해집니다.
    private val dataSource: DataSource,
    private val sqlService: SqlService,
) : UserDao
```

```kotlin
import org.springframework.mail.MailSender
import org.springframework.stereotype.Service

@Service("userService")
// 빈 이름을 명시한 이유: 테스트용 UserService와 충돌할 수 있기 때문입니다.
class UserServiceImpl(
    private val userDao: UserDao,
    private val mailSender: MailSender,
) : UserService
```

즉, 7.6.2의 메시지는 분명합니다. **설정 클래스의 수동 연결 코드를 점점 줄이고, 빈 스스로가 필요한 의존관계를 선언하도록 바꾸자**는 것입니다.

#### 7.6.2 현재식 해석

책은 설명을 위해 필드 주입과 setter 주입을 많이 사용합니다. 하지만 현재 실무에서는 생성자 주입이 더 일반적입니다.

이유는 단순합니다. 생성자 주입은 객체가 반드시 필요한 의존성을 생성 시점에 드러내고, 테스트하기도 쉽고, Kotlin에서는 `val` 기반으로 불변하게 다루기 쉽기 때문입니다.

그래서 7.6.2를 현재식으로 읽으면 핵심은 동일하지만 표현 방식은 조금 다릅니다.

과거에는 **수동 setter 주입 → `@Autowired` 필드/세터 주입 → `@ComponentScan` 자동 등록** 이라는 변화였다면,

지금은 보통 **수동 설정 제거 → 컴포넌트 스캔 + 생성자 주입 + 필요할 때만 `@Configuration`** 이라는 형태로 정리됩니다.

#### 7.6.3 컨텍스트 분리와 `@Import`

7.6.3부터는 설정을 “옮기는 것”을 넘어서 “나누는 것”으로 넘어갑니다.

앞 절까지의 작업으로 XML은 대부분 자바 코드로 옮겨졌고 자동 등록과 자동 주입도 적용되었습니다. 그런데 여전히 하나의 설정 안에 서로 성격이 다른 내용이 섞여 있습니다.

예를 들면 이런 식입니다.

운영에도 필요한 빈이 있습니다. `userDao`, `userService`, `dataSource`, `transactionManager`, `sqlService` 같은 것들입니다.

반면 테스트에서만 필요한 빈도 있습니다. `testUserService`, `DummyMailSender` 같은 것들입니다.

이 둘을 같은 설정 클래스에 계속 섞어두면 어떤 설정이 운영용인지, 어떤 설정이 테스트용인지 드러나지 않습니다. 그래서 책은 테스트 전용 설정을 `TestAppContext`로 분리합니다.

```kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.mail.MailSender

@Configuration
class TestAppContext {

    @Bean
    // 테스트 전용 서비스: 운영용 userService와 분리된 대체 빈입니다.
    fun testUserService(): UserService = TestUserService()

    @Bean
    // 테스트에서는 실제 메일을 보내지 않기 위해 Dummy 구현을 사용합니다.
    fun mailSender(): MailSender = DummyMailSender()
}
```

이후 테스트에서는 여러 설정 클래스를 함께 사용합니다.

```kotlin
@RunWith(SpringJUnit4ClassRunner::class)
// 변경 포인트: 하나의 설정이 아니라,
// 운영용 AppContext + 테스트용 TestAppContext를 함께 조합합니다.
@ContextConfiguration(classes = [TestAppContext::class, AppContext::class])
class UserDaoTest
```

그리고 공통 설정을 더 깔끔하게 합치기 위해 `@Import`를 사용합니다.

```kotlin
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.transaction.annotation.EnableTransactionManagement

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = ["springbook.user"])
// 공통 설정 분리: SQL 서비스 관련 설정을 별도 클래스로 나누고 다시 가져옵니다.
@Import(SqlServiceContext::class)
class AppContext
```

즉, 7.6.3은 이렇게 정리할 수 있습니다. **설정을 한 군데 몰아넣지 말고 성격별로 나눈 뒤, 필요한 곳에서 조합하자**는 것입니다.

#### 7.6.4 프로파일

설정을 나누는 것만으로는 충분하지 않습니다. 이제는 “어떤 설정을 현재 환경에서 활성화할 것인가”를 선택해야 합니다.

예를 들어 테스트에서는 `DummyMailSender`가 필요합니다. 운영에서는 실제 메일 서버와 연결되는 `JavaMailSenderImpl`이 필요합니다.

책은 이 문제를 `@Profile`로 풉니다.

```kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import org.springframework.mail.MailSender

@Configuration
@Profile("test")
// 이 설정 클래스는 test 프로파일이 활성화될 때만 등록됩니다.
class TestAppContext {

    @Bean
    fun testUserService(): UserService = TestUserService()

    @Bean
    fun mailSender(): MailSender = DummyMailSender()
}
```

```kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import org.springframework.mail.MailSender
import org.springframework.mail.javamail.JavaMailSenderImpl

@Configuration
@Profile("production")
// 운영 환경에서만 사용할 메일 발송기입니다.
class ProductionAppContext {

    @Bean
    fun mailSender(): MailSender {
        val sender = JavaMailSenderImpl()
        sender.host = "localhost"
        return sender
    }
}
```

그리고 테스트에서는 `@ActiveProfiles("test")`를 사용합니다.

```kotlin
import org.junit.runner.RunWith
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner

@RunWith(SpringJUnit4ClassRunner::class)
// 테스트 실행 시 test 프로파일을 활성화합니다.
@ActiveProfiles("test")
@ContextConfiguration(classes = [AppContext::class])
class UserServiceTest
```

즉, 7.6.4의 핵심은 이것입니다. **같은 역할의 빈이라도 환경에 따라 다른 구현을 선택해 활성화할 수 있어야 한다**는 것입니다.

#### 7.6.4 현재식 해석

이 흐름은 지금도 그대로 유효합니다. 다만 현재 실무에서는 빈 구현 자체가 다를 때만 `@Profile`을 적극적으로 사용하고, 단순히 값만 달라지는 경우는 `application-test.yml`, `application-prod.yml` 같은 외부 설정 파일로 처리하는 경우가 더 많습니다.

즉, 현재 기준으로는 이렇게 구분하면 이해가 쉽습니다.

구현이 다르면 `@Profile`을 사용합니다. 값만 다르면 외부 설정 파일을 사용합니다.

#### 7.6.5 프로퍼티 소스

7.6.5는 환경 차이 중에서도 “구현이 아니라 값이 다른 경우”를 다룹니다.

예를 들어 `dataSource`는 테스트와 운영 모두 필요합니다. 하지만 URL, 사용자 이름, 비밀번호, 드라이버 클래스는 환경마다 달라질 수 있습니다.

책은 이를 위해 `database.properties`를 도입합니다.

```yml
# 변경 포인트: 코드 안에 박혀 있던 DB 설정값을 외부 파일로 분리합니다.
db.driverClass=com.mysql.jdbc.Driver
db.url=jdbc:mysql://localhost/springbook?characterEncoding=UTF-8
db.username=spring
db.password=book
```

그리고 `@PropertySource`로 파일을 읽고 `Environment`에서 값을 꺼냅니다.

```kotlin
import javax.sql.DataSource
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.PropertySource
import org.springframework.core.env.Environment
import org.springframework.jdbc.datasource.SimpleDriverDataSource

@Configuration
@PropertySource("/database.properties")
// 외부 프로퍼티 파일을 스프링 환경에 등록합니다.
class AppContext {

    @Autowired
    lateinit var env: Environment

    @Bean
    fun dataSource(): DataSource {
        val ds = SimpleDriverDataSource()

        // 기존에는 코드에 직접 적었던 값을,
        // 이제는 Environment를 통해 외부 파일에서 읽어옵니다.
        ds.setDriverClass(Class.forName(env.getProperty("db.driverClass")) as Class<out java.sql.Driver>)
        ds.url = env.getProperty("db.url")
        ds.username = env.getProperty("db.username")
        ds.password = env.getProperty("db.password")

        return ds
    }
}
```

또는 `@Value`로 값을 주입받습니다.

```kotlin
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.PropertySource

@Configuration
@PropertySource("/database.properties")
class AppContext {

    // Environment#getProperty()를 직접 반복 호출하는 대신,
    // 필요한 값을 필드에 바로 주입받습니다.
    @Value("\${db.driverClass}")
    lateinit var driverClass: Class<out java.sql.Driver>

    @Value("\${db.url}")
    lateinit var url: String

    @Value("\${db.username}")
    lateinit var username: String

    @Value("\${db.password}")
    lateinit var password: String
}
```

7.6.5의 메시지는 분명합니다. **환경에 따라 달라지는 값은 코드에 하드코딩하지 말고 외부 설정으로 분리하자**는 것입니다.

#### 7.6.5 현재식 해석

이 절은 현재 실무와 가장 직접적으로 이어집니다.

`@PropertySource`, `Environment`, `@Value`는 지금도 모두 사용 가능합니다. 하지만 현재 Spring Boot에서는 외부 설정을 properties, YAML, 환경 변수, 커맨드라인 인자 등 다양한 소스에서 읽고, 관련 값을 하나의 타입으로 묶는 `@ConfigurationProperties`를 많이 사용합니다.

즉, 과거에는

`@PropertySource` + `Environment` 또는 `@Value`

가 중심이었다면,

현재는

`application.yml` + `@ConfigurationProperties` 또는 Boot 자동 설정

이 더 일반적입니다.

그래서 7.6.5는 오늘날의 언어로 바꾸면 **설정값 외부화의 출발점**이라고 이해하시면 됩니다.

#### 7.6.6 빈 설정의 재사용과 `@Enable*`

7.6.6은 7.6 전체의 마무리입니다. 앞에서 설정을 자바 코드로 옮기고, 자동 등록과 자동 주입을 적용하고, 설정을 분리하고, 프로파일과 외부 설정까지 적용했습니다.

이제 마지막 질문이 남습니다.

**이렇게 만든 설정을 다른 프로젝트에서도 재사용할 수 없을까?**

책은 SQL 서비스 관련 설정을 `SqlServiceContext`라는 별도 설정 클래스로 묶습니다. 그리고 애플리케이션마다 달라질 수 있는 SQL 매핑 파일 위치는 `SqlMapConfig` 인터페이스로 외부화합니다.

```kotlin
import org.springframework.core.io.Resource

interface SqlMapConfig {
    // 확장 포인트: 애플리케이션마다 다른 SQL 매핑 리소스 위치를 여기서 제공합니다.
    fun sqlMapResource(): Resource
}
```

```kotlin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class SqlServiceContext {

    @Autowired
    // 공통 설정은 구체적인 파일 위치를 모르고,
    // 인터페이스를 통해 바깥에서 주입받습니다.
    lateinit var sqlMapConfig: SqlMapConfig

    @Bean
    fun sqlService(): SqlService {
        val sqlService = OxmSqlService()
        sqlService.setUnmarshaller(unmarshaller())
        sqlService.setSqlRegistry(sqlRegistry())

        // 변경 포인트: SQL 매핑 파일 위치를 직접 고정하지 않고,
        // SqlMapConfig를 통해 외부화합니다.
        sqlService.setSqlmap(sqlMapConfig.sqlMapResource())

        return sqlService
    }
}
```

그리고 애플리케이션 설정은 이 인터페이스만 구현하면 됩니다.

```kotlin
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.context.annotation.PropertySource
import org.springframework.core.io.ClassPathResource
import org.springframework.core.io.Resource
import org.springframework.transaction.annotation.EnableTransactionManagement

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = ["springbook.user"])
@Import(SqlServiceContext::class)
@PropertySource("/database.properties")
class AppContext : SqlMapConfig {

    override fun sqlMapResource(): Resource {
        // 애플리케이션마다 바뀌는 부분은 여기 하나로 모았습니다.
        return ClassPathResource("sqlmap.xml", UserDao::class.java)
    }
}
```

여기서 책은 한 단계 더 나아갑니다. `@Import(SqlServiceContext::class)` 자체도 감추고 의미를 더 잘 드러내는 커스텀 애노테이션을 만듭니다.

```kotlin
import org.springframework.context.annotation.Import

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
// 내부적으로는 @Import를 사용하지만,
// 사용하는 쪽에서는 “SQL 서비스를 활성화한다”는 의도가 더 잘 드러납니다.
@Import(SqlServiceContext::class)
annotation class EnableSqlService
```

```kotlin
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.PropertySource
import org.springframework.transaction.annotation.EnableTransactionManagement

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = ["springbook.user"])
@EnableSqlService
@PropertySource("/database.properties")
class AppContext : SqlMapConfig
```

이제 코드만 봐도 “이 애플리케이션은 SQL 서비스를 활성화했다”는 의도가 드러납니다.

#### 7.6.6 현재식 해석

다만 오늘날에는 이 패턴이 두 갈래로 발전했습니다.

첫째, 명시적으로 켜야 하는 기능은 여전히 `@Enable*` 패턴이 유효합니다. 둘째, 여러 프로젝트에 공통 기능을 배포하는 경우는 Spring Boot starter와 auto-configuration으로 더 자주 확장됩니다.

즉, 과거에는 **공통 설정 클래스 + `@Import` + 커스텀 `@Enable*`** 가 재사용의 중심이었다면,

현재는 **공통 설정 클래스 + 필요 시 `@Enable*`**, 그리고 Boot 생태계에서는 **starter + auto-configuration** 으로 더 발전했다고 이해하시면 됩니다.

#### 7.6 전체를 한 흐름으로 정리

이제 7.6 전체를 다시 한 번 묶어보겠습니다.

7.6.1은 XML 설정을 자바 코드로 옮깁니다. 즉, DI 설정의 표현 수단이 XML에서 `@Configuration`, `@Bean`으로 이동합니다.

7.6.2는 자바 설정 안에 남아 있던 수동 wiring을 줄입니다. 즉, `@Autowired`, `@ComponentScan`, `@Repository`, `@Service`를 통해 빈 등록과 연결을 자동화합니다.

7.6.3은 설정을 성격별로 분리합니다. 즉, 테스트용, 운영용, 공통 설정을 나누고 `@Import`로 조합합니다.

7.6.4는 환경별로 어떤 설정을 활성화할지 결정합니다. 즉, `@Profile`, `@ActiveProfiles`로 test와 production을 구분합니다.

7.6.5는 구현이 아니라 값의 차이를 외부화합니다. 즉, `@PropertySource`, `Environment`, `@Value`로 설정값을 코드 밖으로 뺍니다.

7.6.6은 마지막으로 설정 자체를 재사용 가능한 모듈로 만듭니다. 즉, `SqlServiceContext`, `SqlMapConfig`, `@Enable*`로 공통 설정을 다른 애플리케이션에서도 가져다 쓸 수 있게 만듭니다.

결국 7.6 전체는 이렇게 정리할 수 있습니다.

**DI 설정은 단순히 빈을 나열하는 XML이 아니다. 설정은 코드가 될 수 있고, 자동화될 수 있고, 분리될 수 있고, 환경에 따라 선택될 수 있고, 값은 외부화될 수 있으며, 심지어 재사용 가능한 모듈이 될 수도 있다.**

스프링 3.1의 DI는 “XML을 버렸다”가 핵심이 아닙니다. 진짜 핵심은 **설정을 코드로 옮긴 뒤, 그 설정을 더 작게 나누고, 더 명확하게 조합하고, 환경에 따라 바꾸고, 필요하면 재사용 가능한 모듈로 발전시켰다**는 데 있습니다.

그리고 이 흐름은 지금도 이어집니다. 현재는 여기에 Spring Boot 자동 설정, 외부 설정, `@ConfigurationProperties`, starter 같은 방식이 더해졌을 뿐입니다. 즉, 책의 예제는 오래되었어도 방향은 여전히 유효합니다. **7.6은 스프링 DI가 단순한 설정 기술이 아니라, 애플리케이션 구조를 조직하는 방식이라는 점을 보여주는 장**이라고 정리하시면 됩니다.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wonjoon.gitbook.io/joons-til/books/tobys-spring-vol1/7.-applications-of-core-spring-technologies.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
