[토비의 스프링1] 7.1 SQL과 DAO의 분리

2022. 6. 26. 16:51Spring/개념

DAO와 SQL을 분리해야하는 이유

  • DAO는 데이터를 가져오고 조작하는 작업의 인터페이스일 뿐이다.
  • 데이터 액세스 로직은 바뀌지 않더라도 DB의 테이블, 필드 이름과 SQL문장이 바뀔 수 있다. (어떤 이유든지 SQL변경이 필요한 상황이 발생하면 SQL을 담고 있는 DAO코드가 수정될 수 밖에 없다.
  • 현재 동작하고 있는 애플리케이션에서 사용되는 SQL이 정확히 어떤 것인지 알려면 최종 빌드에 사용한 자바 코드를 가져와 일일이 코드를 뒤져봐야 하기 때문이다.

7.1.1 XML 설정을 이용한 분리

SQL을 스프링의 XML설정파일로 빼오는 것이다.

 

개별 SQL프로퍼티 방식

public class UserDaoJdbc implements UserDao{
	private String sqlAdd;						// 요기에 sql문 주입
    
    public void setSqlAdd(String sqlAdd) {
    	this.sqlAdd = sqlAdd;
    }
public void add(User user) {
	this.jdbcTemplate.update(
    	this.sqlAdd,						// 실제 사용
        user.getId() ...					// 생략
    )
}

이렇게 sql문을 xml파일로 주입시켜 사용한다.

 

SQL 맵 프로퍼티 방식

SQL이 많아지면 그때마다 DAO에  DI용 프로퍼티를 추가하기 귀찮으니 컬렉션으로 담아두는 방법을 시도해보자.

public class UserDaoJdbc implements UserDao{
	private Map<String, String> sqlMap;				// Map에 sql주입
    
    public void setSqlMap(Map<String, String> sqlMap) {
    	this.sqlMap = sqlMap;
    }
public void add(User user) {
	this.jdbcTemplate.update(
    	this.sqlMap.get("add"),					// 실제 사용
        user.getId() ...
    )
}

이렇게 Map에 주입한다. 그러나 프로퍼티 타입이 Map이기 때문에 스프링이 제공하는 <map>태그를 사용해야 한다.

맵을 초기화해서 sqlMap프로퍼티에 넣으려면 <map>과 <entry>태그를 프로퍼티 내부에 넣어줘야 한다.

<property name="sqlMap">
	<map>
    	<entry key="add" value="sql문.." />
       	...
    </map>
</property>

 

7.1.2 SQL 제공 서비스

스프링 설정파일 안에 SQL을 두고 이를 DI해서 DAO가 사용하게 하면 손쉽게 SQL을 코드에서 분리해낼 수 있긴 하지만 사용하기엔 몇가지 문제점이 있다.

  • SQL과 DI 설정정보가 섞여 있으면 보기에도 지저분하고 관리하기에도 좋지 않다. 데이터 액세스 로직의 일부인 SQL 문장을 애플리케이션의 설정정보와 함께 두는건 바람직하지 못하다. 따로 빼두는것이 SQL 리뷰나 SQL 튜닝 작업을 하기도 편하다.

 

  • SQL을 꼭 스프링의 빈을 통해 XML에 담아둘 이유도 없다. SQL을 편집하고 관리할 수 있는 툴에서 생성해주는 SQL 정보 파일이 있다면, 그대로 사용할 수 있어야 편할 것이다. XML이 아니라 프로퍼티 파일이나 엑셀 혹은 임의 포맷 파일에서 SQL을 저장해두고 읽어와야 할 수도 있다. 꼭 파일이 아니라도, SQL문을 DB에 담아두고 DB에서 가져올 경우도 있다.웹 서비스나 리모팅을 이용해 최신 SQL 정보를 보관해두는 외부 시스템에서 가져오는 방법도 가능하다.

 

  • 스프링의 애플리케이션은 다시 시작하기 전에는 생성된 오브젝트나 정보가 변경이 어렵다. 주입된 SQL 맵 오브젝트를 직접 변경하는 방법도 있겠다만, 싱글톤인 DAO의 인스턴스 변수에 접근해서 실시간으로 내용을 수정하는 건 간단한 일이 아니다. 운영 중인 애플리케이션에서 빈번하게 참조되는 맵 내용을 수정할 경우 동시성 문제를 일으킬 수도 있다.

 

이런 문제를 해결하고 요구사항을 모두 충족하려면  DAO가 사용할 SQL을 제공해주는 기능을 독립시킬 필요가 있다. 독립적인 SQL 제공 서비스가 필요하다. SQL 제공 기능을 분리해서 다양한 SQL 정보 소스를 사용할 수 있고, 운영 중에 동적으로 갱신도 가능한 유연하고 확장성이 뛰어난 SQL 서비스를 만들어 보자.

 

SQL 서비스 인터페이스

서비스를 분리한다면 독립적으로 인터페이스를 만들어 DI로 구현 클래스의 오브젝트를 주입해야 한다는 사실을 금방 떠올릴 수 있어야 한다.

public interface SqlService {
	String getSql(String key) throws SqlRetrievalFailureException;	// 런타임 예외
}
public class UserDaoJdbc implements UserDao {
	private SqlService sqlService;				// sql서비스 의존성 주입
    
    public void setSqlService(SqlService sqlService) {
    	this.sqlService = sqlService;
    }

이제 모든 메소드에서 인스턴스 변수인 sqlService를 이용해 SQL을 가져오도록 수정한다.

public void add(User user) {
	this.jdbcTemplate.update(
    	this.sqlService.getSql("userAdd"),					// 실제 사용
        user.getId() ...
    )
}

 

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

SqlService인터페이스는 어떤 기술적인 조건이나 제한사항도 담겨 있지 않다. 

DAO가 요구하는 SQL을 돌려주기만 하면 된다.

SqlService를 구현하는 클래스를 만들고 Map으로 추가를 해보자 그리고 맵에서 SQL을 읽어서 돌려주도록 getSql() 메소드를 구현한다.

public class SimpleSqlService implements SqlService {
	private Map<String, String> sqlMap;				// sql정보를 담을 Map
	
	public void setSqlMap(Map<String, String> sqlMap) {
		this.sqlMap = sqlMap;
	}
	
	@Override
	public String getSql(String key) throws SqlRetrievalFailureException {
		String sql = sqlMap.get(key);
		
		if(sql == null) {
			throw new SqlRetrievalFailureException(key + "에 대한 SQL을 찾을 수 없습니다.");
		} else return sql;
	}
}

이제 UserDao를 포함한 모든 DAO는 SQL을 어디에 저장해두고 가져오는지에 대해서는 전혀 신경 쓰지 않아도 된다. 구체적인 구현 방법과 기술에 상관없이 SqlService 인터페이스 타입의 빈을 DI 받아서 필요한 SQL을 가져다 쓰기만 하면 된다.

동시에 sqlService빈에는 DAO에 전혀 영향을 주지 않은 채로 다양한 방법으로 구현된 SqlService 타입 클래스를 적용할 수 있다. 이제 DAO의 수정 없이도 편리하고 자유롭게 SQL서비스 구현 발전시킬수 있을것이다.