[토비의 스프링1] 7.2 인터페이스의 분리와 자기참조 빈

2022. 6. 26. 20:53Spring/개념

7.2.1 XML 파일 매핑

스프링 XML파일에서 <bean>태그 안에 SQL정보를 넣어놓고 활용하는건 좋은 방법이 아니다. 그보다 SQL을 저장해두는 전용포맷을 가진 독립적인 파일을 이용하는 편이 바람직하다. 독립적이라고 해도 가장 편리한 포맷은 역시 XML이니 검색용 키와 SQL 문장 두가지를 담을 수 있는 간단한 XML을 설계해, 파일에서 SQL을 읽어뒀다가 DAO에게 제공해주는 SQL 서비스 구현 클래스를 만들어보자.

 

JAXB

JAXB(Java Architecture for XML Binding)는 자바 클래스를 XML로 표현하는 자바 API이다. JAXB는 주로 2가지 기능이 있다. 자바 객체를 XML로 직렬화하는 것이고 반대로 XML에서 자바 객체로 역직렬화하는 것이다. 즉, JAXB는 메모리의 데이터를 XML 형식으로 변환하여 저장할 수 있고, 이 과정을 위해 프로그램의 각 클래스에서 XML을 읽고 저장하는 일을 구현해야 한다. [위키백과]

 

JAXB 동작방식

 

XML API와 비교했을때, JAXB의 장점은 XML문서정보를 거의 동일한 구조의 오브젝트로 집정 매핑해준다.

DOM은 XML정보를 마치 자바의 리플렉션 API를 사용해 오브젝트를 조작하는 것처럼 간접적으로 접근해야하는 불편이 있다.

 

SQL 맵을 위한 스키마 작성과 컴파일

SQL정보는 키와 SQL의 목록으로 구성된 맵 구조로 만들어두면 편리하다.

<sqlmap>
	<sql key="userAdd"> insert into ... </sql>
    <sql key="userGet"> select * from ... </sql>
</sqlmap>

이 xml문서의 구조를 정하는 스키마를 만들어서, JAXB 컴파일러로 컴파일해보자.

jxc -p springbook.user.sqlservice.jaxb sqlmap.xsd -d src

  • springbook.user.sqlservice.jaxb : 생성할 클래스의 패키지를 지정한다.
  • sqlmap.xsd : 변환할 스키마 파일
  • src : 생성된 파일이 저장될 위치다. 소스 폴더에 추가할거라서 src

실행하고나면 ObjectFactory.java, SqlType.java, Sqlmap.java, package-info.java가 생성된다.

Sqlmap는 <sqlmap>이 바인딩될 클래스다. Sqltype은 <sql>태그의 정보를 담은 클래스다.

 

언마샬링

컴퓨터 과학에서 마셜링(marshalling, l을 하나만 사용하여 marshaling이라고도 표기)이란 한 객체의 메모리에서 표현방식을 저장 또는 전송에 적합한 다른 데이터 형식으로 변환하는 과정이다. 또한 이는 데이터를 컴퓨터 프로그램의 서로 다른 부분 간에 혹은 한 프로그램에서 다른 프로그램으로 이동해야 할 때도 사용된다. 마셜링은 직렬화(serialization)와 유사하며 한 오브젝트(여기서는 직렬화 된 오브젝트)로 멀리 떨어진 오브젝트와 통신하기 위해 사용된다. 이는 복잡한 통신을 단순화 하기 위해, 기본 요소(primitives) 대신 통신을 위한 맞춤형 오브젝트를 사용한다. 마셜링의 반대 개념으로 언마셜링(unmarshalling)이 있다 (디마셜링-demarshalling-이라고도 불리며, 역직렬화-deserialization-와 유사하다). [위키백과]

 

JAXB를 통해 XML을 읽어, 언마샬링을 할 수 있다.

 

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

이제 SqlService에 적용할 차례다.

 

SQL 맵 XML 파일

UserDaoJdbc에서 사용할 SQL이 담긴 XML문서를 만든다.

 

XML SQL 서비스

XML 문서에서 SQL을 가져올 때는, 학습 테스트로 만들어 봤듯이 JAXB API를 사용하면 된다.

그런데 언제 JAXB를 사용해 XML 문서를 가져와야 할까?

일단 생성자에서 JAXB를 이용해 XML로 된 SQL 문서를 읽어들이고, 변환된 SQL오브젝트들을 맵으로 옮겨서 저장해뒀다가, DAO의 요청에 따라 SQL을 찾아서 전달하는 방식으로 SqlService를 구현해보자.

 

7.2.3 빈의 초기화 작업

몇가지 개선점이 눈에 띈다.

  • 생성자에서 예외가 발생할 수도 있는 복잡한 초기화 작업을 다루는 것은 좋지 않다. 

오브젝트를 생성하는 중에 생성자에서 발생하는 예외는 다루기 힘들고, 상속하기 불편하며, 보안에도 문제가 생길 수 있다. 일단 초기상태를 가진 오브젝트를 만들어놓고 별도의 초기화 메소드를 사용하는 방법이 바람직하다.

  • 읽어들일 파일의 위치와 이름이 코드에 고정되어 있다.

SQL을 담은 XML파일의 위치와 이름을 코드에 고정하는 건 별로 좋은 생각이 아니다. 코드의 로직과 여타 이유로 바뀔 가능성이 있는 내용은 외부에서 DI로 설정해줄수 있게 만들어야한다.

 

생성자에서 작업하던 작업을 별도의 초기화 메소드로 옮긴다.

public void loadSql() {
	// 초기화용 코드
}

하지만 loadSql은 언제 실행돼야 할까? 또 어떻게 실행시킬 수 있을까?

다른 오브젝트에 대한 제어권이 우리가 만드는 코드에 있다면, 오브젝트를 만드는 시점에서 loadSql()을 호출해주면 된다.

그러나 오브젝트는 빈이므로 제어권이 스프링에 있다. 생성, 초기화도 스프링에게 맡길 수밖에 없다. 그래서 스프링은 빈 오브젝트를 생성하고 DI작업을 수행해서 프로퍼티를 모두 주입해준 뒤에 미리 지정한 초기화 메소드를 호출해주는 기능을 갖고있다. AOP의 빈 후처리기를 사용해 초기화를 한다.

 

@PostConstruct 어노테이션은 초기화 작업을 수행할 메소드에 부여해주면 클래스로 등록된 빈의 오브젝트를 생성하고 DI작업을 마친 뒤에 어노테이션이 붙은 메소드를 자동으로 실행해준다. 생성자와는 달리 프로퍼티까지 모두 준비된 후에 실행된다는 면에서 @PostConstruct 초기화 메소드는 굉장히 유용하다.

 

public class XmlSqlService implements SqlSErvice {
	@PostConstruct
    public void loadSql() { ... } // loadSql은 프로퍼티까지 실행된 후 실행된다.

PostConstruct의 우선순위

 

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

SQL 서비스 기능에는 아직 확장할 영역이 많이 남아 있다.

XmlSqlService는 특정 포맷의 XML에서 SQL 데이터를 가져오고, 이를 HashMap 타입의 맵 오브젝트에 저장해둔다. SQL을 가져오는 방법에 있어서는 특정 기술에 고정되어 있다. XML 대신 다른 포맷의 파일에서 SQL을 읽어올 경우 SqlService인터페이스를 구현하는 완전히 새로운 클래스를 만들거나 XmlSqlService의 코드를 뜯어고쳐야 한다.

 

책임에 따른 인터페이스 정의

분리 가능한 관심사를 구분해 XmlSqlService 구현을 참고해서 독립적으로 변경 가능한 책임을 뽑을 수 있다.

  • SQL이 담겨 있는 리소스가 어떤 것이든 상관없이 애플리케이션에서 활용 가능하도록 메모리에 읽어들이는 것 (텍스트 파일, XML, 엑셀, DB 등)
  • SQL에 대한 애플리케이션 내의 저장소를 제공하는 것 (키를 이용해 SQL을 검색하는 방법, 성능에 대해서도 고려사항이 있다)
  • 부가적으로 서비스를 위해서 한번 가져온 SQL을 필요에 따라 수정할 수 있게 하는 것 (시스템 운영중에 서버의 재가동이나 애플리케이션 설치 없이 SQL문을 수정하는 기능)

변경 가능한 기능은 전략 패턴을 적용해 별도의 오브젝트로 분리해줘야 한다.

 

책임을 분리한 SqlService 구조

DAO관점에서는 SqlService라는 인터페이스를 구현한 오브젝트에만 의존하고 있으므로 달라질 것은 없다. 대신 SqlService의 구현 클래스가 변경 가능한 책임을 가진 SqlReader와 SqlRegistry 두가지 타입의 오브젝트를 사용하도록 만든다. SqlRegistry의 일부 인터페이스는 SqlService가 아닌 다른 오브젝트가 사용할 수도 있다.

SqlReader는 JAXB에 의존해서는 안된다.

SQL과 키를 쌍으로 Map으로 만들어 바로 SqlReader에 SqlRegistry 전략을 제공해주면서  정보를 등록할 수 있다.

 

public interface SqlRegistry {
	void registerSql(String key, String sql);
    String findSql(String key) throws ...;
}
public interface SqlReader{
	void read(SqlRegistry sqlRegistry);
}

이렇게 SqlReader에 전략을 제공한 코드를 만들어 데이터를 두번 옮길일 없이 사용할 수 있다.

 

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

다중 인터페이스 구현과 참조

내일 다시