Back to writing
토비의 스프링 - 오브젝트와 의존관계
초난감 DAO
User
data class User(
val id: String,
val name: String,
val password: String,
)
UserDao
- DB 연결을 위한 Connection을 가져온다.
- SQL을 담은 Statement 를 만든다.
- 만들어진 Statement를 실행한다.
- 조회의 경우 SQL 쿼리의 실행결과를
ResultSet으로 받아서 정보를 저장할 오브젝트에 옮겨준다. - 작업 중에 생성된
Connection,Statement,ResultSet같은 리소스는 작업을 마친 후 반드시 닫아준다. - JDBC API가 만들어내는 예외를 잡아서 직접 처리하거나, 메소드에 throws를 선언해서 예외가 발생하면 메소드 밖으로 던지게 한다.
class UserDao {
@Throws(SQLException::class, ClassNotFoundException::class)
fun add(user: User) {
Class.forName("org.h2.Driver")
val connection = java.sql.DriverManager.getConnection("jdbc:h2:mem:testdb", "sa", "")
val ps = connection.prepareStatement("INSERT INTO users(id, name, password) VALUES(?, ?, ?)")
ps.setString(1, user.id)
ps.setString(2, user.name)
ps.setString(3, user.password)
ps.executeUpdate()
ps.close()
connection.close()
}
@Throws(SQLException::class, ClassNotFoundException::class)
fun get(id: String): User {
Class.forName("org.h2.Driver")
val connection = java.sql.DriverManager.getConnection("jdbc:h2:mem:testdb", "sa", "")
val ps = connection.prepareStatement("SELECT * FROM users WHERE id = ?")
ps.setString(1, id)
val rs = ps.executeQuery()
rs.next()
val user = User(
rs.getString("id"),
rs.getString("name"),
rs.getString("password")
)
rs.close()
ps.close()
connection.close()
return user
}
}
DAO의 분리
관심사의 분리
- 분리와 확장을 고려한 설계를 통해 변경이 일어날 때 필요한 작업을 최소화하고 변경이 다른 곳에 문제를 일으키지 않게 해야함.
- 관심사가 같은 것 끼리 모으고 다른 것은 분리해줌으로써 같은 관심에 효과적으로 집중할 수 있게 만들어 줌
커넥션 만들기의 추출
UserDao의 관심사
- DB와 연결을 위한 커넥션을 어떻게 가져올까?
- User 등록을 위해 SQL문장을 만들 Statement를 만들고 실행
- 사용한 리소스를 반환
- DB 연결과 관련된 부분에 변경이 일어났을 경우 수정이 간단해짐.
DB 커넥션 만들기의 독립
UserDao를 N사와 D사에 제공하여 서로 다른 DB를 사용할 수 있게 하는 방법
- 템플릿 메소드 패턴: 슈퍼클래스에 기본적인 로직의 흐름을 만들고, 그 기능의 일부를 추상 메소드나 오버라이딩이 가능한
protected메소드 등으록 만든 뒤 서브클래스에서 이런 메소드를 필요에 맞게 구현해서 사용하도록 하는 방법 - 팩토리 메소드 패턴: 서브 클래스에서 구체적인 오브젝트 생성 방법을 결정하는 방법
위 코드의 문제
- 다중 상속의 문제
- 클래스의 밀접한 관계(서브클래스가 슈퍼클래스의 기능을 사용할 수 있음)
- 다른 Dao가 만들어진다면 중복되는 getConnection()을 모두 만들어 주어야함
DAO의 확장
클래스의 분리
다른 관심사를 독립된 메소드를 만들어 분리, 상하위 클래스로 분리 했음. 이번엔 완전히 독립된 클래스로 만들기
- N사와 D사 UserDao 분리가 다시 불가능해짐
- DB 커넥션을 제공하는 클래스가 어떤 것인지를 UserDao가 구체적으로 알고있어야 함.
인터페이스의 도입
- 오브젝트 사이에 관계가 만들어지려면 직접 생성자를 호출해서 만드는 방법도 있지만 외부에서 만들어준 것을 가져오는 방법도 있다.
- 외부에서 만든 오브젝트를 전달받으려면 메소드 파라미터나 생성자 파라미터를 이용하면 된다.
- 클래스 사이의 관계가 아닌 오브젝트 사이의 다이나믹한 관계가 만들어짐
원칙과 패턴
개방 폐쇄 원칙
- 클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.
- UserDao는 DB 연결 방법 기능에 대한 확장이 열려있다. -> UserDao 변경없이 연결 방법을 늘릴 수 있음
높은 응집도와 낮은 결합도
- 하나의 모듈, 하나의 클래스가 하나의 책임 또는 관심사에 집중되어 있다.
- 변경이 일어날 때 모듈의 많은 부분이 함께 바뀐다면 응집도가 높다고 말할 수 있다.
- 커넥션 만드는 기능을 추가한다 한다면, ConnectionMaker로 DB 연결을 독립시켰기 때문에 커넥션 풀은 활용하는 ConnectionMaker 구현 클래스를 추가하면 된다.
전략 패턴
- UserDaoTest(main) - UserDao - ConnectionMaker 구조를 전략 패턴이라 볼 수 있다.
- 자신의 기능 맥락에서 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴이다.
제어의 역전(IoC)
오브젝트 팩토리
UserDaoTest(main)이 어떤ConnectionMaker를 사용할지 결정하는 기능을 떠맡았다.- 뭔가 문제가 있음
팩토리
오브젝트 팩토리의 활용
- UserDao 외에 다른 DAO 생성 기능을 넣으면 어떻게 될까? -> 클래스만큼
ConnectionMaker생성코드가 늘어남 - 오브젝트 생성코드가 중복되는 건 좋지 않음
제어권의 이전을 통한 제어관계 역전
- 제어의 역전이라는 건, 프로그램의 제어 흐름 구조가 뒤바뀌는 것
main()메소드와 같이 프로그램의 시작 지점에서 사용할 오브젝트 결정, 생성, 호출.. 작업이 반복된다.- 제어의 역전에서는 오브젝트가 자신이 사용할 오브젝틀르 스스로 선택하지 않는다.
- 추상 UserDao 에서
getConnection()을 구현하면 슈퍼클래스의 템플릿 메소드에 의해 필요할 때 호출된다. 제어권을 상위에 넘기고 자신은 필요할 때 호출되어 사용되도록 한다. -> 제어의 역전 - 자연스럽게 관심을 분리하고 책임을 나누고 유연하게 확장 가능한 구조로 만들기 위해 DaoFactory 를 도입했던 과정이 IoC를 적용하는 작업이었다고 볼 수 있다.
스프링의 IoC
오브젝트 팩토리를 이용한 스프링 IoC
애플리케이션 컨텍스트와 설정정보
- 스프링에서는 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 bean이라고 부른다. -> 제어 역전이 적용된 오브젝트
- 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리 라고 부른다.
- 보통 빈 팩토리보다는 애플리케이션 전반에 걸쳐 모든 구성요소의 제어 작업 담당하는
application context를 주로 사용한다.
애플리케이션 컨텍스트의 동작방식
DaoFactory 와 같은 오브젝트 팩토리에서 사용했던 IoC 원리를 그대로 적용하는 데 애플리케이션 컨텍스트를 사용하는 이유는 범용적이고 유연한 방법으로 IoC 기능을 확장하기 위해서다.
DaoFactory 를 오브젝트 팩토리로 직접 사용했을 때와 비교해서 애플리케이션 컨텍스트를 사용했을 때 얻을 수 있는 장점은 다음과 같다.
- 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.
- 애플리케이션 컨텍스트를 사용하면 일관된 방식으로 원하는 오브젝트를 가져올 수 있다.
- xml이나 yaml 처럼 단순한 방법을 사용해 설정정보를 만들 수도 있다.
- 애플리케이션 컨텍스트는 종합 IoC 서비스를 제공해준다.
- 오브젝트가 만들어지는 방식, 시점, 전략을 다르게 가져갈 수도 있다. 기타 등등..
- 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공한다.
getBean으로 빈을 찾아주고 타입만으로 검색하거나 애노테이션 설정이 되어있는 빈을 찾을 수도 있다.
스프링 IoC의 용어 정리
- Bean: 스프링이 IoC 방식으로 관리하는 오브젝트
- Bean Factory: IoC를 담당하는 핵심 컨테이너
- Application Context: 빈 팩토리를 확장한 IoC 컨테이너 (상속)
싱글톤 레지스트리와 오브젝트 스코프
userDao()를 호출했을 때 리턴되는 오브젝트는 매번 달라진다. (동일성 x, 동등성 o)- 애플리케이션 컨텍스트를 통해 가져온 오브젝트는 같은 오브젝트이다.
싱글톤 레지스트리로서의 애플리케이션 컨텍스트
- 애플리케이션 컨텍스트는 IoC 컨테이너이자 싱글톤을 저장하고 관리하는 싱글톤 레지스트리 다.
- 스프링은 기본적으로 별다른 설정을 하지 않으면 내부에서 생성하는 빈 오브젝트를 모두 싱클톤으로 만든다.
서버 애플리케이션과 싱글톤
- 스프링이 싱글톤으로 빈을 만드는 이유는 대량의 요청이 있는 엔터프라이즈 시스템에서 매번 새로운 객체를 만드는 것이 서버에 부담이 되기 때문이다.
싱글톤 패턴의 한계
- private 생성자를 갖고 있기 때문에 상속할 수 없다. (객체지향의 장점인 다형성 이용 불가)
- 싱글톤은 만들어지는 방식이 제한적이기 때문에 테스트 하기 힘들다.
- 서버 환경에서는 싱글톤이 하나만 만들어지는 것은 보장하지 못한다.
- 싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.
싱글톤 레지스트리
- 스프링은 서버환경에서 싱글톤이 만들어져서 서비스 오브젝트 방식으로 사용되는 것은 적극 지지한다.
- 하지만 구현 방식의 단점 때문에 기능을 제공하고 그게 싱글톤 레지스트리 다.
싱글톤과 오브젝트의 상태
- 멀티스레드 환경이라면 무상태 방식으로 만들어야 한다.
스프링 빈의 스코프
- 빈 스코프는 빈이 생성되고, 존재하고, 적용되는 범위이다.
- 기본 스코프는 싱글톤이다.
prototype scope가 있는데 컨테이너에 빈을 요청할 때마다 새로운 오브젝트를 만들어준다.- 요청마다 생성하는
request scope가 있고, 웹의 세션과 스코프가 유사한session scope가 있다.
의존관계 주입(DI)
제어의 역전과 의존관계 주입
- 스프링 IoC 기능의 대표적인 동작원리는 주로 의존관계 주입이라고 불린다.
런타임 의존관계 설정
- A 가 B에 의존하고 있다면 B가 변한다면 A에 영향을 미친다.
- B의 메소드나 로직이 바뀐다면 A도 수정해야할 수 있다.
// B 클래스: A가 의존하는 대상
class B {
fun doWork() {
println("B: 기본 작업 수행")
}
}
// A 클래스: B에 의존
class A(private val b: B) {
fun perform() {
println("A: 작업 수행 시작")
b.doWork()
println("A: 작업 수행 완료")
}
}
// 실행 예제
fun main() {
val b = B()
val a = A(b)
println("=== 첫 번째 실행 ===")
a.perform()
// B를 수정 – doWork 메서드의 동작이 바뀜
val bModified = object : B() {
override fun doWork() {
println("B (수정됨): 변경된 작업 수행")
}
}
val aWithModifiedB = A(bModified)
println("=== 수정된 B로 실행 ===")
aWithModifiedB.perform()
}
UserDao의 의존관계
class UserDao(
private val connectionMaker: ConnectionMaker,
) {
...
}
UserDao가ConnectionMaker에 의존한다.- 인터페이스를 적용하면 구현 클래스와의 결합은 느슨해진다. -> 구현 클래스의 내부로직이 바뀌어도 변경할 필요없다.
- 의존관계 주입이란 다음과 같은 세 가지 조건을 충족하는 작업을 말한다.
- 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다.
- 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
- 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.
- DI는 자신이 사용할 오브젝트에 대한 선택과 생성 제어권을 외부로 넘기고 자신을 수동적으로 주입받은 오브젝트를 사용한다는 점에서 IoC에 개념에 잘 들어맞는다.
의존관계 검색과 주입
- 의존관계 검색은 애플리케이션 컨텍스트의
getBean()메소드를 사용해 의존관계 검색을 한다.
class UserDao {
private val connectionMaker: ConnectionMaker
init {
val context = AnnotationConfigApplicationContext(DaoFactory::class.java)
connectionMaker = context.getBean("connectionMaker", ConnectionMaker::class.java)
}
- 하지만 스프링 API가 나타나기 때문에 의존관계 주입이 훨씬 단순하고 깔끔하다.
메소드를 이용한 의존관계 주입
- 의존관계 주입 시 반드시 생성자를 사용해야 하는 것은 아니다.
XML 을 이용한 설정
DataSource 인터페이스로 변환
ConnectionMaker는 커넥션을 생성해주는 기능 하나만을 정의한 매우 단순한 인터페이스다.- 자바에서는 DB 커넥션을 가져오는 오브젝트의 기능을 추상화해서 비슷한 용도로 사용할 수 있게 만들어진
DataSource라는 인터페이스가 이미 존재한다.
정리
스프링이란 어떻게 오브젝트가 설계되고, 만들어지고, 어떻게 관계를 맺고 사용되는지에 관심을 갖는 프레임워크